// Scenario 2: nonce games across parallel blocks. One sender's transactions with nonces n..n+k are spread across // several parallel blocks (same selected parent) in different orders, with duplicates in several blocks and a // conflicting same-nonce pair. Design 1.3/1.4: exactly one execution per nonce, deterministic across all nodes, // state roots identical. The parallel blocks are built with igneum-inject off one template so they truly share a // parent; the honest node-1 miner then merges them and the executor orders the mergeset. import { keccak256 } from 'viem'; import * as k from './lib/common.mjs'; const results = { scenario: '2-nonce-games', rounds: [] }; const checks = new k.Checks(); async function allNodesAgreeAt(height, timeoutMs = 30_000) { const start = Date.now(); while (Date.now() - start < timeoutMs) { const roots = []; let ok = true; for (const c of k.clients) { const b = await k.rpc(c, 'eth_getBlockByNumber', ['0x' + height.toString(16), false]).catch(() => null); if (!b) { ok = false; break; } roots.push(b.stateRoot); } if (ok && roots.every((r) => r === roots[0])) return { agree: true, root: roots[0], roots }; await k.sleep(500); } // one last read for the report const roots = []; for (const c of k.clients) { const b = await k.rpc(c, 'eth_getBlockByNumber', ['0x' + height.toString(16), false]).catch(() => null); roots.push(b?.stateRoot ?? null); } return { agree: roots.every((r) => r === roots[0] && r !== null), root: roots[0], roots }; } async function receiptCount(hashes) { let n = 0; for (const h of hashes) if (await k.rpc(k.node1, 'eth_getTransactionReceipt', [h])) n++; return n; } async function main() { await k.waitTip(2); await k.fund([k.A], '200'); // ---- Round 1: ordering + duplicates across parallel blocks ---- let n = await k.nonceOf(k.A); const txs = []; for (let i = 0; i < 4; i++) txs.push(await k.signTx(k.A, { nonce: n + i, to: k.B.address, value: BigInt(100 + i) })); const hashes = txs.map(keccak256); // Parallel blocks, each a contiguous run from nonce n, with heavy duplication: const reports = k.inject([ [txs[0], txs[1], txs[2], txs[3]], [txs[0], txs[1]], [txs[0]], ]); const allAccepted = reports.every((r) => r.accepted === true); checks.check(allAccepted, `round1: 3 parallel blocks accepted (${reports.map((r) => r.accepted).join(',')})`); // Wait for the honest miner to merge them and execute the segment. await k.sleep(5000); const finalNonce = await k.nonceOf(k.A); checks.check(finalNonce === n + 4, `round1: account nonce advanced to n+4 (n=${n}, got ${finalNonce})`); let perNonceOk = true; for (let i = 0; i < 4; i++) { const c = await receiptCount([hashes[i]]); if (c !== 1) { perNonceOk = false; console.error(` nonce ${n + i}: ${c} receipts`); } } checks.check(perNonceOk, 'round1: each nonce executed exactly once (one receipt per unique tx)'); const tip1 = Number(await k.rpc(k.node1, 'eth_blockNumber')); const agree1 = await allNodesAgreeAt(tip1); checks.check(agree1.agree, `round1: state roots identical on all nodes at block ${tip1} (${agree1.root})`); results.rounds.push({ round: 1, allAccepted, finalNonce, expectedNonce: n + 4, perNonceOk, tip: tip1, rootsAgree: agree1.agree, root: agree1.root }); // ---- Round 2: conflicting same-nonce pair in two parallel blocks ---- const m = await k.nonceOf(k.A); const u0 = await k.signTx(k.A, { nonce: m, to: k.B.address, value: 1_000n }); const u0b = await k.signTx(k.A, { nonce: m, to: k.C.address, value: 9_999n }); // same nonce, different content -> different hash const h0 = keccak256(u0), h0b = keccak256(u0b); checks.check(h0 !== h0b, 'round2: conflicting pair has distinct hashes'); const rep2 = k.inject([[u0], [u0b]]); checks.check(rep2.every((r) => r.accepted), `round2: both parallel blocks accepted (${rep2.map((r) => r.accepted).join(',')})`); await k.sleep(5000); const cnt = await receiptCount([h0, h0b]); checks.check(cnt === 1, `round2: exactly one of the conflicting pair executed (${cnt} receipts)`); const nonce2 = await k.nonceOf(k.A); checks.check(nonce2 === m + 1, `round2: nonce advanced by exactly one (m=${m}, got ${nonce2})`); const tip2 = Number(await k.rpc(k.node1, 'eth_blockNumber')); const agree2 = await allNodesAgreeAt(tip2); checks.check(agree2.agree, `round2: state roots identical on all nodes at block ${tip2} (${agree2.root})`); results.rounds.push({ round: 2, bothAccepted: rep2.every((r) => r.accepted), executedCount: cnt, nonce: nonce2, expected: m + 1, rootsAgree: agree2.agree, root: agree2.root, which: (await k.rpc(k.node1, 'eth_getTransactionReceipt', [h0])) ? 'u0' : 'u0b' }); const s = checks.summary('scenario 2'); results.summary = s; const { writeFileSync } = await import('node:fs'); writeFileSync(new URL('./results/scenario2.json', import.meta.url), JSON.stringify(results, null, 2)); process.exit(s.ok ? 0 : 1); } main().catch((e) => { console.error(e); process.exit(2); });