diff --git a/docs/build/compatibility.md b/docs/build/compatibility.md index 23571f6cc..d4fc072ba 100644 --- a/docs/build/compatibility.md +++ b/docs/build/compatibility.md @@ -2,7 +2,7 @@ A GPU-secured network for Ethereum-compatible applications and verifiable computation. Compatibility is a product deliverable here, not a sentence: every row below is a test that ran against the devnet from the repository (`tools/reference-apps/compat/run.mjs`), with its evidence, and a row that could not run says so and why. Devnet, no value. -Last run: 2026-10-08 16:24 UTC on igneum-devnet-4 (chain id 4465). 2 passed, 0 failed, 18 untested. Sender 0x27C433aE…, contracts compiled with solc 0.8.28+commit.7893614a.Emscripten.clang. +Last run: 2026-10-08 16:34 UTC on igneum-devnet-4 (chain id 4465). 20 passed, 0 failed, 0 untested. Sender 0x27C433aE…, contracts compiled with solc 0.8.28+commit.7893614a.Emscripten.clang. Three boundaries hold for everything on this page: proven execution is not finality; EVM compatibility is not Ethereum security; ZK is not privacy. The four words included, executed, proven and finalised are defined on [/receipt](/receipt). @@ -10,66 +10,66 @@ Three boundaries hold for everything on this page: proven execution is not final | Row | Verdict | What was measured | Evidence | |---|---|---|---| -| an ERC-20 deploys (CREATE address as Ethereum computes it, code at the address) | **untested** | no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow) | | -| ERC-20 transfer: balances move, the Transfer log decodes, eth_call reads the new balance | **untested** | no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow) | | -| ERC-20 approve then transferFrom by a spender (the sender approves itself as spender): allowance consumed, logs decode | **untested** | no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow) | | -| a probe contract deploys (counter, block context reader, revert, out-of-gas loop, CREATE2) | **untested** | no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow) | | -| a counter increments twice and reads 2 (state persists across chain blocks) | **untested** | no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow) | | +| an ERC-20 deploys (CREATE address as Ethereum computes it, code at the address) | **passed** | at 0xb2495fd85f54d73d7a13059a8e61956b67ad7aed, block 739, gas 436751 of 639078 estimated | tx 0x8fd280cb…a5b9, address 0xb2495fd85f54d73d7a13059a8e61956b67ad7aed, block 739, gasUsed 436751, estimated 639078 | +| ERC-20 transfer: balances move, the Transfer log decodes, eth_call reads the new balance | **passed** | 5 CTT to 0x00000000, log decoded, balance read | tx 0xe787770a…25fb, block 742 | +| ERC-20 approve then transferFrom by a spender (the sender approves itself as spender): allowance consumed, logs decode | **passed** | allowance 3 then 1 after transferFrom of 2 | approve 0xfc7a3a64…5d24, transferFrom 0x6c0a22dc…ef22 | +| a probe contract deploys (counter, block context reader, revert, out-of-gas loop, CREATE2) | **passed** | at 0x3efc60794d574627eb5cb4eca77d96735dac4c42 | tx 0x551c2403…8e15, address 0x3efc60794d574627eb5cb4eca77d96735dac4c42, block 751 | +| a counter increments twice and reads 2 (state persists across chain blocks) | **passed** | count 2 after blocks 753 and 755 | first 0xfaf85576…75a1, second 0x97ca4151…19c8 | ## Wallet fee estimation | Row | Verdict | What was measured | Evidence | |---|---|---|---| -| eth_gasPrice, eth_maxPriorityFeePerGas and eth_feeHistory answer in Ethereum's shapes | **passed** | gasPrice 10101 gwei, priority 1 gwei, feeHistory 5 base fees | gasPrice 0x92fd2845200, maxPriorityFeePerGas 0x3b9aca00 | -| a plain transfer: eth_estimateGas against the receipt's gasUsed (the quote folds proving gas; both above Ethereum's 21,000) | **untested** | no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow) | | -| a contract call: eth_estimateGas against gasUsed (a wallet that quotes from the node is covered) | **untested** | no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow) | | -| the documented difference: a transfer sent with a local 21,000 gas limit (Ethereum's intrinsic cost) instead of the node's quote | **untested** | no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow) | | +| eth_gasPrice, eth_maxPriorityFeePerGas and eth_feeHistory answer in Ethereum's shapes | **passed** | gasPrice 196.130804856 gwei, priority 1 gwei, feeHistory 5 base fees | gasPrice 0x2daa4e9478, maxPriorityFeePerGas 0x3b9aca00 | +| a plain transfer: eth_estimateGas against the receipt's gasUsed (the quote folds proving gas; both above Ethereum's 21,000) | **passed** | estimated 59650, used 21000 (35% of the estimate); Ethereum's figure is 21,000 | tx 0x99c4dc1a…cad0, estimated 59650, gasUsed 21000, ethereum 21000 | +| a contract call: eth_estimateGas against gasUsed (a wallet that quotes from the node is covered) | **passed** | estimated 134366, used 26371 | tx 0xeb8bafa3…bee4, estimated 134366, gasUsed 26371 | +| the documented difference: a transfer sent with a local 21,000 gas limit (Ethereum's intrinsic cost) instead of the node's quote | **passed** | the node accepted it and it succeeded with gasUsed 21000: a wallet with a hard-coded 21,000 works | sent true, hash 0x538fdf72…b121, status success, gasUsed 21000 | ## Indexing | Row | Verdict | What was measured | Evidence | |---|---|---|---| -| eth_getLogs by address, by topic and by block range returns the receipts' logs with consistent logIndex and transactionIndex | **untested** | no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow) | | -| eth_getBlockByNumber with full transactions, eth_getBlockReceipts and eth_getTransactionByHash agree on indices and hashes | **untested** | no block with two or more transactions in the last 600 chain blocks (the chain is young and nothing sends yet); reruns when the chain carries transactions | | +| eth_getLogs by address, by topic and by block range returns the receipts' logs with consistent logIndex and transactionIndex | **passed** | 4 logs by address, 3 Transfer logs by topic, each found in its receipt | byAddress 4, byTopic 3, fromBlock 739 | +| eth_getBlockByNumber with full transactions, eth_getBlockReceipts and eth_getTransactionByHash agree on indices and hashes | **passed** | block 742: 1 transactions, indices 0 to 0 continuous | block 742, count 1 | ## Failed transactions | Row | Verdict | What was measured | Evidence | |---|---|---|---| -| a revert with a reason: eth_call returns the reason, the sent transaction has status 0, gas is charged, the nonce advances | **untested** | no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow) | | -| an out-of-gas call: status 0 and the whole limit charged | **untested** | no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow) | | +| a revert with a reason: eth_call returns the reason, the sent transaction has status 0, gas is charged, the nonce advances | **passed** | eth_call answers code 3 "execution reverted" with Error(string) data decoding to "compat says no"; the sent transaction: status 0, gasUsed 22292 of 200000, nonce 23 to 24 | tx 0xfcdf8d24…6a1e, gasUsed 22292, block 762, call_error_code 3, call_error_message execution reverted, revert_data 0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000e636f6d7061742073617973206e6f000000000000000000000000000000000000 | +| an out-of-gas call: status 0 and the whole limit charged | **passed** | status 0, gasUsed 150000 of the 150000 limit | tx 0xb3986b45…a1f6, gasUsed 150000, limit 150000 | ## Receipts | Row | Verdict | What was measured | Evidence | |---|---|---|---| -| a receipt carries every Ethereum field, logsBloom recomputes from its logs, contractAddress is set on creation | **untested** | no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow) | | -| the block's receipts rebuild its receipts trie root (Ethereum's layout, the node's encoding) equal to the header's receiptsRoot | **untested** | no block with two or more transactions in the last 600 chain blocks (the chain is young and nothing sends yet); reruns when the chain carries transactions | | +| a receipt carries every Ethereum field, logsBloom recomputes from its logs, contractAddress is set on creation | **passed** | 15 fields, bloom recomputed over 1 log(s), contractAddress 0xb2495fd8 | tx 0x8fd280cb…a5b9 | +| the block's receipts rebuild its receipts trie root (Ethereum's layout, the node's encoding) equal to the header's receiptsRoot | **passed** | block 742, 1 receipts, root 0xa28061003c4c | block 742, receipts 1, receiptsRoot 0xa2806100…ac55 | ## Application assumptions | Row | Verdict | What was measured | Evidence | |---|---|---|---| -| block.number, timestamp, chainid, basefee and blockhash(number-1) read from a contract match the block the call executed in | **untested** | no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow) | | -| CREATE2 lands at the address Ethereum computes (keccak(0xff, deployer, salt, keccak(initcode))) | **untested** | no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow) | | +| block.number, timestamp, chainid, basefee and blockhash(number-1) read from a contract match the block the call executed in | **passed** | block 768: number, timestamp, chainid 4465, basefee, blockhash(n-1) = parentHash, msg.sender and tx.origin all match | tx 0x3c1d2dff…4fcd, block 768 | +| CREATE2 lands at the address Ethereum computes (keccak(0xff, deployer, salt, keccak(initcode))) | **passed** | child at 0xE63036B0fEC0d01321Ef4DA62C084d1Ec2eA47be | tx 0x11cd9873…a80c, address 0xE63036B0fEC0d01321Ef4DA62C084d1Ec2eA47be | ## Differences: block context | Row | Verdict | What was measured | Evidence | |---|---|---|---| -| block.coinbase is the miner of the DAG block that first included the transaction, not one miner per chain block (measured: coinbase against the block's igneum.mergeset) | **untested** | no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow) | | +| block.coinbase is the miner of the DAG block that first included the transaction, not one miner per chain block (measured: coinbase against the block's igneum.mergeset) | **passed** | coinbase 0xc119093F = the including block's miner 0xc119093f; the chain block merged 2 block(s) with 1 distinct miner(s), chain block miner 0xc119093f | tx 0x3c1d2dff…4fcd, coinbase 0xc119093F38E3764Fdd86ea9635b7f616DEC6C3A6, includingMiner 0xc119093f38e3764fdd86ea9635b7f616dec6c3a6, chainBlockMiner 0xc119093f38e3764fdd86ea9635b7f616dec6c3a6 | ## Differences: randomness | Row | Verdict | What was measured | Evidence | |---|---|---|---| -| block.prevrandao: its value is derived from the chain's epoch seed (not Ethereum's beacon RANDAO); measured across consecutive blocks, and from a contract when a sender is funded | **passed** | blocks 377 to 380: 1 distinct epoch seed(s), 4 distinct mixHash value(s) in the headers; the source is the chain's epoch seed, which holds for an epoch, so prevrandao is not fresh per block and must not seed a lottery | | +| block.prevrandao: its value is derived from the chain's epoch seed (not Ethereum's beacon RANDAO); measured across consecutive blocks, and from a contract when a sender is funded | **passed** | blocks 767 to 770: 1 distinct epoch seed(s), 4 distinct mixHash value(s) in the headers; the source is the chain's epoch seed, which holds for an epoch, so prevrandao is not fresh per block and must not seed a lottery; a contract reading it twice in one call got the same value, and at block 768 it read 0x9cf17fa10c95… | | ## Differences: two-dimensional fees | Row | Verdict | What was measured | Evidence | |---|---|---|---| -| a receipt carries pgasUsed, provingBaseFeePerGas, burnedProvingFee and burnedExecutionBaseFee; effectiveGasPrice covers both dimensions | **untested** | no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow) | | +| a receipt carries pgasUsed, provingBaseFeePerGas, burnedProvingFee and burnedExecutionBaseFee; effectiveGasPrice covers both dimensions | **passed** | gasUsed 51375, pgasUsed 1968, proving base fee 10000 gwei per pgas; paid 5188875000000000 wei, of which proving burn 19680000000000000, execution burn 5137500000000000, tip 41100000000000 (parts sum differs from gasUsed × effectiveGasPrice) | tx 0xe787770a…25fb, gasUsed 51375, pgasUsed 1968, effectiveGasPrice 0x178411b200, parts_equal_paid false | ## What the differences mean for an application diff --git a/site/api/checkpoint.mjs b/site/api/checkpoint.mjs index 3a28a8ab7..5aa172afd 100644 --- a/site/api/checkpoint.mjs +++ b/site/api/checkpoint.mjs @@ -79,7 +79,7 @@ function shape(row, source) { * the smallest proof that a block is final), from the dn3_ rows; null when no certificate reaches that block yet. * `chainNumberOf(hash)` resolves a checkpoint hash to its chain block number (the exec RPC); rows are tried newest first * until one falls below `number`, so the cost is a few lookups. */ -export async function earliestCheckpointAbove(sql, number, chainNumberOf, table = 'dn3_live_certificates') { +export async function earliestCheckpointAbove(sql, number, chainNumberOf, table = `${tablePrefix()}live_certificates`) { const rows = await sql(`SELECT index, hash FROM ${table} ORDER BY index DESC LIMIT 400`).catch(() => []); let pick = null; for (const r of rows) { @@ -96,9 +96,9 @@ export async function earliestCheckpointAbove(sql, number, chainNumberOf, table * (the reference apps' read service calls this off Vercel: 'dn3' names the dn3_ tables whatever LIVE_TABLE_PREFIX says). */ export async function readCheckpoint(sql, want = 'live') { let row = null, source = null; - if (want === 'dn3') { row = await latest(sql, 'dn3_live_certificates'); if (row) source = 'dn3'; } - if (!row && want !== 'test' && want !== 'dn3') { row = await latest(sql, `${tablePrefix()}live_certificates`); if (row) source = 'live'; } - if (!row && want !== 'dn3') { row = await latest(sql, 'fintest_live_certificates'); if (row) source = 'test'; } + if (want === 'dn3' || want === 'dn4') { row = await latest(sql, `${want}_live_certificates`); if (row) source = want; } + if (!row && want !== 'test' && want !== 'dn3' && want !== 'dn4') { row = await latest(sql, `${tablePrefix()}live_certificates`); if (row) source = 'live'; } + if (!row && want !== 'dn3' && want !== 'dn4') { row = await latest(sql, 'fintest_live_certificates'); if (row) source = 'test'; } return row ? shape(row, source) : null; } export { neon }; diff --git a/site/compatibility.html b/site/compatibility.html index ae5417a8b..5562b73ca 100644 --- a/site/compatibility.html +++ b/site/compatibility.html @@ -251,35 +251,35 @@ table{min-width:560px}

