Skip to content

feat(dash-spv-bench): multi-scenario sync benchmark framework#908

Open
ZocoLini wants to merge 1 commit into
devfrom
perf/spv-bench
Open

feat(dash-spv-bench): multi-scenario sync benchmark framework#908
ZocoLini wants to merge 1 commit into
devfrom
perf/spv-bench

Conversation

@ZocoLini

@ZocoLini ZocoLini commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

A docker-driven benchmark that syncs the SPV client against a frozen chain snapshot (local peers via tc netem) or the real testnet/mainnet, reporting sync time and a live dashboard.

Summary by CodeRabbit

  • New Features
    • Added a Dash SPV benchmarking tool covering local, testnet, and mainnet scenarios with repeatable “scenario matrix” runs.
    • Introduced a live terminal dashboard and per-run metrics (milestone timings, completion status, error reporting), plus automated TSV/Markdown reports.
    • Added benchmarking support assets including a Docker-based dashd setup and chain snapshot automation.
  • Documentation
    • Added a scenario template and multiple ready-to-run benchmark scenario definitions.
  • Chores / CI
    • Included the benchmarking crate in the workspace and updated CI group settings to exclude its benchmark binary from test-group assignment.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a new dash-spv-bench workspace crate with scenario-driven local and remote SPV benchmarking, Docker peer orchestration, profiling, progress dashboards, synchronization metrics, wallet reporting, and aggregated benchmark reports.

Changes

SPV benchmark harness

