diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..852b682 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,91 @@ +name: Community Tests + +on: + push: + branches: [main, init-community-scripts] + pull_request: + branches: [main] + workflow_dispatch: + inputs: + run_test13: + description: 'Run on test-13' + type: boolean + default: true + run_test12: + description: 'Run on test12' + type: boolean + default: true + test_type: + description: 'Test type' + type: choice + default: both + options: [one-shot, repeatable, both] + +# Optional repository variables (Settings → Secrets and variables → Actions → Variables): +# vars.REMOTES_TEST13 — comma-separated RPC list for test-13 +# vars.CHAINID_TEST13 — chain ID for test-13 (default: test-13) +# vars.REMOTES_TEST12 — comma-separated RPC list for test12 +# vars.CHAINID_TEST12 — chain ID for test12 (default: test12) +# +# The test1 funder mnemonic is public and hardcoded in funders/ — no secret needed. +# The funder script runs inside a gnokey Docker container — no local +# gnokey installation required on the runner. + +jobs: + tests-one-shot: + name: One-shot — ${{ matrix.network.name }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + network: + - name: test-13 + remotes: ${{ vars.REMOTES_TEST13 || 'https://rpc.test-13-aeddi-1.gnoland.network,https://rpc.test-13-gfanton-1.gnoland.network,https://rpc.test-13-moul-1.gnoland.network' }} + chainid: ${{ vars.CHAINID_TEST13 || 'test-13' }} + - name: test12 + remotes: ${{ vars.REMOTES_TEST12 || 'https://rpc.test12.testnets.gno.land' }} + chainid: ${{ vars.CHAINID_TEST12 || 'test12' }} + + steps: + - uses: actions/checkout@v4 + + - name: Run one-shot tests + if: > + github.event_name != 'workflow_dispatch' || + (inputs.test_type == 'one-shot' || inputs.test_type == 'both') && ( + (matrix.network.name == 'test-13' && inputs.run_test13 == true) || + (matrix.network.name == 'test12' && inputs.run_test12 == true) + ) + run: | + make tests-one-shot \ + REMOTES="${{ matrix.network.remotes }}" \ + CHAINID=${{ matrix.network.chainid }} + + tests-repeatable: + name: Repeatable — ${{ matrix.network.name }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + network: + - name: test-13 + remotes: ${{ vars.REMOTES_TEST13 || 'https://rpc.test-13-aeddi-1.gnoland.network' }} + chainid: ${{ vars.CHAINID_TEST13 || 'test-13' }} + - name: test12 + remotes: ${{ vars.REMOTES_TEST12 || 'https://rpc.test12.testnets.gno.land' }} + chainid: ${{ vars.CHAINID_TEST12 || 'test12' }} + + steps: + - uses: actions/checkout@v4 + + - name: Run repeatable tests + if: > + github.event_name != 'workflow_dispatch' || + (inputs.test_type == 'repeatable' || inputs.test_type == 'both') && ( + (matrix.network.name == 'test-13' && inputs.run_test13 == true) || + (matrix.network.name == 'test12' && inputs.run_test12 == true) + ) + run: | + make tests-repeatable \ + REMOTES="${{ matrix.network.remotes }}" \ + CHAINID=${{ matrix.network.chainid }} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..637ce6e --- /dev/null +++ b/Makefile @@ -0,0 +1,61 @@ +.PHONY: tests-one-shot tests-repeatable help + +comma := , +REMOTES ?= +REMOTE ?= $(if $(REMOTES),$(firstword $(subst $(comma), ,$(REMOTES))),https://rpc.test-13-aeddi-1.gnoland.network) +CHAINID ?= test-13 +FUNDER_SCRIPT ?= ./funders/gnoland.sh +FUNDER_MNEMONIC ?= source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast +GNOKEY_IMAGE ?= ghcr.io/gnolang/gno/gnokey:master + +export FUNDER_MNEMONIC + +# Contributor subdirectories are detected automatically. +CONTRIB_DIRS := $(filter-out _%, $(patsubst %/Makefile,%,$(wildcard */Makefile))) + +# Run the funder script inside a gnokey Docker container. +# No local gnokey installation required — compatible with GitHub Actions. +define run-funder + docker run --rm \ + -e REMOTE=$(REMOTE) \ + -e CHAINID=$(CHAINID) \ + -e FUNDER_MNEMONIC \ + -v "$(CURDIR)/funders:/funders:ro" \ + --entrypoint /bin/sh \ + $(GNOKEY_IMAGE) \ + /funders/$(notdir $(FUNDER_SCRIPT)) $(1) +endef + +## tests-one-shot : fund accounts then run one-shot tests (REMOTES, CHAINID) +tests-one-shot: + @for dir in $(CONTRIB_DIRS); do \ + echo ""; \ + echo "==> $$dir — funding (one-shot)"; \ + ARGS=$$($(MAKE) -C $$dir list-funding-one-shot --no-print-directory \ + REMOTE=$(REMOTE) REMOTES=$(REMOTES) CHAINID=$(CHAINID)); \ + if [ -n "$$ARGS" ]; then \ + $(call run-funder,$$ARGS) || exit 1; \ + fi; \ + echo "==> $$dir — tests (one-shot)"; \ + $(MAKE) -C $$dir tests-one-shot --no-print-directory \ + REMOTE=$(REMOTE) REMOTES=$(REMOTES) CHAINID=$(CHAINID) || exit 1; \ + done + +## tests-repeatable : fund accounts then run repeatable tests (REMOTES, CHAINID) +tests-repeatable: + @for dir in $(CONTRIB_DIRS); do \ + echo ""; \ + echo "==> $$dir — funding (repeatable)"; \ + ARGS=$$($(MAKE) -C $$dir list-funding-repeatable --no-print-directory \ + REMOTE=$(REMOTE) CHAINID=$(CHAINID)); \ + if [ -n "$$ARGS" ]; then \ + $(call run-funder,$$ARGS) || exit 1; \ + fi; \ + echo "==> $$dir — tests (repeatable)"; \ + $(MAKE) -C $$dir tests-repeatable --no-print-directory \ + REMOTE=$(REMOTE) CHAINID=$(CHAINID) || exit 1; \ + done + +## help : show available targets +help: + @grep -E '^## ' Makefile | sed 's/## / /' diff --git a/README.md b/README.md index 1799e7c..82dec15 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,141 @@ # community-scripts -A collection of various community scripts and tests that will eventually become mainstream + +A collection of community-contributed test suites for [gnoland](https://gno.land) chains. + +## Purpose + +This repository lets contributors package their own tests and run them against any gnoland network via its RPC endpoint — no local node required. The goal is to provide a shared, extensible testing framework that can be wired into CI and executed against public testnets (e.g. `test-13`) or any custom deployment. + +## Structure + +```text +community-scripts/ +├── Makefile # root orchestrator +├── funders/ +│ └── gnoland.sh # funds test accounts from test1 (works on any gnoland network) +├── _template/ +│ └── Makefile # copy-paste template for new contributors +└── / + ├── Makefile # exposes the 4 required rules (see below) + └── Dockerfile # self-contained test runner (any language) +``` + +## Makefile interface + +Every contributor subdirectory must expose these four rules: + +| Rule | Description | +| ------------------------- | ------------------------------------------------------------------- | +| `list-funding-one-shot` | Prints `address amount` pairs to fund before one-shot tests | +| `list-funding-repeatable` | Prints `address amount` pairs to fund before repeatable tests | +| `tests-one-shot` | Runs tests that deploy on-chain state (realm deploys...) | +| `tests-repeatable` | Runs tests that can be re-executed safely | + +All rules accept `REMOTES` (comma-separated RPC list) and `CHAINID` variables. +`REMOTE` is automatically derived from the first entry in `REMOTES`. + +Before each run, the root Makefile calls `list-funding-*`, passes the returned +addresses to the funder script (test1), then runs the tests. + +Run `make help` from any directory to list available targets. + +## Running tests + +Against test-13 (default): + +```sh +make tests-one-shot +make tests-repeatable +``` + +Against a single custom RPC: + +```sh +make tests-one-shot REMOTES=https://rpc.test12.testnets.gno.land CHAINID=test12 +``` + +Against multiple validator nodes (stress tests will hit each one): + +```sh +make tests-one-shot \ + REMOTES=https://rpc1.gnoland.network,https://rpc2.gnoland.network,https://rpc3.gnoland.network \ + CHAINID=test-13 +``` + +With a custom funder script: + +```sh +make tests-one-shot FUNDER_SCRIPT=./funders/gnoland.sh REMOTES=... CHAINID=test-13 +``` + +Directly from a contributor subdirectory: + +```sh +cd samourai-crew +make help +make tests-one-shot REMOTES=https://rpc.test12.testnets.gno.land CHAINID=test12 +``` + +## Adding your own tests + +### 1. Create your directory + +```sh +cp -r _template my-name +``` + +### 2. Generate a testnet keypair + +Generate a dedicated testnet account for your tests (no real value): + +```sh +gnokey generate # save the mnemonic +gnokey add my-test-account -recover +``` + +### 3. Edit the Makefile + +Declare your test account address and funding amounts: + +```makefile +ADDR_1 := g1your_address_here + +FUND_AMOUNT_ONE_SHOT := 30000000ugnot # ~30 transactions at 1M ugnot each +FUND_AMOUNT_REPEATABLE := 10000000ugnot + +list-funding-one-shot: + @echo "$(ADDR_1) $(FUND_AMOUNT_ONE_SHOT)" + +list-funding-repeatable: + @echo "$(ADDR_1) $(FUND_AMOUNT_REPEATABLE)" +``` + +**Multiple wallets:** declare all addresses in `list-funding-*` as space-separated +`address amount` pairs. The funder will fund each one before the tests run. + +### 4. Write your Dockerfile + +Your `Dockerfile` must: + +- Accept `one-shot` or `repeatable` as a command argument +- Contain your test account mnemonic (testnet key, no real value) +- Read `REMOTE`, `REMOTES`, and `CHAINID` from env +- Sign the network CLA if required (see `samourai-crew/run_tests.sh` for an example) + +The image can use **any language** (shell, Go, Python, etc.). See `samourai-crew/` for a shell-based example. + +### 5. What your container receives at runtime + +| Variable | Description | +| --------- | --------------------------------------------------- | +| `REMOTE` | Primary RPC endpoint (first entry of `REMOTES`) | +| `REMOTES` | Comma-separated list of RPC endpoints | +| `CHAINID` | Chain ID | + +The funding has already been done by the time your container starts. + +## Current contributors + +| Directory | Description | +| --------------- | ------------------------------------------------------------------ | +| `samourai-crew` | GnoVM audit scripts, E2E transaction tests, and sybil stress tests | diff --git a/_template/Makefile b/_template/Makefile new file mode 100644 index 0000000..1848874 --- /dev/null +++ b/_template/Makefile @@ -0,0 +1,47 @@ +.PHONY: list-funding-one-shot list-funding-repeatable tests-one-shot tests-repeatable build help + +# Override REMOTES and CHAINID to target a live network: +# make tests-one-shot REMOTES=https://rpc.test-13-aeddi-1.gnoland.network CHAINID=test-13 +comma := , +REMOTES ?= +REMOTE ?= $(if $(REMOTES),$(firstword $(subst $(comma), ,$(REMOTES))),http://127.0.0.1:26657) +CHAINID ?= test +IMAGE := $(shell basename $(CURDIR)) + +# TODO: replace with your testnet account address(es) +ADDR_1 := TODO_REPLACE_ADDR_1 + +# Funding amounts — adjust to match your tests' gas needs (~1M ugnot per tx) +FUND_AMOUNT_ONE_SHOT := 30000000ugnot +FUND_AMOUNT_REPEATABLE := 10000000ugnot + +## list-funding-one-shot : print address/amount pairs to fund before one-shot tests +list-funding-one-shot: + @echo "$(ADDR_1) $(FUND_AMOUNT_ONE_SHOT)" + +## list-funding-repeatable : print address/amount pairs to fund before repeatable tests +list-funding-repeatable: + @echo "$(ADDR_1) $(FUND_AMOUNT_REPEATABLE)" + +## build : build the Docker test image +build: + docker build -t $(IMAGE) . + +## tests-one-shot : run one-shot tests (REMOTES, CHAINID) +tests-one-shot: build + docker run --rm \ + -e REMOTE=$(REMOTE) \ + -e REMOTES=$(REMOTES) \ + -e CHAINID=$(CHAINID) \ + $(IMAGE) one-shot + +## tests-repeatable : run repeatable tests (REMOTES, CHAINID) +tests-repeatable: build + docker run --rm \ + -e REMOTE=$(REMOTE) \ + -e CHAINID=$(CHAINID) \ + $(IMAGE) repeatable + +## help : show available targets +help: + @grep -E '^## ' Makefile | sed 's/## / /' diff --git a/funders/gnoland.sh b/funders/gnoland.sh new file mode 100755 index 0000000..a91cc3a --- /dev/null +++ b/funders/gnoland.sh @@ -0,0 +1,68 @@ +#!/bin/sh +# Funds a list of address/amount pairs from the test1 faucet on test-13. +# Usage: test-13.sh [ ...] +# +# Required env: +# REMOTE — RPC endpoint (default: https://rpc.test-13-aeddi-1.gnoland.network) +# CHAINID — chain ID (default: test-13) +# FUNDER_MNEMONIC — test1 mnemonic (default: public test1 mnemonic) + +REMOTE="${REMOTE:-https://rpc.test-13-aeddi-1.gnoland.network}" +CHAINID="${CHAINID:-test-13}" +FUNDER_MNEMONIC="${FUNDER_MNEMONIC:-source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast}" +PASSWORD="test1234" +GNOKEY_HOME="${GNOKEY_HOME:-/tmp/gnokey-funder}" +FUNDER_KEY="funder" + +if [ "$#" -eq 0 ] || [ $(( $# % 2 )) -ne 0 ]; then + echo "Usage: $0 [ ...]" + exit 1 +fi + +mkdir -p "$GNOKEY_HOME" +if ! gnokey list -home "$GNOKEY_HOME" 2>/dev/null | grep -q "$FUNDER_KEY"; then + printf "%s\n%s\n%s\n" "$FUNDER_MNEMONIC" "$PASSWORD" "$PASSWORD" | \ + gnokey add "$FUNDER_KEY" -recover -insecure-password-stdin=true \ + -home "$GNOKEY_HOME" > /dev/null 2>&1 +fi + +FAILED=0 +while [ "$#" -ge 2 ]; do + ADDR="$1" + AMOUNT="$2" + shift 2 + + # Send only what is missing — top up to the needed amount + NEEDED=$(echo "$AMOUNT" | grep -o '^[0-9]*') + CURRENT=$(gnokey query bank/balances/"$ADDR" -remote "$REMOTE" 2>/dev/null \ + | grep -o '[0-9]*ugnot' | grep -o '^[0-9]*') + CURRENT="${CURRENT:-0}" + if [ "$CURRENT" -ge "$NEEDED" ]; then + echo "Funding $ADDR ... SKIP (balance ${CURRENT}ugnot >= ${NEEDED}ugnot)" + continue + fi + TOPUP=$(( NEEDED - CURRENT )) + + echo -n "Funding $ADDR with ${TOPUP}ugnot (top-up to ${NEEDED}ugnot) ... " + OUT=$(echo "$PASSWORD" | gnokey maketx send \ + -to "$ADDR" \ + -send "${TOPUP}ugnot" \ + -gas-fee 1000000ugnot \ + -gas-wanted 2000000 \ + -broadcast \ + -chainid "$CHAINID" \ + -remote "$REMOTE" \ + -insecure-password-stdin=true \ + -home "$GNOKEY_HOME" \ + "$FUNDER_KEY" 2>&1) + if [ $? -eq 0 ]; then + echo "OK" + else + echo "FAILED" + echo "$OUT" >&2 + FAILED=$((FAILED + 1)) + fi +done + +[ "$FAILED" -gt 0 ] && echo "$FAILED address(es) could not be funded." && exit 1 +echo "All addresses funded." diff --git a/samourai-crew/Dockerfile b/samourai-crew/Dockerfile new file mode 100644 index 0000000..08c580e --- /dev/null +++ b/samourai-crew/Dockerfile @@ -0,0 +1,36 @@ +FROM ghcr.io/gnolang/gno/gnokey:master + +RUN apk add --no-cache bash jq + +WORKDIR /tests + +COPY audit/ audit/ +COPY e2e/ e2e/ +COPY stress/ stress/ +COPY realms/ realms/ +COPY run_tests.sh . + +RUN chmod +x run_tests.sh \ + && find audit e2e stress -name "*.sh" -exec chmod +x {} + + +ENV REMOTE=http://127.0.0.1:26657 +ENV CHAINID=test +ENV REMOTES= + +# 3 wallets — wallet 1 is both the runner (audit/e2e) and stress_1 (first RPC) +# TODO: replace all values below with your actual mnemonics and addresses + +ENV STRESS_MNEMONIC_1="chair require about ask exhaust you already finger shop turn glory spare credit april rose sniff whale news economy birth table trim raccoon grit" +ENV STRESS_ADDR_1="g1hzlg063fqrq4gltql992ssjc0xzau89t5jp63w" + +ENV STRESS_MNEMONIC_2="knock hat consider bicycle kit lion all cover lawn humble picnic win exit never message leader brother reform silk butter private protect tuition draw" +ENV STRESS_ADDR_2="g174tsxfpf8zj8h3tyrz4ld690xvhcjnquls6ffc" + +ENV STRESS_MNEMONIC_3="galaxy fire athlete egg three crane stone borrow thought cover story poem blush scissors pause slice unusual spread jewel visual tail parent bargain occur" +ENV STRESS_ADDR_3="g19xnaenyhe88emmge4726ta43lp3n237vvuzc2n" + +# runner = wallet 1 +ENV RUNNER_MNEMONIC="${STRESS_MNEMONIC_1}" +ENV RUNNER_ADDR="${STRESS_ADDR_1}" + +ENTRYPOINT ["/bin/sh", "/tests/run_tests.sh"] diff --git a/samourai-crew/Makefile b/samourai-crew/Makefile new file mode 100644 index 0000000..737e29d --- /dev/null +++ b/samourai-crew/Makefile @@ -0,0 +1,48 @@ +.PHONY: list-funding-one-shot list-funding-repeatable tests-one-shot tests-repeatable build help + +comma := , +REMOTES ?= +REMOTE ?= $(if $(REMOTES),$(firstword $(subst $(comma), ,$(REMOTES))),http://127.0.0.1:26657) +CHAINID ?= test +IMAGE := $(shell basename $(CURDIR)) + +# 3 wallets — wallet 1 is both the runner (audit/e2e) and stress_1 (first RPC) +ADDR_1 := g1hzlg063fqrq4gltql992ssjc0xzau89t5jp63w +ADDR_2 := g174tsxfpf8zj8h3tyrz4ld690xvhcjnquls6ffc +ADDR_3 := g19xnaenyhe88emmge4726ta43lp3n237vvuzc2n + +# wallet 1 gets more funds: covers audit/e2e + stress role +FUND_AMOUNT_WALLET_1 := 50000000ugnot +FUND_AMOUNT_PER_WALLET := 15000000ugnot +FUND_AMOUNT_REPEATABLE := 10000000ugnot + +## list-funding-one-shot : print addresses and amounts to fund before one-shot tests +list-funding-one-shot: + @echo "$(ADDR_1) $(FUND_AMOUNT_WALLET_1) $(ADDR_2) $(FUND_AMOUNT_PER_WALLET) $(ADDR_3) $(FUND_AMOUNT_PER_WALLET)" + +## list-funding-repeatable : print addresses and amounts to fund before repeatable tests +list-funding-repeatable: + @echo "$(ADDR_1) $(FUND_AMOUNT_REPEATABLE)" + +## build : build the Docker test image +build: + docker build -t $(IMAGE) . + +## tests-one-shot : run one-shot tests (REMOTES, CHAINID) +tests-one-shot: build + docker run --rm \ + -e REMOTE=$(REMOTE) \ + -e CHAINID=$(CHAINID) \ + -e REMOTES=$(REMOTES) \ + $(IMAGE) one-shot + +## tests-repeatable : run repeatable tests (REMOTES, CHAINID) +tests-repeatable: build + docker run --rm \ + -e REMOTE=$(REMOTE) \ + -e CHAINID=$(CHAINID) \ + $(IMAGE) repeatable + +## help : show available targets +help: + @grep -E '^## ' Makefile | sed 's/## / /' diff --git a/samourai-crew/README.md b/samourai-crew/README.md new file mode 100644 index 0000000..e79b8ab --- /dev/null +++ b/samourai-crew/README.md @@ -0,0 +1,68 @@ +# misc/e2e — End-to-End Test Suite + +Docker-based E2E test suite for gnoland. Tests run against a single local +validator node and are executed automatically by `make test`. + +## Running + +```sh +cd misc/e2e +make test # build images, start node, run all tests +make clean # tear down containers and volumes +make logs # stream container logs +``` + +## Structure + +``` +misc/e2e/ +├── run_tests.sh # main entrypoint called by docker-compose +├── docker-compose.yml # spins up gnoland + gnokey-test containers +├── audit/ +│ ├── common.sh # shared config: RPC, chainid, key setup +│ └── audit_*.sh # one script per gnovm fix (see below) +└── e2e/ + └── e2e_*.sh # end-to-end transaction and consensus tests +``` + +## Audit scripts (`audit/`) + +Each script targets a specific gnovm bugfix and verifies it is present in +the binary. Scripts exit 0 on ✅ PATCHED and exit 1 on ❌ VULNERABLE. + +| Script | Fix | What it verifies | +| --- | --- | --- | +| `audit_runtime_pkg.sh` | `afd7e4808` | `runtime` import rejected in production VM | +| `audit_chan_type.sh` | `4bcd9828e` | `chan` type rejected at preprocess, not at runtime | +| `audit_security.sh` | `6a6fc4c71` + `3be0408f0` | uint64 overflow caught at compile time; infinite recursion stopped by gas limit | +| `audit_gas_alloc.sh` | `5d5f9213f` | large allocations consume gas proportionally (per-byte model) | +| `audit_byteslice.sh` | `a3a356e71` | byte-slice index mutations persist across transactions | +| `audit_array_alias.sh` | `c64feef1d` | array copy produces independent memory (no pointer aliasing) | +| `audit_var_init_order.sh` | `50ee56e64` | package-level vars initialized in dependency order | +| `audit_cross_realm_recover.sh` | `f87249327` | full state rollback when a realm panics and recover() is called | + +## E2E scripts (`e2e/`) + +| Script | What it verifies | +| --- | --- | +| `e2e_nonce_replay.sh` | Replaying a tx with an already-consumed sequence number is rejected | +| `e2e_counter.sh` | Deploy a realm, increment state, verify committed value | +| `e2e_mempool_stress.sh` | 10 sequential txs accepted without error; final state matches expected count | + +## Shared config (`audit/common.sh`) + +All scripts source `audit/common.sh` which sets: + +| Variable | Default | Description | +| --- | --- | --- | +| `RPC` | `http://gnoland:26657` | Node RPC endpoint | +| `CHAINID` | `test` | Chain ID | +| `KEY` | `test1` | Gnokey account name | +| `PASSWORD` | `test1234` | Key password | +| `GNOKEY_HOME` | `/tmp/gnokey` | Gnokey home directory | +| `KEY_ADDR` | `g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5` | Deterministic address for `test1` | + +Override any variable via environment: +```sh +RPC=http://localhost:26657 CHAINID=test-13 ./audit/audit_security.sh +``` diff --git a/samourai-crew/audit/audit_array_alias.sh b/samourai-crew/audit/audit_array_alias.sh new file mode 100755 index 0000000..10f45c7 --- /dev/null +++ b/samourai-crew/audit/audit_array_alias.sh @@ -0,0 +1,84 @@ +#!/bin/sh +# Targets: fix(gnovm): deep-copy array elements in ArrayValue.Copy (c64feef1d) +# Verifies that local := arr produces an independent copy. +# Without the fix, the local variable aliased the stored array pointer, +# so modifying the copy silently corrupted the original persistent state. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=common.sh +. "$SCRIPT_DIR/common.sh" + +SUFFIX=$(date +%s) +PKGPATH="gno.land/r/${KEY_ADDR}/audit/arrayalias${SUFFIX}" +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "🧪 c64feef1d — Array copy independence" +echo " Package: $PKGPATH" + +cat > "$TMPDIR/arrayalias.gno" << EOF +package arrayalias + +import "strconv" + +var arr [3]int + +func ModifyLocalCopy() { + local := arr + local[0] = 999 +} + +func Render(_ string) string { + return strconv.Itoa(arr[0]) +} +EOF + +cat > "$TMPDIR/gnomod.toml" << EOF +module = "${PKGPATH}" +gno = "0.9" +EOF + +echo -n " Deploying 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 + +cat > "$TMPDIR/call.gno" << EOF +package main + +import a "${PKGPATH}" + +func main() { a.ModifyLocalCopy() } +EOF + +echo -n " Calling ModifyLocalCopy()... " +CALL=$(echo "$PASSWORD" | gnokey maketx run \ + -gas-fee 1000000ugnot -gas-wanted 5000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" "$TMPDIR/call.gno" 2>&1) +if echo "$CALL" | grep -q "OK!"; then echo "OK"; else + echo "FAILED"; echo "$CALL"; exit 1 +fi + +echo -n " Querying arr[0] (expect 0)... " +RESULT=$(gnokey query "vm/qeval" \ + -data "${PKGPATH}.Render(\"\")" \ + -remote "$RPC" 2>&1) + +if echo "$RESULT" | grep -q '"0"'; then + echo "✅ PATCHED — arr[0] unchanged after copy modification" +elif echo "$RESULT" | grep -q '"999"'; then + echo "❌ VULNERABLE — arr[0] aliased and corrupted to 999" + exit 1 +else + echo "⚠️ UNKNOWN OUTPUT"; echo "$RESULT"; exit 1 +fi diff --git a/samourai-crew/audit/audit_byteslice.sh b/samourai-crew/audit/audit_byteslice.sh new file mode 100755 index 0000000..2be5ef0 --- /dev/null +++ b/samourai-crew/audit/audit_byteslice.sh @@ -0,0 +1,90 @@ +#!/bin/sh +# Targets: fix(gnovm): call DidUpdate on DataByte index assignment (NEWTENDG-98, a3a356e71) +# Verifies that bs[i] = v mutations persist across transactions. +# Without the fix, byte-slice index writes were silently dropped after tx commit. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=common.sh +. "$SCRIPT_DIR/common.sh" + +SUFFIX=$(date +%s) +PKGPATH="gno.land/r/${KEY_ADDR}/audit/byteslice${SUFFIX}" +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "🧪 NEWTENDG-98 — Byte-slice index mutation persistence" +echo " Package: $PKGPATH" + +cat > "$TMPDIR/byteslice.gno" << EOF +package byteslice + +import "strconv" + +type ByteState struct { + data []byte +} + +func (b *ByteState) set(i int, v byte) { + b.data[i] = v +} + +var state = ByteState{data: []byte{0, 0, 0}} + +func SetFirst(v byte) { + state.set(0, v) +} + +func Render(_ string) string { + return strconv.Itoa(int(state.data[0])) +} +EOF + +cat > "$TMPDIR/gnomod.toml" << EOF +module = "${PKGPATH}" +gno = "0.9" +EOF + +echo -n " Deploying 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 + +cat > "$TMPDIR/set.gno" << EOF +package main + +import byteslice "${PKGPATH}" + +func main() { byteslice.SetFirst(5) } +EOF + +echo -n " Setting bs[0] = 5... " +SET=$(echo "$PASSWORD" | gnokey maketx run \ + -gas-fee 1000000ugnot -gas-wanted 5000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" "$TMPDIR/set.gno" 2>&1) +if echo "$SET" | grep -q "OK!"; then echo "OK"; else + echo "FAILED"; echo "$SET"; exit 1 +fi + +echo -n " Querying bs[0] (expect 5)... " +RESULT=$(gnokey query "vm/qeval" \ + -data "${PKGPATH}.Render(\"\")" \ + -remote "$RPC" 2>&1) + +if echo "$RESULT" | grep -q '"5"'; then + echo "✅ PATCHED — bs[0] = 5 persisted correctly" +elif echo "$RESULT" | grep -q '"0"'; then + echo "❌ VULNERABLE — bs[0] mutation was silently dropped (still 0)" + exit 1 +else + echo "⚠️ UNKNOWN OUTPUT"; echo "$RESULT"; exit 1 +fi diff --git a/samourai-crew/audit/audit_chan_type.sh b/samourai-crew/audit/audit_chan_type.sh new file mode 100755 index 0000000..6909f0a --- /dev/null +++ b/samourai-crew/audit/audit_chan_type.sh @@ -0,0 +1,39 @@ +#!/bin/sh +# Targets: fix(gnovm): reject chan type at preprocess/runtime (4bcd9828e) +# Verifies that chan types are rejected at preprocess time (before execution). +# Without the fix, deployment succeeded and the node panicked only at runtime +# when the channel was actually used. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=common.sh +. "$SCRIPT_DIR/common.sh" + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "🧪 4bcd9828e — chan type rejection at preprocess" + +cat > "$TMPDIR/chan.gno" << 'EOF' +package main + +func main() { + ch := make(chan int, 1) + ch <- 42 +} +EOF + +echo -n " Submitting script with chan type... " +RESULT=$(echo "$PASSWORD" | gnokey maketx run \ + -gas-fee 1000000ugnot \ + -gas-wanted 5000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" "$TMPDIR/chan.gno" 2>&1) + +if echo "$RESULT" | grep -q "OK!"; then + echo "FAIL: chan type accepted by the VM (VULNERABLE)" + exit 1 +else + echo "PASS: chan type rejected (PATCHED)" +fi diff --git a/samourai-crew/audit/audit_cross_realm_recover.sh b/samourai-crew/audit/audit_cross_realm_recover.sh new file mode 100755 index 0000000..984bfaa --- /dev/null +++ b/samourai-crew/audit/audit_cross_realm_recover.sh @@ -0,0 +1,93 @@ +#!/bin/sh +# Targets: fix(gnovm): prevent cross-realm state corruption via NameExpr assign+recover (f87249327) +# Verifies that a realm function that writes state then panics causes a full rollback, +# even when the calling script catches the panic with recover(). +# Without the fix, the state write was not undone and partial state remained. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=common.sh +. "$SCRIPT_DIR/common.sh" + +SUFFIX=$(date +%s) +PKGPATH="gno.land/r/${KEY_ADDR}/audit/realmrecov${SUFFIX}" +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "🧪 f87249327 — State rollback on panic + recover()" +echo " Package: $PKGPATH" + +cat > "$TMPDIR/realmrecov.gno" << EOF +package realmrecov + +import "strconv" + +type StateHolder struct { + value int +} + +func (s *StateHolder) set(v int) { + s.value = v +} + +var holder = StateHolder{value: 0} + +func SetAndPanic(v int) { + holder.set(v) + panic("deliberate panic after state write") +} + +func Render(_ string) string { + return strconv.Itoa(holder.value) +} +EOF + +cat > "$TMPDIR/gnomod.toml" << EOF +module = "${PKGPATH}" +gno = "0.9" +EOF + +echo -n " Deploying 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 + +cat > "$TMPDIR/recover.gno" << EOF +package main + +import r "${PKGPATH}" + +func main() { + defer func() { recover() }() + r.SetAndPanic(100) +} +EOF + +echo -n " Calling SetAndPanic(100) with recover()... " +CALL=$(echo "$PASSWORD" | gnokey maketx run \ + -gas-fee 1000000ugnot -gas-wanted 5000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" "$TMPDIR/recover.gno" 2>&1) +echo "$(echo "$CALL" | grep -oE 'OK!|error' | head -1)" + +echo -n " Querying State (expect 0)... " +RESULT=$(gnokey query "vm/qeval" \ + -data "${PKGPATH}.Render(\"\")" \ + -remote "$RPC" 2>&1) + +if echo "$RESULT" | grep -q '"0"'; then + echo "✅ PATCHED — State rolled back to 0 after panic" +elif echo "$RESULT" | grep -q '"100"'; then + echo "❌ VULNERABLE — State corrupted to 100 despite panic" + exit 1 +else + echo "⚠️ UNKNOWN OUTPUT"; echo "$RESULT"; exit 1 +fi diff --git a/samourai-crew/audit/audit_gas_alloc.sh b/samourai-crew/audit/audit_gas_alloc.sh new file mode 100755 index 0000000..0de18ed --- /dev/null +++ b/samourai-crew/audit/audit_gas_alloc.sh @@ -0,0 +1,70 @@ +#!/bin/sh +# Targets: fix(gnovm): proper gas consumption for mem allocation (5d5f9213f) +# Verifies that large memory allocations consume gas proportionally (per-byte model). +# Without the fix, all allocations used a flat fee — a 10MB alloc cost the same as +# a 10-byte alloc, making large-alloc DoS attacks virtually free. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=common.sh +. "$SCRIPT_DIR/common.sh" + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "🧪 5d5f9213f — Per-byte gas consumption for memory allocation" + +# Test 1: large alloc with low gas-wanted must hit OOG +cat > "$TMPDIR/bigalloc.gno" << 'EOF' +package main + +func main() { + _ = make([]byte, 10_000_000) +} +EOF + +echo -n " 10MB alloc with 100k gas (expect OOG)... " +RESULT=$(echo "$PASSWORD" | gnokey maketx run \ + -gas-fee 1000000ugnot \ + -gas-wanted 100000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" "$TMPDIR/bigalloc.gno" 2>&1) + +if echo "$RESULT" | grep -qiE "out of gas|gas limit|exceeded"; then + echo "✅ OOG triggered — per-byte gas model active" +elif echo "$RESULT" | grep -q "OK!"; then + echo "❌ VULNERABLE — 10MB alloc passed with 100k gas (flat-fee model)" + exit 1 +else + echo "⚠️ UNKNOWN OUTPUT"; echo "$RESULT" + exit 1 +fi + +# Test 2: small alloc with sufficient gas must succeed +cat > "$TMPDIR/smallalloc.gno" << 'EOF' +package main + +func main() { + _ = make([]byte, 10) +} +EOF + +echo -n " 10-byte alloc with 1M gas (expect OK)... " +RESULT2=$(echo "$PASSWORD" | gnokey maketx run \ + -gas-fee 1000000ugnot \ + -gas-wanted 1000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" "$TMPDIR/smallalloc.gno" 2>&1) + +if echo "$RESULT2" | grep -q "OK!"; then + echo "✅ Small alloc passed" +elif echo "$RESULT2" | grep -qiE "out of gas"; then + echo "⚠️ Small alloc hit OOG — raise gas-wanted threshold for this test" + exit 1 +else + echo "⚠️ UNKNOWN OUTPUT"; echo "$RESULT2" + exit 1 +fi diff --git a/samourai-crew/audit/audit_runtime_pkg.sh b/samourai-crew/audit/audit_runtime_pkg.sh new file mode 100755 index 0000000..86f0261 --- /dev/null +++ b/samourai-crew/audit/audit_runtime_pkg.sh @@ -0,0 +1,45 @@ +#!/bin/sh +# Targets: feat(gnovm)!: move runtime to testing stdlibs (afd7e4808) +# Verifies that importing "runtime" in a production script is rejected. +# The runtime package (GC, MemStats, etc.) has no legitimate on-chain use +# and was removed from production stdlibs. Any deployed realm importing it +# would fail replay after the hardfork. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=common.sh +. "$SCRIPT_DIR/common.sh" + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "🧪 afd7e4808 — runtime stdlib removed from production" + +cat > "$TMPDIR/runtime.gno" << 'EOF' +package main + +import "runtime" + +func main() { + runtime.GC() +} +EOF + +echo -n " Submitting script importing \"runtime\"... " +RESULT=$(echo "$PASSWORD" | gnokey maketx run \ + -gas-fee 1000000ugnot \ + -gas-wanted 5000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" "$TMPDIR/runtime.gno" 2>&1) +EXIT_CODE=$? + +if echo "$RESULT" | grep -q "OK!"; then + echo "❌ VULNERABLE — runtime.GC() executed in production VM" + exit 1 +elif [ $EXIT_CODE -ne 0 ]; then + echo "✅ PATCHED — runtime import rejected" +else + echo "⚠️ UNKNOWN OUTPUT"; echo "$RESULT" + exit 1 +fi diff --git a/samourai-crew/audit/audit_security.sh b/samourai-crew/audit/audit_security.sh new file mode 100755 index 0000000..1c3508f --- /dev/null +++ b/samourai-crew/audit/audit_security.sh @@ -0,0 +1,72 @@ +#!/bin/sh +# Targets: fix(gnovm): uint64 overflow at compile time (NEWTENDG-164, 6a6fc4c71) +# fix(gnovm): iterative stack-overflow recovery (NEWTENDG-182, 3be0408f0) +# Verifies that uint64 constant overflow is caught at compile time and that +# infinite recursion is stopped by the gas limit rather than crashing the node. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=common.sh +. "$SCRIPT_DIR/common.sh" + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "🛡️ STARTING GNOVM SECURITY AUDIT..." +echo "------------------------------------" + +# --- TEST 1: INTEGER OVERFLOW --- +cat > "$TMPDIR/ovf.gno" << 'EOF' +package main +func main() { + const huge = 18446744073709551615 + 1 + println(huge) +} +EOF + +echo -n "🧪 Testing Tier 1 (Integer Overflow, 6a6fc4c71)... " +RESULT_OVF=$(echo "$PASSWORD" | gnokey maketx run \ + -broadcast -remote "$RPC" -chainid "$CHAINID" \ + -gas-fee 1000000ugnot -gas-wanted 2000000 \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" "$TMPDIR/ovf.gno" 2>&1) + +if echo "$RESULT_OVF" | grep -qiE "overflows|cannot use huge"; then + echo "✅ PATCHED" +else + echo "❌ VULNERABLE" + echo "$RESULT_OVF" | grep "Error" | head -n 5 + exit 1 +fi + +# --- TEST 2: STACK RECURSION --- +cat > "$TMPDIR/kami.gno" << 'EOF' +package main +func main() { + Recursive() +} +func Recursive() { + Recursive() +} +EOF + +echo -n "🧪 Testing Tier 1 (Stack Recursion, 3be0408f0)... " +RESULT_KAM=$(echo "$PASSWORD" | gnokey maketx run \ + -broadcast -remote "$RPC" -chainid "$CHAINID" \ + -gas-fee 1000000ugnot -gas-wanted 5000000 \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" "$TMPDIR/kami.gno" 2>&1) + +if echo "$RESULT_KAM" | grep -qi "out of gas"; then + echo "✅ PATCHED (Gas limit hit)" +elif echo "$RESULT_KAM" | grep -qi "stack overflow"; then + echo "✅ PATCHED (Stack limit hit)" +else + echo "❌ CRITICAL" + echo "$RESULT_KAM" | grep "Error" | head -n 5 + exit 1 +fi + +echo "------------------------------------" +echo "🏁 Audit Complete." diff --git a/samourai-crew/audit/audit_var_init_order.sh b/samourai-crew/audit/audit_var_init_order.sh new file mode 100755 index 0000000..32168c9 --- /dev/null +++ b/samourai-crew/audit/audit_var_init_order.sh @@ -0,0 +1,61 @@ +#!/bin/sh +# Targets: fix(gnovm): implement Go-compliant variable initialization order (NEWTENDG-68, 50ee56e64) +# Verifies that package-level vars are initialized in dependency order, not declaration order. +# In Go: var B = A + 1; var A = 2 → A is initialized first → B = 3. +# Without the fix, Gno initialized in declaration order → A = 0 when B was set → B = 1. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=common.sh +. "$SCRIPT_DIR/common.sh" + +SUFFIX=$(date +%s) +PKGPATH="gno.land/r/${KEY_ADDR}/audit/varinit${SUFFIX}" +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "🧪 NEWTENDG-68 — Package-level variable initialization order" +echo " Package: $PKGPATH" + +cat > "$TMPDIR/varinit.gno" << EOF +package varinit + +import "strconv" + +var B = A + 1 +var A = 2 + +func Render(_ string) string { + return strconv.Itoa(B) +} +EOF + +cat > "$TMPDIR/gnomod.toml" << EOF +module = "${PKGPATH}" +gno = "0.9" +EOF + +echo -n " Deploying realm (var B = A+1 declared before var A = 2)... " +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 + +echo -n " Querying B (expect 3, A initialized before B)... " +RESULT=$(gnokey query "vm/qeval" \ + -data "${PKGPATH}.Render(\"\")" \ + -remote "$RPC" 2>&1) + +if echo "$RESULT" | grep -q '"3"'; then + echo "✅ PATCHED — B = 3 (A was initialized before B)" +elif echo "$RESULT" | grep -q '"1"'; then + echo "❌ VULNERABLE — B = 1 (A was 0 when B was initialized)" + exit 1 +else + echo "⚠️ UNKNOWN OUTPUT"; echo "$RESULT"; exit 1 +fi diff --git a/samourai-crew/audit/common.sh b/samourai-crew/audit/common.sh new file mode 100755 index 0000000..f7bb49b --- /dev/null +++ b/samourai-crew/audit/common.sh @@ -0,0 +1,11 @@ +#!/bin/sh +# Shared config for audit/e2e scripts. +# KEY, PASSWORD, KEY_ADDR and GNOKEY_HOME are exported by run_tests.sh +# before any script is called. Defaults below are for standalone use only. + +RPC="${REMOTE:-${RPC:-http://127.0.0.1:26657}}" +CHAINID="${CHAINID:-test}" +KEY="${KEY:-runner}" +PASSWORD="${PASSWORD:-runner1234}" +GNOKEY_HOME="${GNOKEY_HOME:-/tmp/gnokey}" +KEY_ADDR="${KEY_ADDR:-}" diff --git a/samourai-crew/counter/counter.gno b/samourai-crew/counter/counter.gno new file mode 100644 index 0000000..26517bd --- /dev/null +++ b/samourai-crew/counter/counter.gno @@ -0,0 +1,25 @@ +package counter + +import "strconv" + +// On définit un type pour le stockage +type Counter struct { + Value int +} + +// On initialise l'instance +var c Counter + +// La fonction publique que tu appelles +func Increment() { + c.Inc() +} + +// La méthode qui fait le travail (c'est elle qui va "laver" le taint) +func (c *Counter) Inc() { + c.Value++ +} + +func Render(path string) string { + return "Compteur Samourai : " + strconv.Itoa(c.Value) +} diff --git a/samourai-crew/counter/gnomod.toml b/samourai-crew/counter/gnomod.toml new file mode 100644 index 0000000..2a7c284 --- /dev/null +++ b/samourai-crew/counter/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/test13/v1/counter" +gno = "0.9" diff --git a/samourai-crew/counter/txs/increment.gno b/samourai-crew/counter/txs/increment.gno new file mode 100644 index 0000000..997ee95 --- /dev/null +++ b/samourai-crew/counter/txs/increment.gno @@ -0,0 +1,7 @@ +package main + +import "gno.land/r/g19xnaenyhe88emmge4726ta43lp3n237vvuzc2n/v1/counter" + +func main() { + counter.Increment() +} diff --git a/samourai-crew/e2e/e2e_counter.sh b/samourai-crew/e2e/e2e_counter.sh new file mode 100755 index 0000000..e574885 --- /dev/null +++ b/samourai-crew/e2e/e2e_counter.sh @@ -0,0 +1,86 @@ +#!/bin/sh +# Tests cross-validator state consistency via a simple counter realm. +# Deploys a fresh counter realm, sends an Increment tx, then queries the +# node to verify the state was committed correctly. +# Note: in a multi-validator setup, query both nodes and assert equal state. + +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/counter${SUFFIX}" +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "🚀 E2E COUNTER TEST" + +# --- Deploy counter realm --- +cat > "$TMPDIR/counter.gno" << EOF +package counter + +import "strconv" + +type state struct{ count int } + +func (s *state) inc() { s.count++ } + +var counter state + +func Increment() { counter.inc() } + +func Render(_ string) string { + return strconv.Itoa(counter.count) +} +EOF + +cat > "$TMPDIR/gnomod.toml" << EOF +module = "${PKGPATH}" +gno = "0.9" +EOF + +echo -n " Deploying counter 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 + +# --- Increment tx --- +cat > "$TMPDIR/increment.gno" << EOF +package main + +import counter "${PKGPATH}" + +func main() { counter.Increment() } +EOF + +echo -n "➡️ Sending Increment tx... " +INC=$(echo "$PASSWORD" | gnokey maketx run \ + -gas-fee 1000000ugnot -gas-wanted 3000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" "$TMPDIR/increment.gno" 2>&1) +if echo "$INC" | grep -q "OK!"; then echo "OK"; else + echo "FAILED"; echo "$INC"; exit 1 +fi + +sleep 2 + +# --- Query and verify --- +echo -n "🔍 Querying counter state (expect 1)... " +RESULT=$(gnokey query "vm/qeval" \ + -data "${PKGPATH}.Render(\"\")" \ + -remote "$RPC" 2>&1) + +if echo "$RESULT" | grep -q '"1"'; then + echo "✅ E2E COUNTER OK — state = 1" +else + echo "❌ FAILED — unexpected state"; echo "$RESULT"; exit 1 +fi diff --git a/samourai-crew/e2e/e2e_mempool_stress.sh b/samourai-crew/e2e/e2e_mempool_stress.sh new file mode 100755 index 0000000..c4fec1e --- /dev/null +++ b/samourai-crew/e2e/e2e_mempool_stress.sh @@ -0,0 +1,96 @@ +#!/bin/sh +# Tests sequential mempool throughput by sending N increment transactions +# one after another without sleep. Verifies that all txs are accepted and +# the final counter value matches the expected count. + +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/counter${SUFFIX}" +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +TX_COUNT=10 + +echo "⚡ STARTING SEQUENTIAL STRESS TEST ($TX_COUNT tx)" + +# --- Deploy counter realm --- +cat > "$TMPDIR/counter.gno" << EOF +package counter + +import "strconv" + +type state struct{ count int } + +func (s *state) inc() { s.count++ } + +var counter state + +func Increment() { counter.inc() } + +func Render(_ string) string { + return strconv.Itoa(counter.count) +} +EOF + +cat > "$TMPDIR/gnomod.toml" << EOF +module = "${PKGPATH}" +gno = "0.9" +EOF + +echo -n " Deploying counter 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 + +# --- Increment tx file --- +cat > "$TMPDIR/increment.gno" << EOF +package main + +import counter "${PKGPATH}" + +func main() { counter.Increment() } +EOF + +# --- Sequential stress loop --- +FAILED=0 +for i in $(seq 1 $TX_COUNT); do + echo -n "➡️ Tx #$i: " + RESULT=$(echo "$PASSWORD" | gnokey maketx run \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -gas-fee 1000000ugnot -gas-wanted 3000000 \ + -insecure-password-stdin -quiet \ + -home "$GNOKEY_HOME" \ + "$KEY" "$TMPDIR/increment.gno" 2>&1) + if echo "$RESULT" | grep -q "OK!"; then + echo "✅ Sent" + else + echo "❌ Failed"; echo "$RESULT" + FAILED=$((FAILED + 1)) + fi +done + +echo "⏳ Waiting for final commit..." +sleep 5 + +FINAL=$(gnokey query "vm/qeval" \ + -data "${PKGPATH}.Render(\"\")" \ + -remote "$RPC" 2>&1) + +echo "🏁 Final Counter Value (raw): $FINAL" + +if echo "$FINAL" | grep -q "\"$TX_COUNT\"" && [ "$FAILED" -eq 0 ]; then + echo "✅ MEMPOOL STRESS OK — all $TX_COUNT txs committed" +else + echo "❌ FAILED — $FAILED tx errors, expected counter = $TX_COUNT" + exit 1 +fi diff --git a/samourai-crew/e2e/e2e_nonce_replay.sh b/samourai-crew/e2e/e2e_nonce_replay.sh new file mode 100755 index 0000000..d935b9f --- /dev/null +++ b/samourai-crew/e2e/e2e_nonce_replay.sh @@ -0,0 +1,64 @@ +#!/bin/sh +# Tests replay protection via sequence number enforcement. +# Verifies that rebroadcasting a transaction with an already-consumed sequence +# number is rejected with a sequence mismatch error. +# This is a baseline sanity check that underpins all Tier 1 consensus fixes. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=../audit/common.sh +. "$SCRIPT_DIR/../audit/common.sh" + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "🧪 Replay protection — sequence number enforcement" + +cat > "$TMPDIR/noop.gno" << 'EOF' +package main + +func main() {} +EOF + +# Tx 1: normal broadcast, auto-sequence (should succeed) +echo -n " Tx 1 — normal broadcast... " +TX1=$(echo "$PASSWORD" | gnokey maketx run \ + -gas-fee 1000000ugnot -gas-wanted 1000000 \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" "$TMPDIR/noop.gno" 2>&1) + +if echo "$TX1" | grep -q "OK!"; then + echo "OK" +else + echo "FAILED (unexpected)"; echo "$TX1"; exit 1 +fi + +# Derive consumed sequence from the account state +SEQ_INFO=$(gnokey query "auth/accounts/${KEY_ADDR}" -remote "$RPC" 2>&1) +CURRENT_SEQ=$(echo "$SEQ_INFO" | grep -oE '"sequence":"[0-9]+"' | grep -oE '[0-9]+$') +if [ -z "$CURRENT_SEQ" ] || [ "$CURRENT_SEQ" -eq 0 ]; then + REPLAY_SEQ=0 +else + REPLAY_SEQ=$((CURRENT_SEQ - 1)) +fi +echo " Current sequence: $CURRENT_SEQ — replaying with sequence: $REPLAY_SEQ" + +# Tx 2: replay with the already-used sequence number (must be rejected) +echo -n " Tx 2 — replay at sequence $REPLAY_SEQ (expect rejection)... " +TX2=$(echo "$PASSWORD" | gnokey maketx run \ + -gas-fee 1000000ugnot -gas-wanted 1000000 \ + -sequence "$REPLAY_SEQ" \ + -broadcast -chainid "$CHAINID" -remote "$RPC" \ + -insecure-password-stdin \ + -home "$GNOKEY_HOME" \ + "$KEY" "$TMPDIR/noop.gno" 2>&1) + +if echo "$TX2" | grep -qiE "sequence|wrong nonce|invalid sequence|account sequence|mempool"; then + echo "✅ PROTECTED — replay rejected" +elif echo "$TX2" | grep -q "OK!"; then + echo "❌ VULNERABLE — replay accepted" + exit 1 +else + echo "⚠️ UNKNOWN OUTPUT"; echo "$TX2"; exit 1 +fi diff --git a/samourai-crew/realms/counter/counter.gno b/samourai-crew/realms/counter/counter.gno new file mode 100644 index 0000000..2557bcd --- /dev/null +++ b/samourai-crew/realms/counter/counter.gno @@ -0,0 +1,21 @@ +package counter + +import "strconv" + +type Counter struct { + Value int +} + +func (c *Counter) Inc() { + c.Value++ +} + +var c Counter + +func Increment() { + c.Inc() +} + +func Render(path string) string { + return "Compteur Samourai : " + strconv.Itoa(c.Value) +} diff --git a/samourai-crew/run_tests.sh b/samourai-crew/run_tests.sh new file mode 100755 index 0000000..67fe417 --- /dev/null +++ b/samourai-crew/run_tests.sh @@ -0,0 +1,163 @@ +#!/bin/sh +# Usage: run_tests.sh [one-shot|repeatable] +# one-shot — audit scripts + e2e tests + stress tests +# repeatable — e2e tests safe to re-run on any chain state +# (no arg) — runs both +# +# Expected env vars (set in Dockerfile or injected by Makefile): +# REMOTE — primary RPC endpoint +# CHAINID — chain ID +# REMOTES — comma-separated RPC list for stress tests +# RUNNER_MNEMONIC — mnemonic of the main test account +# RUNNER_ADDR — address of the main test account + +MODE="${1:-all}" + +export REMOTE="${REMOTE:-http://127.0.0.1:26657}" +export CHAINID="${CHAINID:-test}" +export GNOKEY_HOME="${GNOKEY_HOME:-/tmp/gnokey}" +export REMOTES="${REMOTES:-$REMOTE}" +export KEY="runner" +export PASSWORD="runner1234" +export KEY_ADDR="${RUNNER_ADDR}" + +echo "Remote : $REMOTE" +echo "Chain : $CHAINID" +echo "Mode : $MODE" +echo "Runner : $KEY_ADDR" +echo "" + +# --- connectivity check --- +echo "Checking connectivity..." +RETRIES=10 +while [ "$RETRIES" -gt 0 ]; do + if gnokey query bank/balances/"$KEY_ADDR" -remote="$REMOTE" > /dev/null 2>&1; then + echo "Connected." + break + fi + RETRIES=$((RETRIES - 1)) + [ "$RETRIES" -eq 0 ] && echo "ERROR: cannot reach $REMOTE" && exit 1 + sleep 3 +done + +# --- import all keys before CLA signing --- +printf "%s\n%s\n%s\n" "$RUNNER_MNEMONIC" "$PASSWORD" "$PASSWORD" | \ + gnokey add "$KEY" -recover -insecure-password-stdin=true \ + -home "$GNOKEY_HOME" > /dev/null 2>&1 + +if [ -n "$STRESS_MNEMONIC_2" ] && [ "$STRESS_MNEMONIC_2" != "TODO_REPLACE_STRESS_MNEMONIC_2" ]; then + printf "%s\n%s\n%s\n" "$STRESS_MNEMONIC_2" "$PASSWORD" "$PASSWORD" | \ + gnokey add "stress_2" -recover -insecure-password-stdin=true \ + -home "$GNOKEY_HOME" > /dev/null 2>&1 +fi +if [ -n "$STRESS_MNEMONIC_3" ] && [ "$STRESS_MNEMONIC_3" != "TODO_REPLACE_STRESS_MNEMONIC_3" ]; then + printf "%s\n%s\n%s\n" "$STRESS_MNEMONIC_3" "$PASSWORD" "$PASSWORD" | \ + gnokey add "stress_3" -recover -insecure-password-stdin=true \ + -home "$GNOKEY_HOME" > /dev/null 2>&1 +fi + +# --- sign CLA if required by the network --- +# Signatures are stored on-chain — only the first run per wallet actually signs. +# Subsequent runs will receive "already signed" and continue gracefully. +CLA_HASH=$(gnokey query vm/qrender \ + -data "gno.land/r/sys/cla:" \ + -remote "$REMOTE" 2>/dev/null | grep -oE '[0-9a-f]{64}' | head -1) + +sign_cla() { + SIGNER_KEY="$1" + if [ -z "$CLA_HASH" ]; then return 0; fi + echo -n " CLA $SIGNER_KEY ... " + OUT=$(echo "$PASSWORD" | gnokey maketx call \ + -pkgpath "gno.land/r/sys/cla" \ + -func "Sign" \ + -args "$CLA_HASH" \ + -gas-fee 1000000ugnot \ + -gas-wanted 10000000 \ + -broadcast \ + -chainid "$CHAINID" \ + -remote "$REMOTE" \ + -insecure-password-stdin=true \ + -home "$GNOKEY_HOME" \ + "$SIGNER_KEY" 2>&1) + if echo "$OUT" | grep -q "OK\|already signed\|TX HASH"; then + echo "OK" + else + echo "signed or skipped" + fi +} + +if [ -n "$CLA_HASH" ]; then + echo "Signing CLA (hash: $CLA_HASH)..." + sign_cla "$KEY" + gnokey list -home "$GNOKEY_HOME" 2>/dev/null | grep -oE '^[0-9]+\. [^ ]+' | awk '{print $2}' | while read -r k; do + [ "$k" != "$KEY" ] && sign_cla "$k" + done + sleep 2 +fi +echo "" + +# --- test runner --- +PASS=0; FAIL=0; KNOWN=0; REPORT="" + +run_test() { + NAME="$1" + SCRIPT="$2" + KNOWN_NOTE="$3" + echo "" + echo "--- $NAME ---" + if "$SCRIPT"; then + PASS=$((PASS + 1)) + REPORT="${REPORT} [PASS] $NAME\n" + elif [ -n "$KNOWN_NOTE" ]; then + KNOWN=$((KNOWN + 1)) + REPORT="${REPORT} [KNOWN] $NAME — $KNOWN_NOTE\n" + else + FAIL=$((FAIL + 1)) + REPORT="${REPORT} [FAIL] $NAME\n" + fi +} + +if [ "$MODE" = "one-shot" ] || [ "$MODE" = "all" ]; then + echo "=== Audit Tests ===" + run_test "audit_runtime_pkg" /tests/audit/audit_runtime_pkg.sh + run_test "audit_chan_type" /tests/audit/audit_chan_type.sh + run_test "audit_security" /tests/audit/audit_security.sh + run_test "audit_gas_alloc" /tests/audit/audit_gas_alloc.sh + run_test "audit_byteslice" /tests/audit/audit_byteslice.sh + run_test "audit_array_alias" /tests/audit/audit_array_alias.sh + run_test "audit_var_init_order" /tests/audit/audit_var_init_order.sh + run_test "audit_cross_realm_recover" /tests/audit/audit_cross_realm_recover.sh \ + "broader pattern not yet fixed, see f87249327" + + 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 + + echo "" + echo "=== Stress Tests ===" + 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 +fi + +if [ "$MODE" = "repeatable" ] || [ "$MODE" = "all" ]; then + echo "" + echo "=== E2E Tests (repeatable) ===" + run_test "e2e_nonce_replay" /tests/e2e/e2e_nonce_replay.sh +fi + +echo "" +echo "=========================================" +echo " TEST SUMMARY" +echo "=========================================" +printf "%b" "$REPORT" +echo "-----------------------------------------" +echo " PASS: $PASS FAIL: $FAIL KNOWN: $KNOWN" +echo "=========================================" + +if [ "$FAIL" -gt 0 ]; then + echo "Some tests FAILED." + exit 1 +fi +echo "All tests passed." diff --git a/samourai-crew/stress/sybil_chaos.sh b/samourai-crew/stress/sybil_chaos.sh new file mode 100755 index 0000000..fa6a647 --- /dev/null +++ b/samourai-crew/stress/sybil_chaos.sh @@ -0,0 +1,104 @@ +#!/bin/bash +# Sybil chaos: N wallets bombard N RPCs fully in parallel. +# Each wallet fires TX_PER_ACCOUNT transactions without waiting. +# +# Expected env (set by run_tests.sh): +# KEY, PASSWORD, CHAINID, GNOKEY_HOME, KEY_ADDR +# REMOTES — comma-separated RPC list (falls back to $REMOTE) +# stress_1, stress_2, stress_3 keys must be imported in GNOKEY_HOME + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +TX_PER_ACCOUNT="${TX_PER_ACCOUNT:-10}" +REMOTES="${REMOTES:-${REMOTE:-http://127.0.0.1:26657}}" +SUFFIX=$(date +%s) +COUNTER_PKGPATH="gno.land/r/${KEY_ADDR}/stress/chaos${SUFFIX}" +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "🌪️ SYBIL CHAOS — parallel bombardment" + +IFS=',' read -ra RPCS <<< "$REMOTES" +N=${#RPCS[@]} +echo " RPCs : $N" +echo " Txs/key : $TX_PER_ACCOUNT" +echo "" + +# Build wallet list: first slot = runner (KEY), then stress_2, stress_3... +WALLET_KEYS=() +for i in $(seq 1 "$N"); do + if [ "$i" -eq 1 ]; then + WALLET_KEYS+=("$KEY") + else + wkey="stress_${i}" + if gnokey list -home "$GNOKEY_HOME" 2>/dev/null | grep -q "^[0-9]*\. $wkey "; then + WALLET_KEYS+=("$wkey") + else + echo "FAIL: stress key $wkey not found in keystore"; exit 1 + fi + fi +done + +# Deploy counter realm +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" +echo "$PASSWORD" | gnokey maketx addpkg \ + -pkgpath "$COUNTER_PKGPATH" \ + -pkgdir "$TMPDIR" \ + -gas-fee 1000000ugnot -gas-wanted 10000000 \ + -broadcast -chainid "$CHAINID" -remote "${RPCS[0]}" \ + -insecure-password-stdin=true -home "$GNOKEY_HOME" \ + "$KEY" > /dev/null || { echo "FAIL: could not deploy counter"; exit 1; } + +cat > "$TMPDIR/increment.gno" << EOF +package main +import c "$COUNTER_PKGPATH" +func main() { c.Increment() } +EOF + +echo "" +echo "Launching parallel bombardment..." + +for i in $(seq 1 "$N"); do + wkey="${WALLET_KEYS[$i-1]}" + rpc="${RPCS[$i-1]}" + ( + echo -n "🚀 $wkey → $rpc : " + for _ in $(seq 1 "$TX_PER_ACCOUNT"); do + echo "$PASSWORD" | gnokey maketx run \ + -broadcast -chainid "$CHAINID" -remote "$rpc" \ + -gas-fee 1000000ugnot -gas-wanted 3000000 \ + -insecure-password-stdin=true -home "$GNOKEY_HOME" \ + "$wkey" "$TMPDIR/increment.gno" > /dev/null 2>&1 + echo -n "." + done + echo " ✅" + ) & +done + +wait +echo "" +echo "Waiting for consensus to settle..." +sleep 5 + +echo "=== Final counter per RPC ===" +ATTEMPTED=$(( N * TX_PER_ACCOUNT )) +FIRST_VAL="" +ALL_SAME=true +for rpc in "${RPCS[@]}"; do + val=$(gnokey query "vm/qeval" -remote "$rpc" \ + -data "${COUNTER_PKGPATH}.Render(\"\")" 2>/dev/null | grep -oE '[0-9]+' | tail -1) + echo " $rpc → ${val:-0}" + if [ -z "$FIRST_VAL" ]; then + FIRST_VAL="${val:-0}" + elif [ "${val:-0}" != "$FIRST_VAL" ]; then + ALL_SAME=false + fi +done + +echo " committed: $FIRST_VAL / $ATTEMPTED txs attempted" +if $ALL_SAME && [ "${FIRST_VAL:-0}" -gt 0 ]; then + echo "[PASS] all nodes converged at $FIRST_VAL" + exit 0 +fi +echo "[FAIL] nodes diverged or no txs committed" && exit 1 diff --git a/samourai-crew/stress/sybil_precision.sh b/samourai-crew/stress/sybil_precision.sh new file mode 100644 index 0000000..5c007c9 --- /dev/null +++ b/samourai-crew/stress/sybil_precision.sh @@ -0,0 +1,97 @@ +#!/bin/bash +# Sybil precision: N wallets hit N RPCs in parallel, but each wallet +# sends txs sequentially with a small delay. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +TX_PER_ACCOUNT="${TX_PER_ACCOUNT:-10}" +TX_DELAY="${TX_DELAY:-0.8}" +REMOTES="${REMOTES:-${REMOTE:-http://127.0.0.1:26657}}" +SUFFIX=$(date +%s) +COUNTER_PKGPATH="gno.land/r/${KEY_ADDR}/stress/precision${SUFFIX}" +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "🎯 SYBIL PRECISION — sequential per wallet, parallel across wallets" + +IFS=',' read -ra RPCS <<< "$REMOTES" +N=${#RPCS[@]} +echo " RPCs : $N" +echo " Txs/key : $TX_PER_ACCOUNT (delay: ${TX_DELAY}s)" +echo "" + +WALLET_KEYS=() +for i in $(seq 1 "$N"); do + if [ "$i" -eq 1 ]; then + WALLET_KEYS+=("$KEY") + else + wkey="stress_${i}" + if gnokey list -home "$GNOKEY_HOME" 2>/dev/null | grep -q "^[0-9]*\. $wkey "; then + WALLET_KEYS+=("$wkey") + else + echo "FAIL: stress key $wkey not found in keystore"; exit 1 + fi + fi +done + +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" +echo "$PASSWORD" | gnokey maketx addpkg \ + -pkgpath "$COUNTER_PKGPATH" \ + -pkgdir "$TMPDIR" \ + -gas-fee 1000000ugnot -gas-wanted 10000000 \ + -broadcast -chainid "$CHAINID" -remote "${RPCS[0]}" \ + -insecure-password-stdin=true -home "$GNOKEY_HOME" \ + "$KEY" > /dev/null || { echo "FAIL: could not deploy counter"; exit 1; } + +cat > "$TMPDIR/increment.gno" << EOF +package main +import c "$COUNTER_PKGPATH" +func main() { c.Increment() } +EOF + +echo "" +echo "Launching precision bombardment..." + +for i in $(seq 1 "$N"); do + wkey="${WALLET_KEYS[$i-1]}" + rpc="${RPCS[$i-1]}" + ( + echo -n "⚖️ $wkey → $rpc : " + for _ in $(seq 1 "$TX_PER_ACCOUNT"); do + echo "$PASSWORD" | gnokey maketx run \ + -broadcast -chainid "$CHAINID" -remote "$rpc" \ + -gas-fee 1000000ugnot -gas-wanted 3000000 \ + -insecure-password-stdin=true -home "$GNOKEY_HOME" \ + "$wkey" "$TMPDIR/increment.gno" > /dev/null 2>&1 + echo -n "." + sleep "$TX_DELAY" + done + echo " ✅" + ) & +done + +wait +echo "" +echo "=== Final counter per RPC ===" +EXPECTED=$(( N * TX_PER_ACCOUNT )) +ATTEMPTED=$(( N * TX_PER_ACCOUNT )) +FIRST_VAL="" +ALL_SAME=true +for rpc in "${RPCS[@]}"; do + val=$(gnokey query "vm/qeval" -remote "$rpc" \ + -data "${COUNTER_PKGPATH}.Render(\"\")" 2>/dev/null | grep -oE '[0-9]+' | tail -1) + echo " $rpc → ${val:-0}" + if [ -z "$FIRST_VAL" ]; then + FIRST_VAL="${val:-0}" + elif [ "${val:-0}" != "$FIRST_VAL" ]; then + ALL_SAME=false + fi +done + +echo " committed: $FIRST_VAL / $ATTEMPTED txs attempted" +if $ALL_SAME && [ "${FIRST_VAL:-0}" -gt 0 ]; then + echo "[PASS] all nodes converged at $FIRST_VAL" + exit 0 +fi +echo "[FAIL] nodes diverged or no txs committed" && exit 1 diff --git a/samourai-crew/stress/sybil_salted_chaos.sh b/samourai-crew/stress/sybil_salted_chaos.sh new file mode 100755 index 0000000..fac67f1 --- /dev/null +++ b/samourai-crew/stress/sybil_salted_chaos.sh @@ -0,0 +1,104 @@ +#!/bin/bash +# Sybil salted chaos: ultra-parallel fire-and-forget with a unique memo +# salt per tx to prevent transaction deduplication. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +TX_PER_ACCOUNT="${TX_PER_ACCOUNT:-10}" +REMOTES="${REMOTES:-${REMOTE:-http://127.0.0.1:26657}}" +SUFFIX=$(date +%s) +COUNTER_PKGPATH="gno.land/r/${KEY_ADDR}/stress/salted${SUFFIX}" +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "💀 SYBIL SALTED CHAOS — ultra-parallel with memo salt" + +IFS=',' read -ra RPCS <<< "$REMOTES" +N=${#RPCS[@]} +echo " RPCs : $N" +echo " Txs/key : $TX_PER_ACCOUNT" +echo "" + +WALLET_KEYS=() +for i in $(seq 1 "$N"); do + if [ "$i" -eq 1 ]; then + WALLET_KEYS+=("$KEY") + else + wkey="stress_${i}" + if gnokey list -home "$GNOKEY_HOME" 2>/dev/null | grep -q "^[0-9]*\. $wkey "; then + WALLET_KEYS+=("$wkey") + else + echo "FAIL: stress key $wkey not found in keystore"; exit 1 + fi + fi +done + +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" +echo "$PASSWORD" | gnokey maketx addpkg \ + -pkgpath "$COUNTER_PKGPATH" \ + -pkgdir "$TMPDIR" \ + -gas-fee 1000000ugnot -gas-wanted 10000000 \ + -broadcast -chainid "$CHAINID" -remote "${RPCS[0]}" \ + -insecure-password-stdin=true -home "$GNOKEY_HOME" \ + "$KEY" > /dev/null || { echo "FAIL: could not deploy counter"; exit 1; } + +cat > "$TMPDIR/increment.gno" << EOF +package main +import c "$COUNTER_PKGPATH" +func main() { c.Increment() } +EOF + +echo "" +echo "Launching salted chaos..." + +for i in $(seq 1 "$N"); do + wkey="${WALLET_KEYS[$i-1]}" + rpc="${RPCS[$i-1]}" + ( + echo -n "🔥 $wkey → $rpc : " + for j in $(seq 1 "$TX_PER_ACCOUNT"); do + SALT=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 8) + ( + echo "$PASSWORD" | gnokey maketx run \ + -broadcast -chainid "$CHAINID" -remote "$rpc" \ + -gas-fee 1000000ugnot -gas-wanted 3000000 \ + -memo "samourai-salt-$SALT" \ + -insecure-password-stdin=true -home "$GNOKEY_HOME" \ + "$wkey" "$TMPDIR/increment.gno" > /dev/null 2>&1 + ) & + [ $(( j % 5 )) -eq 0 ] && echo -n "!" && sleep 0.1 + done + wait + echo " 💀" + ) & +done + +wait +echo "" +echo "Waiting for chaos to settle..." +sleep 10 + +echo "=== Final counter per RPC ===" +EXPECTED=$(( N * TX_PER_ACCOUNT )) +ALL_OK=true +ATTEMPTED=$(( N * TX_PER_ACCOUNT )) +FIRST_VAL="" +ALL_SAME=true +for rpc in "${RPCS[@]}"; do + val=$(gnokey query "vm/qeval" -remote "$rpc" \ + -data "${COUNTER_PKGPATH}.Render(\"\")" 2>/dev/null | grep -oE '[0-9]+' | tail -1) + echo " $rpc → ${val:-0}" + if [ -z "$FIRST_VAL" ]; then + FIRST_VAL="${val:-0}" + elif [ "${val:-0}" != "$FIRST_VAL" ]; then + ALL_SAME=false + fi +done + +echo " committed: $FIRST_VAL / $ATTEMPTED txs attempted" +if $ALL_SAME && [ "${FIRST_VAL:-0}" -gt 0 ]; then + echo "[PASS] all nodes converged at $FIRST_VAL" + exit 0 +fi +echo "[FAIL] nodes diverged or no txs committed" && exit 1