Files
node/scripts/cluster.sh
zeekayandHanzo Dev c3977fa896 A transaction reaches every validator, and the cluster runs as processes
Only the window's proposer builds, so a transaction left in the pool of the
node a client reached waited for that node's window. What the chain's own
server takes is relayed once across the mesh (msg 58, lux-rs/node's id) and
handed to each peer's chain.

scripts/cluster.sh runs N luxd processes over the cevm plugin on the local
chain, one transaction at a time from test/local/txs.txt (the Anvil key luxd's
local spec funds), and asserts every validator that is up accepts the same
block at every height — with a seat held down, the same. chain.sh, which
expected blocks with no transactions, is gone.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-24 16:22:23 -07:00

110 lines
4.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# Copyright (C) 2026, Lux Industries, Inc. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause-Eco
#
# cluster.sh — N luxd validators as PROCESSES on the local chain (31337), each
# running its EVM as the cevm plugin, fed one transaction at a time, and held to
# one thing: every validator that is up accepts the same block at every height.
#
# The optional DOWN seat is configured in every committee and never started. Its
# windows lapse and the next window's proposer builds (the windower,
# engine.hpp), so a held-down seat costs its windows and nothing more; the
# "slot" each block was built in is printed, and a slot above 0 is a height
# whose first window was somebody else's.
#
# Transactions go to the validators in turn and are relayed across the mesh, so
# whichever validator holds the window holds the transaction. They are
# test/local/txs.txt: transfers signed by the first Anvil/Hardhat account, the
# key luxd's local spec funds (src/luxd.cpp). A Cancun block that would owe a
# block gas cost is not built, so they are sent a block interval apart.
#
# Usage: scripts/cluster.sh LUXD CEVM [down] [n] [mesh-port] [rpc-port]
set -uo pipefail
LUXD="${1:?luxd}"
CEVM="${2:?cevm}"
DOWN="${3:--1}"
N="${4:-5}"
BASE="${5:-19310}"
RPC="${6:-19410}"
TXS="$(cd "$(dirname "$0")/.." && pwd)/test/local/txs.txt"
FROM=0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
TMP="$(mktemp -d)"
pids=()
cleanup() {
for p in "${pids[@]:-}"; do kill -TERM "$p" 2>/dev/null || true; done
for p in "${pids[@]:-}"; do wait "$p" 2>/dev/null || true; done
rm -rf "$TMP"
}
trap cleanup EXIT
fail=0
ok() { echo " ok $1"; }
bad() { echo " FAIL $1"; fail=1; }
call() { # call <seat> <method> <params-json> -> the raw answer
curl -s --max-time 5 -X POST -H 'content-type: application/json' \
--data "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"$2\",\"params\":${3:-[]}}" \
"http://127.0.0.1:$((RPC + $1))/v1/chain/c/rpc"
}
field() { sed -n "s/.*\"$1\":\"\\([^\"]*\\)\".*/\\1/p"; }
live=()
for i in $(seq 0 $((N - 1))); do [[ "$i" == "$DOWN" ]] || live+=("$i"); done
peers=""
for i in $(seq 0 $((N - 1))); do
"$LUXD" --data "$TMP/v$i" --publish >>"$TMP/committee.txt"
peers="${peers:+$peers,}127.0.0.1:$((BASE + i))"
done
for i in "${live[@]}"; do
"$LUXD" --data "$TMP/v$i" --committee "$TMP/committee.txt" --peers "$peers" --vm "$CEVM" \
--rpc-port $((RPC + i)) --deadline-ms 3000 >"$TMP/node$i.log" 2>&1 &
pids+=("$!")
done
if (( DOWN >= 0 )); then echo "== ${#live[@]} of $N validators up, seat $DOWN held down"; else echo "== $N validators up"; fi
for _ in $(seq 1 60); do grep -q "window" "$TMP/node${live[0]}.log" 2>/dev/null && break; sleep 0.5; done
sent=0
while read -r tx; do
seat="${live[$((sent % ${#live[@]}))]}"
answer="$(call "$seat" eth_sendRawTransaction "[\"$tx\"]")"
echo "$answer" | grep -q '"result"' || bad "transaction $sent to seat $seat: $answer"
sent=$((sent + 1))
sleep 2.5
done <"$TXS"
want=$(printf '0x%x' "$sent")
for _ in $(seq 1 60); do
done_all=1
for i in "${live[@]}"; do
[[ "$(call "$i" eth_getTransactionCount "[\"$FROM\",\"latest\"]" | field result)" == "$want" ]] || done_all=0
done
[[ "$done_all" == 1 ]] && break
sleep 1
done
height=$(( $(call "${live[0]}" eth_blockNumber | field result) ))
(( height >= 1 )) && ok "the chain is at height $height after $sent transactions" || bad "no block was built"
for i in "${live[@]}"; do
n="$(call "$i" eth_getTransactionCount "[\"$FROM\",\"latest\"]" | field result)"
[[ "$n" == "$want" ]] && ok "seat $i executed all $sent transactions" || bad "seat $i nonce $n, want $want"
done
for h in $(seq 1 "$height"); do
hx=$(printf '0x%x' "$h")
first=""
for i in "${live[@]}"; do
hash="$(call "$i" eth_getBlockByNumber "[\"$hx\",false]" | field hash)"
[[ -z "$first" ]] && first="$hash"
[[ -n "$hash" && "$hash" == "$first" ]] || bad "height $h: seat $i has '$hash', seat ${live[0]} has '$first'"
done
done
(( fail == 0 )) && ok "every validator that is up accepted the same block at heights 1..$height"
echo "== who built each block (a slot is a window at the height; each is one proposer's)"
grep -h " led " "$TMP"/node*.log | sort -t' ' -k4,4n -u | sed 's/^/ /'
echo
if (( fail )); then echo "FAIL"; for i in "${live[@]}"; do echo "--- node $i"; tail -15 "$TMP/node$i.log"; done; exit 1; fi
echo "PASS — ${#live[@]} of $N validators, one chain"