Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 148 additions & 1 deletion ergo-chain-generation/src/chain_generation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,9 @@ fn transactions_root(txs: &[Transaction], block_version: u8) -> Digest32 {
#[cfg(test)]
mod tests {
use super::*;
use ergo_nipopow::{NipopowAlgos, NipopowProof, PoPowHeader};
use ergo_lib::ergo_chain_types::Header;
use ergo_nipopow::{NipopowAlgos, NipopowProof, PoPowHeader, PopowHeaderReader};
use std::collections::HashMap;

fn generate_popowheader_chain(len: usize, start: Option<PoPowHeader>) -> Vec<PoPowHeader> {
block_stream(start.map(|p| ErgoFullBlock {
Expand Down Expand Up @@ -505,4 +507,149 @@ mod tests {
assert_eq!(serde_json::from_str::<PoPowHeader>(&json).unwrap(), header);
}
}

/// In-memory `PopowHeaderReader` backed by a synthetic chain. Lives in
/// the test module because `ergo-nipopow` cannot depend on
/// `ergo-chain-generation` (circular dep).
struct MockReader {
by_id: HashMap<BlockId, PoPowHeader>,
by_height: Vec<PoPowHeader>, // index 0 = height 1 (genesis)
}

impl MockReader {
fn from_chain(chain: &[PoPowHeader]) -> Self {
let mut by_id = HashMap::with_capacity(chain.len());
let mut by_height: Vec<PoPowHeader> = Vec::with_capacity(chain.len());
for ph in chain {
by_id.insert(ph.header.id, ph.clone());
by_height.push(ph.clone());
}
// Sanity: heights must be 1..=len contiguous starting at 1.
for (i, ph) in by_height.iter().enumerate() {
assert_eq!(ph.header.height as usize, i + 1);
}
Self { by_id, by_height }
}
}

impl PopowHeaderReader for MockReader {
fn headers_height(&self) -> u32 {
self.by_height.len() as u32
}

fn popow_header_by_id(&self, id: &BlockId) -> Option<PoPowHeader> {
self.by_id.get(id).cloned()
}

fn popow_header_at_height(&self, height: u32) -> Option<PoPowHeader> {
// Genesis is height 1, so subtract 1 to index into by_height.
if height == 0 {
return None;
}
self.by_height.get((height - 1) as usize).cloned()
}

fn last_headers(&self, k: usize) -> Vec<Header> {
let len = self.by_height.len();
let start = len.saturating_sub(k);
self.by_height[start..]
.iter()
.map(|ph| ph.header.clone())
.collect()
}

fn best_headers_after(&self, header: &Header, n: usize) -> Vec<Header> {
// Heights are 1-indexed; the slot immediately after `header` is
// at vec index `header.height` (because by_height[0] is height 1).
let start = header.height as usize;
let end = (start + n).min(self.by_height.len());
if start >= end {
return Vec::new();
}
self.by_height[start..end]
.iter()
.map(|ph| ph.header.clone())
.collect()
}
}

/// Sanity check: on a chain produced by the real Autolykos chain
/// generator, `prove_with_reader(None)` produces a proof whose
/// connections validate, the suffix has length `k`, and the prefix is
/// no larger than the in-memory `prove`'s prefix (the db-backed
/// algorithm walks the interlink hierarchy and never visits level-0
/// blocks, so its prefix is always a subset of the in-memory one when
/// some blocks have `max_level_of == 0`).
///
/// Strict byte-for-byte equivalence between `prove` and
/// `prove_with_reader` only holds when *every* block in the chain has
/// `max_level_of >= 1`, which is not the case here — the real Autolykos
/// generator produces level-0 blocks. The byte-for-byte assertion lives
/// in `fake_pow_scheme.rs`, which uses a fake pow scheme that forces
/// every block to a positive level (matching what
/// `org.ergoplatform.modifiers.history.PoPowAlgosWithDBSpec` does in
/// the JVM via `DefaultFakePowScheme`).
#[test]
fn test_nipopow_prove_with_reader_valid_on_real_autolykos_chain() {
let m = 6;
let k = 10;
let popow_algos = NipopowAlgos::default();
let chain = generate_popowheader_chain(100, None);

let in_memory_proof = popow_algos.prove(&chain, k, m).unwrap();
let reader = MockReader::from_chain(&chain);
let db_backed_proof = popow_algos
.prove_with_reader(&reader, None, k, m)
.unwrap();

assert!(db_backed_proof.has_valid_connections());
assert_eq!(db_backed_proof.suffix_tail.len(), (k - 1) as usize);
// suffix head must be the (len - k + 1)-th block (1-indexed) of the
// chain — i.e. chain[len - k].
assert_eq!(
db_backed_proof.suffix_head.header.id,
chain[chain.len() - k as usize].header.id
);
// Db-backed prefix is a subset of the in-memory prefix on chains
// with level-0 blocks (see doc comment above).
let in_memory_ids: std::collections::HashSet<BlockId> = in_memory_proof
.prefix
.iter()
.map(|p| p.header.id)
.collect();
for ph in &db_backed_proof.prefix {
assert!(
in_memory_ids.contains(&ph.header.id),
"db-backed prefix contained {:?} which is absent from in-memory prefix",
ph.header.id
);
}
}

/// When `prove_with_reader` is given an explicit `header_id`, the
/// resulting suffix head must match the requested header and the proof
/// must have valid connections.
#[test]
fn test_nipopow_prove_with_reader_explicit_header_id() {
let m = 6;
let k = 10;
let popow_algos = NipopowAlgos::default();
let chain = generate_popowheader_chain(100, None);
let reader = MockReader::from_chain(&chain);

let target = chain[80].clone();
let proof = popow_algos
.prove_with_reader(&reader, Some(&target.header.id), k, m)
.unwrap();

assert_eq!(proof.suffix_head.header.id, target.header.id);
assert_eq!(proof.suffix_head.header.height, target.header.height);
// suffix_tail must be the next k-1 headers immediately after
// target, in ascending-height order.
assert_eq!(proof.suffix_tail.len(), (k - 1) as usize);
for (i, h) in proof.suffix_tail.iter().enumerate() {
assert_eq!(h.height, target.header.height + 1 + i as u32);
}
assert!(proof.has_valid_connections());
}
}
112 changes: 111 additions & 1 deletion ergo-chain-generation/src/fake_pow_scheme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
#[cfg(test)]
mod tests {
use ergo_lib::ergo_chain_types::{blake2b256_hash, ADDigest, BlockId, Digest32};
use ergo_nipopow::{NipopowAlgos, NipopowProof};
use ergo_nipopow::{NipopowAlgos, NipopowProof, PopowHeaderReader};

use ergo_chain_types::{autolykos_pow_scheme::order_bigint, AutolykosSolution, Header, Votes};
use ergo_lib::ergotree_interpreter::sigma_protocol::private_input::DlogProverInput;
use ergo_lib::ergotree_ir::serialization::sigma_byte_writer::SigmaByteWriter;
use ergo_nipopow::PoPowHeader;
use num_bigint::BigUint;
use rand::{thread_rng, Rng};
use std::collections::HashMap;

use crate::{default_miner_secret, ErgoFullBlock, ExtensionCandidate};
use ergo_merkle_tree::{MerkleNode, MerkleTree};
Expand Down Expand Up @@ -355,4 +356,113 @@ mod tests {
};
assert!(!proof.has_valid_connections());
}

/// In-memory `PopowHeaderReader` backed by a synthetic chain. Lives in
/// the test module because `ergo-nipopow` cannot depend on
/// `ergo-chain-generation` (circular dep).
struct MockReader {
by_id: HashMap<BlockId, PoPowHeader>,
by_height: Vec<PoPowHeader>, // index 0 = height 1 (genesis)
}

impl MockReader {
fn from_chain(chain: &[PoPowHeader]) -> Self {
let mut by_id = HashMap::with_capacity(chain.len());
let mut by_height: Vec<PoPowHeader> = Vec::with_capacity(chain.len());
for ph in chain {
by_id.insert(ph.header.id, ph.clone());
by_height.push(ph.clone());
}
for (i, ph) in by_height.iter().enumerate() {
assert_eq!(ph.header.height as usize, i + 1);
}
Self { by_id, by_height }
}
}

impl PopowHeaderReader for MockReader {
fn headers_height(&self) -> u32 {
self.by_height.len() as u32
}

fn popow_header_by_id(&self, id: &BlockId) -> Option<PoPowHeader> {
self.by_id.get(id).cloned()
}

fn popow_header_at_height(&self, height: u32) -> Option<PoPowHeader> {
if height == 0 {
return None;
}
self.by_height.get((height - 1) as usize).cloned()
}

fn last_headers(&self, k: usize) -> Vec<Header> {
let len = self.by_height.len();
let start = len.saturating_sub(k);
self.by_height[start..]
.iter()
.map(|ph| ph.header.clone())
.collect()
}

fn best_headers_after(&self, header: &Header, n: usize) -> Vec<Header> {
// Heights are 1-indexed; the slot immediately after `header` is
// at vec index `header.height` (because by_height[0] is height 1).
let start = header.height as usize;
let end = (start + n).min(self.by_height.len());
if start >= end {
return Vec::new();
}
self.by_height[start..end]
.iter()
.map(|ph| ph.header.clone())
.collect()
}
}

/// Asserts byte-for-byte equivalence between `prove(&chain)` and
/// `prove_with_reader(&reader, None, k, m)` on a 100-block synthetic
/// chain at the JVM P2P defaults `m=6, k=10`. This is the Rust
/// counterpart to `PoPowAlgosWithDBSpec` "proof(chain) is equivalent to
/// proof(histReader)" in the JVM ergo node.
///
/// The test must use the fake-pow-scheme chain in this module (where
/// `d = order / (height + 1)` forces `max_level_of >= 1` for every
/// non-genesis block). On real-Autolykos chains the in-memory algorithm
/// adds level-0-only blocks at its level-0 iteration that the db-backed
/// interlink walk never visits, so byte-for-byte equivalence does not
/// hold there. The JVM equivalent test (`PoPowAlgosWithDBSpec`)
/// likewise relies on `DefaultFakePowScheme`, which mints blocks with
/// `d = q / (height + 10)` for the same reason.
#[test]
fn test_nipopow_prove_with_reader_matches_in_memory() {
use sigma_ser::ScorexSerializable;
let m = 6;
let k = 10;
let popow_algos = NipopowAlgos::default();
let chain = generate_popowheader_chain(100, None);

// Sanity: every block must have positive level for the equivalence
// to hold (see test doc).
for ph in &chain {
assert!(popow_algos.max_level_of(&ph.header).unwrap() >= 1);
}

let in_memory_proof = popow_algos.prove(&chain, k, m).unwrap();
let reader = MockReader::from_chain(&chain);
let db_backed_proof = popow_algos
.prove_with_reader(&reader, None, k, m)
.unwrap();

let in_memory_bytes = in_memory_proof.scorex_serialize_bytes().unwrap();
let db_backed_bytes = db_backed_proof.scorex_serialize_bytes().unwrap();

assert_eq!(
in_memory_bytes, db_backed_bytes,
"prove and prove_with_reader produced different proofs:\n\
in-memory prefix len: {}, db-backed prefix len: {}",
in_memory_proof.prefix.len(),
db_backed_proof.prefix.len()
);
}
}
2 changes: 2 additions & 0 deletions ergo-nipopow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
mod nipopow_algos;
mod nipopow_proof;
mod nipopow_verifier;
mod popow_header_reader;

pub use nipopow_algos::{NipopowAlgos, INTERLINK_VECTOR_PREFIX};
pub use nipopow_proof::{NipopowProof, NipopowProofError, PoPowHeader};
pub use nipopow_verifier::NipopowVerifier;
pub use popow_header_reader::PopowHeaderReader;
Loading
Loading