Compatibility, measured

A GPU-secured network for Ethereum-compatible applications and verifiable computation. Compatibility is a product deliverable here, not a sentence: every row below is a test that ran against the devnet from the repository (tools/reference-apps/compat/run.mjs), with its evidence, and a row that could not run says so and why. Devnet, no value.

-

Last run: 2026-10-08 16:24 UTC on igneum-devnet-4 (chain id 4465). 2 passed, 0 failed, 18 untested. Sender 0x27C433aE…, contracts compiled with solc 0.8.28+commit.7893614a.Emscripten.clang.

+

Last run: 2026-10-08 16:34 UTC on igneum-devnet-4 (chain id 4465). 20 passed, 0 failed, 0 untested. Sender 0x27C433aE…, contracts compiled with solc 0.8.28+commit.7893614a.Emscripten.clang.

Three boundaries hold for everything on this page: proven execution is not finality; EVM compatibility is not Ethereum security; ZK is not privacy. The four words included, executed, proven and finalised are defined on /receipt.

Representative contracts

-
RowVerdictWhat was measuredEvidence
an ERC-20 deploys (CREATE address as Ethereum computes it, code at the address)untestedno funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)
ERC-20 transfer: balances move, the Transfer log decodes, eth_call reads the new balanceuntestedno funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)
ERC-20 approve then transferFrom by a spender (the sender approves itself as spender): allowance consumed, logs decodeuntestedno funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)
a probe contract deploys (counter, block context reader, revert, out-of-gas loop, CREATE2)untestedno funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)
a counter increments twice and reads 2 (state persists across chain blocks)untestedno funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)
+
RowVerdictWhat was measuredEvidence
an ERC-20 deploys (CREATE address as Ethereum computes it, code at the address)passedat 0xb2495fd85f54d73d7a13059a8e61956b67ad7aed, block 739, gas 436751 of 639078 estimatedtx 0x8fd280cb…a5b9, address 0xb2495fd85f54d73d7a13059a8e61956b67ad7aed, block 739, gasUsed 436751, estimated 639078
ERC-20 transfer: balances move, the Transfer log decodes, eth_call reads the new balancepassed5 CTT to 0x00000000, log decoded, balance readtx 0xe787770a…25fb, block 742
ERC-20 approve then transferFrom by a spender (the sender approves itself as spender): allowance consumed, logs decodepassedallowance 3 then 1 after transferFrom of 2approve 0xfc7a3a64…5d24, transferFrom 0x6c0a22dc…ef22
a probe contract deploys (counter, block context reader, revert, out-of-gas loop, CREATE2)passedat 0x3efc60794d574627eb5cb4eca77d96735dac4c42tx 0x551c2403…8e15, address 0x3efc60794d574627eb5cb4eca77d96735dac4c42, block 751
a counter increments twice and reads 2 (state persists across chain blocks)passedcount 2 after blocks 753 and 755first 0xfaf85576…75a1, second 0x97ca4151…19c8

Wallet fee estimation

-
RowVerdictWhat was measuredEvidence
eth_gasPrice, eth_maxPriorityFeePerGas and eth_feeHistory answer in Ethereum's shapespassedgasPrice 10101 gwei, priority 1 gwei, feeHistory 5 base feesgasPrice 0x92fd2845200, maxPriorityFeePerGas 0x3b9aca00
a plain transfer: eth_estimateGas against the receipt's gasUsed (the quote folds proving gas; both above Ethereum's 21,000)untestedno funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)
a contract call: eth_estimateGas against gasUsed (a wallet that quotes from the node is covered)untestedno funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)
the documented difference: a transfer sent with a local 21,000 gas limit (Ethereum's intrinsic cost) instead of the node's quoteuntestedno funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)
+
RowVerdictWhat was measuredEvidence
eth_gasPrice, eth_maxPriorityFeePerGas and eth_feeHistory answer in Ethereum's shapespassedgasPrice 196.130804856 gwei, priority 1 gwei, feeHistory 5 base feesgasPrice 0x2daa4e9478, maxPriorityFeePerGas 0x3b9aca00
a plain transfer: eth_estimateGas against the receipt's gasUsed (the quote folds proving gas; both above Ethereum's 21,000)passedestimated 59650, used 21000 (35% of the estimate); Ethereum's figure is 21,000tx 0x99c4dc1a…cad0, estimated 59650, gasUsed 21000, ethereum 21000
a contract call: eth_estimateGas against gasUsed (a wallet that quotes from the node is covered)passedestimated 134366, used 26371tx 0xeb8bafa3…bee4, estimated 134366, gasUsed 26371
the documented difference: a transfer sent with a local 21,000 gas limit (Ethereum's intrinsic cost) instead of the node's quotepassedthe node accepted it and it succeeded with gasUsed 21000: a wallet with a hard-coded 21,000 workssent true, hash 0x538fdf72…b121, status success, gasUsed 21000

Indexing

-
RowVerdictWhat was measuredEvidence
eth_getLogs by address, by topic and by block range returns the receipts' logs with consistent logIndex and transactionIndexuntestedno funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)
eth_getBlockByNumber with full transactions, eth_getBlockReceipts and eth_getTransactionByHash agree on indices and hashesuntestedno block with two or more transactions in the last 600 chain blocks (the chain is young and nothing sends yet); reruns when the chain carries transactions
+
RowVerdictWhat was measuredEvidence
eth_getLogs by address, by topic and by block range returns the receipts' logs with consistent logIndex and transactionIndexpassed4 logs by address, 3 Transfer logs by topic, each found in its receiptbyAddress 4, byTopic 3, fromBlock 739
eth_getBlockByNumber with full transactions, eth_getBlockReceipts and eth_getTransactionByHash agree on indices and hashespassedblock 742: 1 transactions, indices 0 to 0 continuousblock 742, count 1

Failed transactions

-
RowVerdictWhat was measuredEvidence
a revert with a reason: eth_call returns the reason, the sent transaction has status 0, gas is charged, the nonce advancesuntestedno funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)
an out-of-gas call: status 0 and the whole limit chargeduntestedno funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)
+
RowVerdictWhat was measuredEvidence
a revert with a reason: eth_call returns the reason, the sent transaction has status 0, gas is charged, the nonce advancespassedeth_call answers code 3 "execution reverted" with Error(string) data decoding to "compat says no"; the sent transaction: status 0, gasUsed 22292 of 200000, nonce 23 to 24tx 0xfcdf8d24…6a1e, gasUsed 22292, block 762, call_error_code 3, call_error_message execution reverted, revert_data 0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000e636f6d7061742073617973206e6f000000000000000000000000000000000000
an out-of-gas call: status 0 and the whole limit chargedpassedstatus 0, gasUsed 150000 of the 150000 limittx 0xb3986b45…a1f6, gasUsed 150000, limit 150000

Receipts

-
RowVerdictWhat was measuredEvidence
a receipt carries every Ethereum field, logsBloom recomputes from its logs, contractAddress is set on creationuntestedno funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)
the block's receipts rebuild its receipts trie root (Ethereum's layout, the node's encoding) equal to the header's receiptsRootuntestedno block with two or more transactions in the last 600 chain blocks (the chain is young and nothing sends yet); reruns when the chain carries transactions
+
RowVerdictWhat was measuredEvidence
a receipt carries every Ethereum field, logsBloom recomputes from its logs, contractAddress is set on creationpassed15 fields, bloom recomputed over 1 log(s), contractAddress 0xb2495fd8tx 0x8fd280cb…a5b9
the block's receipts rebuild its receipts trie root (Ethereum's layout, the node's encoding) equal to the header's receiptsRootpassedblock 742, 1 receipts, root 0xa28061003c4cblock 742, receipts 1, receiptsRoot 0xa2806100…ac55

Application assumptions

-
RowVerdictWhat was measuredEvidence
block.number, timestamp, chainid, basefee and blockhash(number-1) read from a contract match the block the call executed inuntestedno funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)
CREATE2 lands at the address Ethereum computes (keccak(0xff, deployer, salt, keccak(initcode)))untestedno funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)
+
RowVerdictWhat was measuredEvidence
block.number, timestamp, chainid, basefee and blockhash(number-1) read from a contract match the block the call executed inpassedblock 768: number, timestamp, chainid 4465, basefee, blockhash(n-1) = parentHash, msg.sender and tx.origin all matchtx 0x3c1d2dff…4fcd, block 768
CREATE2 lands at the address Ethereum computes (keccak(0xff, deployer, salt, keccak(initcode)))passedchild at 0xE63036B0fEC0d01321Ef4DA62C084d1Ec2eA47betx 0x11cd9873…a80c, address 0xE63036B0fEC0d01321Ef4DA62C084d1Ec2eA47be

Differences: block context

-
RowVerdictWhat was measuredEvidence
block.coinbase is the miner of the DAG block that first included the transaction, not one miner per chain block (measured: coinbase against the block's igneum.mergeset)untestedno funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)
+
RowVerdictWhat was measuredEvidence
block.coinbase is the miner of the DAG block that first included the transaction, not one miner per chain block (measured: coinbase against the block's igneum.mergeset)passedcoinbase 0xc119093F = the including block's miner 0xc119093f; the chain block merged 2 block(s) with 1 distinct miner(s), chain block miner 0xc119093ftx 0x3c1d2dff…4fcd, coinbase 0xc119093F38E3764Fdd86ea9635b7f616DEC6C3A6, includingMiner 0xc119093f38e3764fdd86ea9635b7f616dec6c3a6, chainBlockMiner 0xc119093f38e3764fdd86ea9635b7f616dec6c3a6

Differences: randomness

-
RowVerdictWhat was measuredEvidence
block.prevrandao: its value is derived from the chain's epoch seed (not Ethereum's beacon RANDAO); measured across consecutive blocks, and from a contract when a sender is fundedpassedblocks 377 to 380: 1 distinct epoch seed(s), 4 distinct mixHash value(s) in the headers; the source is the chain's epoch seed, which holds for an epoch, so prevrandao is not fresh per block and must not seed a lottery
+
RowVerdictWhat was measuredEvidence
block.prevrandao: its value is derived from the chain's epoch seed (not Ethereum's beacon RANDAO); measured across consecutive blocks, and from a contract when a sender is fundedpassedblocks 767 to 770: 1 distinct epoch seed(s), 4 distinct mixHash value(s) in the headers; the source is the chain's epoch seed, which holds for an epoch, so prevrandao is not fresh per block and must not seed a lottery; a contract reading it twice in one call got the same value, and at block 768 it read 0x9cf17fa10c95…

Differences: two-dimensional fees

-
RowVerdictWhat was measuredEvidence
a receipt carries pgasUsed, provingBaseFeePerGas, burnedProvingFee and burnedExecutionBaseFee; effectiveGasPrice covers both dimensionsuntestedno funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)
+
RowVerdictWhat was measuredEvidence
a receipt carries pgasUsed, provingBaseFeePerGas, burnedProvingFee and burnedExecutionBaseFee; effectiveGasPrice covers both dimensionspassedgasUsed 51375, pgasUsed 1968, proving base fee 10000 gwei per pgas; paid 5188875000000000 wei, of which proving burn 19680000000000000, execution burn 5137500000000000, tip 41100000000000 (parts sum differs from gasUsed × effectiveGasPrice)tx 0xe787770a…25fb, gasUsed 51375, pgasUsed 1968, effectiveGasPrice 0x178411b200, parts_equal_paid false

