fin-proof: the Merkle key table (a slot per key, one path per touched key in the fold, the full table once per certificate over the dense prefix)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-07 08:51:13 +00:00
parent 125d65b039
commit b00a008e17
9 changed files with 432 additions and 139 deletions

View file

@ -6,6 +6,7 @@ use crate::executor::Carry;
use crate::shard::ShardOutput;
use alloy_primitives::{keccak256, B256};
use igneum_fin_core::fold::{fold_block, ChainBlockWitness, FoldWitness};
use igneum_fin_core::keys::{KeyLeafWitness, WitnessKeys};
use igneum_fin_core::ring::{RingLeafWitness, WitnessRing};
use igneum_fin_core::{FinExt, FinParams, FinState};
use serde::{Deserialize, Serialize};
@ -34,6 +35,8 @@ pub struct FinInput {
pub prev_state: FinState,
pub block: ChainBlockWitness,
pub ring: Vec<RingLeafWitness>,
#[serde(default)]
pub keys: Vec<KeyLeafWitness>,
pub witness: FoldWitness,
}
@ -215,8 +218,9 @@ pub fn aggregate(input: &AggInput, verify: &mut dyn FnMut(&[u32; 8], &[u8])) ->
assert_eq!(f.block.number, first.number, "the finality witness is of this chain block");
assert_eq!(f.block.block_hash.as_slice(), first.block_hash.as_slice(), "the finality witness names this chain block");
let mut ring = WitnessRing::new(f.ring.clone());
fold_block(&mut state, &f.params, &f.block, &mut ring, &f.witness, &igneum_fin_core::bls::ZkBls).expect("the finality fold");
assert!(ring.witnesses.is_empty(), "every ring witness consumed");
let mut keys = WitnessKeys::new(f.keys.clone());
fold_block(&mut state, &f.params, &f.block, &mut ring, &mut keys, &f.witness, &igneum_fin_core::bls::ZkBls).expect("the finality fold");
assert!(ring.witnesses.is_empty() && keys.witnesses.is_empty(), "every witness consumed");
state.extension()
});
BlockOutput {

View file

@ -5,15 +5,16 @@
use crate::bls::Bls;
use crate::mmr::{HistoryLeaf, MmrProof};
use crate::{keys_hash, vote_message, voters_at, FinParams, FinState, KeyEntry, Lock, H, SIG_LEN};
use crate::keys::{canonical, dense_root};
use crate::{vote_message, voters_at, FinParams, FinState, KeyEntry, Lock, H, SIG_LEN};
use serde::{Deserialize, Serialize};
/// A table at a chain block: the leaf that commits it and the entries.
/// A table at a chain block: the leaf that commits it and every entry with its slot, slot order.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TableAt {
pub leaf: HistoryLeaf,
pub proof: MmrProof,
pub keys: Vec<KeyEntry>,
pub keys: Vec<(u64, KeyEntry)>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@ -49,17 +50,16 @@ pub fn signer_positions(bitmap: &[u8], voter_count: u32) -> Vec<usize> {
out
}
fn check_table(state: &FinState, t: &TableAt, what: &str) -> Result<(), String> {
/// The table's entries against the leaf the history holds for that block: the dense root over the slots must be
/// the leaf's. Returns the canonical (sorted, duplicate-free) list.
fn check_table(state: &FinState, t: &TableAt, what: &str) -> Result<Vec<KeyEntry>, String> {
if !t.proof.verify(&t.leaf.hash(), &state.history_root(), state.leaves) {
return Err(format!("the {what} block is not in the proof's history"));
}
if keys_hash(&t.keys) != t.leaf.keys_hash {
if dense_root(&t.keys, t.leaf.key_count)? != t.leaf.keys_root {
return Err(format!("the {what} table is not the one the proof committed at that block"));
}
if !t.keys.windows(2).all(|p| p[0].key_hash < p[1].key_hash) {
return Err(format!("the {what} table is not sorted"));
}
Ok(())
canonical(&t.keys).map_err(|e| format!("the {what} table: {e}"))
}
pub fn verify_certificate(state: &mut FinState, params: &FinParams, c: &CertificateWitness, bls: &dyn Bls) -> Result<(), String> {
@ -69,7 +69,7 @@ pub fn verify_certificate(state: &mut FinState, params: &FinParams, c: &Certific
if c.index <= state.lock.index {
return Err(format!("certificate index {} is not above the last lock {}", c.index, state.lock.index));
}
check_table(state, &c.at, "checkpoint")?;
let at_keys = check_table(state, &c.at, "checkpoint")?;
let leaf = &c.at.leaf;
if leaf.block_hash != c.checkpoint {
return Err("the certificate's checkpoint is not the block the history leaf names".into());
@ -84,13 +84,10 @@ pub fn verify_certificate(state: &mut FinState, params: &FinParams, c: &Certific
return Err(format!("checkpoint DAA score {} is below min_daa {} (C5)", leaf.daa, params.min_daa));
}
// the canonical voter list at the checkpoint and the signers
let (voters, total) = voters_at(&c.at.keys, leaf.daa, params.dust);
let (voters, total) = voters_at(&at_keys, leaf.daa, params.dust);
if voters.len() != c.voter_count as usize {
return Err(format!("the certificate names {} voters, the table at the checkpoint has {}", c.voter_count, voters.len()));
}
if total != leaf.total {
return Err("the table's total differs from the leaf's".into());
}
let positions = signer_positions(&c.bitmap, c.voter_count);
if positions.is_empty() {
return Err("the certificate has no signer".into());
@ -99,7 +96,7 @@ pub fn verify_certificate(state: &mut FinState, params: &FinParams, c: &Certific
let mut pubkeys = Vec::with_capacity(positions.len());
let mut signer_hashes: Vec<H> = Vec::with_capacity(positions.len());
for p in &positions {
let k = &c.at.keys[voters[*p]];
let k = &at_keys[voters[*p]];
if !k.revealed() {
return Err("a signer's key is not revealed".into());
}
@ -130,20 +127,17 @@ pub fn verify_certificate(state: &mut FinState, params: &FinParams, c: &Certific
// Q5, never expiring in the proof
let (frozen_signed, frozen_total) = if state.lock.index > 0 {
let f = c.frozen.as_ref().ok_or("a lock exists and the frozen table is missing")?;
check_table(state, f, "frozen")?;
let f_keys = check_table(state, f, "frozen")?;
if f.leaf.number != state.lock.number || f.leaf.block_hash != state.lock.hash {
return Err("the frozen table is not the table at the last lock's block".into());
}
let (fvoters, ftotal) = voters_at(&f.keys, f.leaf.daa, params.dust);
if ftotal != f.leaf.total {
return Err("the frozen table's total differs from its leaf's".into());
}
let (fvoters, ftotal) = voters_at(&f_keys, f.leaf.daa, params.dust);
// keys gone at the checkpoint (their leaves carried after the lock) leave the frozen denominator
let gone = |kh: &H| c.at.keys.binary_search_by(|k| k.key_hash.cmp(kh)).ok().map(|i| c.at.keys[i].is_gone(leaf.daa)).unwrap_or(false);
let gone = |kh: &H| at_keys.binary_search_by(|k| k.key_hash.cmp(kh)).ok().map(|i| at_keys[i].is_gone(leaf.daa)).unwrap_or(false);
let mut left = 0u64;
let mut fsigned = 0u64;
for &i in &fvoters {
let k = &f.keys[i];
let k = &f_keys[i];
if gone(&k.key_hash) {
left += k.blocks;
} else if signer_hashes.binary_search(&k.key_hash).is_ok() {

View file

@ -7,7 +7,8 @@ use crate::cert::{verify_certificate, CertificateWitness};
use crate::header::{outranks, HeaderWitness};
use crate::mmr::{self, HistoryLeaf};
use crate::ring::{slot_of, RingAccess, RingEntry, RING_LEAF_DAA};
use crate::{find_key, vote_key_hash, vote_message, voters_at, FinParams, FinState, KeyEntry, H, PUBKEY_LEN, SIG_LEN};
use crate::keys::KeyAccess;
use crate::{vote_key_hash, vote_message, FinParams, FinState, KeyEntry, H, PUBKEY_LEN, SIG_LEN};
use serde::{Deserialize, Serialize};
/// One blue block of the chain block's past counted for its key: the chain block itself and its mergeset blues
@ -85,18 +86,28 @@ pub struct FoldReport {
pub locks: u64,
}
fn touch(keys: &mut Vec<KeyEntry>, key_hash: &H) -> usize {
match find_key(keys, key_hash) {
Ok(i) => i,
Err(i) => {
keys.insert(i, KeyEntry::new(*key_hash));
i
/// Opens the key's leaf, applies `f` to its entry (a fresh one when the key has no slot), writes it back (an entry
/// that holds nothing at `daa` empties its leaf; the slot is never reused).
fn touch(state: &mut FinState, keys: &mut dyn KeyAccess, key: &H, daa: u64, f: &mut dyn FnMut(&mut KeyEntry) -> Result<(), String>) -> Result<(), String> {
let (slot, entry, siblings) = keys.touch(key, &state.keys_root, state.key_count)?;
let mut e = match entry {
Some(e) => e,
None => {
if slot != state.key_count {
return Err("a new key must take the next free slot".into());
}
state.key_count += 1;
KeyEntry::new(*key)
}
}
};
f(&mut e)?;
let out = if e.is_empty_at(daa) { None } else { Some(e) };
state.keys_root = keys.write(slot, out, &siblings);
Ok(())
}
/// Folds chain block `block` into `state`. Errors make the proof impossible (the guest panics on them).
pub fn fold_block(state: &mut FinState, params: &FinParams, block: &ChainBlockWitness, ring: &mut dyn RingAccess, witness: &FoldWitness, bls: &dyn Bls) -> Result<FoldReport, String> {
pub fn fold_block(state: &mut FinState, params: &FinParams, block: &ChainBlockWitness, ring: &mut dyn RingAccess, keys: &mut dyn KeyAccess, witness: &FoldWitness, bls: &dyn Bls) -> Result<FoldReport, String> {
if state.params_hash != params.hash() {
return Err("the state was computed under other finality parameters".into());
}
@ -133,8 +144,10 @@ pub fn fold_block(state: &mut FinState, params: &FinParams, block: &ChainBlockWi
let pos = entries.binary_search(&e).unwrap_err();
entries.insert(pos, e);
state.ring_root = ring.write(slot, entries, &siblings);
let i = touch(&mut state.keys, &b.key_hash);
state.keys[i].blocks += 1;
touch(state, keys, &b.key_hash, block.daa, &mut |e| {
e.blocks += 1;
Ok(())
})?;
report.counted += 1;
}
for r in &reds {
@ -163,8 +176,10 @@ pub fn fold_block(state: &mut FinState, params: &FinParams, block: &ChainBlockWi
let (gone, kept): (Vec<RingEntry>, Vec<RingEntry>) = entries.into_iter().partition(|e| e.daa <= window_start);
if !gone.is_empty() {
for g in gone.iter().filter(|g| g.blue) {
let i = find_key(&state.keys, &g.key_hash).map_err(|_| format!("expired block {} names a key not in the table", hex32(&g.block_hash)))?;
state.keys[i].blocks = state.keys[i].blocks.checked_sub(1).ok_or("a key's block count went below zero")?;
touch(state, keys, &g.key_hash, block.daa, &mut |e| {
e.blocks = e.blocks.checked_sub(1).ok_or("a key's block count went below zero")?;
Ok(())
})?;
report.expired += 1;
}
state.ring_root = ring.write(slot, kept, &siblings);
@ -179,11 +194,13 @@ pub fn fold_block(state: &mut FinState, params: &FinParams, block: &ChainBlockWi
return Err("a key reveal's proof of possession does not verify".into());
}
let kh = vote_key_hash(&r.pubkey);
let i = touch(&mut state.keys, &kh);
if state.keys[i].revealed() && state.keys[i].pubkey != r.pubkey {
return Err("a key reveal names another key for a revealed hash".into());
}
state.keys[i].pubkey = r.pubkey;
touch(state, keys, &kh, block.daa, &mut |e| {
if e.revealed() && e.pubkey != r.pubkey {
return Err("a key reveal names another key for a revealed hash".into());
}
e.pubkey = r.pubkey;
Ok(())
})?;
}
for e in &witness.evidence {
if e.hash_a == e.hash_b {
@ -198,12 +215,14 @@ pub fn fold_block(state: &mut FinState, params: &FinParams, block: &ChainBlockWi
return Err("an equivocation vote does not verify".into());
}
let kh = vote_key_hash(&e.pubkey);
let i = touch(&mut state.keys, &kh);
let until = e.carrier_daa.saturating_add(params.equivocation_ban);
state.keys[i].ban_until = state.keys[i].ban_until.max(until);
if !state.keys[i].revealed() {
state.keys[i].pubkey = e.pubkey;
}
touch(state, keys, &kh, block.daa, &mut |k| {
k.ban_until = k.ban_until.max(until);
if !k.revealed() {
k.pubkey = e.pubkey;
}
Ok(())
})?;
}
for l in &witness.leaves {
if l.carrier_daa > block.daa {
@ -213,11 +232,14 @@ pub fn fold_block(state: &mut FinState, params: &FinParams, block: &ChainBlockWi
return Err("a leave does not verify".into());
}
let kh = vote_key_hash(&l.pubkey);
let i = touch(&mut state.keys, &kh);
if state.keys[i].leave_until == 0 {
state.keys[i].leave_from = l.carrier_daa.saturating_add(params.leave_delay);
state.keys[i].leave_until = l.carrier_daa.saturating_add(params.weight_window);
}
let (from, until) = (l.carrier_daa.saturating_add(params.leave_delay), l.carrier_daa.saturating_add(params.weight_window));
touch(state, keys, &kh, block.daa, &mut |k| {
if k.leave_until == 0 {
k.leave_from = from;
k.leave_until = until;
}
Ok(())
})?;
}
// 4. the certificates that landed with this block, against the history as it stood before it (every witness
@ -226,6 +248,7 @@ pub fn fold_block(state: &mut FinState, params: &FinParams, block: &ChainBlockWi
verify_certificate(state, params, c, bls)?;
report.locks += 1;
}
// slots whose entries emptied before this fold's touches stay empty; nothing to retain
// 5. staleness: a window without a lock (the frozen table never expires in the proof, design 4.4)
if state.lock.index > 0 && block.daa >= state.lock.daa.saturating_add(params.weight_window) {
@ -240,9 +263,7 @@ pub fn fold_block(state: &mut FinState, params: &FinParams, block: &ChainBlockWi
state.end_blue_score = h.blue_score;
state.end_blue_work = h.blue_work.clone();
}
state.keys.retain(|k| !k.is_empty_at(block.daa));
let (_, total) = voters_at(&state.keys, block.daa, params.dust);
let leaf = HistoryLeaf { number: block.number, block_hash: block.block_hash, daa: block.daa, table_root: state.table_root(), keys_hash: state.keys_hash(), total };
let leaf = HistoryLeaf { number: block.number, block_hash: block.block_hash, daa: block.daa, table_root: state.table_root(), keys_root: state.keys_root, key_count: state.key_count };
mmr::append(&mut state.peaks, &mut state.leaves, leaf.hash());
Ok(report)
}

View file

@ -0,0 +1,260 @@
//! The key table as a Merkle tree (docs/design/finality-in-proof.md section 2, the scale form): 2^KEY_DEPTH leaf
//! slots, a key takes the next free slot the first time it is seen and keeps it until its entry empties (a slot is
//! never reused), so the non-empty leaves are a prefix `0..key_count` and the fold touches one path per key it
//! changes instead of hashing the whole table at every block. The certificate step takes every entry once and
//! rebuilds the root over that dense prefix (about 2N hashes at N keys), sorts by key hash for the canonical voter
//! list of spec 3.10 C3 and refuses a key that appears twice.
//!
//! What a lying witness can do: insert a key a second time at a fresh slot (the guest cannot know the key has a
//! slot already). The split is refused at the next certificate (duplicate key hashes in the full table), so no
//! certificate verifies while it stands, and the native compare vetoes the record on the chain.
use crate::{sha256, KeyEntry, H, ZERO};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub const KEY_DEPTH: usize = 24;
pub fn leaf_hash(entry: Option<&KeyEntry>) -> H {
match entry {
None => ZERO,
Some(e) => {
let mut buf = Vec::with_capacity(1 + KeyEntry::LEN);
buf.push(5u8);
e.write(&mut buf);
sha256(&[&buf])
}
}
}
pub fn node_hash(left: &H, right: &H) -> H {
sha256(&[&[6u8], left, right])
}
pub fn defaults() -> Vec<H> {
let mut d = Vec::with_capacity(KEY_DEPTH + 1);
d.push(ZERO);
for l in 1..=KEY_DEPTH {
let below = d[l - 1];
d.push(node_hash(&below, &below));
}
d
}
pub fn empty_root() -> H {
defaults()[KEY_DEPTH]
}
pub fn root_from_path(slot: u64, leaf: H, siblings: &[H]) -> H {
assert_eq!(siblings.len(), KEY_DEPTH, "a key path has {KEY_DEPTH} siblings");
let mut h = leaf;
let mut idx = slot;
for s in siblings {
h = if idx & 1 == 0 { node_hash(&h, s) } else { node_hash(s, &h) };
idx >>= 1;
}
h
}
/// The root over the dense prefix: `entries` in slot order (every non-empty leaf below `key_count`), empty slots
/// between them the zero leaf, everything from `key_count` on the default subtrees.
pub fn dense_root(entries: &[(u64, KeyEntry)], key_count: u64) -> Result<H, String> {
let d = defaults();
let mut level: Vec<H> = Vec::with_capacity(key_count as usize);
let mut next_slot = 0u64;
for (slot, e) in entries {
if *slot < next_slot || *slot >= key_count {
return Err(format!("key table entries out of order or past key_count ({slot}, {key_count})"));
}
while next_slot < *slot {
level.push(ZERO);
next_slot += 1;
}
level.push(leaf_hash(Some(e)));
next_slot += 1;
}
while next_slot < key_count {
level.push(ZERO);
next_slot += 1;
}
for l in 0..KEY_DEPTH {
if level.is_empty() {
return Ok(d[KEY_DEPTH]);
}
let mut next = Vec::with_capacity(level.len().div_ceil(2));
for i in (0..level.len()).step_by(2) {
let r = level.get(i + 1).copied().unwrap_or(d[l]);
next.push(node_hash(&level[i], &r));
}
level = next;
}
Ok(level[0])
}
/// The canonical voter order: by key hash, no key twice.
pub fn canonical(entries: &[(u64, KeyEntry)]) -> Result<Vec<KeyEntry>, String> {
let mut v: Vec<KeyEntry> = entries.iter().map(|(_, e)| e.clone()).collect();
v.sort_by(|a, b| a.key_hash.cmp(&b.key_hash));
if v.windows(2).any(|p| p[0].key_hash == p[1].key_hash) {
return Err("a key appears twice in the table".into());
}
Ok(v)
}
/// One key leaf opened for the guest: the slot the key sits at (or the next free slot when absent), its entry and
/// the siblings as they stand at that moment of the fold.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct KeyLeafWitness {
pub slot: u64,
pub entry: Option<KeyEntry>,
pub siblings: Vec<H>,
}
pub trait KeyAccess {
/// Opens the leaf of `key`: its slot, its entry (None when the key has no slot: the slot is then `key_count`,
/// the next free one) and the siblings, checked against `root`.
fn touch(&mut self, key: &H, root: &H, key_count: u64) -> Result<(u64, Option<KeyEntry>, Vec<H>), String>;
/// Writes `entry` (None empties the leaf) to `slot` and returns the new root.
fn write(&mut self, slot: u64, entry: Option<KeyEntry>, siblings: &[H]) -> H;
}
/// The guest's table: a queue of witnesses.
pub struct WitnessKeys {
pub witnesses: std::collections::VecDeque<KeyLeafWitness>,
}
impl WitnessKeys {
pub fn new(witnesses: Vec<KeyLeafWitness>) -> Self {
Self { witnesses: witnesses.into() }
}
}
impl KeyAccess for WitnessKeys {
fn touch(&mut self, key: &H, root: &H, key_count: u64) -> Result<(u64, Option<KeyEntry>, Vec<H>), String> {
let w = self.witnesses.pop_front().ok_or("key witness missing")?;
match &w.entry {
Some(e) => {
if e.key_hash != *key || w.slot >= key_count {
return Err("key witness names another key or a slot past the table".into());
}
}
None => {
if w.slot != key_count {
return Err(format!("a new key takes slot {key_count}, the witness names {}", w.slot));
}
}
}
if root_from_path(w.slot, leaf_hash(w.entry.as_ref()), &w.siblings) != *root {
return Err("key witness does not open the table root".into());
}
Ok((w.slot, w.entry, w.siblings))
}
fn write(&mut self, slot: u64, entry: Option<KeyEntry>, siblings: &[H]) -> H {
root_from_path(slot, leaf_hash(entry.as_ref()), siblings)
}
}
/// The native table (the tracker, the host, the tests): every entry by slot, the key index, a sparse node map,
/// and the witnesses recorded for the guest.
#[derive(Clone, Debug)]
pub struct SparseKeys {
defaults: Vec<H>,
nodes: HashMap<(u8, u64), H>,
entries: Vec<Option<KeyEntry>>,
index: HashMap<H, u64>,
pub recorded: Vec<KeyLeafWitness>,
}
impl Default for SparseKeys {
fn default() -> Self {
Self::new()
}
}
impl SparseKeys {
pub fn new() -> Self {
Self { defaults: defaults(), nodes: HashMap::new(), entries: Vec::new(), index: HashMap::new(), recorded: Vec::new() }
}
fn node(&self, level: u8, idx: u64) -> H {
self.nodes.get(&(level, idx)).copied().unwrap_or(self.defaults[level as usize])
}
pub fn root(&self) -> H {
self.node(KEY_DEPTH as u8, 0)
}
pub fn key_count(&self) -> u64 {
self.entries.len() as u64
}
pub fn siblings(&self, slot: u64) -> Vec<H> {
let mut idx = slot;
let mut out = Vec::with_capacity(KEY_DEPTH);
for level in 0..KEY_DEPTH as u8 {
out.push(self.node(level, idx ^ 1));
idx >>= 1;
}
out
}
pub fn get(&self, key: &H) -> Option<&KeyEntry> {
self.index.get(key).and_then(|&s| self.entries[s as usize].as_ref())
}
/// Every non-empty entry with its slot, slot order (the certificate step's table).
pub fn all_entries(&self) -> Vec<(u64, KeyEntry)> {
self.entries.iter().enumerate().filter_map(|(i, e)| e.as_ref().map(|e| (i as u64, e.clone()))).collect()
}
pub fn set(&mut self, slot: u64, entry: Option<KeyEntry>) {
if slot as usize >= self.entries.len() {
self.entries.resize(slot as usize + 1, None);
}
if let Some(old) = &self.entries[slot as usize] {
self.index.remove(&old.key_hash);
}
if let Some(e) = &entry {
self.index.insert(e.key_hash, slot);
}
let mut h = leaf_hash(entry.as_ref());
self.entries[slot as usize] = entry;
let mut idx = slot;
for level in 0..=KEY_DEPTH as u8 {
if h == self.defaults[level as usize] {
self.nodes.remove(&(level, idx));
} else {
self.nodes.insert((level, idx), h);
}
if level == KEY_DEPTH as u8 {
break;
}
let sib = self.node(level, idx ^ 1);
h = if idx & 1 == 0 { node_hash(&h, &sib) } else { node_hash(&sib, &h) };
idx >>= 1;
}
}
pub fn take_witnesses(&mut self) -> Vec<KeyLeafWitness> {
std::mem::take(&mut self.recorded)
}
}
impl KeyAccess for SparseKeys {
fn touch(&mut self, key: &H, root: &H, key_count: u64) -> Result<(u64, Option<KeyEntry>, Vec<H>), String> {
if self.root() != *root || self.key_count() != key_count {
return Err("the native key table differs from the state's".into());
}
let slot = self.index.get(key).copied().unwrap_or(key_count);
let entry = if slot < key_count { self.entries[slot as usize].clone() } else { None };
let siblings = self.siblings(slot);
self.recorded.push(KeyLeafWitness { slot, entry: entry.clone(), siblings: siblings.clone() });
Ok((slot, entry, siblings))
}
fn write(&mut self, slot: u64, entry: Option<KeyEntry>, _siblings: &[H]) -> H {
self.set(slot, entry);
self.root()
}
}

View file

@ -13,6 +13,7 @@ pub mod bls;
pub mod cert;
pub mod fold;
pub mod header;
pub mod keys;
pub mod mmr;
pub mod ring;
pub mod tracker;
@ -165,16 +166,6 @@ impl KeyEntry {
}
}
/// The key table: sorted by key hash, which is the canonical voter order of spec 3.10 C3.
pub fn keys_hash(keys: &[KeyEntry]) -> H {
let mut buf = Vec::with_capacity(8 + keys.len() * KeyEntry::LEN);
buf.extend_from_slice(&(keys.len() as u64).to_le_bytes());
for k in keys {
k.write(&mut buf);
}
sha256(&[b"igneum-fin-keys-v1", &buf])
}
/// The canonical voter list at `daa` (positions index a certificate's bitmap) and the total weight.
pub fn voters_at(keys: &[KeyEntry], daa: u64, dust: u64) -> (Vec<usize>, u64) {
let mut positions = Vec::new();
@ -223,8 +214,9 @@ pub struct FinState {
pub end_blue_work: Vec<u8>,
/// The first chain block this attestation counted from (section 1 of the design: `history_first`).
pub history_first: u64,
/// Sorted by key hash.
pub keys: Vec<KeyEntry>,
/// The key table's Merkle root and the number of slots taken (`keys`).
pub keys_root: H,
pub key_count: u64,
/// Root of the block ring (`ring`).
pub ring_root: H,
/// The history MMR's peaks, left to right, and its leaf count (`mmr`).
@ -310,7 +302,8 @@ impl FinState {
end_blue_score: 0,
end_blue_work: Vec::new(),
history_first: first_number,
keys: Vec::new(),
keys_root: keys::empty_root(),
key_count: 0,
ring_root: ring::empty_root(),
peaks: Vec::new(),
leaves: 0,
@ -319,13 +312,9 @@ impl FinState {
}
}
pub fn keys_hash(&self) -> H {
keys_hash(&self.keys)
}
/// `sha256("igneum-fin-state-v1" || params_hash || end_daa || end_number || keys_hash || ring_root)`.
/// `sha256("igneum-fin-state-v1" || params || end block || keys_root || key_count || ring_root)`.
pub fn table_root(&self) -> H {
sha256(&[b"igneum-fin-state-v1", &self.params_hash, &self.end_daa.to_le_bytes(), &self.end_number.to_le_bytes(), &self.end_hash, &self.end_blue_score.to_le_bytes(), &(self.end_blue_work.len() as u64).to_le_bytes(), &self.end_blue_work, &self.keys_hash(), &self.ring_root])
sha256(&[b"igneum-fin-state-v1", &self.params_hash, &self.end_daa.to_le_bytes(), &self.end_number.to_le_bytes(), &self.end_hash, &self.end_blue_score.to_le_bytes(), &(self.end_blue_work.len() as u64).to_le_bytes(), &self.end_blue_work, &self.keys_root, &self.key_count.to_le_bytes(), &self.ring_root])
}
pub fn history_root(&self) -> H {

View file

@ -12,14 +12,14 @@ pub struct HistoryLeaf {
pub daa: u64,
/// The table root (keys and ring) after this block was folded.
pub table_root: H,
/// The keys hash alone, so a certificate's table witness can be checked without the ring.
pub keys_hash: H,
pub total: u64,
/// The key table's root and slot count alone, so a certificate's table witness is checked without the ring.
pub keys_root: H,
pub key_count: u64,
}
impl HistoryLeaf {
pub fn hash(&self) -> H {
sha256(&[&[4u8], &self.number.to_le_bytes(), &self.block_hash, &self.daa.to_le_bytes(), &self.table_root, &self.keys_hash, &self.total.to_le_bytes()])
sha256(&[&[4u8], &self.number.to_le_bytes(), &self.block_hash, &self.daa.to_le_bytes(), &self.table_root, &self.keys_root, &self.key_count.to_le_bytes()])
}
}

View file

@ -10,9 +10,10 @@ use igneum_fin_core::bls::{Bls, ZkBls};
use igneum_fin_core::cert::{signer_positions, CertificateWitness, TableAt};
use igneum_fin_core::fold::{fold_block, BlueBlock, ChainBlockWitness, FoldWitness, LeaveWitness, Reveal};
use igneum_fin_core::mmr::{HistoryLeaf, Mmr};
use igneum_fin_core::keys::{canonical, SparseKeys, WitnessKeys};
use igneum_fin_core::ring::WitnessRing;
use igneum_fin_core::tracker::SparseRing;
use igneum_fin_core::{keys_hash, vote_key_hash, vote_message, voters_at, FinExt, FinParams, FinState, KeyEntry, DST_LEAVE, DST_POP, DST_VOTE, H, PUBKEY_LEN, SIG_LEN};
use igneum_fin_core::{vote_key_hash, vote_message, voters_at, FinExt, FinParams, FinState, KeyEntry, DST_LEAVE, DST_POP, DST_VOTE, H, PUBKEY_LEN, SIG_LEN};
use std::collections::HashMap;
struct Key {
@ -71,11 +72,12 @@ struct Sim {
keys: Vec<Key>,
/// (number, hash, daa, blues) per chain block
blocks: Vec<ChainBlockWitness>,
/// the full table after each block (number -> keys) and the history leaves, from the tracker's fold
tables: HashMap<u64, Vec<KeyEntry>>,
/// the full table after each block (number -> entries by slot) and the history leaves, from the tracker's fold
tables: HashMap<u64, Vec<(u64, KeyEntry)>>,
mmr: Mmr,
leaves: HashMap<u64, HistoryLeaf>,
tracker: SparseRing,
keytree: SparseKeys,
state: FinState,
bls: BlstBls,
}
@ -84,7 +86,7 @@ impl Sim {
fn new(params: FinParams, labels: &[&str]) -> Self {
let keys: Vec<Key> = labels.iter().map(|l| key(l)).collect();
let state = FinState::empty(&params, 0);
Self { params, keys, blocks: Vec::new(), tables: HashMap::new(), mmr: Mmr::default(), leaves: HashMap::new(), tracker: SparseRing::new(), state, bls: BlstBls }
Self { params, keys, blocks: Vec::new(), tables: HashMap::new(), mmr: Mmr::default(), leaves: HashMap::new(), tracker: SparseRing::new(), keytree: SparseKeys::new(), state, bls: BlstBls }
}
/// Chain block `n` at DAA `n`, mined by key `miner`, with `extra` more blue blocks by the keys named; folds it.
@ -99,7 +101,7 @@ impl Sim {
// whenever a key whose entry aged out mines again)
let mut witness = witness;
for b in &block.blues {
let known = igneum_fin_core::find_key(&self.state.keys, &b.key_hash).map(|i| self.state.keys[i].revealed()).unwrap_or(false);
let known = self.keytree.get(&b.key_hash).map(|e| e.revealed()).unwrap_or(false);
if !known && !witness.reveals.iter().any(|r| vote_key_hash(&r.pubkey) == b.key_hash) {
let k = self.keys.iter().find(|k| k.hash == b.key_hash).unwrap();
witness.reveals.push(reveal(k));
@ -107,32 +109,45 @@ impl Sim {
}
let before = self.state.clone();
let ring_before = self.tracker.clone();
let r = fold_block(&mut self.state, &self.params, &block, &mut self.tracker, &witness, &self.bls);
let keys_before = self.keytree.clone();
let r = fold_block(&mut self.state, &self.params, &block, &mut self.tracker, &mut self.keytree, &witness, &self.bls);
let witnesses = self.tracker.take_witnesses();
let key_witnesses = self.keytree.take_witnesses();
match r {
Ok(_) => {}
Err(e) => {
self.state = before;
self.tracker = ring_before;
self.keytree = keys_before;
return Err(e);
}
}
// the guest's path: the same fold through the witnesses, from the same previous state, must agree
let mut replay = before;
let mut ring = WitnessRing::new(witnesses);
fold_block(&mut replay, &self.params, &block, &mut ring, &witness, &ZkBls).expect("the witnessed replay folds");
let mut keys = WitnessKeys::new(key_witnesses);
fold_block(&mut replay, &self.params, &block, &mut ring, &mut keys, &witness, &ZkBls).expect("the witnessed replay folds");
assert_eq!(replay.extension(), self.state.extension(), "the guest's fold and the tracker's agree at block {n}");
assert!(ring.witnesses.is_empty(), "every witness consumed");
let (_, total) = voters_at(&self.state.keys, n, self.params.dust);
let leaf = HistoryLeaf { number: n, block_hash: block.block_hash, daa: n, table_root: self.state.table_root(), keys_hash: self.state.keys_hash(), total };
assert!(ring.witnesses.is_empty() && keys.witnesses.is_empty(), "every witness consumed");
let leaf = HistoryLeaf { number: n, block_hash: block.block_hash, daa: n, table_root: self.state.table_root(), keys_root: self.state.keys_root, key_count: self.state.key_count };
self.mmr.append(leaf.hash());
assert_eq!(self.mmr.root(), self.state.history_root());
self.leaves.insert(n, leaf);
self.tables.insert(n, self.state.keys.clone());
self.tables.insert(n, self.keytree.all_entries());
self.blocks.push(block);
Ok(())
}
/// The canonical table after block `number`.
fn table(&self, number: u64) -> Vec<KeyEntry> {
canonical(&self.tables[&number]).unwrap()
}
/// A key's blocks in the current table.
fn blocks_of(&self, key: &Key) -> u64 {
self.keytree.get(&key.hash).map(|e| e.blocks).unwrap_or(0)
}
/// The brute-force window count: blue blocks per key with DAA in (daa - W, daa].
fn brute(&self, daa: u64) -> HashMap<H, u64> {
let mut m = HashMap::new();
@ -153,7 +168,7 @@ impl Sim {
/// A certificate for index `index` over chain block `number`, signed by `signers`.
fn certificate(&self, index: u64, number: u64, signers: &[usize]) -> CertificateWitness {
let checkpoint = self.blocks[number as usize].block_hash;
let table = &self.tables[&number];
let table = &self.table(number);
let (voters, _) = voters_at(table, number, self.params.dust);
let msg = vote_message(&self.params.chain_id, index, &checkpoint);
let mut sigs = Vec::new();
@ -191,12 +206,11 @@ fn the_carried_table_equals_a_brute_force_count_of_the_window_at_every_block() {
let extra: Vec<usize> = if n % 7 == 0 { vec![1, 2] } else if n % 11 == 0 { vec![3] } else { vec![] };
sim.mine(miner, &extra, FoldWitness::default()).unwrap();
let brute = sim.brute(n);
for k in &sim.state.keys {
for (_, k) in sim.keytree.all_entries() {
assert_eq!(k.blocks, brute.get(&k.key_hash).copied().unwrap_or(0), "key blocks at block {n}");
}
for (kh, b) in &brute {
let i = igneum_fin_core::find_key(&sim.state.keys, kh).expect("every key with blocks is in the table");
assert_eq!(sim.state.keys[i].blocks, *b);
assert_eq!(sim.keytree.get(kh).expect("every key with blocks is in the table").blocks, *b);
}
let ring_count: u64 = sim.tracker.all_entries().iter().filter(|e| e.blue).count() as u64;
assert_eq!(ring_count, brute.values().sum::<u64>(), "the ring holds exactly the window at block {n}");
@ -214,18 +228,17 @@ fn a_block_counted_twice_is_refused_and_ageing_is_exact_at_the_edge() {
let dup = BlueBlock { block_hash: hash_of("dup", 1), key_hash: sim.keys[1].hash, daa: n };
let block = ChainBlockWitness { number: n, block_hash: hash_of("chain", n), daa: n, blues: vec![BlueBlock { block_hash: hash_of("chain", n), key_hash: sim.keys[0].hash, daa: n }, dup.clone(), dup], headers: Vec::new() };
let mut s = sim.state.clone();
let err = fold_block(&mut s, &sim.params, &block, &mut sim.tracker, &FoldWitness::default(), &sim.bls).unwrap_err();
let err = fold_block(&mut s, &sim.params, &block, &mut sim.tracker.clone(), &mut sim.keytree.clone(), &FoldWitness::default(), &sim.bls).unwrap_err();
assert!(err.contains("counted twice"), "{err}");
sim.tracker.take_witnesses();
// ageing: with W = 200, the block at DAA d leaves exactly when the chain reaches d + 200
let mut sim = Sim::new(params(), &["a", "b"]);
for _ in 0..201u64 {
sim.mine(0, &[], FoldWitness::default()).unwrap();
}
// blocks 0..=200 mined; at DAA 200 the window is (0, 200]: block 0 is out, 200 blocks counted
assert_eq!(sim.state.keys[0].blocks, 200);
assert_eq!(sim.blocks_of(&sim.keys[0]), 200);
sim.mine(0, &[], FoldWitness::default()).unwrap();
assert_eq!(sim.state.keys[0].blocks, 200, "one in, one out");
assert_eq!(sim.blocks_of(&sim.keys[0]), 200, "one in, one out");
}
/// Builds a chain past the first-month gate with four keys of weight 40, 30, 20 and 10 percent, every key revealed.
@ -246,7 +259,8 @@ fn known_failed_first_todays_light_client_takes_the_voter_list_from_a_node_and_t
// keys a, b, c, d hold 40, 30, 20, 10 percent of the real 200-block window; e is the attacker with nothing
let mut sim = chain_past_the_gate(&["a", "b", "c", "d", "e"]);
let cp = 250u64;
let real_table = sim.tables[&cp].clone();
let real_entries = sim.tables[&cp].clone();
let real_table = sim.table(cp);
let (real_voters, real_total) = voters_at(&real_table, cp, sim.params.dust);
assert_eq!(real_voters.len(), 4, "e mined nothing and is no voter");
@ -260,7 +274,7 @@ fn known_failed_first_todays_light_client_takes_the_voter_list_from_a_node_and_t
e_entry.pubkey = e.pk;
e_entry.blocks = real_total * 7 / 3 + 1;
let pos = forged.binary_search_by(|k| k.key_hash.cmp(&e.hash)).unwrap_err();
forged.insert(pos, e_entry);
forged.insert(pos, e_entry.clone());
let (forged_voters, forged_total) = voters_at(&forged, cp, sim.params.dust);
let checkpoint = sim.blocks[cp as usize].block_hash;
let msg = vote_message(&sim.params.chain_id, 9, &checkpoint);
@ -280,7 +294,9 @@ fn known_failed_first_todays_light_client_takes_the_voter_list_from_a_node_and_t
// is refused (the table at the checkpoint is the one the proof committed), and with the real table it is
// refused too (e holds nothing there and is no voter: the bitmap names a position outside the list, or e's
// weight is zero).
let forged_at = TableAt { leaf: sim.leaves[&cp].clone(), proof: sim.mmr.proof(cp).unwrap(), keys: forged.clone() };
let mut forged_entries = real_entries.clone();
forged_entries.push((real_entries.len() as u64 + 7, e_entry.clone()));
let forged_at = TableAt { leaf: sim.leaves[&cp].clone(), proof: sim.mmr.proof(cp).unwrap(), keys: forged_entries };
let cert = CertificateWitness { index: 9, checkpoint, voter_count: forged_voters.len() as u32, bitmap: bitmap.clone(), signature: sig, signer_points: Vec::new(), at: forged_at, frozen: None };
let mut w = FoldWitness::default();
w.certificates.push(cert);
@ -323,7 +339,7 @@ fn two_thirds_is_inclusive_and_under_it_is_refused() {
sim.mine((n % 6) as usize, &[], FoldWitness::default()).unwrap();
}
let cp = 240u64;
let (_, total) = voters_at(&sim.tables[&cp], cp, sim.params.dust);
let (_, total) = voters_at(&sim.table(cp), cp, sim.params.dust);
assert_eq!(total, 240);
let three = sim.certificate(8, cp, &[0, 1, 2]);
let mut w = FoldWitness::default();
@ -366,8 +382,8 @@ fn the_frozen_table_holds_a_side_without_its_partner_and_a_leave_releases_it_aft
sim.mine(0, &[], FoldWitness::default()).unwrap();
}
let n = sim.blocks.len() as u64 - 1;
let (_, total) = voters_at(&sim.state.keys, n, sim.params.dust);
let a_blocks = sim.state.keys.iter().find(|k| k.key_hash == sim.keys[0].hash).unwrap().blocks;
let (_, total) = voters_at(&sim.table(n), n, sim.params.dust);
let a_blocks = sim.blocks_of(&sim.keys[0]);
assert!(FinParams::floor_met(a_blocks, total), "a holds two thirds of the sliding table by now ({a_blocks} of {total})");
let cert = sim.certificate(12, n, &[0]);
let mut w = FoldWitness::default();
@ -453,14 +469,14 @@ fn a_light_client_answers_final_at_checkpoint_from_the_extension_and_a_history_p
let mut bad = sim.leaves[&240].clone();
bad.block_hash = hash_of("other", 240);
assert!(!sim.mmr.proof(240).unwrap().verify(&bad.hash(), &ext.history_root, leaves));
assert_eq!(keys_hash(&sim.tables[&250]), sim.leaves[&250].keys_hash);
assert_eq!(igneum_fin_core::keys::dense_root(&sim.tables[&250], sim.leaves[&250].key_count).unwrap(), sim.leaves[&250].keys_root);
}
#[test]
fn the_guest_curve_and_blst_agree_on_real_certificates_and_disagree_with_nothing() {
let sim = chain_past_the_gate(&["a", "b", "c"]);
let cert = sim.certificate(9, 250, &[0, 1]);
let table = &sim.tables[&250];
let table = &sim.table(250);
let (voters, _) = voters_at(table, 250, sim.params.dust);
let pks: Vec<[u8; PUBKEY_LEN]> = signer_positions(&cert.bitmap, cert.voter_count).iter().map(|&p| table[voters[p]].pubkey).collect();
let msg = vote_message(&sim.params.chain_id, 9, &cert.checkpoint);
@ -495,6 +511,7 @@ fn level_one_pins_the_blues_to_headers_and_parent_links() {
let p = params();
let mut state = FinState::empty(&p, 0);
let mut ring = SparseRing::new();
let mut kt = SparseKeys::new();
let keys: Vec<Key> = ["a", "b", "c"].iter().map(|l| key(l)).collect();
let hdr = |parents: Vec<H>, daa: u64, blue_score: u64, work: u8, key: &Key, blue: bool| HeaderWitness { version: 2, parents_by_level: vec![parents], hash_merkle_root: hash_of("m", daa), accepted_id_merkle_root: ZERO_H, utxo_commitment: ZERO_H, timestamp: 1_000 + daa, bits: 0x1e00ffff, nonce: daa * 7, daa_score: daa, blue_work: vec![work], blue_score, pruning_point: ZERO_H, vote_key_hash: key.hash, blue };
// block 0 (genesis-like, no parents), by a
@ -502,8 +519,9 @@ fn level_one_pins_the_blues_to_headers_and_parent_links() {
let b0 = ChainBlockWitness { number: 0, block_hash: h0.hash(), daa: 0, blues: vec![BlueBlock { block_hash: h0.hash(), key_hash: keys[0].hash, daa: 0 }], headers: vec![h0.clone()] };
let mut w = FoldWitness::default();
w.reveals = keys.iter().map(reveal).collect();
fold_block(&mut state, &p, &b0, &mut ring, &w, &BlstBls).unwrap();
fold_block(&mut state, &p, &b0, &mut ring, &mut kt, &w, &BlstBls).unwrap();
ring.take_witnesses();
kt.take_witnesses();
// a side block s by b (parent: block 0) and a red r by c (parent: block 0); block 1 by a with parents [0, s, r]
let hs = hdr(vec![h0.hash()], 1, 1, 2, &keys[1], true);
let hr = hdr(vec![h0.hash()], 1, 1, 1, &keys[2], false);
@ -512,33 +530,34 @@ fn level_one_pins_the_blues_to_headers_and_parent_links() {
// known-failed cases first
let mut swapped = good.clone();
swapped.blues[1].key_hash = keys[2].hash;
let e = fold_block(&mut state.clone(), &p, &swapped, &mut ring.clone(), &FoldWitness::default(), &BlstBls).unwrap_err();
let e = fold_block(&mut state.clone(), &p, &swapped, &mut ring.clone(), &mut kt.clone(), &FoldWitness::default(), &BlstBls).unwrap_err();
assert!(e.contains("differs from its header"), "{e}");
let mut miscount = good.clone();
miscount.blues.push(BlueBlock { block_hash: hr.hash(), key_hash: keys[2].hash, daa: 1 });
let e = fold_block(&mut state.clone(), &p, &miscount, &mut ring.clone(), &FoldWitness::default(), &BlstBls).unwrap_err();
let e = fold_block(&mut state.clone(), &p, &miscount, &mut ring.clone(), &mut kt.clone(), &FoldWitness::default(), &BlstBls).unwrap_err();
assert!(e.contains("blue score") || e.contains("differs from its header"), "{e}");
let mut unreached = good.clone();
let hx = hdr(vec![hash_of("nowhere", 9)], 1, 1, 1, &keys[1], false);
unreached.headers.push(hx);
let e = fold_block(&mut state.clone(), &p, &unreached, &mut ring.clone(), &FoldWitness::default(), &BlstBls).unwrap_err();
let e = fold_block(&mut state.clone(), &p, &unreached, &mut ring.clone(), &mut kt.clone(), &FoldWitness::default(), &BlstBls).unwrap_err();
assert!(e.contains("not reached"), "{e}");
let mut wrong_sp = good.clone();
wrong_sp.headers[0].parents_by_level = vec![vec![hs.hash(), hr.hash()]];
wrong_sp.block_hash = wrong_sp.headers[0].hash();
wrong_sp.blues[0].block_hash = wrong_sp.block_hash;
let e = fold_block(&mut state.clone(), &p, &wrong_sp, &mut ring.clone(), &FoldWitness::default(), &BlstBls).unwrap_err();
let e = fold_block(&mut state.clone(), &p, &wrong_sp, &mut ring.clone(), &mut kt.clone(), &FoldWitness::default(), &BlstBls).unwrap_err();
assert!(e.contains("not a direct parent"), "{e}");
// the good block folds: b credited for s, c not credited for r, r in the ring uncounted
fold_block(&mut state, &p, &good, &mut ring, &FoldWitness::default(), &BlstBls).unwrap();
fold_block(&mut state, &p, &good, &mut ring, &mut kt, &FoldWitness::default(), &BlstBls).unwrap();
ring.take_witnesses();
let w_of = |k: &Key| igneum_fin_core::find_key(&state.keys, &k.hash).map(|i| state.keys[i].blocks).unwrap_or(0);
kt.take_witnesses();
let w_of = |k: &Key| kt.get(&k.hash).map(|e| e.blocks).unwrap_or(0);
assert_eq!((w_of(&keys[0]), w_of(&keys[1]), w_of(&keys[2])), (2, 1, 0));
assert!(ring.all_entries().iter().any(|e| e.block_hash == hr.hash() && !e.blue));
// block 2 replays r as a blue: refused by the ring
let h2 = hdr(vec![h1.hash(), hr.hash()], 2, 4, 20, &keys[0], true);
let replay = ChainBlockWitness { number: 2, block_hash: h2.hash(), daa: 2, blues: vec![BlueBlock { block_hash: h2.hash(), key_hash: keys[0].hash, daa: 2 }, BlueBlock { block_hash: hr.hash(), key_hash: keys[2].hash, daa: 1 }], headers: vec![h2.clone(), { let mut x = hr.clone(); x.blue = true; x }] };
let e = fold_block(&mut state.clone(), &p, &replay, &mut ring.clone(), &FoldWitness::default(), &BlstBls).unwrap_err();
let e = fold_block(&mut state.clone(), &p, &replay, &mut ring.clone(), &mut kt.clone(), &FoldWitness::default(), &BlstBls).unwrap_err();
assert!(e.contains("counted twice") || e.contains("not a direct parent") || e.contains("listed twice"), "{e}");
}

View file

@ -6,6 +6,7 @@ use anyhow::{anyhow, bail, Context, Result};
use igneum_fin_core::cert::TableAt;
use igneum_fin_core::fold::{fold_block, ChainBlockWitness, FoldWitness};
use igneum_fin_core::mmr::{HistoryLeaf, MmrProof};
use igneum_fin_core::keys::{KeyLeafWitness, SparseKeys, WitnessKeys};
use igneum_fin_core::ring::{RingLeafWitness, WitnessRing};
use igneum_fin_core::{FinExt, FinParams, FinState};
use igneum_prove_core::agg::{BlockOutput, FinInput};
@ -17,6 +18,8 @@ pub struct BlockFin {
pub block: ChainBlockWitness,
pub ring: Vec<RingLeafWitness>,
#[serde(default)]
pub keys: Vec<KeyLeafWitness>,
#[serde(default)]
pub witness: FoldWitness,
}
@ -51,11 +54,12 @@ pub fn prepare(file: &FinWitnessFile, first_number: u64, rooted: bool) -> Result
if b.block.number != first_number + i as u64 {
bail!("finality witness {i} is of chain block {}, expected {}", b.block.number, first_number + i as u64);
}
let input = FinInput { params: file.params.clone(), prev_state: state.clone(), block: b.block.clone(), ring: b.ring.clone(), witness: b.witness.clone() };
let input = FinInput { params: file.params.clone(), prev_state: state.clone(), block: b.block.clone(), ring: b.ring.clone(), keys: b.keys.clone(), witness: b.witness.clone() };
let mut ring = WitnessRing::new(b.ring.clone());
fold_block(&mut state, &file.params, &b.block, &mut ring, &b.witness, &igneum_fin_core::bls::ZkBls).map_err(|e| anyhow!("finality fold of chain block {} natively: {e}", b.block.number))?;
if !ring.witnesses.is_empty() {
bail!("finality witness of chain block {} carries {} ring leaves the fold did not open", b.block.number, ring.witnesses.len());
let mut keys = WitnessKeys::new(b.keys.clone());
fold_block(&mut state, &file.params, &b.block, &mut ring, &mut keys, &b.witness, &igneum_fin_core::bls::ZkBls).map_err(|e| anyhow!("finality fold of chain block {} natively: {e}", b.block.number))?;
if !ring.witnesses.is_empty() || !keys.witnesses.is_empty() {
bail!("finality witness of chain block {} carries {} ring and {} key leaves the fold did not open", b.block.number, ring.witnesses.len(), keys.witnesses.len());
}
out.push(input);
}
@ -131,7 +135,7 @@ pub fn load_query(path: &str) -> Result<BlockQuery> {
/// Certificate witnesses carry tables; this is what one weighs on the wire.
pub fn table_bytes(t: &TableAt) -> usize {
t.keys.len() * igneum_fin_core::KeyEntry::LEN + 32 * (t.proof.siblings.len() + t.proof.peaks.len())
t.keys.len() * (8 + igneum_fin_core::KeyEntry::LEN) + 32 * (t.proof.siblings.len() + t.proof.peaks.len())
}
// ---------------------------------------------------------------------------------------------------------------
@ -145,6 +149,7 @@ 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_fin_core::keys::canonical;
use igneum_prove_core::Fixture;
struct SynthKey {
@ -184,6 +189,7 @@ pub fn synth(fixtures: &[Fixture], keys: usize, voters: usize, window_blocks: u6
let root_number = first.block.env.number - prefix;
let mut state = FinState::empty(&params, root_number);
let mut ring = SparseRing::new();
let mut keys_tree = SparseKeys::new();
let bls = igneum_fin_core::bls::ZkBls;
let mut mmr = Mmr::default();
for i in 0..prefix {
@ -191,28 +197,29 @@ pub fn synth(fixtures: &[Fixture], keys: usize, voters: usize, window_blocks: u6
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 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 }], headers: Vec::new() };
let mut w = FoldWitness::default();
reveals_for(&state, &synth_keys, &block, &mut w);
fold_block(&mut state, &params, &block, &mut ring, &w, &bls).map_err(|e| anyhow!("prefix fold {n}: {e}"))?;
reveals_for(&keys_tree, &synth_keys, &block, &mut w);
fold_block(&mut state, &params, &block, &mut ring, &mut keys_tree, &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());
keys_tree.take_witnesses();
mmr.append(HistoryLeaf { number: n, block_hash: block.block_hash, daa, table_root: state.table_root(), keys_root: state.keys_root, key_count: state.key_count }.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 tables: Vec<(u64, Vec<(u64, KeyEntry)>, HistoryLeaf)> = Vec::new();
let mut blocks = Vec::new();
for (i, f) in fixtures.iter().enumerate() {
let n = f.block.env.number;
let daa = (daa_base + i as u64).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 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 }], headers: Vec::new() };
let mut w = FoldWitness::default();
reveals_for(&state, &synth_keys, &block, &mut w);
reveals_for(&keys_tree, &synth_keys, &block, &mut w);
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 (cp_number, cp_entries, cp_leaf) = tables[tables.len() - cert_back].clone();
let cp_keys = canonical(&cp_entries).map_err(|e| anyhow!(e))?;
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));
@ -239,22 +246,21 @@ pub fn synth(fixtures: &[Fixture], keys: usize, voters: usize, window_blocks: u6
let signer_points: Vec<Vec<u8>> = by_pos.iter().map(|(_, v)| { let kh = cp_keys[**v].key_hash; synth_keys.iter().find(|k| k.hash == kh).unwrap().sk.sk_to_pk().serialize().to_vec() }).collect();
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, signer_points, at: TableAt { leaf: cp_leaf, proof, keys: cp_keys }, frozen: None });
w.certificates.push(CertificateWitness { index, checkpoint: cp_leaf.block_hash, voter_count: vlist.len() as u32, bitmap, signature: agg, signer_points, at: TableAt { leaf: cp_leaf, proof, keys: cp_entries }, 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 };
fold_block(&mut state, &params, &block, &mut ring, &mut keys_tree, &w, &bls).map_err(|e| anyhow!("fold {n}: {e}"))?;
let leaf = HistoryLeaf { number: n, block_hash: block.block_hash, daa, table_root: state.table_root(), keys_root: state.keys_root, key_count: state.key_count };
mmr.append(leaf.hash());
tables.push((n, state.keys.clone(), leaf));
blocks.push(BlockFin { block, ring: ring.take_witnesses(), witness: w });
tables.push((n, keys_tree.all_entries(), leaf));
blocks.push(BlockFin { block, ring: ring.take_witnesses(), keys: keys_tree.take_witnesses(), witness: w });
}
Ok(FinWitnessFile { format: FORMAT.into(), params, root_state, blocks })
}
/// W1 as the tracker applies it: a key's reveal rides with its first block in the table.
fn reveals_for(state: &FinState, keys: &[SynthKey], block: &ChainBlockWitness, w: &mut FoldWitness) {
fn reveals_for(table: &SparseKeys, keys: &[SynthKey], block: &ChainBlockWitness, w: &mut FoldWitness) {
for b in &block.blues {
let known = igneum_fin_core::find_key(&state.keys, &b.key_hash).map(|i| state.keys[i].revealed()).unwrap_or(false);
let known = table.get(&b.key_hash).map(|e| e.revealed()).unwrap_or(false);
if !known && !w.reveals.iter().any(|r| vote_key_hash(&r.pubkey) == b.key_hash) {
if let Some(k) = keys.iter().find(|k| k.hash == b.key_hash) {
w.reveals.push(Reveal { pubkey: k.pk, pop: k.sk.sign(&k.pk, DST_POP, &[]).compress() });

View file

@ -97,7 +97,7 @@ fn run() -> Result<()> {
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());
println!("RESULT fin-synth: {} blocks {}..={}, {} keys ({} slots in the table), {} signers, window {} blocks, certificate at block {} over {} back; {} 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.key_count, voters, window, file.blocks.len(), cert_back, text.len(), t.elapsed().as_secs_f64(), now());
return Ok(());
}
if mode == "fin-execute" {
@ -127,13 +127,13 @@ fn run() -> Result<()> {
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());
println!("RESULT fin-execute block {}: plain {} cycles ({:.2} s), with finality {} cycles ({:.2} s), extra {} cycles; key slots {} 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.key_count, 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());
results.insert("keys".into(), file.root_state.key_count.into());
drop(sp1);
return finish(results, out_path.clone());
}
@ -748,7 +748,7 @@ fn run_chain(pinned: &pinned::Pinned, fixtures: &[String], prover: Address, out_
if inputs.len() != built.len() {
bail!("{p} carries {} finality witnesses for {} blocks", inputs.len(), built.len());
}
println!("RESULT chain fin: {} finality witnesses folded natively, root state ends at chain block {} with {} keys, {} ring leaves opened, {} certificates at {}", inputs.len(), file.root_state.end_number, file.root_state.keys.len(), file.blocks.iter().map(|b| b.ring.len()).sum::<usize>(), file.blocks.iter().map(|b| b.witness.certificates.len()).sum::<usize>(), now());
println!("RESULT chain fin: {} finality witnesses folded natively, root state ends at chain block {} with {} key slots, {} ring and {} key leaves opened, {} certificates at {}", inputs.len(), file.root_state.end_number, file.root_state.key_count, file.blocks.iter().map(|b| b.ring.len()).sum::<usize>(), file.blocks.iter().map(|b| b.keys.len()).sum::<usize>(), file.blocks.iter().map(|b| b.witness.certificates.len()).sum::<usize>(), now());
inputs
}
};