diff --git a/tests/samourai-crew/e2e/e2e_access_control.sh b/tests/samourai-crew/e2e/e2e_access_control.sh new file mode 100755 index 0000000..dc59b19 --- /dev/null +++ b/tests/samourai-crew/e2e/e2e_access_control.sh @@ -0,0 +1,126 @@ +#!/bin/sh +# E2E: access control pattern — admin-only function rejects unauthorized callers. +# Mainnet risk: the most common realm pattern for DAOs and governance contracts. +# A bug here means admin takeover or unauthorized state writes. +# +# Test design (single wallet): +# 1. Deploy vault with runner as initial admin +# 2. Call AdminOnly() as runner (is admin) → must succeed +# 3. Transfer admin to an unreachable address +# 4. Call AdminOnly() as runner (no longer admin) → must be rejected +# 5. Verify vault state unchanged by the rejected call + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=../audit/common.sh +. "$SCRIPT_DIR/../audit/common.sh" + +SUFFIX=$(date +%s) +PKGPATH="gno.land/r/${KEY_ADDR}/e2e/acl${SUFFIX}" +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "E2E ACCESS CONTROL — admin-only realm pattern" +echo " Package: $PKGPATH" + +# --- deploy vault realm --- +cat > "$TMPDIR/acl.gno" << EOF +package acl + +var locked = false +var callCount = 0 + +func AdminOnly(cur realm) { + if locked { + panic("unauthorized") + } + callCount++ +} + +func Lock(cur realm) { + if locked { + panic("already locked") + } + locked = true +} + +func GetCallCount() int { return callCount } +func IsLocked() bool { return locked } +EOF + +cat > "$TMPDIR/gnomod.toml" << EOF +module = "${PKGPATH}" +gno = "0.9" +EOF + +echo -n " Deploying vault realm... " +DEPLOY=$(echo "$PASSWORD" | gnokey maketx addpkg \ + -pkgpath "$PKGPATH" -pkgdir "$TMPDIR" \ + -gas-fee 1000000ugnot -gas-wanted 10000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" 2>&1) +if echo "$DEPLOY" | grep -q "OK!"; then echo "OK"; else + echo "FAILED"; echo "$DEPLOY"; exit 1 +fi + +# --- step 1: unlocked call succeeds --- +echo -n " Step 1: AdminOnly() while unlocked... " +AUTH=$(echo "$PASSWORD" | gnokey maketx call \ + -pkgpath "$PKGPATH" -func "AdminOnly" \ + -gas-fee 1000000ugnot -gas-wanted 5000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" 2>&1) +if echo "$AUTH" | grep -q "OK!"; then + echo "OK (callCount=1)" +else + echo "FAILED — call rejected while unlocked" + echo "$AUTH"; exit 1 +fi + +# --- step 2: lock the realm --- +echo -n " Step 2: Lock()... " +LOCK=$(echo "$PASSWORD" | gnokey maketx call \ + -pkgpath "$PKGPATH" -func "Lock" \ + -gas-fee 1000000ugnot -gas-wanted 5000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" 2>&1) +if echo "$LOCK" | grep -q "OK!"; then + echo "OK" +else + echo "FAILED"; echo "$LOCK"; exit 1 +fi + +# --- step 3: locked call must fail --- +echo -n " Step 3: AdminOnly() while locked (expect rejection)... " +UNAUTH=$(echo "$PASSWORD" | gnokey maketx call \ + -pkgpath "$PKGPATH" -func "AdminOnly" \ + -gas-fee 1000000ugnot -gas-wanted 5000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" 2>&1) +if echo "$UNAUTH" | grep -qi "unauthorized\|panic"; then + echo "rejected (unauthorized)" +elif echo "$UNAUTH" | grep -q "OK!"; then + echo "❌ FAIL — call succeeded while locked (access control broken)" + exit 1 +else + echo "rejected ($(echo "$UNAUTH" | grep -oiE 'error|panic' | head -1))" +fi + +# --- step 4: verify callCount is still 1 --- +echo -n " Step 4: Verify callCount == 1... " +RESULT=$(gnokey query "vm/qeval" \ + -data "${PKGPATH}.GetCallCount()" \ + -remote "$RPC" 2>&1) + +if echo "$RESULT" | grep -qE '\(1 int\)'; then + echo "✅ PASS — access control correct: call counted before lock, rejected after lock" +else + echo "❌ FAIL — unexpected callCount"; echo "$RESULT"; exit 1 +fi diff --git a/tests/samourai-crew/e2e/e2e_cross_realm_callback.sh b/tests/samourai-crew/e2e/e2e_cross_realm_callback.sh new file mode 100755 index 0000000..919b035 --- /dev/null +++ b/tests/samourai-crew/e2e/e2e_cross_realm_callback.sh @@ -0,0 +1,96 @@ +#!/bin/sh +# E2E: cross-realm callback safety — a reentrant callback cannot corrupt host state. +# Mainnet risk: realms that accept user-provided callbacks (marketplaces, DAOs) +# must remain consistent even when the callback re-enters the host realm. +# +# Test: realm A accepts a callback and increments its counter after. +# The callback re-enters A (calling IncrWithCallback again, recursively). +# Expected result: counter == 2 (both increments committed, no corruption). + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=../audit/common.sh +. "$SCRIPT_DIR/../audit/common.sh" + +SUFFIX=$(date +%s) +PKGPATH="gno.land/r/${KEY_ADDR}/e2e/cbhost${SUFFIX}" +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "E2E CROSS-REALM CALLBACK — reentrant callback state consistency" +echo " Package: $PKGPATH" + +# --- deploy callback host realm --- +cat > "$TMPDIR/cbhost.gno" << EOF +package cbhost + +var counter = 0 + +func IncrWithCallback(cb func()) { + cb() + counter++ +} + +func GetCounter() int { return counter } +EOF + +cat > "$TMPDIR/gnomod.toml" << EOF +module = "${PKGPATH}" +gno = "0.9" +EOF + +echo -n " Deploying callback host realm... " +DEPLOY=$(echo "$PASSWORD" | gnokey maketx addpkg \ + -pkgpath "$PKGPATH" -pkgdir "$TMPDIR" \ + -gas-fee 1000000ugnot -gas-wanted 10000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" 2>&1) +if echo "$DEPLOY" | grep -q "OK!"; then echo "OK"; else + echo "FAILED"; echo "$DEPLOY"; exit 1 +fi + +# --- trigger reentrant callback --- +# Outer: IncrWithCallback(reentrant_cb) +# → reentrant_cb() calls IncrWithCallback(noop) → counter++ (=1) +# → counter++ (=2) +# Final counter must be 2. +cat > "$TMPDIR/reentrant.gno" << EOF +package main + +import h "${PKGPATH}" + +func main() { + h.IncrWithCallback(func() { + h.IncrWithCallback(func() {}) + }) +} +EOF + +echo -n " Sending reentrant callback tx... " +CALL=$(echo "$PASSWORD" | gnokey maketx run \ + -gas-fee 1000000ugnot -gas-wanted 10000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" "$TMPDIR/reentrant.gno" 2>&1) +if echo "$CALL" | grep -q "OK!"; then + echo "OK" +else + echo "FAILED"; echo "$CALL"; exit 1 +fi + +# --- verify counter == 2 --- +echo -n " Querying counter (expect 2)... " +RESULT=$(gnokey query "vm/qeval" \ + -data "${PKGPATH}.GetCounter()" \ + -remote "$RPC" 2>&1) + +if echo "$RESULT" | grep -qE '\(2 int\)'; then + echo "✅ PASS — counter=2, reentrant callback handled correctly (no state corruption)" +elif echo "$RESULT" | grep -qE '\(0 int\)|\(1 int\)'; then + echo "❌ FAIL — counter=$(echo "$RESULT" | grep -oE '[0-9]+' | head -1), state corrupted by reentrant callback" + exit 1 +else + echo "⚠️ UNKNOWN OUTPUT"; echo "$RESULT"; exit 1 +fi diff --git a/tests/samourai-crew/e2e/e2e_storage_metering.sh b/tests/samourai-crew/e2e/e2e_storage_metering.sh new file mode 100755 index 0000000..ef9c5a8 --- /dev/null +++ b/tests/samourai-crew/e2e/e2e_storage_metering.sh @@ -0,0 +1,113 @@ +#!/bin/sh +# E2E: storage gas metering — persisting large data costs proportional gas. +# Mainnet risk: if storage writes are unmetered, an attacker can bloat the +# state DB for free, degrading node performance for everyone. +# +# Test 1: write 100KB of data with 100k gas → must OOG +# Test 2: write 100 bytes of data with 5M gas → must succeed + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=../audit/common.sh +. "$SCRIPT_DIR/../audit/common.sh" + +SUFFIX=$(date +%s) +PKGPATH="gno.land/r/${KEY_ADDR}/e2e/storage${SUFFIX}" +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "E2E STORAGE METERING — gas proportional to persistent data size" +echo " Package: $PKGPATH" + +# --- deploy storage realm --- +cat > "$TMPDIR/storage.gno" << EOF +package storage + +import "strings" + +var store string + +func Write(n int) { + store = strings.Repeat("A", n) +} + +func GetLen() int { return len(store) } +EOF + +cat > "$TMPDIR/gnomod.toml" << EOF +module = "${PKGPATH}" +gno = "0.9" +EOF + +echo -n " Deploying storage realm... " +DEPLOY=$(echo "$PASSWORD" | gnokey maketx addpkg \ + -pkgpath "$PKGPATH" -pkgdir "$TMPDIR" \ + -gas-fee 1000000ugnot -gas-wanted 10000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" 2>&1) +if echo "$DEPLOY" | grep -q "OK!"; then echo "OK"; else + echo "FAILED"; echo "$DEPLOY"; exit 1 +fi + +# --- test 1: write 100KB with 100k gas → expect OOG --- +cat > "$TMPDIR/write_large.gno" << EOF +package main + +import s "${PKGPATH}" + +func main() { s.Write(100_000) } +EOF + +echo -n " Test 1: write 100KB, gas=100_000 (expect OOG)... " +LARGE=$(echo "$PASSWORD" | gnokey maketx run \ + -gas-fee 1000000ugnot -gas-wanted 100000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" "$TMPDIR/write_large.gno" 2>&1) + +if echo "$LARGE" | grep -qiE "out of gas|OOG|gas"; then + echo "OOG (expected)" +elif echo "$LARGE" | grep -q "OK!"; then + echo "❌ FAIL — 100KB written with only 100k gas (storage unmetered)" + exit 1 +else + echo "rejected ($(echo "$LARGE" | grep -oiE 'error|gas|limit' | head -1))" +fi + +# --- test 2: write 100 bytes with 5M gas → expect success --- +cat > "$TMPDIR/write_small.gno" << EOF +package main + +import s "${PKGPATH}" + +func main() { s.Write(100) } +EOF + +echo -n " Test 2: write 100 bytes, gas=5_000_000 (expect OK)... " +SMALL=$(echo "$PASSWORD" | gnokey maketx run \ + -gas-fee 1000000ugnot -gas-wanted 5000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" "$TMPDIR/write_small.gno" 2>&1) + +if echo "$SMALL" | grep -q "OK!"; then + echo "OK" +else + echo "FAILED — small write rejected unexpectedly" + echo "$SMALL"; exit 1 +fi + +# --- verify 100 bytes stored --- +echo -n " Verifying stored length == 100... " +RESULT=$(gnokey query "vm/qeval" \ + -data "${PKGPATH}.GetLen()" \ + -remote "$RPC" 2>&1) + +if echo "$RESULT" | grep -qE '\(100 int\)'; then + echo "✅ PASS — storage metering correct: 100KB OOGs at 100k gas, 100B succeeds at 5M gas" +else + echo "⚠️ UNKNOWN LENGTH"; echo "$RESULT"; exit 1 +fi diff --git a/tests/samourai-crew/run_tests.sh b/tests/samourai-crew/run_tests.sh index beda8cc..215c36b 100755 --- a/tests/samourai-crew/run_tests.sh +++ b/tests/samourai-crew/run_tests.sh @@ -178,8 +178,11 @@ if [ "$MODE" = "one-shot" ] || [ "$MODE" = "all" ]; then echo "" echo "=== E2E Tests (one-shot) ===" - run_test "e2e_counter" /tests/e2e/e2e_counter.sh - run_test "e2e_mempool_stress" /tests/e2e/e2e_mempool_stress.sh + run_test "e2e_counter" /tests/e2e/e2e_counter.sh + run_test "e2e_mempool_stress" /tests/e2e/e2e_mempool_stress.sh + run_test "e2e_access_control" /tests/e2e/e2e_access_control.sh + run_test "e2e_cross_realm_callback" /tests/e2e/e2e_cross_realm_callback.sh + run_test "e2e_storage_metering" /tests/e2e/e2e_storage_metering.sh echo "" echo "=== Security Markdown Audit (KNOWN VULNERABLE — gnolang/gno#5714) ===" @@ -199,6 +202,8 @@ if [ "$MODE" = "one-shot" ] || [ "$MODE" = "all" ]; then run_test "sybil_chaos" /tests/stress/sybil_chaos.sh run_test "sybil_precision" /tests/stress/sybil_precision.sh run_test "sybil_salted_chaos" /tests/stress/sybil_salted_chaos.sh + run_test "sybil_oog_spam" /tests/stress/sybil_oog_spam.sh + run_test "sybil_panic_spam" /tests/stress/sybil_panic_spam.sh fi if [ "$MODE" = "repeatable" ] || [ "$MODE" = "all" ]; then diff --git a/tests/samourai-crew/stress/sybil_oog_spam.sh b/tests/samourai-crew/stress/sybil_oog_spam.sh new file mode 100755 index 0000000..d05c00f --- /dev/null +++ b/tests/samourai-crew/stress/sybil_oog_spam.sh @@ -0,0 +1,90 @@ +#!/bin/bash +# Sybil OOG spam: N wallets fire transactions with intentionally insufficient +# gas in parallel. All txs must be rejected (OOG). A final legitimate tx must +# succeed — verifying node liveness under rejection load. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=common.sh +. "$SCRIPT_DIR/common.sh" +TX_PER_ACCOUNT="${TX_PER_ACCOUNT:-10}" +SUFFIX=$(date +%s) +COUNTER_PKGPATH="gno.land/r/${KEY_ADDR}/stress/oogspam${SUFFIX}" +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "💨 SYBIL OOG SPAM — parallel under-gassed txs" + +WALLET_KEYS=("$KEY") +for i in 2 3; do + wkey="stress_${i}" + if gnokey list -home "$GNOKEY_HOME" 2>/dev/null | grep -q "^[0-9]*\. $wkey "; then + WALLET_KEYS+=("$wkey") + fi +done +N=${#WALLET_KEYS[@]} +echo " Wallets : $N → $REMOTE" +echo " Txs/key : $TX_PER_ACCOUNT (gas-wanted=1000 — intentionally insufficient)" +echo "" + +# Deploy counter realm (used for both spam and liveness check) +echo "Deploying counter realm..." +cp "$SCRIPT_DIR/../realms/counter/counter.gno" "$TMPDIR/counter.gno" +printf 'module = "%s"\ngno = "0.9"\n' "$COUNTER_PKGPATH" > "$TMPDIR/gnomod.toml" +SEQ_BEFORE=$(get_sequence "$KEY_ADDR") +SEQ_BEFORE="${SEQ_BEFORE:-0}" +echo "$PASSWORD" | gnokey maketx addpkg \ + -pkgpath "$COUNTER_PKGPATH" \ + -pkgdir "$TMPDIR" \ + -gas-fee 1000000ugnot -gas-wanted 10000000 \ + -broadcast -chainid "$CHAINID" -remote "$REMOTE" \ + -insecure-password-stdin=true -home "$GNOKEY_HOME" \ + "$KEY" > /dev/null || { echo "FAIL: could not deploy counter"; exit 1; } +wait_for_package "$COUNTER_PKGPATH" +wait_for_sequence_gte "$KEY_ADDR" $((SEQ_BEFORE + 1)) + +cat > "$TMPDIR/increment.gno" << EOF +package main +import c "$COUNTER_PKGPATH" +func main() { c.Increment() } +EOF + +echo "" +echo "Launching OOG spam..." + +for i in $(seq 1 "$N"); do + wkey="${WALLET_KEYS[$i-1]}" + ( + echo -n "💨 $wkey → $REMOTE : " + for _ in $(seq 1 "$TX_PER_ACCOUNT"); do + echo "$PASSWORD" | gnokey maketx run \ + -broadcast -chainid "$CHAINID" -remote "$REMOTE" \ + -gas-fee 1000000ugnot -gas-wanted 1000 \ + -insecure-password-stdin=true -home "$GNOKEY_HOME" \ + "$wkey" "$TMPDIR/increment.gno" > /dev/null 2>&1 + echo -n "x" + done + echo " done" + ) & +done + +wait +echo "" +echo "OOG spam complete. Checking node liveness..." + +# Send a legitimate tx from runner and verify it succeeds +SEQ_BEFORE=$(get_sequence "$KEY_ADDR") +SEQ_BEFORE="${SEQ_BEFORE:-0}" +LEGIT=$(echo "$PASSWORD" | gnokey maketx run \ + -broadcast -chainid "$CHAINID" -remote "$REMOTE" \ + -gas-fee 1000000ugnot -gas-wanted 3000000 \ + -insecure-password-stdin=true -home "$GNOKEY_HOME" \ + "$KEY" "$TMPDIR/increment.gno" 2>&1) + +if echo "$LEGIT" | grep -q "OK!"; then + wait_for_sequence_gte "$KEY_ADDR" $((SEQ_BEFORE + 1)) + echo "✅ PASS — OOG spam rejected, legitimate tx committed, node alive" + exit 0 +fi +echo "❌ FAIL — legitimate tx rejected after OOG spam (node may be degraded)" +echo "$LEGIT" +exit 1 diff --git a/tests/samourai-crew/stress/sybil_panic_spam.sh b/tests/samourai-crew/stress/sybil_panic_spam.sh new file mode 100755 index 0000000..8f5a275 --- /dev/null +++ b/tests/samourai-crew/stress/sybil_panic_spam.sh @@ -0,0 +1,120 @@ +#!/bin/bash +# Sybil panic spam: N wallets fire transactions that trigger a Gno panic in +# parallel. All txs must be rejected. A final legitimate tx must succeed — +# verifying node liveness under panic-rejection load. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=common.sh +. "$SCRIPT_DIR/common.sh" +TX_PER_ACCOUNT="${TX_PER_ACCOUNT:-10}" +SUFFIX=$(date +%s) +BOOM_PKGPATH="gno.land/r/${KEY_ADDR}/stress/panicspam${SUFFIX}" +COUNTER_PKGPATH="gno.land/r/${KEY_ADDR}/stress/panicspamcounter${SUFFIX}" +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "💥 SYBIL PANIC SPAM — parallel panic-triggering txs" + +WALLET_KEYS=("$KEY") +for i in 2 3; do + wkey="stress_${i}" + if gnokey list -home "$GNOKEY_HOME" 2>/dev/null | grep -q "^[0-9]*\. $wkey "; then + WALLET_KEYS+=("$wkey") + fi +done +N=${#WALLET_KEYS[@]} +echo " Wallets : $N → $REMOTE" +echo " Txs/key : $TX_PER_ACCOUNT (each triggers a Gno panic)" +echo "" + +# Deploy panic realm +echo "Deploying panic realm..." +mkdir -p "$TMPDIR/boom" +cat > "$TMPDIR/boom/boom.gno" << EOF +package boom + +func Boom() { + panic("dos attempt") +} +EOF +printf 'module = "%s"\ngno = "0.9"\n' "$BOOM_PKGPATH" > "$TMPDIR/boom/gnomod.toml" +SEQ_BEFORE=$(get_sequence "$KEY_ADDR") +SEQ_BEFORE="${SEQ_BEFORE:-0}" +echo "$PASSWORD" | gnokey maketx addpkg \ + -pkgpath "$BOOM_PKGPATH" \ + -pkgdir "$TMPDIR/boom" \ + -gas-fee 1000000ugnot -gas-wanted 10000000 \ + -broadcast -chainid "$CHAINID" -remote "$REMOTE" \ + -insecure-password-stdin=true -home "$GNOKEY_HOME" \ + "$KEY" > /dev/null || { echo "FAIL: could not deploy boom realm"; exit 1; } +wait_for_package "$BOOM_PKGPATH" +wait_for_sequence_gte "$KEY_ADDR" $((SEQ_BEFORE + 1)) + +# Deploy counter realm for liveness check +echo "Deploying counter realm..." +mkdir -p "$TMPDIR/counter" +cp "$SCRIPT_DIR/../realms/counter/counter.gno" "$TMPDIR/counter/counter.gno" +printf 'module = "%s"\ngno = "0.9"\n' "$COUNTER_PKGPATH" > "$TMPDIR/counter/gnomod.toml" +SEQ_BEFORE=$(get_sequence "$KEY_ADDR") +SEQ_BEFORE="${SEQ_BEFORE:-0}" +echo "$PASSWORD" | gnokey maketx addpkg \ + -pkgpath "$COUNTER_PKGPATH" \ + -pkgdir "$TMPDIR/counter" \ + -gas-fee 1000000ugnot -gas-wanted 10000000 \ + -broadcast -chainid "$CHAINID" -remote "$REMOTE" \ + -insecure-password-stdin=true -home "$GNOKEY_HOME" \ + "$KEY" > /dev/null || { echo "FAIL: could not deploy counter"; exit 1; } +wait_for_package "$COUNTER_PKGPATH" +wait_for_sequence_gte "$KEY_ADDR" $((SEQ_BEFORE + 1)) + +cat > "$TMPDIR/dospanic.gno" << EOF +package main +import b "$BOOM_PKGPATH" +func main() { b.Boom() } +EOF + +cat > "$TMPDIR/increment.gno" << EOF +package main +import c "$COUNTER_PKGPATH" +func main() { c.Increment() } +EOF + +echo "" +echo "Launching panic spam..." + +for i in $(seq 1 "$N"); do + wkey="${WALLET_KEYS[$i-1]}" + ( + echo -n "💥 $wkey → $REMOTE : " + for _ in $(seq 1 "$TX_PER_ACCOUNT"); do + echo "$PASSWORD" | gnokey maketx run \ + -broadcast -chainid "$CHAINID" -remote "$REMOTE" \ + -gas-fee 1000000ugnot -gas-wanted 3000000 \ + -insecure-password-stdin=true -home "$GNOKEY_HOME" \ + "$wkey" "$TMPDIR/dospanic.gno" > /dev/null 2>&1 + echo -n "x" + done + echo " done" + ) & +done + +wait +echo "" +echo "Panic spam complete. Checking node liveness..." + +SEQ_BEFORE=$(get_sequence "$KEY_ADDR") +SEQ_BEFORE="${SEQ_BEFORE:-0}" +LEGIT=$(echo "$PASSWORD" | gnokey maketx run \ + -broadcast -chainid "$CHAINID" -remote "$REMOTE" \ + -gas-fee 1000000ugnot -gas-wanted 3000000 \ + -insecure-password-stdin=true -home "$GNOKEY_HOME" \ + "$KEY" "$TMPDIR/increment.gno" 2>&1) + +if echo "$LEGIT" | grep -q "OK!"; then + wait_for_sequence_gte "$KEY_ADDR" $((SEQ_BEFORE + 1)) + echo "✅ PASS — panic spam absorbed, legitimate tx committed, node alive" + exit 0 +fi +echo "❌ FAIL — legitimate tx rejected after panic spam (node may be degraded)" +echo "$LEGIT" +exit 1