Reference apps: the oracle's write helper waits for an empty deployer mempool and re-reads before sending (a duplicate submit landed once)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-08 10:52:41 +00:00
parent 09d451a0ec
commit 1c0b4874fa

View file

@ -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 });