fin-proof: fin-synth and fin-execute host modes (the cycle-count measurement), prepare stamps the root as the guest does

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-07 07:56:07 +00:00
parent b2f9434513
commit 98bb67389f
6 changed files with 196 additions and 14 deletions

View file

@ -12,7 +12,8 @@ The aggregator guest's public values (`BlockOutput`, 340 bytes, mirrored in cons
|---|---|---|
| `fin_version` | 2 | 1. Zero means "no finality claim" (the old layout never carries the extension) |
| `table_root` | 32 | Commitment to the weight state after the segment's last chain block (section 2) |
| `history_root` | 32 | Merkle mountain range over every chain block the proof chain attests: leaf `n` = `sha256(number ‖ block_hash ‖ table_root_n ‖ total_n ‖ daa_n)` |
| `history_root` | 32 | Merkle mountain range over every chain block the proof chain attests: leaf `n` = `sha256(number ‖ block_hash ‖ daa_n ‖ table_root_n ‖ keys_hash_n ‖ total_n)` |
| `history_first` | 8 | The chain block this proof chain's own attestation started at (section 4.5); earlier blocks in the history come from the root |
| `lock_index` | 8 | Highest checkpoint index the proof chain has verified a certificate for; 0 before the first |
| `lock_hash` | 32 | That checkpoint's block hash |
| `lock_number` | 8 | Its chain-block number |
@ -23,9 +24,9 @@ The aggregator guest's public values (`BlockOutput`, 340 bytes, mirrored in cons
| `lock_daa` | 8 | DAA score of the certified checkpoint, so a verifier can judge staleness |
| `flags` | 2 | bit 0 `stale`: the previous lock is more than one weight window older than the segment's last block and the guest refused every certificate since (section 4.4) |
154 bytes. `BlockOutput::LEN2 = 494`. Keccak of the public values stays the record's statement, so `SegmentRecord` is unchanged (its `public_values` field already carries the bytes inline, spec 7.8 item 3).
164 bytes (`FinExt::LEN`). `BlockOutput::LEN2 = 504`. Keccak of the public values stays the record's statement, so `SegmentRecord` is unchanged (its `public_values` field already carries the bytes inline, spec 7.8 item 3).
What the extension says, in words: every chain block from the proof chain's first block to `number` is in `history_root`; the table at every one of those blocks is committed in its leaf; checkpoint `lock_index` at chain block `lock_number` carries a certificate whose signers hold `lock_signed` of `lock_total` at that block's own table, and `lock_frozen_signed` of `lock_frozen_total` at the previous lock's table; both pass two thirds. A verifier that trusts the aggregator program id and the chain of proofs from a trusted root learns all of that from 494 bytes and one proof verification.
What the extension says, in words: every chain block from the proof chain's first block to `number` is in `history_root`; the table at every one of those blocks is committed in its leaf; checkpoint `lock_index` at chain block `lock_number` carries a certificate whose signers hold `lock_signed` of `lock_total` at that block's own table, and `lock_frozen_signed` of `lock_frozen_total` at the previous lock's table; both pass two thirds. A verifier that trusts the aggregator program id and the chain of proofs from a trusted root learns all of that from 504 bytes and one proof verification.
## 2. The carried weight state
@ -133,13 +134,13 @@ Baselines on record: a chained aggregation on the RTX 5090 costs 2.5 s with the
| Table update, no certificate, 1,000 keys | guest cycles | under 5 M (two table hashes of 112 KB on the SHA-256 precompile, 530 ring hashes) | measured in section 6.1 |
| Table update, 10,000 keys | guest cycles | about 40 M (two hashes of 1.1 MB): the Merkle form of section 2 when this binds | measured |
| One certificate, 1,000 voters | guest cycles | G1 aggregation of up to 1,000 keys on the precompile, one hash-to-G2, one pairing: tens of millions of cycles from memory of SP1's bls12-381 benchmarks, unmeasured here | frontier gate (a): under 50 M |
| Proof bytes | public values | +154 bytes; the compressed proof size is unchanged (constant) | none |
| Proof bytes | public values | +164 bytes; the compressed proof size is unchanged (constant) | none |
| Prover time per segment | 5090, box CPU | the cycle count divided by the measured cycles per second of the aggregator (the 5090 aggregation at about 2.5 s is the known cost of the existing guest) | section 6.2 |
| Verifier, native | Mac core | unchanged, 0.032 s: the extension is parsed, not verified separately | section 6.3 |
| Verifier, browser WASM | tab | the wrapped-proof verifier of frontier 3.4 (unmeasured); the extension parse and the MMR check are microseconds | open, frontier 3.4 |
| Witness bytes per segment | gossip | the key table (112 bytes per key) once per segment at the prototype, plus the certificate's bitmap and the ring leaves; 1.1 MB at 10,000 keys, which is the reason the Merkle form exists | section 6.1 reports the bytes |
Per tier (the standing rule): a home miner on any card mines as before, since the table update is the aggregator's work and a shard prover never sees it; a 12 GB prover that aggregates pays the extension's cycles once per segment and the certificate's once per 30 s, so about one more shard in thirty at launch traffic (frontier 3.3's figure, to be replaced by section 6.1's); a rig or pool that aggregates pays the same from the same 20 percent pool, so the aggregator's share (`proving_v1_aggregator_share_bps`) is the parameter to revisit once the cycles are known; a holder or wallet user gets "locked, voter set verified in the proof" from 494 bytes and one verification; a rollup customer's bridge verifies one proof for state and finality, the proof bridge of spec 7.3 delivered early; a node operator relays the key table with the segment witness (the bytes above).
Per tier (the standing rule): a home miner on any card mines as before, since the table update is the aggregator's work and a shard prover never sees it; a 12 GB prover that aggregates pays the extension's cycles once per segment and the certificate's once per 30 s, so about one more shard in thirty at launch traffic (frontier 3.3's figure, to be replaced by section 6.1's); a rig or pool that aggregates pays the same from the same 20 percent pool, so the aggregator's share (`proving_v1_aggregator_share_bps`) is the parameter to revisit once the cycles are known; a holder or wallet user gets "locked, voter set verified in the proof" from 504 bytes and one verification; a rollup customer's bridge verifies one proof for state and finality, the proof bridge of spec 7.3 delivered early; a node operator relays the key table with the segment witness (the bytes above).
### 6.1 Cycle counts (box CPU, SP1 execute mode, no GPU needed)
To be filled by the measurement: the aggregator in `execute` mode over the harness's segments at 100, 1,000 and 10,000 keys, with and without a certificate, under `BR_MEASURE=1` on igneum-build-1.