Layer / File(s) Summary
Workspace and benchmark packaging
Cargo.toml, .github/ci-groups.yml, dash-spv-bench/Cargo.toml, dash-spv-bench/.gitignore, dash-spv-bench/Dockerfile
Registers the benchmark crate, defines dependencies and the binary, excludes generated artifacts from version control and CI grouping, and packages the Dash Core runtime image.
Scenario execution and peer orchestration
dash-spv-bench/run.sh, dash-spv-bench/bench-scenarios.sh, dash-spv-bench/snapshot-chain.sh, dash-spv-bench/scenario.example.yml, dash-spv-bench/scenarios/*
Adds scenario definitions and scripts for local or remote runs, Docker peer setup, network conditions, chain snapshots, CPU pinning, profiling, repeated execution, and report aggregation.
Synchronization metrics and dashboard
dash-spv-bench/src/dashboard.rs, dash-spv-bench/src/metrics.rs
Adds terminal progress bars, coordinated log output, milestone timing, completion notification, error capture, and formatted synchronization metrics.
Benchmark client lifecycle and wallet report
dash-spv-bench/src/main.rs
Configures and runs DashSpvClient, creates storage and wallets, handles shutdown and timeouts, and writes synchronization and wallet summaries.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Scenario as Scenario YAML
  participant Runner as run.sh
  participant Peers as Docker dashd peers
  participant Client as dash-spv-bench
  participant Metrics as BenchEventHandler
  participant Report as bench-scenarios.sh
  Scenario->>Runner: load benchmark configuration
  Runner->>Peers: create local peers and chain snapshot
  Runner->>Client: pass benchmark environment
  Client->>Metrics: emit synchronization events
  Metrics-->>Client: signal completion or timeout
  Client-->>Runner: write metrics and summary
  Report->>Runner: repeat scenarios and aggregate results
Loading

Suggested labels: ready-for-review

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the new multi-scenario Dash SPV benchmark framework.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/spv-bench

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ZocoLini
ZocoLini marked this pull request as ready for review July 20, 2026 22:20

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
dash-spv-bench/scenario.example.yml (1)

7-9: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider documenting the repeats field.

The repeats field is used in several scenario files (e.g., congested-2core.yml, ideal-1-peer.yml) to define the number of times a scenario should be run in a batch. Consider adding it to this example for completeness.

♻️ Proposed fix
 cpus: "0-3"            # optional. taskset CPU list pinning the MEASURED client (here cores 0,1,2,3).
                        # In local mode the docker peers get every OTHER core. Omit => no pinning.
 
+repeats: 1             # optional. Number of times to run this scenario in batch mode. Omit => 1.
+
 max_peers: 3           # optional. Max simultaneous peers. Omit => the ClientConfig default.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dash-spv-bench/scenario.example.yml` around lines 7 - 9, Add the repeats
field to scenario.example.yml alongside the other scenario configuration fields,
with a concise comment explaining that it controls how many times the scenario
runs in a batch and documenting its default behavior if applicable.
dash-spv-bench/src/metrics.rs (1)

17-52: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding unit tests for the non-trivial metric logic.

record, snapshot, and the Display impl for RunMetrics contain real branching/formatting logic (milestone dedup, fallback to elapsed time, TIMED OUT suffix) with no tests in this file. As per coding guidelines, write unit tests for new functionality.

Also applies to: 81-109

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dash-spv-bench/src/metrics.rs` around lines 17 - 52, The metric logic lacks
unit coverage for branching and formatting behavior. Add focused unit tests for
RunMetrics::record, RunMetrics::snapshot, and its Display implementation,
covering milestone deduplication, elapsed-time fallback, completed versus
timed-out output, and error formatting; keep tests scoped to this module and use
existing APIs.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@dash-spv-bench/run.sh`:
- Around line 310-318: Update the perf profiling branch in dash-spv-bench/run.sh
at lines 310-318 to create a unique temporary data-file path with mktemp, pass
that path to perf record and perf script, and remove it after processing. Apply
the same change at lines 319-329 for the sample file: generate its path with
mktemp, use it throughout the profiling pipeline, and delete it when complete.

In `@dash-spv-bench/src/main.rs`:
- Around line 111-112: Make the benchmark timeout configurable through an
environment variable, matching the existing run-parameter pattern in main and
preserving 1800 seconds as the default when unset or invalid. Update the timeout
initialization next to load_mnemonics and use the resulting Duration wherever
the benchmark enforces its timeout.
- Around line 169-172: Preserve the result of run_client.run().await in the
spawned task and propagate or record its error so the surrounding benchmark flow
can distinguish an early client failure from the 30-minute timeout. Update the
run_handle handling to inspect the task’s returned result and report the client
error while retaining the existing timeout behavior.
- Around line 74-78: Update the tracing subscriber file writer in the fmt layer
to use a shared Mutex<File> implementing MakeWriter instead of calling
log_file.try_clone() for each event. Remove the per-event expect-based
descriptor cloning while preserving the existing file_filter and non-ANSI
formatting configuration.

In `@dash-spv-bench/src/metrics.rs`:
- Around line 87-92: Update Metrics::wait_done so it creates and snapshots the
self.done.notified() future before checking inner.completed, then retain the
early return when completed is true and await the captured notification
otherwise. This ordering prevents the SyncComplete notification from being lost
between the condition check and await.

---

Nitpick comments:
In `@dash-spv-bench/scenario.example.yml`:
- Around line 7-9: Add the repeats field to scenario.example.yml alongside the
other scenario configuration fields, with a concise comment explaining that it
controls how many times the scenario runs in a batch and documenting its default
behavior if applicable.

In `@dash-spv-bench/src/metrics.rs`:
- Around line 17-52: The metric logic lacks unit coverage for branching and
formatting behavior. Add focused unit tests for RunMetrics::record,
RunMetrics::snapshot, and its Display implementation, covering milestone
deduplication, elapsed-time fallback, completed versus timed-out output, and
error formatting; keep tests scoped to this module and use existing APIs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: db1b5739-2c02-49a2-a598-799e065c5ff2

📥 Commits

Reviewing files that changed from the base of the PR and between 19690d3 and 645de61.

📒 Files selected for processing (23)
  • .github/ci-groups.yml
  • Cargo.toml
  • dash-spv-bench/.gitignore
  • dash-spv-bench/Cargo.toml
  • dash-spv-bench/Dockerfile
  • dash-spv-bench/bench-scenarios.sh
  • dash-spv-bench/run.sh
  • dash-spv-bench/scenario.example.yml
  • dash-spv-bench/scenarios/congested-2core.yml
  • dash-spv-bench/scenarios/ideal-1-peer.yml
  • dash-spv-bench/scenarios/ideal-3-peers.yml
  • dash-spv-bench/scenarios/ideal-5-peers.yml
  • dash-spv-bench/scenarios/no-lag-ideal-1-peer.yml
  • dash-spv-bench/scenarios/no-lag-ideal-3-peers.yml
  • dash-spv-bench/scenarios/no-lag-ideal-5-peers.yml
  • dash-spv-bench/scenarios/one-bad-peer.yml
  • dash-spv-bench/scenarios/real-mainnet.yml
  • dash-spv-bench/scenarios/real-testnet.yml
  • dash-spv-bench/scenarios/slow-many-peers.yml
  • dash-spv-bench/snapshot-chain.sh
  • dash-spv-bench/src/dashboard.rs
  • dash-spv-bench/src/main.rs
  • dash-spv-bench/src/metrics.rs

Comment thread dash-spv-bench/run.sh
Comment thread dash-spv-bench/src/main.rs
Comment thread dash-spv-bench/src/main.rs
Comment on lines +169 to +172
let run_client = client.clone();
let run_handle = tokio::spawn(async move {
let _ = run_client.run().await;
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Silently discarded client run() error hides real failures.

let _ = run_client.run().await; drops any error the client returns. Combined with the fixed 30-minute timeout, an early client failure (e.g. connection/storage error) would be indistinguishable from a slow sync — both surface only as completed: false (TIMED OUT) in the report, making failures hard to diagnose.

🐛 Suggested fix
     let run_handle = tokio::spawn(async move {
-        let _ = run_client.run().await;
+        if let Err(e) = run_client.run().await {
+            tracing::error!("client run failed: {e}");
+        }
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let run_client = client.clone();
let run_handle = tokio::spawn(async move {
let _ = run_client.run().await;
});
let run_client = client.clone();
let run_handle = tokio::spawn(async move {
if let Err(e) = run_client.run().await {
tracing::error!("client run failed: {e}");
}
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dash-spv-bench/src/main.rs` around lines 169 - 172, Preserve the result of
run_client.run().await in the spawned task and propagate or record its error so
the surrounding benchmark flow can distinguish an early client failure from the
30-minute timeout. Update the run_handle handling to inspect the task’s returned
result and report the client error while retaining the existing timeout
behavior.

Comment thread dash-spv-bench/src/metrics.rs
@codecov

codecov Bot commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 74.57%. Comparing base (19690d3) to head (fd614fc).

Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #908      +/-   ##
==========================================
+ Coverage   74.54%   74.57%   +0.02%     
==========================================
  Files         327      327              
  Lines       75032    75032              
==========================================
+ Hits        55936    55954      +18     
+ Misses      19096    19078      -18     
Flag Coverage Δ
core 77.29% <ø> (ø)
ffi 51.12% <ø> (+0.17%) ⬆️
rpc 20.00% <ø> (ø)
spv 91.21% <ø> (ø)
wallet 74.41% <ø> (ø)
see 15 files with indirect coverage changes

A docker-driven benchmark that syncs the SPV client against a frozen chain
snapshot (local peers via tc netem) or the real testnet/mainnet, reporting
sync time and a live dashboard. Base branch for the network-rewrite PR stacked
on top; it drives the rewritten client and does not build on dev alone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CUb3bkX9C1gBFA65GFsN53

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
dash-spv-bench/Dockerfile (1)

18-21: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Verify integrity of externally-fetched binaries before use. Both sites curl a release binary from GitHub and use it immediately with no checksum or signature check, even though Dash Core publishes a signed SHA256SUMS.asc for its releases.

  • dash-spv-bench/Dockerfile#L18-L21: verify the downloaded dashcore-${DASHVERSION}-${dlarch}-linux-gnu.tar.gz against Dash Core's published SHA256SUMS/SHA256SUMS.asc before extracting and copying dashd into the image.
  • dash-spv-bench/run.sh#L63-L66: verify the downloaded yq_${os}_${arch} binary's checksum (mikefarah/yq publishes checksums/attestations per release) before chmod +x and using it to parse scenario files.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dash-spv-bench/Dockerfile` around lines 18 - 21, Verify externally downloaded
binaries before use: in dash-spv-bench/Dockerfile lines 18-21, download Dash
Core’s published SHA256SUMS/SHA256SUMS.asc, validate the selected dashcore
archive, and only then extract and copy dashd; in dash-spv-bench/run.sh lines
63-66, validate the downloaded yq_${os}_${arch} binary against mikefarah/yq’s
release checksum or attestation before chmod +x or using it.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@dash-spv-bench/run.sh`:
- Around line 265-279: Update bring_up’s peer-start loop to health-check any
trailing partial batch after iteration completes: when started is non-empty and
count is not divisible by batch, call wait_loaded for the remaining services and
fail with the same timeout behavior if it does not become ready. Preserve the
existing full-batch checks and startup reporting.
- Around line 148-172: Separate command substitution from export for
BENCH_MAX_PEERS and BENCH_PEERS: assign each value first, then export the
variable. Preserve their existing scn expressions and ensure a failing scn
invocation remains visible to set -euo pipefail instead of being masked by
export.

In `@dash-spv-bench/snapshot-chain.sh`:
- Line 7: Update the snapshot-chain.sh image configuration to use an immutable
Dash Core version or digest instead of the latest tag. Adjust the surrounding
image reuse logic so it rebuilds or pulls when the Dockerfile changes, rather
than skipping whenever any local image exists, ensuring benchmark runs use the
intended image consistently.
- Line 11: Validate BENCH_HEIGHT as a non-negative integer immediately after its
required-variable check and before any Docker or directory setup in
snapshot-chain.sh; reject malformed or negative values with a clear error and
exit, while allowing valid integer heights to reach -stopatheight.
- Around line 21-26: After the docker run in snapshot-chain.sh, query the
generated chain’s actual tip height and validate it against the scenario’s
target-height contract represented by BENCH_HEIGHT. Fail the script when the tip
is missing or does not match the required height, and print the success message
only after validation passes.

---

Nitpick comments:
In `@dash-spv-bench/Dockerfile`:
- Around line 18-21: Verify externally downloaded binaries before use: in
dash-spv-bench/Dockerfile lines 18-21, download Dash Core’s published
SHA256SUMS/SHA256SUMS.asc, validate the selected dashcore archive, and only then
extract and copy dashd; in dash-spv-bench/run.sh lines 63-66, validate the
downloaded yq_${os}_${arch} binary against mikefarah/yq’s release checksum or
attestation before chmod +x or using it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 376b86db-aa4b-4506-b67a-a55eed273a7a

📥 Commits

Reviewing files that changed from the base of the PR and between 645de61 and fd614fc.

📒 Files selected for processing (23)
  • .github/ci-groups.yml
  • Cargo.toml
  • dash-spv-bench/.gitignore
  • dash-spv-bench/Cargo.toml
  • dash-spv-bench/Dockerfile
  • dash-spv-bench/bench-scenarios.sh
  • dash-spv-bench/run.sh
  • dash-spv-bench/scenario.example.yml
  • dash-spv-bench/scenarios/congested-2core.yml
  • dash-spv-bench/scenarios/ideal-1-peer.yml
  • dash-spv-bench/scenarios/ideal-3-peers.yml
  • dash-spv-bench/scenarios/ideal-5-peers.yml
  • dash-spv-bench/scenarios/no-lag-ideal-1-peer.yml
  • dash-spv-bench/scenarios/no-lag-ideal-3-peers.yml
  • dash-spv-bench/scenarios/no-lag-ideal-5-peers.yml
  • dash-spv-bench/scenarios/one-bad-peer.yml
  • dash-spv-bench/scenarios/real-mainnet.yml
  • dash-spv-bench/scenarios/real-testnet.yml
  • dash-spv-bench/scenarios/slow-many-peers.yml
  • dash-spv-bench/snapshot-chain.sh
  • dash-spv-bench/src/dashboard.rs
  • dash-spv-bench/src/main.rs
  • dash-spv-bench/src/metrics.rs
🚧 Files skipped from review as they are similar to previous changes (19)
  • dash-spv-bench/scenarios/no-lag-ideal-1-peer.yml
  • dash-spv-bench/scenarios/slow-many-peers.yml
  • dash-spv-bench/Cargo.toml
  • .github/ci-groups.yml
  • dash-spv-bench/scenarios/no-lag-ideal-3-peers.yml
  • dash-spv-bench/scenarios/real-mainnet.yml
  • dash-spv-bench/.gitignore
  • Cargo.toml
  • dash-spv-bench/scenarios/congested-2core.yml
  • dash-spv-bench/scenarios/no-lag-ideal-5-peers.yml
  • dash-spv-bench/scenarios/ideal-1-peer.yml
  • dash-spv-bench/scenarios/ideal-5-peers.yml
  • dash-spv-bench/scenario.example.yml
  • dash-spv-bench/scenarios/ideal-3-peers.yml
  • dash-spv-bench/scenarios/one-bad-peer.yml
  • dash-spv-bench/scenarios/real-testnet.yml
  • dash-spv-bench/src/metrics.rs
  • dash-spv-bench/src/dashboard.rs
  • dash-spv-bench/src/main.rs

Comment thread dash-spv-bench/run.sh
Comment on lines +148 to +172
MODE="$(scn '.mode // "local"')"
case "${MODE}" in local | testnet | mainnet) ;; *) echo "Error: mode must be 'local', 'testnet' or 'mainnet' (got '${MODE}')" >&2; exit 1 ;; esac
export BENCH_MODE="${MODE}"
export CLONE_DIR="${CLONE_DIR:-/nonexistent}"

BENCH_CPUS="$(scn '.cpus // ""')"
export BENCH_MAX_PEERS="$(scn '.max_peers // ""')" # only if set; else the ClientConfig default

BENCH_WALLET_FILE="${SCRIPT_DIR}/wallets.txt"
[ -n "${WALLETS_ARG}" ] && BENCH_WALLET_FILE="$(abspath "${WALLETS_ARG}")"
export BENCH_WALLET_FILE
export BENCH_STORAGE_DIR="${SCRIPT_DIR}/bench-storage"

DESC="$(scn '.description // ""')"
[ -n "${DESC}" ] && echo "==> description: ${DESC}"

BLOCKS=""
if [ "${MODE}" = local ]; then
BLOCKS="$(scn '.blocks // 1000000')"
export BENCH_HEIGHT="${BLOCKS}"
unset BENCH_START_HEIGHT # local always syncs from genesis
else
export BENCH_PEERS="$(scn '.peers // [] | join(",")')"
sh="$(scn '.start_height // ""')"; [ -n "${sh}" ] && export BENCH_START_HEIGHT="${sh}"
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

export VAR="$(cmd)" masks command-substitution failures under set -e.

Lines 154 and 170 combine export with a command substitution (export BENCH_MAX_PEERS="$(scn ...)", export BENCH_PEERS="$(scn ...)"). Per shellcheck SC2155, the exit status reported for such a statement is that of export/local, not the substituted command, so a failing yq invocation (e.g., malformed scenario YAML) would silently produce an empty value instead of aborting via set -euo pipefail.

🔧 Proposed fix: declare and assign separately
-export BENCH_MAX_PEERS="$(scn '.max_peers // ""')"   # only if set; else the ClientConfig default
+BENCH_MAX_PEERS="$(scn '.max_peers // ""')"   # only if set; else the ClientConfig default
+export BENCH_MAX_PEERS
-  export BENCH_PEERS="$(scn '.peers // [] | join(",")')"
+  BENCH_PEERS="$(scn '.peers // [] | join(",")')"
+  export BENCH_PEERS
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
MODE="$(scn '.mode // "local"')"
case "${MODE}" in local | testnet | mainnet) ;; *) echo "Error: mode must be 'local', 'testnet' or 'mainnet' (got '${MODE}')" >&2; exit 1 ;; esac
export BENCH_MODE="${MODE}"
export CLONE_DIR="${CLONE_DIR:-/nonexistent}"
BENCH_CPUS="$(scn '.cpus // ""')"
export BENCH_MAX_PEERS="$(scn '.max_peers // ""')" # only if set; else the ClientConfig default
BENCH_WALLET_FILE="${SCRIPT_DIR}/wallets.txt"
[ -n "${WALLETS_ARG}" ] && BENCH_WALLET_FILE="$(abspath "${WALLETS_ARG}")"
export BENCH_WALLET_FILE
export BENCH_STORAGE_DIR="${SCRIPT_DIR}/bench-storage"
DESC="$(scn '.description // ""')"
[ -n "${DESC}" ] && echo "==> description: ${DESC}"
BLOCKS=""
if [ "${MODE}" = local ]; then
BLOCKS="$(scn '.blocks // 1000000')"
export BENCH_HEIGHT="${BLOCKS}"
unset BENCH_START_HEIGHT # local always syncs from genesis
else
export BENCH_PEERS="$(scn '.peers // [] | join(",")')"
sh="$(scn '.start_height // ""')"; [ -n "${sh}" ] && export BENCH_START_HEIGHT="${sh}"
fi
MODE="$(scn '.mode // "local"')"
case "${MODE}" in local | testnet | mainnet) ;; *) echo "Error: mode must be 'local', 'testnet' or 'mainnet' (got '${MODE}')" >&2; exit 1 ;; esac
export BENCH_MODE="${MODE}"
export CLONE_DIR="${CLONE_DIR:-/nonexistent}"
BENCH_CPUS="$(scn '.cpus // ""')"
BENCH_MAX_PEERS="$(scn '.max_peers // ""')" # only if set; else the ClientConfig default
export BENCH_MAX_PEERS
BENCH_WALLET_FILE="${SCRIPT_DIR}/wallets.txt"
[ -n "${WALLETS_ARG}" ] && BENCH_WALLET_FILE="$(abspath "${WALLETS_ARG}")"
export BENCH_WALLET_FILE
export BENCH_STORAGE_DIR="${SCRIPT_DIR}/bench-storage"
DESC="$(scn '.description // ""')"
[ -n "${DESC}" ] && echo "==> description: ${DESC}"
BLOCKS=""
if [ "${MODE}" = local ]; then
BLOCKS="$(scn '.blocks // 1000000')"
export BENCH_HEIGHT="${BLOCKS}"
unset BENCH_START_HEIGHT # local always syncs from genesis
else
BENCH_PEERS="$(scn '.peers // [] | join(",")')"
export BENCH_PEERS
sh="$(scn '.start_height // ""')"; [ -n "${sh}" ] && export BENCH_START_HEIGHT="${sh}"
fi
🧰 Tools
🪛 Shellcheck (0.11.0)

[warning] 154-154: Declare and assign separately to avoid masking return values.

(SC2155)


[warning] 170-170: Declare and assign separately to avoid masking return values.

(SC2155)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dash-spv-bench/run.sh` around lines 148 - 172, Separate command substitution
from export for BENCH_MAX_PEERS and BENCH_PEERS: assign each value first, then
export the variable. Preserve their existing scn expressions and ensure a
failing scn invocation remains visible to set -euo pipefail instead of being
masked by export.

Source: Linters/SAST tools

Comment thread dash-spv-bench/run.sh
Comment on lines +265 to +279
local batch=4
echo "==> starting ${n} peers in batches of ${batch}"
local started="" count=0
for svc in ${services}; do
CLONE_DIR="${clone_dir}" compose up -d "${svc}" >/dev/null 2>&1
started="${started} spv-bench-${svc}"; count=$((count + 1))
if [ $((count % batch)) -eq 0 ]; then
echo -n " loaded ${count}/${n} "
wait_loaded ${started} || { echo " timeout loading batch" >&2; exit 1; }
echo " ok"
fi
done

echo "==> ${n} peers started"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Trailing partial batch of peers is never health-checked before bring_up() returns.

wait_loaded is only invoked when count % batch == 0 (Line 271). If n (total peers) isn't a multiple of batch, the last group of started containers is never waited on — bring_up() returns and the sync benchmark begins while those peers may still be loading. Since the entire point of this harness is accurate timing, starting the measured sync before all peers are actually ready to serve data would skew results.

🐛 Proposed fix: wait for stragglers after the loop
   local batch=4
   echo "==> starting ${n} peers in batches of ${batch}"
   local started="" count=0
   for svc in ${services}; do
     CLONE_DIR="${clone_dir}" compose up -d "${svc}" >/dev/null 2>&1
     started="${started} spv-bench-${svc}"; count=$((count + 1))
     if [ $((count % batch)) -eq 0 ]; then
       echo -n "  loaded ${count}/${n} "
       wait_loaded ${started} || { echo " timeout loading batch" >&2; exit 1; }
       echo " ok"
     fi
   done
+  if [ $((count % batch)) -ne 0 ]; then
+    echo -n "  loaded ${n}/${n} "
+    wait_loaded ${started} || { echo " timeout loading batch" >&2; exit 1; }
+    echo " ok"
+  fi

   echo "==> ${n} peers started"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
local batch=4
echo "==> starting ${n} peers in batches of ${batch}"
local started="" count=0
for svc in ${services}; do
CLONE_DIR="${clone_dir}" compose up -d "${svc}" >/dev/null 2>&1
started="${started} spv-bench-${svc}"; count=$((count + 1))
if [ $((count % batch)) -eq 0 ]; then
echo -n " loaded ${count}/${n} "
wait_loaded ${started} || { echo " timeout loading batch" >&2; exit 1; }
echo " ok"
fi
done
echo "==> ${n} peers started"
}
local batch=4
echo "==> starting ${n} peers in batches of ${batch}"
local started="" count=0
for svc in ${services}; do
CLONE_DIR="${clone_dir}" compose up -d "${svc}" >/dev/null 2>&1
started="${started} spv-bench-${svc}"; count=$((count + 1))
if [ $((count % batch)) -eq 0 ]; then
echo -n " loaded ${count}/${n} "
wait_loaded ${started} || { echo " timeout loading batch" >&2; exit 1; }
echo " ok"
fi
done
if [ $((count % batch)) -ne 0 ]; then
echo -n " loaded ${n}/${n} "
wait_loaded ${started} || { echo " timeout loading batch" >&2; exit 1; }
echo " ok"
fi
echo "==> ${n} peers started"
}
🧰 Tools
🪛 Shellcheck (0.11.0)

[info] 273-273: Double quote to prevent globbing and word splitting.

(SC2086)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dash-spv-bench/run.sh` around lines 265 - 279, Update bring_up’s peer-start
loop to health-check any trailing partial batch after iteration completes: when
started is non-empty and count is not divisible by batch, call wait_loaded for
the remaining services and fail with the same timeout behavior if it does not
become ready. Preserve the existing full-batch checks and startup reporting.

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CHAIN_DIR="${SCRIPT_DIR}/chain-data"
SUBDIR="testnet3"
IMAGE="dash-spv-bench/dashd:latest"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Pin the Dash Core image for reproducible benchmarks.

Using latest and skipping the build whenever any local image exists can silently reuse a stale or different Dash Core image. Pin an immutable version/digest and rebuild or pull when the Dockerfile changes; otherwise benchmark results are not comparable across runs or machines.

Also applies to: 14-15

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dash-spv-bench/snapshot-chain.sh` at line 7, Update the snapshot-chain.sh
image configuration to use an immutable Dash Core version or digest instead of
the latest tag. Adjust the surrounding image reuse logic so it rebuilds or pulls
when the Dockerfile changes, rather than skipping whenever any local image
exists, ensuring benchmark runs use the intended image consistently.


cd "${SCRIPT_DIR}"

: "${BENCH_HEIGHT:?BENCH_HEIGHT must be set — run.sh exports it from the scenario blocks}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate BENCH_HEIGHT before invoking Docker.

The presence check accepts malformed values, which are passed to -stopatheight and fail only after image and directory setup. Reject non-integer or negative values before performing side effects.

Also applies to: 21-24

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dash-spv-bench/snapshot-chain.sh` at line 11, Validate BENCH_HEIGHT as a
non-negative integer immediately after its required-variable check and before
any Docker or directory setup in snapshot-chain.sh; reject malformed or negative
values with a clear error and exit, while allowing valid integer heights to
reach -stopatheight.

Comment on lines +21 to +26
docker run --rm --user "$(id -u):$(id -g)" -v "${CHAIN_DIR}:/data" "${IMAGE}" \
dashd -testnet -datadir=/data -daemon=0 -server=1 \
-blockfilterindex=1 -peerblockfilters=1 -stopatheight="${BENCH_HEIGHT}" \
-txindex=0 -prune=0 -disablewallet=1

echo "==> done. Snapshot is at ${CHAIN_DIR}/${SUBDIR} (height ${BENCH_HEIGHT})."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Verify the generated snapshot height before reporting success.

The script prints that the snapshot is at BENCH_HEIGHT without checking the actual chain tip. Reused or partially existing chain-data can make this message inaccurate; query the resulting tip and fail if it does not satisfy the scenario’s target-height contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dash-spv-bench/snapshot-chain.sh` around lines 21 - 26, After the docker run
in snapshot-chain.sh, query the generated chain’s actual tip height and validate
it against the scenario’s target-height contract represented by BENCH_HEIGHT.
Fail the script when the tip is missing or does not match the required height,
and print the success message only after validation passes.

@ZocoLini ZocoLini changed the title feat(dash-spv-bench): single-scenario sync benchmark harness feat(dash-spv-bench): multi-scenario sync benchmark framework Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant