From 7dd7c35e1a86edeac49cf9750ee5836385e1f3ff Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:45:03 +0000 Subject: [PATCH] Shard guest: the D4 proving-payment split mirrored behind FixtureEnv::proving_payment_to_pool (known-failed first; the ELF and program id not yet re-pinned) The guest core applies the node's split at both charge sites (proving_payment_split, the same arithmetic as igneum_exec::executor): off, the whole proving charge burns as every existing fixture recorded; on, 90 percent of pgas used x f_p is credited to PROVING_POOL_ADDRESS and 10 percent burns. The flag is serde-default false, so every fixture written before it reads unchanged and reproduces its roots; the exporter reads it from the node's segment JSON (provingPaymentToPool, proving-payment 930b6322 of the fork). Test: host fee_switch_tests::the_proving_payment_flag_moves_90_percent_of_the_charge_to_the_pool (off: the recorded roots and the subsidy credit alone in the escrow; on: 90 percent of the charges in the escrow, another post-root, the proving work unchanged); fee-switch tests 5 passed on build-2 at 17:44 UK. The guest ELF, verifying key and program id in elf/ are NOT re-pinned by this commit: until pin-guests.sh runs on the new sources and the object pins the new id, proving_payment_activation_daa stays at never on every object. Co-Authored-By: Claude Fable 5.1 --- proving/igneum-prove/core/src/executor.rs | 27 ++++++++++++++--- proving/igneum-prove/core/src/fixture.rs | 6 ++++ proving/igneum-prove/export/src/main.rs | 2 ++ proving/igneum-prove/host/src/main.rs | 37 +++++++++++++++++++++++ 4 files changed, 68 insertions(+), 4 deletions(-) diff --git a/proving/igneum-prove/core/src/executor.rs b/proving/igneum-prove/core/src/executor.rs index 116db54e0..b53368a6f 100644 --- a/proving/igneum-prove/core/src/executor.rs +++ b/proving/igneum-prove/core/src/executor.rs @@ -191,6 +191,17 @@ pub struct RangeOutcome { /// Executes a contiguous range of a segment's transactions on `db` from `carry_in` (design 1.2 and 5.1): the /// rewards first when given (the segment's first shard), then every transaction in order. `db` is the state /// before the range on entry and after it on return. With `roots`, the state root is computed at every boundary. +/// Igneum 2.0 D4: how a transaction's proving charge divides, exactly as the node (`igneum_exec::executor::proving_payment_split`). +/// Off: the whole charge burns. On: 90 percent is the provers' payment (to the pool escrow), 10 percent burns, the +/// rounding wei to the burn. +pub fn proving_payment_split(charge: u128, to_pool: bool) -> (u128, u128) { + if !to_pool { + return (0, charge); + } + let payment = charge / 100 * 90 + (charge % 100) * 90 / 100; + (payment, charge - payment) +} + pub fn execute_range(db: &mut IgneumDb, chain_id: u64, env: &FixtureEnv, schedule: &FeeSchedule, rewards: Option<(&[(Address, U256)], U256, &[(Address, U256)])>, txs: &[ShardTx], carry_in: Carry, roots: bool) -> RangeOutcome { // The fee set that meters this chain block: the schedule read at the block's own DAA score, so the guest // replays the height switch as the node does. The base fees carried in are raised to this set's floors here @@ -287,10 +298,14 @@ pub fn execute_range(db: &mut IgneumDb, chain_id: u64, env: &FixtureEnv, schedul let (status, gas_used, logs) = if insp.over_budget { let gas_used = tx.gas_limit(); let burned_exec = (gas_used as u128) * base_fee_exec; - let burned_proving = ((pgas_used as u128) * base_fee_proving).min(budget.saturating_sub(burned_exec)); - let tip_total = budget.saturating_sub(burned_exec + burned_proving); + let proving_charge = ((pgas_used as u128) * base_fee_proving).min(budget.saturating_sub(burned_exec)); + let tip_total = budget.saturating_sub(burned_exec + proving_charge); db.bump_nonce(tx.sender); db.sub_balance(tx.sender, U256::from(budget)); + let (proving_payment, _burned_proving) = proving_payment_split(proving_charge, env.proving_payment_to_pool); + if proving_payment > 0 { + db.add_balance(PROVING_POOL_ADDRESS, U256::from(proving_payment)); + } let shares = developer_shares(tip_total, &insp.attributions, |code| registered_payee(db, code)); let dev_total: u128 = shares.iter().map(|(_, w)| *w).sum(); db.add_balance(t.miner, U256::from(tip_total - dev_total)); @@ -315,8 +330,12 @@ pub fn execute_range(db: &mut IgneumDb, chain_id: u64, env: &FixtureEnv, schedul (status, gas_used, logs) }; // The proving charge never takes the sender past the signed budget (design 4.1). - let burned_proving = ((pgas_used as u128) * base_fee_proving).min(budget.saturating_sub((gas_used as u128) * price)); - db.sub_balance(tx.sender, U256::from(burned_proving)); + let proving_charge = ((pgas_used as u128) * base_fee_proving).min(budget.saturating_sub((gas_used as u128) * price)); + db.sub_balance(tx.sender, U256::from(proving_charge)); + let (proving_payment, _burned_proving) = proving_payment_split(proving_charge, env.proving_payment_to_pool); + if proving_payment > 0 { + db.add_balance(PROVING_POOL_ADDRESS, U256::from(proving_payment)); + } let tip_total = (gas_used as u128) * (price - base_fee_exec); let shares = developer_shares(tip_total, &insp.attributions, |code| registered_payee(db, code)); let dev_total: u128 = shares.iter().map(|(_, w)| *w).sum(); diff --git a/proving/igneum-prove/core/src/fixture.rs b/proving/igneum-prove/core/src/fixture.rs index 10d48ff8a..8e3da24ca 100644 --- a/proving/igneum-prove/core/src/fixture.rs +++ b/proving/igneum-prove/core/src/fixture.rs @@ -25,6 +25,12 @@ pub struct FixtureEnv { /// A fixture written before the switch existed has none, and 0 keeps it on the schedule's base set. #[serde(default)] pub daa_score: u64, + /// Igneum 2.0 D4 (`docs/design/proving-payment.md`): the node's `proving_payment_activation_daa` reached at this + /// chain block, so a transaction's proving charge (pgas used x f_p) is the provers' payment: 90 percent credited to + /// the proving pool escrow, 10 percent burned; false (every fixture written before the field) burns the whole + /// charge, as spec 5.1 stood. Exactly what the node does (`igneum_exec::executor::proving_payment_split`). + #[serde(default)] + pub proving_payment_to_pool: bool, } /// One block of the segment in sequence order: its miner (the beneficiary of its transactions) and its diff --git a/proving/igneum-prove/export/src/main.rs b/proving/igneum-prove/export/src/main.rs index cc38f99e0..51d7c8e5a 100644 --- a/proving/igneum-prove/export/src/main.rs +++ b/proving/igneum-prove/export/src/main.rs @@ -128,6 +128,8 @@ fn fixture_of(export: &Value, segments: &[Value], n: usize, hashes: &[(u64, B256 None if fees.v1_activation_daa == 0 || fees.v1_activation_daa == u64::MAX => 0, None => bail!("segment {}: the dump carries no daaScore and the schedule switches at DAA {}; re-export with tools/prove-fixtures/gen.mjs, which writes it", seg["number"], fees.v1_activation_daa), }, + // Igneum 2.0 D4: the node says whether this block's proving charges went 90 percent to the pool (absent before the field: no) + proving_payment_to_pool: seg["provingPaymentToPool"].as_bool().unwrap_or(false), }; let rewards = seg["rewards"].as_array().context("rewards")?.iter().map(|r| Ok((addr(&r["address"])?, u256(&r["wei"])?))).collect::>>()?; let proving_pool_credit: U256 = seg["provingPoolCredit"].as_str().context("provingPoolCredit")?.parse()?; diff --git a/proving/igneum-prove/host/src/main.rs b/proving/igneum-prove/host/src/main.rs index 8b261007b..98e45f3fe 100644 --- a/proving/igneum-prove/host/src/main.rs +++ b/proving/igneum-prove/host/src/main.rs @@ -980,6 +980,43 @@ mod fee_switch_tests { native_matches(&f); } + /// Igneum 2.0 D4, known-failed first: with `proving_payment_to_pool` off the fixture reproduces its recorded roots + /// and the pool escrow holds only the 20 percent subsidy credit; with it on, 90 percent of the block's proving + /// charges (pgas used x f_p over the executed transactions) sit in PROVING_POOL_ADDRESS, the post-root differs from + /// the recorded one (another statement, which is why the floor stays at never until this guest is pinned), and the + /// split's arithmetic is the node's. + #[test] + fn the_proving_payment_flag_moves_90_percent_of_the_charge_to_the_pool() { + use igneum_prove_core::config::PROVING_POOL_ADDRESS; + use igneum_prove_core::executor::{execute_block, load_pre_state, proving_payment_split}; + let f = load("fees-v1-shards2.json"); + assert!(f.expected.pgas_used > 0); + // off: the recorded roots, the pool holds the subsidy credit alone + let mut db = load_pre_state(&f.block); + let before = db.balance(PROVING_POOL_ADDRESS); + let off = execute_block(&mut db, &f.block); + assert_eq!(off.state_root, f.expected.post_state_root); + let pool_off = db.balance(PROVING_POOL_ADDRESS) - before; + assert_eq!(pool_off, f.block.proving_pool_credit, "off: the 20 percent credit alone reaches the escrow"); + // on: 90 percent of every executed transaction's proving charge joins it + let mut block = f.block.clone(); + block.env.proving_payment_to_pool = true; + let mut db = load_pre_state(&block); + let before = db.balance(PROVING_POOL_ADDRESS); + let on = execute_block(&mut db, &block); + let pool_on = db.balance(PROVING_POOL_ADDRESS) - before; + let charges: u128 = on.executed.iter().map(|t| t.pgas_used as u128 * block.env.base_fee_proving as u128).sum(); + let payment: u128 = on.executed.iter().map(|t| proving_payment_split(t.pgas_used as u128 * block.env.base_fee_proving as u128, true).0).sum(); + assert!(charges > 0, "the fixture carries proving charges"); + assert_eq!(pool_on - pool_off, alloy_primitives::U256::from(payment), "90 percent of the charges reach the escrow"); + assert!(payment >= charges / 100 * 90 && payment <= charges / 100 * 90 + on.executed.len() as u128); + assert_ne!(on.state_root, off.state_root, "another post-root: another statement"); + assert_eq!(on.pgas_used, off.pgas_used, "the proving work is unchanged"); + assert_eq!(proving_payment_split(100, true), (90, 10)); + assert_eq!(proving_payment_split(7, true), (6, 1)); + assert_eq!(proving_payment_split(7, false), (0, 7)); + } + #[test] fn the_v1_blocks_at_and_above_the_switch() { for (name, shards) in [("fees-v1-shards2.json", 2usize), ("fees-v1-shards3.json", 3)] {