move ledger into separate crate (#606)

* move ledger into separate crate

* clippy happy
This commit is contained in:
Giacomo Pasini 2024-03-11 15:15:17 +01:00 committed by GitHub
parent 06d225db20
commit e51865fe33
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 375 additions and 376 deletions

View File

@ -23,6 +23,7 @@ members = [
"simulations",
"consensus/carnot-engine",
"consensus/cryptarchia-engine",
"ledger/cryptarchia-ledger",
"tests",
"mixnet/node",
"mixnet/client",

View File

@ -6,6 +6,4 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
blake2 = "0.10"
rpds = "1"
thiserror = "1"

View File

@ -1,13 +1,3 @@
use crate::{Epoch, Slot};
#[derive(Clone, Debug, PartialEq)]
pub struct TimeConfig {
// How long a slot lasts in seconds
pub slot_duration: u64,
// Start of the first epoch, in unix timestamp second precision
pub chain_start_time: u64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Config {
// The k parameter in the Common Prefix property.
@ -17,23 +7,9 @@ pub struct Config {
pub security_param: u32,
// f, the rate of occupied slots
pub active_slot_coeff: f64,
// The stake distribution is always taken at the beginning of the previous epoch.
// This parameters controls how many slots to wait for it to be stabilized
// The value is computed as epoch_stake_distribution_stabilization * int(floor(k / f))
pub epoch_stake_distribution_stabilization: u8,
// This parameter controls how many slots we wait after the stake distribution
// snapshot has stabilized to take the nonce snapshot.
pub epoch_period_nonce_buffer: u8,
// This parameter controls how many slots we wait for the nonce snapshot to be considered
// stabilized
pub epoch_period_nonce_stabilization: u8,
pub time: TimeConfig,
}
impl Config {
pub fn time_config(&self) -> &TimeConfig {
&self.time
}
impl Config {
pub fn base_period_length(&self) -> u64 {
(f64::from(self.security_param) / self.active_slot_coeff).floor() as u64
}
@ -42,22 +18,4 @@ impl Config {
pub fn s(&self) -> u64 {
self.base_period_length() * 3
}
pub fn epoch_length(&self) -> u64 {
(self.epoch_stake_distribution_stabilization as u64
+ self.epoch_period_nonce_buffer as u64
+ self.epoch_period_nonce_stabilization as u64)
* self.base_period_length()
}
pub fn nonce_snapshot(&self, epoch: Epoch) -> Slot {
let offset = self.base_period_length()
* (self.epoch_period_nonce_buffer + self.epoch_stake_distribution_stabilization) as u64;
let base = u32::from(epoch) as u64 * self.epoch_length();
(base + offset).into()
}
pub fn stake_distribution_snapshot(&self, epoch: Epoch) -> Slot {
(u32::from(epoch) as u64 * self.epoch_length()).into()
}
}

View File

@ -1,68 +1,80 @@
pub mod block;
pub mod config;
pub mod crypto;
pub mod leader_proof;
pub mod ledger;
pub mod time;
pub use block::*;
mod time;
pub use config::*;
pub use leader_proof::*;
use ledger::{Ledger, LedgerState};
use std::collections::{HashMap, HashSet};
use thiserror::Error;
pub use time::*;
pub use time::{Epoch, Slot};
#[derive(Clone, Debug)]
pub struct Cryptarchia {
local_chain: Branch,
branches: Branches,
ledger: Ledger,
pub struct Cryptarchia<Id> {
local_chain: Branch<Id>,
branches: Branches<Id>,
config: Config,
genesis: HeaderId,
genesis: Id,
}
#[derive(Clone, Debug)]
pub struct Branches {
branches: HashMap<HeaderId, Branch>,
tips: HashSet<HeaderId>,
pub struct Branches<Id> {
branches: HashMap<Id, Branch<Id>>,
tips: HashSet<Id>,
}
#[derive(Clone, Debug)]
pub struct Branch {
header: Header,
pub struct Branch<Id> {
id: Id,
parent: Id,
slot: Slot,
// chain length
length: u64,
}
impl Branches {
pub fn from_genesis(genesis: &Header) -> Self {
impl<Id> Branches<Id>
where
Id: Eq + std::hash::Hash + Copy,
{
pub fn from_genesis(genesis: Id) -> Self {
let mut branches = HashMap::new();
branches.insert(
genesis.id(),
genesis,
Branch {
header: genesis.clone(),
id: genesis,
parent: genesis,
slot: 0.into(),
length: 0,
},
);
let tips = HashSet::from([genesis.id()]);
let tips = HashSet::from([genesis]);
Self { branches, tips }
}
#[must_use]
fn apply_header(&self, header: Header) -> Self {
#[must_use = "this returns the result of the operation, without modifying the original"]
fn apply_header(&self, header: Id, parent: Id, slot: Slot) -> Result<Self, Error<Id>> {
let mut branches = self.branches.clone();
let mut tips = self.tips.clone();
// if the parent was the head of a branch, remove it as it has been superseded by the new header
tips.remove(&header.parent());
let length = branches[&header.parent()].length + 1;
tips.insert(header.id());
branches.insert(header.id(), Branch { header, length });
tips.remove(&parent);
let length = branches
.get(&parent)
.ok_or(Error::ParentMissing(parent))?
.length
+ 1;
tips.insert(header);
branches.insert(
header,
Branch {
id: header,
parent,
length,
slot,
},
);
Self { branches, tips }
Ok(Self { branches, tips })
}
pub fn branches(&self) -> Vec<Branch> {
pub fn branches(&self) -> Vec<Branch<Id>> {
self.tips
.iter()
.map(|id| self.branches[id].clone())
@ -70,88 +82,82 @@ impl Branches {
}
// find the lowest common ancestor of two branches
pub fn lca<'a>(&'a self, mut b1: &'a Branch, mut b2: &'a Branch) -> Branch {
pub fn lca<'a>(&'a self, mut b1: &'a Branch<Id>, mut b2: &'a Branch<Id>) -> Branch<Id> {
// first reduce branches to the same length
while b1.length > b2.length {
b1 = &self.branches[&b1.header.parent()];
b1 = &self.branches[&b1.parent];
}
while b2.length > b1.length {
b2 = &self.branches[&b2.header.parent()];
b2 = &self.branches[&b2.parent];
}
// then walk up the chain until we find the common ancestor
while b1.header.id() != b2.header.id() {
b1 = &self.branches[&b1.header.parent()];
b2 = &self.branches[&b2.header.parent()];
while b1.id != b2.id {
b1 = &self.branches[&b1.parent];
b2 = &self.branches[&b2.parent];
}
b1.clone()
}
pub fn get(&self, id: &HeaderId) -> Option<&Branch> {
pub fn get(&self, id: &Id) -> Option<&Branch<Id>> {
self.branches.get(id)
}
// Walk back the chain until the target slot
fn walk_back_before(&self, branch: &Branch, slot: Slot) -> Branch {
fn walk_back_before(&self, branch: &Branch<Id>, slot: Slot) -> Branch<Id> {
let mut current = branch;
while current.header.slot() > slot {
current = &self.branches[&current.header.parent()];
while current.slot > slot {
current = &self.branches[&current.parent];
}
current.clone()
}
}
#[derive(Debug, Clone, Error)]
pub enum Error {
#[error("Ledger error: {0}")]
LedgerError(#[from] ledger::LedgerError),
pub enum Error<Id> {
#[error("Parent block: {0:?} is not know to this node")]
ParentMissing(HeaderId),
ParentMissing(Id),
#[error("Orphan proof has was not found in the ledger: {0:?}, can't import it")]
OrphanMissing(HeaderId),
OrphanMissing(Id),
}
impl Cryptarchia {
pub fn from_genesis(header: Header, state: LedgerState, config: Config) -> Self {
assert_eq!(header.slot(), Slot::genesis());
impl<Id> Cryptarchia<Id>
where
Id: Eq + std::hash::Hash + Copy,
{
pub fn from_genesis(id: Id, config: Config) -> Self {
Self {
ledger: Ledger::from_genesis(header.id(), state, config.clone()),
branches: Branches::from_genesis(&header),
branches: Branches::from_genesis(id),
local_chain: Branch {
header: header.clone(),
id,
length: 0,
parent: id,
slot: 0.into(),
},
config,
genesis: header.id(),
genesis: id,
}
}
#[must_use = "this returns the result of the operation, without modifying the original"]
pub fn receive_block(&self, block: Block) -> Result<Self, Error> {
let header = block.header();
pub fn receive_block(&self, id: Id, parent: Id, slot: Slot) -> Result<Self, Error<Id>> {
let mut new: Self = self.clone();
new.branches = new.branches.apply_header(header.clone());
new.ledger = new.ledger.try_apply_header(header)?;
new.branches = new.branches.apply_header(id, parent, slot)?;
new.local_chain = new.fork_choice();
Ok(new)
}
pub fn fork_choice(&self) -> Branch {
pub fn fork_choice(&self) -> Branch<Id> {
let k = self.config.security_param as u64;
let s = self.config.s();
Self::maxvalid_bg(self.local_chain.clone(), &self.branches, k, s)
}
pub fn tip(&self) -> &Header {
&self.local_chain.header
}
pub fn tip_id(&self) -> HeaderId {
self.local_chain.header.id()
pub fn tip(&self) -> Id {
self.local_chain.id
}
// prune all states deeper than 'depth' with regard to the current
@ -160,18 +166,18 @@ impl Cryptarchia {
todo!()
}
pub fn genesis(&self) -> &HeaderId {
&self.genesis
pub fn genesis(&self) -> Id {
self.genesis
}
pub fn branches(&self) -> &Branches {
pub fn branches(&self) -> &Branches<Id> {
&self.branches
}
// Implementation of the fork choice rule as defined in the Ouroboros Genesis paper
// k defines the forking depth of chain we accept without more analysis
// s defines the length of time (unit of slots) after the fork happened we will inspect for chain density
fn maxvalid_bg(local_chain: Branch, branches: &Branches, k: u64, s: u64) -> Branch {
fn maxvalid_bg(local_chain: Branch<Id>, branches: &Branches<Id>, k: u64, s: u64) -> Branch<Id> {
let mut cmax = local_chain;
let forks = branches.branches();
for chain in forks {
@ -185,7 +191,7 @@ impl Cryptarchia {
} else {
// The chain is forking too much, we need to pay a bit more attention
// In particular, select the chain that is the densest after the fork
let density_slot = Slot::from(u64::from(lowest_common_ancestor.header.slot()) + s);
let density_slot = Slot::from(u64::from(lowest_common_ancestor.slot) + s);
let cmax_density = branches.walk_back_before(&cmax, density_slot).length;
let candidate_density = branches.walk_back_before(&chain, density_slot).length;
if cmax_density < candidate_density {
@ -199,173 +205,72 @@ impl Cryptarchia {
#[cfg(test)]
pub mod tests {
use crate::{
crypto::Blake2b, Block, Commitment, Config, Header, HeaderId, LeaderProof, Nullifier, Slot,
TimeConfig,
};
use blake2::Digest;
use super::Cryptarchia;
use crate::Config;
use std::hash::{DefaultHasher, Hash, Hasher};
use super::{
ledger::{tests::genesis_state, LedgerError},
Cryptarchia, Error,
};
pub fn header(slot: impl Into<Slot>, parent: HeaderId, coin: Coin) -> Header {
let slot = slot.into();
Header::new(parent, 0, [0; 32].into(), slot, coin.to_proof(slot))
}
pub fn block(slot: impl Into<Slot>, parent: HeaderId, coin: Coin) -> Block {
Block::new(header(slot, parent, coin))
}
pub fn block_with_orphans(
slot: impl Into<Slot>,
parent: HeaderId,
coin: Coin,
orphans: Vec<Header>,
) -> Block {
Block::new(header(slot, parent, coin).with_orphaned_proofs(orphans))
}
pub fn propose_and_evolve(
slot: impl Into<Slot>,
parent: HeaderId,
coin: &mut Coin,
eng: &mut Cryptarchia,
) -> HeaderId {
let b = block(slot, parent, *coin);
let id = b.header().id();
*eng = eng.receive_block(b).unwrap();
*coin = coin.evolve();
id
}
pub fn genesis_header() -> Header {
Header::new(
[0; 32].into(),
0,
[0; 32].into(),
0.into(),
LeaderProof::dummy(0.into()),
)
}
fn engine(commitments: &[Commitment]) -> Cryptarchia {
Cryptarchia::from_genesis(genesis_header(), genesis_state(commitments), config())
}
pub fn config() -> Config {
Config {
security_param: 1,
active_slot_coeff: 1.0,
epoch_stake_distribution_stabilization: 4,
epoch_period_nonce_buffer: 3,
epoch_period_nonce_stabilization: 3,
time: TimeConfig {
slot_duration: 1,
chain_start_time: 0,
},
}
}
#[derive(Debug, Clone, Copy)]
pub struct Coin {
sk: u64,
nonce: u64,
}
impl Coin {
pub fn new(sk: u64) -> Self {
Self { sk, nonce: 0 }
}
pub fn commitment(&self) -> Commitment {
<[u8; 32]>::from(
Blake2b::new_with_prefix("commitment".as_bytes())
.chain_update(self.sk.to_be_bytes())
.chain_update(self.nonce.to_be_bytes())
.finalize(),
)
.into()
}
pub fn nullifier(&self) -> Nullifier {
<[u8; 32]>::from(
Blake2b::new_with_prefix("nullifier".as_bytes())
.chain_update(self.sk.to_be_bytes())
.chain_update(self.nonce.to_be_bytes())
.finalize(),
)
.into()
}
pub fn evolve(&self) -> Self {
let mut h = DefaultHasher::new();
self.nonce.hash(&mut h);
let nonce = h.finish();
Self { sk: self.sk, nonce }
}
pub fn to_proof(&self, slot: Slot) -> LeaderProof {
LeaderProof::new(
self.commitment(),
self.nullifier(),
slot,
self.evolve().commitment(),
)
}
}
#[test]
fn test_fork_choice() {
let mut long_coin = Coin::new(0);
let mut short_coin = Coin::new(1);
let mut long_dense_coin = Coin::new(2);
// TODO: use cryptarchia
let mut engine = engine(&[
long_coin.commitment(),
short_coin.commitment(),
long_dense_coin.commitment(),
]);
let mut engine = Cryptarchia::from_genesis([0; 32], config());
// by setting a low k we trigger the density choice rule, and the shorter chain is denser after
// the fork
engine.config.security_param = 10;
let mut parent = *engine.genesis();
let mut parent = engine.genesis();
for i in 1..50 {
parent = propose_and_evolve(i, parent, &mut long_coin, &mut engine);
let new_block = hash(&i);
engine = engine.receive_block(new_block, parent, i.into()).unwrap();
parent = new_block;
println!("{:?}", engine.tip());
}
println!("{:?}", engine.tip());
assert_eq!(engine.tip_id(), parent);
assert_eq!(engine.tip(), parent);
let mut long_p = parent;
let mut short_p = parent;
// the node sees first the short chain
for slot in 50..70 {
short_p = propose_and_evolve(slot, short_p, &mut short_coin, &mut engine);
let new_block = hash(&format!("short-{}", slot));
engine = engine
.receive_block(new_block, short_p, slot.into())
.unwrap();
short_p = new_block;
}
assert_eq!(engine.tip_id(), short_p);
assert_eq!(engine.tip(), short_p);
// then it receives a longer chain which is however less dense after the fork
for slot in 50..70 {
if slot % 2 == 0 {
long_p = propose_and_evolve(slot, long_p, &mut long_coin, &mut engine);
let new_block = hash(&format!("long-{}", slot));
engine = engine
.receive_block(new_block, long_p, slot.into())
.unwrap();
long_p = new_block;
}
assert_eq!(engine.tip_id(), short_p);
assert_eq!(engine.tip(), short_p);
}
// even if the long chain is much longer, it will never be accepted as it's not dense enough
for slot in 70..100 {
long_p = propose_and_evolve(slot, long_p, &mut long_coin, &mut engine);
assert_eq!(engine.tip_id(), short_p);
let new_block = hash(&format!("long-{}", slot));
engine = engine
.receive_block(new_block, long_p, slot.into())
.unwrap();
long_p = new_block;
assert_eq!(engine.tip(), short_p);
}
let bs = engine.branches().branches();
let long_branch = bs.iter().find(|b| b.header.id() == long_p).unwrap();
let short_branch = bs.iter().find(|b| b.header.id() == short_p).unwrap();
let long_branch = bs.iter().find(|b| b.id == long_p).unwrap();
let short_branch = bs.iter().find(|b| b.id == short_p).unwrap();
assert!(long_branch.length > short_branch.length);
// however, if we set k to the fork length, it will be accepted
@ -377,105 +282,27 @@ pub mod tests {
k,
engine.config.s()
)
.header
.id(),
.id,
long_p
);
// a longer chain which is equally dense after the fork will be selected as the main tip
for slot in 50..71 {
parent = propose_and_evolve(slot, parent, &mut long_dense_coin, &mut engine);
let new_block = hash(&format!("long-dense-{}", slot));
engine = engine
.receive_block(new_block, parent, slot.into())
.unwrap();
parent = new_block;
}
assert_eq!(engine.tip_id(), parent);
assert_eq!(engine.tip(), parent);
}
#[test]
fn test_orphan_proof_import() {
let coin = Coin::new(0);
let mut engine = engine(&[coin.commitment()]);
let coin_new = coin.evolve();
let coin_new_new = coin_new.evolve();
// produce a fork where the coin has been spent twice
let fork_1 = block(1, *engine.genesis(), coin);
let fork_2 = block(2, fork_1.header().id(), coin_new);
// neither of the evolved coins should be usable right away in another branch
assert!(matches!(
engine.receive_block(block(1, *engine.genesis(), coin_new)),
Err(Error::LedgerError(LedgerError::CommitmentNotFound))
));
assert!(matches!(
engine.receive_block(block(1, *engine.genesis(), coin_new_new)),
Err(Error::LedgerError(LedgerError::CommitmentNotFound))
));
// they also should not be accepted if the fork from where they have been imported has not been seen already
assert!(matches!(
engine.receive_block(block_with_orphans(
1,
*engine.genesis(),
coin_new,
vec![fork_1.header().clone()]
)),
Err(Error::LedgerError(LedgerError::OrphanMissing(_)))
));
// now the first block of the fork is seen (and accepted)
engine = engine.receive_block(fork_1.clone()).unwrap();
// and it can now be imported in another branch (note this does not validate it's for an earlier slot)
engine
.receive_block(block_with_orphans(
1,
*engine.genesis(),
coin_new,
vec![fork_1.header().clone()],
))
.unwrap();
// but the next coin is still not accepted since the second block using the evolved coin has not been seen yet
assert!(matches!(
engine.receive_block(block_with_orphans(
1,
*engine.genesis(),
coin_new_new,
vec![fork_1.header().clone(), fork_2.header().clone()]
)),
Err(Error::LedgerError(LedgerError::OrphanMissing(_)))
));
// now the second block of the fork is seen as well and the coin evolved twice can be used in another branch
engine = engine.receive_block(fork_2.clone()).unwrap();
engine
.receive_block(block_with_orphans(
1,
*engine.genesis(),
coin_new_new,
vec![fork_1.header().clone(), fork_2.header().clone()],
))
.unwrap();
// but we can't import just the second proof because it's using an evolved coin that has not been seen yet
assert!(matches!(
engine.receive_block(block_with_orphans(
1,
*engine.genesis(),
coin_new_new,
vec![fork_2.header().clone()]
)),
Err(Error::LedgerError(LedgerError::CommitmentNotFound))
));
// an imported proof that uses a coin that was already used in the base branch should not be allowed
let block_1 = block(1, *engine.genesis(), coin);
engine = engine.receive_block(block_1.clone()).unwrap();
assert!(matches!(
engine.receive_block(block_with_orphans(
2,
block_1.header().id(),
coin_new_new,
vec![fork_1.header().clone(), fork_2.header().clone()]
)),
Err(Error::LedgerError(LedgerError::NullifierExists))
));
fn hash<T: Hash>(t: &T) -> [u8; 32] {
let mut s = DefaultHasher::new();
t.hash(&mut s);
let hash = s.finish();
let mut res = [0; 32];
res[..8].copy_from_slice(&hash.to_be_bytes());
res
}
}

View File

@ -1,4 +1,3 @@
use crate::config::Config;
use std::ops::Add;
#[derive(Clone, Debug, Eq, PartialEq, Copy, Hash, PartialOrd, Ord)]
@ -15,10 +14,6 @@ impl Slot {
pub fn genesis() -> Self {
Self(0)
}
pub fn epoch(&self, config: &Config) -> Epoch {
Epoch((self.0 / config.epoch_length()) as u32)
}
}
impl From<u32> for Epoch {

View File

@ -0,0 +1,13 @@
[package]
name = "cryptarchia-ledger"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
blake2 = "0.10"
rpds = "1"
thiserror = "1"
# TODO: we only need types definition from this crate
cryptarchia-engine = { path = "../../consensus/cryptarchia-engine" }

View File

@ -1,5 +1,6 @@
use crate::{crypto::Blake2b, leader_proof::LeaderProof, time::Slot};
use crate::{crypto::Blake2b, leader_proof::LeaderProof};
use blake2::Digest;
use cryptarchia_engine::Slot;
#[derive(Clone, Debug, Eq, PartialEq, Copy, Hash)]
pub struct HeaderId([u8; 32]);
@ -84,25 +85,6 @@ impl Header {
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Block {
header: Header,
_contents: (),
}
impl Block {
pub fn header(&self) -> &Header {
&self.header
}
pub fn new(header: Header) -> Self {
Self {
header,
_contents: (),
}
}
}
// ----------- conversions
impl From<[u8; 32]> for Nonce {

View File

@ -0,0 +1,44 @@
use cryptarchia_engine::{Epoch, Slot};
#[derive(Clone, Debug, PartialEq)]
pub struct Config {
// The stake distribution is always taken at the beginning of the previous epoch.
// This parameters controls how many slots to wait for it to be stabilized
// The value is computed as epoch_stake_distribution_stabilization * int(floor(k / f))
pub epoch_stake_distribution_stabilization: u8,
// This parameter controls how many slots we wait after the stake distribution
// snapshot has stabilized to take the nonce snapshot.
pub epoch_period_nonce_buffer: u8,
// This parameter controls how many slots we wait for the nonce snapshot to be considered
// stabilized
pub epoch_period_nonce_stabilization: u8,
pub consensus_config: cryptarchia_engine::Config,
}
impl Config {
pub fn base_period_length(&self) -> u64 {
self.consensus_config.base_period_length()
}
pub fn epoch_length(&self) -> u64 {
(self.epoch_stake_distribution_stabilization as u64
+ self.epoch_period_nonce_buffer as u64
+ self.epoch_period_nonce_stabilization as u64)
* self.base_period_length()
}
pub fn nonce_snapshot(&self, epoch: Epoch) -> Slot {
let offset = self.base_period_length()
* (self.epoch_period_nonce_buffer + self.epoch_stake_distribution_stabilization) as u64;
let base = u32::from(epoch) as u64 * self.epoch_length();
(base + offset).into()
}
pub fn stake_distribution_snapshot(&self, epoch: Epoch) -> Slot {
(u32::from(epoch) as u64 * self.epoch_length()).into()
}
pub fn epoch(&self, slot: Slot) -> Epoch {
((u64::from(slot) / self.epoch_length()) as u32).into()
}
}

View File

@ -1,4 +1,4 @@
use crate::time::Slot;
use cryptarchia_engine::Slot;
#[derive(Clone, Debug, Eq, PartialEq, Copy, Hash)]
pub struct LeaderProof {

View File

@ -1,12 +1,19 @@
use crate::{
crypto::Blake2b, Commitment, Config, Epoch, Header, HeaderId, LeaderProof, Nonce, Nullifier,
Slot,
};
mod block;
mod config;
mod crypto;
mod leader_proof;
use crate::{crypto::Blake2b, Commitment, LeaderProof, Nullifier};
use blake2::Digest;
use cryptarchia_engine::{Epoch, Slot};
use rpds::HashTrieSet;
use std::collections::HashMap;
use thiserror::Error;
pub use block::*;
pub use config::Config;
pub use leader_proof::*;
#[derive(Clone, Debug, Error)]
pub enum LedgerError {
#[error("Commitment not found in the ledger state")]
@ -141,8 +148,8 @@ impl LedgerState {
});
}
let current_epoch = self.slot.epoch(config);
let new_epoch = slot.epoch(config);
let current_epoch = config.epoch(self.slot);
let new_epoch = config.epoch(slot);
// there are 3 cases to consider:
// 1. we are in the same epoch as the parent state
@ -198,7 +205,7 @@ impl LedgerState {
}
fn try_apply_proof(self, proof: &LeaderProof, config: &Config) -> Result<Self, LedgerError> {
assert_eq!(proof.slot().epoch(config), self.epoch_state.epoch);
assert_eq!(config.epoch(proof.slot()), self.epoch_state.epoch);
// The leadership coin either has to be in the state snapshot or be derived from
// a coin that is in the state snapshot (i.e. be in the lead coins commitments)
if !self.can_lead(proof.commitment())
@ -295,13 +302,97 @@ impl core::fmt::Debug for LedgerState {
#[cfg(test)]
pub mod tests {
use crate::{ledger::LedgerError, Commitment, Header};
use super::{
super::tests::{config, genesis_header, header, Coin},
EpochState, Ledger, LedgerState,
use super::{EpochState, Ledger, LedgerState};
use crate::{
crypto::Blake2b, Commitment, Config, Header, HeaderId, LeaderProof, LedgerError, Nullifier,
};
use blake2::Digest;
use cryptarchia_engine::Slot;
use std::hash::{DefaultHasher, Hash, Hasher};
pub fn header(slot: impl Into<Slot>, parent: HeaderId, coin: Coin) -> Header {
let slot = slot.into();
Header::new(parent, 0, [0; 32].into(), slot, coin.to_proof(slot))
}
pub fn header_with_orphans(
slot: impl Into<Slot>,
parent: HeaderId,
coin: Coin,
orphans: Vec<Header>,
) -> Header {
header(slot, parent, coin).with_orphaned_proofs(orphans)
}
pub fn genesis_header() -> Header {
Header::new(
[0; 32].into(),
0,
[0; 32].into(),
0.into(),
LeaderProof::dummy(0.into()),
)
}
pub fn config() -> Config {
Config {
epoch_stake_distribution_stabilization: 4,
epoch_period_nonce_buffer: 3,
epoch_period_nonce_stabilization: 3,
consensus_config: cryptarchia_engine::Config {
security_param: 1,
active_slot_coeff: 1.0,
},
}
}
#[derive(Debug, Clone, Copy)]
pub struct Coin {
sk: u64,
nonce: u64,
}
impl Coin {
pub fn new(sk: u64) -> Self {
Self { sk, nonce: 0 }
}
pub fn commitment(&self) -> Commitment {
<[u8; 32]>::from(
Blake2b::new_with_prefix("commitment".as_bytes())
.chain_update(self.sk.to_be_bytes())
.chain_update(self.nonce.to_be_bytes())
.finalize(),
)
.into()
}
pub fn nullifier(&self) -> Nullifier {
<[u8; 32]>::from(
Blake2b::new_with_prefix("nullifier".as_bytes())
.chain_update(self.sk.to_be_bytes())
.chain_update(self.nonce.to_be_bytes())
.finalize(),
)
.into()
}
pub fn evolve(&self) -> Self {
let mut h = DefaultHasher::new();
self.nonce.hash(&mut h);
let nonce = h.finish();
Self { sk: self.sk, nonce }
}
pub fn to_proof(&self, slot: Slot) -> LeaderProof {
LeaderProof::new(
self.commitment(),
self.nullifier(),
slot,
self.evolve().commitment(),
)
}
}
pub fn genesis_state(commitments: &[Commitment]) -> LedgerState {
LedgerState {
@ -513,4 +604,94 @@ pub mod tests {
let h_2_1 = header(21, h_2_0.id(), coin_1.evolve());
ledger.try_apply_header(&h_2_1).unwrap();
}
#[test]
fn test_orphan_proof_import() {
let coin = Coin::new(0);
let (mut ledger, genesis) = ledger(&[coin.commitment()]);
let coin_new = coin.evolve();
let coin_new_new = coin_new.evolve();
// produce a fork where the coin has been spent twice
let fork_1 = header(1, genesis.id(), coin);
let fork_2 = header(2, fork_1.id(), coin_new);
// neither of the evolved coins should be usable right away in another branch
assert!(matches!(
ledger.try_apply_header(&header(1, genesis.id(), coin_new)),
Err(LedgerError::CommitmentNotFound)
));
assert!(matches!(
ledger.try_apply_header(&header(1, genesis.id(), coin_new_new)),
Err(LedgerError::CommitmentNotFound)
));
// they also should not be accepted if the fork from where they have been imported has not been seen already
assert!(matches!(
ledger.try_apply_header(&header_with_orphans(
1,
genesis.id(),
coin_new,
vec![fork_1.clone()]
)),
Err(LedgerError::OrphanMissing(_))
));
// now the first block of the fork is seen (and accepted)
ledger = ledger.try_apply_header(&fork_1).unwrap();
// and it can now be imported in another branch (note this does not validate it's for an earlier slot)
ledger
.try_apply_header(&header_with_orphans(
1,
genesis.id(),
coin_new,
vec![fork_1.clone()],
))
.unwrap();
// but the next coin is still not accepted since the second block using the evolved coin has not been seen yet
assert!(matches!(
ledger.try_apply_header(&header_with_orphans(
1,
genesis.id(),
coin_new_new,
vec![fork_1.clone(), fork_2.clone()]
)),
Err(LedgerError::OrphanMissing(_))
));
// now the second block of the fork is seen as well and the coin evolved twice can be used in another branch
ledger = ledger.try_apply_header(&fork_2).unwrap();
ledger
.try_apply_header(&header_with_orphans(
1,
genesis.id(),
coin_new_new,
vec![fork_1.clone(), fork_2.clone()],
))
.unwrap();
// but we can't import just the second proof because it's using an evolved coin that has not been seen yet
assert!(matches!(
ledger.try_apply_header(&header_with_orphans(
1,
genesis.id(),
coin_new_new,
vec![fork_2.clone()]
)),
Err(LedgerError::CommitmentNotFound)
));
// an imported proof that uses a coin that was already used in the base branch should not be allowed
let header_1 = header(1, genesis.id(), coin);
ledger = ledger.try_apply_header(&header_1).unwrap();
assert!(matches!(
ledger.try_apply_header(&header_with_orphans(
2,
header_1.id(),
coin_new_new,
vec![fork_1.clone(), fork_2.clone()]
)),
Err(LedgerError::NullifierExists)
));
}
}