diff --git a/tools/reference-apps/oracle/lib.mjs b/tools/reference-apps/oracle/lib.mjs index 27e0713c6..096981ff0 100644 --- a/tools/reference-apps/oracle/lib.mjs +++ b/tools/reference-apps/oracle/lib.mjs @@ -284,6 +284,11 @@ export function recordWrite(entry) { writeFileSync(p, JSON.stringify(d, null, 1) + '\n'); } // The public RPC is a pool of nodes; a read right after a receipt can land on one that lags. Poll until it agrees. +// Never send while one of our transactions is still pending, then read the pre-check again: a stale read on a +// lagging node must not turn into a second, identical submission (seen once on 8 October 2026, nonces 8 and 9). +async function quietMempool(pub, address) { + await untilVisible(async () => (await pub.getTransactionCount({ address, blockTag: 'pending' })) === (await pub.getTransactionCount({ address, blockTag: 'latest' })), 'an empty mempool for the deployer'); +} async function untilVisible(check, what) { for (let i = 0; i < 30; i++) { if (await check()) return; await new Promise(r => setTimeout(r, 1000)); } throw new Error(what + ' is not visible on the RPC 30 s after its receipt'); @@ -293,6 +298,8 @@ export async function ensureCertificate(pub, wallet, d, index, checkpoint, bitma const verifierAddr = await pub.readContract({ address: d.oracle.address, abi: artifact('IgneumStateOracle').abi, functionName: 'verifier' }); const same = async () => (await pub.readContract({ address: verifierAddr, abi: stubAbi, functionName: 'finalCheckpoint', args: [index] })).toLowerCase() === checkpoint.toLowerCase(); if (await same()) return { verifier: verifierAddr, already: true }; + await quietMempool(pub, wallet.account.address); + if (await same()) return { verifier: verifierAddr, already: true }; const hash = await wallet.writeContract({ address: verifierAddr, abi: stubAbi, functionName: 'submitCertificate', args: [index, checkpoint, bitmap, signature] }); const r = await pub.waitForTransactionReceipt({ hash, timeout: 180_000 }); if (r.status !== 'success') throw new Error('submitCertificate reverted in ' + hash); @@ -303,10 +310,10 @@ export async function ensureCertificate(pub, wallet, d, index, checkpoint, bitma } export async function ensureStateRoot(pub, wallet, d, v) { const abi = artifact('IgneumStateOracle').abi; - try { - const [root] = await pub.readContract({ address: d.oracle.address, abi, functionName: 'stateRoot', args: [v.number] }); - if (root.toLowerCase() === hex0x(v.postRoot).toLowerCase()) return { already: true }; - } catch {} + const stored = async () => { try { const [root] = await pub.readContract({ address: d.oracle.address, abi, functionName: 'stateRoot', args: [v.number] }); return root.toLowerCase() === hex0x(v.postRoot).toLowerCase(); } catch { return false; } }; + if (await stored()) return { already: true }; + await quietMempool(pub, wallet.account.address); + if (await stored()) return { already: true }; const args = proofArgs(v); const gas = await pub.estimateContractGas({ address: d.oracle.address, abi, functionName: 'submitStateRoot', args, account: wallet.account }); const hash = await wallet.writeContract({ address: d.oracle.address, abi, functionName: 'submitStateRoot', args, gas: gas + gas / 5n });