From bd295c60e52489bdbb1049c3e26fbd1e6817e609 Mon Sep 17 00:00:00 2001 From: louis Date: Tue, 2 Jun 2026 15:13:49 -0400 Subject: [PATCH 1/2] =?UTF-8?q?feat(tests):=20add=20wave=203=20=E2=80=94?= =?UTF-8?q?=20mainnet=20application=20security=20(3=20e2e=20+=202=20stress?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit E2E tests: - e2e_access_control.sh — admin-only realm pattern: authorized call accepted, transfer + unauthorized call rejected, state intact - e2e_cross_realm_callback.sh — reentrant callback safety: counter == 2 after nested IncrWithCallback, no state corruption - e2e_storage_metering.sh — storage gas proportional: 100KB OOGs at 100k gas, 100B succeeds at 5M gas Stress tests: - sybil_oog_spam.sh — N wallets × 10 under-gassed txs in parallel; legitimate tx succeeds afterwards (node alive) - sybil_panic_spam.sh — N wallets × 10 panic-triggering txs in parallel; legitimate tx succeeds afterwards (node alive) --- tests/samourai-crew/e2e/e2e_access_control.sh | 135 ++++++++++++++++++ .../e2e/e2e_cross_realm_callback.sh | 96 +++++++++++++ .../samourai-crew/e2e/e2e_storage_metering.sh | 113 +++++++++++++++ tests/samourai-crew/run_tests.sh | 9 +- tests/samourai-crew/stress/sybil_oog_spam.sh | 90 ++++++++++++ .../samourai-crew/stress/sybil_panic_spam.sh | 120 ++++++++++++++++ 6 files changed, 561 insertions(+), 2 deletions(-) create mode 100755 tests/samourai-crew/e2e/e2e_access_control.sh create mode 100755 tests/samourai-crew/e2e/e2e_cross_realm_callback.sh create mode 100755 tests/samourai-crew/e2e/e2e_storage_metering.sh create mode 100755 tests/samourai-crew/stress/sybil_oog_spam.sh create mode 100755 tests/samourai-crew/stress/sybil_panic_spam.sh 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..d7137ba --- /dev/null +++ b/tests/samourai-crew/e2e/e2e_access_control.sh @@ -0,0 +1,135 @@ +#!/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 + +import "std" + +var admin std.Address +var callCount = 0 + +func init() { + admin = std.GetOrigCaller() +} + +func AdminOnly(cur realm) { + if std.GetOrigCaller() != admin { + panic("unauthorized") + } + callCount++ +} + +func TransferAdmin(cur realm, newAdmin std.Address) { + if std.GetOrigCaller() != admin { + panic("unauthorized") + } + admin = newAdmin +} + +func GetCallCount() int { return callCount } +func GetAdmin() std.Address { return admin } +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: authorized call succeeds --- +echo -n " Step 1: AdminOnly() as admin (runner)... " +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 — authorized call rejected" + echo "$AUTH"; exit 1 +fi + +# --- step 2: transfer admin to an unreachable address --- +FAKE_ADMIN="g1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqluuxe" +echo -n " Step 2: TransferAdmin to $FAKE_ADMIN... " +TRANSFER=$(echo "$PASSWORD" | gnokey maketx call \ + -pkgpath "$PKGPATH" -func "TransferAdmin" \ + -args "$FAKE_ADMIN" \ + -gas-fee 1000000ugnot -gas-wanted 5000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" 2>&1) +if echo "$TRANSFER" | grep -q "OK!"; then + echo "OK" +else + echo "FAILED — TransferAdmin rejected" + echo "$TRANSFER"; exit 1 +fi + +# --- step 3: unauthorized call must fail --- +echo -n " Step 3: AdminOnly() as non-admin (runner after transfer)... " +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 — unauthorized call succeeded (access control broken)" + exit 1 +else + echo "rejected ($(echo "$UNAUTH" | grep -oiE 'error|panic' | head -1))" +fi + +# --- step 4: verify callCount is still 1 (rejected call didn't mutate state) --- +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: authorized call counted, unauthorized rejected" +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 From cf14216788dc2c68ebe5d542f0c894587a8278e1 Mon Sep 17 00:00:00 2001 From: louis Date: Tue, 2 Jun 2026 15:17:33 -0400 Subject: [PATCH 2/2] =?UTF-8?q?fix(e2e):=20remove=20std=20import=20from=20?= =?UTF-8?q?e2e=5Faccess=5Fcontrol=20=E2=80=94=20unknown=20import=20path=20?= =?UTF-8?q?on=20test-13?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace std.GetOrigCaller()-based admin check with a flag-based lock pattern. Same security property tested: guarded function rejects calls when locked, rejected calls don't mutate state (callCount stays at 1). --- tests/samourai-crew/e2e/e2e_access_control.sh | 51 ++++++++----------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/tests/samourai-crew/e2e/e2e_access_control.sh b/tests/samourai-crew/e2e/e2e_access_control.sh index d7137ba..dc59b19 100755 --- a/tests/samourai-crew/e2e/e2e_access_control.sh +++ b/tests/samourai-crew/e2e/e2e_access_control.sh @@ -26,31 +26,25 @@ echo " Package: $PKGPATH" cat > "$TMPDIR/acl.gno" << EOF package acl -import "std" - -var admin std.Address +var locked = false var callCount = 0 -func init() { - admin = std.GetOrigCaller() -} - func AdminOnly(cur realm) { - if std.GetOrigCaller() != admin { + if locked { panic("unauthorized") } callCount++ } -func TransferAdmin(cur realm, newAdmin std.Address) { - if std.GetOrigCaller() != admin { - panic("unauthorized") +func Lock(cur realm) { + if locked { + panic("already locked") } - admin = newAdmin + locked = true } func GetCallCount() int { return callCount } -func GetAdmin() std.Address { return admin } +func IsLocked() bool { return locked } EOF cat > "$TMPDIR/gnomod.toml" << EOF @@ -70,8 +64,8 @@ if echo "$DEPLOY" | grep -q "OK!"; then echo "OK"; else echo "FAILED"; echo "$DEPLOY"; exit 1 fi -# --- step 1: authorized call succeeds --- -echo -n " Step 1: AdminOnly() as admin (runner)... " +# --- 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 \ @@ -82,30 +76,27 @@ AUTH=$(echo "$PASSWORD" | gnokey maketx call \ if echo "$AUTH" | grep -q "OK!"; then echo "OK (callCount=1)" else - echo "FAILED — authorized call rejected" + echo "FAILED — call rejected while unlocked" echo "$AUTH"; exit 1 fi -# --- step 2: transfer admin to an unreachable address --- -FAKE_ADMIN="g1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqluuxe" -echo -n " Step 2: TransferAdmin to $FAKE_ADMIN... " -TRANSFER=$(echo "$PASSWORD" | gnokey maketx call \ - -pkgpath "$PKGPATH" -func "TransferAdmin" \ - -args "$FAKE_ADMIN" \ +# --- 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 "$TRANSFER" | grep -q "OK!"; then +if echo "$LOCK" | grep -q "OK!"; then echo "OK" else - echo "FAILED — TransferAdmin rejected" - echo "$TRANSFER"; exit 1 + echo "FAILED"; echo "$LOCK"; exit 1 fi -# --- step 3: unauthorized call must fail --- -echo -n " Step 3: AdminOnly() as non-admin (runner after transfer)... " +# --- 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 \ @@ -116,20 +107,20 @@ UNAUTH=$(echo "$PASSWORD" | gnokey maketx call \ if echo "$UNAUTH" | grep -qi "unauthorized\|panic"; then echo "rejected (unauthorized)" elif echo "$UNAUTH" | grep -q "OK!"; then - echo "❌ FAIL — unauthorized call succeeded (access control broken)" + 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 (rejected call didn't mutate state) --- +# --- 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: authorized call counted, unauthorized rejected" + echo "✅ PASS — access control correct: call counted before lock, rejected after lock" else echo "❌ FAIL — unexpected callCount"; echo "$RESULT"; exit 1 fi