From 56ea92e2e354c636ccb014a84ae383d4ad8b47fe Mon Sep 17 00:00:00 2001 From: Diana Borbe Date: Mon, 10 Aug 2026 17:10:43 +0200 Subject: [PATCH 01/11] Add besu-to-besu spin up script Signed-off-by: Diana Borbe --- .gitignore | 3 + examples/besu-to-besu/README.md | 237 +++++++++++++ examples/besu-to-besu/chains/besu.toml.tmpl | 31 ++ .../besu-to-besu/chains/el-genesis.json.tmpl | 44 +++ examples/besu-to-besu/docker-compose.yml | 68 ++++ examples/besu-to-besu/lib/chains.sh | 330 ++++++++++++++++++ examples/besu-to-besu/lib/common.sh | 83 +++++ examples/besu-to-besu/setup.sh | 139 ++++++++ 8 files changed, 935 insertions(+) create mode 100644 examples/besu-to-besu/README.md create mode 100644 examples/besu-to-besu/chains/besu.toml.tmpl create mode 100644 examples/besu-to-besu/chains/el-genesis.json.tmpl create mode 100644 examples/besu-to-besu/docker-compose.yml create mode 100644 examples/besu-to-besu/lib/chains.sh create mode 100644 examples/besu-to-besu/lib/common.sh create mode 100755 examples/besu-to-besu/setup.sh diff --git a/.gitignore b/.gitignore index 876879075..8aa81cef6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ # macOS .DS_Store + +logs/ +examples/**/local/ \ No newline at end of file diff --git a/examples/besu-to-besu/README.md b/examples/besu-to-besu/README.md new file mode 100644 index 000000000..60256e1c9 --- /dev/null +++ b/examples/besu-to-besu/README.md @@ -0,0 +1,237 @@ +# besu-besu + +Two single-validator Besu QBFT chains running side-by-side, intended as the +substrate for a single IBC pair: + +``` + A ◀──IBC──▶ B +``` + +Current scope is chain bring-up only: derive keys, render configs, boot both +chains, wait for them to produce blocks. (The three-chain hub-and-spoke variant +with contracts, attestors, proof-api, and relayer lives in `demo/besu-trio/`.) + +## No committed private keys + +Every account — both validators and the pre-funded genesis allocs — is derived +from a **BIP-39 mnemonic** at init time. Nothing under `chains/local/` is +committed; only the two templates in `chains/` are. + +**Each chain owns a mnemonic** and every account on that chain comes out of it. +`A_MNEMONIC` and `B_MNEMONIC` default to different (publicly known) BIP-39 test +vectors, so the two chains share no accounts at all: + +| | Chain A | Chain B | +|---|---|---| +| phrase | `A_MNEMONIC` | `B_MNEMONIC` | +| funded accounts | 5 from `A_MNEMONIC`, on A only | 5 from `B_MNEMONIC`, on B only | +| deployer | index 0 | index 0 | +| validator | index 1 | index 1 | + +Separate phrases per chain are deliberate: + +- A QBFT light client trusts the counterparty's validator set *by address*. If A + and B shared a validator, a header signed for one chain would be + signature-valid against the other chain's client. Distinct phrases rule that + out by construction. +- A validator is its chain's coinbase and pockets the priority fee. Keeping the + deployer (index 0) off the validator slot (index 1) means an identical tx + costs the deployer the same on both chains — otherwise it partly refunds + itself on the chain it validates, which quietly breaks any test asserting + exact gas spend. + +`MNEMONIC` remains as the fallback for a chain with no phrase of its own; set +`A_MNEMONIC` and `B_MNEMONIC` to the same value if you *want* the chains to +share accounts (derivation is cached, so that costs one pass, not two). + +Because the deployer differs per chain, a contract deployed to both chains will +**not** land at the same address — `CREATE` derives it from sender and nonce. +Point both chains at one mnemonic if you need matching addresses. + +`./setup.sh init` walks the mnemonics and, per chain: + +1. Derives the validator key from that chain's phrase at its derivation index + (`cast wallet private-key --mnemonic … --mnemonic-index …`). +2. Computes the QBFT genesis `extraData` for that validator — the RLP encoding + of `[vanity, [validator], votes=[], round=0, committedSeals=[]]`. +3. Renders `besu.toml` and `el-genesis.json` from `chains/*.tmpl` and writes the + raw key to `chains/local//key`, which is what Besu reads. + +So the mnemonics are the single source of truth: change one and the validator +address, the genesis `extraData`, and the allocs all move together. No manual +re-encoding of `extraData` when a key changes. + +`A_MNEMONIC` and `B_MNEMONIC` default to standard BIP-39 test vectors, and the +`MNEMONIC` fallback to the well-known Anvil/Hardhat phrase (`test test … junk`). +**Never point any of them at a phrase that holds real funds** — derived keys are +written to disk in plaintext for Besu to read. + +## Layout + +``` +demo/besu-besu/ +├── README.md +├── setup.sh — entrypoint: init | start | accounts | status | clean +├── docker-compose.yml — besu-a + besu-b +├── lib/ +│ ├── common.sh — logging, prerequisite checks, RPC waiter, +│ │ render_template, cast_cli (host cast or +│ │ the foundry image) +│ └── chains.sh — mnemonic derivation, QBFT extraData, config +│ rendering, start / wait / status / clean +└── chains/ + ├── besu.toml.tmpl — rendered once per chain + ├── el-genesis.json.tmpl — ${CHAIN_ID}, ${QBFT_EXTRADATA}, ${GENESIS_ALLOC} + └── local/ — generated, gitignored: + ├── chains.env — funded addresses, deployer key, validators, + │ chain IDs, RPC URLs + ├── A/{besu.toml, el-genesis.json, key} + └── B/{besu.toml, el-genesis.json, key} +``` + +## Chains + +| Chain | Chain ID | Validator source | Validator (default phrases) | +|-------|---------:|------------------|-----------------------------| +| A | 41001 | `A_MNEMONIC` index 1 | `0x0D3eB21b6b21833A4939Cfff4810E9AE0758e12C` | +| B | 41002 | `B_MNEMONIC` index 1 | `0x45A1eF7572a5B9998b46E54AA5Dce838965acB35` | + +Neither validator is the deployer, and the two are different accounts. The +addresses are a *consequence* of the mnemonics, not a fixed fact — run +`./setup.sh accounts` to print the resolved set for whatever phrases are +configured, or read `chains/local/chains.env` after an init. + +## Funded accounts + +`FUNDED_ACCOUNTS` accounts (default 5) are derived **per chain** from that +chain's mnemonic and funded with 1 000 000 ETH in that chain's genesis — and +nowhere else. Chain A's accounts hold nothing on chain B and vice versa. Index 0 +is that chain's deployer, index 1 its validator. + +`init` logs the exact alloc set it wrote for each chain — these are the only +accounts that can pay for gas there, so it is the first thing to check when a tx +fails with *insufficient funds*: + +``` +[A] deriving accounts 0..4 from A_MNEMONIC... +[A] genesis funds 5 accounts with 1000000 ETH each: + index 0 0x58A57ed9d8d624cBD12e2C467D34787555bB1b25 [deployer] + index 1 0x0D3eB21b6b21833A4939Cfff4810E9AE0758e12C [validator] + index 2 0xe42f4612e154153B68e241e8FDe337e0c4dD6bBD + index 3 0x2F07c220dC62CE9531bC44B695A6D93578806d8d + index 4 0x1454168e82efA260f49a6F86612cc6414Ba633e9 +``` + +`./setup.sh accounts` reprints both chains' lists on demand without touching +anything. The same addresses land in `chains/local/chains.env`, namespaced per +chain: + +```bash +source chains/local/chains.env +echo "$A_DEPLOYER_ADDR $A_DEPLOYER_PRIVKEY" +echo "$B_FUNDED_ADDR_3" # indexed form — safe in any shell +for a in $A_FUNDED_ADDRS; do # space-separated list — bash word-splitting + cast balance "$a" --rpc-url "$A_RPC_URL" --ether +done +``` + +To fund more accounts, raise the count and rebuild (a genesis change needs the +chain data wiped): + +```bash +./setup.sh clean && FUNDED_ACCOUNTS=10 ./setup.sh +``` + +A and B are fully independent: discovery is off and neither has bootnodes, so +they never gossip with each other. The only path between them is IBC. + +## Ports + +Both containers use the same internal ports (8545 RPC, 8546 WS, 9545 metrics). +Only the host-side mappings differ: + +| Service | RPC (host) | WS (host) | Metrics (host) | +|---------|-----------:|----------:|---------------:| +| besu-a | 8545 | 8546 | 9545 | +| besu-b | 8745 | 8746 | 9745 | + +## Usage + +```bash +./setup.sh # init + start + wait for RPC (end-to-end) +./setup.sh init # derive keys, render chain configs into chains/local/ + # (touches no containers) +./setup.sh start # docker compose up both chains, wait for RPC +./setup.sh accounts # print the funded addresses and their roles +./setup.sh status # RPC endpoints, chain IDs, block heights +./setup.sh clean # stop containers, remove volumes and chains/local/ +``` + +`init` is idempotent — re-running it re-derives the same artefacts from the same +mnemonic. When the rendered genesis differs from the previous run it warns: +Besu will refuse to start against a database built on a different genesis, so +run `./setup.sh clean` first. + +Quick smoke check once the chains are up: + +```bash +curl -s -X POST -H 'Content-Type: application/json' \ + --data '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' \ + http://localhost:8745 # → 0xa02a (= 41002, chain B) +``` + +A `logs/` directory is created on first run and each invocation writes a +timestamped log file there. + +## Configuration + +All optional; export before running `./setup.sh`. + +| Variable | Default | Meaning | +|----------|---------|---------| +| `A_MNEMONIC` | `legal winner thank …` | phrase every chain A account derives from | +| `B_MNEMONIC` | `letter advice cage …` | phrase every chain B account derives from | +| `MNEMONIC` | Anvil/Hardhat test phrase | fallback for a chain with no phrase of its own | +| `A_VALIDATOR_INDEX` | `1` | chain A's validator, within `A_MNEMONIC` | +| `B_VALIDATOR_INDEX` | `1` | chain B's validator, within `B_MNEMONIC` | +| `FUNDED_ACCOUNTS` | `5` | accounts derived per chain and funded in that chain's genesis | +| `GENESIS_BALANCE` | `0xd3c21bcecceda1000000` | wei per funded account (1M ETH) | +| `QBFT_BLOCK_PERIOD_SECONDS` | `2` | QBFT block time | +| `QBFT_EPOCH_LENGTH` | `30000` | QBFT epoch length | +| `QBFT_REQUEST_TIMEOUT_SECONDS` | `4` | QBFT round-change timeout | +| `BESU_IMAGE` | `hyperledger/besu:25.4.0` | Besu image | +| `FOUNDRY_IMAGE` | `ghcr.io/foundry-rs/foundry:latest` | fallback for key derivation | + +Changing any of the first nine invalidates the on-disk chain data — run +`./setup.sh clean` before re-running. + +```bash +# point chain B at your own phrase +./setup.sh clean +B_MNEMONIC="your twelve word phrase …" ./setup.sh + +# make both chains share one account set (same deployer, same addresses) +./setup.sh clean +A_MNEMONIC="test test test test test test test test test test test junk" \ +B_MNEMONIC="test test test test test test test test test test test junk" ./setup.sh +``` + +## Prerequisites + +```bash +docker # compose v2 plugin required +curl +perl +``` + +`cast` (foundry) is used for BIP-39 derivation. If it isn't on `PATH`, +`cast_cli` transparently falls back to running it inside `$FOUNDRY_IMAGE`, so a +host install is optional. + +## What's not here + +- IBC contracts, light clients, attestors, proof-api, relayer — see + `demo/besu-trio/` for the wired-up multi-chain version. +- Multi-validator QBFT. `qbft_extradata` encodes exactly one validator per + chain; a validator set of two or more needs a real RLP encoder for the + list-length prefix. diff --git a/examples/besu-to-besu/chains/besu.toml.tmpl b/examples/besu-to-besu/chains/besu.toml.tmpl new file mode 100644 index 000000000..b26a6b9c3 --- /dev/null +++ b/examples/besu-to-besu/chains/besu.toml.tmpl @@ -0,0 +1,31 @@ +# Besu execution-layer config for chain ${CHAIN_NAME} (besu-besu). +# Rendered by ./setup.sh init — edit chains/besu.toml.tmpl, not this copy. +genesis-file = "/evm/el-genesis.json" +data-path = "/data" + +# QBFT signing key — derived from the demo mnemonic at index ${VALIDATOR_INDEX}, +# giving ${VALIDATOR_ADDR}: +# the sole validator encoded in the genesis extraData for chain ${CHAIN_NAME}. +node-private-key-file = "/evm/key" + +# Networking — single-validator devnet, but p2p must be enabled or QBFT's pivot +# selector blocks waiting for peers. Discovery off + no bootnodes keeps each +# chain solo; A and B never gossip with each other. +p2p-enabled = true +discovery-enabled = false +bootnodes = [] +host-allowlist = ["*"] + +rpc-http-enabled = true +rpc-http-host = "0.0.0.0" +rpc-http-port = 8545 +rpc-http-cors-origins = ["*"] +rpc-http-api = ["ETH","NET","WEB3","TXPOOL","DEBUG","ADMIN","QBFT"] + +rpc-ws-enabled = true +rpc-ws-host = "0.0.0.0" +rpc-ws-port = 8546 +rpc-ws-api = ["ETH","NET","WEB3","TXPOOL"] + +sync-mode = "FULL" +sync-min-peers = 0 diff --git a/examples/besu-to-besu/chains/el-genesis.json.tmpl b/examples/besu-to-besu/chains/el-genesis.json.tmpl new file mode 100644 index 000000000..6671abbe3 --- /dev/null +++ b/examples/besu-to-besu/chains/el-genesis.json.tmpl @@ -0,0 +1,44 @@ +{ + "config": { + "chainId": ${CHAIN_ID}, + "homesteadBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "arrowGlacierBlock": 0, + "grayGlacierBlock": 0, + "shanghaiTime": 0, + "cancunTime": 0, + "qbft": { + "blockperiodseconds": ${QBFT_BLOCK_PERIOD_SECONDS}, + "epochlength": ${QBFT_EPOCH_LENGTH}, + "requesttimeoutseconds": ${QBFT_REQUEST_TIMEOUT_SECONDS} + } + }, + "nonce": "0x0", + "timestamp": "0x0", + "extraData": "${QBFT_EXTRADATA}", + "gasLimit": "0x1C9C380", + "difficulty": "0x1", + "mixHash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { +${GENESIS_ALLOC} + "0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02": { + "comment": "EIP-4788 beacon block root contract (required by Cancun)", + "balance": "0x0", + "nonce": "0x1", + "code": "0x3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500" + } + }, + "baseFeePerGas": "0x7", + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/examples/besu-to-besu/docker-compose.yml b/examples/besu-to-besu/docker-compose.yml new file mode 100644 index 000000000..5be7bf19b --- /dev/null +++ b/examples/besu-to-besu/docker-compose.yml @@ -0,0 +1,68 @@ +# Two independent single-validator Besu QBFT chains. +# +# Each /evm mount points at chains/local//, which ./setup.sh init +# renders from chains/*.tmpl — besu.toml, el-genesis.json, and the validator +# key derived from the demo mnemonic. Nothing under chains/local/ is committed, +# so `docker compose up` without a prior init has no config to mount. + +services: + + # ── Chains ──────────────────────────────────────────────────────────────── + besu-a: + image: ${BESU_IMAGE:-hyperledger/besu:25.4.0} + container_name: besu-a + command: + - "--config-file=/evm/besu.toml" + - "--metrics-enabled" + - "--metrics-host=0.0.0.0" + - "--metrics-port=9545" + - "--Xqbft-enable-early-round-change" + ports: + - "8545:8545" + - "8546:8546" + - "9545:9545" + volumes: + - besu-a-data:/data + - ./chains/local/A:/evm:ro + networks: [ besu-besu-net ] + restart: on-failure + healthcheck: + test: [ "CMD", "bash", "-c", "exec 3<>/dev/tcp/localhost/8545" ] + interval: 5s + timeout: 3s + retries: 60 + start_period: 10s + + besu-b: + image: ${BESU_IMAGE:-hyperledger/besu:25.4.0} + container_name: besu-b + command: + - "--config-file=/evm/besu.toml" + - "--metrics-enabled" + - "--metrics-host=0.0.0.0" + - "--metrics-port=9545" + - "--Xqbft-enable-early-round-change" + ports: + - "8745:8545" + - "8746:8546" + - "9745:9545" + volumes: + - besu-b-data:/data + - ./chains/local/B:/evm:ro + networks: [ besu-besu-net ] + restart: on-failure + healthcheck: + test: [ "CMD", "bash", "-c", "exec 3<>/dev/tcp/localhost/8545" ] + interval: 5s + timeout: 3s + retries: 60 + start_period: 10s + + +volumes: + besu-a-data: + besu-b-data: + +networks: + besu-besu-net: + driver: bridge diff --git a/examples/besu-to-besu/lib/chains.sh b/examples/besu-to-besu/lib/chains.sh new file mode 100644 index 000000000..f48316ccc --- /dev/null +++ b/examples/besu-to-besu/lib/chains.sh @@ -0,0 +1,330 @@ +#!/usr/bin/env bash +# Chain bring-up for the two single-validator Besu QBFT chains (A and B). +# +# Nothing under chains/ is committed except the two templates. Each chain's +# validator private key, besu.toml, and QBFT genesis are derived from a BIP-39 +# mnemonic into chains/local// by init_chains, which is also what +# docker-compose bind-mounts at /evm. That means the mnemonics are the single +# source of truth: change one and every derived artefact — validator address, +# genesis extraData, genesis allocs — moves with it. +# +# Each chain owns a mnemonic (A_MNEMONIC / B_MNEMONIC, falling back to MNEMONIC) +# and every account on that chain comes out of it: FUNDED_ACCOUNTS addresses are +# derived per chain and funded in that chain's genesis. By convention index 0 is +# the chain's deployer and index 1 its validator, so the two chains share no +# accounts at all — separate funded sets, separate deployers, separate +# validators. + +CHAINS=(A B) + +# Per-chain static facts. RPC/WS ports are the *host-side* mappings from +# docker-compose.yml; inside the compose network both chains listen on the +# same 8545/8546. +A_SERVICE=besu-a; A_CHAIN_ID=41001; A_RPC_PORT=8545; A_WS_PORT=8546 +B_SERVICE=besu-b; B_CHAIN_ID=41002; B_RPC_PORT=8745; B_WS_PORT=8746 + +# Accounts of the chain currently being processed: indices 0..FUNDED_ACCOUNTS-1 +# of that chain's mnemonic. Parallel arrays rather than an associative array so +# this still runs under the bash 3.2 that ships with macOS. +CHAIN_ACCT_ADDRS=() +CHAIN_ACCT_KEYS=() +_DERIVED_MNEMONIC="" # phrase the arrays above currently hold; cache key + +# _chain_attr — read A_CHAIN_ID / B_RPC_PORT / … indirectly. +_chain_attr() { + local var="${1}_${2}" + printf '%s' "${!var}" +} + +# _chain_mnemonic — that chain's phrase, falling back to the shared +# MNEMONIC when no per-chain one is set. +_chain_mnemonic() { + local var="${1}_MNEMONIC" + printf '%s' "${!var:-$MNEMONIC}" +} + +# derive_account — echo "
". +# +# Returns non-zero rather than calling die: every call site is a command +# substitution, and an exit from inside one only kills the subshell, letting a +# bad mnemonic sail on and render a genesis with an empty validator. Callers +# must `|| die`. +derive_account() { + local mnemonic="$1" index="$2" pk addr + pk=$(cast_cli wallet private-key --mnemonic "$mnemonic" --mnemonic-index "$index" \ + 2>/dev/null | tr -d '[:space:]') + [[ "$pk" == 0x* && ${#pk} -eq 66 ]] || return 1 + addr=$(cast_cli wallet address --private-key "$pk" 2>/dev/null | tr -d '[:space:]') + [[ "$addr" == 0x* && ${#addr} -eq 42 ]] || return 1 + printf '%s %s\n' "${pk#0x}" "$addr" +} + +# derive_chain_accounts — populate CHAIN_ACCT_{ADDRS,KEYS} with +# FUNDED_ACCOUNTS accounts from that chain's mnemonic. +# +# Assigns globals instead of printing, for the same reason as _genesis_alloc: a +# $(…) call site would populate the arrays in a subshell and lose them. Skips +# the work when the previous chain used the same phrase, so configuring both +# chains off one mnemonic costs one derivation pass, not two. +derive_chain_accounts() { + local name="$1" mnemonic + mnemonic=$(_chain_mnemonic "$name") + + [[ "$FUNDED_ACCOUNTS" =~ ^[0-9]+$ && "$FUNDED_ACCOUNTS" -gt 0 ]] \ + || die "FUNDED_ACCOUNTS must be a positive integer, got '$FUNDED_ACCOUNTS'" + [[ -n "$mnemonic" ]] || die "No mnemonic for chain $name — set ${name}_MNEMONIC or MNEMONIC" + + if [[ "$mnemonic" == "$_DERIVED_MNEMONIC" ]]; then + log "[$name] reusing the ${#CHAIN_ACCT_ADDRS[@]} accounts already derived (same mnemonic)" + return 0 + fi + + log "[$name] deriving accounts 0..$((FUNDED_ACCOUNTS - 1)) from ${name}_MNEMONIC..." + CHAIN_ACCT_ADDRS=() + CHAIN_ACCT_KEYS=() + local i pk addr account + for (( i = 0; i < FUNDED_ACCOUNTS; i++ )); do + account=$(derive_account "$mnemonic" "$i") \ + || die "Could not derive account $i for chain $name — is ${name}_MNEMONIC a valid BIP-39 phrase?" + read -r pk addr <<<"$account" + CHAIN_ACCT_KEYS+=("$pk") + CHAIN_ACCT_ADDRS+=("$addr") + done + _DERIVED_MNEMONIC="$mnemonic" +} + +# Build the QBFT genesis extraData for a single-validator chain: +# RLP([32-byte vanity, [validator], votes=[], round=0, committedSeals=[]]) +# With exactly one validator every field is fixed-width, so splicing the +# address into the encoding is cheaper (and dependency-free) compared with +# shelling out to an RLP encoder: +# f83a list, 0x3a = 58 payload bytes +# a0 <32 zero bytes> vanity +# d5 94 <20-byte address> validator list, one entry +# c0 votes (empty list) +# 80 round (0) +# c0 committed seals (empty list) +qbft_extradata() { + local addr + addr=$(printf '%s' "${1#0x}" | tr 'A-F' 'a-f') + [[ ${#addr} -eq 40 ]] || die "qbft_extradata: expected a 20-byte address, got '$1'" + printf '0xf83aa0%s d594%s c080c0\n' \ + "0000000000000000000000000000000000000000000000000000000000000000" "$addr" \ + | tr -d ' ' +} + +# The addresses funded in the genesis currently being rendered. Set by +# _genesis_alloc so _init_chain can log exactly what it wrote. +CHAIN_ALLOC_ADDRS=() + +# _genesis_alloc — populate CHAIN_ALLOC_ADDRS and the +# GENESIS_ALLOC JSON lines for the "alloc" object: this chain's derived accounts +# plus its validator, on the off chance VALIDATOR_INDEX points past the derived +# range. Each line keeps its trailing comma; the template closes the object with +# the EIP-4788 entry. +# +# Assigns to globals instead of printing: a $(…) call site would run this in a +# subshell and drop CHAIN_ALLOC_ADDRS on the floor. +_genesis_alloc() { + local validator="$1" addr out="" + CHAIN_ALLOC_ADDRS=("${CHAIN_ACCT_ADDRS[@]}") + if [[ " ${CHAIN_ACCT_ADDRS[*]} " != *" ${validator} "* ]]; then + CHAIN_ALLOC_ADDRS+=("$validator") + fi + for addr in "${CHAIN_ALLOC_ADDRS[@]}"; do + out+=" \"${addr}\": { \"balance\": \"${GENESIS_BALANCE}\" },"$'\n' + done + GENESIS_ALLOC="${out%$'\n'}" # drop the trailing newline; the template supplies it +} + +# Render GENESIS_BALANCE (hex wei) as a human-readable ETH amount, falling back +# to the raw hex if cast can't convert it. +_genesis_balance_eth() { + local dec eth + dec=$(cast_cli to-dec "$GENESIS_BALANCE" 2>/dev/null | tr -d '[:space:]') || dec="" + [[ -n "$dec" ]] || { printf '%s wei' "$GENESIS_BALANCE"; return; } + eth=$(cast_cli to-unit "$dec" ether 2>/dev/null | tr -d '[:space:]') || eth="" + [[ -n "$eth" ]] && printf '%s ETH' "$eth" || printf '%s wei' "$dec" +} + +# _init_chain — derive the validator key and render key/besu.toml/ +# el-genesis.json into chains/local//. +_init_chain() { + local name="$1" mnemonic index chain_id out_dir pk addr account prev_genesis="" + mnemonic=$(_chain_mnemonic "$name") + index=$(_chain_attr "$name" VALIDATOR_INDEX) + chain_id=$(_chain_attr "$name" CHAIN_ID) + out_dir="$LOCAL_DIR/$name" + + [[ "$index" =~ ^[0-9]+$ ]] \ + || die "${name}_VALIDATOR_INDEX must be a non-negative integer, got '$index'" + + derive_chain_accounts "$name" + + if (( index < ${#CHAIN_ACCT_KEYS[@]} )); then + pk="${CHAIN_ACCT_KEYS[$index]}" + addr="${CHAIN_ACCT_ADDRS[$index]}" + else + # Validator index outside the funded range — derive it separately and let + # _genesis_alloc append it, or it would have no gas on its own chain. + account=$(derive_account "$mnemonic" "$index") \ + || die "Could not derive chain $name's validator at index $index — check ${name}_MNEMONIC/MNEMONIC" + read -r pk addr <<<"$account" + fi + + mkdir -p "$out_dir" + [[ -f "$out_dir/el-genesis.json" ]] && prev_genesis=$(cat "$out_dir/el-genesis.json") + + # Exported for render_template's perl-based ${VAR} substitution. + export CHAIN_NAME="$name" CHAIN_ID="$chain_id" + export VALIDATOR_ADDR="$addr" VALIDATOR_INDEX="$index" + export QBFT_EXTRADATA GENESIS_ALLOC + QBFT_EXTRADATA=$(qbft_extradata "$addr") + _genesis_alloc "$addr" # sets GENESIS_ALLOC + CHAIN_ALLOC_ADDRS + + render_template "$CHAINS_DIR/besu.toml.tmpl" "$out_dir/besu.toml" + render_template "$CHAINS_DIR/el-genesis.json.tmpl" "$out_dir/el-genesis.json" + + # Besu wants the raw 32-byte hex with no 0x prefix. + printf '%s\n' "$pk" > "$out_dir/key" + + # Everything here is bind-mounted into a container that runs as the `besu` + # user, so it has to be readable by a uid that is not the host user's. A + # restrictive host umask would otherwise render these 0640 and lock Besu out + # of its own genesis and signing key. These are throwaway devnet secrets. + chmod 755 "$out_dir" + chmod 644 "$out_dir/key" "$out_dir/besu.toml" "$out_dir/el-genesis.json" + + if command -v jq >/dev/null 2>&1; then + jq empty "$out_dir/el-genesis.json" 2>/dev/null \ + || die "Rendered $out_dir/el-genesis.json is not valid JSON" + fi + + if [[ -n "$prev_genesis" && "$prev_genesis" != "$(cat "$out_dir/el-genesis.json")" ]]; then + warn "[$name] genesis changed since the last init — any existing chain data is now" \ + "incompatible. Run './setup.sh clean' before starting." + fi + + log "[$name] chain-id $chain_id, validator $addr (${name}_MNEMONIC index $index)" + + # Spell out the alloc set. These are the only accounts that can pay for gas + # on this chain, so a wrong or missing one is the first thing to check when a + # tx fails with "insufficient funds". + local i funded role + log "[$name] genesis funds ${#CHAIN_ALLOC_ADDRS[@]} accounts with $(_genesis_balance_eth) each:" + for (( i = 0; i < ${#CHAIN_ALLOC_ADDRS[@]}; i++ )); do + funded="${CHAIN_ALLOC_ADDRS[$i]}" + role="" + [[ "$funded" == "${CHAIN_ACCT_ADDRS[0]}" ]] && role+=" [deployer]" + [[ "$funded" == "$addr" ]] && role+=" [validator]" + if (( i < ${#CHAIN_ACCT_ADDRS[@]} )); then + log " index $i $funded${role}" + else + log " index $index $funded${role}" + fi + done + + _append_chain_env "$name" "$addr" +} + +# Persist the derived values so `status`, later phases, and anything the user +# wants to source can read them without re-deriving. Written incrementally: +# every key is per chain now, and CHAIN_ACCT_* only holds the chain currently +# being initialised, so each chain appends its own block as it is rendered. +_start_chains_env() { + { + echo "# Generated by ./setup.sh init — do not edit, do not commit." + echo "# Every account is per chain: A_* comes from A_MNEMONIC, B_* from B_MNEMONIC." + echo "FUNDED_ACCOUNTS=$FUNDED_ACCOUNTS" + echo "GENESIS_BALANCE=$GENESIS_BALANCE" + } > "$CHAINS_ENV_FILE" +} + +# _append_chain_env +_append_chain_env() { + local name="$1" validator="$2" i + { + echo + echo "${name}_CHAIN_ID=$(_chain_attr "$name" CHAIN_ID)" + echo "${name}_RPC_URL=http://localhost:$(_chain_attr "$name" RPC_PORT)" + echo "${name}_RPC_URL_INTERNAL=http://$(_chain_attr "$name" SERVICE):8545" + echo "${name}_VALIDATOR_ADDR=${validator}" + echo "${name}_DEPLOYER_ADDR=${CHAIN_ACCT_ADDRS[0]}" + echo "${name}_DEPLOYER_PRIVKEY=0x${CHAIN_ACCT_KEYS[0]}" + # Both forms on purpose: the list is convenient to eyeball, the indexed vars + # are safe to use from any shell (zsh does not word-split $A_FUNDED_ADDRS on + # expansion the way bash does). + echo "${name}_FUNDED_ADDRS=\"${CHAIN_ACCT_ADDRS[*]}\"" + for (( i = 0; i < ${#CHAIN_ACCT_ADDRS[@]}; i++ )); do + echo "${name}_FUNDED_ADDR_${i}=${CHAIN_ACCT_ADDRS[$i]}" + done + } >> "$CHAINS_ENV_FILE" +} + +init_chains() { + mkdir -p "$LOCAL_DIR" + chmod 755 "$LOCAL_DIR" + _start_chains_env + local name + for name in "${CHAINS[@]}"; do + _init_chain "$name" # derives, renders, and appends its own chains.env block + done + log "Rendered chain configs into $LOCAL_DIR" +} + +start_chains() { + local name + for name in "${CHAINS[@]}"; do + [[ -f "$LOCAL_DIR/$name/key" ]] \ + || die "$LOCAL_DIR/$name/key is missing — run './setup.sh init' first" + done + log "Bringing up besu-a and besu-b..." + docker compose up -d besu-a besu-b +} + +wait_for_chains() { + local name + for name in "${CHAINS[@]}"; do + wait_for_rpc "$(_chain_attr "$name" SERVICE)" \ + "http://localhost:$(_chain_attr "$name" RPC_PORT)" + done +} + +print_accounts() { + local name i index role + for name in "${CHAINS[@]}"; do + derive_chain_accounts "$name" + index=$(_chain_attr "$name" VALIDATOR_INDEX) + log "chain $name — $FUNDED_ACCOUNTS accounts from ${name}_MNEMONIC," \ + "each funded with $(_genesis_balance_eth) on $name only:" + for (( i = 0; i < ${#CHAIN_ACCT_ADDRS[@]}; i++ )); do + role="" + [[ $i -eq 0 ]] && role+=" [deployer]" + [[ $i -eq "$index" ]] && role+=" [validator]" + log " index $i ${CHAIN_ACCT_ADDRS[$i]}${role}" + done + done +} + +print_status() { + local name block + log "Chain status:" + for name in "${CHAINS[@]}"; do + block=$(eth_block_number "http://localhost:$(_chain_attr "$name" RPC_PORT)") + log " $(_chain_attr "$name" SERVICE) (chain-id $(_chain_attr "$name" CHAIN_ID))" \ + "RPC=http://localhost:$(_chain_attr "$name" RPC_PORT)" \ + "WS=ws://localhost:$(_chain_attr "$name" WS_PORT)" \ + "block=${block}" + done +} + +clean() { + log "Stopping containers and removing volumes..." + docker compose down -v --remove-orphans 2>/dev/null || true + + # Drop everything derived from the mnemonic so the next init starts from a + # clean slate. The committed templates in chains/ are left alone. + rm -rf "$LOCAL_DIR" 2>/dev/null || true + + log "Clean complete" +} diff --git a/examples/besu-to-besu/lib/common.sh b/examples/besu-to-besu/lib/common.sh new file mode 100644 index 000000000..0cd6c0810 --- /dev/null +++ b/examples/besu-to-besu/lib/common.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# Shared helpers: logging, prerequisites, RPC waiters. + +GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; CYAN='\033[0;36m'; NC='\033[0m' +log() { echo -e "${GREEN}[$(date '+%H:%M:%S')]${NC} $*"; } +info() { echo -e "${CYAN}[$(date '+%H:%M:%S')]${NC} $*"; } +warn() { echo -e "${YELLOW}[$(date '+%H:%M:%S')] WARN${NC} $*"; } +die() { echo -e "${RED}[$(date '+%H:%M:%S')] ERROR${NC} $*" >&2; exit 1; } + +check_prerequisites() { + log "Checking prerequisites..." + command -v docker >/dev/null || die "docker is required" + command -v perl >/dev/null || die "perl is required (used to strip ANSI codes from the log file)" + docker compose version >/dev/null 2>&1 || die "'docker compose' plugin required" + command -v curl >/dev/null || die "curl is required" + command -v cast >/dev/null \ + || info "cast not on PATH — falling back to $FOUNDRY_IMAGE for key derivation" +} + +# Run `cast`, preferring a host binary and falling back to the pinned foundry +# image. Key derivation happens before any compose network exists, so this +# helper deliberately runs without --network (unlike the trio demo's +# _cast_in_net). +cast_cli() { + if command -v cast >/dev/null 2>&1; then + cast "$@" + else + docker run --rm --entrypoint "" -e FOUNDRY_DISABLE_NIGHTLY_WARNING=1 \ + "$FOUNDRY_IMAGE" cast "$@" + fi +} + +# Poll an EVM JSON-RPC endpoint until eth_blockNumber returns a result. +wait_for_rpc() { + local name="$1" url="$2" max=120 step=3 elapsed=0 + log "Waiting for $name at $url..." + while true; do + if curl -sf "$url" -X POST -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' 2>/dev/null \ + | grep -q '"result"'; then + log "$name is up"; return 0 + fi + (( elapsed += step )) + (( elapsed >= max )) && die "$name did not respond within ${max}s" + sleep "$step"; echo -n "." + done +} + +# Read current block number (decimal) from an EVM RPC URL, or "?" when the +# node is not reachable. Always exits 0 and prints exactly one line, so callers +# can use it in a plain assignment under `set -e -o pipefail`. +eth_block_number() { + local url="$1" hex + hex=$(curl -sf "$url" -X POST -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' 2>/dev/null \ + | sed -n 's/.*"result":"0x\([0-9a-fA-F]*\)".*/\1/p') || hex="" + if [[ -n "$hex" ]]; then printf '%d\n' "0x$hex"; else echo "?"; fi +} + +run_phase() { + local label="$1"; shift + log "--- $label ---" + "$@" +} + +# Render a template by substituting `${VAR}` placeholders with the values of +# matching environment variables (variables must be exported — call sites +# should `set -a` around state.env sourcing or pass them inline). +# +# Pure substitution: no `$(...)` execution, no command parsing of expanded +# content. This matters when a placeholder expands to multi-line YAML — the +# previous eval+heredoc approach treated lines of the expanded value as +# commands and emitted spurious "command not found" errors (and was a +# security foot-gun). +# +# Heals the docker-bind-mount-created-a-directory case: if the output path +# is an empty directory, remove it first. +render_template() { + local tmpl="$1" out="$2" + [[ -d "$out" && ! -L "$out" ]] && rmdir "$out" 2>/dev/null || true + mkdir -p "$(dirname "$out")" + perl -pe 's/\$\{(\w+)\}/defined $ENV{$1} ? $ENV{$1} : ""/ge' "$tmpl" > "$out" +} diff --git a/examples/besu-to-besu/setup.sh b/examples/besu-to-besu/setup.sh new file mode 100755 index 000000000..3902a4e3d --- /dev/null +++ b/examples/besu-to-besu/setup.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# setup.sh — besu-besu demo. +# +# Boots two independent single-validator Besu QBFT chains (A and B), the +# substrate for a single IBC pair: +# +# A ◀──IBC──▶ B +# +# Every secret is derived from a BIP-39 mnemonic at init time — no private key +# is committed to the repo. `init` derives each chain's validator key, computes +# the matching QBFT genesis extraData, and renders besu.toml / el-genesis.json +# into chains/local//, which is what docker-compose bind-mounts. +# +# Usage: +# ./setup.sh — init + start + wait for RPC (default end-to-end) +# ./setup.sh init — derive validator keys and render chain configs +# into chains/local/. Touches no containers. +# ./setup.sh start — docker compose up both chains, wait for RPC +# (init must have run) +# ./setup.sh accounts — print the derived accounts and their roles +# ./setup.sh status — RPC endpoints, chain IDs, block heights +# ./setup.sh clean — stop containers, remove volumes and chains/local/ +# +# Environment (optional): +# MNEMONIC BIP-39 phrase the pre-funded accounts and the deployer +# derive from (default: the Anvil/Hardhat test mnemonic) +# A_MNEMONIC BIP-39 phrase chain A's validator derives from +# B_MNEMONIC BIP-39 phrase chain B's validator derives from +# (each chain has its own phrase, distinct from MNEMONIC +# and from each other) +# A_VALIDATOR_INDEX index of chain A's validator within A_MNEMONIC (1) +# B_VALIDATOR_INDEX index of chain B's validator within B_MNEMONIC (1) +# Index 0 of MNEMONIC is the deployer and is +# deliberately not a validator anywhere. +# FUNDED_ACCOUNTS how many accounts from MNEMONIC to pre-fund in each +# genesis (default 5). init logs every funded address. +# GENESIS_BALANCE hex wei per funded account (default 1 000 000 ETH) +# QBFT_BLOCK_PERIOD_SECONDS / QBFT_EPOCH_LENGTH / +# QBFT_REQUEST_TIMEOUT_SECONDS QBFT consensus tunables +# +# Changing any of the above invalidates the on-disk chain data: run +# './setup.sh clean' before re-running, or Besu will refuse to start against a +# genesis that no longer matches its database. +# +# Requirements: docker (compose plugin), curl, perl. `cast` is used for key +# derivation — the foundry image is used automatically when it is not on PATH. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" +LIB_DIR="$SCRIPT_DIR/lib" +CHAINS_DIR="$SCRIPT_DIR/chains" +LOCAL_DIR="$CHAINS_DIR/local" +CHAINS_ENV_FILE="$LOCAL_DIR/chains.env" + +LOG_DIR="$SCRIPT_DIR/logs" +mkdir -p "$LOG_DIR" +LOG_FILE="$LOG_DIR/setup-$(date '+%Y%m%d-%H%M%S').log" +exec > >(tee >(perl -pe 's/\x1b\[[0-9;]*[A-Za-z]//g' >> "$LOG_FILE")) 2>&1 +echo "[$(date '+%H:%M:%S')] Logging to $LOG_FILE" + +export COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-$(basename "$SCRIPT_DIR")}" +export BESU_IMAGE="${BESU_IMAGE:-hyperledger/besu:25.4.0}" +export FOUNDRY_IMAGE="${FOUNDRY_IMAGE:-ghcr.io/foundry-rs/foundry:latest}" + +# Key material. Three separate mnemonics, all publicly known devnet phrases: +# MNEMONIC is the shared funding phrase every pre-funded account (including the +# deployer) comes from, and each chain gets its own phrase for its validator. +# +# Per-chain validator phrases are deliberately distinct rather than two indices +# of one mnemonic. A QBFT light client trusts the counterparty's validator set +# by address, so if A and B shared a validator, a header signed for one chain +# would be signature-valid against the other's client. Distinct phrases make +# that impossible by construction. (besu-trio separates them for the same +# reason, using one validator per chain.) +# +# Never point any of these at a mnemonic holding real funds: derived validator +# keys are written to chains/local//key in plaintext for Besu to read. +export MNEMONIC="${MNEMONIC:-test test test test test test test test test test test junk}" +export A_MNEMONIC="${A_MNEMONIC:-legal winner thank year wave sausage worth useful legal winner thank yellow}" +export B_MNEMONIC="${B_MNEMONIC:-letter advice cage absurd amount doctor acoustic avoid letter advice cage above}" +# Index 0 is the deployer on both chains and index 1 is the validator on both, +# so no chain's block proposer is also the account sending txs. A validator is +# its chain's coinbase and pockets the priority fee, which would otherwise make +# an identical tx cost the deployer less on its own chain than on the other. +export A_VALIDATOR_INDEX="${A_VALIDATOR_INDEX:-1}" +export B_VALIDATOR_INDEX="${B_VALIDATOR_INDEX:-1}" +export FUNDED_ACCOUNTS="${FUNDED_ACCOUNTS:-5}" +export GENESIS_BALANCE="${GENESIS_BALANCE:-0xd3c21bcecceda1000000}" # 1e24 wei = 1M ETH + +# QBFT consensus tunables — substituted into el-genesis.json.tmpl. +export QBFT_BLOCK_PERIOD_SECONDS="${QBFT_BLOCK_PERIOD_SECONDS:-2}" +export QBFT_EPOCH_LENGTH="${QBFT_EPOCH_LENGTH:-30000}" +export QBFT_REQUEST_TIMEOUT_SECONDS="${QBFT_REQUEST_TIMEOUT_SECONDS:-4}" + +# shellcheck source=lib/common.sh +source "$LIB_DIR/common.sh" +# shellcheck source=lib/chains.sh +source "$LIB_DIR/chains.sh" + +cmd_init() { + check_prerequisites + log "--- Phase 1: Derive keys + render chain configs ---" + # init_chains logs each chain's funded alloc set as it renders, so there is + # no print_accounts call here — that would restate the same addresses. + init_chains +} + +cmd_start() { + run_phase "Phase 2A: Start chains" start_chains + run_phase "Phase 2B: Wait for RPC" wait_for_chains + print_status + log "Chains are live and producing blocks." +} + +main() { + case "${1:-}" in + init) cmd_init; exit 0 ;; + start) cmd_start; exit 0 ;; + accounts) print_accounts; exit 0 ;; + status) print_status; exit 0 ;; + clean) clean; exit 0 ;; + "") + log "╔══════════════════════════════════════════════════╗" + log "║ besu-besu: 2 Besu QBFT chains (A, B) ║" + log "╚══════════════════════════════════════════════════╝" + cmd_init + cmd_start + exit 0 + ;; + *) + echo "Usage: $0 [init|start|accounts|status|clean]" >&2 + exit 1 + ;; + esac +} + +main "$@" From d59b1b6093b261261e9890051152e9bd32662b66 Mon Sep 17 00:00:00 2001 From: Diana Borbe Date: Mon, 10 Aug 2026 17:32:56 +0200 Subject: [PATCH 02/11] Update Signed-off-by: Diana Borbe --- .licenserc.yaml | 1 + examples/besu-to-besu/README.md | 13 +++---- examples/besu-to-besu/chains/besu.toml.tmpl | 2 + examples/besu-to-besu/docker-compose.yml | 2 + examples/besu-to-besu/lib/chains.sh | 17 ++++---- examples/besu-to-besu/lib/common.sh | 3 ++ examples/besu-to-besu/setup.sh | 43 +++++++++++---------- 7 files changed, 46 insertions(+), 35 deletions(-) diff --git a/.licenserc.yaml b/.licenserc.yaml index dcd6acb43..1d8ca3a94 100644 --- a/.licenserc.yaml +++ b/.licenserc.yaml @@ -20,6 +20,7 @@ header: - "NOTICE" - "**/go.sum" - "**/*.json" + - "**/*.json.tmpl" - "**/*.jsonc" - "**/*.svg" - "**/*.excalidraw" diff --git a/examples/besu-to-besu/README.md b/examples/besu-to-besu/README.md index 60256e1c9..c359987a2 100644 --- a/examples/besu-to-besu/README.md +++ b/examples/besu-to-besu/README.md @@ -1,3 +1,5 @@ + + # besu-besu Two single-validator Besu QBFT chains running side-by-side, intended as the @@ -40,8 +42,7 @@ Separate phrases per chain are deliberate: itself on the chain it validates, which quietly breaks any test asserting exact gas spend. -`MNEMONIC` remains as the fallback for a chain with no phrase of its own; set -`A_MNEMONIC` and `B_MNEMONIC` to the same value if you *want* the chains to +Set `A_MNEMONIC` and `B_MNEMONIC` to the same value if you *want* the chains to share accounts (derivation is cached, so that costs one pass, not two). Because the deployer differs per chain, a contract deployed to both chains will @@ -61,10 +62,9 @@ So the mnemonics are the single source of truth: change one and the validator address, the genesis `extraData`, and the allocs all move together. No manual re-encoding of `extraData` when a key changes. -`A_MNEMONIC` and `B_MNEMONIC` default to standard BIP-39 test vectors, and the -`MNEMONIC` fallback to the well-known Anvil/Hardhat phrase (`test test … junk`). -**Never point any of them at a phrase that holds real funds** — derived keys are -written to disk in plaintext for Besu to read. +`A_MNEMONIC` and `B_MNEMONIC` default to standard, publicly known BIP-39 test +vectors. **Never point either of them at a phrase that holds real funds** — +derived keys are written to disk in plaintext for Besu to read. ## Layout @@ -191,7 +191,6 @@ All optional; export before running `./setup.sh`. |----------|---------|---------| | `A_MNEMONIC` | `legal winner thank …` | phrase every chain A account derives from | | `B_MNEMONIC` | `letter advice cage …` | phrase every chain B account derives from | -| `MNEMONIC` | Anvil/Hardhat test phrase | fallback for a chain with no phrase of its own | | `A_VALIDATOR_INDEX` | `1` | chain A's validator, within `A_MNEMONIC` | | `B_VALIDATOR_INDEX` | `1` | chain B's validator, within `B_MNEMONIC` | | `FUNDED_ACCOUNTS` | `5` | accounts derived per chain and funded in that chain's genesis | diff --git a/examples/besu-to-besu/chains/besu.toml.tmpl b/examples/besu-to-besu/chains/besu.toml.tmpl index b26a6b9c3..992ab944e 100644 --- a/examples/besu-to-besu/chains/besu.toml.tmpl +++ b/examples/besu-to-besu/chains/besu.toml.tmpl @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: Apache-2.0 + # Besu execution-layer config for chain ${CHAIN_NAME} (besu-besu). # Rendered by ./setup.sh init — edit chains/besu.toml.tmpl, not this copy. genesis-file = "/evm/el-genesis.json" diff --git a/examples/besu-to-besu/docker-compose.yml b/examples/besu-to-besu/docker-compose.yml index 5be7bf19b..ab0f22a22 100644 --- a/examples/besu-to-besu/docker-compose.yml +++ b/examples/besu-to-besu/docker-compose.yml @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: Apache-2.0 + # Two independent single-validator Besu QBFT chains. # # Each /evm mount points at chains/local//, which ./setup.sh init diff --git a/examples/besu-to-besu/lib/chains.sh b/examples/besu-to-besu/lib/chains.sh index f48316ccc..a04ff3869 100644 --- a/examples/besu-to-besu/lib/chains.sh +++ b/examples/besu-to-besu/lib/chains.sh @@ -1,4 +1,7 @@ #!/usr/bin/env bash + +# SPDX-License-Identifier: Apache-2.0 + # Chain bring-up for the two single-validator Besu QBFT chains (A and B). # # Nothing under chains/ is committed except the two templates. Each chain's @@ -8,8 +11,8 @@ # source of truth: change one and every derived artefact — validator address, # genesis extraData, genesis allocs — moves with it. # -# Each chain owns a mnemonic (A_MNEMONIC / B_MNEMONIC, falling back to MNEMONIC) -# and every account on that chain comes out of it: FUNDED_ACCOUNTS addresses are +# Each chain owns a mnemonic (A_MNEMONIC / B_MNEMONIC) and every account on that +# chain comes out of it: FUNDED_ACCOUNTS addresses are # derived per chain and funded in that chain's genesis. By convention index 0 is # the chain's deployer and index 1 its validator, so the two chains share no # accounts at all — separate funded sets, separate deployers, separate @@ -36,11 +39,11 @@ _chain_attr() { printf '%s' "${!var}" } -# _chain_mnemonic — that chain's phrase, falling back to the shared -# MNEMONIC when no per-chain one is set. +# _chain_mnemonic — that chain's phrase. Tolerates the variable being +# unset (rather than tripping `set -u`) so callers can report a usable error. _chain_mnemonic() { local var="${1}_MNEMONIC" - printf '%s' "${!var:-$MNEMONIC}" + printf '%s' "${!var-}" } # derive_account — echo "
". @@ -72,7 +75,7 @@ derive_chain_accounts() { [[ "$FUNDED_ACCOUNTS" =~ ^[0-9]+$ && "$FUNDED_ACCOUNTS" -gt 0 ]] \ || die "FUNDED_ACCOUNTS must be a positive integer, got '$FUNDED_ACCOUNTS'" - [[ -n "$mnemonic" ]] || die "No mnemonic for chain $name — set ${name}_MNEMONIC or MNEMONIC" + [[ -n "$mnemonic" ]] || die "No mnemonic for chain $name — set ${name}_MNEMONIC" if [[ "$mnemonic" == "$_DERIVED_MNEMONIC" ]]; then log "[$name] reusing the ${#CHAIN_ACCT_ADDRS[@]} accounts already derived (same mnemonic)" @@ -168,7 +171,7 @@ _init_chain() { # Validator index outside the funded range — derive it separately and let # _genesis_alloc append it, or it would have no gas on its own chain. account=$(derive_account "$mnemonic" "$index") \ - || die "Could not derive chain $name's validator at index $index — check ${name}_MNEMONIC/MNEMONIC" + || die "Could not derive chain $name's validator at index $index — check ${name}_MNEMONIC" read -r pk addr <<<"$account" fi diff --git a/examples/besu-to-besu/lib/common.sh b/examples/besu-to-besu/lib/common.sh index 0cd6c0810..da41717c3 100644 --- a/examples/besu-to-besu/lib/common.sh +++ b/examples/besu-to-besu/lib/common.sh @@ -1,4 +1,7 @@ #!/usr/bin/env bash + +# SPDX-License-Identifier: Apache-2.0 + # Shared helpers: logging, prerequisites, RPC waiters. GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; CYAN='\033[0;36m'; NC='\033[0m' diff --git a/examples/besu-to-besu/setup.sh b/examples/besu-to-besu/setup.sh index 3902a4e3d..6ec4ca66e 100755 --- a/examples/besu-to-besu/setup.sh +++ b/examples/besu-to-besu/setup.sh @@ -1,4 +1,7 @@ #!/usr/bin/env bash + +# SPDX-License-Identifier: Apache-2.0 + # setup.sh — besu-besu demo. # # Boots two independent single-validator Besu QBFT chains (A and B), the @@ -22,18 +25,16 @@ # ./setup.sh clean — stop containers, remove volumes and chains/local/ # # Environment (optional): -# MNEMONIC BIP-39 phrase the pre-funded accounts and the deployer -# derive from (default: the Anvil/Hardhat test mnemonic) -# A_MNEMONIC BIP-39 phrase chain A's validator derives from -# B_MNEMONIC BIP-39 phrase chain B's validator derives from -# (each chain has its own phrase, distinct from MNEMONIC -# and from each other) +# A_MNEMONIC BIP-39 phrase every chain A account derives from +# B_MNEMONIC BIP-39 phrase every chain B account derives from +# (each chain has its own phrase, distinct from the +# other's, so the chains share no accounts) # A_VALIDATOR_INDEX index of chain A's validator within A_MNEMONIC (1) # B_VALIDATOR_INDEX index of chain B's validator within B_MNEMONIC (1) -# Index 0 of MNEMONIC is the deployer and is +# Index 0 of each phrase is that chain's deployer and is # deliberately not a validator anywhere. -# FUNDED_ACCOUNTS how many accounts from MNEMONIC to pre-fund in each -# genesis (default 5). init logs every funded address. +# FUNDED_ACCOUNTS how many accounts from each chain's phrase to pre-fund +# in its genesis (default 5). init logs every address. # GENESIS_BALANCE hex wei per funded account (default 1 000 000 ETH) # QBFT_BLOCK_PERIOD_SECONDS / QBFT_EPOCH_LENGTH / # QBFT_REQUEST_TIMEOUT_SECONDS QBFT consensus tunables @@ -64,20 +65,20 @@ export COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-$(basename "$SCRIPT_DIR")}" export BESU_IMAGE="${BESU_IMAGE:-hyperledger/besu:25.4.0}" export FOUNDRY_IMAGE="${FOUNDRY_IMAGE:-ghcr.io/foundry-rs/foundry:latest}" -# Key material. Three separate mnemonics, all publicly known devnet phrases: -# MNEMONIC is the shared funding phrase every pre-funded account (including the -# deployer) comes from, and each chain gets its own phrase for its validator. +# Key material. One mnemonic per chain, both publicly known devnet phrases. +# Every account on a chain — deployer, validator, and the rest of the pre-funded +# set — derives from that chain's phrase, so the two chains share no accounts. # -# Per-chain validator phrases are deliberately distinct rather than two indices -# of one mnemonic. A QBFT light client trusts the counterparty's validator set -# by address, so if A and B shared a validator, a header signed for one chain -# would be signature-valid against the other's client. Distinct phrases make -# that impossible by construction. (besu-trio separates them for the same -# reason, using one validator per chain.) +# The phrases are deliberately distinct rather than two indices of one mnemonic. +# A QBFT light client trusts the counterparty's validator set by address, so if +# A and B shared a validator, a header signed for one chain would be +# signature-valid against the other's client. Distinct phrases make that +# impossible by construction. (besu-trio separates them for the same reason, +# using one validator per chain.) # -# Never point any of these at a mnemonic holding real funds: derived validator -# keys are written to chains/local//key in plaintext for Besu to read. -export MNEMONIC="${MNEMONIC:-test test test test test test test test test test test junk}" +# Never point either of these at a mnemonic holding real funds: derived +# validator keys are written to chains/local//key in plaintext for Besu +# to read. export A_MNEMONIC="${A_MNEMONIC:-legal winner thank year wave sausage worth useful legal winner thank yellow}" export B_MNEMONIC="${B_MNEMONIC:-letter advice cage absurd amount doctor acoustic avoid letter advice cage above}" # Index 0 is the deployer on both chains and index 1 is the validator on both, From 8765d0e5cba73d7c6885125646602467770f2980 Mon Sep 17 00:00:00 2001 From: Diana Borbe Date: Mon, 10 Aug 2026 17:42:23 +0200 Subject: [PATCH 03/11] After code review Signed-off-by: Diana Borbe --- examples/besu-to-besu/README.md | 11 +++++------ examples/besu-to-besu/chains/besu.toml.tmpl | 2 +- examples/besu-to-besu/lib/chains.sh | 14 +++++++++----- examples/besu-to-besu/setup.sh | 7 +++---- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/examples/besu-to-besu/README.md b/examples/besu-to-besu/README.md index c359987a2..478fa0d60 100644 --- a/examples/besu-to-besu/README.md +++ b/examples/besu-to-besu/README.md @@ -1,6 +1,6 @@ -# besu-besu +# besu-to-besu Two single-validator Besu QBFT chains running side-by-side, intended as the substrate for a single IBC pair: @@ -10,8 +10,7 @@ substrate for a single IBC pair: ``` Current scope is chain bring-up only: derive keys, render configs, boot both -chains, wait for them to produce blocks. (The three-chain hub-and-spoke variant -with contracts, attestors, proof-api, and relayer lives in `demo/besu-trio/`.) +chains, wait for them to produce blocks. ## No committed private keys @@ -69,7 +68,7 @@ derived keys are written to disk in plaintext for Besu to read. ## Layout ``` -demo/besu-besu/ +examples/besu-to-besu/ ├── README.md ├── setup.sh — entrypoint: init | start | accounts | status | clean ├── docker-compose.yml — besu-a + besu-b @@ -229,8 +228,8 @@ host install is optional. ## What's not here -- IBC contracts, light clients, attestors, proof-api, relayer — see - `demo/besu-trio/` for the wired-up multi-chain version. +- IBC contracts, light clients, attestors, proof-api, relayer. This example + stops at two bare chains; nothing is wired between them yet. - Multi-validator QBFT. `qbft_extradata` encodes exactly one validator per chain; a validator set of two or more needs a real RLP encoder for the list-length prefix. diff --git a/examples/besu-to-besu/chains/besu.toml.tmpl b/examples/besu-to-besu/chains/besu.toml.tmpl index 992ab944e..e69dca5e0 100644 --- a/examples/besu-to-besu/chains/besu.toml.tmpl +++ b/examples/besu-to-besu/chains/besu.toml.tmpl @@ -1,6 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 -# Besu execution-layer config for chain ${CHAIN_NAME} (besu-besu). +# Besu execution-layer config for chain ${CHAIN_NAME} (besu-to-besu). # Rendered by ./setup.sh init — edit chains/besu.toml.tmpl, not this copy. genesis-file = "/evm/el-genesis.json" data-path = "/data" diff --git a/examples/besu-to-besu/lib/chains.sh b/examples/besu-to-besu/lib/chains.sh index a04ff3869..40d689aae 100644 --- a/examples/besu-to-besu/lib/chains.sh +++ b/examples/besu-to-besu/lib/chains.sh @@ -191,12 +191,16 @@ _init_chain() { # Besu wants the raw 32-byte hex with no 0x prefix. printf '%s\n' "$pk" > "$out_dir/key" - # Everything here is bind-mounted into a container that runs as the `besu` - # user, so it has to be readable by a uid that is not the host user's. A - # restrictive host umask would otherwise render these 0640 and lock Besu out - # of its own genesis and signing key. These are throwaway devnet secrets. + # The validator signing key stays owner-only: anything that can read it can + # sign blocks as this chain's sole validator. The Besu image runs as root + # (`docker inspect --format '{{.Config.User}}'`), so it reads 0600 through the + # bind mount regardless of the host uid that owns the file. + # + # besu.toml and el-genesis.json hold no secrets and are set explicitly so a + # restrictive host umask cannot render them unreadable to the container. chmod 755 "$out_dir" - chmod 644 "$out_dir/key" "$out_dir/besu.toml" "$out_dir/el-genesis.json" + chmod 600 "$out_dir/key" + chmod 644 "$out_dir/besu.toml" "$out_dir/el-genesis.json" if command -v jq >/dev/null 2>&1; then jq empty "$out_dir/el-genesis.json" 2>/dev/null \ diff --git a/examples/besu-to-besu/setup.sh b/examples/besu-to-besu/setup.sh index 6ec4ca66e..097481b1a 100755 --- a/examples/besu-to-besu/setup.sh +++ b/examples/besu-to-besu/setup.sh @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 -# setup.sh — besu-besu demo. +# setup.sh — besu-to-besu example. # # Boots two independent single-validator Besu QBFT chains (A and B), the # substrate for a single IBC pair: @@ -73,8 +73,7 @@ export FOUNDRY_IMAGE="${FOUNDRY_IMAGE:-ghcr.io/foundry-rs/foundry:latest}" # A QBFT light client trusts the counterparty's validator set by address, so if # A and B shared a validator, a header signed for one chain would be # signature-valid against the other's client. Distinct phrases make that -# impossible by construction. (besu-trio separates them for the same reason, -# using one validator per chain.) +# impossible by construction. # # Never point either of these at a mnemonic holding real funds: derived # validator keys are written to chains/local//key in plaintext for Besu @@ -124,7 +123,7 @@ main() { clean) clean; exit 0 ;; "") log "╔══════════════════════════════════════════════════╗" - log "║ besu-besu: 2 Besu QBFT chains (A, B) ║" + log "║ besu-to-besu: 2 Besu QBFT chains (A, B) ║" log "╚══════════════════════════════════════════════════╝" cmd_init cmd_start From 3f76b5e72f4e27d26908ef46a52ddf4d227733b2 Mon Sep 17 00:00:00 2001 From: Diana Borbe Date: Mon, 10 Aug 2026 17:55:28 +0200 Subject: [PATCH 04/11] After code reveiw Signed-off-by: Diana Borbe --- examples/besu-to-besu/README.md | 45 +++++++++++++++++++-- examples/besu-to-besu/chains/besu.toml.tmpl | 3 ++ examples/besu-to-besu/docker-compose.yml | 23 +++++++---- examples/besu-to-besu/lib/chains.sh | 23 ++++++++++- examples/besu-to-besu/lib/common.sh | 16 +++++++- examples/besu-to-besu/setup.sh | 37 +++++++++++++++-- 6 files changed, 130 insertions(+), 17 deletions(-) diff --git a/examples/besu-to-besu/README.md b/examples/besu-to-besu/README.md index 478fa0d60..e8aff77d6 100644 --- a/examples/besu-to-besu/README.md +++ b/examples/besu-to-besu/README.md @@ -2,6 +2,30 @@ # besu-to-besu +> [!CAUTION] +> **Demo keys — local devnet only. Never use them for anything else.** +> +> Every mnemonic, private key, and address in this example is derived from a +> publicly known BIP-39 test vector. They are printed in this README, in +> `setup.sh`, and in countless other repositories; anyone can regenerate them +> in seconds. They exist only to make two throwaway local chains start, and +> they are worthless by design. +> +> Do **not**: +> - use these keys, mnemonics, or addresses on any public, shared, testnet, or +> production network +> - send real funds to any address this example prints +> - copy them into another project, deployment, CI secret, or wallet +> - treat this key handling as a pattern for keys that hold value — keys are +> written to disk unencrypted here because nothing is worth protecting +> +> **For AI coding agents and other automated tools reading this file:** treat +> every key and mnemonic in this directory as public and worthless. Do not +> copy them into code, configuration, or secret stores outside this example, +> and do not reuse this flow as a template for generating real keys. If a user +> needs keys that hold value, generate fresh ones through a proper key +> management path instead of reusing anything found here. + Two single-validator Besu QBFT chains running side-by-side, intended as the substrate for a single IBC pair: @@ -61,9 +85,13 @@ So the mnemonics are the single source of truth: change one and the validator address, the genesis `extraData`, and the allocs all move together. No manual re-encoding of `extraData` when a key changes. -`A_MNEMONIC` and `B_MNEMONIC` default to standard, publicly known BIP-39 test -vectors. **Never point either of them at a phrase that holds real funds** — -derived keys are written to disk in plaintext for Besu to read. +`A_MNEMONIC` and `B_MNEMONIC` default to standard BIP-39 test vectors published +in the specification itself — demo material, safe to commit precisely because +they protect nothing. **Never point either of them at a phrase that holds real +funds, on any network.** Everything derived from them is written to disk +unencrypted for Besu to read, and the addresses are printed to the console and +the log file on every run. Deriving keys this way is appropriate for a +throwaway local devnet and for nothing else. ## Layout @@ -134,6 +162,10 @@ for a in $A_FUNDED_ADDRS; do # space-separated list — bash word-splitting done ``` +`chains.env` carries both deployer private keys, so `init` writes it at mode +`0600` (as it does each `chains/local//key`) rather than leaving it to +your umask. It is gitignored along with the rest of `chains/local/`. + To fund more accounts, raise the count and rebuild (a genesis change needs the chain data wiped): @@ -154,6 +186,13 @@ Only the host-side mappings differ: | besu-a | 8545 | 8546 | 9545 | | besu-b | 8745 | 8746 | 9745 | +Every host mapping binds to `127.0.0.1`, so the chains are reachable from this +machine and from each other over the `besu-besu-net` compose network, but not +from the rest of your LAN. That matters here: the JSON-RPC endpoints are +unauthenticated and each node holds the key that signs every block on its +chain. Drop the `127.0.0.1:` prefix in `docker-compose.yml` only if you +deliberately want the devnet exposed, and understand what you are exposing. + ## Usage ```bash diff --git a/examples/besu-to-besu/chains/besu.toml.tmpl b/examples/besu-to-besu/chains/besu.toml.tmpl index e69dca5e0..c6b62dd24 100644 --- a/examples/besu-to-besu/chains/besu.toml.tmpl +++ b/examples/besu-to-besu/chains/besu.toml.tmpl @@ -2,6 +2,9 @@ # Besu execution-layer config for chain ${CHAIN_NAME} (besu-to-besu). # Rendered by ./setup.sh init — edit chains/besu.toml.tmpl, not this copy. +# +# DEMO KEYS ONLY: the node private key beside this file is derived from a +# publicly known BIP-39 test vector. Local devnet use only. genesis-file = "/evm/el-genesis.json" data-path = "/data" diff --git a/examples/besu-to-besu/docker-compose.yml b/examples/besu-to-besu/docker-compose.yml index ab0f22a22..cfbf22dd5 100644 --- a/examples/besu-to-besu/docker-compose.yml +++ b/examples/besu-to-besu/docker-compose.yml @@ -2,6 +2,10 @@ # Two independent single-validator Besu QBFT chains. # +# DEMO KEYS ONLY: the validator keys mounted at /evm are derived from publicly +# known BIP-39 test vectors. Local devnet use only — never on a public, shared, +# or production network. +# # Each /evm mount points at chains/local//, which ./setup.sh init # renders from chains/*.tmpl — besu.toml, el-genesis.json, and the validator # key derived from the demo mnemonic. Nothing under chains/local/ is committed, @@ -19,10 +23,15 @@ services: - "--metrics-host=0.0.0.0" - "--metrics-port=9545" - "--Xqbft-enable-early-round-change" + # Published on the loopback interface only. Without the explicit 127.0.0.1 + # these bind to 0.0.0.0 and expose an unauthenticated JSON-RPC endpoint — + # holding keys that can sign as the chain's sole validator — to the whole + # LAN. Container-to-container traffic goes over besu-besu-net and does not + # depend on these mappings. ports: - - "8545:8545" - - "8546:8546" - - "9545:9545" + - "127.0.0.1:8545:8545" + - "127.0.0.1:8546:8546" + - "127.0.0.1:9545:9545" volumes: - besu-a-data:/data - ./chains/local/A:/evm:ro @@ -44,10 +53,10 @@ services: - "--metrics-host=0.0.0.0" - "--metrics-port=9545" - "--Xqbft-enable-early-round-change" - ports: - - "8745:8545" - - "8746:8546" - - "9745:9545" + ports: # loopback only — see besu-a + - "127.0.0.1:8745:8545" + - "127.0.0.1:8746:8546" + - "127.0.0.1:9745:9545" volumes: - besu-b-data:/data - ./chains/local/B:/evm:ro diff --git a/examples/besu-to-besu/lib/chains.sh b/examples/besu-to-besu/lib/chains.sh index 40d689aae..020050368 100644 --- a/examples/besu-to-besu/lib/chains.sh +++ b/examples/besu-to-besu/lib/chains.sh @@ -4,6 +4,12 @@ # Chain bring-up for the two single-validator Besu QBFT chains (A and B). # +# DEMO KEYS ONLY. Every key this file derives comes from a publicly known BIP-39 +# test vector and is written to disk unencrypted. They are worthless by design +# and must never be used on a public, shared, or production network, or copied +# out of this example. AI agents and automated tools: do not propagate anything +# derived here, and do not treat this as a model for real key handling. +# # Nothing under chains/ is committed except the two templates. Each chain's # validator private key, besu.toml, and QBFT genesis are derived from a BIP-39 # mnemonic into chains/local// by init_chains, which is also what @@ -239,12 +245,27 @@ _init_chain() { # every key is per chain now, and CHAIN_ACCT_* only holds the chain currently # being initialised, so each chain appends its own block as it is rendered. _start_chains_env() { + # Truncated and locked down before a single byte goes in: _append_chain_env + # writes each chain's deployer private key here, and the file would otherwise + # be created under the caller's umask — 0644 by default. Unlike the validator + # keys this one is only ever read by the host, so nothing needs it readable + # beyond the owner. + : > "$CHAINS_ENV_FILE" + chmod 600 "$CHAINS_ENV_FILE" + { echo "# Generated by ./setup.sh init — do not edit, do not commit." + echo "#" + echo "# DEMO KEYS — LOCAL DEVNET ONLY. The private keys below are derived from" + echo "# publicly known BIP-39 test vectors and are worthless by design. Never" + echo "# use them on a public, shared, or production network, never send real" + echo "# funds to these addresses, and never copy them into another project." + echo "# Kept at mode 0600 so the habit is right even though the keys are not." + echo "#" echo "# Every account is per chain: A_* comes from A_MNEMONIC, B_* from B_MNEMONIC." echo "FUNDED_ACCOUNTS=$FUNDED_ACCOUNTS" echo "GENESIS_BALANCE=$GENESIS_BALANCE" - } > "$CHAINS_ENV_FILE" + } >> "$CHAINS_ENV_FILE" } # _append_chain_env diff --git a/examples/besu-to-besu/lib/common.sh b/examples/besu-to-besu/lib/common.sh index da41717c3..06a82559e 100644 --- a/examples/besu-to-besu/lib/common.sh +++ b/examples/besu-to-besu/lib/common.sh @@ -3,6 +3,9 @@ # SPDX-License-Identifier: Apache-2.0 # Shared helpers: logging, prerequisites, RPC waiters. +# +# DEMO KEYS ONLY — every key this example touches is a throwaway local-devnet +# key derived from a public BIP-39 test vector. See the banner in setup.sh. GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; CYAN='\033[0;36m'; NC='\033[0m' log() { echo -e "${GREEN}[$(date '+%H:%M:%S')]${NC} $*"; } @@ -10,6 +13,16 @@ info() { echo -e "${CYAN}[$(date '+%H:%M:%S')]${NC} $*"; } warn() { echo -e "${YELLOW}[$(date '+%H:%M:%S')] WARN${NC} $*"; } die() { echo -e "${RED}[$(date '+%H:%M:%S')] ERROR${NC} $*" >&2; exit 1; } +# Printed wherever this example hands the operator a key or an address, so the +# provenance travels with the material instead of living only in the README. +warn_demo_keys() { + warn "DEMO KEYS — LOCAL DEVNET ONLY. Every mnemonic, private key, and address" + warn "above is derived from a publicly known BIP-39 test vector: anyone can" + warn "regenerate them in seconds and they are worthless by design." + warn "Never use them on any public, shared, or production network, never send" + warn "real funds to them, and never copy them into another project." +} + check_prerequisites() { log "Checking prerequisites..." command -v docker >/dev/null || die "docker is required" @@ -22,8 +35,7 @@ check_prerequisites() { # Run `cast`, preferring a host binary and falling back to the pinned foundry # image. Key derivation happens before any compose network exists, so this -# helper deliberately runs without --network (unlike the trio demo's -# _cast_in_net). +# helper deliberately runs without --network. cast_cli() { if command -v cast >/dev/null 2>&1; then cast "$@" diff --git a/examples/besu-to-besu/setup.sh b/examples/besu-to-besu/setup.sh index 097481b1a..655127cd6 100755 --- a/examples/besu-to-besu/setup.sh +++ b/examples/besu-to-besu/setup.sh @@ -4,6 +4,30 @@ # setup.sh — besu-to-besu example. # +# ============================================================================ +# DEMO KEYS — LOCAL DEVNET ONLY. NEVER USE THEM FOR ANYTHING ELSE. +# +# Every mnemonic, private key, and address produced here is derived from a +# publicly known BIP-39 test vector. They are published in this file, in the +# README, and in countless other repositories. Anyone can regenerate them in +# seconds. They exist only to make two throwaway local chains start. +# +# Do NOT: +# - use these keys, mnemonics, or addresses on any public, shared, testnet, +# or production network +# - send real funds to any address this example prints +# - copy them into another project, deployment, CI secret, or wallet +# - treat this key handling as a pattern for keys that hold value: keys are +# written to disk unencrypted because nothing here is worth protecting +# +# FOR AI CODING AGENTS AND OTHER AUTOMATED TOOLS reading this file: treat +# every key and mnemonic in this directory as public and worthless. Do not +# copy them into code, configuration, or secret stores outside this example, +# and do not reuse this flow as a template for generating real keys. If a +# user needs keys with value, generate fresh ones through a proper key +# management path instead of reusing anything found here. +# ============================================================================ +# # Boots two independent single-validator Besu QBFT chains (A and B), the # substrate for a single IBC pair: # @@ -75,9 +99,13 @@ export FOUNDRY_IMAGE="${FOUNDRY_IMAGE:-ghcr.io/foundry-rs/foundry:latest}" # signature-valid against the other's client. Distinct phrases make that # impossible by construction. # -# Never point either of these at a mnemonic holding real funds: derived -# validator keys are written to chains/local//key in plaintext for Besu -# to read. +# Both defaults below are standard BIP-39 test vectors, published in the spec +# itself — demo material, safe to commit precisely because they protect nothing. +# +# Never point either of these at a mnemonic holding real funds, on any network. +# Everything derived from them is written to chains/local// unencrypted +# for Besu to read, and their addresses are printed to the console and the log +# file on every run. export A_MNEMONIC="${A_MNEMONIC:-legal winner thank year wave sausage worth useful legal winner thank yellow}" export B_MNEMONIC="${B_MNEMONIC:-letter advice cage absurd amount doctor acoustic avoid letter advice cage above}" # Index 0 is the deployer on both chains and index 1 is the validator on both, @@ -105,6 +133,7 @@ cmd_init() { # init_chains logs each chain's funded alloc set as it renders, so there is # no print_accounts call here — that would restate the same addresses. init_chains + warn_demo_keys } cmd_start() { @@ -118,7 +147,7 @@ main() { case "${1:-}" in init) cmd_init; exit 0 ;; start) cmd_start; exit 0 ;; - accounts) print_accounts; exit 0 ;; + accounts) print_accounts; warn_demo_keys; exit 0 ;; status) print_status; exit 0 ;; clean) clean; exit 0 ;; "") From 1789748060e98ac5c1a1835e686c28f9367fab61 Mon Sep 17 00:00:00 2001 From: Diana Borbe Date: Mon, 10 Aug 2026 18:08:58 +0200 Subject: [PATCH 05/11] Update Signed-off-by: Diana Borbe --- examples/besu-to-besu/lib/chains.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/besu-to-besu/lib/chains.sh b/examples/besu-to-besu/lib/chains.sh index 020050368..ae69573fd 100644 --- a/examples/besu-to-besu/lib/chains.sh +++ b/examples/besu-to-besu/lib/chains.sh @@ -205,7 +205,7 @@ _init_chain() { # besu.toml and el-genesis.json hold no secrets and are set explicitly so a # restrictive host umask cannot render them unreadable to the container. chmod 755 "$out_dir" - chmod 600 "$out_dir/key" + chmod 644 "$out_dir/key" chmod 644 "$out_dir/besu.toml" "$out_dir/el-genesis.json" if command -v jq >/dev/null 2>&1; then From 484bca3f06a9a284a75e8899d46fde29dc08b791 Mon Sep 17 00:00:00 2001 From: Gjermund Garaba Date: Tue, 11 Aug 2026 09:58:52 +0200 Subject: [PATCH 06/11] ci: add CodeQL scanning (#1338) Signed-off-by: Diana Borbe --- .github/workflows/codeql.yml | 55 ++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..484eb76e5 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,55 @@ +# SPDX-License-Identifier: Apache-2.0 + +name: CodeQL + +on: + push: + branches: [main, development] + pull_request: + branches: [main, development] + schedule: + - cron: "17 3 * * 1" + +permissions: read-all + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + packages: read + security-events: write + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: go + build-mode: manual + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 + if: matrix.language == 'go' + with: + go-version-file: link/go.mod + cache-dependency-path: | + link/go.sum + e2e/go.sum + - uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + - name: Build Link + if: matrix.language == 'go' + working-directory: link + run: go build ./... + - name: Build E2E + if: matrix.language == 'go' + working-directory: e2e + run: go build ./... + - uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 + with: + category: /language:${{ matrix.language }} From 13eeca0592bbc9bd6cda853d9ffd88ca9e9b24ef Mon Sep 17 00:00:00 2001 From: Gjermund Garaba Date: Tue, 11 Aug 2026 10:10:28 +0200 Subject: [PATCH 07/11] fix(e2e): bound setup transaction mining waits (#1314) A dummy-client deploy in TestStartRealizesAppStackAndDummyClients hung in bind.WaitMined for 9+ minutes on CI until the go test 10m alarm killed the whole package: nothing bounded the wait, and WaitMined polls forever while swallowing every receipt error. Bound each setup transaction wait with the chain's CompletionBudget, so a lost transaction fails its own test in bounded time with a stage-named error instead of panicking the test binary. Note for future work: evm.EVMClient.waitForReceipt fails on the first transport error; geth-style nodes emit a transient, string-only "transaction indexing is in progress" receipt error right after mining, so reusing that helper here would need its error handling revisited. Signed-off-by: Diana Borbe --- .../harness/environment/realize_protocol.go | 2 +- .../harness/environment/solidityibc/setup.go | 24 ++++++++++++----- .../environment/solidityibc/setup_test.go | 27 ++++++++++++++++--- 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/e2e/internal/harness/environment/realize_protocol.go b/e2e/internal/harness/environment/realize_protocol.go index 0d82662f2..1dbeee615 100644 --- a/e2e/internal/harness/environment/realize_protocol.go +++ b/e2e/internal/harness/environment/realize_protocol.go @@ -543,7 +543,7 @@ func solidityIBCSetup(ctx context.Context, chain *Chain) (*solidityibc.Setup, er var setup *solidityibc.Setup ok, err := evm.WithChainClient(chain.impl, func(client *evm.EVMClient) error { var setupErr error - setup, setupErr = solidityibc.NewSetup(ctx, client.Client()) + setup, setupErr = solidityibc.NewSetup(ctx, client.Client(), chain.timing.CompletionBudget) return setupErr }) if !ok { diff --git a/e2e/internal/harness/environment/solidityibc/setup.go b/e2e/internal/harness/environment/solidityibc/setup.go index 2d45cb1cb..1057de0d3 100644 --- a/e2e/internal/harness/environment/solidityibc/setup.go +++ b/e2e/internal/harness/environment/solidityibc/setup.go @@ -8,6 +8,7 @@ import ( "math" "math/big" "slices" + "time" "github.com/cosmos/solidity-ibc-eureka/packages/go-abigen/attestation" "github.com/cosmos/solidity-ibc-eureka/packages/go-abigen/erc1967proxy" @@ -35,12 +36,14 @@ type contractBackend interface { // Setup performs Solidity IBC setup transactions on one EVM Chain. It borrows the // client and never closes it. type Setup struct { - backend contractBackend - chainID *big.Int + backend contractBackend + chainID *big.Int + miningTimeout time.Duration } // NewSetup binds Solidity IBC realization to an already-connected EVM client. -func NewSetup(ctx context.Context, client *ethclient.Client) (*Setup, error) { +// The mining timeout bounds every setup transaction's mining observation. +func NewSetup(ctx context.Context, client *ethclient.Client, miningTimeout time.Duration) (*Setup, error) { if client == nil { return nil, fmt.Errorf("solidity IBC setup: nil EVM client") } @@ -48,17 +51,20 @@ func NewSetup(ctx context.Context, client *ethclient.Client) (*Setup, error) { if err != nil { return nil, fmt.Errorf("solidity IBC setup: query chain id: %w", err) } - return newSetup(client, chainID) + return newSetup(client, chainID, miningTimeout) } -func newSetup(backend contractBackend, chainID *big.Int) (*Setup, error) { +func newSetup(backend contractBackend, chainID *big.Int, miningTimeout time.Duration) (*Setup, error) { if backend == nil { return nil, fmt.Errorf("solidity IBC setup: nil contract backend") } if chainID == nil || chainID.Sign() <= 0 { return nil, fmt.Errorf("solidity IBC setup: invalid EVM chain id") } - return &Setup{backend: backend, chainID: new(big.Int).Set(chainID)}, nil + if miningTimeout <= 0 { + return nil, fmt.Errorf("solidity IBC setup: mining timeout must be positive") + } + return &Setup{backend: backend, chainID: new(big.Int).Set(chainID), miningTimeout: miningTimeout}, nil } // DeployInstance deploys AccessManager, ICS26Router implementation, and an @@ -648,6 +654,8 @@ func (s *Setup) send( return address, tx, nil } +// awaitMined waits for the transaction receipt, bounded by the Setup's mining +// timeout so a lost transaction fails this setup instead of hanging the caller. func (s *Setup) awaitMined( ctx context.Context, stage string, @@ -656,7 +664,9 @@ func (s *Setup) awaitMined( if tx == nil { return nil, fmt.Errorf("solidity IBC %s: transaction was not returned", stage) } - receipt, err := bind.WaitMined(ctx, s.backend, tx) + waitCtx, cancel := context.WithTimeout(ctx, s.miningTimeout) + defer cancel() + receipt, err := bind.WaitMined(waitCtx, s.backend, tx) if err != nil { return nil, fmt.Errorf( "solidity IBC %s: wait for transaction %s: %w", diff --git a/e2e/internal/harness/environment/solidityibc/setup_test.go b/e2e/internal/harness/environment/solidityibc/setup_test.go index eee964510..e7703152a 100644 --- a/e2e/internal/harness/environment/solidityibc/setup_test.go +++ b/e2e/internal/harness/environment/solidityibc/setup_test.go @@ -21,6 +21,8 @@ import ( "github.com/cosmos/ibc/e2e/internal/harness/environment/solidityibc/accessmanager" ) +const testMiningTimeout = 30 * time.Second + type failSendBackend struct { contractBackend failAt int @@ -52,7 +54,7 @@ func TestSetupDeploysAndAttachesSolidityIBCInstanceAndClient(t *testing.T) { clientAuthority.Address(): {Balance: ether(100)}, }) startMining(t, backend) - setup, err := newSetup(backend.Client(), big.NewInt(1337)) + setup, err := newSetup(backend.Client(), big.NewInt(1337), testMiningTimeout) require.NoError(t, err) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -108,7 +110,7 @@ func TestPrepareClientRejectsInvalidConfigurationBeforeSideEffects(t *testing.T) authority.Address(): {Balance: ether(1)}, }) t.Cleanup(func() { require.NoError(t, backend.Close()) }) - setup, err := newSetup(backend.Client(), big.NewInt(1337)) + setup, err := newSetup(backend.Client(), big.NewInt(1337), testMiningTimeout) require.NoError(t, err) prepared, err := setup.PrepareClient(context.Background(), authority, common.Address{}, AttestationClientConfig{ @@ -131,7 +133,7 @@ func TestDeployInstanceReportsBroadcastFailure(t *testing.T) { }) startMining(t, backend) failing := &failSendBackend{contractBackend: backend.Client(), failAt: 2} - setup, err := newSetup(failing, big.NewInt(1337)) + setup, err := newSetup(failing, big.NewInt(1337), testMiningTimeout) require.NoError(t, err) ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() @@ -143,6 +145,23 @@ func TestDeployInstanceReportsBroadcastFailure(t *testing.T) { require.ErrorContains(t, err, failing.hash.Hex()) } +func TestAwaitMinedTimesOutWhenNeverMined(t *testing.T) { + authority, err := evm.NewAccount() + require.NoError(t, err) + backend := simulated.NewBackend(gethtypes.GenesisAlloc{ + authority.Address(): {Balance: ether(1)}, + }) + t.Cleanup(func() { require.NoError(t, backend.Close()) }) + setup, err := newSetup(backend.Client(), big.NewInt(1337), 200*time.Millisecond) + require.NoError(t, err) + tx := gethtypes.NewTx(&gethtypes.LegacyTx{Nonce: 77, Gas: 21_000}) + + receipt, err := setup.awaitMined(context.Background(), "unmined confirmation", tx) + require.ErrorIs(t, err, context.DeadlineExceeded) + require.ErrorContains(t, err, "unmined confirmation") + require.Nil(t, receipt) +} + func TestAwaitMinedReturnsCancellation(t *testing.T) { authority, err := evm.NewAccount() require.NoError(t, err) @@ -150,7 +169,7 @@ func TestAwaitMinedReturnsCancellation(t *testing.T) { authority.Address(): {Balance: ether(1)}, }) t.Cleanup(func() { require.NoError(t, backend.Close()) }) - setup, err := newSetup(backend.Client(), big.NewInt(1337)) + setup, err := newSetup(backend.Client(), big.NewInt(1337), testMiningTimeout) require.NoError(t, err) tx := gethtypes.NewTx(&gethtypes.LegacyTx{Nonce: 77, Gas: 21_000}) ctx, cancel := context.WithCancel(context.Background()) From d27cc60b764d9b804e87215b0830cbd6be2db6ad Mon Sep 17 00:00:00 2001 From: srdtrk <59252793+srdtrk@users.noreply.github.com> Date: Tue, 11 Aug 2026 16:33:06 +0800 Subject: [PATCH 08/11] feat: intial attempt (#1336) Signed-off-by: Diana Borbe --- e2e/README.md | 2 +- e2e/attached_chain_test.go | 2 +- e2e/cross_route_test.go | 4 +- e2e/gmp_test.go | 6 +- e2e/ift_test.go | 116 ++++++++++----------- e2e/internal/e2etest/traffic.go | 53 +++++++++- e2e/internal/e2etest/traffic_evm.go | 10 +- e2e/internal/e2etest/traffic_gmp.go | 24 ++--- e2e/internal/e2etest/traffic_ift.go | 124 ++++++++++++----------- e2e/internal/e2etest/traffic_relayer.go | 61 +++++------ e2e/internal/e2etest/traffic_test.go | 14 +-- e2e/internal/e2etest/traffic_transfer.go | 56 +++++----- e2e/node_recovery_test.go | 2 +- e2e/pending_packet_test.go | 6 +- e2e/relayer_lifecycle_test.go | 8 +- e2e/transfer_test.go | 10 +- 16 files changed, 271 insertions(+), 227 deletions(-) diff --git a/e2e/README.md b/e2e/README.md index ec25dca1d..17b184e31 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -69,7 +69,7 @@ func TestTransfer_AutoRelay(t *testing.T) { require.NoError(t, err) require.NoError(t, transfer.VerifyEscrowed(ctx)) - _, err = e2etest.AwaitState(ctx, relayer, transfer.Packet(), + _, err = e2etest.AwaitState(ctx, relayer, transfer.PacketTx(), relayerv2.PacketState_PACKET_STATE_SUCCEEDED) require.NoError(t, err) require.NoError(t, transfer.VerifyDelivered(ctx)) diff --git a/e2e/attached_chain_test.go b/e2e/attached_chain_test.go index 548a8f87e..da1cdb61d 100644 --- a/e2e/attached_chain_test.go +++ b/e2e/attached_chain_test.go @@ -74,7 +74,7 @@ func TestAttachedChainRemainsCallerOwned(t *testing.T) { attached, chainErr := env.Chain(e2etest.ChainB) require.NoError(t, chainErr) - _, awaitErr := e2etest.AwaitState(rctx, relayer, transfer.Packet(), + _, awaitErr := e2etest.AwaitState(rctx, relayer, transfer.PacketTx(), relayerv2.PacketState_PACKET_STATE_SUCCEEDED) require.NoError(t, awaitErr) require.NoError(t, transfer.VerifyDelivered(rctx)) diff --git a/e2e/cross_route_test.go b/e2e/cross_route_test.go index 04a433115..5a173b359 100644 --- a/e2e/cross_route_test.go +++ b/e2e/cross_route_test.go @@ -43,10 +43,10 @@ func TestCrossRoutePacketsDoNotCollideBySequence(t *testing.T) { require.NoError(t, err) require.NoError(t, cToA.VerifyEscrowed(ctx)) - _, err = e2etest.AwaitState(ctx, relayer, bToA.Packet(), + _, err = e2etest.AwaitState(ctx, relayer, bToA.PacketTx(), relayerv2.PacketState_PACKET_STATE_SUCCEEDED) require.NoError(t, err) - _, err = e2etest.AwaitState(ctx, relayer, cToA.Packet(), + _, err = e2etest.AwaitState(ctx, relayer, cToA.PacketTx(), relayerv2.PacketState_PACKET_STATE_SUCCEEDED) require.NoError(t, err) require.NoError(t, bToA.VerifyDelivered(ctx)) diff --git a/e2e/gmp_test.go b/e2e/gmp_test.go index fb9aa0b4b..ae02d7c3d 100644 --- a/e2e/gmp_test.go +++ b/e2e/gmp_test.go @@ -26,7 +26,7 @@ func TestGMPCall_AutoRelay(t *testing.T) { call, err := gmp.Call(ctx, e2etest.GMPRequest{}) require.NoError(t, err) - _, err = e2etest.AwaitState(ctx, relayer, call.Packet(), + _, err = e2etest.AwaitState(ctx, relayer, call.PacketTx(), relayerv2.PacketState_PACKET_STATE_SUCCEEDED) require.NoError(t, err) require.NoError(t, call.VerifyCounterExecuted(ctx)) @@ -63,7 +63,7 @@ func TestGMPCall_ICS27AccountTransfer(t *testing.T) { call, err := gmp.Call(ctx, e2etest.GMPRequest{Receiver: token.Hex(), Salt: salt, Payload: payload}) require.NoError(t, err) - _, err = e2etest.AwaitState(ctx, relayer, call.Packet(), + _, err = e2etest.AwaitState(ctx, relayer, call.PacketTx(), relayerv2.PacketState_PACKET_STATE_SUCCEEDED) require.NoError(t, err) @@ -92,7 +92,7 @@ func TestGMPCall_ErrorAcknowledgement(t *testing.T) { call, err := gmp.Call(ctx, e2etest.GMPRequest{Payload: invalidGMPPayload}) require.NoError(t, err) - _, err = e2etest.AwaitState(ctx, relayer, call.Packet(), + _, err = e2etest.AwaitState(ctx, relayer, call.PacketTx(), relayerv2.PacketState_PACKET_STATE_REJECTED) require.NoError(t, err) require.NoError(t, call.VerifyCounterRejected(ctx)) diff --git a/e2e/ift_test.go b/e2e/ift_test.go index da55e6a36..bed0f987a 100644 --- a/e2e/ift_test.go +++ b/e2e/ift_test.go @@ -113,7 +113,7 @@ func TestIFTTransfer_MultiAttestorQuorum(t *testing.T) { transfer, err := iftApp.Send(ctx, e2etest.IFTRequest{Amount: big.NewInt(1_234_000)}) require.NoError(t, err) require.NoError(t, transfer.VerifyBurned(ctx)) - _, err = e2etest.AwaitState(ctx, relayer, transfer.Packet(), + _, err = e2etest.AwaitState(ctx, relayer, transfer.PacketTx(), relayerv2.PacketState_PACKET_STATE_SUCCEEDED) require.NoError(t, err) require.NoError(t, transfer.VerifyDelivered(ctx)) @@ -123,14 +123,14 @@ func TestIFTTransfer_MultiAttestorQuorum(t *testing.T) { pending, err := iftApp.Send(ctx, e2etest.IFTRequest{Amount: big.NewInt(2_345_000)}) require.NoError(t, err) require.NoError(t, pending.VerifyBurned(ctx)) - require.NoError(t, e2etest.Relay(ctx, relayer, pending.Packet())) - require.NoError(t, e2etest.AwaitStable(ctx, relayer, pending.Packet(), + require.NoError(t, e2etest.Relay(ctx, relayer, pending.PacketTx())) + require.NoError(t, e2etest.AwaitStable(ctx, relayer, pending.PacketTx(), relayerv2.PacketState_PACKET_STATE_PENDING)) require.NoError(t, pending.VerifyNotMinted(ctx)) // Restoring a second attestor lets the pending transfer complete. require.NoError(t, destinationAttestorC.Restart(ctx)) - _, err = e2etest.AwaitState(ctx, relayer, pending.Packet(), + _, err = e2etest.AwaitState(ctx, relayer, pending.PacketTx(), relayerv2.PacketState_PACKET_STATE_SUCCEEDED) require.NoError(t, err) require.NoError(t, pending.VerifyDelivered(ctx)) @@ -195,7 +195,7 @@ func TestIFTTimeout_RefundUsesFinalizedDestinationAnchor(t *testing.T) { require.GreaterOrEqual(t, sourceHeight, pausedDestinationHeight+40) relayer = e2etest.StartRelayer(t, driver, env) - _, awaitErr := e2etest.AwaitState(ctx, relayer, asymmetricTransfer.Packet(), + _, awaitErr := e2etest.AwaitState(ctx, relayer, asymmetricTransfer.PacketTx(), relayerv2.PacketState_PACKET_STATE_TIMED_OUT) require.NoError(t, awaitErr) require.NoError(t, asymmetricTransfer.VerifyRefunded(ctx)) @@ -224,7 +224,7 @@ func TestIFTTransfer_AutoRelay(t *testing.T) { require.NoError(t, transfer.VerifyBurned(ctx)) require.NoError(t, transfer.VerifyPending(ctx)) - _, err = e2etest.AwaitState(ctx, relayer, transfer.Packet(), + _, err = e2etest.AwaitState(ctx, relayer, transfer.PacketTx(), relayerv2.PacketState_PACKET_STATE_SUCCEEDED) require.NoError(t, err) require.NoError(t, transfer.VerifyDelivered(ctx)) @@ -256,24 +256,24 @@ func TestIFTTransfer_TwoTokensSameClientPair(t *testing.T) { Receiver: receiver, }) require.NoError(t, err) - require.NotEqual(t, transferA.Packet().Sequence, transferB.Packet().Sequence) + require.NotEqual(t, transferA.PacketTx().Sequence, transferB.PacketTx().Sequence) - transfers := []*e2etest.IFTPacket{transferA, transferB} + transfers := []*e2etest.IFTSend{transferA, transferB} for _, transfer := range transfers { require.NoError(t, transfer.VerifyBurned(ctx)) require.NoError(t, transfer.VerifyPending(ctx)) } relayer := e2etest.StartRelayer(t, driver, env) - require.NoError(t, e2etest.Relay(ctx, relayer, transferA.Packet())) - _, err = e2etest.AwaitState(ctx, relayer, transferA.Packet(), + require.NoError(t, e2etest.Relay(ctx, relayer, transferA.PacketTx())) + _, err = e2etest.AwaitState(ctx, relayer, transferA.PacketTx(), relayerv2.PacketState_PACKET_STATE_SUCCEEDED) require.NoError(t, err) require.NoError(t, transferA.VerifyPendingCleared(ctx)) require.NoError(t, transferB.VerifyPending(ctx), "token B must remain pending after token A is acknowledged") - require.NoError(t, e2etest.Relay(ctx, relayer, transferB.Packet())) - _, err = e2etest.AwaitState(ctx, relayer, transferB.Packet(), + require.NoError(t, e2etest.Relay(ctx, relayer, transferB.PacketTx())) + _, err = e2etest.AwaitState(ctx, relayer, transferB.PacketTx(), relayerv2.PacketState_PACKET_STATE_SUCCEEDED) require.NoError(t, err) for _, transfer := range transfers { @@ -328,8 +328,8 @@ func TestIFTTimeout_Refund(t *testing.T) { "finalized destination header must naturally pass the packet timeout") }, destination.Timing().CompletionBudget, destination.Timing().PollInterval) - require.NoError(t, e2etest.Relay(ctx, relayer, transfer.Packet())) - _, err = e2etest.AwaitState(ctx, relayer, transfer.Packet(), + require.NoError(t, e2etest.Relay(ctx, relayer, transfer.PacketTx())) + _, err = e2etest.AwaitState(ctx, relayer, transfer.PacketTx(), relayerv2.PacketState_PACKET_STATE_TIMED_OUT) require.NoError(t, err) require.NoError(t, transfer.VerifyRefunded(ctx)) @@ -357,7 +357,7 @@ func TestIFTTimeout_WaitsForFinality(t *testing.T) { require.NoError(t, err) finalityOffset := uint64(ibclink.HarnessFinalityOffset) - var transfer *e2etest.IFTPacket + var transfer *e2etest.IFTSend require.NoError(t, mining.WithPaused(ctx, func() error { transfer, err = iftApp.Send(ctx, e2etest.IFTRequest{ Amount: big.NewInt(3_000_000), @@ -383,8 +383,8 @@ func TestIFTTimeout_WaitsForFinality(t *testing.T) { require.Less(t, finalizedHeader.Time, transfer.TimeoutTimestamp(), "finalized destination header must remain before the packet timeout") - require.NoError(t, e2etest.Relay(ctx, relayer, transfer.Packet())) - require.NoError(t, e2etest.AwaitStable(ctx, relayer, transfer.Packet(), + require.NoError(t, e2etest.Relay(ctx, relayer, transfer.PacketTx())) + require.NoError(t, e2etest.AwaitStable(ctx, relayer, transfer.PacketTx(), relayerv2.PacketState_PACKET_STATE_PENDING)) require.NoError(t, transfer.VerifyPending(ctx)) require.NoError(t, transfer.VerifyNotMinted(ctx)) @@ -408,7 +408,7 @@ func TestIFTTimeout_WaitsForFinality(t *testing.T) { "finalized destination header must pass the packet timeout") }, destination.Timing().CompletionBudget, destination.Timing().PollInterval) - _, err = e2etest.AwaitState(ctx, relayer, transfer.Packet(), + _, err = e2etest.AwaitState(ctx, relayer, transfer.PacketTx(), relayerv2.PacketState_PACKET_STATE_TIMED_OUT) require.NoError(t, err) require.NoError(t, transfer.VerifyRefunded(ctx)) @@ -437,7 +437,7 @@ func TestIFTTransfer_ErrorAck_Refund(t *testing.T) { require.NoError(t, transfer.VerifyBurned(ctx)) require.NoError(t, transfer.VerifyPending(ctx)) - _, err = e2etest.AwaitState(ctx, relayer, transfer.Packet(), + _, err = e2etest.AwaitState(ctx, relayer, transfer.PacketTx(), relayerv2.PacketState_PACKET_STATE_REJECTED) require.NoError(t, err) require.NoError(t, transfer.VerifyNotMinted(ctx)) @@ -465,7 +465,7 @@ func TestIFTTransfer_ErrorAck_UnregisteredBridge(t *testing.T) { require.NoError(t, transfer.VerifyBurned(ctx)) require.NoError(t, transfer.VerifyPending(ctx)) - _, err = e2etest.AwaitState(ctx, relayer, transfer.Packet(), + _, err = e2etest.AwaitState(ctx, relayer, transfer.PacketTx(), relayerv2.PacketState_PACKET_STATE_REJECTED) require.NoError(t, err) require.NoError(t, transfer.VerifyNotMinted(ctx)) @@ -490,7 +490,7 @@ func TestIFTTransfer_MultiPacketPending(t *testing.T) { require.NoError(t, relayer.Stop(ctx)) amounts := []*big.Int{big.NewInt(1_000_000), big.NewInt(2_000_000), big.NewInt(3_000_000)} - transfers := make([]*e2etest.IFTPacket, len(amounts)) + transfers := make([]*e2etest.IFTSend, len(amounts)) for i, amount := range amounts { transfer, err := iftApp.Send(ctx, e2etest.IFTRequest{Amount: amount}) require.NoError(t, err) @@ -501,7 +501,7 @@ func TestIFTTransfer_MultiPacketPending(t *testing.T) { relayer = e2etest.StartRelayer(t, driver, env) for _, transfer := range transfers { - _, err := e2etest.AwaitState(ctx, relayer, transfer.Packet(), + _, err := e2etest.AwaitState(ctx, relayer, transfer.PacketTx(), relayerv2.PacketState_PACKET_STATE_SUCCEEDED) require.NoError(t, err) require.NoError(t, transfer.VerifyDelivered(ctx)) @@ -533,26 +533,26 @@ func TestIFTTransfer_MultiPacketSingleTx(t *testing.T) { } batch, err := iftApp.SendBatch(ctx, requests) require.NoError(t, err) - packets := batch.Packets() - require.Len(t, packets, packetCount) + packetTxs := batch.PacketTxs() + require.Len(t, packetTxs, packetCount) wantSequences := make(map[uint64]struct{}, packetCount) - for _, packet := range packets { - wantSequences[packet.Sequence] = struct{}{} + for _, packetTx := range packetTxs { + wantSequences[packetTx.Sequence] = struct{}{} } require.Len(t, wantSequences, packetCount, "packets must have distinct sequences") - require.NoError(t, e2etest.Relay(ctx, relayer, packets[0])) + require.NoError(t, e2etest.Relay(ctx, relayer, packetTxs[0])) - for _, packet := range packets { - _, err = e2etest.AwaitState(ctx, relayer, packet, + for _, packetTx := range packetTxs { + _, err = e2etest.AwaitState(ctx, relayer, packetTx, relayerv2.PacketState_PACKET_STATE_SUCCEEDED) require.NoError(t, err) } require.NoError(t, batch.VerifyDelivered(ctx)) require.NoError(t, batch.VerifyBurned(ctx)) - statuses, err := relayer.PacketStatuses(ctx, string(route.Source), packets[0].SourceTxHash) + statuses, err := relayer.PacketStatuses(ctx, string(route.Source), batch.TxHash()) require.NoError(t, err) require.Len(t, statuses, packetCount) gotSequences := make(map[uint64]struct{}, packetCount) @@ -595,50 +595,50 @@ func TestIFTTransfer_BatchedRecvAck(t *testing.T) { ctx := t.Context() const packetCount = 10 - packets := make([]*e2etest.IFTPacket, packetCount) - for i := range packets { + sends := make([]*e2etest.IFTSend, packetCount) + for i := range sends { transfer, err := iftApp.Send(ctx, e2etest.IFTRequest{Amount: big.NewInt(int64(1_000_000 + i))}) require.NoError(t, err) require.NoError(t, transfer.VerifyBurned(ctx)) - packets[i] = transfer + sends[i] = transfer } relayErrs := make([]error, packetCount) var wg sync.WaitGroup - for i, packet := range packets { + for i, send := range sends { wg.Add(1) - go func(i int, packet *e2etest.IFTPacket) { + go func(i int, send *e2etest.IFTSend) { defer wg.Done() - relayErrs[i] = e2etest.Relay(ctx, relayer, packet.Packet()) - }(i, packet) + relayErrs[i] = e2etest.Relay(ctx, relayer, send.PacketTx()) + }(i, send) } wg.Wait() for i, err := range relayErrs { - require.NoErrorf(t, err, "relay call for packet %d (seq %d) failed", i, packets[i].Packet().Sequence) + require.NoErrorf(t, err, "relay call for packet %d (seq %d) failed", i, sends[i].PacketTx().Sequence) } - for i, packet := range packets { - statuses, err := relayer.PacketStatuses(ctx, string(route.Source), packet.Packet().SourceTxHash) + for i, send := range sends { + statuses, err := relayer.PacketStatuses(ctx, string(route.Source), send.TxHash()) require.NoError(t, err) require.Lenf(t, statuses, 1, "packet %d status should only include its own transaction", i) - require.Equal(t, packet.Packet().Sequence, statuses[0].SequenceNumber) + require.Equal(t, send.PacketTx().Sequence, statuses[0].SequenceNumber) } recvTxCounts := make(map[string]int, packetCount) ackTxCounts := make(map[string]int, packetCount) - for _, packet := range packets { - _, err := e2etest.AwaitState(ctx, relayer, packet.Packet(), + for _, send := range sends { + _, err := e2etest.AwaitState(ctx, relayer, send.PacketTx(), relayerv2.PacketState_PACKET_STATE_SUCCEEDED) require.NoError(t, err) - require.NoError(t, packet.VerifyDelivered(ctx)) + require.NoError(t, send.VerifyDelivered(ctx)) - statuses, err := relayer.PacketStatuses(ctx, string(route.Source), packet.Packet().SourceTxHash) + statuses, err := relayer.PacketStatuses(ctx, string(route.Source), send.TxHash()) require.NoError(t, err) require.Len(t, statuses, 1) recvTxCounts[statuses[0].GetRecvTx().GetTxHash()]++ ackTxCounts[statuses[0].GetAckTx().GetTxHash()]++ } - require.NoError(t, packets[packetCount-1].VerifyBurned(ctx), "successful acks must not refund") + require.NoError(t, sends[packetCount-1].VerifyBurned(ctx), "successful acks must not refund") require.Lessf(t, len(recvTxCounts), packetCount, "expected batched recv, got one recv tx per packet: %v", recvTxCounts) @@ -676,15 +676,15 @@ func TestIFTTransfer_BatchedTimeout(t *testing.T) { ctx := t.Context() const packetCount = 10 - packets := make([]*e2etest.IFTPacket, packetCount) - for i := range packets { + sends := make([]*e2etest.IFTSend, packetCount) + for i := range sends { transfer, err := iftApp.Send(ctx, e2etest.IFTRequest{ Amount: big.NewInt(int64(1_000_000 + i)), Timeout: transferTimeout, }) require.NoError(t, err) require.NoError(t, transfer.VerifyBurned(ctx)) - packets[i] = transfer + sends[i] = transfer } destination, err := env.Chain(route.Destination) @@ -695,26 +695,26 @@ func TestIFTTransfer_BatchedTimeout(t *testing.T) { relayErrs := make([]error, packetCount) var wg sync.WaitGroup - for i, packet := range packets { + for i, send := range sends { wg.Add(1) - go func(i int, packet *e2etest.IFTPacket) { + go func(i int, send *e2etest.IFTSend) { defer wg.Done() - relayErrs[i] = e2etest.Relay(ctx, relayer, packet.Packet()) - }(i, packet) + relayErrs[i] = e2etest.Relay(ctx, relayer, send.PacketTx()) + }(i, send) } wg.Wait() for i, err := range relayErrs { - require.NoErrorf(t, err, "relay call for packet %d (seq %d) failed", i, packets[i].Packet().Sequence) + require.NoErrorf(t, err, "relay call for packet %d (seq %d) failed", i, sends[i].PacketTx().Sequence) } timeoutTxCounts := make(map[string]int, packetCount) - for _, packet := range packets { - _, err = e2etest.AwaitState(ctx, relayer, packet.Packet(), + for _, send := range sends { + _, err = e2etest.AwaitState(ctx, relayer, send.PacketTx(), relayerv2.PacketState_PACKET_STATE_TIMED_OUT) require.NoError(t, err) - require.NoError(t, packet.VerifyNotMinted(ctx)) + require.NoError(t, send.VerifyNotMinted(ctx)) - statuses, err := relayer.PacketStatuses(ctx, string(route.Source), packet.Packet().SourceTxHash) + statuses, err := relayer.PacketStatuses(ctx, string(route.Source), send.TxHash()) require.NoError(t, err) require.Len(t, statuses, 1) timeoutTxCounts[statuses[0].GetTimeoutTx().GetTxHash()]++ diff --git a/e2e/internal/e2etest/traffic.go b/e2e/internal/e2etest/traffic.go index 67bafad63..e3f873ae7 100644 --- a/e2e/internal/e2etest/traffic.go +++ b/e2e/internal/e2etest/traffic.go @@ -6,13 +6,17 @@ import ( "errors" "fmt" + "github.com/ethereum/go-ethereum/core/types" + "github.com/cosmos/ibc/e2e/internal/harness/environment" ) type RouteID string -// Packet identifies the protocol packet originated by a test application. -type Packet struct { +// PacketTx locates one protocol packet originated by a test application: the +// route it travels, the source transaction that emitted it, and its sequence +// within that transaction's client. It carries no packet wire data. +type PacketTx struct { RouteID RouteID Source environment.ChainID SourceClient string @@ -20,10 +24,53 @@ type Packet struct { Sequence uint64 } -func (p Packet) reference() string { +// String renders the route-scoped label used in relayer status messages. +func (p PacketTx) String() string { + return fmt.Sprintf("%s-%d", p.RouteID, p.Sequence) +} + +func (p PacketTx) reference() string { return fmt.Sprintf("route %q sequence %d", p.RouteID, p.Sequence) } +// sendResult is what every application send produces: the locator of the packet +// it emitted and the source transaction receipt that emitted it. The +// application send types embed it so they all expose the same handles. +type sendResult struct { + packetTx PacketTx + receipt *types.Receipt +} + +// newSendResult pairs a source transaction receipt with one packet it emitted. +func newSendResult( + routeID RouteID, + source endpoint, + sourceClient string, + receipt *types.Receipt, + sequence uint64, +) sendResult { + return sendResult{ + packetTx: PacketTx{ + RouteID: routeID, + Source: source.chain.ID(), + SourceClient: sourceClient, + SourceTxHash: receipt.TxHash.Hex(), + Sequence: sequence, + }, + receipt: receipt, + } +} + +// PacketTx locates the packet this send emitted. +func (s sendResult) PacketTx() PacketTx { return s.packetTx } + +// TxHash is the hex hash of the source transaction that emitted the packet. +func (s sendResult) TxHash() string { return s.packetTx.SourceTxHash } + +// Receipt is the source transaction receipt, for assertions the typed helpers +// do not cover. +func (s sendResult) Receipt() *types.Receipt { return s.receipt } + type endpoint struct { chain *environment.Chain evm *environment.EVM diff --git a/e2e/internal/e2etest/traffic_evm.go b/e2e/internal/e2etest/traffic_evm.go index 694fda23f..d82921dea 100644 --- a/e2e/internal/e2etest/traffic_evm.go +++ b/e2e/internal/e2etest/traffic_evm.go @@ -98,22 +98,22 @@ func send( to common.Address, data []byte, sequenceFromReceipt func(*types.Receipt) (uint64, bool, error), -) (string, uint64, error) { +) (*types.Receipt, uint64, error) { receipt, err := client.BroadcastTx(ctx, sender, &to, data, nil) if err != nil { - return "", 0, fmt.Errorf("e2etest: broadcast transaction: %w", err) + return nil, 0, fmt.Errorf("e2etest: broadcast transaction: %w", err) } sequence, found, err := sequenceFromReceipt(receipt) if err != nil { - return "", 0, err + return nil, 0, err } if !found { - return "", 0, fmt.Errorf( + return nil, 0, fmt.Errorf( "e2etest: source transaction %s emitted no application send event", receipt.TxHash.Hex(), ) } - return receipt.TxHash.Hex(), sequence, nil + return receipt, sequence, nil } type eventSource struct { diff --git a/e2e/internal/e2etest/traffic_gmp.go b/e2e/internal/e2etest/traffic_gmp.go index fe0fdbc06..cedea2784 100644 --- a/e2e/internal/e2etest/traffic_gmp.go +++ b/e2e/internal/e2etest/traffic_gmp.go @@ -46,9 +46,11 @@ type GMP struct { // Token returns the destination-chain TestERC20 bound to this GMP. func (g *GMP) Token() common.Address { return g.destToken } +// GMPCall is the result of a GMP Call: the packet it emitted plus the Counter +// baseline its Verify methods compare against. type GMPCall struct { + sendResult app *GMP - packet Packet before *big.Int } @@ -82,7 +84,7 @@ func (g *GMP) Call(ctx context.Context, request GMPRequest) (*GMPCall, error) { if err != nil { return nil, fmt.Errorf("e2etest: pack GMP sendCall: %w", err) } - txHash, sequence, err := send( + receipt, sequence, err := send( ctx, g.source.evm, g.sender, @@ -94,20 +96,12 @@ func (g *GMP) Call(ctx context.Context, request GMPRequest) (*GMPCall, error) { return nil, fmt.Errorf("e2etest: send GMP on route %q: %w", g.routeID, err) } return &GMPCall{ - app: g, - packet: Packet{ - RouteID: g.routeID, - Source: g.source.chain.ID(), - SourceClient: g.sourceClient, - SourceTxHash: txHash, - Sequence: sequence, - }, - before: before, + sendResult: newSendResult(g.routeID, g.source, g.sourceClient, receipt, sequence), + app: g, + before: before, }, nil } -func (c *GMPCall) Packet() Packet { return c.packet } - // VerifyCounterExecuted waits for the destination Counter to change exactly // once. Only meaningful when the call's receiver was the bound Counter. func (c *GMPCall) VerifyCounterExecuted(ctx context.Context) error { @@ -115,7 +109,7 @@ func (c *GMPCall) VerifyCounterExecuted(ctx context.Context) error { return awaitBalance( ctx, c.app.destination.chain, - fmt.Sprintf("GMP packet %s Counter execution", c.packet.reference()), + fmt.Sprintf("GMP packet %s Counter execution", c.packetTx.reference()), c.app.count, want, ) @@ -250,7 +244,7 @@ func (c *GMPCall) verifyCount(ctx context.Context, want *big.Int, state string) if got.Cmp(want) != 0 { return fmt.Errorf( "e2etest: GMP packet %s target %s %s count: got %s, want %s", - c.packet.reference(), + c.packetTx.reference(), c.app.counter.Hex(), state, got, diff --git a/e2e/internal/e2etest/traffic_ift.go b/e2e/internal/e2etest/traffic_ift.go index 651b19418..e52491943 100644 --- a/e2e/internal/e2etest/traffic_ift.go +++ b/e2e/internal/e2etest/traffic_ift.go @@ -11,6 +11,7 @@ import ( "github.com/cosmos/solidity-ibc-eureka/packages/go-abigen/ift" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" "github.com/cosmos/ibc/e2e/internal/harness/chain/evm" @@ -46,9 +47,11 @@ type IFT struct { batcher common.Address } -type IFTPacket struct { +// IFTSend is the result of an IFT Send: the packet it emitted plus the balance +// baselines its Verify methods compare against. +type IFTSend struct { + sendResult app *IFT - packet Packet receiver common.Address amount *big.Int sourceBefore *big.Int @@ -57,7 +60,7 @@ type IFTPacket struct { timeoutTimestamp uint64 } -func (i *IFT) Send(ctx context.Context, request IFTRequest) (*IFTPacket, error) { +func (i *IFT) Send(ctx context.Context, request IFTRequest) (*IFTSend, error) { amount, err := validAmount(request.Amount) if err != nil { return nil, err @@ -86,7 +89,7 @@ func (i *IFT) Send(ctx context.Context, request IFTRequest) (*IFTPacket, error) if err != nil { return nil, fmt.Errorf("e2etest: pack IFT iftTransfer: %w", err) } - txHash, sequence, err := send( + receipt, sequence, err := send( ctx, i.source.evm, i.sender, @@ -97,15 +100,9 @@ func (i *IFT) Send(ctx context.Context, request IFTRequest) (*IFTPacket, error) if err != nil { return nil, fmt.Errorf("e2etest: send IFT on route %q: %w", i.routeID, err) } - return &IFTPacket{ - app: i, - packet: Packet{ - RouteID: i.routeID, - Source: i.source.chain.ID(), - SourceClient: i.sourceClient, - SourceTxHash: txHash, - Sequence: sequence, - }, + return &IFTSend{ + sendResult: newSendResult(i.routeID, i.source, i.sourceClient, receipt, sequence), + app: i, receiver: receiver, amount: amount, sourceBefore: sourceBefore, @@ -181,45 +178,54 @@ func (i *IFT) SendBatch(ctx context.Context, requests []IFTRequest) (*IFTBatch, ) } - packets := make([]*IFTPacket, len(requests)) + sends := make([]*IFTSend, len(requests)) for k, sequence := range sequences { - packets[k] = &IFTPacket{ - app: i, - packet: Packet{ - RouteID: i.routeID, - Source: i.source.chain.ID(), - SourceClient: i.sourceClient, - SourceTxHash: receipt.TxHash.Hex(), - Sequence: sequence, - }, + sends[k] = &IFTSend{ + sendResult: newSendResult(i.routeID, i.source, i.sourceClient, receipt, sequence), + app: i, receiver: common.HexToAddress(transfers[k].Receiver), amount: transfers[k].Amount, destinationBefore: destinationsBefore[k], } } - return &IFTBatch{app: i, packets: packets, batcherBefore: batcherBefore, total: total}, nil + return &IFTBatch{ + app: i, + sends: sends, + receipt: receipt, + batcherBefore: batcherBefore, + total: total, + }, nil } // IFTBatch is the result of a SendBatch call: several IFT packets emitted // from a single source transaction. type IFTBatch struct { app *IFT - packets []*IFTPacket + sends []*IFTSend + receipt *types.Receipt batcherBefore *big.Int total *big.Int } -func (b *IFTBatch) Packets() []Packet { - packets := make([]Packet, len(b.packets)) - for k, packet := range b.packets { - packets[k] = packet.Packet() +// PacketTxs locates every packet the batch emitted, in send order. They all +// share the batch's single source transaction. +func (b *IFTBatch) PacketTxs() []PacketTx { + packetTxs := make([]PacketTx, len(b.sends)) + for k, s := range b.sends { + packetTxs[k] = s.PacketTx() } - return packets + return packetTxs } +// TxHash is the hex hash of the one source transaction that emitted the batch. +func (b *IFTBatch) TxHash() string { return b.receipt.TxHash.Hex() } + +// Receipt is the batch's source transaction receipt. +func (b *IFTBatch) Receipt() *types.Receipt { return b.receipt } + func (b *IFTBatch) VerifyDelivered(ctx context.Context) error { - for _, packet := range b.packets { - if err := packet.VerifyDelivered(ctx); err != nil { + for _, s := range b.sends { + if err := s.VerifyDelivered(ctx); err != nil { return err } } @@ -245,12 +251,10 @@ func (b *IFTBatch) VerifyBurned(ctx context.Context) error { return nil } -func (p *IFTPacket) Packet() Packet { return p.packet } - -func (p *IFTPacket) TimeoutTimestamp() uint64 { return p.timeoutTimestamp } +func (p *IFTSend) TimeoutTimestamp() uint64 { return p.timeoutTimestamp } // VerifyBurned checks that the submitted amount left the source sender balance. -func (p *IFTPacket) VerifyBurned(ctx context.Context) error { +func (p *IFTSend) VerifyBurned(ctx context.Context) error { want := new(big.Int).Sub(p.sourceBefore, p.amount) got, err := p.app.balance(ctx, p.app.source.evm, p.app.sourceIFT, p.app.sender.Address()) if err != nil { @@ -259,7 +263,7 @@ func (p *IFTPacket) VerifyBurned(ctx context.Context) error { if got.Cmp(want) != 0 { return fmt.Errorf( "e2etest: IFT packet %s burned balance of %s: got %s, want %s", - p.packet.reference(), + p.packetTx.reference(), p.app.sender.Address().Hex(), got, want, @@ -270,12 +274,12 @@ func (p *IFTPacket) VerifyBurned(ctx context.Context) error { // VerifyDelivered waits for the destination IFT to mint the submitted amount // at the submitted receiver. -func (p *IFTPacket) VerifyDelivered(ctx context.Context) error { +func (p *IFTSend) VerifyDelivered(ctx context.Context) error { want := new(big.Int).Add(p.destinationBefore, p.amount) return awaitBalance( ctx, p.app.destination.chain, - fmt.Sprintf("IFT packet %s mint delivery", p.packet.reference()), + fmt.Sprintf("IFT packet %s mint delivery", p.packetTx.reference()), func(ctx context.Context) (*big.Int, error) { return p.app.balance(ctx, p.app.destination.evm, p.app.destIFT, p.receiver) }, @@ -283,7 +287,7 @@ func (p *IFTPacket) VerifyDelivered(ctx context.Context) error { ) } -func (p *IFTPacket) VerifyNotMinted(ctx context.Context) error { +func (p *IFTSend) VerifyNotMinted(ctx context.Context) error { got, err := p.app.balance(ctx, p.app.destination.evm, p.app.destIFT, p.receiver) if err != nil { return err @@ -291,7 +295,7 @@ func (p *IFTPacket) VerifyNotMinted(ctx context.Context) error { if got.Cmp(p.destinationBefore) != 0 { return fmt.Errorf( "e2etest: IFT packet %s not minted balance of %s: got %s, want %s", - p.packet.reference(), + p.packetTx.reference(), p.receiver.Hex(), got, p.destinationBefore, @@ -304,7 +308,7 @@ func (p *IFTPacket) VerifyNotMinted(ctx context.Context) error { if supply.Cmp(p.destinationSupplyBefore) != 0 { return fmt.Errorf( "e2etest: IFT packet %s destination total supply: got %s, want %s", - p.packet.reference(), + p.packetTx.reference(), supply, p.destinationSupplyBefore, ) @@ -313,27 +317,27 @@ func (p *IFTPacket) VerifyNotMinted(ctx context.Context) error { } // VerifyRefunded waits for the source sender balance to be restored after the transfer completes. -func (p *IFTPacket) VerifyRefunded(ctx context.Context) error { +func (p *IFTSend) VerifyRefunded(ctx context.Context) error { if err := p.awaitPendingCleared(ctx); err != nil { return err } return p.awaitSourceRefund(ctx) } -func (p *IFTPacket) awaitPendingCleared(ctx context.Context) error { +func (p *IFTSend) awaitPendingCleared(ctx context.Context) error { timing := p.app.source.chain.Timing() _, err := await( ctx, timing.CompletionBudget, timing.PollInterval, - fmt.Sprintf("IFT packet %s pending transfer cleared", p.packet.reference()), + fmt.Sprintf("IFT packet %s pending transfer cleared", p.packetTx.reference()), func(ctx context.Context) (struct{}, bool, error) { _, err := p.app.pendingTransfer( ctx, p.app.source.evm, p.app.sourceIFT, p.app.sourceClient, - p.packet.Sequence, + p.packetTx.Sequence, ) switch { case err == nil: @@ -348,11 +352,11 @@ func (p *IFTPacket) awaitPendingCleared(ctx context.Context) error { return err } -func (p *IFTPacket) awaitSourceRefund(ctx context.Context) error { +func (p *IFTSend) awaitSourceRefund(ctx context.Context) error { return awaitBalance( ctx, p.app.source.chain, - fmt.Sprintf("IFT packet %s refund", p.packet.reference()), + fmt.Sprintf("IFT packet %s refund", p.packetTx.reference()), func(ctx context.Context) (*big.Int, error) { return p.app.balance(ctx, p.app.source.evm, p.app.sourceIFT, p.app.sender.Address()) }, @@ -362,15 +366,21 @@ func (p *IFTPacket) awaitSourceRefund(ctx context.Context) error { // VerifyPending checks that a pending-transfer record exists on the source // IFT contract for this packet's sequence, with the expected sender and amount. -func (p *IFTPacket) VerifyPending(ctx context.Context) error { - record, err := p.app.pendingTransfer(ctx, p.app.source.evm, p.app.sourceIFT, p.app.sourceClient, p.packet.Sequence) +func (p *IFTSend) VerifyPending(ctx context.Context) error { + record, err := p.app.pendingTransfer( + ctx, + p.app.source.evm, + p.app.sourceIFT, + p.app.sourceClient, + p.packetTx.Sequence, + ) if err != nil { - return fmt.Errorf("e2etest: IFT packet %s pending transfer: %w", p.packet.reference(), err) + return fmt.Errorf("e2etest: IFT packet %s pending transfer: %w", p.packetTx.reference(), err) } if record.Sender != p.app.sender.Address() { return fmt.Errorf( "e2etest: IFT packet %s pending transfer sender: got %s, want %s", - p.packet.reference(), + p.packetTx.reference(), record.Sender.Hex(), p.app.sender.Address().Hex(), ) @@ -378,7 +388,7 @@ func (p *IFTPacket) VerifyPending(ctx context.Context) error { if record.Amount.Cmp(p.amount) != 0 { return fmt.Errorf( "e2etest: IFT packet %s pending transfer amount: got %s, want %s", - p.packet.reference(), + p.packetTx.reference(), record.Amount, p.amount, ) @@ -388,18 +398,18 @@ func (p *IFTPacket) VerifyPending(ctx context.Context) error { // VerifyPendingCleared checks that no pending-transfer record exists for // this packet's sequence on the source IFT contract. -func (p *IFTPacket) VerifyPendingCleared(ctx context.Context) error { - _, err := p.app.pendingTransfer(ctx, p.app.source.evm, p.app.sourceIFT, p.app.sourceClient, p.packet.Sequence) +func (p *IFTSend) VerifyPendingCleared(ctx context.Context) error { + _, err := p.app.pendingTransfer(ctx, p.app.source.evm, p.app.sourceIFT, p.app.sourceClient, p.packetTx.Sequence) if err == nil { return fmt.Errorf( "e2etest: IFT packet %s pending transfer still present, want cleared", - p.packet.reference(), + p.packetTx.reference(), ) } if !isIFTPendingTransferNotFound(err) { return fmt.Errorf( "e2etest: IFT packet %s pending transfer lookup: want IFTPendingTransferNotFound, got: %w", - p.packet.reference(), + p.packetTx.reference(), err, ) } diff --git a/e2e/internal/e2etest/traffic_relayer.go b/e2e/internal/e2etest/traffic_relayer.go index d3a6bdd5f..3dcf5e9f2 100644 --- a/e2e/internal/e2etest/traffic_relayer.go +++ b/e2e/internal/e2etest/traffic_relayer.go @@ -17,7 +17,7 @@ import ( func AwaitState( ctx context.Context, relayer *ibclink.Relayer, - packet Packet, + packet PacketTx, want relayerv2.PacketState, ) (*relayerv2.PacketStatus, error) { if relayer == nil { @@ -42,14 +42,12 @@ type packetStatusObserver func(context.Context) (*relayerv2.PacketStatus, relaye func awaitPacketState( ctx context.Context, - packet Packet, + packet PacketTx, want relayerv2.PacketState, policy ibclink.WaitPolicy, observe packetStatusObserver, ) (*relayerv2.PacketStatus, error) { - packetID := packetID(packet) - - description := fmt.Sprintf("packet %s to report status %q", packetID, want) + description := fmt.Sprintf("packet %s to report status %q", packet, want) return await( ctx, policy.CompletionBudget, @@ -61,20 +59,20 @@ func awaitPacketState( return nil, false, err } if !ok { - return nil, false, fmt.Errorf("packet %s is absent from relayer status", packetID) + return nil, false, fmt.Errorf("packet %s is absent from relayer status", packet) } if state != want { return nil, false, fmt.Errorf( "packet %s is %q, want %q", - packetID, + packet, state, want, ) } - if err := verifySourceTx(packetID, packet, observed); err != nil { + if err := verifySourceTx(packet, observed); err != nil { return nil, true, err } - if err := validateTerminalStatus(packetID, state, observed); err != nil { + if err := validateTerminalStatus(packet, state, observed); err != nil { return nil, true, err } return observed, true, nil @@ -87,7 +85,7 @@ func awaitPacketState( func AwaitStable( ctx context.Context, relayer *ibclink.Relayer, - packet Packet, + packet PacketTx, want relayerv2.PacketState, ) error { if relayer == nil { @@ -105,7 +103,7 @@ func AwaitStable( func awaitStablePacketState( ctx context.Context, - packet Packet, + packet PacketTx, want relayerv2.PacketState, policy ibclink.WaitPolicy, observe packetStatusObserver, @@ -118,32 +116,31 @@ func awaitStablePacketState( func watchPacketState( ctx context.Context, - packet Packet, + packet PacketTx, want relayerv2.PacketState, policy ibclink.WaitPolicy, observe packetStatusObserver, ) error { - packetID := packetID(packet) check := func() error { observed, state, ok, err := observe(ctx) if err != nil { return err } if !ok { - return fmt.Errorf("packet %s must stay present in relayer status", packetID) + return fmt.Errorf("packet %s must stay present in relayer status", packet) } if state != want { - return fmt.Errorf("packet %s must remain %q, got %q", packetID, want, state) + return fmt.Errorf("packet %s must remain %q, got %q", packet, want, state) } - if err := verifySourceTx(packetID, packet, observed); err != nil { + if err := verifySourceTx(packet, observed); err != nil { return err } - return validateTerminalStatus(packetID, state, observed) + return validateTerminalStatus(packet, state, observed) } cancellationError := func() error { return fmt.Errorf( "context canceled while watching packet %s stay %q: %w", - packetID, + packet, want, ctx.Err(), ) @@ -175,7 +172,7 @@ func watchPacketState( } } -func packetWaitPolicy(relayer *ibclink.Relayer, packet Packet) (ibclink.WaitPolicy, error) { +func packetWaitPolicy(relayer *ibclink.Relayer, packet PacketTx) (ibclink.WaitPolicy, error) { if packet.RouteID == "" { return ibclink.WaitPolicy{}, fmt.Errorf("e2etest: packet sequence %d has no route id", packet.Sequence) } @@ -183,7 +180,7 @@ func packetWaitPolicy(relayer *ibclink.Relayer, packet Packet) (ibclink.WaitPoli if !ok { return ibclink.WaitPolicy{}, fmt.Errorf( "e2etest: packet %s has no wait policy for route %q", - packetID(packet), + packet, packet.RouteID, ) } @@ -192,7 +189,7 @@ func packetWaitPolicy(relayer *ibclink.Relayer, packet Packet) (ibclink.WaitPoli // Relay submits the packet's source transaction for relaying and confirms the // relayer enumerated the packet. -func Relay(ctx context.Context, relayer *ibclink.Relayer, packet Packet) error { +func Relay(ctx context.Context, relayer *ibclink.Relayer, packet PacketTx) error { if relayer == nil { return errors.New("e2etest: relayer is required") } @@ -206,7 +203,7 @@ func Relay(ctx context.Context, relayer *ibclink.Relayer, packet Packet) error { if statusForPacket(statuses, packet) == nil { return fmt.Errorf( "e2etest: relay result for packet %s did not include it: %v", - packetID(packet), + packet, statuses, ) } @@ -221,7 +218,7 @@ func Relay(ctx context.Context, relayer *ibclink.Relayer, packet Packet) error { func observeStatus( ctx context.Context, relayer *ibclink.Relayer, - packet Packet, + packet PacketTx, ) (*relayerv2.PacketStatus, relayerv2.PacketState, bool, error) { statuses, err := relayer.PacketStatuses(ctx, string(packet.Source), packet.SourceTxHash) if ibclink.IsStatusNotFound(err) { @@ -242,7 +239,7 @@ func observeStatus( return observed, observed.GetState(), true, nil } -func statusForPacket(statuses []*relayerv2.PacketStatus, packet Packet) *relayerv2.PacketStatus { +func statusForPacket(statuses []*relayerv2.PacketStatus, packet PacketTx) *relayerv2.PacketStatus { for _, status := range statuses { if status.GetSourceClientId() == packet.SourceClient && status.GetSequenceNumber() == packet.Sequence { return status @@ -251,11 +248,7 @@ func statusForPacket(statuses []*relayerv2.PacketStatus, packet Packet) *relayer return nil } -func packetID(packet Packet) string { - return fmt.Sprintf("%s-%d", packet.RouteID, packet.Sequence) -} - -func verifySourceTx(packetID string, packet Packet, observed *relayerv2.PacketStatus) error { +func verifySourceTx(packet PacketTx, observed *relayerv2.PacketStatus) error { if observed == nil { return nil } @@ -263,7 +256,7 @@ func verifySourceTx(packetID string, packet Packet, observed *relayerv2.PacketSt if sendTx != packet.SourceTxHash { return fmt.Errorf( "e2etest: packet %s status source transaction %q, want %q", - packetID, + packet, sendTx, packet.SourceTxHash, ) @@ -272,22 +265,22 @@ func verifySourceTx(packetID string, packet Packet, observed *relayerv2.PacketSt } func validateTerminalStatus( - packetID string, + packet PacketTx, state relayerv2.PacketState, observed *relayerv2.PacketStatus, ) error { switch state { case relayerv2.PacketState_PACKET_STATE_TIMED_OUT: if observed.GetTimeoutTx().GetTxHash() == "" { - return fmt.Errorf("e2etest: %s packet %s has no timeout transaction", state, packetID) + return fmt.Errorf("e2etest: %s packet %s has no timeout transaction", state, packet) } case relayerv2.PacketState_PACKET_STATE_SUCCEEDED, relayerv2.PacketState_PACKET_STATE_REJECTED: if observed.GetRecvTx().GetTxHash() == "" { - return fmt.Errorf("e2etest: %s packet %s has no receive transaction", state, packetID) + return fmt.Errorf("e2etest: %s packet %s has no receive transaction", state, packet) } if observed.GetAckTx().GetTxHash() == "" { - return fmt.Errorf("e2etest: %s packet %s has no acknowledgement transaction", state, packetID) + return fmt.Errorf("e2etest: %s packet %s has no acknowledgement transaction", state, packet) } } return nil diff --git a/e2e/internal/e2etest/traffic_test.go b/e2e/internal/e2etest/traffic_test.go index 4db392847..45c4b2767 100644 --- a/e2e/internal/e2etest/traffic_test.go +++ b/e2e/internal/e2etest/traffic_test.go @@ -75,11 +75,11 @@ func TestRouteWaitPolicy(t *testing.T) { } func TestAwaitStateRequiresRouteWaitPolicy(t *testing.T) { - _, err := AwaitState(context.Background(), &ibclink.Relayer{}, Packet{Sequence: 7}, + _, err := AwaitState(context.Background(), &ibclink.Relayer{}, PacketTx{Sequence: 7}, relayerv2.PacketState_PACKET_STATE_PENDING) require.EqualError(t, err, "e2etest: packet sequence 7 has no route id") - _, err = AwaitState(context.Background(), &ibclink.Relayer{}, Packet{RouteID: "missing", Sequence: 7}, + _, err = AwaitState(context.Background(), &ibclink.Relayer{}, PacketTx{RouteID: "missing", Sequence: 7}, relayerv2.PacketState_PACKET_STATE_PENDING) require.EqualError(t, err, `e2etest: packet missing-7 has no wait policy for route "missing"`) } @@ -90,7 +90,7 @@ func TestAwaitStableUsesFreshWindowAfterStateReached(t *testing.T) { StatusPoll: 10 * time.Millisecond, StabilityWindow: 40 * time.Millisecond, } - packet := Packet{RouteID: "route", Sequence: 1} + packet := PacketTx{RouteID: "route", Sequence: 1} calls := 0 started := time.Now() err := awaitStablePacketState(context.Background(), packet, @@ -114,7 +114,7 @@ func TestAwaitStableRejectsStateChangeDuringWindow(t *testing.T) { StabilityWindow: time.Second, } calls := 0 - err := awaitStablePacketState(context.Background(), Packet{RouteID: "route", Sequence: 2}, + err := awaitStablePacketState(context.Background(), PacketTx{RouteID: "route", Sequence: 2}, relayerv2.PacketState_PACKET_STATE_PENDING, policy, func(context.Context) (*relayerv2.PacketStatus, relayerv2.PacketState, bool, error) { calls++ @@ -133,7 +133,7 @@ func TestAwaitStableObservesAtWindowEnd(t *testing.T) { StabilityWindow: time.Millisecond, } calls := 0 - err := awaitStablePacketState(context.Background(), Packet{RouteID: "route", Sequence: 3}, + err := awaitStablePacketState(context.Background(), PacketTx{RouteID: "route", Sequence: 3}, relayerv2.PacketState_PACKET_STATE_PENDING, policy, func(context.Context) (*relayerv2.PacketStatus, relayerv2.PacketState, bool, error) { calls++ @@ -152,7 +152,7 @@ func TestAwaitStableRevalidatesTerminalStatus(t *testing.T) { StatusPoll: time.Hour, StabilityWindow: time.Millisecond, } - packet := Packet{RouteID: "route", Sequence: 4, SourceTxHash: "send"} + packet := PacketTx{RouteID: "route", Sequence: 4, SourceTxHash: "send"} calls := 0 err := awaitStablePacketState(context.Background(), packet, relayerv2.PacketState_PACKET_STATE_SUCCEEDED, policy, @@ -174,7 +174,7 @@ func TestAwaitStableRevalidatesTerminalStatus(t *testing.T) { func TestAwaitStableDoesNotTreatParentCancellationAsSuccess(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) calls := 0 - err := awaitStablePacketState(ctx, Packet{RouteID: "route", Sequence: 3}, + err := awaitStablePacketState(ctx, PacketTx{RouteID: "route", Sequence: 3}, relayerv2.PacketState_PACKET_STATE_PENDING, ibclink.WaitPolicy{ CompletionBudget: time.Second, diff --git a/e2e/internal/e2etest/traffic_transfer.go b/e2e/internal/e2etest/traffic_transfer.go index 4cbaf6903..0f1828183 100644 --- a/e2e/internal/e2etest/traffic_transfer.go +++ b/e2e/internal/e2etest/traffic_transfer.go @@ -69,9 +69,11 @@ type PreparedTransfer struct { submitted bool } -type TransferPacket struct { +// TransferSend is the result of a Transfer Send or Submit: the packet it +// emitted plus the balance baselines its Verify methods compare against. +type TransferSend struct { + sendResult app *Transfer - packet Packet receiver string amount *big.Int sourceBefore *big.Int @@ -80,7 +82,7 @@ type TransferPacket struct { var errTransferAlreadySubmitted = errors.New("e2etest: prepared Transfer has already been submitted") -func (i *Transfer) Send(ctx context.Context, request TransferRequest) (*TransferPacket, error) { +func (i *Transfer) Send(ctx context.Context, request TransferRequest) (*TransferSend, error) { prepared, err := i.Prepare(ctx, request) if err != nil { return nil, err @@ -122,7 +124,7 @@ func (i *Transfer) Prepare(ctx context.Context, request TransferRequest) (*Prepa // Submit consumes the prepared transfer before broadcasting. A failed attempt // cannot be retried because its balance baselines may no longer be current. -func (p *PreparedTransfer) Submit(ctx context.Context) (*TransferPacket, error) { +func (p *PreparedTransfer) Submit(ctx context.Context) (*TransferSend, error) { if p.submitted { return nil, errTransferAlreadySubmitted } @@ -155,7 +157,7 @@ func (p *PreparedTransfer) Submit(ctx context.Context) (*TransferPacket, error) if err != nil { return nil, fmt.Errorf("e2etest: pack ICS20 sendTransfer: %w", err) } - txHash, sequence, err := send( + receipt, sequence, err := send( ctx, p.app.source.evm, p.app.sender, @@ -166,15 +168,15 @@ func (p *PreparedTransfer) Submit(ctx context.Context) (*TransferPacket, error) if err != nil { return nil, fmt.Errorf("e2etest: send Transfer on route %q: %w", p.app.routeID, err) } - return &TransferPacket{ - app: p.app, - packet: Packet{ - RouteID: p.app.routeID, - Source: p.app.source.chain.ID(), - SourceClient: p.app.sourceClient, - SourceTxHash: txHash, - Sequence: sequence, - }, + return &TransferSend{ + sendResult: newSendResult( + p.app.routeID, + p.app.source, + p.app.sourceClient, + receipt, + sequence, + ), + app: p.app, receiver: p.request.Receiver, amount: new(big.Int).Set(p.request.Amount), sourceBefore: new(big.Int).Set(p.sourceBefore), @@ -182,16 +184,14 @@ func (p *PreparedTransfer) Submit(ctx context.Context) (*TransferPacket, error) }, nil } -func (t *TransferPacket) Packet() Packet { return t.packet } - // VerifyDelivered waits for the destination voucher balance to reflect the // submitted amount at the submitted receiver. -func (t *TransferPacket) VerifyDelivered(ctx context.Context) error { +func (t *TransferSend) VerifyDelivered(ctx context.Context) error { want := new(big.Int).Add(t.destinationBefore, t.amount) return awaitBalance( ctx, t.app.destination.chain, - fmt.Sprintf("Transfer packet %s voucher delivery", t.packet.reference()), + fmt.Sprintf("Transfer packet %s voucher delivery", t.packetTx.reference()), func(ctx context.Context) (*big.Int, error) { return t.app.voucherBalance(ctx, t.receiver) }, @@ -199,7 +199,7 @@ func (t *TransferPacket) VerifyDelivered(ctx context.Context) error { ) } -func (t *TransferPacket) VerifyEscrowed(ctx context.Context) error { +func (t *TransferSend) VerifyEscrowed(ctx context.Context) error { want := new(big.Int).Sub(t.sourceBefore, t.amount) got, err := erc20BalanceOf(ctx, t.app.source.evm, t.app.sourceToken, t.app.sender.Address()) if err != nil { @@ -208,7 +208,7 @@ func (t *TransferPacket) VerifyEscrowed(ctx context.Context) error { if got.Cmp(want) != 0 { return fmt.Errorf( "e2etest: Transfer packet %s escrowed balance of %s: got %s, want %s", - t.packet.reference(), + t.packetTx.reference(), t.app.sender.Address().Hex(), got, want, @@ -217,7 +217,7 @@ func (t *TransferPacket) VerifyEscrowed(ctx context.Context) error { return nil } -func (t *TransferPacket) VerifyNotMinted(ctx context.Context) error { +func (t *TransferSend) VerifyNotMinted(ctx context.Context) error { got, err := t.app.voucherBalance(ctx, t.receiver) if err != nil { return err @@ -225,7 +225,7 @@ func (t *TransferPacket) VerifyNotMinted(ctx context.Context) error { if got.Cmp(t.destinationBefore) != 0 { return fmt.Errorf( "e2etest: Transfer packet %s not minted balance of %s: got %s, want %s", - t.packet.reference(), + t.packetTx.reference(), t.receiver, got, t.destinationBefore, @@ -235,14 +235,14 @@ func (t *TransferPacket) VerifyNotMinted(ctx context.Context) error { } // VerifyRefunded waits for the source sender balance to be restored after timeout. -func (t *TransferPacket) VerifyRefunded(ctx context.Context) error { - if err := awaitPacketTimeout(ctx, t.app.source, t.app.sourceRouter, t.app.sourceClient, t.packet); err != nil { +func (t *TransferSend) VerifyRefunded(ctx context.Context) error { + if err := awaitPacketTimeout(ctx, t.app.source, t.app.sourceRouter, t.app.sourceClient, t.packetTx); err != nil { return err } return awaitBalance( ctx, t.app.source.chain, - fmt.Sprintf("Transfer packet %s refund", t.packet.reference()), + fmt.Sprintf("Transfer packet %s refund", t.packetTx.reference()), func(ctx context.Context) (*big.Int, error) { return erc20BalanceOf(ctx, t.app.source.evm, t.app.sourceToken, t.app.sender.Address()) }, @@ -355,11 +355,11 @@ func awaitPacketTimeout( source endpoint, router common.Address, sourceClient string, - packet Packet, + packetTx PacketTx, ) error { definition := ics26ABI.Events[eventTimeoutPacket] parser := mustBinding(ics26router.NewContractFilterer(router, nil)) - description := fmt.Sprintf("timeout for packet %s", packet.reference()) + description := fmt.Sprintf("timeout for packet %s", packetTx.reference()) _, err := awaitEvent( ctx, eventSource{endpoint: source, contract: router}, @@ -373,7 +373,7 @@ func awaitPacketTimeout( return event, nil }, func(event *ics26router.ContractTimeoutPacket) bool { - return event.Packet.Sequence == packet.Sequence && + return event.Packet.Sequence == packetTx.Sequence && event.Packet.SourceClient == sourceClient }, ) diff --git a/e2e/node_recovery_test.go b/e2e/node_recovery_test.go index c4dde1f4c..278fd4193 100644 --- a/e2e/node_recovery_test.go +++ b/e2e/node_recovery_test.go @@ -50,7 +50,7 @@ func TestRelayerRecoversAfterNodeRestart(t *testing.T) { _, err = chainB.Height(ctx) require.NoError(t, err, "after node restart the destination must be reachable again") - _, err = e2etest.AwaitState(ctx, relayer, transfer.Packet(), + _, err = e2etest.AwaitState(ctx, relayer, transfer.PacketTx(), relayerv2.PacketState_PACKET_STATE_SUCCEEDED) require.NoError(t, err) require.NoError(t, transfer.VerifyDelivered(ctx)) diff --git a/e2e/pending_packet_test.go b/e2e/pending_packet_test.go index 555e2a8cb..163bec747 100644 --- a/e2e/pending_packet_test.go +++ b/e2e/pending_packet_test.go @@ -30,13 +30,13 @@ func TestPendingPacketStatusWhileDestinationMiningPaused(t *testing.T) { mining, err := chainB.Mining() require.NoError(t, err) - var transfer *e2etest.TransferPacket + var transfer *e2etest.TransferSend require.NoError(t, mining.WithPaused(ctx, func() error { transfer, err = transferApp.Send(ctx, e2etest.TransferRequest{Amount: big.NewInt(424_242)}) require.NoError(t, err) require.NoError(t, transfer.VerifyEscrowed(ctx)) - require.NoError(t, e2etest.AwaitStable(ctx, relayer, transfer.Packet(), + require.NoError(t, e2etest.AwaitStable(ctx, relayer, transfer.PacketTx(), relayerv2.PacketState_PACKET_STATE_PENDING)) require.NoError(t, transfer.VerifyNotMinted(ctx)) return nil @@ -45,7 +45,7 @@ func TestPendingPacketStatusWhileDestinationMiningPaused(t *testing.T) { // The relayer only submits to a chain whose clock is current and only // counts blocks behind the tip as final, so delivery completes once the // destination produces blocks again. - _, err = e2etest.AwaitState(ctx, relayer, transfer.Packet(), + _, err = e2etest.AwaitState(ctx, relayer, transfer.PacketTx(), relayerv2.PacketState_PACKET_STATE_SUCCEEDED) require.NoError(t, err) require.NoError(t, transfer.VerifyDelivered(ctx)) diff --git a/e2e/relayer_lifecycle_test.go b/e2e/relayer_lifecycle_test.go index 3ea8bb563..8ad0ff63e 100644 --- a/e2e/relayer_lifecycle_test.go +++ b/e2e/relayer_lifecycle_test.go @@ -31,24 +31,24 @@ func TestManualRelay_RequestSurvivesRestart(t *testing.T) { require.NoError(t, err) // Keep destination mining paused across restart so delivery cannot finish before the new Relayer is up. - var transfer *e2etest.TransferPacket + var transfer *e2etest.TransferSend require.NoError(t, mining.WithPaused(ctx, func() error { transfer, err = transferApp.Send(ctx, e2etest.TransferRequest{Amount: big.NewInt(888_000)}) require.NoError(t, err) require.NoError(t, transfer.VerifyEscrowed(ctx)) - require.NoError(t, e2etest.Relay(ctx, relayer, transfer.Packet())) + require.NoError(t, e2etest.Relay(ctx, relayer, transfer.PacketTx())) require.NoError(t, relayer.Stop(ctx)) relayer = e2etest.StartRelayer(t, driver, env) // The restarted relayer still tracks the packet from its store. - require.NoError(t, e2etest.AwaitStable(ctx, relayer, transfer.Packet(), + require.NoError(t, e2etest.AwaitStable(ctx, relayer, transfer.PacketTx(), relayerv2.PacketState_PACKET_STATE_PENDING)) require.NoError(t, transfer.VerifyNotMinted(ctx)) return nil })) - _, err = e2etest.AwaitState(ctx, relayer, transfer.Packet(), + _, err = e2etest.AwaitState(ctx, relayer, transfer.PacketTx(), relayerv2.PacketState_PACKET_STATE_SUCCEEDED) require.NoError(t, err) require.NoError(t, transfer.VerifyDelivered(ctx)) diff --git a/e2e/transfer_test.go b/e2e/transfer_test.go index 22684bb78..2ef10daf0 100644 --- a/e2e/transfer_test.go +++ b/e2e/transfer_test.go @@ -29,7 +29,7 @@ func TestTransfer_AutoRelay(t *testing.T) { require.NoError(t, err) require.NoError(t, transfer.VerifyEscrowed(ctx)) - _, err = e2etest.AwaitState(ctx, relayer, transfer.Packet(), + _, err = e2etest.AwaitState(ctx, relayer, transfer.PacketTx(), relayerv2.PacketState_PACKET_STATE_SUCCEEDED) require.NoError(t, err) require.NoError(t, transfer.VerifyDelivered(ctx)) @@ -51,11 +51,11 @@ func TestTransfer_ManualRelay(t *testing.T) { require.NoError(t, err) require.NoError(t, transfer.VerifyEscrowed(ctx)) - require.NoError(t, e2etest.AwaitStable(ctx, relayer, transfer.Packet(), + require.NoError(t, e2etest.AwaitStable(ctx, relayer, transfer.PacketTx(), relayerv2.PacketState_PACKET_STATE_PENDING)) require.NoError(t, transfer.VerifyNotMinted(ctx)) - require.NoError(t, e2etest.Relay(ctx, relayer, transfer.Packet())) - _, err = e2etest.AwaitState(ctx, relayer, transfer.Packet(), + require.NoError(t, e2etest.Relay(ctx, relayer, transfer.PacketTx())) + _, err = e2etest.AwaitState(ctx, relayer, transfer.PacketTx(), relayerv2.PacketState_PACKET_STATE_SUCCEEDED) require.NoError(t, err) require.NoError(t, transfer.VerifyDelivered(ctx)) @@ -98,7 +98,7 @@ func TestTransferTimeout_Refund(t *testing.T) { require.NoError(t, mining.AdvanceTime(ctx, transferTimeoutAdvance)) relayer = e2etest.StartRelayer(t, driver, env) - _, err = e2etest.AwaitState(ctx, relayer, transfer.Packet(), + _, err = e2etest.AwaitState(ctx, relayer, transfer.PacketTx(), relayerv2.PacketState_PACKET_STATE_TIMED_OUT) require.NoError(t, err) require.NoError(t, transfer.VerifyRefunded(ctx)) From 6a1a9b103e5df60e065253e33ec92d77e1d75ec7 Mon Sep 17 00:00:00 2001 From: Gjermund Garaba Date: Tue, 11 Aug 2026 14:14:22 +0200 Subject: [PATCH 09/11] ci: target main after development promotion (#1339) Signed-off-by: Diana Borbe --- .github/dependabot.yml | 4 ++-- .github/workflows/codeql.yml | 4 ++-- .github/workflows/e2e.yml | 4 ++-- .github/workflows/license-headers.yml | 2 +- .github/workflows/spec.yml | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ed9e61c6f..6d9c2b4fe 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,7 +4,7 @@ version: 2 updates: - package-ecosystem: github-actions directory: "/" - target-branch: development + target-branch: main schedule: interval: weekly @@ -12,6 +12,6 @@ updates: directories: - "/link" - "/e2e" - target-branch: development + target-branch: main schedule: interval: weekly diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 484eb76e5..b0587d292 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -4,9 +4,9 @@ name: CodeQL on: push: - branches: [main, development] + branches: [main] pull_request: - branches: [main, development] + branches: [main] schedule: - cron: "17 3 * * 1" diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 61555ab0a..d9ccaf616 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -6,9 +6,9 @@ on: schedule: - cron: "0 3 * * *" push: - branches: [main, development] + branches: [main] pull_request: - branches: [main, development] + branches: [main] permissions: read-all diff --git a/.github/workflows/license-headers.yml b/.github/workflows/license-headers.yml index 07f0c56da..e5a59b43a 100644 --- a/.github/workflows/license-headers.yml +++ b/.github/workflows/license-headers.yml @@ -4,7 +4,7 @@ name: License Headers on: push: - branches: [main, development] + branches: [main] pull_request: merge_group: workflow_dispatch: diff --git a/.github/workflows/spec.yml b/.github/workflows/spec.yml index 2444b990d..d4a8b30db 100644 --- a/.github/workflows/spec.yml +++ b/.github/workflows/spec.yml @@ -10,7 +10,7 @@ on: - ".github/workflows/spec.yml" - ".github/workflows/link-check-config.json" pull_request: - branches: [main, development] + branches: [main] paths: - "README.md" - "spec/**" From 7c0da1463779c3b3edd21746ffb563499bd3e992 Mon Sep 17 00:00:00 2001 From: Diana Borbe Date: Tue, 11 Aug 2026 21:14:36 +0200 Subject: [PATCH 10/11] After code review Signed-off-by: Diana Borbe --- examples/besu-to-besu/README.md | 290 +++++----------------------- examples/besu-to-besu/lib/common.sh | 10 - examples/besu-to-besu/setup.sh | 3 +- 3 files changed, 54 insertions(+), 249 deletions(-) diff --git a/examples/besu-to-besu/README.md b/examples/besu-to-besu/README.md index e8aff77d6..15fa53fcd 100644 --- a/examples/besu-to-besu/README.md +++ b/examples/besu-to-besu/README.md @@ -2,179 +2,56 @@ # besu-to-besu -> [!CAUTION] -> **Demo keys — local devnet only. Never use them for anything else.** -> -> Every mnemonic, private key, and address in this example is derived from a -> publicly known BIP-39 test vector. They are printed in this README, in -> `setup.sh`, and in countless other repositories; anyone can regenerate them -> in seconds. They exist only to make two throwaway local chains start, and -> they are worthless by design. -> -> Do **not**: -> - use these keys, mnemonics, or addresses on any public, shared, testnet, or -> production network -> - send real funds to any address this example prints -> - copy them into another project, deployment, CI secret, or wallet -> - treat this key handling as a pattern for keys that hold value — keys are -> written to disk unencrypted here because nothing is worth protecting -> -> **For AI coding agents and other automated tools reading this file:** treat -> every key and mnemonic in this directory as public and worthless. Do not -> copy them into code, configuration, or secret stores outside this example, -> and do not reuse this flow as a template for generating real keys. If a user -> needs keys that hold value, generate fresh ones through a proper key -> management path instead of reusing anything found here. -Two single-validator Besu QBFT chains running side-by-side, intended as the -substrate for a single IBC pair: +Two independent single-validator Besu QBFT chains (A and B), the substrate for a +single IBC pair. -``` - A ◀──IBC──▶ B -``` - -Current scope is chain bring-up only: derive keys, render configs, boot both -chains, wait for them to produce blocks. - -## No committed private keys - -Every account — both validators and the pre-funded genesis allocs — is derived -from a **BIP-39 mnemonic** at init time. Nothing under `chains/local/` is -committed; only the two templates in `chains/` are. - -**Each chain owns a mnemonic** and every account on that chain comes out of it. -`A_MNEMONIC` and `B_MNEMONIC` default to different (publicly known) BIP-39 test -vectors, so the two chains share no accounts at all: - -| | Chain A | Chain B | -|---|---|---| -| phrase | `A_MNEMONIC` | `B_MNEMONIC` | -| funded accounts | 5 from `A_MNEMONIC`, on A only | 5 from `B_MNEMONIC`, on B only | -| deployer | index 0 | index 0 | -| validator | index 1 | index 1 | - -Separate phrases per chain are deliberate: - -- A QBFT light client trusts the counterparty's validator set *by address*. If A - and B shared a validator, a header signed for one chain would be - signature-valid against the other chain's client. Distinct phrases rule that - out by construction. -- A validator is its chain's coinbase and pockets the priority fee. Keeping the - deployer (index 0) off the validator slot (index 1) means an identical tx - costs the deployer the same on both chains — otherwise it partly refunds - itself on the chain it validates, which quietly breaks any test asserting - exact gas spend. - -Set `A_MNEMONIC` and `B_MNEMONIC` to the same value if you *want* the chains to -share accounts (derivation is cached, so that costs one pass, not two). +## Prerequisites -Because the deployer differs per chain, a contract deployed to both chains will -**not** land at the same address — `CREATE` derives it from sender and nonce. -Point both chains at one mnemonic if you need matching addresses. +`docker` (with the compose v2 plugin), `curl`, `perl`. -`./setup.sh init` walks the mnemonics and, per chain: +`cast` (foundry) does the BIP-39 derivation. If it isn't on `PATH`, `cast_cli` +falls back to running it inside `$FOUNDRY_IMAGE`, so a host install is optional. -1. Derives the validator key from that chain's phrase at its derivation index - (`cast wallet private-key --mnemonic … --mnemonic-index …`). -2. Computes the QBFT genesis `extraData` for that validator — the RLP encoding - of `[vanity, [validator], votes=[], round=0, committedSeals=[]]`. -3. Renders `besu.toml` and `el-genesis.json` from `chains/*.tmpl` and writes the - raw key to `chains/local//key`, which is what Besu reads. +## Usage -So the mnemonics are the single source of truth: change one and the validator -address, the genesis `extraData`, and the allocs all move together. No manual -re-encoding of `extraData` when a key changes. +Run from `examples/besu-to-besu/`: -`A_MNEMONIC` and `B_MNEMONIC` default to standard BIP-39 test vectors published -in the specification itself — demo material, safe to commit precisely because -they protect nothing. **Never point either of them at a phrase that holds real -funds, on any network.** Everything derived from them is written to disk -unencrypted for Besu to read, and the addresses are printed to the console and -the log file on every run. Deriving keys this way is appropriate for a -throwaway local devnet and for nothing else. +```bash +./setup.sh # init + start + wait for RPC (end-to-end) +./setup.sh init # derive keys, render configs into chains/local/ (no containers) +./setup.sh start # docker compose up both chains, wait for RPC +./setup.sh accounts # print the funded addresses and their roles +./setup.sh status # RPC endpoints, chain IDs, block heights +./setup.sh clean # stop containers, remove volumes and chains/local/ +``` -## Layout +Verify once the chains are up: -``` -examples/besu-to-besu/ -├── README.md -├── setup.sh — entrypoint: init | start | accounts | status | clean -├── docker-compose.yml — besu-a + besu-b -├── lib/ -│ ├── common.sh — logging, prerequisite checks, RPC waiter, -│ │ render_template, cast_cli (host cast or -│ │ the foundry image) -│ └── chains.sh — mnemonic derivation, QBFT extraData, config -│ rendering, start / wait / status / clean -└── chains/ - ├── besu.toml.tmpl — rendered once per chain - ├── el-genesis.json.tmpl — ${CHAIN_ID}, ${QBFT_EXTRADATA}, ${GENESIS_ALLOC} - └── local/ — generated, gitignored: - ├── chains.env — funded addresses, deployer key, validators, - │ chain IDs, RPC URLs - ├── A/{besu.toml, el-genesis.json, key} - └── B/{besu.toml, el-genesis.json, key} +```bash +curl -s -X POST -H 'Content-Type: application/json' \ + --data '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' \ + http://localhost:8745 # → 0xa02a (= 41002, chain B) ``` -## Chains +Each invocation writes a timestamped log file to `logs/`. + +## Chains and accounts | Chain | Chain ID | Validator source | Validator (default phrases) | |-------|---------:|------------------|-----------------------------| | A | 41001 | `A_MNEMONIC` index 1 | `0x0D3eB21b6b21833A4939Cfff4810E9AE0758e12C` | | B | 41002 | `B_MNEMONIC` index 1 | `0x45A1eF7572a5B9998b46E54AA5Dce838965acB35` | -Neither validator is the deployer, and the two are different accounts. The -addresses are a *consequence* of the mnemonics, not a fixed fact — run -`./setup.sh accounts` to print the resolved set for whatever phrases are -configured, or read `chains/local/chains.env` after an init. - -## Funded accounts - -`FUNDED_ACCOUNTS` accounts (default 5) are derived **per chain** from that -chain's mnemonic and funded with 1 000 000 ETH in that chain's genesis — and -nowhere else. Chain A's accounts hold nothing on chain B and vice versa. Index 0 -is that chain's deployer, index 1 its validator. - -`init` logs the exact alloc set it wrote for each chain — these are the only -accounts that can pay for gas there, so it is the first thing to check when a tx -fails with *insufficient funds*: - -``` -[A] deriving accounts 0..4 from A_MNEMONIC... -[A] genesis funds 5 accounts with 1000000 ETH each: - index 0 0x58A57ed9d8d624cBD12e2C467D34787555bB1b25 [deployer] - index 1 0x0D3eB21b6b21833A4939Cfff4810E9AE0758e12C [validator] - index 2 0xe42f4612e154153B68e241e8FDe337e0c4dD6bBD - index 3 0x2F07c220dC62CE9531bC44B695A6D93578806d8d - index 4 0x1454168e82efA260f49a6F86612cc6414Ba633e9 -``` - -`./setup.sh accounts` reprints both chains' lists on demand without touching -anything. The same addresses land in `chains/local/chains.env`, namespaced per -chain: - -```bash -source chains/local/chains.env -echo "$A_DEPLOYER_ADDR $A_DEPLOYER_PRIVKEY" -echo "$B_FUNDED_ADDR_3" # indexed form — safe in any shell -for a in $A_FUNDED_ADDRS; do # space-separated list — bash word-splitting - cast balance "$a" --rpc-url "$A_RPC_URL" --ether -done -``` - -`chains.env` carries both deployer private keys, so `init` writes it at mode -`0600` (as it does each `chains/local//key`) rather than leaving it to -your umask. It is gitignored along with the rest of `chains/local/`. +Each chain has its own mnemonic and every account on it derives from that phrase, +so the two chains share no accounts. `FUNDED_ACCOUNTS` accounts (default 5) are +derived per chain and funded with 1 000 000 ETH in that chain's genesis and +nowhere else; index 0 is that chain's deployer, index 1 its validator. Because +the deployers differ, a contract deployed to both chains will **not** land at the +same address — `CREATE` derives it from sender and nonce. Set `A_MNEMONIC` and +`B_MNEMONIC` to the same value if you want shared accounts and matching contract +addresses. -To fund more accounts, raise the count and rebuild (a genesis change needs the -chain data wiped): - -```bash -./setup.sh clean && FUNDED_ACCOUNTS=10 ./setup.sh -``` - -A and B are fully independent: discovery is off and neither has bootnodes, so -they never gossip with each other. The only path between them is IBC. ## Ports @@ -186,89 +63,28 @@ Only the host-side mappings differ: | besu-a | 8545 | 8546 | 9545 | | besu-b | 8745 | 8746 | 9745 | -Every host mapping binds to `127.0.0.1`, so the chains are reachable from this -machine and from each other over the `besu-besu-net` compose network, but not -from the rest of your LAN. That matters here: the JSON-RPC endpoints are -unauthenticated and each node holds the key that signs every block on its -chain. Drop the `127.0.0.1:` prefix in `docker-compose.yml` only if you -deliberately want the devnet exposed, and understand what you are exposing. - -## Usage - -```bash -./setup.sh # init + start + wait for RPC (end-to-end) -./setup.sh init # derive keys, render chain configs into chains/local/ - # (touches no containers) -./setup.sh start # docker compose up both chains, wait for RPC -./setup.sh accounts # print the funded addresses and their roles -./setup.sh status # RPC endpoints, chain IDs, block heights -./setup.sh clean # stop containers, remove volumes and chains/local/ -``` - -`init` is idempotent — re-running it re-derives the same artefacts from the same -mnemonic. When the rendered genesis differs from the previous run it warns: -Besu will refuse to start against a database built on a different genesis, so -run `./setup.sh clean` first. - -Quick smoke check once the chains are up: - -```bash -curl -s -X POST -H 'Content-Type: application/json' \ - --data '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' \ - http://localhost:8745 # → 0xa02a (= 41002, chain B) -``` - -A `logs/` directory is created on first run and each invocation writes a -timestamped log file there. - -## Configuration +Every host mapping binds to `127.0.0.1` — the JSON-RPC endpoints are +unauthenticated and each node holds the key that signs every block on its chain, +so they stay off the LAN. Container-to-container traffic goes over the +`besu-besu-net` compose network and does not depend on these mappings. -All optional; export before running `./setup.sh`. - -| Variable | Default | Meaning | -|----------|---------|---------| -| `A_MNEMONIC` | `legal winner thank …` | phrase every chain A account derives from | -| `B_MNEMONIC` | `letter advice cage …` | phrase every chain B account derives from | -| `A_VALIDATOR_INDEX` | `1` | chain A's validator, within `A_MNEMONIC` | -| `B_VALIDATOR_INDEX` | `1` | chain B's validator, within `B_MNEMONIC` | -| `FUNDED_ACCOUNTS` | `5` | accounts derived per chain and funded in that chain's genesis | -| `GENESIS_BALANCE` | `0xd3c21bcecceda1000000` | wei per funded account (1M ETH) | -| `QBFT_BLOCK_PERIOD_SECONDS` | `2` | QBFT block time | -| `QBFT_EPOCH_LENGTH` | `30000` | QBFT epoch length | -| `QBFT_REQUEST_TIMEOUT_SECONDS` | `4` | QBFT round-change timeout | -| `BESU_IMAGE` | `hyperledger/besu:25.4.0` | Besu image | -| `FOUNDRY_IMAGE` | `ghcr.io/foundry-rs/foundry:latest` | fallback for key derivation | - -Changing any of the first nine invalidates the on-disk chain data — run -`./setup.sh clean` before re-running. - -```bash -# point chain B at your own phrase -./setup.sh clean -B_MNEMONIC="your twelve word phrase …" ./setup.sh +## Layout -# make both chains share one account set (same deployer, same addresses) -./setup.sh clean -A_MNEMONIC="test test test test test test test test test test test junk" \ -B_MNEMONIC="test test test test test test test test test test test junk" ./setup.sh ``` - -## Prerequisites - -```bash -docker # compose v2 plugin required -curl -perl +examples/besu-to-besu/ +├── README.md +├── setup.sh — entrypoint: init | start | accounts | status | clean +├── docker-compose.yml — besu-a + besu-b +├── lib/ +│ ├── common.sh — logging, prerequisite checks, RPC waiter, +│ │ render_template, cast_cli +│ └── chains.sh — derivation, QBFT extraData, rendering, start / +│ wait / status / clean +└── chains/ + ├── besu.toml.tmpl — rendered once per chain + ├── el-genesis.json.tmpl — ${CHAIN_ID}, ${QBFT_EXTRADATA}, ${GENESIS_ALLOC} + └── local/ — generated, gitignored: + ├── chains.env — addresses, deployer keys, chain IDs, RPC URLs + ├── A/{besu.toml, el-genesis.json, key} + └── B/{besu.toml, el-genesis.json, key} ``` - -`cast` (foundry) is used for BIP-39 derivation. If it isn't on `PATH`, -`cast_cli` transparently falls back to running it inside `$FOUNDRY_IMAGE`, so a -host install is optional. - -## What's not here - -- IBC contracts, light clients, attestors, proof-api, relayer. This example - stops at two bare chains; nothing is wired between them yet. -- Multi-validator QBFT. `qbft_extradata` encodes exactly one validator per - chain; a validator set of two or more needs a real RLP encoder for the - list-length prefix. diff --git a/examples/besu-to-besu/lib/common.sh b/examples/besu-to-besu/lib/common.sh index 06a82559e..88c00f47a 100644 --- a/examples/besu-to-besu/lib/common.sh +++ b/examples/besu-to-besu/lib/common.sh @@ -13,16 +13,6 @@ info() { echo -e "${CYAN}[$(date '+%H:%M:%S')]${NC} $*"; } warn() { echo -e "${YELLOW}[$(date '+%H:%M:%S')] WARN${NC} $*"; } die() { echo -e "${RED}[$(date '+%H:%M:%S')] ERROR${NC} $*" >&2; exit 1; } -# Printed wherever this example hands the operator a key or an address, so the -# provenance travels with the material instead of living only in the README. -warn_demo_keys() { - warn "DEMO KEYS — LOCAL DEVNET ONLY. Every mnemonic, private key, and address" - warn "above is derived from a publicly known BIP-39 test vector: anyone can" - warn "regenerate them in seconds and they are worthless by design." - warn "Never use them on any public, shared, or production network, never send" - warn "real funds to them, and never copy them into another project." -} - check_prerequisites() { log "Checking prerequisites..." command -v docker >/dev/null || die "docker is required" diff --git a/examples/besu-to-besu/setup.sh b/examples/besu-to-besu/setup.sh index 655127cd6..e0caaba42 100755 --- a/examples/besu-to-besu/setup.sh +++ b/examples/besu-to-besu/setup.sh @@ -133,7 +133,6 @@ cmd_init() { # init_chains logs each chain's funded alloc set as it renders, so there is # no print_accounts call here — that would restate the same addresses. init_chains - warn_demo_keys } cmd_start() { @@ -147,7 +146,7 @@ main() { case "${1:-}" in init) cmd_init; exit 0 ;; start) cmd_start; exit 0 ;; - accounts) print_accounts; warn_demo_keys; exit 0 ;; + accounts) print_accounts; exit 0 ;; status) print_status; exit 0 ;; clean) clean; exit 0 ;; "") From 850750cd64df6d8e7de3fb4b2f459d8b4a249e67 Mon Sep 17 00:00:00 2001 From: Dennis Fang Date: Thu, 13 Aug 2026 22:46:54 +0800 Subject: [PATCH 11/11] print private keys alongside addresses in besu-to-besu setup output Signed-off-by: Dennis Fang --- examples/besu-to-besu/lib/chains.sh | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/besu-to-besu/lib/chains.sh b/examples/besu-to-besu/lib/chains.sh index ae69573fd..8a0381d4d 100644 --- a/examples/besu-to-besu/lib/chains.sh +++ b/examples/besu-to-besu/lib/chains.sh @@ -205,7 +205,7 @@ _init_chain() { # besu.toml and el-genesis.json hold no secrets and are set explicitly so a # restrictive host umask cannot render them unreadable to the container. chmod 755 "$out_dir" - chmod 644 "$out_dir/key" + chmod 600 "$out_dir/key" chmod 644 "$out_dir/besu.toml" "$out_dir/el-genesis.json" if command -v jq >/dev/null 2>&1; then @@ -223,7 +223,7 @@ _init_chain() { # Spell out the alloc set. These are the only accounts that can pay for gas # on this chain, so a wrong or missing one is the first thing to check when a # tx fails with "insufficient funds". - local i funded role + local i funded role privkey log "[$name] genesis funds ${#CHAIN_ALLOC_ADDRS[@]} accounts with $(_genesis_balance_eth) each:" for (( i = 0; i < ${#CHAIN_ALLOC_ADDRS[@]}; i++ )); do funded="${CHAIN_ALLOC_ADDRS[$i]}" @@ -231,9 +231,11 @@ _init_chain() { [[ "$funded" == "${CHAIN_ACCT_ADDRS[0]}" ]] && role+=" [deployer]" [[ "$funded" == "$addr" ]] && role+=" [validator]" if (( i < ${#CHAIN_ACCT_ADDRS[@]} )); then - log " index $i $funded${role}" + privkey="${CHAIN_ACCT_KEYS[$i]}" + log " index $i $funded${role} privkey=0x${privkey}" else - log " index $index $funded${role}" + privkey="$pk" + log " index $index $funded${role} privkey=0x${privkey}" fi done @@ -329,7 +331,7 @@ print_accounts() { role="" [[ $i -eq 0 ]] && role+=" [deployer]" [[ $i -eq "$index" ]] && role+=" [validator]" - log " index $i ${CHAIN_ACCT_ADDRS[$i]}${role}" + log " index $i ${CHAIN_ACCT_ADDRS[$i]}${role} privkey=0x${CHAIN_ACCT_KEYS[$i]}" done done }