fin-proof: level 1 of the blue colouring (headers hashed in the guest, selected parent and blue count from blue scores, parent-link reachability, reds kept in the ring)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
5d39642faf
commit
c8aef63aaf
5 changed files with 271 additions and 7 deletions
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
use crate::bls::Bls;
|
||||
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};
|
||||
|
|
@ -24,6 +25,10 @@ pub struct ChainBlockWitness {
|
|||
pub block_hash: H,
|
||||
pub daa: u64,
|
||||
pub blues: Vec<BlueBlock>,
|
||||
/// Level 1 (design 5.2): the chain block's own header first, then every mergeset block's (blue and red).
|
||||
/// Empty: level 0, the blues are the prover's word (vetoed on the chain by every full node).
|
||||
#[serde(default)]
|
||||
pub headers: Vec<HeaderWitness>,
|
||||
}
|
||||
|
||||
/// W1: a key reveal with its proof of possession.
|
||||
|
|
@ -107,6 +112,9 @@ pub fn fold_block(state: &mut FinState, params: &FinParams, block: &ChainBlockWi
|
|||
let mut report = FoldReport::default();
|
||||
let window_start = block.daa.saturating_sub(params.weight_window);
|
||||
|
||||
// 0. level 1: the headers pin the blues (design 5.2); reds ride into the ring uncounted
|
||||
let reds = if block.headers.is_empty() { Vec::new() } else { check_headers(state, block)? };
|
||||
|
||||
// 1. the block's blue blocks into the ring and their keys
|
||||
for b in &block.blues {
|
||||
if b.daa > block.daa {
|
||||
|
|
@ -118,7 +126,7 @@ pub fn fold_block(state: &mut FinState, params: &FinParams, block: &ChainBlockWi
|
|||
}
|
||||
let slot = slot_of(b.daa);
|
||||
let (mut entries, siblings) = ring.open(slot, &state.ring_root)?;
|
||||
let e = RingEntry { daa: b.daa, block_hash: b.block_hash, key_hash: b.key_hash };
|
||||
let e = RingEntry { daa: b.daa, block_hash: b.block_hash, key_hash: b.key_hash, blue: true };
|
||||
if entries.iter().any(|x| x.block_hash == b.block_hash) {
|
||||
return Err(format!("blue block {} counted twice", hex32(&b.block_hash)));
|
||||
}
|
||||
|
|
@ -129,6 +137,20 @@ pub fn fold_block(state: &mut FinState, params: &FinParams, block: &ChainBlockWi
|
|||
state.keys[i].blocks += 1;
|
||||
report.counted += 1;
|
||||
}
|
||||
for r in &reds {
|
||||
if r.daa <= window_start {
|
||||
continue;
|
||||
}
|
||||
let slot = slot_of(r.daa);
|
||||
let (mut entries, siblings) = ring.open(slot, &state.ring_root)?;
|
||||
if entries.iter().any(|x| x.block_hash == r.block_hash) {
|
||||
return Err(format!("red block {} was in an earlier mergeset", hex32(&r.block_hash)));
|
||||
}
|
||||
let e = RingEntry { daa: r.daa, block_hash: r.block_hash, key_hash: r.key_hash, blue: false };
|
||||
let pos = entries.binary_search(&e).unwrap_err();
|
||||
entries.insert(pos, e);
|
||||
state.ring_root = ring.write(slot, entries, &siblings);
|
||||
}
|
||||
|
||||
// 2. blocks that left the window: DAA scores in (old_start, window_start]
|
||||
let old_start = state.end_daa.saturating_sub(params.weight_window);
|
||||
|
|
@ -140,7 +162,7 @@ pub fn fold_block(state: &mut FinState, params: &FinParams, block: &ChainBlockWi
|
|||
let (entries, siblings) = ring.open(slot, &state.ring_root)?;
|
||||
let (gone, kept): (Vec<RingEntry>, Vec<RingEntry>) = entries.into_iter().partition(|e| e.daa <= window_start);
|
||||
if !gone.is_empty() {
|
||||
for g in &gone {
|
||||
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")?;
|
||||
report.expired += 1;
|
||||
|
|
@ -213,6 +235,11 @@ pub fn fold_block(state: &mut FinState, params: &FinParams, block: &ChainBlockWi
|
|||
// 6. the block's own entry in the history, with the table after it
|
||||
state.end_daa = block.daa;
|
||||
state.end_number = block.number;
|
||||
state.end_hash = block.block_hash;
|
||||
if let Some(h) = block.headers.first() {
|
||||
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 };
|
||||
|
|
@ -227,3 +254,87 @@ pub fn hex32(h: &H) -> String {
|
|||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// Level 1: every header hashes to the hash it stands for; the chain block's header names this block, its DAA
|
||||
/// score and the selected parent (the previous chain block, by blue work then hash among its direct parents);
|
||||
/// every blue of the witness is a header's block with that header's key and DAA score; the blue count equals
|
||||
/// the blue score step; every mergeset block is reached from the chain block by parent links through mergeset
|
||||
/// blocks. Returns the reds (uncounted, for the ring).
|
||||
fn check_headers(state: &FinState, block: &ChainBlockWitness) -> Result<Vec<BlueBlock>, String> {
|
||||
let own = &block.headers[0];
|
||||
let own_hash = own.hash();
|
||||
if own_hash != block.block_hash {
|
||||
return Err(format!("the chain block's header hashes to {}, not {}", hex32(&own_hash), hex32(&block.block_hash)));
|
||||
}
|
||||
if own.daa_score != block.daa {
|
||||
return Err("the chain block's header carries another DAA score".into());
|
||||
}
|
||||
if !own.blue {
|
||||
return Err("the chain block is blue in its own mergeset".into());
|
||||
}
|
||||
// the mergeset headers by hash
|
||||
let mut by_hash: std::collections::BTreeMap<H, &HeaderWitness> = std::collections::BTreeMap::new();
|
||||
for h in &block.headers[1..] {
|
||||
let hh = h.hash();
|
||||
if hh == block.block_hash || by_hash.insert(hh, h).is_some() {
|
||||
return Err(format!("mergeset header {} listed twice", hex32(&hh)));
|
||||
}
|
||||
}
|
||||
// the selected parent: the previous chain block, and the greatest direct parent by (blue work, hash)
|
||||
if state.leaves > 0 {
|
||||
let parents = own.direct_parents();
|
||||
if !parents.contains(&state.end_hash) {
|
||||
return Err("the previous chain block is not a direct parent of this one".into());
|
||||
}
|
||||
for p in parents {
|
||||
if *p == state.end_hash {
|
||||
continue;
|
||||
}
|
||||
let Some(ph) = by_hash.get(p) else { return Err(format!("direct parent {} is neither the selected parent nor in the mergeset", hex32(p))) };
|
||||
if outranks(&ph.blue_work, p, &state.end_blue_work, &state.end_hash) {
|
||||
return Err(format!("direct parent {} outranks the selected parent", hex32(p)));
|
||||
}
|
||||
}
|
||||
if own.blue_score != state.end_blue_score + block.blues.len() as u64 {
|
||||
return Err(format!("blue score {} is not the selected parent's {} plus the {} blues counted", own.blue_score, state.end_blue_score, block.blues.len()));
|
||||
}
|
||||
}
|
||||
// every blue is a header's block with that header's key and DAA score (the chain block itself first)
|
||||
let mut seen_blue = std::collections::BTreeSet::new();
|
||||
for b in &block.blues {
|
||||
let h = if b.block_hash == block.block_hash { own } else { by_hash.get(&b.block_hash).copied().ok_or_else(|| format!("blue block {} has no header in the witness", hex32(&b.block_hash)))? };
|
||||
if !h.blue || h.vote_key_hash != b.key_hash || h.daa_score != b.daa {
|
||||
return Err(format!("blue block {} differs from its header (colour, key or DAA score)", hex32(&b.block_hash)));
|
||||
}
|
||||
if !seen_blue.insert(b.block_hash) {
|
||||
return Err(format!("blue block {} listed twice", hex32(&b.block_hash)));
|
||||
}
|
||||
}
|
||||
let mut reds = Vec::new();
|
||||
for (hh, h) in &by_hash {
|
||||
if h.blue {
|
||||
if !seen_blue.contains(hh) {
|
||||
return Err(format!("mergeset block {} is blue in its header and not counted", hex32(hh)));
|
||||
}
|
||||
} else {
|
||||
reds.push(BlueBlock { block_hash: *hh, key_hash: h.vote_key_hash, daa: h.daa_score });
|
||||
}
|
||||
}
|
||||
// reachability: from the chain block's direct parents through mergeset blocks' direct parents
|
||||
let mut reached: std::collections::BTreeSet<H> = std::collections::BTreeSet::new();
|
||||
let mut queue: Vec<H> = own.direct_parents().to_vec();
|
||||
while let Some(p) = queue.pop() {
|
||||
if p == state.end_hash || !reached.insert(p) {
|
||||
continue;
|
||||
}
|
||||
if let Some(h) = by_hash.get(&p) {
|
||||
queue.extend(h.direct_parents().iter().copied());
|
||||
}
|
||||
}
|
||||
for hh in by_hash.keys() {
|
||||
if !reached.contains(hh) {
|
||||
return Err(format!("mergeset block {} is not reached from the chain block by parent links", hex32(hh)));
|
||||
}
|
||||
}
|
||||
Ok(reds)
|
||||
}
|
||||
|
|
|
|||
75
proving/igneum-prove/fin/src/header.rs
Normal file
75
proving/igneum-prove/fin/src/header.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
//! Level 1 of the blue colouring (docs/design/finality-in-proof.md 5.2): the fold takes every mergeset block's
|
||||
//! header, recomputes the chain's block hash (BLAKE2b-256 keyed `BlockHash` over the fields in the order of the
|
||||
//! fork's `consensus/core/src/hashing/header.rs`), and checks what a header pins: the block's vote key hash, its
|
||||
//! DAA score, the chain block's blue score against the previous one (the blue count), the selected parent (the
|
||||
//! direct parent of greatest blue work, hash as the tie-break), and that every mergeset block is reached from the
|
||||
//! chain block by parent links through mergeset blocks. What a lie can still do: swap the colours of two mergeset
|
||||
//! blocks of one chain block (the blue count holds, the ring refuses any block seen before).
|
||||
|
||||
use crate::H;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct HeaderWitness {
|
||||
pub version: u16,
|
||||
/// Parents by level, level 0 first (the direct parents).
|
||||
pub parents_by_level: Vec<Vec<H>>,
|
||||
pub hash_merkle_root: H,
|
||||
pub accepted_id_merkle_root: H,
|
||||
pub utxo_commitment: H,
|
||||
pub timestamp: u64,
|
||||
pub bits: u32,
|
||||
pub nonce: u64,
|
||||
pub daa_score: u64,
|
||||
/// Big-endian bytes of the 192-bit blue work with leading zeros stripped (what the hash writes).
|
||||
pub blue_work: Vec<u8>,
|
||||
pub blue_score: u64,
|
||||
pub pruning_point: H,
|
||||
pub vote_key_hash: H,
|
||||
/// Blue in its chain block's mergeset (counted) or red (kept in the ring uncounted).
|
||||
pub blue: bool,
|
||||
}
|
||||
|
||||
impl HeaderWitness {
|
||||
/// The fork's `hash_override_nonce_time` with the header's own nonce and timestamp.
|
||||
pub fn hash(&self) -> H {
|
||||
let mut st = blake2b_simd::Params::new().hash_length(32).key(b"BlockHash").to_state();
|
||||
st.update(&self.version.to_le_bytes());
|
||||
st.update(&(self.parents_by_level.len() as u64).to_le_bytes());
|
||||
for level in &self.parents_by_level {
|
||||
st.update(&(level.len() as u64).to_le_bytes());
|
||||
for p in level {
|
||||
st.update(p);
|
||||
}
|
||||
}
|
||||
st.update(&self.hash_merkle_root);
|
||||
st.update(&self.accepted_id_merkle_root);
|
||||
st.update(&self.utxo_commitment);
|
||||
st.update(&self.timestamp.to_le_bytes());
|
||||
st.update(&self.bits.to_le_bytes());
|
||||
st.update(&self.nonce.to_le_bytes());
|
||||
st.update(&self.daa_score.to_le_bytes());
|
||||
st.update(&self.blue_score.to_le_bytes());
|
||||
st.update(&(self.blue_work.len() as u64).to_le_bytes());
|
||||
st.update(&self.blue_work);
|
||||
st.update(&self.pruning_point);
|
||||
st.update(&self.vote_key_hash);
|
||||
let mut h = [0u8; 32];
|
||||
h.copy_from_slice(st.finalize().as_bytes());
|
||||
h
|
||||
}
|
||||
|
||||
pub fn direct_parents(&self) -> &[H] {
|
||||
self.parents_by_level.first().map(|v| v.as_slice()).unwrap_or(&[])
|
||||
}
|
||||
}
|
||||
|
||||
/// Greater blue work wins; equal work, the greater hash (GHOSTDAG's `find_selected_parent` order on
|
||||
/// `SortableBlock`: blue work then hash).
|
||||
pub fn outranks(work_a: &[u8], hash_a: &H, work_b: &[u8], hash_b: &H) -> bool {
|
||||
match work_a.len().cmp(&work_b.len()).then_with(|| work_a.cmp(work_b)) {
|
||||
std::cmp::Ordering::Greater => true,
|
||||
std::cmp::Ordering::Less => false,
|
||||
std::cmp::Ordering::Equal => hash_a > hash_b,
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
pub mod bls;
|
||||
pub mod cert;
|
||||
pub mod fold;
|
||||
pub mod header;
|
||||
pub mod mmr;
|
||||
pub mod ring;
|
||||
pub mod tracker;
|
||||
|
|
@ -212,6 +213,14 @@ pub struct FinState {
|
|||
/// DAA score and chain number of the last chain block folded in.
|
||||
pub end_daa: u64,
|
||||
pub end_number: u64,
|
||||
/// Level 1 (design 5.2): the last chain block's hash, blue score and blue work, so the next fold can check
|
||||
/// its selected parent and its blue count against the headers. Zero before level 1 witnesses.
|
||||
#[serde(default)]
|
||||
pub end_hash: H,
|
||||
#[serde(default)]
|
||||
pub end_blue_score: u64,
|
||||
#[serde(default)]
|
||||
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.
|
||||
|
|
@ -297,6 +306,9 @@ impl FinState {
|
|||
params_hash: params.hash(),
|
||||
end_daa: 0,
|
||||
end_number: first_number.saturating_sub(1),
|
||||
end_hash: ZERO,
|
||||
end_blue_score: 0,
|
||||
end_blue_work: Vec::new(),
|
||||
history_first: first_number,
|
||||
keys: Vec::new(),
|
||||
ring_root: ring::empty_root(),
|
||||
|
|
@ -313,7 +325,7 @@ impl FinState {
|
|||
|
||||
/// `sha256("igneum-fin-state-v1" || params_hash || end_daa || end_number || keys_hash || 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.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_hash(), &self.ring_root])
|
||||
}
|
||||
|
||||
pub fn history_root(&self) -> H {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,14 @@ pub struct RingEntry {
|
|||
pub daa: u64,
|
||||
pub block_hash: H,
|
||||
pub key_hash: H,
|
||||
/// Level 1 (docs/design/finality-in-proof.md 5.2): a red mergeset block is kept in the ring uncounted, so a
|
||||
/// block seen in one chain block's mergeset is refused in any later one, blue or red.
|
||||
#[serde(default = "default_blue")]
|
||||
pub blue: bool,
|
||||
}
|
||||
|
||||
fn default_blue() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl RingEntry {
|
||||
|
|
@ -27,6 +35,7 @@ impl RingEntry {
|
|||
out.extend_from_slice(&self.daa.to_le_bytes());
|
||||
out.extend_from_slice(&self.block_hash);
|
||||
out.extend_from_slice(&self.key_hash);
|
||||
out.push(u8::from(self.blue));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -39,7 +48,7 @@ pub fn leaf_hash(entries: &[RingEntry]) -> H {
|
|||
if entries.is_empty() {
|
||||
return ZERO;
|
||||
}
|
||||
let mut buf = Vec::with_capacity(1 + entries.len() * 72);
|
||||
let mut buf = Vec::with_capacity(1 + entries.len() * 73);
|
||||
buf.push(0u8);
|
||||
for e in entries {
|
||||
e.write(&mut buf);
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ impl Sim {
|
|||
for (j, &k) in extra.iter().enumerate() {
|
||||
blues.push(BlueBlock { block_hash: hash_of(&format!("side-{j}"), n), key_hash: self.keys[k].hash, daa: n });
|
||||
}
|
||||
let block = ChainBlockWitness { number: n, block_hash: hash_of("chain", n), daa: n, blues };
|
||||
let block = ChainBlockWitness { number: n, block_hash: hash_of("chain", n), daa: n, blues, headers: Vec::new() };
|
||||
// a key's reveal rides with its first block in the table (the node's W1 rule; the tracker re-supplies it
|
||||
// whenever a key whose entry aged out mines again)
|
||||
let mut witness = witness;
|
||||
|
|
@ -198,7 +198,7 @@ fn the_carried_table_equals_a_brute_force_count_of_the_window_at_every_block() {
|
|||
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);
|
||||
}
|
||||
let ring_count: u64 = sim.tracker.all_entries().len() as u64;
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
|
@ -212,7 +212,7 @@ fn a_block_counted_twice_is_refused_and_ageing_is_exact_at_the_edge() {
|
|||
// the same side block hash twice in one chain block
|
||||
let n = sim.blocks.len() as u64;
|
||||
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] };
|
||||
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();
|
||||
assert!(err.contains("counted twice"), "{err}");
|
||||
|
|
@ -486,3 +486,60 @@ fn the_guest_curve_and_blst_agree_on_real_certificates_and_disagree_with_nothing
|
|||
assert!(ZkBls.verify_one(&r.pubkey, &r.pubkey, DST_POP, &r.pop));
|
||||
assert!(BlstBls.verify_one(&r.pubkey, &r.pubkey, DST_POP, &r.pop));
|
||||
}
|
||||
|
||||
/// Level 1: a chain of headers hashed the chain's way, with a side block blue and one red; a swapped key, a
|
||||
/// wrong blue count, an unreached block and a replayed red are each refused.
|
||||
#[test]
|
||||
fn level_one_pins_the_blues_to_headers_and_parent_links() {
|
||||
use igneum_fin_core::header::HeaderWitness;
|
||||
let p = params();
|
||||
let mut state = FinState::empty(&p, 0);
|
||||
let mut ring = SparseRing::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
|
||||
let h0 = hdr(vec![], 0, 0, 1, &keys[0], true);
|
||||
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();
|
||||
ring.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);
|
||||
let h1 = hdr(vec![h0.hash(), hs.hash(), hr.hash()], 1, 2, 9, &keys[0], true);
|
||||
let good = ChainBlockWitness { number: 1, block_hash: h1.hash(), daa: 1, blues: vec![BlueBlock { block_hash: h1.hash(), key_hash: keys[0].hash, daa: 1 }, BlueBlock { block_hash: hs.hash(), key_hash: keys[1].hash, daa: 1 }], headers: vec![h1.clone(), hs.clone(), hr.clone()] };
|
||||
// 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();
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
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);
|
||||
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();
|
||||
assert!(e.contains("counted twice") || e.contains("not a direct parent") || e.contains("listed twice"), "{e}");
|
||||
}
|
||||
|
||||
const ZERO_H: H = [0u8; 32];
|
||||
|
|
|
|||
Loading…
Reference in a new issue