Add orphan leader proofs import (#600)

* Add orphan leader proofs import

* add additional test case covering double import
This commit is contained in:
Giacomo Pasini 2024-03-04 16:02:28 +01:00 committed by GitHub
parent a036b8adc3
commit 31c5f69121
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 118 additions and 16 deletions

View File

@ -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<Self, LedgerError> {
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());

View File

@ -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<Slot>, 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<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,
@ -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))
));
}
}