View file

@ -2850,6 +2850,7 @@ dependencies = [
"alloy-trie",
"anyhow",
"bincode",
"blst",
"hex",
"igneum-evm-types",
"igneum-fin-core",

View file

@ -8,6 +8,7 @@ license.workspace = true
[dependencies]
igneum-prove-core.workspace = true
igneum-fin-core.workspace = true
blst = "0.3"
igneum-evm-types.workspace = true
sp1-sdk = { workspace = true, features = ["blocking"] }
alloy-primitives.workspace = true

View file

@ -33,14 +33,22 @@ pub const FORMAT: &str = "igneum-fin-witness-v1";
/// Folds every block natively (the known-finished case before any proof) and returns one `FinInput` per block,
/// each with the state its fold starts from.
pub fn prepare(file: &FinWitnessFile, first_number: u64) -> Result<Vec<FinInput>> {
pub fn prepare(file: &FinWitnessFile, first_number: u64, rooted: bool) -> Result<Vec<FinInput>> {
if file.format != FORMAT {
bail!("finality witness format {} (want {FORMAT})", file.format);
}
let mut state = file.root_state.clone();
if state.end_number + 1 != first_number && !(state.leaves == 0 && state.history_first == first_number) {
if state.leaves == 0 {
state.history_first = first_number;
state.end_number = first_number.saturating_sub(1);
}
if state.end_number + 1 != first_number {
bail!("the finality root state ends at chain block {} and the first block to aggregate is {first_number}", state.end_number);
}
if rooted {
// no previous proof: the guest stamps the attestation's start at this block (agg.rs, the root branch)
state.history_first = first_number;
}
let mut out = Vec::with_capacity(file.blocks.len());
for (i, b) in file.blocks.iter().enumerate() {
if b.block.number != first_number + i as u64 {
@ -128,3 +136,114 @@ pub fn load_query(path: &str) -> Result<BlockQuery> {
pub fn table_bytes(t: &TableAt) -> usize {
t.keys.len() * igneum_fin_core::KeyEntry::LEN + 32 * (t.proof.siblings.len() + t.proof.peaks.len())
}
// ---------------------------------------------------------------------------------------------------------------
// The measurement's synthetic witness (design section 6.1): N keys with real BLS key pairs, a window of blocks
// behind the fixtures' chain blocks, every key revealed in the first block, and one certificate signed by the
// heaviest keys over an earlier block of the run. The chain blocks are the real fixtures' numbers, hashes and DAA
// scores, so the guest's cross-check against the shard statements holds; the blue blocks and keys are synthetic.
use igneum_fin_core::cert::CertificateWitness;
use igneum_fin_core::fold::{BlueBlock, Reveal};
use igneum_fin_core::mmr::Mmr;
use igneum_fin_core::tracker::SparseRing;
use igneum_fin_core::{vote_key_hash, vote_message, voters_at, KeyEntry, DST_POP, DST_VOTE, H};
use igneum_prove_core::Fixture;
struct SynthKey {
sk: blst::min_pk::SecretKey,
pk: [u8; 48],
hash: H,
}
fn synth_key(i: usize) -> SynthKey {
let mut ikm = [0x5au8; 32];
ikm[..8].copy_from_slice(&(i as u64).to_le_bytes());
let sk = blst::min_pk::SecretKey::key_gen(&ikm, b"igneum-fin-synth").unwrap();
let pk = sk.sk_to_pk().compress();
SynthKey { sk, pk, hash: vote_key_hash(&pk) }
}
fn synth_hash(tag: &str, n: u64) -> H {
igneum_fin_core::sha256(&[tag.as_bytes(), &n.to_le_bytes()])
}
/// Builds the witness file for `fixtures` (consecutive chain blocks): `keys` synthetic keys holding a window of
/// `window_blocks` blue blocks before the first fixture (the root state), two blue blocks a chain block during the
/// run, every key revealed in the first block, and a certificate signed by the heaviest `voters` keys carried in
/// the last block over the chain block `cert_back` blocks before it. Returns the file and the number of blocks a
/// certificate verification touched (for the report).
pub fn synth(fixtures: &[Fixture], keys: usize, voters: usize, window_blocks: u64, cert_back: usize) -> Result<FinWitnessFile> {
let first = fixtures.first().ok_or_else(|| anyhow!("no fixtures"))?;
let first_daa = first.block.env.daa_score.max(1);
// the window of the root state: W spans the synthetic history; min_daa under the first DAA so certificates pass C5
let params = FinParams { chain_id: "igneum-fin-synth".into(), weight_window: window_blocks.max(16) * 2, dust: 1, min_daa: 1, leave_delay: 3_600, equivocation_ban: window_blocks.max(16) * 2 };
let synth_keys: Vec<SynthKey> = (0..keys.max(1)).map(synth_key).collect();
// root: fold a synthetic prefix so the table and ring are full (number counts down from the first fixture)
let prefix = window_blocks.min(first.block.env.number.saturating_sub(1)).min(first_daa.saturating_sub(1));
let root_number = first.block.env.number - prefix;
let mut state = FinState::empty(&params, root_number);
let mut ring = SparseRing::new();
let bls = igneum_fin_core::bls::ZkBls;
let mut mmr = Mmr::default();
for i in 0..prefix {
let n = root_number + i;
let daa = first_daa - prefix + i;
let k = &synth_keys[(i as usize) % synth_keys.len()];
let k2 = &synth_keys[(i as usize * 7 + 3) % synth_keys.len()];
let block = ChainBlockWitness { number: n, block_hash: synth_hash("prefix", n), daa, blues: vec![BlueBlock { block_hash: synth_hash("prefix", n), key_hash: k.hash, daa }, BlueBlock { block_hash: synth_hash("prefix-side", n), key_hash: k2.hash, daa }] };
let mut w = FoldWitness::default();
if i == 0 {
w.reveals = synth_keys.iter().map(|k| Reveal { pubkey: k.pk, pop: k.sk.sign(&k.pk, DST_POP, &[]).compress() }).collect();
}
fold_block(&mut state, &params, &block, &mut ring, &w, &bls).map_err(|e| anyhow!("prefix fold {n}: {e}"))?;
ring.take_witnesses();
let (_, total) = voters_at(&state.keys, daa, params.dust);
mmr.append(HistoryLeaf { number: n, block_hash: block.block_hash, daa, table_root: state.table_root(), keys_hash: state.keys_hash(), total }.hash());
}
let root_state = state.clone();
// the run over the fixtures, witnesses recorded; tables and leaves kept for the certificate
let mut tables: Vec<(u64, Vec<KeyEntry>, HistoryLeaf)> = Vec::new();
let mut blocks = Vec::new();
for (i, f) in fixtures.iter().enumerate() {
let n = f.block.env.number;
let daa = f.block.env.daa_score.max(state.end_daa);
let k = &synth_keys[i % synth_keys.len()];
let k2 = &synth_keys[(i * 5 + 1) % synth_keys.len()];
let block = ChainBlockWitness { number: n, block_hash: f.block.env.hash.0, daa, blues: vec![BlueBlock { block_hash: f.block.env.hash.0, key_hash: k.hash, daa }, BlueBlock { block_hash: synth_hash("side", n), key_hash: k2.hash, daa }] };
let mut w = FoldWitness::default();
if i + 1 == fixtures.len() && fixtures.len() > cert_back && cert_back > 0 {
let (cp_number, cp_keys, cp_leaf) = tables[tables.len() - cert_back].clone();
let (vlist, _) = voters_at(&cp_keys, cp_leaf.daa, params.dust);
let mut heavy: Vec<usize> = vlist.clone();
heavy.sort_by_key(|&v| std::cmp::Reverse(cp_keys[v].blocks));
heavy.truncate(voters.max(1));
let index = 1 + (cp_number / 30);
let msg = vote_message(&params.chain_id, index, &cp_leaf.block_hash);
let mut sigs = Vec::new();
let mut positions = Vec::new();
for v in &heavy {
let kh = cp_keys[*v].key_hash;
let sk = &synth_keys.iter().find(|k| k.hash == kh).unwrap().sk;
sigs.push(sk.sign(&msg, DST_VOTE, &[]));
positions.push(vlist.iter().position(|x| x == v).unwrap());
}
let refs: Vec<&blst::min_pk::Signature> = sigs.iter().collect();
let agg = blst::min_pk::AggregateSignature::aggregate(&refs, true).unwrap().to_signature().compress();
let mut bitmap = vec![0u8; vlist.len().div_ceil(8)];
for p in positions {
bitmap[p / 8] |= 1 << (p % 8);
}
let position = cp_number - root_state.history_first;
let proof = mmr.proof(position).ok_or_else(|| anyhow!("no history proof for {cp_number}"))?;
w.certificates.push(CertificateWitness { index, checkpoint: cp_leaf.block_hash, voter_count: vlist.len() as u32, bitmap, signature: agg, at: TableAt { leaf: cp_leaf, proof, keys: cp_keys }, frozen: None });
}
fold_block(&mut state, &params, &block, &mut ring, &w, &bls).map_err(|e| anyhow!("fold {n}: {e}"))?;
let (_, total) = voters_at(&state.keys, daa, params.dust);
let leaf = HistoryLeaf { number: n, block_hash: block.block_hash, daa, table_root: state.table_root(), keys_hash: state.keys_hash(), total };
mmr.append(leaf.hash());
tables.push((n, state.keys.clone(), leaf));
blocks.push(BlockFin { block, ring: ring.take_witnesses(), witness: w });
}
Ok(FinWitnessFile { format: FORMAT.into(), params, root_state, blocks })
}

View file

@ -83,13 +83,66 @@ fn run() -> Result<()> {
// proving v1 (spec 7.8): the node's pool verifies an aggregated segment proof against the pinned aggregator key
return run_verify_segment(&pinned, &arg("--proof").context("--proof <file>")?, &arg("--statement").context("--statement 0x<keccak of the block public values>")?);
}
let out_path = arg("--out");
if mode == "fin-synth" {
// the measurement's witness (design 6.1): --chain <fixtures> --keys N --voters V --window B --cert-back K --out <file>
let list = arg("--chain").context("--chain <f1.json,f2.json,...>")?;
let fixtures: Vec<Fixture> = list.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).map(load_fixture).collect::<Result<_>>()?;
let keys: usize = arg("--keys").map(|s| s.parse()).transpose()?.unwrap_or(1_000);
let voters: usize = arg("--voters").map(|s| s.parse()).transpose()?.unwrap_or(keys * 7 / 10);
let window: u64 = arg("--window").map(|s| s.parse()).transpose()?.unwrap_or(2_000);
let cert_back: usize = arg("--cert-back").map(|s| s.parse()).transpose()?.unwrap_or(4);
let out = arg("--out").context("--out <witness.json>")?;
let t = Instant::now();
let file = fin::synth(&fixtures, keys, voters, window, cert_back)?;
let text = serde_json::to_string(&file)?;
std::fs::write(&out, &text).with_context(|| format!("write {out}"))?;
println!("RESULT fin-synth: {} blocks {}..={}, {} keys ({} in the table), {} signers, window {} blocks, certificate at block {} over {} back; root state {} keys; {} bytes of JSON in {:.1} s at {}", file.blocks.len(), fixtures[0].block.env.number, fixtures[fixtures.len() - 1].block.env.number, keys, file.root_state.keys.len(), voters, window, file.blocks.len(), cert_back, file.root_state.keys.len(), text.len(), t.elapsed().as_secs_f64(), now());
return Ok(());
}
if mode == "fin-execute" {
// the cycle count (design 6.1): every block's aggregator in execute mode, with and without the finality input
let list = arg("--chain").context("--chain <f1.json,f2.json,...>")?;
let fixtures: Vec<Fixture> = list.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).map(load_fixture).collect::<Result<_>>()?;
let file = fin::load_witness(&arg("--fin").context("--fin <witness.json>")?)?;
let prover: Address = Address::from_slice(&[0x19; 20]);
let first = fixtures[0].block.env.number;
let inputs = fin::prepare(&file, first, true)?;
let mut results = serde_json::Map::new();
results.insert("mode".into(), "fin-execute".into());
let sp1 = setup_sp1(&pinned, &mut results)?;
let (mut prev_plain, mut prev_fin): (Option<Vec<u8>>, Option<Vec<u8>>) = (None, None);
let mut rows = Vec::new();
for (i, f) in fixtures.iter().enumerate() {
let (_, _, shards) = build_shards(&f.block, f.plan.shard_budget, prover);
let outputs: Vec<ShardOutput> = shards.iter().map(|s| s.output.clone()).collect();
let parent = f.block.env.parent_hash;
stage(&format!("fin-execute block {} plain", f.block.env.number));
let (out_plain, rep_plain, dt_plain) = sp1.execute_aggregator_with(&outputs, parent, prev_plain.take(), None)?;
stage(&format!("fin-execute block {} with finality", f.block.env.number));
let fin_input = inputs[i].clone();
let witness_bytes = bincode::serialize(&fin_input)?.len();
let certs = fin_input.witness.certificates.len();
let (out_fin, rep_fin, dt_fin) = sp1.execute_aggregator_with(&outputs, parent, prev_fin.take(), Some(fin_input))?;
let ext = out_fin.fin.clone().ok_or_else(|| anyhow!("no extension in the output"))?;
let (c0, c1) = (rep_plain.total_instruction_count(), rep_fin.total_instruction_count());
let sys: Vec<(String, u64)> = rep_fin.syscall_counts.iter().map(|(k, v)| (format!("{k:?}"), *v)).filter(|(_, v)| *v > 0).collect();
println!("RESULT fin-execute block {}: plain {} cycles ({:.2} s), with finality {} cycles ({:.2} s), extra {} cycles; keys {} witness {} bytes certificates {}; lock index {} at block {} ({} of {}); syscalls {:?} at {}", f.block.env.number, c0, dt_plain.as_secs_f64(), c1, dt_fin.as_secs_f64(), c1.saturating_sub(c0), file.root_state.keys.len(), witness_bytes, certs, ext.lock.index, ext.lock.number, ext.lock.signed, ext.lock.total, sys, now());
rows.push(serde_json::json!({ "number": f.block.env.number, "plain_cycles": c0, "fin_cycles": c1, "extra_cycles": c1.saturating_sub(c0), "witness_bytes": witness_bytes, "certificates": certs, "lock_index": ext.lock.index, "syscalls": sys.iter().map(|(k, v)| serde_json::json!({"name": k, "count": v})).collect::<Vec<_>>() }));
prev_plain = Some(out_plain.to_bytes());
prev_fin = Some(out_fin.to_bytes());
}
results.insert("blocks".into(), rows.into());
results.insert("keys".into(), file.root_state.keys.len().into());
drop(sp1);
return finish(results, out_path.clone());
}
if mode == "final-at" {
// finality in the proof (docs/design/finality-in-proof.md section 4): the light client's question answered
// from one verified proof's extension and, for an earlier block, a history proof; no node asked
return run_final_at(&pinned, &arg("--proof").context("--proof <file>")?, arg("--block").as_deref(), arg("--leaves").as_deref());
}
let prover: Address = arg("--prover").map(|s| s.parse()).transpose()?.unwrap_or_else(|| Address::from_slice(&[0x19; 20]));
let out_path = arg("--out");
// --fin <file>: the finality witness of every block aggregated (the statement gains the extension)
let fin_path = arg("--fin");
if mode == "aggregate" {
@ -230,7 +283,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 { shard_vk: [0; 8], shards: shards.iter().map(|s| s.output.to_bytes()).collect(), parent_hash: block.env.parent_hash, prev: None, fin: 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");
}
@ -691,7 +744,7 @@ fn run_chain(pinned: &pinned::Pinned, fixtures: &[String], prover: Address, out_
None => Vec::new(),
Some(p) => {
let file = fin::load_witness(p)?;
let inputs = fin::prepare(&file, first)?;
let inputs = fin::prepare(&file, first, prev.is_none())?;
if inputs.len() != built.len() {
bail!("{p} carries {} finality witnesses for {} blocks", inputs.len(), built.len());
}
@ -860,7 +913,7 @@ fn run_aggregate(pinned: &pinned::Pinned, proofs: &str, parent: &str, prev_path:
None => Vec::new(),
Some(p) => {
let file = fin::load_witness(p)?;
let inputs = fin::prepare(&file, first)?;
let inputs = fin::prepare(&file, first, prev.is_none())?;
if inputs.len() != blocks.len() {
bail!("{p} carries {} finality witnesses for {} blocks", inputs.len(), blocks.len());
}

View file

@ -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 { shard_vk: [0; 8], shards: shards.iter().map(|s| s.output.to_bytes()).collect(), parent_hash, prev: None, fin: None };
let out = agg::aggregate(&input, &mut |_, _| {});
let claim = SegmentClaim::from_block(&out);
Ok(StubProof { mac: self.mac(claim.digest()), claim })
@ -223,13 +223,20 @@ 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 };
self.execute_aggregator_with(shard_outputs, parent_hash, None, None)
}
/// The aggregator in execute mode with the previous block's public values and the finality input (the cycle
/// count of docs/design/finality-in-proof.md section 6.1; deferred proof verification off, as above).
pub fn execute_aggregator_with(&self, shard_outputs: &[ShardOutput], parent_hash: B256, prev_public_values: Option<Vec<u8>>, fin: Option<igneum_prove_core::agg::FinInput>) -> Result<(BlockOutput, sp1_sdk::ExecutionReport, Duration)> {
let prev = prev_public_values.map(|public_values| PrevLink { agg_vk: self.agg_vk_hash(), public_values });
let input = AggInput { shard_vk: self.shard_vk_hash(), shards: shard_outputs.iter().map(|o| o.to_bytes()).collect(), parent_hash, prev, fin };
let mut stdin = SP1Stdin::new();
stdin.write_vec(bincode::serialize(&input)?);
let t = Instant::now();
let (pv, report) = self.client.execute(self.agg_pk.elf().clone(), stdin).deferred_proof_verification(false).calculate_gas(true).run().map_err(|e| anyhow!("{e}"))?;
let dt = t.elapsed();
let out = BlockOutput::from_bytes(pv.as_slice()).ok_or_else(|| anyhow!("block public values are {} bytes, expected {}", pv.as_slice().len(), BlockOutput::LEN))?;
let out = BlockOutput::from_bytes(pv.as_slice()).ok_or_else(|| anyhow!("block public values are {} bytes, expected {} or {}", pv.as_slice().len(), BlockOutput::LEN, BlockOutput::LEN2))?;
Ok((out, report, dt))
}