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/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 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" +} 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"
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