diff --git a/consensus/cryptarchia-engine/src/ledger.rs b/consensus/cryptarchia-engine/src/ledger.rs index 3e877123..0b19f768 100644 --- a/consensus/cryptarchia-engine/src/ledger.rs +++ b/consensus/cryptarchia-engine/src/ledger.rs @@ -84,6 +84,17 @@ impl Ledger { .ok_or(LedgerError::ParentNotFound(parent_id))?; let config = self.config.clone(); + // Oprhan proofs need to be: + // * locally valid for the block they were originally in + // * not in conflict with the current ledger state + // This first condition is checked here, the second one is checked in the state update + // (in particular, we do not check the imported leader proof is for an earlier slot) + for orphan in header.orphaned_proofs() { + if !self.states.contains_key(&orphan.id()) { + return Err(LedgerError::OrphanMissing(orphan.id())); + } + } + let new_state = parent_state .clone() .try_apply_header(header, &self.config)?; @@ -140,7 +151,6 @@ impl LedgerState { // use the next epoch state as the current epoch state and reset next epoch state // 3. we are in the next-next or later epoch: // use the parent state as the epoch state and reset next epoch state - if current_epoch == new_epoch { // case 1) let next_epoch_state = self @@ -222,6 +232,10 @@ impl LedgerState { header: &Header, config: &Config, ) -> Result { + for proof in header.orphaned_proofs() { + self = self.try_apply_proof(proof.leader_proof(), config)?; + } + self = self .try_apply_proof(header.leader_proof(), config)? .update_nonce(header.leader_proof()); diff --git a/consensus/cryptarchia-engine/src/lib.rs b/consensus/cryptarchia-engine/src/lib.rs index c93bc820..47674c5f 100644 --- a/consensus/cryptarchia-engine/src/lib.rs +++ b/consensus/cryptarchia-engine/src/lib.rs @@ -181,13 +181,6 @@ impl Cryptarchia { // Classic longest chain rule with parameter k if cmax.length < chain.length { cmax = chain; - } else { - println!( - "shorter {:?} {} {}", - chain.header.id(), - cmax.length, - chain.length - ) } } else { // The chain is forking too much, we need to pay a bit more attention @@ -197,13 +190,6 @@ impl Cryptarchia { let candidate_density = branches.walk_back_before(&chain, density_slot).length; if cmax_density < candidate_density { cmax = chain; - } else { - println!( - "less dense {:?} {} {}", - chain.header.id(), - cmax_density, - candidate_density - ) } } } @@ -220,7 +206,10 @@ pub mod tests { use blake2::Digest; use std::hash::{DefaultHasher, Hash, Hasher}; - use super::{ledger::tests::genesis_state, Cryptarchia}; + use super::{ + ledger::{tests::genesis_state, LedgerError}, + Cryptarchia, Error, + }; pub fn header(slot: impl Into, parent: HeaderId, coin: Coin) -> Header { let slot = slot.into(); @@ -231,6 +220,15 @@ pub mod tests { Block::new(header(slot, parent, coin)) } + pub fn block_with_orphans( + slot: impl Into, + parent: HeaderId, + coin: Coin, + orphans: Vec
, + ) -> Block { + Block::new(header(slot, parent, coin).with_orphaned_proofs(orphans)) + } + pub fn propose_and_evolve( slot: impl Into, parent: HeaderId, @@ -390,4 +388,94 @@ pub mod tests { } assert_eq!(engine.tip_id(), 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)) + )); + } }