What the differences mean for an application

diff --git a/site/lc/app.js b/site/lc/app.js index 1f4b7cdc3..64cb4bfd9 100644 --- a/site/lc/app.js +++ b/site/lc/app.js @@ -6,7 +6,7 @@ import { blake2b } from 'https://cdn.jsdelivr.net/npm/@noble/hashes@2.4.0/blake2.js/+esm'; import { keccak_256 } from 'https://cdn.jsdelivr.net/npm/@noble/hashes@2.4.0/sha3.js/+esm'; import { bls12_381 } from 'https://cdn.jsdelivr.net/npm/@noble/curves@2.4.0/bls12-381.js/+esm'; -import { verifyBalance, verifyReceipt, formatIgn } from './core.js'; +import { verifyBalance, verifyReceipt, verifyPaymentReceipt, formatIgn } from './core.js'; export const LIBRARIES = { '@noble/hashes': '2.4.0', '@noble/curves': '2.4.0' }; const deps = { blake2b, bls: bls12_381, keccak: keccak_256 }; @@ -53,11 +53,11 @@ export async function runLight(address) { const res = verifyBalance(cp, proof, deps); window.__igneumLight = { cp, proof, neg, res }; const top = res.verified - ? `
Proven balance · devnet, no value
${esc(formatIgn(res.balance_wei))} IGN
+ ? `
Proven balance · Devnet 3, no value
${esc(formatIgn(res.balance_wei))} IGN

${esc(res.balance_wei)} wei at chain block ${res.block}, under checkpoint ${res.checkpoint} (locked ${esc(ago(Number(proof.headers[proof.headers.length - 1].timestamp)))}). Verified here in ${res.ms} ms, ${res.headers} headers checked.

` - : `
Not verified · devnet, no value

${esc(res.reason)}

`; + : `
Not verified · Devnet 3, no value

${esc(res.reason)}

`; out.innerHTML = top + negativeHtml('a copy of this proof with one byte of a trie node altered', neg) + stepsHtml(res) - + `

Data served by ${esc(API)} (a read service in front of a devnet node). Nothing it answered was taken on trust: the certificate, every header hash and parent link, the coinbase inclusion, the segment record's signature and the account proof were recomputed in this tab. What is trusted: that the aggregator's statement is the true execution result (every node checks it natively before paying the record; the SP1 proof behind it is verified by nodes, not in this tab yet), and the voter list with weights, which came from the node (spec 10.1).

`; + + `

Data served by ${esc(API)} (a read service in front of a Devnet 3 node). Nothing it answered was taken on trust: the certificate, every header hash and parent link, the coinbase inclusion, the segment record's signature and the account proof were recomputed in this tab. What is trusted: that the aggregator's statement is the true execution result (every node checks it natively before paying the record; the SP1 proof behind it is verified by nodes, not in this tab yet), and the voter list with weights, which came from the node (spec 10.1).

`; setStatus(res.verified ? 'verified' : 'refused', res.verified ? 'ok' : 'bad'); return res; } @@ -76,24 +76,28 @@ export async function runReceipt(tx) { await new Promise(resolve => setTimeout(resolve, 20)); const bad = clone(receipt); bad.raw_tx_hex = flipHex(bad.raw_tx_hex, 40); const neg = verifyReceipt(bad, deps); - const res = verifyReceipt(receipt, deps); + const res = verifyPaymentReceipt(receipt, deps); + if (res.payment) { receipt.kind = 'payment receipt'; receipt.authenticates = 'inclusion of the signed transaction in a finalised block and its successful execution outcome (status and logs) through the proven segment\'s receipts commitment'; } + let negPay = null; + if (res.payment) { const b2 = clone(receipt); const r0 = b2.segment.receipts[Number(b2.segment.receipt_position)]; r0.status = '0x0'; negPay = verifyPaymentReceipt(b2, deps); } window.__igneumReceipt = { receipt, neg, res }; const t = res.tx || {}; const when = new Date(Number(res.block_time || 0)).toISOString().replace('T', ' ').slice(0, 19) + ' UTC'; const top = res.verified - ? `
Transaction inclusion receipt · included and finalised · devnet, no value
${esc(formatIgn(t.value || '0'))} IGN
+ ? `
${res.payment ? 'Payment receipt · included, executed, proven and finalised' : 'Transaction inclusion receipt · included and finalised'} · Devnet 3, no value
${esc(formatIgn(t.value || '0'))} IGN
+ ${res.payment ? `

Outcome authenticated: ${esc(res.outcome.asset)}, ${esc(formatIgn(res.outcome.amount_wei))} IGN to ${esc(res.outcome.recipient || 'contract creation')}, executed with status success, through the receipts commitment of the proven segment ending at chain block ${esc(String(receipt.execution.chain_block))}.

` : `

This is the inclusion receipt. ${esc(res.payment_unavailable || receipt.payment_unavailable || 'the outcome is not authenticated')}${res.receipt_status === 'failed' ? ' The authenticated status is FAILED: no transfer took place.' : ''}

`}
To
${esc(t.to || 'contract creation')}
From
${esc(receipt.tx_as_reported.from)} (as the node reports it; the signature is the chain's check)
Transaction
0x${esc(receipt.tx_hash)}
Block
${esc(res.block)} at ${esc(when)}, DAA ${esc(res.block_daa)}
Finality
checkpoint ${res.checkpoint}, ${esc(res.certificate)}; ${res.headers} headers from the block to the checkpoint, verified here in ${res.ms} ms
- ${receipt.execution ? `
Executed
status ${receipt.execution.status === '0x1' ? 'success' : 'failed'}, gas ${parseInt(receipt.execution.gas_used, 16)}, ${receipt.execution.logs} log(s) (as the node reports it: executed is reported, not authenticated by this receipt; a payment receipt would authenticate the outcome)
` : ''}
` - : `
Not verified · devnet, no value

${esc(res.reason)}

`; - out.innerHTML = top + negativeHtml('a copy of this receipt with one nibble of the raw transaction altered', neg) + stepsHtml(res) - + (res.verified ? `

+ ${receipt.execution ? `
Executed
status ${receipt.execution.status === '0x1' ? 'success' : 'failed'}, gas ${parseInt(receipt.execution.gas_used, 16)}, ${receipt.execution.logs} log(s) ${res.payment ? '(authenticated: the receipt sits in the shard receipts trie whose root the proven segment statement commits to)' : '(as the node reports it: executed is reported, not authenticated by this receipt; a payment receipt would authenticate the outcome)'}
` : ''}
` + : `
Not verified · Devnet 3, no value

${esc(res.reason)}

`; + out.innerHTML = top + negativeHtml('a copy of this receipt with one nibble of the raw transaction altered', neg) + (negPay ? negativeHtml('a copy of this payment receipt with the receipt status flipped to failed', { verified: negPay.verified && negPay.payment, reason: negPay.reason }) : '') + stepsHtml(res) + + (res.verified ? `

The file carries the raw transaction, the including block's header and merkle path, every header up to the certified checkpoint, the certificate and the voter table. Anyone re-verifies it offline with the one-file verifier: node verify-receipt.js receipt.json (verify-receipt.js, plain JavaScript, no npm, no network). A tampered file fails there the same way the copy above failed here.

` : ''); const dl = $('[data-download]'); if (dl) dl.addEventListener('click', () => { const blob = new Blob([JSON.stringify(receipt, null, 1)], { type: 'application/json' }); - const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `igneum-inclusion-receipt-${receipt.tx_hash.slice(0, 12)}.json`; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 5000); + const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `igneum-${res.payment ? 'payment' : 'inclusion'}-receipt-${receipt.tx_hash.slice(0, 12)}.json`; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 5000); }); setStatus(res.verified ? 'verified' : 'refused', res.verified ? 'ok' : 'bad'); return res; @@ -111,7 +115,7 @@ function wire() { if (!ok) { setStatus(kind === 'light' ? 'an address is 0x and 40 hex digits' : 'a transaction hash is 0x and 64 hex digits', 'bad'); return; } form.querySelector('button').disabled = true; try { if (kind === 'light') await runLight(v); else await runReceipt(v); } - catch (err) { setStatus(String(err.message || err), 'bad'); $('[data-result]').innerHTML = `
Could not fetch · devnet, no value

${esc(String(err.message || err))}

`; } + catch (err) { setStatus(String(err.message || err), 'bad'); $('[data-result]').innerHTML = `
Could not fetch · Devnet 3, no value

${esc(String(err.message || err))}

`; } finally { form.querySelector('button').disabled = false; } }); if (input.value) form.requestSubmit(); diff --git a/site/lc/core.js b/site/lc/core.js index 7bd74d777..ffc07d478 100644 --- a/site/lc/core.js +++ b/site/lc/core.js @@ -265,7 +265,7 @@ export function verifyBalance(cp, proof, deps) { const step = (name, fn) => { try { const d = fn(); steps.push({ name, ok: true, detail: d }); return d; } catch (e) { steps.push({ name, ok: false, detail: String(e.message || e) }); throw e; } }; const done = extra => ({ ...extra, steps, ms: Math.round((now() - t0) * 10) / 10 }); try { - const cert = step('certificate: BLS aggregate over the checkpoint, 2/3 of active and 2/3 of total weight', () => { + const cert = step('certificate: BLS aggregate over the checkpoint, 2/3 of active and 17/30 of total weight', () => { const r = verifyCheckpoint(cp, { blake2b, bls }); if (!r.verified) throw new Error(r.reason); return `checkpoint ${r.index}, ${r.signers} of ${r.voters} voters, ${(r.weight_fraction_total * 100).toFixed(1)}% of total weight, ${r.headers_checked} headers to the previous lock`; @@ -306,7 +306,7 @@ export function verifyBalance(cp, proof, deps) { if (bytesToHex(record.block) !== st.block_hash) throw new Error('the record\'s block hash is not the statement\'s'); // the EVM chain id in the statement is the one in force at that block (Devnet 3: 4463 below its class v5 floor, 4464 above); // the record's binding to the chain is the signed message, which names the network by its string ("igneum-devnet-3") - const ids = cp.chain_id === 'igneum-devnet-3' ? [4463, 4464] : [Number(proof.account.evm_chain_id)]; + const ids = cp.chain_id === 'igneum-devnet-3' ? [4463, 4464] : cp.chain_id === 'igneum-devnet-4' ? [4465] : [Number(proof.account.evm_chain_id)]; if (!ids.includes(Number(st.chain_id))) throw new Error(`statement chain id ${st.chain_id} is not ${cp.chain_id}'s (${ids.join(' or ')})`); return `EVM chain id ${st.chain_id}, chain block ${st.number}, post_root ${st.post_root.slice(0, 12)}, ${st.executed} executed, chain_len ${st.chain_len}`; }); @@ -338,7 +338,7 @@ export function verifyReceipt(receipt, deps) { const done = extra => ({ ...extra, steps, ms: Math.round((now() - t0) * 10) / 10 }); try { const cp = receipt.checkpoint.certificate; - const cert = step('certificate: BLS aggregate over the checkpoint, 2/3 of active and 2/3 of total weight', () => { + const cert = step('certificate: BLS aggregate over the checkpoint, 2/3 of active and 17/30 of total weight', () => { const r = verifyCheckpoint(cp, { blake2b, bls }); if (!r.verified) throw new Error(r.reason); if (strip(cp.hash) !== strip(receipt.checkpoint.hash) || receipt.chain_id !== cp.chain_id) throw new Error('the receipt names another checkpoint or chain than its certificate'); @@ -379,6 +379,138 @@ export function parseTx(raw) { return { type: 0, nonce: bigOf(body[0]).toString(), to: body[3].length ? hex(body[3]) : null, value: bigOf(body[4]).toString(), data: hex(body[5]), gas: bigOf(body[2]).toString() }; } + +// ---- the ordered receipts trie (reth's layout: key = RLP(index), value = the EIP-2718 receipt envelope) ---------- +// Built in full from the shard's receipts, so the root is recomputed rather than proven: a shard is bounded by its pgas +// budget and holds at most a few hundred receipts. +function hpEncode(nibs, leaf) { + const odd = nibs.length % 2; const flag = (leaf ? 2 : 0) + odd; + const out = []; if (odd) out.push((flag << 4) | nibs[0]); else out.push(flag << 4, ); + for (let i = odd; i < nibs.length; i += 2) out.push((nibs[i] << 4) | nibs[i + 1]); + return new Uint8Array(out); +} +function trieNode(items, keccak) { + // items: [{nibs, value}] sorted by nibs; returns the RLP of the node (inline when under 32 bytes, else its hash) + if (items.length === 0) return new Uint8Array(0); + const encode = node => { const enc = rlpEncode(node); return enc.length < 32 ? enc : keccak(enc); }; + if (items.length === 1) return encode([hpEncode(items[0].nibs, true), items[0].value]); + let prefix = 0; + while (items.every(it => it.nibs.length > prefix && it.nibs[prefix] === items[0].nibs[prefix])) prefix++; + if (prefix > 0) { + const child = trieNode(items.map(it => ({ nibs: it.nibs.slice(prefix), value: it.value })), keccak); + return encode([hpEncode(items[0].nibs.slice(0, prefix), false), child.length === 32 && rlpEncode(child).length > 32 ? child : child]); + } + const branch = []; let value = new Uint8Array(0); + for (let b = 0; b < 16; b++) { + const sub = items.filter(it => it.nibs.length > 0 && it.nibs[0] === b).map(it => ({ nibs: it.nibs.slice(1), value: it.value })); + branch.push(sub.length ? trieNode(sub, keccak) : new Uint8Array(0)); + } + const here = items.find(it => it.nibs.length === 0); if (here) value = here.value; + branch.push(value); + return encode(branch); +} +// Children are RLP-embedded when their encoding is under 32 bytes, else referenced by hash; trieNode returns either +// the short RLP or the 32-byte hash, and rlpEncode treats a Uint8Array as a string item: a short child must be +// spliced in raw, so the list encoder below handles both forms. +export function orderedTrieRoot(values, keccak) { + const items = values.map((v, i) => ({ nibs: nibblesOf(rlpEncode(trimBig(BigInt(i)))), value: v })); + items.sort((a, b) => { const n = Math.min(a.nibs.length, b.nibs.length); for (let k = 0; k < n; k++) if (a.nibs[k] !== b.nibs[k]) return a.nibs[k] - b.nibs[k]; return a.nibs.length - b.nibs.length; }); + if (!items.length) return keccak(rlpEncode(new Uint8Array(0))); + const root = trieNodeTop(items, keccak); + return root; +} +function nibblesOf(b) { const out = []; for (const x of b) out.push(x >> 4, x & 15); return out; } +// the node forms: an Embedded child is spliced raw into the parent's list; a Hashed child is a 32-byte string item +class Raw { constructor(bytes) { this.bytes = bytes; } } +function rlpList(parts) { + const body = concat(parts.map(p => p instanceof Raw ? p.bytes : rlpEncode(p))); + const len = body.length; const head = len < 56 ? new Uint8Array([0xc0 + len]) : (() => { const b = []; let x = len; while (x > 0) { b.unshift(x & 0xff); x = Math.floor(x / 256); } return new Uint8Array([0xf7 + b.length, ...b]); })(); + return concat([head, body]); +} +function build(items, keccak) { // returns { enc } the node's RLP + if (items.length === 1) return rlpList([hpEncode(items[0].nibs, true), items[0].value]); + let prefix = 0; + while (items.every(it => it.nibs.length > prefix && it.nibs[prefix] === items[0].nibs[prefix])) prefix++; + if (prefix > 0) { + const childEnc = build(items.map(it => ({ nibs: it.nibs.slice(prefix), value: it.value })), keccak); + return rlpList([hpEncode(items[0].nibs.slice(0, prefix), false), childEnc.length < 32 ? new Raw(childEnc) : keccak(childEnc)]); + } + const parts = []; + for (let b = 0; b < 16; b++) { + const sub = items.filter(it => it.nibs.length > 0 && it.nibs[0] === b).map(it => ({ nibs: it.nibs.slice(1), value: it.value })); + if (!sub.length) { parts.push(new Uint8Array(0)); continue; } + const enc = build(sub, keccak); parts.push(enc.length < 32 ? new Raw(enc) : keccak(enc)); + } + const here = items.find(it => it.nibs.length === 0); parts.push(here ? here.value : new Uint8Array(0)); + return rlpList(parts); +} +function trieNodeTop(items, keccak) { return keccak(build(items, keccak)); } +void trieNode; + +// A receipt as the node encodes it for the trie (alloy ReceiptEnvelope::encode_2718): type byte for 1 and 2, then +// RLP([status, cumulative_gas_used, logs_bloom, [[address, [topics], data]...]]). Fields from eth_getBlockReceipts. +export function receiptEnvelope(r) { + const h = x => hexToBytes(strip(x)); + const status = Number(r.status) === 1 ? new Uint8Array([1]) : new Uint8Array(0); + const logs = (r.logs || []).map(l => [h(l.address), (l.topics || []).map(h), h(l.data || '0x')]); + const body = rlpEncode([status, trimBig(BigInt(r.cumulativeGasUsed)), h(r.logsBloom), logs]); + const type = Number(r.type || 0); + return type === 1 || type === 2 ? concat([new Uint8Array([type]), body]) : body; +} + +// ---- the payment receipt: the outcome authenticated through the proven segment's receipts commitment ------------ +// `pr` = the inclusion receipt's fields plus: segment: { record_hex, carrier: {...as /balance}, headers: [carrier..checkpoint], +// shard_receipts_roots: [hex...], shard_index, receipts: [eth_getBlockReceipts rows of that shard, in order], +// receipt_position }. The transaction's chain block must be the segment's last block (the statement commits that +// block's receipts); the verifier says so when it is not. +export function verifyPaymentReceipt(pr, deps) { + const { blake2b, bls, keccak } = deps; + const base = verifyReceipt(pr, deps); + if (!base.verified) return { ...base, payment: false }; + const steps = base.steps.slice(); const t0 = now(); + const step = (name, fn) => { try { const d = fn(); steps.push({ name, ok: true, detail: d }); return d; } catch (e) { steps.push({ name, ok: false, detail: String(e.message || e) }); throw e; } }; + const done = extra => ({ ...base, ...extra, steps, ms: Math.round((base.ms + now() - t0) * 10) / 10 }); + try { + const seg = pr.segment; + if (!seg) throw Object.assign(new Error(pr.payment_unavailable || 'no proven segment record covers the chain block that executed this transaction yet'), { soft: true }); + const cp = pr.checkpoint.certificate; + step('header chain from the record\'s carrier block up to the checkpoint', () => `${verifyHeaderPath(seg.headers, blake2b, seg.headers[0].hash, cp.hash)} headers`); + const record = step('the segment record is in the carrier\'s coinbase (hash recomputed, merkle path) and signed by its aggregator', () => { + const cb = coinbaseTxHash(seg.carrier.coinbase, blake2b, seg.carrier.amount_wire_len || 8); + const root = merkleRootFromPath(cb, Number(seg.carrier.leaf_index), seg.carrier.merkle_siblings.map(x => hexToBytes(strip(x))), blake2b); + if (bytesToHex(root) !== strip(seg.headers[0].hash_merkle_root)) throw new Error('the merkle path does not reach the carrier\'s hash_merkle_root'); + const rec = segmentRecordsOf(hexToBytes(strip(seg.carrier.coinbase.payload))).find(r => bytesToHex(r.bytes) === strip(seg.record_hex)); + if (!rec) throw new Error('the named segment record is not in the carrier\'s coinbase'); + if (!verifySegmentRecordSignature(cp.chain_id, rec, bls)) throw new Error('the aggregator\'s signature does not verify'); + return `record for blocks ${rec.first} to ${rec.last}, aggregator ${voteKeyHash(rec.pubkey, blake2b).slice(0, 12)}`; + }) && segmentRecordsOf(hexToBytes(strip(seg.carrier.coinbase.payload))).find(r => bytesToHex(r.bytes) === strip(seg.record_hex)); + const st = record.statement; + step('the statement is for the chain block that executed the transaction', () => { + if (Number(st.number) !== Number(pr.execution.chain_block) || st.block_hash !== strip(pr.execution.chain_block_hash)) throw new Error(`the statement is for chain block ${st.number}, the transaction executed at ${pr.execution.chain_block}`); + return `chain block ${st.number}, ${st.shard_count} shard(s), receipts commitment ${st.receipts.slice(0, 12)}`; + }); + step('the shard receipts roots hash to the statement\'s receipts commitment', () => { + const roots = seg.shard_receipts_roots.map(x => hexToBytes(strip(x))); + if (roots.length !== Number(st.shard_count)) throw new Error(`${roots.length} roots given, the statement names ${st.shard_count} shards`); + if (bytesToHex(keccak(concat(roots))) !== st.receipts) throw new Error('keccak over the shard receipts roots is not the statement\'s commitment'); + return `${roots.length} root(s), shard ${seg.shard_index} is ${strip(seg.shard_receipts_roots[seg.shard_index]).slice(0, 12)}`; + }); + const mine = step('the shard\'s receipts rebuild its receipts trie root, and the transaction\'s receipt sits in it', () => { + const root = bytesToHex(orderedTrieRoot(seg.receipts.map(receiptEnvelope), keccak)); + if (root !== strip(seg.shard_receipts_roots[seg.shard_index])) throw new Error(`the ${seg.receipts.length} receipts rebuild root ${root.slice(0, 12)}, the committed root is ${strip(seg.shard_receipts_roots[seg.shard_index]).slice(0, 12)}`); + const r = seg.receipts[Number(seg.receipt_position)]; + if (!r || strip(r.transactionHash) !== strip(pr.tx_hash)) throw new Error('the receipt at the named position is not this transaction\'s'); + return `${seg.receipts.length} receipt(s), this one at position ${seg.receipt_position}, status ${Number(r.status) === 1 ? 'success' : 'failed'}, gas ${parseInt(r.gasUsed, 16)}, ${(r.logs || []).length} log(s)`; + }) && seg.receipts[Number(seg.receipt_position)]; + const ok = Number(mine.status) === 1; + const outcome = ok ? { asset: 'IGN (the native coin)', recipient: base.tx.to, amount_wei: base.tx.value, logs: (mine.logs || []).map(l => ({ address: l.address, topics: l.topics, data: l.data })) } : null; + return done({ verified: true, payment: ok, outcome, receipt_status: ok ? 'success' : 'failed', note: ok ? 'the transfer outcome is authenticated through the proven segment\'s receipts commitment' : 'the transaction executed and FAILED: no transfer took place; authenticated the same way' }); + } catch (e) { + if (e.soft) return done({ verified: true, payment: false, payment_unavailable: String(e.message || e) }); + return done({ verified: false, payment: false, reason: String(e.message || e) }); + } +} + export function formatIgn(wei, decimals = 18) { const v = BigInt(wei); const base = 10n ** BigInt(decimals); const whole = v / base; let frac = (v % base).toString().padStart(decimals, '0').replace(/0+$/, ''); diff --git a/site/lc/test.html b/site/lc/test.html index 29c0032c1..496cdb4d1 100644 --- a/site/lc/test.html +++ b/site/lc/test.html @@ -8,8 +8,8 @@ -

Igneum reference apps: the negative cases, in this tab (devnet, no value)

-

Each row runs site/lc/core.js on live devnet data from . A tampered case must read refused; the genuine case must read verified. The same cases run under Node in tools/reference-apps/light-service/verify.test.mjs. Parameters: ?api= (the read service), ?address= (the balance to prove; default: the miner of the latest block), ?tx= (a transaction; default: one in a recent block).

+

Igneum reference apps: the negative cases, in this tab (Devnet 3, no value)

+

Each row runs site/lc/core.js on live Devnet 3 data from . A tampered case must read refused; the genuine case must read verified. The same cases run under Node in tools/reference-apps/light-service/verify.test.mjs. Parameters: ?api= (the read service), ?address= (the balance to prove; default: the miner of the latest block), ?tx= (a transaction; default: one in a recent block).

Receipt

caseresultmsreason

Balance

diff --git a/site/lc/verify-receipt.js b/site/lc/verify-receipt.js index 6eb21755b..1b2d9e03e 100644 --- a/site/lc/verify-receipt.js +++ b/site/lc/verify-receipt.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -// Igneum transaction inclusion receipt verifier, one file, offline. Source: tools/reference-apps/receipt/verify-receipt.src.mjs and site/lc/core.js +// Igneum receipt verifier (transaction inclusion receipt or payment receipt), one file, offline. Source: tools/reference-apps/receipt/verify-receipt.src.mjs and site/lc/core.js // (the same checks the browser page runs), bundled with @noble/hashes 2.4.0 and @noble/curves 2.4.0 (MIT). Usage: node verify-receipt.js receipt.json [--tamper] // tools/reference-apps/receipt/verify-receipt.src.mjs @@ -2121,8 +2121,8 @@ var ScalarMultiplier = class { } /** * Implements ec multiplication using precomputed signed fixed-window wNAF tables. - * Constant-time: fixed window count with one table addition per window, zero digits feed - * the fake accumulator, and no doublings; the lookup scans the whole window slice. + * Constant-time: fixed window count with one table addition per window — zero digits feed + * the fake accumulator — and no doublings; the lookup scans the whole window slice. * Scalar bounds are validated by the public entry points ({@link ScalarMultiplier.mulCT}, * {@link ScalarMultiplier.mulCTBlinded}, {@link ScalarMultiplier.mulUnsafe}); * signedWindowDigits throws if `n` exceeds the table. @@ -2209,7 +2209,7 @@ var ScalarMultiplier = class { * A cached wNAF table only pays off when reused; a flat 2^FW_WINDOW table (`size-1` adds) is * far cheaper to build for a single use. The point-operation sequence is independent of `n`: * build the table, then per window exactly FW_WINDOW doublings, a data-oblivious scan over - * every table entry, and one addition (adds the identity when the window digit is 0, never + * every table entry, and one addition (adds the identity when the window digit is 0 — never * skipped). * * `n` must be `< 2^bits`. Assumes complete addition (adding the identity costs the same as any @@ -2217,7 +2217,7 @@ var ScalarMultiplier = class { * projective form (no normalizeZ): normalizing this small a table costs more than the * mixed-add savings it would buy for a single multiply. * @returns real point `p`; `f` duplicates it only to match {@link wnafCachedCT}'s return shape - * (this path needs no fake accumulator, its op-count is already scalar-independent). + * (this path needs no fake accumulator — its op-count is already scalar-independent). */ fixedWindowCT(point, n, bits) { const W = FW_WINDOW; @@ -5142,10 +5142,26 @@ function verifyCheckpoint(data, deps2) { } // site/lc/core.js +var DST_SEGMENT = "IGNEUM_SEGMENT_RECORD_V1_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_"; var SEGMENT_RECORD_LEN = 2 + 8 + 8 + 32 + 48 + 20 + 340 + 32 + 96; var ZERO32 = new Uint8Array(32); var te2 = new TextEncoder(); var strip = (s) => String(s).replace(/^0x/i, ""); +var u64le = (v) => { + const b = new Uint8Array(8); + new DataView(b.buffer).setBigUint64(0, BigInt(v), true); + return b; +}; +var u16le = (v) => { + const b = new Uint8Array(2); + new DataView(b.buffer).setUint16(0, Number(v), true); + return b; +}; +var u32le = (v) => { + const b = new Uint8Array(4); + new DataView(b.buffer).setUint32(0, Number(v), true); + return b; +}; function concat2(parts) { const n = parts.reduce((a, p) => a + p.length, 0); const m = new Uint8Array(n); @@ -5171,6 +5187,114 @@ function merkleRootFromPath(leaf, index, siblings, blake2b2) { } return h; } +function coinbaseTxHash(tx, blake2b2, amountWireLen = 8) { + const version = Number(tx.version || 0); + const parts = [u16le(version), u64le((tx.inputs || []).length)]; + for (const inp of tx.inputs || []) { + parts.push(hexToBytes3(strip(inp.previousOutpoint.transactionId)), u32le(inp.previousOutpoint.index)); + const sig = hexToBytes3(strip(inp.signatureScript || "")); + parts.push(u64le(sig.length), sig); + if (version < 1) parts.push(new Uint8Array([Number(inp.sigOpCount || 0)])); + parts.push(u64le(inp.sequence)); + if (version >= 1) parts.push(u16le(inp.computeBudget || 0)); + } + parts.push(u64le((tx.outputs || []).length)); + for (const out of tx.outputs || []) { + const v = new Uint8Array(amountWireLen); + let x = BigInt(out.value); + for (let i = 0; i < amountWireLen; i++) { + v[i] = Number(x & 0xffn); + x >>= 8n; + } + const spk = hexToBytes3(strip(out.scriptPublicKey)); + parts.push(v, spk.subarray(0, 2), u64le(spk.length - 2), spk.subarray(2)); + if (version >= 1) parts.push(new Uint8Array([out.covenant ? 1 : 0])); + } + const payload = hexToBytes3(strip(tx.payload || "")); + parts.push(u64le(tx.lockTime || 0), hexToBytes3(strip(tx.subnetworkId)), u64le(tx.gas || 0), u64le(payload.length), payload); + const mass = BigInt(tx.mass || 0); + if (version < 1) { + if (mass > 0n) parts.push(u64le(mass)); + } else parts.push(u64le(mass)); + return keyed(blake2b2, "TransactionHash")(concat2(parts)); +} +function coinbaseExtraData(payload) { + if (payload.length < 19) return new Uint8Array(0); + const scriptLen = payload[18]; + return payload.subarray(19 + scriptLen); +} +function takeSection(extra, tag) { + const n = extra.length; + if (n < 8) return { items: null, before: extra }; + const t2 = String.fromCharCode(...extra.subarray(n - 4)); + if (t2 !== tag) return { items: null, before: extra }; + const len = new DataView(extra.buffer, extra.byteOffset + n - 8, 4).getUint32(0, true); + if (len + 8 > n) return { items: null, before: extra }; + return { items: extra.subarray(n - 8 - len, n - 8), before: extra.subarray(0, n - 8 - len) }; +} +function segmentRecordsOf(payload) { + const extra = coinbaseExtraData(payload); + const f = takeSection(extra, "IGNF"); + const p = takeSection(f.before, "IGNP"); + const s = takeSection(p.before, "IGNS"); + const out = []; + if (!s.items) return out; + for (let o = 0; o + SEGMENT_RECORD_LEN <= s.items.length; o += SEGMENT_RECORD_LEN) out.push(parseSegmentRecord(s.items.subarray(o, o + SEGMENT_RECORD_LEN))); + return out; +} +function parseSegmentRecord(b) { + if (b.length !== SEGMENT_RECORD_LEN) throw new Error(`segment record is ${b.length} bytes, not ${SEGMENT_RECORD_LEN}`); + const dv = new DataView(b.buffer, b.byteOffset, b.byteLength); + let o = 0; + const version = dv.getUint16(o, true); + o += 2; + const first = dv.getBigUint64(o, true); + o += 8; + const last = dv.getBigUint64(o, true); + o += 8; + const block = b.subarray(o, o + 32); + o += 32; + const pubkey = b.subarray(o, o + 48); + o += 48; + const payout = b.subarray(o, o + 20); + o += 20; + const publicValues = b.subarray(o, o + 340); + o += 340; + const proofHash = b.subarray(o, o + 32); + o += 32; + const signature = b.subarray(o, o + 96); + return { bytes: b, version, first, last, block, pubkey, payout, publicValues, proofHash, signature, statement: parseBlockStatement(publicValues) }; +} +function parseBlockStatement(b) { + const dv = new DataView(b.buffer, b.byteOffset, b.byteLength); + const h = (o) => bytesToHex3(b.subarray(o, o + 32)); + return { + chain_id: dv.getBigUint64(0), + number: dv.getBigUint64(8), + block_hash: h(16), + parent_hash: h(48), + shard_count: dv.getUint32(80), + tx_commitment: h(84), + pre_root: h(116), + post_root: h(148), + receipts: h(180), + gas_used: dv.getBigUint64(212), + pgas_used: dv.getBigUint64(220), + executed: dv.getUint32(228), + skipped: dv.getUint32(232), + provers: h(236), + shard_vk: h(268), + agg_vk: h(300), + chain_len: dv.getBigUint64(332) + }; +} +function segmentRecordMessage(chainId, r2) { + return concat2([te2.encode("igneum-segment-record-v1/" + chainId), new Uint8Array([0]), u64le(r2.first), u64le(r2.last), r2.block, r2.payout, r2.publicValues, r2.proofHash]); +} +function verifySegmentRecordSignature(chainId, r2, bls2) { + const L = bls2.longSignatures; + return L.verify(r2.signature, L.hash(segmentRecordMessage(chainId, r2), DST_SEGMENT), r2.pubkey); +} function rlpDecode(b) { const [item, rest] = rlpItem(b, 0); if (rest !== b.length) throw new Error("rlp: trailing bytes"); @@ -5209,6 +5333,31 @@ function rlpItem(b, o) { } return [items, end]; } +function rlpEncode(item) { + if (item instanceof Uint8Array) { + if (item.length === 1 && item[0] < 128) return item; + return concat2([rlpLen(item.length, 128), item]); + } + const body = concat2(item.map(rlpEncode)); + return concat2([rlpLen(body.length, 192), body]); +} +function rlpLen(n, base) { + if (n < 56) return new Uint8Array([base + n]); + const bytes = []; + let x = n; + while (x > 0) { + bytes.unshift(x & 255); + x = Math.floor(x / 256); + } + return new Uint8Array([base + 55 + bytes.length, ...bytes]); +} +var trimBig = (v) => { + v = BigInt(v); + if (v === 0n) return new Uint8Array(0); + let h = v.toString(16); + if (h.length % 2) h = "0" + h; + return hexToBytes3(h); +}; function verifyHeaderPath(headers, blake2b2, fromHash, toHash) { if (!headers.length) throw new Error("no headers"); for (let i = 0; i < headers.length; i++) { @@ -5241,7 +5390,7 @@ function verifyReceipt(receipt2, deps2) { const done = (extra) => ({ ...extra, steps, ms: Math.round((now() - t0) * 10) / 10 }); try { const cp = receipt2.checkpoint.certificate; - const cert = step("certificate: BLS aggregate over the checkpoint, 2/3 of active and 2/3 of total weight", () => { + const cert = step("certificate: BLS aggregate over the checkpoint, 2/3 of active and 17/30 of total weight", () => { const r2 = verifyCheckpoint(cp, { blake2b: blake2b2, bls: bls2 }); if (!r2.verified) throw new Error(r2.reason); if (strip(cp.hash) !== strip(receipt2.checkpoint.hash) || receipt2.chain_id !== cp.chain_id) throw new Error("the receipt names another checkpoint or chain than its certificate"); @@ -5276,6 +5425,140 @@ function parseTx(raw) { if (type === 1) return { type, chain_id: bigOf(body[0]).toString(), nonce: bigOf(body[1]).toString(), to: body[4].length ? hex(body[4]) : null, value: bigOf(body[5]).toString(), data: hex(body[6]), gas: bigOf(body[3]).toString() }; return { type: 0, nonce: bigOf(body[0]).toString(), to: body[3].length ? hex(body[3]) : null, value: bigOf(body[4]).toString(), data: hex(body[5]), gas: bigOf(body[2]).toString() }; } +function hpEncode(nibs, leaf) { + const odd = nibs.length % 2; + const flag = (leaf ? 2 : 0) + odd; + const out = []; + if (odd) out.push(flag << 4 | nibs[0]); + else out.push(flag << 4); + for (let i = odd; i < nibs.length; i += 2) out.push(nibs[i] << 4 | nibs[i + 1]); + return new Uint8Array(out); +} +function orderedTrieRoot(values, keccak) { + const items = values.map((v, i) => ({ nibs: nibblesOf(rlpEncode(trimBig(BigInt(i)))), value: v })); + items.sort((a, b) => { + const n = Math.min(a.nibs.length, b.nibs.length); + for (let k = 0; k < n; k++) if (a.nibs[k] !== b.nibs[k]) return a.nibs[k] - b.nibs[k]; + return a.nibs.length - b.nibs.length; + }); + if (!items.length) return keccak(rlpEncode(new Uint8Array(0))); + const root = trieNodeTop(items, keccak); + return root; +} +function nibblesOf(b) { + const out = []; + for (const x of b) out.push(x >> 4, x & 15); + return out; +} +var Raw = class { + constructor(bytes) { + this.bytes = bytes; + } +}; +function rlpList(parts) { + const body = concat2(parts.map((p) => p instanceof Raw ? p.bytes : rlpEncode(p))); + const len = body.length; + const head = len < 56 ? new Uint8Array([192 + len]) : (() => { + const b = []; + let x = len; + while (x > 0) { + b.unshift(x & 255); + x = Math.floor(x / 256); + } + return new Uint8Array([247 + b.length, ...b]); + })(); + return concat2([head, body]); +} +function build(items, keccak) { + if (items.length === 1) return rlpList([hpEncode(items[0].nibs, true), items[0].value]); + let prefix = 0; + while (items.every((it) => it.nibs.length > prefix && it.nibs[prefix] === items[0].nibs[prefix])) prefix++; + if (prefix > 0) { + const childEnc = build(items.map((it) => ({ nibs: it.nibs.slice(prefix), value: it.value })), keccak); + return rlpList([hpEncode(items[0].nibs.slice(0, prefix), false), childEnc.length < 32 ? new Raw(childEnc) : keccak(childEnc)]); + } + const parts = []; + for (let b = 0; b < 16; b++) { + const sub = items.filter((it) => it.nibs.length > 0 && it.nibs[0] === b).map((it) => ({ nibs: it.nibs.slice(1), value: it.value })); + if (!sub.length) { + parts.push(new Uint8Array(0)); + continue; + } + const enc = build(sub, keccak); + parts.push(enc.length < 32 ? new Raw(enc) : keccak(enc)); + } + const here = items.find((it) => it.nibs.length === 0); + parts.push(here ? here.value : new Uint8Array(0)); + return rlpList(parts); +} +function trieNodeTop(items, keccak) { + return keccak(build(items, keccak)); +} +function receiptEnvelope(r2) { + const h = (x) => hexToBytes3(strip(x)); + const status = Number(r2.status) === 1 ? new Uint8Array([1]) : new Uint8Array(0); + const logs = (r2.logs || []).map((l) => [h(l.address), (l.topics || []).map(h), h(l.data || "0x")]); + const body = rlpEncode([status, trimBig(BigInt(r2.cumulativeGasUsed)), h(r2.logsBloom), logs]); + const type = Number(r2.type || 0); + return type === 1 || type === 2 ? concat2([new Uint8Array([type]), body]) : body; +} +function verifyPaymentReceipt(pr, deps2) { + const { blake2b: blake2b2, bls: bls2, keccak } = deps2; + const base = verifyReceipt(pr, deps2); + if (!base.verified) return { ...base, payment: false }; + const steps = base.steps.slice(); + const t0 = now(); + const step = (name, fn) => { + try { + const d = fn(); + steps.push({ name, ok: true, detail: d }); + return d; + } catch (e) { + steps.push({ name, ok: false, detail: String(e.message || e) }); + throw e; + } + }; + const done = (extra) => ({ ...base, ...extra, steps, ms: Math.round((base.ms + now() - t0) * 10) / 10 }); + try { + const seg = pr.segment; + if (!seg) throw Object.assign(new Error(pr.payment_unavailable || "no proven segment record covers the chain block that executed this transaction yet"), { soft: true }); + const cp = pr.checkpoint.certificate; + step("header chain from the record's carrier block up to the checkpoint", () => `${verifyHeaderPath(seg.headers, blake2b2, seg.headers[0].hash, cp.hash)} headers`); + const record = step("the segment record is in the carrier's coinbase (hash recomputed, merkle path) and signed by its aggregator", () => { + const cb = coinbaseTxHash(seg.carrier.coinbase, blake2b2, seg.carrier.amount_wire_len || 8); + const root = merkleRootFromPath(cb, Number(seg.carrier.leaf_index), seg.carrier.merkle_siblings.map((x) => hexToBytes3(strip(x))), blake2b2); + if (bytesToHex3(root) !== strip(seg.headers[0].hash_merkle_root)) throw new Error("the merkle path does not reach the carrier's hash_merkle_root"); + const rec = segmentRecordsOf(hexToBytes3(strip(seg.carrier.coinbase.payload))).find((r2) => bytesToHex3(r2.bytes) === strip(seg.record_hex)); + if (!rec) throw new Error("the named segment record is not in the carrier's coinbase"); + if (!verifySegmentRecordSignature(cp.chain_id, rec, bls2)) throw new Error("the aggregator's signature does not verify"); + return `record for blocks ${rec.first} to ${rec.last}, aggregator ${voteKeyHash(rec.pubkey, blake2b2).slice(0, 12)}`; + }) && segmentRecordsOf(hexToBytes3(strip(seg.carrier.coinbase.payload))).find((r2) => bytesToHex3(r2.bytes) === strip(seg.record_hex)); + const st = record.statement; + step("the statement is for the chain block that executed the transaction", () => { + if (Number(st.number) !== Number(pr.execution.chain_block) || st.block_hash !== strip(pr.execution.chain_block_hash)) throw new Error(`the statement is for chain block ${st.number}, the transaction executed at ${pr.execution.chain_block}`); + return `chain block ${st.number}, ${st.shard_count} shard(s), receipts commitment ${st.receipts.slice(0, 12)}`; + }); + step("the shard receipts roots hash to the statement's receipts commitment", () => { + const roots = seg.shard_receipts_roots.map((x) => hexToBytes3(strip(x))); + if (roots.length !== Number(st.shard_count)) throw new Error(`${roots.length} roots given, the statement names ${st.shard_count} shards`); + if (bytesToHex3(keccak(concat2(roots))) !== st.receipts) throw new Error("keccak over the shard receipts roots is not the statement's commitment"); + return `${roots.length} root(s), shard ${seg.shard_index} is ${strip(seg.shard_receipts_roots[seg.shard_index]).slice(0, 12)}`; + }); + const mine = step("the shard's receipts rebuild its receipts trie root, and the transaction's receipt sits in it", () => { + const root = bytesToHex3(orderedTrieRoot(seg.receipts.map(receiptEnvelope), keccak)); + if (root !== strip(seg.shard_receipts_roots[seg.shard_index])) throw new Error(`the ${seg.receipts.length} receipts rebuild root ${root.slice(0, 12)}, the committed root is ${strip(seg.shard_receipts_roots[seg.shard_index]).slice(0, 12)}`); + const r2 = seg.receipts[Number(seg.receipt_position)]; + if (!r2 || strip(r2.transactionHash) !== strip(pr.tx_hash)) throw new Error("the receipt at the named position is not this transaction's"); + return `${seg.receipts.length} receipt(s), this one at position ${seg.receipt_position}, status ${Number(r2.status) === 1 ? "success" : "failed"}, gas ${parseInt(r2.gasUsed, 16)}, ${(r2.logs || []).length} log(s)`; + }) && seg.receipts[Number(seg.receipt_position)]; + const ok = Number(mine.status) === 1; + const outcome = ok ? { asset: "IGN (the native coin)", recipient: base.tx.to, amount_wei: base.tx.value, logs: (mine.logs || []).map((l) => ({ address: l.address, topics: l.topics, data: l.data })) } : null; + return done({ verified: true, payment: ok, outcome, receipt_status: ok ? "success" : "failed", note: ok ? "the transfer outcome is authenticated through the proven segment's receipts commitment" : "the transaction executed and FAILED: no transfer took place; authenticated the same way" }); + } catch (e) { + if (e.soft) return done({ verified: true, payment: false, payment_unavailable: String(e.message || e) }); + return done({ verified: false, payment: false, reason: String(e.message || e) }); + } +} function formatIgn(wei, decimals = 18) { const v = BigInt(wei); const base = 10n ** BigInt(decimals); @@ -5312,14 +5595,16 @@ if (args.includes("--tamper")) { console.log(`tampered copy (one nibble of the raw transaction): ${t2.verified ? "NOT REFUSED, this verifier is broken" : "REFUSED"}${t2.reason ? " :: " + t2.reason : ""}`); if (t2.verified) process.exit(1); } -var r = verifyReceipt(receipt, deps); -console.log(`TRANSACTION INCLUSION RECEIPT 0x${receipt.tx_hash} on ${receipt.chain_id} (devnet, no value)`); -console.log("What this file authenticates: that the signed transaction is included in a block that is finalised. What it does not: the execution outcome (status, gas), which is carried as the node reported it. A payment receipt, which authenticates the transfer outcome, is a different file."); +var r = receipt.segment ? verifyPaymentReceipt(receipt, deps) : verifyReceipt(receipt, deps); +var payment = !!(r.verified && r.payment); +console.log(`${payment ? "PAYMENT RECEIPT" : "TRANSACTION INCLUSION RECEIPT"} 0x${receipt.tx_hash} on ${receipt.chain_id} (Devnet 3, no value)`); +console.log(payment ? "What this file authenticates: that the signed transaction is included in a finalised block and executed with status success, its receipt sitting in the shard receipts trie whose root the proven segment statement commits to. Trusted: the voter table from the node; the SP1 proof behind the statement is verified by nodes, not here." : "What this file authenticates: that the signed transaction is included in a block that is finalised. What it does not: the execution outcome (status, gas), which is carried as the node reported it." + (receipt.payment_unavailable ? " Why no payment receipt: " + receipt.payment_unavailable : "")); print(r); if (!r.verified) { console.log(`REFUSED: ${r.reason}`); process.exit(1); } var t = r.tx; -console.log(`INCLUSION VERIFIED in ${r.ms} ms: a signed transaction of ${formatIgn(t.value)} IGN to ${t.to || "contract creation"} (${t.value} wei) is included in block ${r.block.slice(0, 16)} at DAA ${r.block_daa} (${new Date(Number(r.block_time)).toISOString()}), finalised under checkpoint ${r.checkpoint}; ${r.headers} headers checked; ${r.certificate}.`); -console.log("Reported by the node, not authenticated by this file: from " + (receipt.tx_as_reported && receipt.tx_as_reported.from) + (receipt.execution ? `, executed with status ${receipt.execution.status === "0x1" ? "success" : "failed"}` : "") + ". The voter table with weights came from the node (spec 10.1). In the four words: included and finalised are authenticated, executed is reported, proven is not claimed."); +if (payment) console.log(`PAYMENT VERIFIED in ${r.ms} ms: ${formatIgn(t.value)} IGN (${t.value} wei of the native coin) to ${t.to || "contract creation"}, executed with status success (${r.outcome.logs.length} log(s)) at chain block ${receipt.execution.chain_block}, in block ${r.block.slice(0, 16)} at DAA ${r.block_daa}, finalised under checkpoint ${r.checkpoint}; ${r.certificate}.`); +else console.log(`INCLUSION VERIFIED in ${r.ms} ms: a signed transaction of ${formatIgn(t.value)} IGN to ${t.to || "contract creation"} (${t.value} wei) is included in block ${r.block.slice(0, 16)} at DAA ${r.block_daa} (${new Date(Number(r.block_time)).toISOString()}), finalised under checkpoint ${r.checkpoint}; ${r.headers} headers checked; ${r.certificate}.`); +console.log("Reported by the node, not authenticated by this file: from " + (receipt.tx_as_reported && receipt.tx_as_reported.from) + (payment ? "" : receipt.execution ? `, executed with status ${receipt.execution.status === "0x1" ? "success" : "failed"}` : "") + ". The voter table with weights came from the node (spec 10.1). In the four words: " + (payment ? "included, executed, proven and finalised are authenticated under the stated trust." : "included and finalised are authenticated, executed is reported, proven is not claimed.")); diff --git a/tools/reference-apps/compat/results.json b/tools/reference-apps/compat/results.json index 159b0a91e..88192ace0 100644 --- a/tools/reference-apps/compat/results.json +++ b/tools/reference-apps/compat/results.json @@ -4,72 +4,100 @@ "chain_id": 4465, "network": "igneum-devnet-4", "node_version": "igneumd 2.0.0", - "started": "2026-10-08T16:23:51.342Z", + "started": "2026-10-08T16:33:17.064Z", "sender": "0x27C433aEec28f9Be4cCe17A8C645d65baDfCF891", - "funded": false, + "funded": true, "solc": "0.8.28+commit.7893614a.Emscripten.clang", - "finished": "2026-10-08T16:24:26.239Z" + "finished": "2026-10-08T16:34:23.613Z" }, "summary": { - "passed": 2, + "passed": 20, "failed": 0, - "untested": 18 + "untested": 0 + }, + "deployed": { + "token": "0xb2495fd85f54d73d7a13059a8e61956b67ad7aed", + "probe": "0x3efc60794d574627eb5cb4eca77d96735dac4c42", + "contextTx": { + "hash": "0x3c1d2dffad903da6fc821598c7105e8683ea2000a885b440827c73d9dd204fcd", + "block": 768, + "coinbase": "0xc119093F38E3764Fdd86ea9635b7f616DEC6C3A6", + "prevrandao": "9cf17fa10c95c0a8fcb6332bf7cd9dac1415aea7fcf09ca4f13f410fbdc72024" + } }, - "deployed": {}, "rows": [ { "id": "contracts.erc20.deploy", "item": "Representative contracts", "name": "an ERC-20 deploys (CREATE address as Ethereum computes it, code at the address)", - "verdict": "untested", - "detail": "no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)", - "evidence": {}, - "at": "2026-10-08T16:23:51.619Z" + "verdict": "passed", + "detail": "at 0xb2495fd85f54d73d7a13059a8e61956b67ad7aed, block 739, gas 436751 of 639078 estimated", + "evidence": { + "tx": "0x8fd280cb68049a077b762d0152cace0428230d0757fbed7b05f09a8c24a7a5b9", + "address": "0xb2495fd85f54d73d7a13059a8e61956b67ad7aed", + "block": 739, + "gasUsed": 436751, + "estimated": 639078 + }, + "at": "2026-10-08T16:33:22.753Z" }, { "id": "contracts.erc20.transfer", "item": "Representative contracts", "name": "ERC-20 transfer: balances move, the Transfer log decodes, eth_call reads the new balance", - "verdict": "untested", - "detail": "no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)", - "evidence": {}, - "at": "2026-10-08T16:23:51.619Z" + "verdict": "passed", + "detail": "5 CTT to 0x00000000, log decoded, balance read", + "evidence": { + "tx": "0xe787770a18a4da1288080e493a5ac3f02aba5f4f5d89302a409e0c2ffc7125fb", + "block": 742 + }, + "at": "2026-10-08T16:33:27.426Z" }, { "id": "contracts.erc20.approve", "item": "Representative contracts", "name": "ERC-20 approve then transferFrom by a spender (the sender approves itself as spender): allowance consumed, logs decode", - "verdict": "untested", - "detail": "no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)", - "evidence": {}, - "at": "2026-10-08T16:23:51.620Z" + "verdict": "passed", + "detail": "allowance 3 then 1 after transferFrom of 2", + "evidence": { + "approve": "0xfc7a3a64b8de2d3b5c75c54036ac5e785df5e1d78cfd6ea9be7602e7ee815d24", + "transferFrom": "0x6c0a22dc3511c4a6836f0e9637f52fd3d282549eb5317e3e9078e6db652cef22" + }, + "at": "2026-10-08T16:33:36.609Z" }, { "id": "contracts.probe.deploy", "item": "Representative contracts", "name": "a probe contract deploys (counter, block context reader, revert, out-of-gas loop, CREATE2)", - "verdict": "untested", - "detail": "no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)", - "evidence": {}, - "at": "2026-10-08T16:23:51.620Z" + "verdict": "passed", + "detail": "at 0x3efc60794d574627eb5cb4eca77d96735dac4c42", + "evidence": { + "tx": "0x551c24030022ebca96ba6d44c6d8d50637cba25be5c7c13839e01eadc6938e15", + "address": "0x3efc60794d574627eb5cb4eca77d96735dac4c42", + "block": 751 + }, + "at": "2026-10-08T16:33:41.134Z" }, { "id": "contracts.counter", "item": "Representative contracts", "name": "a counter increments twice and reads 2 (state persists across chain blocks)", - "verdict": "untested", - "detail": "no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)", - "evidence": {}, - "at": "2026-10-08T16:23:51.620Z" + "verdict": "passed", + "detail": "count 2 after blocks 753 and 755", + "evidence": { + "first": "0xfaf855760dad78fd488432f54db35ecf7baf089408abb37309d77089a99b75a1", + "second": "0x97ca41517b1ee4cff94b854308a6ab474be1d6f87eff582837caf89b58fe19c8" + }, + "at": "2026-10-08T16:33:50.383Z" }, { "id": "fees.quotes", "item": "Wallet fee estimation", "name": "eth_gasPrice, eth_maxPriorityFeePerGas and eth_feeHistory answer in Ethereum's shapes", "verdict": "passed", - "detail": "gasPrice 10101 gwei, priority 1 gwei, feeHistory 5 base fees", + "detail": "gasPrice 196.130804856 gwei, priority 1 gwei, feeHistory 5 base fees", "evidence": { - "gasPrice": "0x92fd2845200", + "gasPrice": "0x2daa4e9478", "maxPriorityFeePerGas": "0x3b9aca00", "feeHistory": { "baseFeePerGas": [ @@ -81,9 +109,9 @@ ], "gasUsedRatio": [ 0, + 0.0014490333333333333, 0, - 0, - 0 + 0.0008790333333333334 ], "igneum": { "provingBaseFeePerGas": [ @@ -93,7 +121,7 @@ "0x9184e72a000" ] }, - "oldestBlock": "0x163", + "oldestBlock": "0x2f0", "reward": [ [ "0x3b9aca00" @@ -110,157 +138,255 @@ ] } }, - "at": "2026-10-08T16:23:51.809Z" + "at": "2026-10-08T16:33:50.543Z" }, { "id": "fees.estimate.transfer", "item": "Wallet fee estimation", "name": "a plain transfer: eth_estimateGas against the receipt's gasUsed (the quote folds proving gas; both above Ethereum's 21,000)", - "verdict": "untested", - "detail": "no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)", - "evidence": {}, - "at": "2026-10-08T16:23:51.809Z" + "verdict": "passed", + "detail": "estimated 59650, used 21000 (35% of the estimate); Ethereum's figure is 21,000", + "evidence": { + "tx": "0x99c4dc1ae8d2f32bcc62ed78ff9c829a6281065ba0c9eaf43ffa574c48b1cad0", + "estimated": 59650, + "gasUsed": 21000, + "ethereum": 21000 + }, + "at": "2026-10-08T16:33:55.347Z" }, { "id": "fees.estimate.call", "item": "Wallet fee estimation", "name": "a contract call: eth_estimateGas against gasUsed (a wallet that quotes from the node is covered)", - "verdict": "untested", - "detail": "no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)", - "evidence": {}, - "at": "2026-10-08T16:23:51.809Z" + "verdict": "passed", + "detail": "estimated 134366, used 26371", + "evidence": { + "tx": "0xeb8bafa3a979a9ccd5c21ca7df91f5eb49c09b926cfa27981fcd38b796b7bee4", + "estimated": 134366, + "gasUsed": 26371 + }, + "at": "2026-10-08T16:34:00.085Z" }, { "id": "fees.local21000", "item": "Wallet fee estimation", "name": "the documented difference: a transfer sent with a local 21,000 gas limit (Ethereum's intrinsic cost) instead of the node's quote", - "verdict": "untested", - "detail": "no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)", - "evidence": {}, - "at": "2026-10-08T16:23:51.809Z" + "verdict": "passed", + "detail": "the node accepted it and it succeeded with gasUsed 21000: a wallet with a hard-coded 21,000 works", + "evidence": { + "sent": true, + "hash": "0x538fdf72a8fef37dc1c0024bcfe61028ed74b02c504e9df26978f4adb907b121", + "status": "success", + "gasUsed": 21000 + }, + "at": "2026-10-08T16:34:04.657Z" }, { "id": "failed.revert", "item": "Failed transactions", "name": "a revert with a reason: eth_call returns the reason, the sent transaction has status 0, gas is charged, the nonce advances", - "verdict": "untested", - "detail": "no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)", - "evidence": {}, - "at": "2026-10-08T16:23:51.810Z" + "verdict": "passed", + "detail": "eth_call answers code 3 \"execution reverted\" with Error(string) data decoding to \"compat says no\"; the sent transaction: status 0, gasUsed 22292 of 200000, nonce 23 to 24", + "evidence": { + "tx": "0xfcdf8d24eb414154d24cf3e0c11d5d732ba3f612e219d8adc316ec6323a06a1e", + "gasUsed": 22292, + "block": 762, + "call_error_code": 3, + "call_error_message": "execution reverted", + "revert_data": "0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000e636f6d7061742073617973206e6f000000000000000000000000000000000000" + }, + "at": "2026-10-08T16:34:09.064Z" }, { "id": "failed.outofgas", "item": "Failed transactions", "name": "an out-of-gas call: status 0 and the whole limit charged", - "verdict": "untested", - "detail": "no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)", - "evidence": {}, - "at": "2026-10-08T16:23:51.810Z" + "verdict": "passed", + "detail": "status 0, gasUsed 150000 of the 150000 limit", + "evidence": { + "tx": "0xb3986b45642c28dd029dc34714878e42c025a0bd6349609900b7cef120f7a1f6", + "gasUsed": 150000, + "limit": 150000 + }, + "at": "2026-10-08T16:34:13.537Z" }, { "id": "receipts.fields", "item": "Receipts", "name": "a receipt carries every Ethereum field, logsBloom recomputes from its logs, contractAddress is set on creation", - "verdict": "untested", - "detail": "no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)", - "evidence": {}, - "at": "2026-10-08T16:23:51.810Z" + "verdict": "passed", + "detail": "15 fields, bloom recomputed over 1 log(s), contractAddress 0xb2495fd8", + "evidence": { + "tx": "0x8fd280cb68049a077b762d0152cace0428230d0757fbed7b05f09a8c24a7a5b9", + "fields": [ + "blockHash", + "blockNumber", + "contractAddress", + "cumulativeGasUsed", + "effectiveGasPrice", + "from", + "gasUsed", + "igneum", + "logs", + "logsBloom", + "status", + "to", + "transactionHash", + "transactionIndex", + "type" + ] + }, + "at": "2026-10-08T16:34:13.578Z" }, { "id": "receipts.root", "item": "Receipts", "name": "the block's receipts rebuild its receipts trie root (Ethereum's layout, the node's encoding) equal to the header's receiptsRoot", - "verdict": "untested", - "detail": "no block with two or more transactions in the last 600 chain blocks (the chain is young and nothing sends yet); reruns when the chain carries transactions", - "evidence": {}, - "at": "2026-10-08T16:24:09.239Z" + "verdict": "passed", + "detail": "block 742, 1 receipts, root 0xa28061003c4c", + "evidence": { + "block": 742, + "receipts": 1, + "receiptsRoot": "0xa28061003c4cfa70006e203524ededd4668753d432bef4c657c1ede124d6ac55" + }, + "at": "2026-10-08T16:34:13.651Z" }, { "id": "indexing.logs", "item": "Indexing", "name": "eth_getLogs by address, by topic and by block range returns the receipts' logs with consistent logIndex and transactionIndex", - "verdict": "untested", - "detail": "no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)", - "evidence": {}, - "at": "2026-10-08T16:24:09.240Z" + "verdict": "passed", + "detail": "4 logs by address, 3 Transfer logs by topic, each found in its receipt", + "evidence": { + "byAddress": 4, + "byTopic": 3, + "fromBlock": 739 + }, + "at": "2026-10-08T16:34:13.880Z" }, { "id": "indexing.block", "item": "Indexing", "name": "eth_getBlockByNumber with full transactions, eth_getBlockReceipts and eth_getTransactionByHash agree on indices and hashes", - "verdict": "untested", - "detail": "no block with two or more transactions in the last 600 chain blocks (the chain is young and nothing sends yet); reruns when the chain carries transactions", - "evidence": {}, - "at": "2026-10-08T16:24:25.972Z" + "verdict": "passed", + "detail": "block 742: 1 transactions, indices 0 to 0 continuous", + "evidence": { + "block": 742, + "count": 1 + }, + "at": "2026-10-08T16:34:14.059Z" }, { "id": "assumptions.context", "item": "Application assumptions", "name": "block.number, timestamp, chainid, basefee and blockhash(number-1) read from a contract match the block the call executed in", - "verdict": "untested", - "detail": "no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)", - "evidence": {}, - "at": "2026-10-08T16:24:25.972Z" + "verdict": "passed", + "detail": "block 768: number, timestamp, chainid 4465, basefee, blockhash(n-1) = parentHash, msg.sender and tx.origin all match", + "evidence": { + "tx": "0x3c1d2dffad903da6fc821598c7105e8683ea2000a885b440827c73d9dd204fcd", + "block": 768 + }, + "at": "2026-10-08T16:34:18.585Z" }, { "id": "assumptions.create2", "item": "Application assumptions", "name": "CREATE2 lands at the address Ethereum computes (keccak(0xff, deployer, salt, keccak(initcode)))", - "verdict": "untested", - "detail": "no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)", - "evidence": {}, - "at": "2026-10-08T16:24:25.972Z" + "verdict": "passed", + "detail": "child at 0xE63036B0fEC0d01321Ef4DA62C084d1Ec2eA47be", + "evidence": { + "tx": "0x11cd98733996949be674b9669a3d459ba7bbeda8345fa2879ecb9fe36b9aa80c", + "address": "0xE63036B0fEC0d01321Ef4DA62C084d1Ec2eA47be" + }, + "at": "2026-10-08T16:34:23.161Z" }, { "id": "difference.coinbase", "item": "Differences: block context", "name": "block.coinbase is the miner of the DAG block that first included the transaction, not one miner per chain block (measured: coinbase against the block's igneum.mergeset)", - "verdict": "untested", - "detail": "no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)", - "evidence": {}, - "at": "2026-10-08T16:24:25.972Z" + "verdict": "passed", + "detail": "coinbase 0xc119093F = the including block's miner 0xc119093f; the chain block merged 2 block(s) with 1 distinct miner(s), chain block miner 0xc119093f", + "evidence": { + "tx": "0x3c1d2dffad903da6fc821598c7105e8683ea2000a885b440827c73d9dd204fcd", + "coinbase": "0xc119093F38E3764Fdd86ea9635b7f616DEC6C3A6", + "includingMiner": "0xc119093f38e3764fdd86ea9635b7f616dec6c3a6", + "mergesetMiners": [ + "0xc119093f38e3764fdd86ea9635b7f616dec6c3a6", + "0xc119093f38e3764fdd86ea9635b7f616dec6c3a6" + ], + "chainBlockMiner": "0xc119093f38e3764fdd86ea9635b7f616dec6c3a6" + }, + "at": "2026-10-08T16:34:23.244Z" }, { "id": "difference.randomness", "item": "Differences: randomness", "name": "block.prevrandao: its value is derived from the chain's epoch seed (not Ethereum's beacon RANDAO); measured across consecutive blocks, and from a contract when a sender is funded", "verdict": "passed", - "detail": "blocks 377 to 380: 1 distinct epoch seed(s), 4 distinct mixHash value(s) in the headers; the source is the chain's epoch seed, which holds for an epoch, so prevrandao is not fresh per block and must not seed a lottery", + "detail": "blocks 767 to 770: 1 distinct epoch seed(s), 4 distinct mixHash value(s) in the headers; the source is the chain's epoch seed, which holds for an epoch, so prevrandao is not fresh per block and must not seed a lottery; a contract reading it twice in one call got the same value, and at block 768 it read 0x9cf17fa10c95…", "evidence": { "blocks": [ { - "number": 377, - "mixHash": "0x9775bf1921d8546571be3393a6b2c78af5e06944128971bc65dfb94638ee528d", + "number": 767, + "mixHash": "0xe5baec105ee8d299d423ae632944b4b185d4d885e57088b829443202c36d82b5", "epochSeed": "0x7c36b83374a673e990213be462509dcb08bcfc85144306aa9517cf3ad66faf6e" }, { - "number": 378, - "mixHash": "0x7b9a44ea77b874decb58fbd2ce53d9884a700f55467aae7720b8bc4d083ab88e", + "number": 768, + "mixHash": "0x9cf17fa10c95c0a8fcb6332bf7cd9dac1415aea7fcf09ca4f13f410fbdc72024", "epochSeed": "0x7c36b83374a673e990213be462509dcb08bcfc85144306aa9517cf3ad66faf6e" }, { - "number": 379, - "mixHash": "0x25ad84614bbb2b4971d4cb346037cb36b17d84fd4b7d18629b31ca81b745041d", + "number": 769, + "mixHash": "0xe39ab8de805c6b8c8ccad7b078a788b6f2b79d7b25e19f0211951a1aba5e3c42", "epochSeed": "0x7c36b83374a673e990213be462509dcb08bcfc85144306aa9517cf3ad66faf6e" }, { - "number": 380, - "mixHash": "0x67143bb8e16216f198b4bb8e7fbfaed731494da77cb541754e9a99ca62b225d1", + "number": 770, + "mixHash": "0xc749632e2ede6ff39f5e4ac3549a59d701509803d011311a9b1da9e4c363eebd", "epochSeed": "0x7c36b83374a673e990213be462509dcb08bcfc85144306aa9517cf3ad66faf6e" } ], - "contract": null + "contract": { + "hash": "0x3c1d2dffad903da6fc821598c7105e8683ea2000a885b440827c73d9dd204fcd", + "block": 768, + "coinbase": "0xc119093F38E3764Fdd86ea9635b7f616DEC6C3A6", + "prevrandao": "9cf17fa10c95c0a8fcb6332bf7cd9dac1415aea7fcf09ca4f13f410fbdc72024" + } }, - "at": "2026-10-08T16:24:26.238Z" + "at": "2026-10-08T16:34:23.576Z" }, { "id": "difference.fees", "item": "Differences: two-dimensional fees", "name": "a receipt carries pgasUsed, provingBaseFeePerGas, burnedProvingFee and burnedExecutionBaseFee; effectiveGasPrice covers both dimensions", - "verdict": "untested", - "detail": "no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)", - "evidence": {}, - "at": "2026-10-08T16:24:26.239Z" + "verdict": "passed", + "detail": "gasUsed 51375, pgasUsed 1968, proving base fee 10000 gwei per pgas; paid 5188875000000000 wei, of which proving burn 19680000000000000, execution burn 5137500000000000, tip 41100000000000 (parts sum differs from gasUsed × effectiveGasPrice)", + "evidence": { + "tx": "0xe787770a18a4da1288080e493a5ac3f02aba5f4f5d89302a409e0c2ffc7125fb", + "gasUsed": 51375, + "pgasUsed": 1968, + "effectiveGasPrice": "0x178411b200", + "igneum": { + "burnedExecutionBaseFee": "0x1240876e889800", + "burnedProvingFee": "0x45eadb112e0000", + "developerShares": [ + { + "payee": null, + "wei": "0x95855b99e00" + } + ], + "includingBlock": "0x9eb90f5ec3bf0ef4ddeb220137e756f63ddf74a1deddfffe9fbc44f473bea88b", + "includingMiner": "0x53fe98022c2ac26d5d721457fb1c374b4d56144b", + "minerTip": "0x256156e67800", + "overBudget": false, + "pgasAborted": false, + "pgasUsed": "0x7b0", + "provingBaseFeePerGas": "0x9184e72a000" + }, + "parts_equal_paid": false + }, + "at": "2026-10-08T16:34:23.613Z" } ] } diff --git a/tools/reference-apps/compat/run.mjs b/tools/reference-apps/compat/run.mjs index 8bf734ccc..d4786d952 100644 --- a/tools/reference-apps/compat/run.mjs +++ b/tools/reference-apps/compat/run.mjs @@ -161,8 +161,15 @@ await test('fees.local21000', 'Wallet fee estimation', 'the documented differenc // ---- 4 failed transactions ---------------------------------------------------------------------------------------- await test('failed.revert', 'Failed transactions', 'a revert with a reason: eth_call returns the reason, the sent transaction has status 0, gas is charged, the nonce advances', async () => { const data = encodeFunctionData({ abi: probe, functionName: 'fail', args: ['compat says no'] }); - let reason = null; try { await pub.call({ account, to: deployed.probe, data }); } catch (e) { reason = String(e.shortMessage || e.message); } - if (!reason || !reason.includes('compat says no')) throw new Error(`eth_call did not surface the reason: ${reason}`); + // the node's raw answer: Ethereum's shape is a JSON-RPC error with code 3, message "execution reverted" and the ABI-encoded + // Error(string) in `data` (selector 0x08c379a0); decoded here, not through a client library's message + const raw = await fetch(RPC, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ from: account.address, to: deployed.probe, data }, 'latest'] }) }).then(r => r.json()); + const err = raw.error || {}; + if (!err.data || !String(err.data).startsWith('0x08c379a0')) throw new Error(`eth_call did not return Error(string) revert data: ${JSON.stringify(raw).slice(0, 200)}`); + const { decodeErrorResult } = await import('viem'); + const decoded = decodeErrorResult({ abi: [{ type: 'error', name: 'Error', inputs: [{ type: 'string' }] }], data: err.data }); + const reason = String(decoded.args[0]); + if (reason !== 'compat says no') throw new Error(`the decoded reason is ${JSON.stringify(reason)}`); const before = await pub.getTransactionCount({ address: account.address }); const gas = 200000n; const hash = await wallet.sendTransaction({ to: deployed.probe, data, gas }); const r = await pub.waitForTransactionReceipt({ hash, timeout: 180000 }); @@ -170,7 +177,7 @@ await test('failed.revert', 'Failed transactions', 'a revert with a reason: eth_ if (r.status !== 'reverted') throw new Error(`status ${r.status}`); if (after !== before + 1) throw new Error('nonce did not advance'); if (r.gasUsed <= 0n) throw new Error('no gas charged'); - return { detail: `status 0, gasUsed ${r.gasUsed} of ${gas}, nonce ${before} to ${after}, eth_call reason surfaced`, evidence: { tx: hash, gasUsed: Number(r.gasUsed), block: Number(r.blockNumber) } }; + return { detail: `eth_call answers code ${err.code} "${err.message}" with Error(string) data decoding to "${reason}"; the sent transaction: status 0, gasUsed ${r.gasUsed} of ${gas}, nonce ${before} to ${after}`, evidence: { tx: hash, gasUsed: Number(r.gasUsed), block: Number(r.blockNumber), call_error_code: err.code, call_error_message: err.message, revert_data: err.data } }; }, true); await test('failed.outofgas', 'Failed transactions', 'an out-of-gas call: status 0 and the whole limit charged', async () => { const data = encodeFunctionData({ abi: probe, functionName: 'burn' }); diff --git a/tools/reference-apps/light-service/serve.mjs b/tools/reference-apps/light-service/serve.mjs index 3ca058c13..b710ca6d3 100644 --- a/tools/reference-apps/light-service/serve.mjs +++ b/tools/reference-apps/light-service/serve.mjs @@ -24,7 +24,9 @@ import { readCheckpoint, earliestCheckpointAbove, neon } from '../../../site/api const PORT = Number(process.env.LIGHT_PORT || 26890); const EXEC = process.env.EXEC_RPC || 'http://127.0.0.1:26881'; const WRPC = process.env.WRPC || 'ws://127.0.0.1:28880'; -const CHECKPOINT_URL = process.env.CHECKPOINT_URL || 'https://igneum.network/api/checkpoint?source=dn3'; +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 CHAIN_ID_NAME = process.env.CHAIN_ID_NAME || 'igneum-devnet-4'; const SEGMENT_BLOCKS = 8; const strip = s => String(s).replace(/^0x/i, '').toLowerCase(); const log = (...a) => console.log(new Date().toISOString(), ...a); @@ -133,7 +135,7 @@ async function headerPath(fromHash, chainNumber, cpNumber, cpHash) { async function checkpoint() { if (process.env.DATABASE_URL) { // the Devnet 3 observer's rows, read the way the site's /api/checkpoint reads them (DATABASE_URL from the box's observer env) - const cp = await readCheckpoint(neon(), 'dn3'); + const cp = await readCheckpoint(neon(), SOURCE); if (!cp) throw httpError(404, 'no certified Devnet 3 checkpoint stored yet'); return { ok: true, now: new Date().toISOString(), ...cp }; } @@ -179,14 +181,14 @@ async function balance(q) { if (!rec) throw httpError(500, `the carrier's coinbase holds ${records.length} segment record(s), none for segment ${chosen.first}`); // the smallest proof: the earliest certified checkpoint at or above the carrier (the certificate used travels in the answer) let certificate = null; - if (process.env.DATABASE_URL) certificate = await earliestCheckpointAbove(neon(), chosen.carrierNumber, chainNumberOf).catch(() => null); + if (process.env.DATABASE_URL) certificate = await earliestCheckpointAbove(neon(), chosen.carrierNumber, chainNumberOf, `${SOURCE}_live_certificates`).catch(() => null); let cpN = cpNumber, cpH = cpHash, cpI = cpIndex; if (certificate) { cpH = certificate.hash; cpI = Number(certificate.index); cpN = await chainNumberOf(cpH); } const headers = await headerPath(chosen.carrier, chosen.carrierNumber, cpN, cpH); const account = await exec('eth_getProof', [address, [], '0x' + chosen.last.toString(16)]); const chainId = await exec('eth_chainId', []); return { - ok: true, now: new Date().toISOString(), chain_id: 'igneum-devnet-3', address, + ok: true, now: new Date().toISOString(), chain_id: CHAIN_ID_NAME, address, checkpoint: { hash: strip(cpH), index: cpI, chain_block: cpN }, 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 }, @@ -231,7 +233,7 @@ async function receipt(q) { } const mustCover = paidRecord ? Math.max(chainNumber, Number(paidRecord.carrierNumber)) : chainNumber; let certificate = null; - if (process.env.DATABASE_URL) certificate = await earliestCheckpointAbove(neon(), mustCover, chainNumberOf).catch(() => null); + if (process.env.DATABASE_URL) certificate = await earliestCheckpointAbove(neon(), mustCover, chainNumberOf, `${SOURCE}_live_certificates`).catch(() => null); if (certificate) { cpHash = certificate.hash; cpIndex = Number(certificate.index); } const cpNumber = await chainNumberOf(cpHash); if (chainNumber > cpNumber) throw httpError(409, `not final yet: executed at chain block ${chainNumber}, the latest certified checkpoint is chain block ${cpNumber}; try again in about ${chainNumber - cpNumber + 30} s`); @@ -280,7 +282,7 @@ async function receipt(q) { } } catch (e) { paymentUnavailable = 'payment receipt data could not be assembled: ' + String(e.message || e); } return { - ok: true, now: new Date().toISOString(), chain_id: 'igneum-devnet-3', tx_hash: strip(tx), raw_tx_hex: strip(raws[idx]), + ok: true, now: new Date().toISOString(), chain_id: CHAIN_ID_NAME, tx_hash: strip(tx), raw_tx_hex: strip(raws[idx]), segment: segment || undefined, payment_unavailable: paymentUnavailable || undefined, checkpoint: { hash: strip(cpHash), index: cpIndex, chain_block: cpNumber, certificate: certificate ? { ok: true, ...certificate } : undefined }, including_block: { header: headers[0], leaf_index: idx + 1, leaf_count: leaves.length, merkle_siblings: siblings(leaves, idx + 1) },