From c0ca6d587d1c4d2cb50fd7e39df0339ac8ec1b0a Mon Sep 17 00:00:00 2001 From: olathedev Date: Sun, 23 Aug 2026 05:51:13 +0100 Subject: [PATCH 1/4] chore: add shared load-test helper lib and lint it in CI The two testnet load tests each carry their own copy of the same logging, timing and invocation helpers. Add the shared home for them first, before either script is migrated, so CI is checking the new file from the commit that introduces it. `shellcheck scripts/*.sh` does not glob into scripts/lib/, so the lint step now lists that directory explicitly. It also gains -x: without it shellcheck refuses to follow a `source` line and reports SC1091, which is an error under the default severity; with it, each script is checked against the helper definitions it actually gets at runtime rather than the two halves being analysed in isolation. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 7 ++- scripts/lib/load-test-common.sh | 93 +++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 scripts/lib/load-test-common.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cda2834..225f468 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,8 +61,13 @@ jobs: - name: Check formatting run: cargo fmt --check + # scripts/*.sh doesn't cover the shared helpers the load tests source + # from scripts/lib/, so that directory is listed explicitly; -x makes + # shellcheck follow the `source` lines and check each script against + # the helpers it actually gets, instead of reporting SC1091 and + # analysing the two halves in isolation. - name: Lint shell scripts - run: shellcheck scripts/*.sh + run: shellcheck -x scripts/*.sh scripts/lib/*.sh # demo-consumer imports tholos's compiled wasm at compile time # (contractimport!), so it must exist before anything below this diff --git a/scripts/lib/load-test-common.sh b/scripts/lib/load-test-common.sh new file mode 100644 index 0000000..49a6c6d --- /dev/null +++ b/scripts/lib/load-test-common.sh @@ -0,0 +1,93 @@ +# shellcheck shell=bash +# Helpers shared by the testnet load tests (scripts/testnet-load.sh and +# scripts/testnet-load-v2.sh). Sourced, never executed, so it deliberately +# has no shebang and no `set -euo pipefail` of its own: sourcing runs in the +# caller's shell, which already sets those, and re-setting them here would +# silently re-enable them for any future caller that had turned one off. +# +# Two variables must be set by the sourcing script *before* the source line: +# NETWORK - the Stellar network passed to every CLI call. +# DEPLOYER_IDENTITY - a funded `stellar keys` identity name, used as the +# source for the read-only balance() query. +# Everything else here is self-contained. + +# The installed stellar CLI. +STELLAR="stellar" + +log() { + echo -e "\033[1;34m>>\033[0m $*" +} + +log_success() { + echo -e "\033[1;32m✓\033[0m $*" +} + +log_error() { + echo -e "\033[1;31m✗\033[0m $*" +} + +get_time() { + date +%s.%N 2>/dev/null || date +%s +} + +elapsed_time() { + local start=$1 + local end=$2 + if command -v awk >/dev/null 2>&1; then + awk -v s="$start" -v e="$end" 'BEGIN { printf "%.2f", e - s }' + else + local diff=$(( ${end%.*} - ${start%.*} )) + echo "$diff" + fi +} + +# Averages its arguments, formatted to 2dp; "0.00" for an empty list, so a +# phase that recorded no timings doesn't divide by zero. +avg_time() { + local sum=0 + local count=${#@} + if [ "$count" -eq 0 ]; then + echo "0.00" + return + fi + for val in "$@"; do + sum=$(awk -v s="$sum" -v v="$val" 'BEGIN { print s + v }') + done + awk -v s="$sum" -v c="$count" 'BEGIN { printf "%.2f", s / c }' +} + +gen_key() { + local name=$1 + $STELLAR keys generate "$name" --network "$NETWORK" --fund --overwrite >/dev/null + $STELLAR keys address "$name" +} + +# Reads an address's balance in a token contract. The invoke is a read-only +# simulation, so DEPLOYER_IDENTITY is just a funded identity to source it +# from; nothing is charged to or mutated on it. +balance() { + local token=$1 + local addr=$2 + $STELLAR contract invoke --id "$token" --source "$DEPLOYER_IDENTITY" --network "$NETWORK" -- balance --id "$addr" 2>/dev/null \ + | tr -d '"' +} + +# Wrapper to execute contract calls, capturing stdout/stderr for robust error reporting +invoke_contract() { + local source=$1 + shift + local tmp_out + tmp_out=$(mktemp) + local tmp_err + tmp_err=$(mktemp) + + if ! $STELLAR contract invoke --source "$source" --network "$NETWORK" "$@" >"$tmp_out" 2>"$tmp_err"; then + log_error "Invocation failed!" + cat "$tmp_err" >&2 + rm -f "$tmp_out" "$tmp_err" + return 1 + fi + + tail -1 "$tmp_out" + rm -f "$tmp_out" "$tmp_err" +} From 295acedac52af84fcfd20b978490789e9097b14e Mon Sep 17 00:00:00 2001 From: olathedev Date: Sun, 23 Aug 2026 05:51:21 +0100 Subject: [PATCH 2/4] chore: source the shared helpers in testnet-load.sh Drops this script's copies of log/log_success/log_error, get_time/ elapsed_time/avg_time, gen_key, balance and invoke_contract in favour of scripts/lib/load-test-common.sh. Every one of them is byte-identical to the shared version once parsed, so behaviour and output are unchanged. balance() was the single exception: it hardcoded `--source load_deployer`, which the v2 script spells `v2load_deployer`. The identity name now lives in DEPLOYER_IDENTITY, set once in the configuration block and used at the script's other deployer call sites too, so the shared helper reads it instead of a literal and the name still has exactly one definition. Phase logic, the contract-specific parameters and the resolver-committee setup all stay here; only the genuinely duplicated helpers moved. Co-Authored-By: Claude Opus 5 --- scripts/testnet-load.sh | 94 ++++++----------------------------------- 1 file changed, 12 insertions(+), 82 deletions(-) diff --git a/scripts/testnet-load.sh b/scripts/testnet-load.sh index c88d526..6ddb989 100755 --- a/scripts/testnet-load.sh +++ b/scripts/testnet-load.sh @@ -9,6 +9,13 @@ CONTRACT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" WASM_PATH="$CONTRACT_DIR/target/wasm32v1-none/release/tholos.wasm" BOND_AMOUNT=1000000 CHALLENGE_WINDOW_SECS=120 # Short challenge window for quick test execution +# Funded identity this script deploys and reads balances from. +DEPLOYER_IDENTITY=load_deployer + +# Logging, timing and invocation helpers shared with testnet-load-v2.sh. +# They read NETWORK and DEPLOYER_IDENTITY, so this must come after them. +# shellcheck source=SCRIPTDIR/lib/load-test-common.sh +source "$CONTRACT_DIR/scripts/lib/load-test-common.sh" # Input parameters N=${1:-5} @@ -20,69 +27,6 @@ if [ "$D" -gt "$N" ]; then D=$N fi -log() { - echo -e "\033[1;34m>>\033[0m $*" -} - -log_success() { - echo -e "\033[1;32m✓\033[0m $*" -} - -log_error() { - echo -e "\033[1;31m✗\033[0m $*" -} - -get_time() { - date +%s.%N 2>/dev/null || date +%s -} - -elapsed_time() { - local start=$1 - local end=$2 - if command -v awk >/dev/null 2>&1; then - awk -v s="$start" -v e="$end" 'BEGIN { printf "%.2f", e - s }' - else - local diff=$(( ${end%.*} - ${start%.*} )) - echo "$diff" - fi -} - -# Use the installed stellar CLI -STELLAR="stellar" - -gen_key() { - local name=$1 - $STELLAR keys generate "$name" --network "$NETWORK" --fund --overwrite >/dev/null - $STELLAR keys address "$name" -} - -balance() { - local token=$1 - local addr=$2 - $STELLAR contract invoke --id "$token" --source load_deployer --network "$NETWORK" -- balance --id "$addr" 2>/dev/null \ - | tr -d '"' -} - -# Wrapper to execute contract calls, capturing stdout/stderr for robust error reporting -invoke_contract() { - local source=$1 - shift - local tmp_out - tmp_out=$(mktemp) - local tmp_err - tmp_err=$(mktemp) - - if ! $STELLAR contract invoke --source "$source" --network "$NETWORK" "$@" >"$tmp_out" 2>"$tmp_err"; then - log_error "Invocation failed!" - cat "$tmp_err" >&2 - rm -f "$tmp_out" "$tmp_err" - return 1 - fi - - tail -1 "$tmp_out" - rm -f "$tmp_out" "$tmp_err" -} - log "Starting E2E load test (N=$N, D=$D)" # Ensure contract is built @@ -91,7 +35,7 @@ log "Rebuilding contract if necessary" setup_start=$(get_time) log "Generating and funding load test identities on testnet..." -DEPLOYER=$(gen_key load_deployer) +DEPLOYER=$(gen_key "$DEPLOYER_IDENTITY") R1=$(gen_key load_resolver1) R2=$(gen_key load_resolver2) R3=$(gen_key load_resolver3) @@ -99,14 +43,14 @@ ASSERTER=$(gen_key load_asserter) DISPUTER=$(gen_key load_disputer) log "Deploying contract" -CONTRACT=$($STELLAR contract deploy --wasm "$WASM_PATH" --source load_deployer --network "$NETWORK" 2>/dev/null | tail -1) +CONTRACT=$($STELLAR contract deploy --wasm "$WASM_PATH" --source "$DEPLOYER_IDENTITY" --network "$NETWORK" 2>/dev/null | tail -1) log "Contract ID: $CONTRACT" TOKEN=$($STELLAR contract id asset --asset native --network "$NETWORK") log "Token (native XLM SAC): $TOKEN" log "Initializing contract with 3-member resolver committee and challenge_window_secs=$CHALLENGE_WINDOW_SECS" -invoke_contract load_deployer --id "$CONTRACT" -- initialize \ +invoke_contract "$DEPLOYER_IDENTITY" --id "$CONTRACT" -- initialize \ --admin "$DEPLOYER" \ --token "$TOKEN" \ --bond_amount "$BOND_AMOUNT" \ @@ -207,7 +151,7 @@ for ((i=0; i Date: Sun, 23 Aug 2026 05:51:29 +0100 Subject: [PATCH 3/4] chore: source the shared helpers in testnet-load-v2.sh Same removal as the previous commit, from the v2 script's own copies of the identical helpers, with DEPLOYER_IDENTITY set to v2load_deployer so balance() keeps sourcing its read-only query from this script's deployer. network_id() and json_field() stay put: both are specific to v2's commitment scheme and have no counterpart in the v1 script, so there is nothing to share. With this the duplication the issue describes is gone, one copy of each helper remains, and `shellcheck -x scripts/*.sh scripts/lib/*.sh` is clean. Closes #97 Co-Authored-By: Claude Opus 5 --- scripts/testnet-load-v2.sh | 97 ++++++-------------------------------- 1 file changed, 15 insertions(+), 82 deletions(-) diff --git a/scripts/testnet-load-v2.sh b/scripts/testnet-load-v2.sh index 034df95..954e9d9 100755 --- a/scripts/testnet-load-v2.sh +++ b/scripts/testnet-load-v2.sh @@ -31,84 +31,17 @@ ANTI_SNIPE_HARD_MAX_SECS=240 REVEAL_DURATION_SECS=180 MAX_POSITION=10000000 MAX_TOTAL_WEIGHT=1000000000 +# Funded identity this script deploys and reads balances from. +DEPLOYER_IDENTITY=v2load_deployer + +# Logging, timing and invocation helpers shared with testnet-load.sh. They +# read NETWORK and DEPLOYER_IDENTITY, so this must come after them. +# shellcheck source=SCRIPTDIR/lib/load-test-common.sh +source "$CONTRACT_DIR/scripts/lib/load-test-common.sh" # Number of third-party positions to fund on the strict-majority dispute. P=${1:-8} -log() { - echo -e "\033[1;34m>>\033[0m $*" -} - -log_success() { - echo -e "\033[1;32m✓\033[0m $*" -} - -log_error() { - echo -e "\033[1;31m✗\033[0m $*" -} - -get_time() { - date +%s.%N 2>/dev/null || date +%s -} - -elapsed_time() { - local start=$1 - local end=$2 - if command -v awk >/dev/null 2>&1; then - awk -v s="$start" -v e="$end" 'BEGIN { printf "%.2f", e - s }' - else - local diff=$(( ${end%.*} - ${start%.*} )) - echo "$diff" - fi -} - -avg_time() { - local sum=0 - local count=${#@} - if [ "$count" -eq 0 ]; then - echo "0.00" - return - fi - for val in "$@"; do - sum=$(awk -v s="$sum" -v v="$val" 'BEGIN { print s + v }') - done - awk -v s="$sum" -v c="$count" 'BEGIN { printf "%.2f", s / c }' -} - -STELLAR="stellar" - -gen_key() { - local name=$1 - $STELLAR keys generate "$name" --network "$NETWORK" --fund --overwrite >/dev/null - $STELLAR keys address "$name" -} - -balance() { - local token=$1 - local addr=$2 - $STELLAR contract invoke --id "$token" --source v2load_deployer --network "$NETWORK" -- balance --id "$addr" 2>/dev/null \ - | tr -d '"' -} - -invoke_contract() { - local source=$1 - shift - local tmp_out - tmp_out=$(mktemp) - local tmp_err - tmp_err=$(mktemp) - - if ! $STELLAR contract invoke --source "$source" --network "$NETWORK" "$@" >"$tmp_out" 2>"$tmp_err"; then - log_error "Invocation failed!" - cat "$tmp_err" >&2 - rm -f "$tmp_out" "$tmp_err" - return 1 - fi - - tail -1 "$tmp_out" - rm -f "$tmp_out" "$tmp_err" -} - network_id() { # register()'s commitment hashes over env.ledger().network_id(), the # sha256 of the network passphrase (a public, well-known Stellar @@ -150,7 +83,7 @@ log "Network id (sha256 of the testnet passphrase): $NETWORK_ID" setup_start=$(get_time) log "Generating and funding load test identities on testnet..." -DEPLOYER=$(gen_key v2load_deployer) +DEPLOYER=$(gen_key "$DEPLOYER_IDENTITY") ASSERTER=$(gen_key v2load_asserter) DISPUTER=$(gen_key v2load_disputer) @@ -164,14 +97,14 @@ for ((i=0; i/dev/null | tail -1) +CONTRACT=$($STELLAR contract deploy --wasm "$WASM_PATH" --source "$DEPLOYER_IDENTITY" --network "$NETWORK" 2>/dev/null | tail -1) log "Contract ID: $CONTRACT" TOKEN=$($STELLAR contract id asset --asset native --network "$NETWORK") log "Token (native XLM SAC): $TOKEN" log "Initializing contract" -invoke_contract v2load_deployer --id "$CONTRACT" -- initialize \ +invoke_contract "$DEPLOYER_IDENTITY" --id "$CONTRACT" -- initialize \ --admin "$DEPLOYER" \ --token "$TOKEN" \ --base_bond "$BOND_AMOUNT" \ @@ -225,7 +158,7 @@ log_success "Phase 1 completed in ${phase1_duration}s." # register()/reveal() actually verify (computed by compute_commitment, see # lib.rs's VoteCommitmentPreimage), not an arbitrary placeholder: reveal # would reject anything else with CommitmentVerificationFailed. -MAJORITY_POLICY_HASH=$(json_field "$(invoke_contract v2load_deployer --id "$CONTRACT" -- get_assertion --id "$MAJORITY_ID")" policy_hash) +MAJORITY_POLICY_HASH=$(json_field "$(invoke_contract "$DEPLOYER_IDENTITY" --id "$CONTRACT" -- get_assertion --id "$MAJORITY_ID")" policy_hash) log "Starting Phase 2: registering $P third-party positions on $MAJORITY_ID (all agreeing, driving it to a strict majority)..." phase2_start=$(get_time) registration_times=() @@ -286,7 +219,7 @@ phase3_end=$(get_time) phase3_duration=$(elapsed_time "$phase3_start" "$phase3_end") log_success "Phase 3 (Reveal) completed in ${phase3_duration}s." -state=$(invoke_contract v2load_deployer --id "$CONTRACT" -- get_assertion --id "$MAJORITY_ID") +state=$(invoke_contract "$DEPLOYER_IDENTITY" --id "$CONTRACT" -- get_assertion --id "$MAJORITY_ID") if ! echo "$state" | grep -q '"terminal_cause":"StrictMajorityFor"'; then log_error "Expected $MAJORITY_ID to have locked StrictMajorityFor. Got: $state" exit 1 @@ -298,7 +231,7 @@ log "Waiting for $TIMEOUT_ID's registration and reveal windows to close..." sleep $((REGISTRATION_DURATION_SECS + REVEAL_DURATION_SECS + 10)) log "Closing $TIMEOUT_ID via resolve_outcome (permissionless)..." -cause=$(invoke_contract v2load_deployer --id "$CONTRACT" -- resolve_outcome --id "$TIMEOUT_ID") +cause=$(invoke_contract "$DEPLOYER_IDENTITY" --id "$CONTRACT" -- resolve_outcome --id "$TIMEOUT_ID") if [ "$cause" != '"OptimisticTimeout"' ]; then log_error "Expected $TIMEOUT_ID to resolve as OptimisticTimeout. Got: $cause" exit 1 @@ -333,7 +266,7 @@ settle_one() { local addr=$2 local s_start s_start=$(get_time) - invoke_contract v2load_deployer --id "$CONTRACT" -- settle --id "$id" --address "$addr" >/dev/null + invoke_contract "$DEPLOYER_IDENTITY" --id "$CONTRACT" -- settle --id "$id" --address "$addr" >/dev/null local s_end s_end=$(get_time) elapsed_time "$s_start" "$s_end" @@ -380,7 +313,7 @@ withdraw_if_owed() { local name=$2 local addr=$3 local credit - credit=$(invoke_contract v2load_deployer --id "$CONTRACT" -- get_credit --id "$id" --address "$addr") + credit=$(invoke_contract "$DEPLOYER_IDENTITY" --id "$CONTRACT" -- get_credit --id "$id" --address "$addr") credit=$(echo "$credit" | tr -d '"') if [ "$credit" = "0" ]; then log_success "Nothing owed to $name on $id, skipping withdraw" From e95501bb741d8e02875ad6981ce88ded6d5c7527 Mon Sep 17 00:00:00 2001 From: olathedev Date: Tue, 25 Aug 2026 04:37:45 +0100 Subject: [PATCH 4/4] chore: match the shellcheck command in Makefile, README and CONTRIBUTING The CI lint step gained `-x` and `scripts/lib/*.sh` so it could follow the load tests' new `source` line, but three places that document the same command locally were left on the old `shellcheck scripts/*.sh`: - Makefile's `shellcheck` target, which `make check` runs - CONTRIBUTING.md's "raw commands directly" list - README.md's block labelled "same checks CI runs" All three exited 1 with SC1091 on the new source line while CI passed, so the local commands contradicted both CI and their own descriptions. Point them at the same command CI runs; `make shellcheck` and the two documented command lists are now clean again. Co-Authored-By: Claude Opus 5 --- CONTRIBUTING.md | 2 +- Makefile | 2 +- README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fd928db..f8e1def 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -191,7 +191,7 @@ Or run the raw commands directly: ```sh cargo fmt --check -shellcheck scripts/*.sh +shellcheck -x scripts/*.sh scripts/lib/*.sh cargo build -p tholos --target wasm32v1-none --release cargo clippy --workspace --all-targets -- -D warnings cargo test diff --git a/Makefile b/Makefile index 799ba66..04e0bfb 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ fmt: # Lint shell scripts (fails if shellcheck isn't installed, matching CI) shellcheck: @command -v shellcheck >/dev/null 2>&1 || { echo "shellcheck not installed; install it before running make check."; exit 1; } - shellcheck scripts/*.sh + shellcheck -x scripts/*.sh scripts/lib/*.sh # Build Tholos WASM first (required by demo-consumer at compile time) build-wasm: diff --git a/README.md b/README.md index 121cdeb..305a42a 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ cargo test # Check formatting and lints (same checks CI runs) cargo fmt --check -shellcheck scripts/*.sh +shellcheck -x scripts/*.sh scripts/lib/*.sh cargo clippy --workspace --all-targets -- -D warnings # Build the optimized, deployable contract wasm