diff --git a/docs/analysis/v6-10-guest-repin-2026-10-08.md b/docs/analysis/v6-10-guest-repin-2026-10-08.md new file mode 100644 index 000000000..395c8a7e2 --- /dev/null +++ b/docs/analysis/v6-10-guest-repin-2026-10-08.md @@ -0,0 +1,44 @@ +# V6-10: source, ELF and wire format pinned together (review B, 8 October 2026) + +The finding: the ELF manifest was pinned_at 2026-10-05 while the fee split (D4) and the FixtureEnv flag changed the guest input; four matching hashes prove the files are the committed ones, not that the committed ELF was built from the current source and input layout. Closure asked: a deterministic rebuild and repin, a versioned guest input format, genuine compressed proofs with the new host under the old and the new fee modes, equality with the node's native state and receipt outputs, tests for fee rounding, aborts, duplicate payments and the succession window; activation never from a passing native test alone. + +## 1. The versioned guest input format (in code, branch p22-stage-2, 7604fcf5d) + +`ShardInput.format` and `AggInput.format` are the first field of every guest input. `GUEST_INPUT_FORMAT` is 3 (1: proving v0; 2: the D4 proving payment flag and the P22 stage 1 inputs commitment; 3: the stage 2 carried fixtures). `shard_statement` and `aggregate` call `check_input_format` before anything else, so a host and a guest built from different layouts refuse each other at the first field instead of proving garbage (before this, the mismatch read as "public values are 0 bytes, expected 392" from a guest that panicked on bincode: run76 on build-3, 20:44 UK, the stage 1 ELF fed a stage 2 input). Every host site stamps the format. `igneum-prove-pin` writes `source_commit` (IGNEUM_PIN_SOURCE_COMMIT or git HEAD) and `guest_input_format` into the manifest and carries the `prior` pair of a key succession over unless `--no-prior`. + +## 2. The deterministic rebuild and repin (build-1, 20:35 to 20:5x UK) + +| Build | Tree | Target dir | Shard ELF sha256 | Aggregator ELF sha256 | +|---|---|---|---|---| +| A | 75547f2a5 plus the uncommitted format tag (overlay) | target | 0ce9e351…b712f04 | 3ef25e3f…a29516 | +| B | the same, rebuilt into a clean target dir two minutes later | target-det | 0ce9e351…b712f04 | 3ef25e3f…a29516 | +| C | c45db4420, the committed tree, the elf output dir removed first (20:47 UK) | target | 0ce9e351…b712f04 | 3ef25e3f…a29516 | + +The verifying keys matched byte for byte across A and B (program vk 0dbb6605…, aggregator vk 074eb906…). The pinned manifest is C's, with `source_commit` c45db4420 and `guest_input_format` 3. Program ids: shard 0x51cd8cba314a32b60fac393949a4571714da6d6656717d286011601b2a163fe7, aggregator 0x05b395ec238f67406084f8a449d400f64c87dc62151daebf66695864727ded8a, the same from A, B and C; the vks matched across all three. Committed as 7d38077df (the pin, its manifest and elf/prior) and HELD OFF MASTER on branch p22-stage-2-pin (tip b5b82c958, on the box mirror): master's proving/igneum-prove/elf/ is what the 2.0.1 node kits embed, and the 2.0.1 start check refuses an elf/ whose top pair is not the object's pinned pair (devnet-4 pins the live pair), so pin C moves onto master together with the digest move when main names H (the node lane, 21:0x UK). The stages' code lands with elf/ unchanged (the served pair at the top). The prior pair carried in the manifest is the live pair 0x2b1a81cb… / 0x474678f3… (the 17:58 UK re-pin's current pair, 0x282dcfce… / 0x3fd721e8…, is superseded by this pin because stage 2 changed the public values again; the node lane places the new pair on the 2.0.2 tree after the D1 freeze). + +## 3. Genuine compressed proofs with the new host, both fee modes + +| Row | Fixture | Mode | Box | Result | +|---|---|---|---|---| +| old fee mode | fixtures/fees-v1-shards2.json (the whole charge burns) | compressed, shard 0 | build-3, CPU, 20:51 to 20:53 UK | VERIFIED: setup matches the manifest, execute 4,720,511 cycles, prove 111.1 s, proof 1,272,961 bytes, verify 0.035 s; statement 0xe1c82681b9891306d248d8a7b482aa5ade55f2f8d451b892dd6ed070bc091de1, proof sha256 0xa2bbabb10aff5084988fb822c059cf23f58fb627fca59aaef976898cf50b2135 (run86) | +| new fee mode | fixtures/d4-block-149-payment-on.json: block 149 of a fast-time chain mined by the D4 node 930b6322 with proving_payment_activation_daa 0 (build-8, 20:51 UK), cut by the exporter on build-2 | compressed, shard 0 | build-2, CPU, 20:56 to 20:58 UK | VERIFIED: setup matches the manifest, execute 175,044 cycles, prove 68.5 s, proof 1,272,961 bytes, verify 0.079 s; statement 0x51e5217007366c0264ab967d3c1bfb9a51c2def600e0b8b8d658adf38248e58f, proof sha256 0x76ed880959bcfcc52d1a7e0bbd7c5313a4aa05d18f65e321cee4620d5ddc0a1b (run88) | + +A hand-flagged copy of fees-v1-shards2 with the flag on was tried first and withdrawn (c45db4420): its plan's shard roots were the old mode's, and a fixture whose expected values come from the host itself is not the node-equality row. The limit of the new-mode row, stated: block 149 carries no transactions (pgas 0), so the split moved no wei in state; the mode is on by the node's params (the daemon wires `proving_payment_activation_daa` into the exec service at start, `kaspad/src/daemon.rs`), and the split's arithmetic is covered by the host test of section 5 (90 percent of every executed transaction's charge to the pool escrow on fees-v1-shards2, 11 transactions). A row with transactions in the new mode needs a funded account on the fast-time chain and is owed with the D4 merge onto the 2.0.2 line. Found on the way (the class): the exec RPC read a segment's `provingPaymentToPool` as "some transaction paid", false on an empty block with the mode on, so a fixture cut from such a block ran the guest in the old mode; fixed on the node branch proving-payment-flag (the record carries the mode it was metered in, `ChainBlockRecord.proving_payment_to_pool`, serde default, the RPC falls back to the charges for records written before the field). + +## 4. Equality with the node's native state and receipt outputs + +`igneum-prove-export` replays every exported segment through `execute_block` and compares each state root with the node's `stateRoot`; a mismatch is fatal. The D4 node's export through the exporter is that row for the new fee mode: 149 segments replayed on build-2, every state root equal to the node's, final root 0x561134497bd3311a601040f06849f09ea9940de298b0487647a07b52cd93f61d (run87, 20:54 UK); the three fixtures cut from blocks 147 to 149 each read node_state_root equal to post_state_root. The old mode's equality is the existing fixtures' `node_state_root` (every fixture under proving/fixtures carries it and the native test asserts it). + +## 5. Tests (host suite, green on build-2, run69, 20:39 UK) + +| Case | Test | +|---|---| +| another input format refused before anything else, both guests; this format passes | `v6_10_another_guest_input_format_is_refused_before_anything_else` | +| fee rounding: pool plus burn equals the charge for 2,000 charges and the u128 edge, the odd wei to the burn, off burns all | `v6_10_the_proving_payment_rounds_its_odd_wei_to_the_burn_and_always_sums_to_the_charge` | +| duplicate payments: a second record for the same (block, shard), a paid-before record and an invalid record pay nothing inside the guest's derivation | `v6_10_a_duplicate_a_paid_before_and_an_invalid_carried_record_pay_nothing_inside_the_guest` | +| aborts: a reverting transaction's charge still divides (the executor's over-budget and revert branches both call `proving_payment_split`) | `the_proving_payment_flag_moves_90_percent_of_the_charge_to_the_pool` (existing) | +| the succession window | the node suite's `succession_*` tests and the fast-time case `--succession 240 --window 120` (docs/spec/proving-enforcement.md section 3a) | + +## 6. Activation + +The rebuilt host read back the pinned ids on build-2 and build-3 (`--mode id`, 20:49 UK: shard 0x51cd8cba…, aggregator 0x05b395ec…, pinned 2026-10-08T19:47:57Z on igneum-build-1), and the compressed prove on build-3 printed "setup matches the manifest" (SP1's key setup derived the manifest's program id) before proving. Never from a passing native test alone: the new pair activates as a key succession (`proving_key_succession_daa` with its window) on the line the shipper names, after the compressed proofs of section 3 verify under the embedded pair on a box (`--mode id` read back from the rebuilt host, then `--mode compressed` VERIFIED lines), and after the fast-time succession case passes on a node embedding this pair as next. A manifest timestamp alone is never read as a binary mismatch either way. diff --git a/docs/design/consensus-proof-stages.md b/docs/design/consensus-proof-stages.md index 05886f77c..da4922381 100644 --- a/docs/design/consensus-proof-stages.md +++ b/docs/design/consensus-proof-stages.md @@ -16,7 +16,7 @@ What it proves: the proof names the inputs it applied, and every node checks the Branch: `p22-stage-1` (main repo, the guest core, the host's known-failed test `the_inputs_commitment_binds_the_rewards_and_payouts_through_shard_0_and_the_aggregator`, the vector pinned in both crates) and `p22-stage-1-node` (the fork, off `key-succession-node` 291ee6ae: the mirror function, the statement bytes, the native block statement, the veto, the tests `p22_stage_1_inputs_commitment_vector` and `p22_stage_1_a_statement_over_altered_inputs_is_vetoed_by_its_commitment`). Suites on build-2 tonight. Ships through the first key succession that follows it (new program ids). -## 2. Stage 2: the payouts derived inside the aggregator (clock 14:00 UK, 9 October 2026, as the branch; shipping with a succession) +## 2. Stage 2: the payouts derived inside the aggregator (in code, 8 October 2026, 20:1x UK: guest 566d0900b on p22-stage-2, node 7df233eb on p22-stage-2-node; shipping with a succession) The payouts a carrier applies are a pure function of (a) the carried shard records of the segment's blocks, in sequence order, (b) the segment's plan (its shard count) and pool credit, (c) the paid map before the carrier, and (d) the rules of spec 7.7 item 6 and 7.8 item 9 (`carried_payouts`, `shard_parts`, `split_pool_credit`). Stage 2 moves (d) into the aggregator guest: the guest takes the carried records' bytes and the prior paid set as inputs, derives the payouts list itself and commits to the derivation's inputs (the records' commitment and the prior paid set's commitment) beside the `inputs` of stage 1, so a proof over an invented payouts list is impossible unless its invented records pass the same checks. @@ -26,6 +26,8 @@ What the code already gives stage 2 (read 19:4x UK): the node's segment record k Tests, known-failed first, on branch `p22-stage-2`: the guest derives the node's payouts for a fixture with carried records (the fixture format gains `carried_records` and `paid_before`); a changed record, a changed paid set or a changed pool credit changes the commitment; the node's `check_segment_record` vetoes a segment statement whose derivation inputs differ from its own; the seven refusals hold unchanged. +What shipped (read from the code, 20:1x UK): the guest's fixture gains `carried` as a list of `CarriedFixture {record, shards, pool_credit, segment_daa, paid_before, valid, v1_active, aggregator_share_bps}` (one per carried record, the per-record split facts the node had in hand when it applied the record); `derive_payouts` runs the node's split natively inside the guest (equal parts with the remainder to shard 0 below the proving v1 switch, the aggregator's share in bps first from it, a record marked invalid or paid before pays nothing); `derivation_commitment` is keccak over the fixture list (the empty list commits to zero; vector 0xe824944e49f90e9dc7734ae5764b1413f9a29407f7b399ea26d6087a14a95baa, pinned by a test in both crates) and closes `ShardOutput` and `BlockOutput` after the stage 1 `inputs` (lengths 360 to 392 and 372 to 404); the aggregator carries it out unchanged. On the node, `BlockStatement.derivation` follows `inputs` (shard statement 328 to 360 inputs, 360 to 392 derivation; segment record 618 to 650), `CarriedRecord` carries the split facts with serde defaults so an older pool still deserialises, `carried_fixtures_of` builds the list the guest saw, and the native check vetoes on "derivation" with the roots and the inputs untouched; the RPC's `carried` block and the exporter carry the fixture fields. What the proof says now: "from these carried records and these split facts, these payouts follow by the rules"; what it still does not say: "these records are valid" (the signatures and the sortition stay native, the stage 3 item). The seven refusals, the succession suite and the stage 1 tests hold unchanged (suites green on build-7 and build-8, 8 October 2026). + ## 3. Stage 3: the rewards derived inside the aggregator from consensus data it verifies (the design by 18:00 UK, 9 October 2026; the code clock from the primitive's cost) The rewards a segment credits are a pure function of consensus data: the mergeset's blocks and which are blue (`SegmentBlock.is_blue`), each block's miner address (from its coinbase payload) and subsidy at its DAA score (`emission_table().block_subsidy`), the silent-builder reading (`finality_silent_builder`, a pure function of the block's past), the signing bonus and the pool split (`silent_split_units`, `split_producer`). Stage 3 moves that derivation into the aggregator guest, which means the guest verifies the headers and coinbase payloads of the mergeset and the facts about them it needs: diff --git a/docs/plans/igneum-2.0-test-registry.json b/docs/plans/igneum-2.0-test-registry.json index b7a5470f8..c4cb5d883 100644 --- a/docs/plans/igneum-2.0-test-registry.json +++ b/docs/plans/igneum-2.0-test-registry.json @@ -1547,9 +1547,9 @@ "manual_page": 24, "owner_lane": "adversary lane (a1a9876a88f5a72fc)", "run_status": "FAIL", - "evidence_path": "docs/analysis/class-v6/connected-state.md", + "evidence_path": "docs/analysis/class-v6/connected-state.md; docs/analysis/class-v6", "run_id": "kills-20261008", - "updated": "2026-10-08T20:23:03.042Z", + "updated": "2026-10-08T20:10:24.624Z", "evidence_record": { "requirement_id": "POW-03", "decision": "FAIL", @@ -1557,7 +1557,7 @@ "cell": "experiment:connected-state", "manifest_sha": "7cfa422a", "run_id": "kills-20261008", - "evidence": "docs/analysis/class-v6/connected-state.md", + "evidence": "docs/analysis/class-v6", "in_progress": false, "coverage": "the experiment ran and its claim failed: live state does not make the work unavoidable for a chip; the master's register row 18 reads FAIL, published, never PASSED", "release_identity": { @@ -1570,7 +1570,7 @@ }, "claim_impact": "the claims the two experiments carried are withdrawn: live state as an ASIC barrier (connected state), a cheaper mixed-FP32 lane", "reviewer": "", - "at": "2026-10-08T20:23:03.042Z" + "at": "2026-10-08T20:10:24.624Z" }, "approvals": { "scope_approved": null, @@ -1608,7 +1608,7 @@ "cell": "experiment:connected-state", "manifest_sha": "7cfa422a", "run_id": "kills-20261008", - "evidence": "docs/analysis/class-v6/connected-state.md", + "evidence": "docs/analysis/class-v6", "in_progress": false, "coverage": "the experiment ran and its claim failed: live state does not make the work unavoidable for a chip; the master's register row 18 reads FAIL, published, never PASSED", "release_identity": { @@ -1621,7 +1621,7 @@ }, "claim_impact": "the claims the two experiments carried are withdrawn: live state as an ASIC barrier (connected state), a cheaper mixed-FP32 lane", "reviewer": "", - "at": "2026-10-08T20:23:03.042Z" + "at": "2026-10-08T20:10:24.624Z" } } }, @@ -1897,9 +1897,9 @@ "manual_page": 26, "owner_lane": "hash lane (a690540514aa453d7)", "run_status": "FAIL", - "evidence_path": "docs/analysis/class-v6/rows/pow-07-fp32-unreachable.md; docs/analysis/class-v6/mixed-fp32.md; docs/analysis/class-v6/rows/pow-07-fp32-unreachable.md", + "evidence_path": "docs/analysis/class-v6/rows/pow-07-fp32-unreachable.md; docs/analysis/class-v6", "run_id": "kills-20261008", - "updated": "2026-10-08T20:23:03.042Z", + "updated": "2026-10-08T20:10:24.624Z", "evidence_record": { "requirement_id": "POW-07", "decision": "FAIL", @@ -1907,7 +1907,7 @@ "cell": "experiment:mixed-fp32", "manifest_sha": "7cfa422a", "run_id": "kills-20261008", - "evidence": "docs/analysis/class-v6/mixed-fp32.md; docs/analysis/class-v6/rows/pow-07-fp32-unreachable.md", + "evidence": "docs/analysis/class-v6", "in_progress": false, "coverage": "the branch ran and its claim failed: the FP32 branch costs the card more energy and widens the chip edge; the master's register row 12 reads FAIL, published; the branch is excluded and unreachable on master (the grep evidence stays)", "release_identity": { @@ -1920,7 +1920,7 @@ }, "claim_impact": "the claims the two experiments carried are withdrawn: live state as an ASIC barrier (connected state), a cheaper mixed-FP32 lane", "reviewer": "", - "at": "2026-10-08T20:23:03.042Z" + "at": "2026-10-08T20:10:24.624Z" }, "approvals": { "scope_approved": null, @@ -1962,7 +1962,7 @@ "cell": "experiment:mixed-fp32", "manifest_sha": "7cfa422a", "run_id": "kills-20261008", - "evidence": "docs/analysis/class-v6/mixed-fp32.md; docs/analysis/class-v6/rows/pow-07-fp32-unreachable.md", + "evidence": "docs/analysis/class-v6", "in_progress": false, "coverage": "the branch ran and its claim failed: the FP32 branch costs the card more energy and widens the chip edge; the master's register row 12 reads FAIL, published; the branch is excluded and unreachable on master (the grep evidence stays)", "release_identity": { @@ -1975,7 +1975,7 @@ }, "claim_impact": "the claims the two experiments carried are withdrawn: live state as an ASIC barrier (connected state), a cheaper mixed-FP32 lane", "reviewer": "", - "at": "2026-10-08T20:23:03.042Z" + "at": "2026-10-08T20:10:24.624Z" } } }, diff --git a/infra/fast-time/override-60x.json b/infra/fast-time/override-60x.json index 7810fc255..d9daa030a 100644 --- a/infra/fast-time/override-60x.json +++ b/infra/fast-time/override-60x.json @@ -92,6 +92,10 @@ "proving_shard_program_id": "", "proving_aggregator_id": "", "verifier_in_consensus": false, + "proving_key_succession_daa": 18446744073709551615, + "proving_key_succession_window_daa": 86400, + "proving_next_shard_program_id": "", + "proving_next_aggregator_id": "", "proving_payment_activation_daa": 18446744073709551615, "sig_scheme": 0, "sig_scheme_activation_daa": 18446744073709551615, diff --git a/infra/fast-time/proving-enforcement.mjs b/infra/fast-time/proving-enforcement.mjs index bbd5c4b7b..608a86fb3 100644 --- a/infra/fast-time/proving-enforcement.mjs +++ b/infra/fast-time/proving-enforcement.mjs @@ -52,9 +52,20 @@ const FLOOR = sflag('floor', '240'); const BEFORE = flag('before', 90), AFTER = flag('after', 150); const SLOT = flag('slot', 0); const REAL_PROOF = sflag('real-proof'); +// Key succession (docs/design/key-succession.md): --succession schedules the next pair at H with --window +// DAA (the honest nodes must embed both pairs: elf/ the next pair, elf/prior/ the prior with the manifest naming it); +// --next-proof is a real proof under the next pair, --real-proof one under the prior pair. The signal is the honest +// nodes' refusal REASON: below H a next-pair proof is refused on its pair ("epoch does not accept"), in the window on +// its statement (another chain's), after H+W a prior-pair proof is refused on its pair. Three forges: below H, in the +// window, after H+W. +const SUCCESSION = sflag('succession'); +const WINDOW = sflag('window', '120'); +const NEXT_PROOF = sflag('next-proof'); +const PRIOR_IDS = sflag('prior-ids'); // "0x,0x" of the prior pair, default the manifest's +const NEXT_IDS = sflag('next-ids'); // "0x,0x" of the next pair const EXPECT = sflag('expect', FLOOR === 'never' ? 'pay' : 'refuse'); const GENESIS_BITS = flag('genesis-bits', 0x1f010000); -const CASE = sflag('case') || `floor-${FLOOR}-expect-${EXPECT}`; +const CASE = sflag('case') || (SUCCESSION ? `succession-${SUCCESSION}-window-${WINDOW}` : `floor-${FLOOR}-expect-${EXPECT}`); const OUT = sflag('out') || `${ROOT}docs/plans/proving-enforcement/${CASE}.json`; // 30890 and up: clear of every other fast-time harness (fork-gate 30690s, nuisance 30490s, headers-proof 30590s) const BASE = 30890 + SLOT * 40, SUFFIX = 985 + SLOT; @@ -98,8 +109,10 @@ writeFileSync(override, mergeOverrideText(baseText, { genesis_bits: GENESIS_BITS, skip_proof_of_work: false, proving_v0_activation_daa: '0', proving_consensus_verify_daa: FLOOR === 'never' ? NEVER : FLOOR, - proving_shard_program_id: manifest.shard.program_id, proving_aggregator_id: manifest.aggregator.program_id, + proving_shard_program_id: PRIOR_IDS ? PRIOR_IDS.split(',')[0] : manifest.shard.program_id, proving_aggregator_id: PRIOR_IDS ? PRIOR_IDS.split(',')[1] : manifest.aggregator.program_id, verifier_in_consensus: 'false', + proving_key_succession_daa: SUCCESSION || NEVER, proving_key_succession_window_daa: WINDOW, + proving_next_shard_program_id: NEXT_IDS ? NEXT_IDS.split(',')[0] : '', proving_next_aggregator_id: NEXT_IDS ? NEXT_IDS.split(',')[1] : '', program_class_v3_activation_daa: NEVER, program_class_v4_activation_daa: NEVER, })); log(`case ${CASE}: floor ${FLOOR} DAA, before ${BEFORE} s, after ${AFTER} s, expect ${EXPECT}, real proof ${REAL_PROOF || 'none'}`); @@ -194,6 +207,11 @@ async function forge(n, phase) { const real = readFileSync(REAL_PROOF); await submit('c-other-program', signRecord('forger-c', CHAIN, block, n, 0, PAYOUT_A, statement, '0x' + sha256(real)).record, hex(real)); } else shapes.push({ shape: 'c-other-program', skipped: 'no --real-proof file' }); + // (h) a real proof under the NEXT pair (key succession): refused on its pair below H, on its statement from H + if (NEXT_PROOF && existsSync(NEXT_PROOF)) { + const nextp = readFileSync(NEXT_PROOF); + await submit('h-next-pair-proof', signRecord('forger-h', CHAIN, block, n, 0, PAYOUT_A, statement, '0x' + sha256(nextp)).record, hex(nextp)); + } else shapes.push({ shape: 'h-next-pair-proof', skipped: 'no --next-proof file' }); // (d) a replayed record await submit('d-replay', recB, hex(wrong)); // (e) a wrong network id @@ -217,7 +235,8 @@ async function observe(n) { } const refusals = (node) => { const t = node.logText(); - return { invalid: (t.match(/IgneumInvalidProofRecord|invalid proof record|proof record .* does not verify|REFUSED/gi) || []).length, dropped: (t.match(/carried proofs did not arrive|did not deliver its carried proofs/g) || []).length }; + return { invalid: (t.match(/IgneumInvalidProofRecord|invalid proof record|proof record .* does not verify|REFUSED/gi) || []).length, dropped: (t.match(/carried proofs did not arrive|did not deliver its carried proofs/g) || []).length, + pair: (t.match(/epoch does not accept|does not embed/g) || []).length, statement: (t.match(/public values hash to/g) || []).length }; }; try { @@ -250,6 +269,35 @@ try { const afterRefusals = { H1: refusals(H1), H2: refusals(H2) }; result.phases.atFloor = { daaAtForge: d, forge: f2, observed: o2, refusalsBefore: beforeRefusals, refusalsAfter: afterRefusals }; log(`at floor: H1 paid ${JSON.stringify(o2.paid)}; carried ${JSON.stringify(o2.carried)}; refusals ${JSON.stringify(afterRefusals)}`); + // key succession: a third forge after the window, and the verdict on the refusal reasons + if (SUCCESSION) { + const h = Number(SUCCESSION), w = Number(WINDOW); + while ((d = await daa(A)) < h + w + 5) { log(`waiting for the window's end: DAA ${d} of ${h + w}`); await sleep(10000); } + tip = await tipNumber(A); + const n3 = Math.max(1, tip - 15); + const r2 = { H1: refusals(H1), H2: refusals(H2) }; + const f3 = await forge(n3, 'after-window'); + await sleep(AFTER * 1000); + const o3 = await observe(n3); + const r3 = { H1: refusals(H1), H2: refusals(H2) }; + result.phases.afterWindow = { daaAtForge: d, forge: f3, observed: o3, refusalsBefore: r2, refusalsAfter: r3 }; + const rb = result.phases.below.refusals, rw = afterRefusals; + // below H: the next-pair proof refused on its pair; in the window: no new pair refusal, both on statements; + // after H+W: the prior-pair proof refused on its pair + const pairBelow = rb.H1.pair > 0; + const pairWindow = rw.H1.pair - rb.H1.pair; + const stmtWindow = rw.H1.statement - rb.H1.statement; + const pairAfter = r3.H1.pair - r2.H1.pair; + const nothingPaid = (o1.paid || []).length === 0 && (o2.paid || []).length === 0 && (o3.paid || []).length === 0; + const pass = pairBelow && pairWindow === 0 && stmtWindow > 0 && pairAfter > 0 && nothingPaid; + result.verdict = { pairBelow, pairWindow, stmtWindow, pairAfter, nothingPaid, pass }; + result.endedAt = new Date().toISOString(); + mkdirSync(OUT.replace(/\/[^/]+$/, ''), { recursive: true }); + writeFileSync(OUT, JSON.stringify(result, null, 2)); + log(`RESULT ${pass ? 'PASS' : 'FAIL'}: below H pair refusals=${rb.H1.pair}; window new pair refusals=${pairWindow}, new statement refusals=${stmtWindow}; after H+W new pair refusals=${pairAfter}; nothing paid=${nothingPaid}; ${OUT}`); + await stopAll(); + process.exit(pass ? 0 : 1); + } // the verdict const paidBelow = (o1.paid || []).length > 0; const paidAt = (o2.paid || []).length > 0; diff --git a/proving/fixtures/d4-block-149-payment-on.json b/proving/fixtures/d4-block-149-payment-on.json new file mode 100644 index 000000000..ee2de4eb8 --- /dev/null +++ b/proving/fixtures/d4-block-149-payment-on.json @@ -0,0 +1,722 @@ +{ + "format": "igneum-prove-fixture-v1", + "source": "V6-10 new fee mode: D4 node 930b6322, proving_payment_activation_daa 0, build-8 fast time 8 October 2026 | the node ran with proving_payment_activation_daa 0 (the daemon wires it to the exec service at start), so the mode is on at this block; the export reads the flag as \"any executed transaction paid\" (igneum/exec/src/rpc.rs), false on a block with no transactions, so it is set here from the params; no charges in this block, the roots are the node's", + "block": { + "chain_id": 4463, + "env": { + "number": 149, + "hash": "0x321a17e37ed6f7d9993aa11e89d150a5d38f7b557ef083741d6519fff7386ecd", + "parent_hash": "0xeb9e2c5823450b8b6eeb4a292aca6979912091f7f118dd90dcd9ce0aa883522f", + "timestamp": 1791489244, + "miner": "0x5b79d55716f8e20499968caa8884df1ae56b6191", + "prevrandao": "0xf8e5d12fa82cee54150a8aa962ddcd44dcc9a6653eea959509c630126fcbfcc4", + "base_fee_exec": 100000000000, + "base_fee_proving": 10000000000000, + "daa_score": 148, + "proving_payment_to_pool": true + }, + "block_hashes": [ + [ + 0, + "0x234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062" + ], + [ + 1, + "0x221cbeb0a723d3095a9c1e95fc53056a815b0b14d871622cbed85eb223af5a42" + ], + [ + 2, + "0xc3c199a33cac0f4c50e1d23014c53a67f77ab625261a119956e31dbeaef9b89f" + ], + [ + 3, + "0x82f25480862df0abc8e21e047a3b7f13ae20a9e866843270ec9cde6254dcd47e" + ], + [ + 4, + "0xbf8d04245e9991151ebf84fdb9d0f8c7f8688aa8b83504286b64f26b732d5b6e" + ], + [ + 5, + "0xe20f8e80ebb455da6f9962cbc772149337d8862f1927b9673d41efd53d0f5d7e" + ], + [ + 6, + "0x942f71390c0351023173156c0ad69b4a7583231482475cbb152933a405e71d5e" + ], + [ + 7, + "0x734b9b846f3293a692998e60d83b60e4dc62ebca6e267581c37a85adc38b9438" + ], + [ + 8, + "0xcd39cd2758208121a9d161a6831809c9d7bc556193631da1e7439efa45d4e663" + ], + [ + 9, + "0x345843066234008221c69422099eb0a32badf82c0c333fef1b3c39e7110d6203" + ], + [ + 10, + "0xfe24cdb370181fc617d219e5ca8afa1a920c55749c2099e8b2aacf21cf82a86b" + ], + [ + 11, + "0x1bcc4c7e85380f853be27bf436e0045aa6a15d9a424f3cd6bebfc51349df35a1" + ], + [ + 12, + "0x5d96ad94e0acaf4ff7332da41159fd6ca00fead438225ad7c41254ed953d3127" + ], + [ + 13, + "0x7e859d4c8816cb35075da105764b76227f5ee2391e6835472f36aeb8ef90d672" + ], + [ + 14, + "0x0c57c6bb26b764ea73eb9d372f307b2107aa2d0e71902edfd158d1bf50d4c575" + ], + [ + 15, + "0x1e8af5f74e4ffd60b1db6f26fffddc3d7f86ddcb1ae099ce763a1fc679b50c99" + ], + [ + 16, + "0xe32ba8af14bc38645304cca3a30f78fd6f57d689d502dfc0731113241981bfb3" + ], + [ + 17, + "0x1908a636b085d84c8a2f0257275c8162c4b7b28b88425148f953253d08d80a7a" + ], + [ + 18, + "0x01695b963042f3fa535895c3725e40eca62e27e44093c827312232be7e1bdb2a" + ], + [ + 19, + "0x0808210b7f83d9702408710d4dd78b4a8c05e4dc2ae6556de67533284bd42217" + ], + [ + 20, + "0xad70636a030a8f0d078297ad34c4db507bf7a9d04ec02a515dab10c55dd4632c" + ], + [ + 21, + "0x3863b26349ca6d735e7f52a63964feabda9e6d65c2da0e7e604cbeb162053807" + ], + [ + 22, + "0x93e5ccef17ac41804f6921af200dd165dc718123f7604c9848b8454e47b2b16c" + ], + [ + 23, + "0x29c21798e9c9643d907e08b8f6e8296f9d40d9eb4599a70051888beacca97ce1" + ], + [ + 24, + "0xa1feb94c32debf0dcf4db4ba58f64dc8b802e129197b4a3fe3cc310a864a8d7f" + ], + [ + 25, + "0xf297c97afcaf1ac99755b4be850764bcab10a8b448af40155b31123d5a7de62a" + ], + [ + 26, + "0x6c2598f9e1427ae7ea5ec003035927874a202c58bedd1e48eb31dbcf997e2da6" + ], + [ + 27, + "0x821131fc80341201aecd92ff7e6edee7cc6be20c68d637f1d354da12b745df6d" + ], + [ + 28, + "0xef2e7aa3d3dc48592370336e5e77f2c56023cf5b6c5201d17d53b3d46131b3c9" + ], + [ + 29, + "0x8b93f0e1d418688a651f246a7b5a7ce05270b79be7ba7b6dfc1d9c3b809f719b" + ], + [ + 30, + "0x7c12d5afc34d54931b294457acc1f327a6b02dc0f1f50aaa4f736fe700047d53" + ], + [ + 31, + "0x8169ae44407f563d232c38dc6bfbaf948f337ed090f90b522a450ba2678c4938" + ], + [ + 32, + "0x92e68cb92d38d371e5a5b5eef82a07f033bae3e65585a138a32ba183dffce896" + ], + [ + 33, + "0x9507faf892cefdb1bfc747795120bac659a04e575973a96e53933bc4253a26b1" + ], + [ + 34, + "0xbcd81de7ee4635a7ad2b18aa880a71198f7713ba3abdb317d497a06439b2908f" + ], + [ + 35, + "0x73b221252997b7eb6f99fb5b3f87ebf33acf11a1768da35a55d3bab037a86e65" + ], + [ + 36, + "0xea5140d95aace8d3cfeb256348c8940c560eeeebe4994c48e054f7bf5a78ee8e" + ], + [ + 37, + "0xcf7122dd60fbbb3e22d0c6581b388969cf6e5322d8e0c13ca6609cfa45909e1b" + ], + [ + 38, + "0x426d989727477e5fe129e605aec6ef43be4b4f6ee9897f6bc72a45605e3fb925" + ], + [ + 39, + "0x25d960ae17b0595ce70542dcd9e965c3b99859fd3490c73b1e1c023081ce8f19" + ], + [ + 40, + "0xc314e5cfcb1f63f887166c05ed89122009d8cd9b1d2ffd22db1bdfd9b29556e3" + ], + [ + 41, + "0x99d8b3111ab080eaf3b6079051b251bea36901cd0d8d6d93a21f7858f5e59dcf" + ], + [ + 42, + "0x028f45615937d064aff75c08e680c31f8b49f48dcc94fd66bdc0b4c2eb52c20a" + ], + [ + 43, + "0x6ccd1e21c7c4bc76eec33fff316d3049ab83f96be41b0bce5afae02decb7dee9" + ], + [ + 44, + "0xa5a8e8c929f3bc7dea980584235bc39e436aff39892916770a7ad63b4f529829" + ], + [ + 45, + "0x05cc11b2caebe985f491586152f9cf25b7b798fb2e872fd8d1923ac81714834c" + ], + [ + 46, + "0xe9e5b40bf1171e2d319129f6ccabac51412a0d3d33806dae79e06724e9cb2f12" + ], + [ + 47, + "0xec0546c1b7aa060caeca28eb1cfc9671db43ec55c3368812c97ad26898f2f5eb" + ], + [ + 48, + "0x5002e246d308393bfa64c4777214acf9ca0bf644abc6962e126178cde53bdfec" + ], + [ + 49, + "0xdceab619e910c2ed56de0f50892833d503b0dc51886c315fc2f853925f4ee323" + ], + [ + 50, + "0xb750c862c81784fcc69c114d68dd9b8202ace11d26d9f4e92b02271b654da640" + ], + [ + 51, + "0x3002afeabe13aaf537d713b1c0df8c687e7cb3cec9ee18764958ff8ec030cce2" + ], + [ + 52, + "0xc5dc75677f3ae41c8a90254ff2275d3ee53ce6ddc790ebc999cf3f21831ec0fd" + ], + [ + 53, + "0x7811259726d0061fee69b1a3de3c330745a59bffa42a72f4280c7d84d8b35e9a" + ], + [ + 54, + "0x3559c78ace1a321ca6798f63d91ce220e226b7dee5326b47b3a68797a16073fc" + ], + [ + 55, + "0x9b5f7d9f5fdef11660b1a7e190bdbeb9f91bbd47202129f84a6b304afddbb6e3" + ], + [ + 56, + "0x363ce48ead2724f957db5dfb52de7a1219219a06ee24a2172fd5fdc2cbc98026" + ], + [ + 57, + "0xfbe9a023a54e54e5cb3f756e1c5d31e21d4e04876e159403b63ccba7de98d64e" + ], + [ + 58, + "0x3810e2219bd00a4ee1e528a1eef0046502a254d3e0516fceb380cd52ec0475a9" + ], + [ + 59, + "0x2b879f9be3080946c188ccbc74e47fe369c65e62ad5f703993d67ab8482e350d" + ], + [ + 60, + "0x912680852e95a8039470c034a3426ce0ac99092f894efdc07d0569acc0ee68be" + ], + [ + 61, + "0x323b796f63bb79d7998fecee10f500c818f2a18090b51a3b4e9b70a872e31aeb" + ], + [ + 62, + "0xaef8090989a572534b086c9cbf8dec26954bd9ee59e2ac0c2bbd370e31442f56" + ], + [ + 63, + "0xbfbaae5f7e4795d7ee72856e1dfe857c55fdc138b3984bc700f7ecc853462d89" + ], + [ + 64, + "0x3ee6b303e95447481b1d51b8e65eee5d02cf78280910621f66f8b6c0b3091c43" + ], + [ + 65, + "0x9f5fe4ae89ea6578121e83f9e7df1fa67f09a3f05e3908f2c17859095759783b" + ], + [ + 66, + "0x5beae24451c5ce9c17d456d6819506ecaf6c2164fe8e68098f0fafe90b7f8cee" + ], + [ + 67, + "0xd72e09b56a83bc3ecc1520440f1148059eefcd78a0e48aed235312f3cd98e06f" + ], + [ + 68, + "0xccaf9e82e89203337309151c826b6c82ddb602346ee6bc554054ab421e85b535" + ], + [ + 69, + "0x7f3f8cc366d32a2a123d8eeb96c153e7ca15e318596cf704505cd572e1c7733b" + ], + [ + 70, + "0x18ef9ca89a4c61b2ba71ce6e9526409efb1c76a7fcfa5a8e9a113cd3b0a04af6" + ], + [ + 71, + "0xab517e39c9ef2470219a12a2b746d4e9312f4a7fd65c2be0522176d06b2c6c41" + ], + [ + 72, + "0xe0f35535b86819a32992753a1363f4d113635b0f444bb689d02cd8e66aa80707" + ], + [ + 73, + "0xc61719ea5f2d1c5e56a0d782c81a085d9f555e24ac20ff2d5decfc6ba83011e8" + ], + [ + 74, + "0xada48c8c2b8e31214a3c2286dd2e9b6f8ec8eb03bae5c51238dd41c69c7bdf87" + ], + [ + 75, + "0xe94bcef04410af1c8b6f55c7ba13f72719ef730de8432080038da3cd6f1a9e3e" + ], + [ + 76, + "0x50bb1833b83e8937853adde3527b06915898d71374f31a4c4e1699900672a91b" + ], + [ + 77, + "0x91b13055b2ec6f62995acd382df77ca51f88eb1c332a7cd726f72d0b831e6ea3" + ], + [ + 78, + "0x231139f6e15a8b5e9c3fb7e7fa2bc7a6ec8a76d4f7e7253803f099817784629a" + ], + [ + 79, + "0xd1ae78c361da13c6f00de4892872be1338bb7c022454e394266f852954ef49e6" + ], + [ + 80, + "0x7ae10ab69d8611676f50aa0af97eaed077be1b716adad6a8278b8f8e83b635d4" + ], + [ + 81, + "0x02fc13dec2a46d0af294750bf0379a5b9d6e767c0432eae3acc8e6a65146c5cf" + ], + [ + 82, + "0xc76f176006bf49c28e002457ac3cc2dec1f1bfc0ba33ad5e2be8035104480028" + ], + [ + 83, + "0x6b7490aae3c24c54ca54b4e10ae685c8924f61356bd766ba12bc844874ad0a85" + ], + [ + 84, + "0xa665984d94ebb130a6a0d9e33196980516eecd9a58394029085f945ef272d1a2" + ], + [ + 85, + "0x06a5158e04c5d770c124214417c7951caa4ac350488e2f477384162239e92879" + ], + [ + 86, + "0x90c4d62415b4e4fa72ed02be8b51a5e3a8d92dd918d7a74ca3f725ab86d8a988" + ], + [ + 87, + "0xa09443f6724fb745ad9520b1f6114c0523e1e580702de21c1e0f984f925d6673" + ], + [ + 88, + "0x52a09140c91ce95eed383dfbd5e262dee59e05842b0bfd7a6e4cc732f31fae1e" + ], + [ + 89, + "0x4a7f9b53762d6ffecbf636ee163495a3d9fc7023ef0482318ddd4406ae38e9c0" + ], + [ + 90, + "0xd9dccd0eba6254643692980127ae7d34d2fb5abf374f668ef13b467fff867dce" + ], + [ + 91, + "0xff4e05fc74c7d648bec6daec7a25126ff1b7cfccbee74c729ea2bd3674db8236" + ], + [ + 92, + "0x3a435e13d47c8931d9ce0ea6c385db34d87e38d8a6bcb4e3c330a6809b94b821" + ], + [ + 93, + "0x9a239ac0c4b4037713e4fc97bfceb650f1c76fc0bee0345b8d82b953e29c9f17" + ], + [ + 94, + "0xc0376dcb0c6450f8aa7c1d3a6a6cea645aa7254597b2f9c0bd889ca0957d402f" + ], + [ + 95, + "0x7c561feade91f497ad9ff352a35fc4de5f4862c84cf2e27a73129af231135680" + ], + [ + 96, + "0x78549006fc84199f177ae2a904e43fae71f33cbd64110abf3c6fc87db8fb32cc" + ], + [ + 97, + "0xba386836847b2b8b36d786c0f274e475484defaaa48ec7d258a08de5466be6bb" + ], + [ + 98, + "0xa62ff07decb42031dff525e3b184616c023bdc9b65f146ac5b4c4faf03b72cd6" + ], + [ + 99, + "0x7b38cb908036ddc3e6a3f0878f40014df53da9c9941c04582ffcb23eef0a8d98" + ], + [ + 100, + "0x8814da87544084069339e09bc7bfa21100c544e8b8c07ae932d447429bd356ad" + ], + [ + 101, + "0x958ed006ff6c5f19abdce9dc547e56b193a5016c08ccb99328d9ddb262a74a8f" + ], + [ + 102, + "0xfbdde51c8ff0ac738e4dc0f58f79dc36e503af813272054ef82640b5fc1b1ec4" + ], + [ + 103, + "0x245ba35d74ecf438659e2fb994a733f07faed4bffff9afd1e02c5292ae49d4bb" + ], + [ + 104, + "0xd125b9ecf9101f740656efee1085d1e605e3ee2d6ba9a239677654386c077962" + ], + [ + 105, + "0xa5a5703a2e8eb45229faa97716a631c1a4f8c24f1a04c78118b830880f17b3bd" + ], + [ + 106, + "0x81313562f149d44b2f50175895747a218acd2e5536925901cbc2aa3f1b035817" + ], + [ + 107, + "0xf3913d1a6ef1a1af99ad61a93ac0d525ab0f690606b3706e5031b84855cf8ea1" + ], + [ + 108, + "0xbbcc1ba572c6c0dd8bc5618447e16d79ac281690ba717df485a52b1d25f98073" + ], + [ + 109, + "0x0fa4607d2ab9207d8c1b1a1deef25cf1ed62747bb661b5b00180fe8bf3f4c87d" + ], + [ + 110, + "0xc00624f4ae86afbffa052ac29fa6baf91e49999fc399067dab879901528062ae" + ], + [ + 111, + "0x0b99cb45957db64d16c3ef303debb35ee18f5e70529e2283b3681b39436b1bf0" + ], + [ + 112, + "0x3029b2e51b3a8b23c9ea61128698e802663eeb9ed34ea9887a61608e707461a2" + ], + [ + 113, + "0x646381a7ee4c0c15e118df93fd6f7f6e31a7ec1df6e5fd39d0bb0d9bf55d2378" + ], + [ + 114, + "0x1671fdf68f9fb890eddb9c2bffc27743a7a7f1b398caa5b5ecb76314d210e836" + ], + [ + 115, + "0xdd3de2c897c46e40dc506d8d23f9122d5892a9be27c450f61502de9876479f06" + ], + [ + 116, + "0xa3482930283bc0acaea62ee4b21ed9ed202338e52978852ca5ed9394893bc8f9" + ], + [ + 117, + "0x43dd565bfd5a836c9764ea6f1cb2543ba69b0200c780ec4a59fab800914831d8" + ], + [ + 118, + "0xb2d9b3542361c82bfac995c0cf5cf78bd7a78ad13af6a4cb60b24fb1396659c6" + ], + [ + 119, + "0xe09e973d443771a5d9e5c2fc653e27ee315b646aa323b420f94b912b6b7e231e" + ], + [ + 120, + "0x5e30fc687eb671f915084545c33461b3ab7d0181808793fb1dace6cfb79556ec" + ], + [ + 121, + "0x9e7f4e7ec04dc38731b38a8a2f655e3206f04f5271b70b21e8a303d794039acf" + ], + [ + 122, + "0x9ee37892a2f4fdfecea01dc69baf07fed2163a165bfb3cb235e17f6e4cc44e4e" + ], + [ + 123, + "0x009b565cfed38faf32445d63b036ab3669cf464f00c00fe1090e36befe71f43b" + ], + [ + 124, + "0xfbacc82e004ac4403ab35c3dec6ddd8143e0f8ac6ae1d39913245dec393e290f" + ], + [ + 125, + "0xb56739e2949bcf46a299bd8bac0c2e9b5d592c2b9b461aef0343f1932e51f54e" + ], + [ + 126, + "0x859368cd5f7b4a925052dcf60d624dfc3bfdd76d8350b4604af631d965f8874f" + ], + [ + 127, + "0x5d47ec97ac034dbe38855c699cfd10b8904d4e4fc4795a9dd23d9ac6b1614d91" + ], + [ + 128, + "0x9ec1013565cb7ee4cb444553054890b9caaf2870aeaaacf112438a490e0588c3" + ], + [ + 129, + "0xff1cb575baab1b3557afb2ead8218bc7f1d50fe4d0097838b2d0498c4138da80" + ], + [ + 130, + "0xe10040aec19a92af7f9174b11a54efd7dae8006d4c4e57ed92dd005e7965c62b" + ], + [ + 131, + "0xd6b786d4ca5c616c52fad352e2cbbcb70e431e69f9b7fb6505ea37c878f55a55" + ], + [ + 132, + "0xc74f0b84230578d7612a92a1b5a92542142272387a51a4749f8244baac09e950" + ], + [ + 133, + "0x43ec7b82b580ea996e02079c8d6271ca73a20f089f3bad0a48f48940b29603e7" + ], + [ + 134, + "0x0ee1df79299be064bfc52da878b6d52a4430e21908d10ec31cc22ddb8d8ad36c" + ], + [ + 135, + "0x383ab3177dfb3de3b60008fc1c5c248751a2f889a6a5cbd14ca2186d885169e1" + ], + [ + 136, + "0xb38ca1561537ce000ce812cf3a4094e53992ec8918e5cbe5e22cd24b5e62e9fb" + ], + [ + 137, + "0x683634e521a4f563ab9cb0b90b99a3ea952d40659db7e396c2776ee1dc7713a1" + ], + [ + 138, + "0x5be2ead070d4f5a6cb98750f7ac4d1fdd7f81cf6f0870053eb80ff2b8af1ccba" + ], + [ + 139, + "0xfb9162b512ba511d858cb7bfd842e0b501ff448b25afd57fc6bd6e8f93f18933" + ], + [ + 140, + "0xb873564a05e7f15a6d163af314231934f345314ff65121804d59a2aa30dbfbdd" + ], + [ + 141, + "0x20c6013c53fa42b68bc2dbf89c009bcfc6783302225ef1d20173111e4c1e353b" + ], + [ + 142, + "0xd298187b6801e6f37a4cec970ff44b8e2eec80943b43ea5ab8f5d1b5f0147b14" + ], + [ + 143, + "0x7fe59edf5dee62bdc3dc220710b8bd9605ee60ba1f72b89e2f4e47231e57f25f" + ], + [ + 144, + "0x9bb1a74380fc41b90d500311dba597fd40b117b0ef1d43b59b8b5197b1e3f2e2" + ], + [ + 145, + "0x4b7d6c0ec7f681a5eb2cdc13e9276c1c36660b979c015f1bf293a3655a6ebe59" + ], + [ + 146, + "0xc563186a2d3b1906acf67b69b81fa2225c9aa76ada4ccd1d5872d448d5bb5dab" + ], + [ + 147, + "0x7239137996f2473b41dd217003aced19ba2b751d9844d2c4dc905308e39f49be" + ], + [ + 148, + "0xeb9e2c5823450b8b6eeb4a292aca6979912091f7f118dd90dcd9ce0aa883522f" + ] + ], + "rewards": [ + [ + "0x5b79d55716f8e20499968caa8884df1ae56b6191", + "0x2332ecab26f28000" + ] + ], + "proving_pool_credit": "0x8ccbb2875b0bc00", + "payouts": [], + "carried": [], + "blocks": [ + { + "miner": "0x5b79d55716f8e20499968caa8884df1ae56b6191", + "blue": true, + "txs": [] + } + ], + "pre_state": [ + { + "address": "0x0000000000000000000000000000000000000210", + "nonce": 1, + "balance": "0x0", + "code": "0x608060405234801561000f575f5ffd5b506004361061003f575f3560e01c8063aa67735414610043578063dea5c2e014610058578063fe7e05d51461009f575b5f5ffd5b6100566100513660046101e0565b6100ca565b005b610083610066366004610211565b6001600160a01b039081165f908152600160205260409020541690565b6040516001600160a01b03909116815260200160405180910390f35b6100836100ad366004610211565b6001600160a01b039081165f908152602081905260409020541690565b336001600160a01b03831614806100f957506001600160a01b038281165f908152600160205260409020541633145b6101635760405162461bcd60e51b815260206004820152603160248201527f446576656c6f70657252656769737472793a206e6f7420746865206163636f75604482015270373a1037b91034ba399031b932b0ba37b960791b606482015260840160405180910390fd5b6001600160a01b038281165f818152602081815260409182902080546001600160a01b031916948616948517905590513381527fa47563c41dab010f91a8ef9dc7ac2bcdfa0ef2af697e575048e71e6eec60dda3910160405180910390a35050565b80356001600160a01b03811681146101db575f5ffd5b919050565b5f5f604083850312156101f1575f5ffd5b6101fa836101c5565b9150610208602084016101c5565b90509250929050565b5f60208284031215610221575f5ffd5b61022a826101c5565b939250505056fea2646970667358221220cbf48f5aa6f911f83b3c2b09adf8c418f5da6fc530c3d5db9d4ba0be24c4bf5864736f6c63430008250033", + "storage": [] + }, + { + "address": "0x0000000000000000000000000000000000000220", + "nonce": 0, + "balance": "0x51606008a031aa000", + "code": "0x", + "storage": [] + }, + { + "address": "0x5b79d55716f8e20499968caa8884df1ae56b6191", + "nonce": 0, + "balance": "0x1411b82c9f0243fc00", + "code": "0x", + "storage": [] + }, + { + "address": "0xdfaea67368f3e3753397d878f97efe6aa8020c2e", + "nonce": 0, + "balance": "0x465fd8438815b400", + "code": "0x", + "storage": [] + } + ], + "fees": { + "base": { + "pgas": { + "version": 1, + "cycles_per_pgas": 1000, + "intrinsic_pgas_per_tx": 300, + "modexp_base": 10, + "modexp_per_byte_numer": 1, + "modexp_per_byte_denom": 10 + }, + "block_proving_gas_limit": 120000, + "shard_proving_gas_budget": 30000, + "min_execution_base_fee_wei": 100000000000, + "min_proving_base_fee_wei": 10000000000000, + "initial_execution_base_fee_wei": 100000000000, + "initial_proving_base_fee_wei": 10000000000000, + "base_fee_change_denominator": 8 + }, + "v1_activation_daa": 0 + } + }, + "plan": { + "shard_budget": 30000, + "consensus": true, + "shards": [ + { + "index": 0, + "tx_start": 0, + "tx_end": 0, + "over_budget": false, + "pre_root": "0x12848a00fd24bd6b3c2c734c597d5e8eabfabbeab1ce539a2a72812dd5f20b79", + "post_root": "0x561134497bd3311a601040f06849f09ea9940de298b0487647a07b52cd93f61d", + "receipts_root": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "link_in": "0x38046159e1bf364d16df7645adc5a18f254dd70ffab279ed30785b727b52a3d5", + "link_out": "0x38046159e1bf364d16df7645adc5a18f254dd70ffab279ed30785b727b52a3d5", + "gas_used": 0, + "pgas_used": 0, + "executed": 0, + "skipped": 0, + "witness": [ + 2, + 0, + 2, + 2, + 8249 + ] + } + ] + }, + "expected": { + "pre_state_root": "0x12848a00fd24bd6b3c2c734c597d5e8eabfabbeab1ce539a2a72812dd5f20b79", + "post_state_root": "0x561134497bd3311a601040f06849f09ea9940de298b0487647a07b52cd93f61d", + "receipts_root": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "tx_commitment": "0x0000000000000000000000000000000000000000000000000000000000000000", + "gas_used": 0, + "pgas_used": 0, + "executed": 0, + "skipped": 0, + "node_state_root": "0x561134497bd3311a601040f06849f09ea9940de298b0487647a07b52cd93f61d" + } +} \ No newline at end of file diff --git a/proving/igneum-prove/core/src/agg.rs b/proving/igneum-prove/core/src/agg.rs index 8f8277880..d358e6233 100644 --- a/proving/igneum-prove/core/src/agg.rs +++ b/proving/igneum-prove/core/src/agg.rs @@ -24,6 +24,8 @@ pub struct PrevLink { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AggInput { + /// The guest input format (V6-10): the first field, checked by `aggregate` first (`shard::GUEST_INPUT_FORMAT`). + pub format: u32, pub shard_vk: [u32; 8], /// The shard proofs' public values, shard order. pub shards: Vec>, @@ -54,10 +56,15 @@ pub struct BlockOutput { pub agg_vk: B256, /// Blocks attested by this proof: 1, or the previous proof's count plus one. pub chain_len: u64, + /// P22 stage 1: shard 0's inputs commitment (the rewards, the pool credit and the payouts the segment applied), + /// carried out so every node can veto a proof over other inputs than the ones it derived. + pub inputs: B256, + /// P22 stage 2: shard 0's derivation commitment (the carried records the payouts were derived from), carried out. + pub derivation: B256, } impl BlockOutput { - pub const LEN: usize = 8 + 8 + 32 + 32 + 4 + 32 * 4 + 8 + 8 + 4 + 4 + 32 * 3 + 8; + pub const LEN: usize = 8 + 8 + 32 + 32 + 4 + 32 * 4 + 8 + 8 + 4 + 4 + 32 * 3 + 8 + 32 + 32; pub fn to_bytes(&self) -> Vec { let mut v = Vec::with_capacity(Self::LEN); @@ -77,6 +84,8 @@ impl BlockOutput { v.extend_from_slice(w.as_slice()); } v.extend_from_slice(&self.chain_len.to_be_bytes()); + v.extend_from_slice(self.inputs.as_slice()); + v.extend_from_slice(self.derivation.as_slice()); debug_assert_eq!(v.len(), Self::LEN); v } @@ -106,6 +115,8 @@ impl BlockOutput { shard_vk: b256_at(268), agg_vk: b256_at(300), chain_len: u64_at(332), + inputs: b256_at(340), + derivation: b256_at(372), }) } } @@ -114,6 +125,7 @@ impl BlockOutput { /// guest and a no-op or a real verification on the host; everything else is checked here and panics on any /// inconsistency, which makes the proof impossible. pub fn aggregate(input: &AggInput, verify: &mut dyn FnMut(&[u32; 8], &[u8])) -> BlockOutput { + crate::shard::check_input_format(input.format).expect("the aggregator input format is this guest's"); assert!(!input.shards.is_empty(), "a block has at least one shard"); let mut receipts = Vec::with_capacity(32 * input.shards.len()); let mut provers = Vec::with_capacity(20 * input.shards.len()); @@ -142,6 +154,11 @@ pub fn aggregate(input: &AggInput, verify: &mut dyn FnMut(&[u32; 8], &[u8])) -> } receipts.extend_from_slice(s.receipts_root.as_slice()); provers.extend_from_slice(s.prover.as_slice()); + // P22 stage 1: only the first shard applies the segment's inputs; every other shard's commitment is zero + if i > 0 { + assert_eq!(s.inputs, B256::ZERO, "shard {i} carries an inputs commitment; only shard 0 applies the segment's inputs"); + assert_eq!(s.derivation, B256::ZERO, "shard {i} carries a derivation commitment; only shard 0 derives the payouts"); + } gas += s.gas_used; pgas += s.pgas_used; executed += s.executed; @@ -187,5 +204,7 @@ pub fn aggregate(input: &AggInput, verify: &mut dyn FnMut(&[u32; 8], &[u8])) -> shard_vk, agg_vk, chain_len, + inputs: first.inputs, + derivation: first.derivation, } } diff --git a/proving/igneum-prove/core/src/fixture.rs b/proving/igneum-prove/core/src/fixture.rs index 8e3da24ca..33e0a5233 100644 --- a/proving/igneum-prove/core/src/fixture.rs +++ b/proving/igneum-prove/core/src/fixture.rs @@ -33,6 +33,30 @@ pub struct FixtureEnv { pub proving_payment_to_pool: bool, } +/// P22 stage 2: one carried shard record as the derivation sees it. The record is its 274-byte wire form +/// (`kaspa_consensus_core::proving::ProofRecord`); the segment facts are what the node's `check_record` read for it. +/// The guest does not verify the record's signature or its sortition (no BLS verifier in the guest yet): it commits +/// to the bytes and derives the payouts by the rules, and every node checks natively that the same records give the +/// same list (`igneum_exec::proving::carried_payouts` is the native check). +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct CarriedFixture { + /// The record's wire bytes, hex in the file. + pub record: Bytes, + /// The record's segment: its shard count, its pool credit in wei and its DAA score (the v1 split switch). + pub shards: u32, + pub pool_credit: U256, + pub segment_daa: u64, + /// Whether the record's shard was already paid before this carrier (the node's paid map). + pub paid_before: bool, + /// The split in force for the record's segment: proving v1 active at the segment (the aggregator's share taken + /// first) and that share in bps (the object's `proving_v1_aggregator_share_bps`). + pub v1_active: bool, + pub aggregator_share_bps: u64, + /// Whether the node's native checks held the record valid (the signature, the sortition, the native statement, the + /// window): a record the node rejected pays nothing, and the guest carries the node's reading as data. + pub valid: bool, +} + /// One block of the segment in sequence order: its miner (the beneficiary of its transactions) and its /// transactions in body order. `blue` blocks earned a reward (carried separately in `rewards`). #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -66,6 +90,11 @@ pub struct BlockFixture { /// Consensus data (from the proof records the segment's blocks carry), applied by shard 0 after the rewards. #[serde(default)] pub payouts: Vec<(Address, U256)>, + /// P22 stage 2 (`docs/design/consensus-proof-stages.md` section 2): the shard records this carrier's segment + /// carried, in sequence order, with what the derivation needs of each one's segment; the guest derives `payouts` + /// from them by the rules and commits to them. Empty for a carrier that carried none (then `payouts` is empty). + #[serde(default)] + pub carried: Vec, pub blocks: Vec, pub pre_state: Vec, /// The fee schedule the node ran (its base set and `fees_v1_activation_daa`), read at `env.daa_score`. A diff --git a/proving/igneum-prove/core/src/shard.rs b/proving/igneum-prove/core/src/shard.rs index 56043b0df..7b4505bd1 100644 --- a/proving/igneum-prove/core/src/shard.rs +++ b/proving/igneum-prove/core/src/shard.rs @@ -6,12 +6,17 @@ use crate::config::FeeSchedule; use crate::executor::{execute_range, Carry, ShardTx}; use crate::fixture::FixtureEnv; use crate::witness::{self, StateWitness}; -use alloy_primitives::{Address, B256, U256}; +use alloy_primitives::{keccak256, Address, B256, U256}; use serde::{Deserialize, Serialize}; /// What the shard guest reads. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ShardInput { + /// The guest input format (review B V6-10, 8 October 2026): the first field of every guest input, checked by + /// `shard_statement` before anything else, so a host and a guest built from different input layouts refuse + /// each other at the first byte instead of proving garbage. `GUEST_INPUT_FORMAT` moves with every layout change + /// (1: proving v0; 2: the D4 proving payment flag and the stage 1 inputs; 3: the stage 2 carried fixtures). + pub format: u32, pub chain_id: u64, pub env: FixtureEnv, pub block_hashes: Vec<(u64, B256)>, @@ -21,6 +26,9 @@ pub struct ShardInput { /// Applied by shard 0 only, after the rewards (proving v0 payouts, spec 7.7). #[serde(default)] pub payouts: Vec<(Address, U256)>, + /// P22 stage 2: the carried records `payouts` were derived from (shard 0 commits to them and asserts the derivation). + #[serde(default)] + pub carried: Vec, pub shard_index: u32, pub txs: Vec, pub carry_in: Carry, @@ -53,10 +61,16 @@ pub struct ShardOutput { pub executed: u32, pub skipped: u32, pub prover: Address, + /// P22 stage 1: shard 0's `inputs_commitment` over the rewards, the pool credit and the payouts it applied; zero + /// for every other shard. The aggregator carries shard 0's out; every node recomputes it. + pub inputs: B256, + /// P22 stage 2: shard 0's `derivation_commitment` over the carried records the payouts were derived from; zero + /// for every other shard and for a segment that carried none. + pub derivation: B256, } impl ShardOutput { - pub const LEN: usize = 8 + 8 + 32 + 4 + 4 + 4 + 32 * 7 + 8 + 8 + 4 + 4 + 20; + pub const LEN: usize = 8 + 8 + 32 + 4 + 4 + 4 + 32 * 7 + 8 + 8 + 4 + 4 + 20 + 32 + 32; pub fn to_bytes(&self) -> Vec { let mut v = Vec::with_capacity(Self::LEN); @@ -74,6 +88,8 @@ impl ShardOutput { v.extend_from_slice(&self.executed.to_be_bytes()); v.extend_from_slice(&self.skipped.to_be_bytes()); v.extend_from_slice(self.prover.as_slice()); + v.extend_from_slice(self.inputs.as_slice()); + v.extend_from_slice(self.derivation.as_slice()); debug_assert_eq!(v.len(), Self::LEN); v } @@ -104,12 +120,29 @@ impl ShardOutput { executed: u32_at(300), skipped: u32_at(304), prover: Address::from_slice(&b[308..328]), + inputs: b256_at(328), + derivation: b256_at(360), }) } } /// The statement, as the guest runs it and as the host checks it natively first. +/// The guest input format this guest and host were built for (see `ShardInput::format`). +pub const GUEST_INPUT_FORMAT: u32 = 3; + +/// Refuses a guest input of another format (V6-10): the check every guest runs first. +pub fn check_input_format(format: u32) -> Result<(), String> { + if format == GUEST_INPUT_FORMAT { Ok(()) } else { Err(format!("guest input format {format} is not this guest's {GUEST_INPUT_FORMAT}: host and guest were built from different input layouts")) } +} + pub fn shard_statement(input: &ShardInput) -> ShardOutput { + check_input_format(input.format).expect("the shard input format is this guest's"); + // P22 stage 2: shard 0 applies only payouts it derived itself from the carried records (a host that feeds another + // list makes the proof impossible); the schedule's v1 switch and the aggregator share are the fee schedule's + if input.shard_index == 0 && !input.carried.is_empty() { + let derived = derive_payouts(&input.carried); + assert_eq!(derived, input.payouts, "the payouts are not the ones the carried records derive"); + } let (mut db, pre_root) = witness::load(&input.witness, &input.block_hashes); let rewards = (input.shard_index == 0).then_some((&input.rewards[..], input.proving_pool_credit, &input.payouts[..])); let out = execute_range(&mut db, input.chain_id, &input.env, &input.fees, rewards, &input.txs, input.carry_in, false); @@ -133,9 +166,107 @@ pub fn shard_statement(input: &ShardInput) -> ShardOutput { executed: out.executed.len() as u32, skipped: out.skipped.len() as u32, prover: input.prover, + inputs: if input.shard_index == 0 { inputs_commitment(&input.rewards, input.proving_pool_credit, &input.payouts) } else { B256::ZERO }, + derivation: if input.shard_index == 0 { derivation_commitment(&input.carried) } else { B256::ZERO }, } } +/// P22 stage 2: the payouts a carrier applies, derived from the records it carried by the rules of spec 7.7 item 6 +/// and 7.8 item 9 (`igneum_exec::proving::carried_payouts` byte for byte, minus the checks the guest cannot run, +/// which the fixture carries as `valid`): in sequence order, the first valid record per shard not already paid pays +/// that shard's part of its segment's pool credit (equal parts with the remainder to shard 0 below the proving v1 +/// switch, the aggregator's share taken first from it), to the record's payout address. +pub fn derive_payouts(carried: &[crate::fixture::CarriedFixture]) -> Vec<(Address, U256)> { + let mut paid: std::collections::HashSet<(B256, u32)> = std::collections::HashSet::new(); + let mut out = Vec::new(); + for c in carried { + let b = c.record.as_ref(); + // the wire form: version u16 | block 32 | number u64 | shard u32 | pubkey 48 | payout 20 | ... + if b.len() < 2 + 32 + 8 + 4 + 48 + 20 { + continue; + } + let block = B256::from_slice(&b[2..34]); + let shard = u32::from_le_bytes(b[42..46].try_into().unwrap()); + let payout = Address::from_slice(&b[94..114]); + if !c.valid || c.paid_before || !paid.insert((block, shard)) { + continue; + } + let credit: u128 = c.pool_credit.try_into().unwrap_or(u128::MAX); + let parts = if c.v1_active { split_pool_credit(credit, c.shards, c.aggregator_share_bps).0 } else { shard_payouts(credit, c.shards) }; + let wei = parts.get(shard as usize).copied().unwrap_or(0); + if wei > 0 { + out.push((payout, U256::from(wei))); + } + } + out +} + +/// The per-shard split below the proving v1 switch: equal parts, the remainder to shard 0 (`kaspa_consensus_core::proving::shard_payouts`). +pub fn shard_payouts(pool_credit_wei: u128, shards: u32) -> Vec { + if shards == 0 { + return Vec::new(); + } + let n = shards as u128; + let each = pool_credit_wei / n; + let mut out = vec![each; shards as usize]; + out[0] += pool_credit_wei - each * n; + out +} + +/// The split from the proving v1 switch: the aggregator's share in bps first, the rest in shard parts (`kaspa_consensus_core::proving::split_pool_credit`). +pub fn split_pool_credit(pool_credit_wei: u128, shards: u32, aggregator_share_bps: u64) -> (Vec, u128) { + let bps = aggregator_share_bps.min(10_000) as u128; + let aggregator = pool_credit_wei / 10_000 * bps + (pool_credit_wei % 10_000) * bps / 10_000; + (shard_payouts(pool_credit_wei - aggregator, shards), aggregator) +} + +/// P22 stage 2: the commitment over the derivation's inputs: keccak-256 over `u32 count || (u32 len || record bytes +/// || u32 shards || credit as 32 bytes || u64 segment_daa || u8 paid_before || u8 valid || u8 v1_active || u64 +/// aggregator_share_bps)*`, every integer big-endian. +/// Zero for an empty list. Byte for byte `igneum_exec::proving::derivation_commitment`. +pub fn derivation_commitment(carried: &[crate::fixture::CarriedFixture]) -> B256 { + if carried.is_empty() { + return B256::ZERO; + } + let mut v = Vec::new(); + v.extend_from_slice(&(carried.len() as u32).to_be_bytes()); + for c in carried { + v.extend_from_slice(&(c.record.len() as u32).to_be_bytes()); + v.extend_from_slice(c.record.as_ref()); + v.extend_from_slice(&c.shards.to_be_bytes()); + v.extend_from_slice(&c.pool_credit.to_be_bytes::<32>()); + v.extend_from_slice(&c.segment_daa.to_be_bytes()); + v.push(c.paid_before as u8); + v.push(c.valid as u8); + v.push(c.v1_active as u8); + v.extend_from_slice(&c.aggregator_share_bps.to_be_bytes()); + } + keccak256(v) +} +/// P22 stage 1 (`docs/spec/proving-enforcement.md` section 7; 8 October 2026): the commitment over the inputs a +/// segment's first shard applied before its first transaction: the rewards list, the proving pool credit and the +/// payouts list, in order. keccak-256 over `u32 count || (address || wei as 32 bytes)* || credit as 32 bytes || +/// u32 count || (address || wei)*`, every integer big-endian. Shard 0 carries it in its public values, the +/// aggregator carries shard 0's out, and every node recomputes it from the rewards and payouts consensus derived +/// and vetoes a statement whose commitment differs. The node's `igneum_exec::proving::inputs_commitment` is this +/// function byte for byte (one test vector in both). +pub fn inputs_commitment(rewards: &[(Address, U256)], proving_pool_credit: U256, payouts: &[(Address, U256)]) -> B256 { + let mut v = Vec::with_capacity(8 + 52 * (rewards.len() + payouts.len()) + 32); + v.extend_from_slice(&(rewards.len() as u32).to_be_bytes()); + for (a, w) in rewards { + v.extend_from_slice(a.as_slice()); + v.extend_from_slice(&w.to_be_bytes::<32>()); + } + v.extend_from_slice(&proving_pool_credit.to_be_bytes::<32>()); + v.extend_from_slice(&(payouts.len() as u32).to_be_bytes()); + for (a, w) in payouts { + v.extend_from_slice(a.as_slice()); + v.extend_from_slice(&w.to_be_bytes::<32>()); + } + keccak256(v) +} + + /// One planned shard with its witness, as a full node builds them (design 5.1): the plan from the native trace, /// the witness from the access log of the shard's native execution, and the native statement for the check. pub struct BuiltShard { @@ -166,12 +297,14 @@ pub fn build_shards(block: &crate::fixture::BlockFixture, budget: u64, prover: A let log = state.take_log(); let witness = witness::generate(&pre, &log); let input = ShardInput { + format: GUEST_INPUT_FORMAT, chain_id: block.chain_id, env: block.env.clone(), block_hashes: block.block_hashes.clone(), rewards: if spec.index == 0 { block.rewards.clone() } else { Vec::new() }, proving_pool_credit: if spec.index == 0 { block.proving_pool_credit } else { U256::ZERO }, payouts: if spec.index == 0 { block.payouts.clone() } else { Vec::new() }, + carried: if spec.index == 0 { block.carried.clone() } else { Vec::new() }, shard_index: spec.index, txs: range.to_vec(), carry_in: spec.carry_in, diff --git a/proving/igneum-prove/export/src/main.rs b/proving/igneum-prove/export/src/main.rs index 51d7c8e5a..02a3c64ef 100644 --- a/proving/igneum-prove/export/src/main.rs +++ b/proving/igneum-prove/export/src/main.rs @@ -135,6 +135,18 @@ fn fixture_of(export: &Value, segments: &[Value], n: usize, hashes: &[(u64, B256 let proving_pool_credit: U256 = seg["provingPoolCredit"].as_str().context("provingPoolCredit")?.parse()?; // proving v0 payouts (spec 7.7); an export from a node before proving v0 has none let payouts = seg["payouts"].as_array().map(|a| a.iter().map(|r| Ok((addr(&r["address"])?, u256(&r["wei"])?))).collect::>>()).transpose()?.unwrap_or_default(); + // P22 stage 2: the carried records the payouts were derived from, with the segment facts the derivation reads (an + // export from a node before the field carries none; then the fixture's payouts are data, as stage 1) + let carried = seg["carried"].as_array().map(|a| a.iter().filter(|c| c.get("record").is_some()).map(|c| Ok(igneum_prove_core::fixture::CarriedFixture { + record: Bytes::from(hex::decode(c["record"].as_str().context("carried record")?.trim_start_matches("0x"))?), + shards: c["segmentShards"].as_u64().context("segmentShards")? as u32, + pool_credit: c["segmentPoolCredit"].as_str().context("segmentPoolCredit")?.parse()?, + segment_daa: u64_of(&c["segmentDaa"])?.unwrap_or(0), + paid_before: c["paidBefore"].as_bool().unwrap_or(false), + valid: c["validForDerivation"].as_bool().unwrap_or(false), + v1_active: c["v1Active"].as_bool().unwrap_or(false), + aggregator_share_bps: u64_of(&c["aggregatorShareBps"])?.unwrap_or(0), + })).collect::>>()).transpose()?.unwrap_or_default(); let pre_state = db .addresses() .into_iter() @@ -148,6 +160,7 @@ fn fixture_of(export: &Value, segments: &[Value], n: usize, hashes: &[(u64, B256 rewards, proving_pool_credit, payouts, + carried, blocks: blocks_of(seg)?, pre_state, fees, diff --git a/proving/igneum-prove/host/src/bin/pin.rs b/proving/igneum-prove/host/src/bin/pin.rs index 29ec4e7c6..1bd6aa645 100644 --- a/proving/igneum-prove/host/src/bin/pin.rs +++ b/proving/igneum-prove/host/src/bin/pin.rs @@ -95,12 +95,21 @@ fn main() -> Result<()> { let light = LightProver::new(); let shard = pin_one(&light, &from, &out, "igneum-prove-program")?; let aggregator = pin_one(&light, &from, &out, "igneum-prove-aggregator")?; + // V6-10 (review B, 8 October 2026): the manifest names the source it was built from and the guest input format, + // so source, ELF and wire layout are pinned together; a `prior` pair already in the manifest (key succession: + // docs/design/key-succession.md) is carried over unchanged unless --no-prior drops it. + let source_commit = std::env::var("IGNEUM_PIN_SOURCE_COMMIT").ok().filter(|v| !v.is_empty()).or_else(|| { + std::process::Command::new("git").args(["-C", here.to_str().unwrap_or("."), "rev-parse", "HEAD"]).output().ok() + .filter(|o| o.status.success()).map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) + }).unwrap_or_else(|| "unknown".into()); let mut manifest = serde_json::json!({ "format": "igneum-prove-elf-manifest-v1", "sp1_crate_version": "6.8.1", "sp1_circuit_version": sp1_sdk::SP1_CIRCUIT_VERSION, "pinned_at": now_utc(), "pinned_on": host, + "source_commit": source_commit, + "guest_input_format": igneum_prove_core::shard::GUEST_INPUT_FORMAT, "shard": shard, "aggregator": aggregator, }); diff --git a/proving/igneum-prove/host/src/main.rs b/proving/igneum-prove/host/src/main.rs index 98e45f3fe..f34647c11 100644 --- a/proving/igneum-prove/host/src/main.rs +++ b/proving/igneum-prove/host/src/main.rs @@ -222,7 +222,7 @@ fn run() -> Result<()> { if prev_root != outcome.state_root || sum_gas != outcome.gas_used || sum_pgas != outcome.pgas_used { bail!("the shards do not chain to the block's post-root or do not sum to its gas and pgas"); } - let native_block = agg::aggregate(&AggInput { shard_vk: [0; 8], shards: shards.iter().map(|s| s.output.to_bytes()).collect(), parent_hash: block.env.parent_hash, prev: None }, &mut |_, _| {}); + let native_block = agg::aggregate(&AggInput { format: igneum_prove_core::shard::GUEST_INPUT_FORMAT, shard_vk: [0; 8], shards: shards.iter().map(|s| s.output.to_bytes()).collect(), parent_hash: block.env.parent_hash, prev: None }, &mut |_, _| {}); if native_block.post_root != outcome.state_root || native_block.tx_commitment != outcome.tx_commitment || native_block.gas_used != outcome.gas_used { bail!("the native aggregation does not reproduce the block"); } @@ -953,10 +953,78 @@ mod fee_switch_tests { assert_eq!(shards.len(), f.plan.shards.len()); for (s, x) in shards.iter().zip(&f.plan.shards) { assert_eq!((s.output.pre_root, s.output.post_root, s.output.pgas_used, s.output.gas_used), (x.pre_root, x.post_root, x.pgas_used, x.gas_used), "shard {}", x.index); - assert_eq!(s.output.to_bytes().len(), 328, "the statement layout is unchanged"); + assert_eq!(s.output.to_bytes().len(), 392, "the statement layout: 328 bytes plus the P22 stage 1 inputs and stage 2 derivation commitments"); } } + /// V6-10 (review B, 8 October 2026), known-failed first: a guest input of another format is refused at the first + /// field by both guests; this guest's own format passes and the native run is unchanged. + #[test] + fn v6_10_another_guest_input_format_is_refused_before_anything_else() { + use igneum_prove_core::agg::{aggregate, AggInput}; + use igneum_prove_core::shard::{check_input_format, GUEST_INPUT_FORMAT}; + assert!(check_input_format(GUEST_INPUT_FORMAT - 1).unwrap_err().contains("guest input format")); + assert!(check_input_format(GUEST_INPUT_FORMAT + 1).is_err() && check_input_format(0).is_err() && check_input_format(GUEST_INPUT_FORMAT).is_ok()); + assert_eq!(GUEST_INPUT_FORMAT, 3, "1 proving v0, 2 the D4 flag and the stage 1 inputs, 3 the stage 2 carried fixtures"); + let f = load("fees-v1-shards2.json"); + let prover = alloy_primitives::Address::from_slice(&[0x19; 20]); + let (_, _, shards) = build_shards(&f.block, f.plan.shard_budget, prover); + let mut input = shards[0].input.clone(); + assert_eq!(input.format, GUEST_INPUT_FORMAT, "the host stamps every shard input with its format"); + input.format = GUEST_INPUT_FORMAT - 1; + assert!(std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| shard_statement(&input))).is_err(), "a shard input of another format is refused"); + let mut agg_in = AggInput { format: GUEST_INPUT_FORMAT - 1, shard_vk: [0; 8], shards: shards.iter().map(|s| s.output.to_bytes()).collect(), parent_hash: f.block.env.parent_hash, prev: None }; + assert!(std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| aggregate(&agg_in, &mut |_, _| {}))).is_err(), "an aggregator input of another format is refused"); + agg_in.format = GUEST_INPUT_FORMAT; + assert_eq!(aggregate(&agg_in, &mut |_, _| {}).shard_count, shards.len() as u32); + } + + /// V6-10, fee rounding: the proving charge divides exactly, the rounding wei to the burn, off burns the whole charge. + #[test] + fn v6_10_the_proving_payment_rounds_its_odd_wei_to_the_burn_and_always_sums_to_the_charge() { + use igneum_prove_core::executor::proving_payment_split; + for charge in (0u128..2_000).chain([999_999_999_999_999_999u128, u128::MAX / 100 * 100, u128::MAX]) { + let (pool, burn) = proving_payment_split(charge, true); + assert_eq!(pool + burn, charge, "charge {charge}"); + assert_eq!(pool, charge / 100 * 90 + (charge % 100) * 90 / 100, "charge {charge}"); + assert!(pool <= charge / 10 * 9 + 9 && burn >= charge / 10, "charge {charge}: the rounding goes to the burn"); + assert_eq!(proving_payment_split(charge, false), (0, charge), "off: the whole charge burns"); + } + assert_eq!(proving_payment_split(100, true), (90, 10)); + assert_eq!(proving_payment_split(101, true), (90, 11), "the odd wei burns"); + assert_eq!(proving_payment_split(1, true), (0, 1)); + assert_eq!(proving_payment_split(11, true), (9, 2)); + } + + /// V6-10, duplicate payments: inside the guest's derivation a record for a shard already paid, a second record for + /// the same (block, shard), and an invalid record each pay nothing; the honest first record pays its part once. + #[test] + fn v6_10_a_duplicate_a_paid_before_and_an_invalid_carried_record_pay_nothing_inside_the_guest() { + use alloy_primitives::{Address, B256, U256}; + use igneum_prove_core::fixture::CarriedFixture; + use igneum_prove_core::shard::{derive_payouts, derivation_commitment}; + let record = |block: u8, shard: u32, payout: u8| -> Vec { + let mut b = vec![1u8, 0]; + b.extend_from_slice(B256::repeat_byte(block).as_slice()); + b.extend_from_slice(&7u64.to_le_bytes()); + b.extend_from_slice(&shard.to_le_bytes()); + b.extend_from_slice(&[0x33; 48]); + b.extend_from_slice(Address::repeat_byte(payout).as_slice()); + b.extend_from_slice(&[0; 40]); + b + }; + let fx = |rec: Vec, valid: bool, paid_before: bool| CarriedFixture { record: rec.into(), shards: 2, pool_credit: U256::from(1_000u64), segment_daa: 10, paid_before, valid, v1_active: false, aggregator_share_bps: 0 }; + let honest = fx(record(0xa1, 0, 0x01), true, false); + assert_eq!(derive_payouts(std::slice::from_ref(&honest)), vec![(Address::repeat_byte(0x01), U256::from(500u64))], "two shards: half each, shard 0 takes the remainder"); + let duplicate = fx(record(0xa1, 0, 0x02), true, false); + assert_eq!(derive_payouts(&[honest.clone(), duplicate.clone()]).len(), 1, "a second record for the same (block, shard) pays nothing"); + assert_eq!(derive_payouts(&[duplicate.clone(), honest.clone()])[0].0, Address::repeat_byte(0x02), "whichever record the carrier applied first is the one paid"); + assert!(derive_payouts(&[fx(record(0xa1, 0, 0x01), true, true)]).is_empty(), "paid before the carrier: nothing"); + assert!(derive_payouts(&[fx(record(0xa1, 0, 0x01), false, false)]).is_empty(), "invalid: nothing"); + assert_eq!(derive_payouts(&[honest.clone(), fx(record(0xa1, 1, 0x03), true, false)]).len(), 2, "another shard of the same block is its own payment"); + assert_ne!(derivation_commitment(&[honest.clone(), duplicate.clone()]), derivation_commitment(&[honest.clone()]), "the duplicate is in the commitment even though it pays nothing"); + } + #[test] fn a_fixture_without_fees_is_the_prototype_side() { let f = load("block-338-shard1.json"); @@ -1017,6 +1085,101 @@ mod fee_switch_tests { assert_eq!(proving_payment_split(7, false), (0, 7)); } + /// P22 stage 1, known-failed first: shard 0's public values commit to the rewards, the pool credit and the payouts + /// the segment applied, every other shard's commitment is zero, the aggregator carries shard 0's out, and the + /// same fixture with other rewards commits to another value. The test vector pins the function byte for byte + /// against the node's copy (`igneum_exec::proving::tests::p22_stage_1_inputs_commitment_vector`). + #[test] + fn the_inputs_commitment_binds_the_rewards_and_payouts_through_shard_0_and_the_aggregator() { + use igneum_prove_core::agg::{aggregate, AggInput}; + use igneum_prove_core::shard::inputs_commitment; + let f = load("fees-v1-shards2.json"); + let prover = alloy_primitives::Address::from_slice(&[0x19; 20]); + let (_, _, shards) = build_shards(&f.block, f.plan.shard_budget, prover); + assert!(shards.len() >= 2); + let want = inputs_commitment(&f.block.rewards, f.block.proving_pool_credit, &f.block.payouts); + assert_eq!(shards[0].output.inputs, want, "shard 0 commits to the segment's inputs"); + for s in &shards[1..] { + assert_eq!(s.output.inputs, alloy_primitives::B256::ZERO, "shard {} applies no inputs", s.output.shard_index); + } + let out = aggregate(&AggInput { format: igneum_prove_core::shard::GUEST_INPUT_FORMAT, shard_vk: [0; 8], shards: shards.iter().map(|s| s.output.to_bytes()).collect(), parent_hash: f.block.env.parent_hash, prev: None }, &mut |_, _| {}); + assert_eq!(out.inputs, want, "the aggregator carries shard 0's commitment out"); + assert_eq!(out.to_bytes().len(), 404); + // other rewards: another commitment, so a proof over them is not the native statement + let mut other = f.block.clone(); + other.rewards.push((alloy_primitives::Address::from_slice(&[0x99; 20]), alloy_primitives::U256::from(7u64))); + let (_, _, shards2) = build_shards(&other, f.plan.shard_budget, prover); + assert_ne!(shards2[0].output.inputs, want); + // the test vector, one in both crates + let a = alloy_primitives::Address::from_slice(&[0x11; 20]); + let b = alloy_primitives::Address::from_slice(&[0x22; 20]); + let v = inputs_commitment(&[(a, alloy_primitives::U256::from(1u64))], alloy_primitives::U256::from(2u64), &[(b, alloy_primitives::U256::from(3u64))]); + eprintln!("RESULT inputs_commitment vector: {v}"); + assert_eq!(format!("{v}"), "0x911a87e447671fb5afa8126624e411cfd3c2b4e48baa5fac38a477c725e4a5a5", "the one vector in both crates (pinned from the first run, build-2 19:01 UK)"); + } + + /// P22 stage 2, known-failed first: shard 0 derives the carrier's payouts from the carried records by the rules + /// (the first valid record per unpaid shard pays that shard's part), commits to the records, the aggregator carries + /// the commitment out, a host that feeds another payouts list cannot make the statement, and the rules' edges hold + /// (a record the node rejected, a shard paid before, a duplicate, the v1 split). The vector is pinned in both crates. + #[test] + fn the_derivation_commitment_binds_the_carried_records_and_the_payouts_follow_from_them() { + use igneum_prove_core::agg::{aggregate, AggInput}; + use igneum_prove_core::fixture::CarriedFixture; + use igneum_prove_core::shard::{derivation_commitment, derive_payouts}; + use alloy_primitives::{Address, Bytes, B256, U256}; + // two records on one segment (block 0x11, 2 shards, 1,000,000 wei credit, v0 split): the 274-byte wire form + // is version u16 | block 32 | number u64 | shard u32 | pubkey 48 | payout 20 | statement 32 | proof hash 32 | sig 96 + let wire = |shard: u32, payout: u8| -> Bytes { + let mut v = vec![1u8, 0]; + v.extend_from_slice(&[0x11u8; 32]); + v.extend_from_slice(&7u64.to_le_bytes()); + v.extend_from_slice(&shard.to_le_bytes()); + v.extend_from_slice(&[0xaa; 48]); + v.extend_from_slice(&[payout; 20]); + v.extend_from_slice(&[0x22; 32]); + v.extend_from_slice(&[0x33; 32]); + v.extend_from_slice(&[0x44; 96]); + assert_eq!(v.len(), 274); + Bytes::from(v) + }; + let rec = |shard: u32, payout: u8, valid: bool, paid_before: bool| CarriedFixture { record: wire(shard, payout), shards: 2, pool_credit: U256::from(1_000_001u64), segment_daa: 95, paid_before, v1_active: false, aggregator_share_bps: 1_000, valid }; + let carried = vec![rec(0, 0xa1, true, false), rec(0, 0xa2, true, false), rec(1, 0xb1, false, false), rec(1, 0xb2, true, false)]; + let derived = derive_payouts(&carried); + assert_eq!(derived, vec![(Address::repeat_byte(0xa1), U256::from(500_001u64)), (Address::repeat_byte(0xb2), U256::from(500_000u64))], "the first valid record per shard pays its part (the remainder to shard 0); a duplicate and a rejected record pay nothing"); + assert_eq!(derive_payouts(&[rec(0, 0xa1, true, true)]), vec![], "a shard paid before the carrier pays nothing"); + let v1 = CarriedFixture { v1_active: true, ..rec(0, 0xa1, true, false) }; + assert_eq!(derive_payouts(&[v1]), vec![(Address::repeat_byte(0xa1), U256::from(450_001u64))], "the v1 split: the aggregator's 10 percent first, the remainder to shard 0"); + let c = derivation_commitment(&carried); + assert_ne!(c, B256::ZERO); + assert_ne!(c, derivation_commitment(&carried[..3]), "every record is in it"); + assert_eq!(derivation_commitment(&[]), B256::ZERO); + eprintln!("RESULT derivation_commitment vector: {c}"); + assert_eq!(format!("{c}"), "0xe824944e49f90e9dc7734ae5764b1413f9a29407f7b399ea26d6087a14a95baa", "the one vector in both crates (pinned from the first run, build-4 19:50 UK)"); + // through shard 0 and the aggregator on a real fixture: the fixture carries no records, so shard 0's derivation + // is zero and the aggregator carries zero; a fixture given the carried list with its own payouts commits to it + let f = load("fees-v1-shards2.json"); + let prover = Address::from_slice(&[0x19; 20]); + let (_, _, shards) = build_shards(&f.block, f.plan.shard_budget, prover); + assert_eq!(shards[0].output.derivation, B256::ZERO); + let out = aggregate(&AggInput { format: igneum_prove_core::shard::GUEST_INPUT_FORMAT, shard_vk: [0; 8], shards: shards.iter().map(|s| s.output.to_bytes()).collect(), parent_hash: f.block.env.parent_hash, prev: None }, &mut |_, _| {}); + assert_eq!(out.derivation, B256::ZERO); + assert_eq!(out.to_bytes().len(), 404); + let mut with = f.block.clone(); + with.carried = carried.clone(); + with.payouts = derived.clone(); + let (_, _, shards2) = build_shards(&with, f.plan.shard_budget, prover); + assert_eq!(shards2[0].output.derivation, c, "shard 0 commits to the records it derived the payouts from"); + for s in &shards2[1..] { + assert_eq!(s.output.derivation, B256::ZERO); + } + // a host that feeds another payouts list cannot make the statement + let mut wrong = with.clone(); + wrong.payouts = vec![(Address::repeat_byte(0xee), U256::from(1u64))]; + let r = std::panic::catch_unwind(|| build_shards(&wrong, f.plan.shard_budget, prover)); + assert!(r.is_err(), "the payouts are not the ones the carried records derive: the guest panics, the proof is impossible"); + } + #[test] fn the_v1_blocks_at_and_above_the_switch() { for (name, shards) in [("fees-v1-shards2.json", 2usize), ("fees-v1-shards3.json", 3)] { diff --git a/proving/igneum-prove/host/src/proof_system.rs b/proving/igneum-prove/host/src/proof_system.rs index bfc60e2ea..074ae8d91 100644 --- a/proving/igneum-prove/host/src/proof_system.rs +++ b/proving/igneum-prove/host/src/proof_system.rs @@ -122,7 +122,7 @@ impl ProofSystem for StubProofSystem { } } let parent_hash = B256::ZERO; - let input = AggInput { shard_vk: [0; 8], shards: shards.iter().map(|s| s.output.to_bytes()).collect(), parent_hash, prev: None }; + let input = AggInput { format: igneum_prove_core::shard::GUEST_INPUT_FORMAT, shard_vk: [0; 8], shards: shards.iter().map(|s| s.output.to_bytes()).collect(), parent_hash, prev: None }; let out = agg::aggregate(&input, &mut |_, _| {}); let claim = SegmentClaim::from_block(&out); Ok(StubProof { mac: self.mac(claim.digest()), claim }) @@ -223,7 +223,7 @@ impl Sp1ProofSystem { /// Executes the aggregator over the shards' public values without proofs (deferred verification off): /// the cycle count of the aggregation statement itself. pub fn execute_aggregator(&self, shard_outputs: &[ShardOutput], parent_hash: B256) -> Result<(BlockOutput, sp1_sdk::ExecutionReport, Duration)> { - let input = AggInput { shard_vk: self.shard_vk_hash(), shards: shard_outputs.iter().map(|o| o.to_bytes()).collect(), parent_hash, prev: None }; + let input = AggInput { format: igneum_prove_core::shard::GUEST_INPUT_FORMAT, shard_vk: self.shard_vk_hash(), shards: shard_outputs.iter().map(|o| o.to_bytes()).collect(), parent_hash, prev: None }; let mut stdin = SP1Stdin::new(); stdin.write_vec(bincode::serialize(&input)?); let t = Instant::now(); @@ -301,6 +301,7 @@ impl ProofSystem for Sp1ProofSystem { let t_stdin = Instant::now(); let mut stdin = SP1Stdin::new(); let input = AggInput { + format: igneum_prove_core::shard::GUEST_INPUT_FORMAT, shard_vk: self.shard_vk_hash(), shards: shards.iter().map(|s| s.output.to_bytes()).collect(), parent_hash: first.parent_hash, diff --git a/tools/ci/export-exclude.txt b/tools/ci/export-exclude.txt index 77ce6c9c0..aab450e01 100644 --- a/tools/ci/export-exclude.txt +++ b/tools/ci/export-exclude.txt @@ -43,6 +43,7 @@ docs/design/app-audit-2026-10-08.md docs/ledger-public-pre-2.0.md # 8 October 2026: the Devnet 3 proving pipeline record (the fleet lane: box names, the operations record, the external review quoted) docs/analysis/proving-pipeline-2026-10-08.md +docs/analysis/v6-10-guest-repin-2026-10-08.md docs/analysis/class-v6/coexistence-model.md docs/analysis/class-v6/operator-simulation.md docs/analysis/class-v6/coexistence-workbook.md