Basic Cryptarchia fuzz test

This commit is contained in:
Youngjoon Lee 2024-03-28 13:40:35 +09:00
parent 7e4d00cc78
commit ed11d84fc7
No known key found for this signature in database
GPG Key ID: 7E8EE3EC8D5CEBA2
4 changed files with 250 additions and 7 deletions

View File

@ -3,6 +3,7 @@ use cryptarchia_ledger::{Coin, Commitment, Config, EpochState, LeaderProof};
use nomos_core::header::HeaderId;
use std::collections::HashMap;
#[derive(Clone, Debug)]
pub struct Leader {
coins: HashMap<HeaderId, Vec<Coin>>,
config: cryptarchia_ledger::Config,

View File

@ -1,4 +1,4 @@
mod leadership;
pub mod leadership;
pub mod network;
mod time;
@ -48,13 +48,14 @@ pub enum Error {
Consensus(#[from] cryptarchia_engine::Error<HeaderId>),
}
struct Cryptarchia {
ledger: cryptarchia_ledger::Ledger<HeaderId>,
consensus: cryptarchia_engine::Cryptarchia<HeaderId>,
#[derive(Debug, Clone)]
pub struct Cryptarchia {
pub ledger: cryptarchia_ledger::Ledger<HeaderId>,
pub consensus: cryptarchia_engine::Cryptarchia<HeaderId>,
}
impl Cryptarchia {
fn tip(&self) -> HeaderId {
pub fn tip(&self) -> HeaderId {
self.consensus.tip()
}
@ -62,7 +63,7 @@ impl Cryptarchia {
self.consensus.genesis()
}
fn try_apply_header(&self, header: &Header) -> Result<Self, Error> {
pub fn try_apply_header(&self, header: &Header) -> Result<Self, Error> {
let id = header.id();
let parent = header.parent();
let slot = header.slot();
@ -81,7 +82,7 @@ impl Cryptarchia {
Ok(Self { ledger, consensus })
}
fn epoch_state_for_slot(&self, slot: Slot) -> Option<&cryptarchia_ledger::EpochState> {
pub fn epoch_state_for_slot(&self, slot: Slot) -> Option<&cryptarchia_ledger::EpochState> {
let tip = self.tip();
let state = self.ledger.state(&tip).expect("no state for tip");
let requested_epoch = self.ledger.config().epoch(slot);

View File

@ -35,6 +35,8 @@ ntest = "0.9.0"
criterion = { version = "0.5", features = ["async_tokio"] }
nomos-cli = { path = "../nomos-cli" }
time = "0.3"
proptest = "1.4.0"
proptest-state-machine = "0.3.0"
[[test]]
name = "test_cryptarchia_happy_path"
@ -44,6 +46,9 @@ path = "src/tests/cryptarchia/happy.rs"
name = "test_cli"
path = "src/tests/cli.rs"
[[test]]
name = "fuzz_cryptarchia"
path = "src/tests/cryptarchia/fuzz.rs"
[features]
mixnet = ["nomos-network/mixnet"]

View File

@ -0,0 +1,236 @@
use std::collections::HashMap;
use cryptarchia_consensus::{leadership::Leader, Cryptarchia, Error};
use cryptarchia_engine::Slot;
use cryptarchia_ledger::{Coin, LedgerState};
use nomos_core::header::{
cryptarchia::{Builder, Header},
HeaderId,
};
use proptest::{
strategy::{BoxedStrategy, Just, Strategy},
test_runner::Config,
};
use proptest_state_machine::{prop_state_machine, ReferenceStateMachine, StateMachineTest};
prop_state_machine! {
#![proptest_config(Config {
// Only run 10 cases by default to avoid running out of system resources
// and taking too long to finish.
cases: 10,
.. Config::default()
})]
#[test]
// Run max 100 state transitions per test case
fn cryptarchia_ledger_test(sequential 1..100 => FollowerNode);
}
const GENESIS_ID: [u8; 32] = [0; 32];
const SECRET_KEY: [u8; 32] = [0; 32];
const INITIAL_COIN_NONCE: [u8; 32] = [0; 32];
const TOTAL_STAKE: u32 = 1;
const INITIAL_SLOT: u64 = 0;
const MAX_UNBROADCASTED_BLOCKS: usize = 5;
#[derive(Clone, Debug)]
enum Transition {
ProcessBlock(Header),
}
// A leader node that proposes all new blocks.
// This is used as a reference state that generates a sequence of transitions
// and can be compared against the SUT (the state machine that we want to test).
#[derive(Debug, Clone)]
struct LeaderNode {
cryptarchia: Cryptarchia,
leader: Leader,
next_slot: Slot,
unbroadcasted_blocks: HashMap<HeaderId, Header>,
}
impl ReferenceStateMachine for LeaderNode {
type State = Self;
type Transition = Transition;
// Initialize the reference state machine
fn init_state() -> BoxedStrategy<Self::State> {
let (cryptarchia, leader) = init();
let mut leader_node = Self {
cryptarchia,
leader,
next_slot: (INITIAL_SLOT + 1).into(),
unbroadcasted_blocks: HashMap::new(),
};
// Prepare several blocks in advance, so that they can be sent to the SUT (follower node)
// in random order. For details, see the `transitions` function below.
for _ in 0..MAX_UNBROADCASTED_BLOCKS {
leader_node.prepare_new_block();
}
Just(leader_node).boxed()
}
// Generate transitions based on the current reference state machine
fn transitions(state: &Self::State) -> BoxedStrategy<Self::Transition> {
state.transition_process_block()
}
// Check if the transition is valid for a given reference state, before applying the transition
// If invalid, the transition will be ignored and a new transition will be generated.
//
// In most cases, this will return true as the transition was generated as intended by the
// `transitions` function. However, this also checks if the transition is still valid during
// shrinking in failure cases. If the transition becomes invalid for the shrunk state,
// the shrinking is stopped or continued to other directions.
fn preconditions(state: &Self::State, transition: &Self::Transition) -> bool {
match transition {
Transition::ProcessBlock(header) => {
state.unbroadcasted_blocks.contains_key(&header.id())
}
}
}
// Apply the given transition on the reference state machine,
// so that it can be used to generate next transitions.
fn apply(mut state: Self::State, transition: &Self::Transition) -> Self::State {
match transition {
Transition::ProcessBlock(header) => {
// Remove the block from the unbroadcasted blocks.
state.unbroadcasted_blocks.remove(&header.id());
// Prepare a new block for the next slot.
state.prepare_new_block();
state
}
}
}
}
impl LeaderNode {
fn prepare_new_block(&mut self) {
// Propose a new block. This must be always succeeded.
let slot = self.next_slot;
let parent = self.cryptarchia.tip();
let epoch_state = self.cryptarchia.epoch_state_for_slot(slot).unwrap();
// Assume that the leader's coin is always eligible to lead the slot.
let proof = self
.leader
.build_proof_for(epoch_state, slot, parent)
.unwrap();
// Generate a new block with no content for simplicity.
let header = Builder::new(parent, proof).build([0; 32].into(), 0);
// Process the new block immediately in the leader node.
// This must be always succeeded.
self.cryptarchia = self.cryptarchia.try_apply_header(&header).unwrap();
self.leader.follow_chain(
header.parent(),
header.id(),
*header.leader_proof().commitment(),
);
// Keep the new block in the queue to broadcast later
self.unbroadcasted_blocks.insert(header.id(), header);
// Advance time
self.next_slot = self.next_slot + 1;
}
// Generate a Transition::ProcessBlock by choosing a block randomly from unbroadcasted blocks.
// The selected block will be sent to the SUT (follower node).
// The random selection simulates unpredictable network delays in p2p communication (even with mixnet).
fn transition_process_block(&self) -> BoxedStrategy<Transition> {
let unbroadcasted_blocks = self.unbroadcasted_blocks.clone();
proptest::sample::select(
self.unbroadcasted_blocks
.keys()
.cloned()
.collect::<Vec<_>>(),
)
.prop_map(move |header_id| {
Transition::ProcessBlock(unbroadcasted_blocks[&header_id].clone())
})
.boxed()
}
}
// A follower node that receives blocks from the leader node.
// This is used as a state machine (SUT) that we want to test.
struct FollowerNode {
cryptarchia: Cryptarchia,
}
impl StateMachineTest for FollowerNode {
// SUT is the real state machine that we want to test.
type SystemUnderTest = Self;
// A reference state machine that should be compared against the SUT.
type Reference = LeaderNode;
// Initialize the SUT
fn init_test(
_ref_state: &<Self::Reference as ReferenceStateMachine>::State,
) -> Self::SystemUnderTest {
let (cryptarchia, _) = init();
Self { cryptarchia }
}
// Apply the transition on the SUT state and check post-conditions
fn apply(
state: Self::SystemUnderTest,
_ref_state: &<Self::Reference as ReferenceStateMachine>::State,
transition: <Self::Reference as ReferenceStateMachine>::Transition,
) -> Self::SystemUnderTest {
match transition {
Transition::ProcessBlock(header) => match state.cryptarchia.try_apply_header(&header) {
Ok(cryptarchia) => Self { cryptarchia },
Err(Error::Ledger(cryptarchia_ledger::LedgerError::ParentNotFound(_parent)))
| Err(Error::Consensus(cryptarchia_engine::Error::ParentMissing(_parent))) => {
// TODO: Request the parent block. This is also not implemented yet in the main code.
// Because all blocks are proposed by a single leader that extends the chain
// without any forks, the follower node cannot extend the chain forever without
// the parent block.
state
}
Err(e) => {
panic!("unexpected invalid block: {:?}", e);
}
},
}
}
fn check_invariants(
_state: &Self::SystemUnderTest,
_ref_state: &<Self::Reference as ReferenceStateMachine>::State,
) {
// TODO: Check if the SUT is following the reference state (leader's chain).
}
}
// Initialize Cryptarchia (engine and ledger) and Leader
fn init() -> (Cryptarchia, Leader) {
let coin = Coin::new(SECRET_KEY, INITIAL_COIN_NONCE.into(), TOTAL_STAKE.into());
let genesis_state = LedgerState::from_commitments([coin.commitment()], TOTAL_STAKE.into());
let config = cryptarchia_ledger::Config {
epoch_stake_distribution_stabilization: 3,
epoch_period_nonce_buffer: 3,
epoch_period_nonce_stabilization: 4,
consensus_config: cryptarchia_engine::Config {
security_param: 1,
active_slot_coeff: 1.0,
},
};
(
Cryptarchia {
consensus: <cryptarchia_engine::Cryptarchia<_>>::from_genesis(
GENESIS_ID.into(),
config.consensus_config.clone(),
),
ledger: <cryptarchia_ledger::Ledger<_>>::from_genesis(
GENESIS_ID.into(),
genesis_state,
config.clone(),
),
},
Leader::new(GENESIS_ID.into(), vec![coin], config),
)
}