Test registry: this lane's run fields inserted into the approved registry textually (master's bytes untouched, rule 26); the read service serialises coinbase amounts at the 2.0 devnet's 16-byte wire width (18 decimals), so /receipt verifies on igneum-devnet-4 (checkpoint 241, 719 headers); the devnet-4 fixtures

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-08 18:12:30 +00:00
parent fe65887c7c
commit 2ce3e68a3c
3 changed files with 12 additions and 7 deletions

File diff suppressed because one or more lines are too long

View file

@ -27,6 +27,8 @@ const WRPC = process.env.WRPC || 'ws://127.0.0.1:28880';
const CHECKPOINT_URL = process.env.CHECKPOINT_URL || 'https://igneum.network/api/checkpoint?source=dn4'; const CHECKPOINT_URL = process.env.CHECKPOINT_URL || 'https://igneum.network/api/checkpoint?source=dn4';
const SOURCE = process.env.CHECKPOINT_SOURCE || 'dn4'; // the observer's table prefix for the devnet the reader follows (dn3, dn4) const SOURCE = process.env.CHECKPOINT_SOURCE || 'dn4'; // the observer's table prefix for the devnet the reader follows (dn3, dn4)
const CHAIN_ID_NAME = process.env.CHAIN_ID_NAME || 'igneum-devnet-4'; const CHAIN_ID_NAME = process.env.CHAIN_ID_NAME || 'igneum-devnet-4';
// the amount's wire width in a transaction's hashing: 8 bytes at 8 decimals (Devnet 3), 16 at 18 decimals (the 2.0 devnet, consensus/core/src/unit.rs amount_wire_len)
const AMOUNT_WIRE_LEN = Number(process.env.AMOUNT_WIRE_LEN || (CHAIN_ID_NAME === 'igneum-devnet-3' ? 8 : 16));
const SEGMENT_BLOCKS = 8; const SEGMENT_BLOCKS = 8;
const strip = s => String(s).replace(/^0x/i, '').toLowerCase(); const strip = s => String(s).replace(/^0x/i, '').toLowerCase();
const log = (...a) => console.log(new Date().toISOString(), ...a); const log = (...a) => console.log(new Date().toISOString(), ...a);
@ -178,7 +180,7 @@ async function balance(q) {
const carrier = await body(chosen.carrier); const carrier = await body(chosen.carrier);
const coinbase = carrier.transactions[0]; const coinbase = carrier.transactions[0];
const evmHashes = (carrier.evmTransactions || []).map(raw => keccak_256(hexToBytes(strip(raw)))); const evmHashes = (carrier.evmTransactions || []).map(raw => keccak_256(hexToBytes(strip(raw))));
const leaves = [coinbaseTxHash(coinbase, blake2b), ...evmHashes]; const leaves = [coinbaseTxHash(coinbase, blake2b, AMOUNT_WIRE_LEN), ...evmHashes];
const root = bytesToHex(merkleRoot(leaves, blake2b)); const root = bytesToHex(merkleRoot(leaves, blake2b));
if (root !== strip(carrier.header.hashMerkleRoot)) throw httpError(500, `the carrier's body root recomputes to ${root.slice(0, 12)}, the header says ${strip(carrier.header.hashMerkleRoot).slice(0, 12)}: the serialisation here is wrong, not the chain`); if (root !== strip(carrier.header.hashMerkleRoot)) throw httpError(500, `the carrier's body root recomputes to ${root.slice(0, 12)}, the header says ${strip(carrier.header.hashMerkleRoot).slice(0, 12)}: the serialisation here is wrong, not the chain`);
const records = segmentRecordsOf(hexToBytes(strip(coinbase.payload))); const records = segmentRecordsOf(hexToBytes(strip(coinbase.payload)));
@ -198,7 +200,7 @@ async function balance(q) {
checkpoint_certificate: certificate ? { ok: true, ...certificate } : undefined, checkpoint_certificate: certificate ? { ok: true, ...certificate } : undefined,
segment: { first: chosen.first, last: chosen.last, carrier: chosen.carrier, carrier_chain_block: chosen.carrierNumber, aggregator_key_hash: chosen.keyHash, paid_wei: chosen.paidWei, statement: chosen.statement }, segment: { first: chosen.first, last: chosen.last, carrier: chosen.carrier, carrier_chain_block: chosen.carrierNumber, aggregator_key_hash: chosen.keyHash, paid_wei: chosen.paidWei, statement: chosen.statement },
headers, headers,
carrier: { coinbase, evm_tx_hashes: evmHashes.map(bytesToHex), leaf_index: 0, merkle_siblings: siblings(leaves, 0), amount_wire_len: 8 }, carrier: { coinbase, evm_tx_hashes: evmHashes.map(bytesToHex), leaf_index: 0, merkle_siblings: siblings(leaves, 0), amount_wire_len: AMOUNT_WIRE_LEN },
segment_record_hex: bytesToHex(rec.bytes), segment_record_hex: bytesToHex(rec.bytes),
account: { ...account, evm_chain_id: Number(chainId) }, account: { ...account, evm_chain_id: Number(chainId) },
}; };
@ -247,7 +249,7 @@ async function receipt(q) {
const raws = b.evmTransactions || []; const raws = b.evmTransactions || [];
const idx = raws.findIndex(raw => bytesToHex(keccak_256(hexToBytes(strip(raw)))) === strip(tx)); const idx = raws.findIndex(raw => bytesToHex(keccak_256(hexToBytes(strip(raw)))) === strip(tx));
if (idx < 0) throw httpError(500, 'the including block\'s body does not carry the transaction'); if (idx < 0) throw httpError(500, 'the including block\'s body does not carry the transaction');
const leaves = [coinbaseTxHash(b.transactions[0], blake2b), ...raws.map(raw => keccak_256(hexToBytes(strip(raw))))]; const leaves = [coinbaseTxHash(b.transactions[0], blake2b, AMOUNT_WIRE_LEN), ...raws.map(raw => keccak_256(hexToBytes(strip(raw))))];
const root = bytesToHex(merkleRoot(leaves, blake2b)); const root = bytesToHex(merkleRoot(leaves, blake2b));
if (root !== strip(b.header.hashMerkleRoot)) throw httpError(500, `the including block's body root recomputes to ${root.slice(0, 12)}, the header says ${strip(b.header.hashMerkleRoot).slice(0, 12)}`); if (root !== strip(b.header.hashMerkleRoot)) throw httpError(500, `the including block's body root recomputes to ${root.slice(0, 12)}, the header says ${strip(b.header.hashMerkleRoot).slice(0, 12)}`);
const headers = await headerPath(including, chainNumber, cpNumber, cpHash); const headers = await headerPath(including, chainNumber, cpNumber, cpHash);
@ -267,7 +269,7 @@ async function receipt(q) {
const carrier = await body(c.carrier); const carrier = await body(c.carrier);
const coinbase = carrier.transactions[0]; const coinbase = carrier.transactions[0];
const evmHashes = (carrier.evmTransactions || []).map(raw => keccak_256(hexToBytes(strip(raw)))); const evmHashes = (carrier.evmTransactions || []).map(raw => keccak_256(hexToBytes(strip(raw))));
const leaves = [coinbaseTxHash(coinbase, blake2b), ...evmHashes]; const leaves = [coinbaseTxHash(coinbase, blake2b, AMOUNT_WIRE_LEN), ...evmHashes];
const rec = segmentRecordsOf(hexToBytes(strip(coinbase.payload))).find(r => Number(r.first) === first && Number(r.last) === last); const rec = segmentRecordsOf(hexToBytes(strip(coinbase.payload))).find(r => Number(r.first) === first && Number(r.last) === last);
if (!rec) throw new Error('the carrier\'s coinbase holds no record for the segment'); if (!rec) throw new Error('the carrier\'s coinbase holds no record for the segment');
const plan = await exec('igneum_getShardPlan', ['0x' + chainNumber.toString(16)]); const plan = await exec('igneum_getShardPlan', ['0x' + chainNumber.toString(16)]);
@ -278,7 +280,7 @@ async function receipt(q) {
if (shardIndex < 0) throw new Error('the transaction index falls in no shard of the plan'); if (shardIndex < 0) throw new Error('the transaction index falls in no shard of the plan');
segment = { segment = {
first, last, record_hex: bytesToHex(rec.bytes), headers: await headerPath(strip(c.carrier), carrierNumber, cpNumber, cpHash), first, last, record_hex: bytesToHex(rec.bytes), headers: await headerPath(strip(c.carrier), carrierNumber, cpNumber, cpHash),
carrier: { coinbase, evm_tx_hashes: evmHashes.map(bytesToHex), leaf_index: 0, merkle_siblings: siblings(leaves, 0), amount_wire_len: 8 }, carrier: { coinbase, evm_tx_hashes: evmHashes.map(bytesToHex), leaf_index: 0, merkle_siblings: siblings(leaves, 0), amount_wire_len: AMOUNT_WIRE_LEN },
shard_receipts_roots: plan.shards.map(sh => strip(sh.receiptsRoot)), shard_index: shardIndex, receipts: mine, receipt_position: txIndex - start, shard_receipts_roots: plan.shards.map(sh => strip(sh.receiptsRoot)), shard_index: shardIndex, receipts: mine, receipt_position: txIndex - start,
aggregator_key_hash: c.keyHash, paid_wei: sr.paid.wei, aggregator_key_hash: c.keyHash, paid_wei: sr.paid.wei,
}; };

View file

@ -16,8 +16,10 @@ for (const [id, fields] of Object.entries(updates)) {
const m = new RegExp(`^([ \\t]*)"id":\\s*"${esc(id)}",?[ \\t]*\\n`, 'm').exec(text); const m = new RegExp(`^([ \\t]*)"id":\\s*"${esc(id)}",?[ \\t]*\\n`, 'm').exec(text);
if (!m) { console.error('case not in the registry: ' + id); process.exit(1); } if (!m) { console.error('case not in the registry: ' + id); process.exit(1); }
const indent = m[1]; let at = m.index + m[0].length; const indent = m[1]; let at = m.index + m[0].length;
// the object's end: the first line at this indent that closes it (a "}" or "}," at the same indent) // the case object's end: its opening line is the one before the id line; the closing brace sits at that line's indent
const endRe = new RegExp(`^${indent}\\},?[ \\t]*\\n`, 'mg'); endRe.lastIndex = at; const openLine = text.lastIndexOf('\n', m.index - 1); const opener = text.slice(text.lastIndexOf('\n', openLine - 1) + 1, openLine);
const parentIndent = (opener.match(/^[ \t]*/) || [''])[0];
const endRe = new RegExp(`^${parentIndent}\\},?[ \\t]*\\n`, 'mg'); endRe.lastIndex = at;
const end = endRe.exec(text); if (!end) { console.error('no object end for ' + id); process.exit(1); } const end = endRe.exec(text); if (!end) { console.error('no object end for ' + id); process.exit(1); }
let body = text.slice(at, end.index); let body = text.slice(at, end.index);
const all = { ...fields, updated: now }; const all = { ...fields, updated: now };