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 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-08 16:45:03 +00:00
parent 1dc7c42ca4
commit 7dd7c35e1a
4 changed files with 68 additions and 4 deletions

View file

@ -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();

View file

@ -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

View file

@ -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::<Result<Vec<_>>>()?;
let proving_pool_credit: U256 = seg["provingPoolCredit"].as_str().context("provingPoolCredit")?.parse()?;

View file

@ -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)] {