Basic Cryptarchia fuzz test
This commit is contained in:
parent
7e4d00cc78
commit
ed11d84fc7
|
@ -3,6 +3,7 @@ use cryptarchia_ledger::{Coin, Commitment, Config, EpochState, LeaderProof};
|
||||||
use nomos_core::header::HeaderId;
|
use nomos_core::header::HeaderId;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub struct Leader {
|
pub struct Leader {
|
||||||
coins: HashMap<HeaderId, Vec<Coin>>,
|
coins: HashMap<HeaderId, Vec<Coin>>,
|
||||||
config: cryptarchia_ledger::Config,
|
config: cryptarchia_ledger::Config,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
mod leadership;
|
pub mod leadership;
|
||||||
pub mod network;
|
pub mod network;
|
||||||
mod time;
|
mod time;
|
||||||
|
|
||||||
|
@ -48,13 +48,14 @@ pub enum Error {
|
||||||
Consensus(#[from] cryptarchia_engine::Error<HeaderId>),
|
Consensus(#[from] cryptarchia_engine::Error<HeaderId>),
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Cryptarchia {
|
#[derive(Debug, Clone)]
|
||||||
ledger: cryptarchia_ledger::Ledger<HeaderId>,
|
pub struct Cryptarchia {
|
||||||
consensus: cryptarchia_engine::Cryptarchia<HeaderId>,
|
pub ledger: cryptarchia_ledger::Ledger<HeaderId>,
|
||||||
|
pub consensus: cryptarchia_engine::Cryptarchia<HeaderId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cryptarchia {
|
impl Cryptarchia {
|
||||||
fn tip(&self) -> HeaderId {
|
pub fn tip(&self) -> HeaderId {
|
||||||
self.consensus.tip()
|
self.consensus.tip()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,7 +63,7 @@ impl Cryptarchia {
|
||||||
self.consensus.genesis()
|
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 id = header.id();
|
||||||
let parent = header.parent();
|
let parent = header.parent();
|
||||||
let slot = header.slot();
|
let slot = header.slot();
|
||||||
|
@ -81,7 +82,7 @@ impl Cryptarchia {
|
||||||
Ok(Self { ledger, consensus })
|
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 tip = self.tip();
|
||||||
let state = self.ledger.state(&tip).expect("no state for tip");
|
let state = self.ledger.state(&tip).expect("no state for tip");
|
||||||
let requested_epoch = self.ledger.config().epoch(slot);
|
let requested_epoch = self.ledger.config().epoch(slot);
|
||||||
|
|
|
@ -35,6 +35,8 @@ ntest = "0.9.0"
|
||||||
criterion = { version = "0.5", features = ["async_tokio"] }
|
criterion = { version = "0.5", features = ["async_tokio"] }
|
||||||
nomos-cli = { path = "../nomos-cli" }
|
nomos-cli = { path = "../nomos-cli" }
|
||||||
time = "0.3"
|
time = "0.3"
|
||||||
|
proptest = "1.4.0"
|
||||||
|
proptest-state-machine = "0.3.0"
|
||||||
|
|
||||||
[[test]]
|
[[test]]
|
||||||
name = "test_cryptarchia_happy_path"
|
name = "test_cryptarchia_happy_path"
|
||||||
|
@ -44,6 +46,9 @@ path = "src/tests/cryptarchia/happy.rs"
|
||||||
name = "test_cli"
|
name = "test_cli"
|
||||||
path = "src/tests/cli.rs"
|
path = "src/tests/cli.rs"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "fuzz_cryptarchia"
|
||||||
|
path = "src/tests/cryptarchia/fuzz.rs"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
mixnet = ["nomos-network/mixnet"]
|
mixnet = ["nomos-network/mixnet"]
|
||||||
|
|
|
@ -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),
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue