Compatibility rows on the Igneum 2.0 devnet: 20 of 20 pass with real transactions on igneum-devnet-4 (chain id 4465; ERC-20 deploy, transfer, approve and transferFrom; a probe contract; fee quotes and estimates against gasUsed; the 21,000-limit transfer; a revert with Error(string) data decoded from eth_call's raw answer; out-of-gas; receipt fields, bloom and the rebuilt receipts trie root; logs and index agreement; block context, CREATE2; the measured differences: coinbase per including block, prevrandao from the epoch seed, the two-dimensional fee split); site/lc restored to the payment-receipt code another lane's landing (ffd6751e) had reverted; the read service and /api/checkpoint read devnet-4's dn4_ certificates (?source=dn4)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
c981bdb31f
commit
3974a355be
10 changed files with 723 additions and 167 deletions
42
docs/build/compatibility.md
vendored
42
docs/build/compatibility.md
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -251,35 +251,35 @@ table{min-width:560px}
|
|||
<nav class="doc-nav" aria-label="Contents"><div class="eyebrow">Contents</div><a href="#representative-contracts">Representative contracts</a><a href="#wallet-fee-estimation">Wallet fee estimation</a><a href="#indexing">Indexing</a><a href="#failed-transactions">Failed transactions</a><a href="#receipts">Receipts</a><a href="#application-assumptions">Application assumptions</a><a href="#differences-block-context">Differences: block context</a><a href="#differences-randomness">Differences: randomness</a><a href="#differences-two-dimensional-fees">Differences: two-dimensional fees</a><a href="#what-the-differences-mean-for-an-application">What the differences mean for an application</a></nav>
|
||||
<article class="doc-body" data-filter-list="compatibility"><section class="doc-section"><h1 id="compatibility-measured">Compatibility, measured</h1>
|
||||
<p>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 (<code>tools/reference-apps/compat/run.mjs</code>), with its evidence, and a row that could not run says so and why. Devnet, no value.</p>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<p>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 <a href="/receipt">/receipt</a>.</p>
|
||||
</section>
|
||||
<section class="doc-section"><h2 id="representative-contracts">Representative contracts</h2>
|
||||
<div class="tbl"><table><thead><tr><th>Row</th><th>Verdict</th><th>What was measured</th><th>Evidence</th></tr></thead><tbody><tr><td>an ERC-20 deploys (CREATE address as Ethereum computes it, code at the address)</td><td><strong>untested</strong></td><td>no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)</td><td></td></tr><tr><td>ERC-20 transfer: balances move, the Transfer log decodes, eth_call reads the new balance</td><td><strong>untested</strong></td><td>no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)</td><td></td></tr><tr><td>ERC-20 approve then transferFrom by a spender (the sender approves itself as spender): allowance consumed, logs decode</td><td><strong>untested</strong></td><td>no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)</td><td></td></tr><tr><td>a probe contract deploys (counter, block context reader, revert, out-of-gas loop, CREATE2)</td><td><strong>untested</strong></td><td>no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)</td><td></td></tr><tr><td>a counter increments twice and reads 2 (state persists across chain blocks)</td><td><strong>untested</strong></td><td>no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)</td><td></td></tr></tbody></table></div>
|
||||
<div class="tbl"><table><thead><tr><th>Row</th><th>Verdict</th><th>What was measured</th><th>Evidence</th></tr></thead><tbody><tr><td>an ERC-20 deploys (CREATE address as Ethereum computes it, code at the address)</td><td><strong>passed</strong></td><td>at 0xb2495fd85f54d73d7a13059a8e61956b67ad7aed, block 739, gas 436751 of 639078 estimated</td><td>tx 0x8fd280cb…a5b9, address 0xb2495fd85f54d73d7a13059a8e61956b67ad7aed, block 739, gasUsed 436751, estimated 639078</td></tr><tr><td>ERC-20 transfer: balances move, the Transfer log decodes, eth_call reads the new balance</td><td><strong>passed</strong></td><td>5 CTT to 0x00000000, log decoded, balance read</td><td>tx 0xe787770a…25fb, block 742</td></tr><tr><td>ERC-20 approve then transferFrom by a spender (the sender approves itself as spender): allowance consumed, logs decode</td><td><strong>passed</strong></td><td>allowance 3 then 1 after transferFrom of 2</td><td>approve 0xfc7a3a64…5d24, transferFrom 0x6c0a22dc…ef22</td></tr><tr><td>a probe contract deploys (counter, block context reader, revert, out-of-gas loop, CREATE2)</td><td><strong>passed</strong></td><td>at 0x3efc60794d574627eb5cb4eca77d96735dac4c42</td><td>tx 0x551c2403…8e15, address 0x3efc60794d574627eb5cb4eca77d96735dac4c42, block 751</td></tr><tr><td>a counter increments twice and reads 2 (state persists across chain blocks)</td><td><strong>passed</strong></td><td>count 2 after blocks 753 and 755</td><td>first 0xfaf85576…75a1, second 0x97ca4151…19c8</td></tr></tbody></table></div>
|
||||
</section>
|
||||
<section class="doc-section"><h2 id="wallet-fee-estimation">Wallet fee estimation</h2>
|
||||
<div class="tbl"><table><thead><tr><th>Row</th><th>Verdict</th><th>What was measured</th><th>Evidence</th></tr></thead><tbody><tr><td>eth_gasPrice, eth_maxPriorityFeePerGas and eth_feeHistory answer in Ethereum's shapes</td><td><strong>passed</strong></td><td>gasPrice 10101 gwei, priority 1 gwei, feeHistory 5 base fees</td><td>gasPrice 0x92fd2845200, maxPriorityFeePerGas 0x3b9aca00</td></tr><tr><td>a plain transfer: eth_estimateGas against the receipt's gasUsed (the quote folds proving gas; both above Ethereum's 21,000)</td><td><strong>untested</strong></td><td>no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)</td><td></td></tr><tr><td>a contract call: eth_estimateGas against gasUsed (a wallet that quotes from the node is covered)</td><td><strong>untested</strong></td><td>no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)</td><td></td></tr><tr><td>the documented difference: a transfer sent with a local 21,000 gas limit (Ethereum's intrinsic cost) instead of the node's quote</td><td><strong>untested</strong></td><td>no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)</td><td></td></tr></tbody></table></div>
|
||||
<div class="tbl"><table><thead><tr><th>Row</th><th>Verdict</th><th>What was measured</th><th>Evidence</th></tr></thead><tbody><tr><td>eth_gasPrice, eth_maxPriorityFeePerGas and eth_feeHistory answer in Ethereum's shapes</td><td><strong>passed</strong></td><td>gasPrice 196.130804856 gwei, priority 1 gwei, feeHistory 5 base fees</td><td>gasPrice 0x2daa4e9478, maxPriorityFeePerGas 0x3b9aca00</td></tr><tr><td>a plain transfer: eth_estimateGas against the receipt's gasUsed (the quote folds proving gas; both above Ethereum's 21,000)</td><td><strong>passed</strong></td><td>estimated 59650, used 21000 (35% of the estimate); Ethereum's figure is 21,000</td><td>tx 0x99c4dc1a…cad0, estimated 59650, gasUsed 21000, ethereum 21000</td></tr><tr><td>a contract call: eth_estimateGas against gasUsed (a wallet that quotes from the node is covered)</td><td><strong>passed</strong></td><td>estimated 134366, used 26371</td><td>tx 0xeb8bafa3…bee4, estimated 134366, gasUsed 26371</td></tr><tr><td>the documented difference: a transfer sent with a local 21,000 gas limit (Ethereum's intrinsic cost) instead of the node's quote</td><td><strong>passed</strong></td><td>the node accepted it and it succeeded with gasUsed 21000: a wallet with a hard-coded 21,000 works</td><td>sent true, hash 0x538fdf72…b121, status success, gasUsed 21000</td></tr></tbody></table></div>
|
||||
</section>
|
||||
<section class="doc-section"><h2 id="indexing">Indexing</h2>
|
||||
<div class="tbl"><table><thead><tr><th>Row</th><th>Verdict</th><th>What was measured</th><th>Evidence</th></tr></thead><tbody><tr><td>eth_getLogs by address, by topic and by block range returns the receipts' logs with consistent logIndex and transactionIndex</td><td><strong>untested</strong></td><td>no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)</td><td></td></tr><tr><td>eth_getBlockByNumber with full transactions, eth_getBlockReceipts and eth_getTransactionByHash agree on indices and hashes</td><td><strong>untested</strong></td><td>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</td><td></td></tr></tbody></table></div>
|
||||
<div class="tbl"><table><thead><tr><th>Row</th><th>Verdict</th><th>What was measured</th><th>Evidence</th></tr></thead><tbody><tr><td>eth_getLogs by address, by topic and by block range returns the receipts' logs with consistent logIndex and transactionIndex</td><td><strong>passed</strong></td><td>4 logs by address, 3 Transfer logs by topic, each found in its receipt</td><td>byAddress 4, byTopic 3, fromBlock 739</td></tr><tr><td>eth_getBlockByNumber with full transactions, eth_getBlockReceipts and eth_getTransactionByHash agree on indices and hashes</td><td><strong>passed</strong></td><td>block 742: 1 transactions, indices 0 to 0 continuous</td><td>block 742, count 1</td></tr></tbody></table></div>
|
||||
</section>
|
||||
<section class="doc-section"><h2 id="failed-transactions">Failed transactions</h2>
|
||||
<div class="tbl"><table><thead><tr><th>Row</th><th>Verdict</th><th>What was measured</th><th>Evidence</th></tr></thead><tbody><tr><td>a revert with a reason: eth_call returns the reason, the sent transaction has status 0, gas is charged, the nonce advances</td><td><strong>untested</strong></td><td>no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)</td><td></td></tr><tr><td>an out-of-gas call: status 0 and the whole limit charged</td><td><strong>untested</strong></td><td>no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)</td><td></td></tr></tbody></table></div>
|
||||
<div class="tbl"><table><thead><tr><th>Row</th><th>Verdict</th><th>What was measured</th><th>Evidence</th></tr></thead><tbody><tr><td>a revert with a reason: eth_call returns the reason, the sent transaction has status 0, gas is charged, the nonce advances</td><td><strong>passed</strong></td><td>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</td><td>tx 0xfcdf8d24…6a1e, gasUsed 22292, block 762, call_error_code 3, call_error_message execution reverted, revert_data 0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000e636f6d7061742073617973206e6f000000000000000000000000000000000000</td></tr><tr><td>an out-of-gas call: status 0 and the whole limit charged</td><td><strong>passed</strong></td><td>status 0, gasUsed 150000 of the 150000 limit</td><td>tx 0xb3986b45…a1f6, gasUsed 150000, limit 150000</td></tr></tbody></table></div>
|
||||
</section>
|
||||
<section class="doc-section"><h2 id="receipts">Receipts</h2>
|
||||
<div class="tbl"><table><thead><tr><th>Row</th><th>Verdict</th><th>What was measured</th><th>Evidence</th></tr></thead><tbody><tr><td>a receipt carries every Ethereum field, logsBloom recomputes from its logs, contractAddress is set on creation</td><td><strong>untested</strong></td><td>no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)</td><td></td></tr><tr><td>the block's receipts rebuild its receipts trie root (Ethereum's layout, the node's encoding) equal to the header's receiptsRoot</td><td><strong>untested</strong></td><td>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</td><td></td></tr></tbody></table></div>
|
||||
<div class="tbl"><table><thead><tr><th>Row</th><th>Verdict</th><th>What was measured</th><th>Evidence</th></tr></thead><tbody><tr><td>a receipt carries every Ethereum field, logsBloom recomputes from its logs, contractAddress is set on creation</td><td><strong>passed</strong></td><td>15 fields, bloom recomputed over 1 log(s), contractAddress 0xb2495fd8</td><td>tx 0x8fd280cb…a5b9</td></tr><tr><td>the block's receipts rebuild its receipts trie root (Ethereum's layout, the node's encoding) equal to the header's receiptsRoot</td><td><strong>passed</strong></td><td>block 742, 1 receipts, root 0xa28061003c4c</td><td>block 742, receipts 1, receiptsRoot 0xa2806100…ac55</td></tr></tbody></table></div>
|
||||
</section>
|
||||
<section class="doc-section"><h2 id="application-assumptions">Application assumptions</h2>
|
||||
<div class="tbl"><table><thead><tr><th>Row</th><th>Verdict</th><th>What was measured</th><th>Evidence</th></tr></thead><tbody><tr><td>block.number, timestamp, chainid, basefee and blockhash(number-1) read from a contract match the block the call executed in</td><td><strong>untested</strong></td><td>no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)</td><td></td></tr><tr><td>CREATE2 lands at the address Ethereum computes (keccak(0xff, deployer, salt, keccak(initcode)))</td><td><strong>untested</strong></td><td>no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)</td><td></td></tr></tbody></table></div>
|
||||
<div class="tbl"><table><thead><tr><th>Row</th><th>Verdict</th><th>What was measured</th><th>Evidence</th></tr></thead><tbody><tr><td>block.number, timestamp, chainid, basefee and blockhash(number-1) read from a contract match the block the call executed in</td><td><strong>passed</strong></td><td>block 768: number, timestamp, chainid 4465, basefee, blockhash(n-1) = parentHash, msg.sender and tx.origin all match</td><td>tx 0x3c1d2dff…4fcd, block 768</td></tr><tr><td>CREATE2 lands at the address Ethereum computes (keccak(0xff, deployer, salt, keccak(initcode)))</td><td><strong>passed</strong></td><td>child at 0xE63036B0fEC0d01321Ef4DA62C084d1Ec2eA47be</td><td>tx 0x11cd9873…a80c, address 0xE63036B0fEC0d01321Ef4DA62C084d1Ec2eA47be</td></tr></tbody></table></div>
|
||||
</section>
|
||||
<section class="doc-section"><h2 id="differences-block-context">Differences: block context</h2>
|
||||
<div class="tbl"><table><thead><tr><th>Row</th><th>Verdict</th><th>What was measured</th><th>Evidence</th></tr></thead><tbody><tr><td>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)</td><td><strong>untested</strong></td><td>no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)</td><td></td></tr></tbody></table></div>
|
||||
<div class="tbl"><table><thead><tr><th>Row</th><th>Verdict</th><th>What was measured</th><th>Evidence</th></tr></thead><tbody><tr><td>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)</td><td><strong>passed</strong></td><td>coinbase 0xc119093F = the including block's miner 0xc119093f; the chain block merged 2 block(s) with 1 distinct miner(s), chain block miner 0xc119093f</td><td>tx 0x3c1d2dff…4fcd, coinbase 0xc119093F38E3764Fdd86ea9635b7f616DEC6C3A6, includingMiner 0xc119093f38e3764fdd86ea9635b7f616dec6c3a6, chainBlockMiner 0xc119093f38e3764fdd86ea9635b7f616dec6c3a6</td></tr></tbody></table></div>
|
||||
</section>
|
||||
<section class="doc-section"><h2 id="differences-randomness">Differences: randomness</h2>
|
||||
<div class="tbl"><table><thead><tr><th>Row</th><th>Verdict</th><th>What was measured</th><th>Evidence</th></tr></thead><tbody><tr><td>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</td><td><strong>passed</strong></td><td>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</td><td></td></tr></tbody></table></div>
|
||||
<div class="tbl"><table><thead><tr><th>Row</th><th>Verdict</th><th>What was measured</th><th>Evidence</th></tr></thead><tbody><tr><td>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</td><td><strong>passed</strong></td><td>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…</td><td></td></tr></tbody></table></div>
|
||||
</section>
|
||||
<section class="doc-section"><h2 id="differences-two-dimensional-fees">Differences: two-dimensional fees</h2>
|
||||
<div class="tbl"><table><thead><tr><th>Row</th><th>Verdict</th><th>What was measured</th><th>Evidence</th></tr></thead><tbody><tr><td>a receipt carries pgasUsed, provingBaseFeePerGas, burnedProvingFee and burnedExecutionBaseFee; effectiveGasPrice covers both dimensions</td><td><strong>untested</strong></td><td>no funded sender on this devnet tonight (the faucet refused this connection; ask again tomorrow)</td><td></td></tr></tbody></table></div>
|
||||
<div class="tbl"><table><thead><tr><th>Row</th><th>Verdict</th><th>What was measured</th><th>Evidence</th></tr></thead><tbody><tr><td>a receipt carries pgasUsed, provingBaseFeePerGas, burnedProvingFee and burnedExecutionBaseFee; effectiveGasPrice covers both dimensions</td><td><strong>passed</strong></td><td>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)</td><td>tx 0xe787770a…25fb, gasUsed 51375, pgasUsed 1968, effectiveGasPrice 0x178411b200, parts_equal_paid false</td></tr></tbody></table></div>
|
||||
</section>
|
||||
<section class="doc-section"><h2 id="what-the-differences-mean-for-an-application">What the differences mean for an application</h2>
|
||||
<ul><li>Block context: <code>block.coinbase</code> is the miner of the DAG block that first included the transaction, so one chain block can carry transactions with different coinbases; code that pays or trusts <code>block.coinbase</code> as "the block producer" sees several producers per block.</li><li>Randomness: <code>block.prevrandao</code> derives from the chain's epoch seed and holds for an epoch, so it is not fresh per block and must not seed a lottery or a commitment; use an application-level randomness source.</li><li>Two-dimensional fees: every transaction pays execution gas and proving gas; the node folds the second into the gas it quotes, so a wallet that quotes from the node (eth_estimateGas, eth_gasPrice, eth_feeHistory) is covered and a wallet with Ethereum's constants (21,000 for a transfer) is not; the receipt carries the split under <code>igneum</code> (pgasUsed, provingBaseFeePerGas, burnedProvingFee, burnedExecutionBaseFee, minerTip).</li></ul>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
? `<div class="headline"><div class="eyebrow ember">Proven balance · devnet, no value</div><div class="big">${esc(formatIgn(res.balance_wei))} <span>IGN</span></div>
|
||||
? `<div class="headline"><div class="eyebrow ember">Proven balance · Devnet 3, no value</div><div class="big">${esc(formatIgn(res.balance_wei))} <span>IGN</span></div>
|
||||
<p class="pt">${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.</p></div>`
|
||||
: `<div class="headline bad"><div class="eyebrow">Not verified · devnet, no value</div><p class="pt">${esc(res.reason)}</p></div>`;
|
||||
: `<div class="headline bad"><div class="eyebrow">Not verified · Devnet 3, no value</div><p class="pt">${esc(res.reason)}</p></div>`;
|
||||
out.innerHTML = top + negativeHtml('a copy of this proof with one byte of a trie node altered', neg) + stepsHtml(res)
|
||||
+ `<p class="note">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).</p>`;
|
||||
+ `<p class="note">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).</p>`;
|
||||
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
|
||||
? `<div class="headline"><div class="eyebrow ember">Transaction inclusion receipt · included and finalised · devnet, no value</div><div class="big">${esc(formatIgn(t.value || '0'))} <span>IGN</span></div>
|
||||
? `<div class="headline"><div class="eyebrow ember">${res.payment ? 'Payment receipt · included, executed, proven and finalised' : 'Transaction inclusion receipt · included and finalised'} · Devnet 3, no value</div><div class="big">${esc(formatIgn(t.value || '0'))} <span>IGN</span></div>
|
||||
${res.payment ? `<p class="pt"><b>Outcome authenticated:</b> ${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))}.</p>` : `<p class="pt"><b>This is the inclusion receipt.</b> ${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.' : ''}</p>`}
|
||||
<div class="kv"><div class="k">To</div><div class="mono">${esc(t.to || 'contract creation')}</div><div class="k">From</div><div class="mono">${esc(receipt.tx_as_reported.from)} <small>(as the node reports it; the signature is the chain's check)</small></div>
|
||||
<div class="k">Transaction</div><div class="mono">0x${esc(receipt.tx_hash)}</div><div class="k">Block</div><div class="mono">${esc(res.block)} <small>at ${esc(when)}, DAA ${esc(res.block_daa)}</small></div>
|
||||
<div class="k">Finality</div><div>checkpoint ${res.checkpoint}, ${esc(res.certificate)}; ${res.headers} headers from the block to the checkpoint, verified here in ${res.ms} ms</div>
|
||||
${receipt.execution ? `<div class="k">Executed</div><div>status ${receipt.execution.status === '0x1' ? 'success' : 'failed'}, gas ${parseInt(receipt.execution.gas_used, 16)}, ${receipt.execution.logs} log(s) <small>(as the node reports it: executed is reported, not authenticated by this receipt; a payment receipt would authenticate the outcome)</small></div>` : ''}</div></div>`
|
||||
: `<div class="headline bad"><div class="eyebrow">Not verified · devnet, no value</div><p class="pt">${esc(res.reason)}</p></div>`;
|
||||
out.innerHTML = top + negativeHtml('a copy of this receipt with one nibble of the raw transaction altered', neg) + stepsHtml(res)
|
||||
+ (res.verified ? `<p><button class="btn" type="button" data-download>Download the inclusion receipt (JSON, ${Math.round(JSON.stringify(receipt).length / 1024)} KB)</button></p>
|
||||
${receipt.execution ? `<div class="k">Executed</div><div>status ${receipt.execution.status === '0x1' ? 'success' : 'failed'}, gas ${parseInt(receipt.execution.gas_used, 16)}, ${receipt.execution.logs} log(s) <small>${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)'}</small></div>` : ''}</div></div>`
|
||||
: `<div class="headline bad"><div class="eyebrow">Not verified · Devnet 3, no value</div><p class="pt">${esc(res.reason)}</p></div>`;
|
||||
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 ? `<p><button class="btn" type="button" data-download>Download the ${res.payment ? 'payment' : 'inclusion'} receipt (JSON, ${Math.round(JSON.stringify(receipt).length / 1024)} KB)</button></p>
|
||||
<p class="note">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: <code>node verify-receipt.js receipt.json</code> (<a href="/lc/verify-receipt.js" download>verify-receipt.js</a>, plain JavaScript, no npm, no network). A tampered file fails there the same way the copy above failed here.</p>` : '');
|
||||
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 = `<div class="headline bad"><div class="eyebrow">Could not fetch · devnet, no value</div><p class="pt">${esc(String(err.message || err))}</p></div>`; }
|
||||
catch (err) { setStatus(String(err.message || err), 'bad'); $('[data-result]').innerHTML = `<div class="headline bad"><div class="eyebrow">Could not fetch · Devnet 3, no value</div><p class="pt">${esc(String(err.message || err))}</p></div>`; }
|
||||
finally { form.querySelector('button').disabled = false; }
|
||||
});
|
||||
if (input.value) form.requestSubmit();
|
||||
|
|
|
|||
138
site/lc/core.js
138
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+$/, '');
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
<style>body{font-family:ui-monospace,Menlo,monospace;font-size:13px;background:#0C0C0E;color:#E8E4DA;padding:24px;max-width:1100px}h1{font-size:16px}h2{font-size:14px;margin-top:28px}table{display:block;overflow-x:auto;max-width:100%}code{overflow-wrap:anywhere}td{padding:4px 10px;border-bottom:1px solid #222;vertical-align:top}.ok{color:#7ED957}.bad{color:#F2541B}p{max-width:90ch}</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Igneum reference apps: the negative cases, in this tab (devnet, no value)</h1>
|
||||
<p>Each row runs <code>site/lc/core.js</code> on live devnet data from <code id="api"></code>. A tampered case must read <b>refused</b>; the genuine case must read <b>verified</b>. The same cases run under Node in <code>tools/reference-apps/light-service/verify.test.mjs</code>. Parameters: <code>?api=</code> (the read service), <code>?address=</code> (the balance to prove; default: the miner of the latest block), <code>?tx=</code> (a transaction; default: one in a recent block).</p>
|
||||
<h1>Igneum reference apps: the negative cases, in this tab (Devnet 3, no value)</h1>
|
||||
<p>Each row runs <code>site/lc/core.js</code> on live Devnet 3 data from <code id="api"></code>. A tampered case must read <b>refused</b>; the genuine case must read <b>verified</b>. The same cases run under Node in <code>tools/reference-apps/light-service/verify.test.mjs</code>. Parameters: <code>?api=</code> (the read service), <code>?address=</code> (the balance to prove; default: the miner of the latest block), <code>?tx=</code> (a transaction; default: one in a recent block).</p>
|
||||
<h2>Receipt</h2>
|
||||
<table id="r"><thead><tr><td>case</td><td>result</td><td>ms</td><td>reason</td></tr></thead><tbody></tbody></table>
|
||||
<h2>Balance</h2>
|
||||
|
|
|
|||
|
|
@ -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."));
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
|
|
|
|||
Loading…
Reference in a new issue