Skip to content

Commit 0918356

Browse files
authored
test: improve the data generation for the consensus simulation (#510)
* test: adjust some log messages for the simulation Signed-off-by: etorreborre <etorreborre@yahoo.com> * test: fix a possible deadlock when running the simulation with too many downstream peers Signed-off-by: etorreborre <etorreborre@yahoo.com> * test: only reverse heap entries when added to the heap Signed-off-by: etorreborre <etorreborre@yahoo.com> * test: parametrize the number of peers for the data generation Signed-off-by: etorreborre <etorreborre@yahoo.com> * chore: rebase on main Signed-off-by: etorreborre <etorreborre@yahoo.com> | Conflicts: | simulation/amaru-sim/src/simulator/run.rs * test: use the test generation from headers tree for the simulation Signed-off-by: etorreborre <etorreborre@yahoo.com> * feat: don't terminate on validation errors, just log Signed-off-by: etorreborre <etorreborre@yahoo.com> * test: make the simulation env. variables a bit more lenient Signed-off-by: etorreborre <etorreborre@yahoo.com> * test: adjust the simulation oracle for generated data Signed-off-by: etorreborre <etorreborre@yahoo.com> * test: remove the unused data generation files Signed-off-by: etorreborre <etorreborre@yahoo.com> | Conflicts: | simulation/amaru-sim/src/simulator/args.rs | simulation/amaru-sim/src/simulator/ledger.rs | simulation/amaru-sim/src/simulator/node_config.rs * test: only check the messages sent to the first downstream peer Signed-off-by: etorreborre <etorreborre@yahoo.com> * test: add an argument to vary the depth of generated chains Signed-off-by: etorreborre <etorreborre@yahoo.com> * test: add generation arguments Signed-off-by: etorreborre <etorreborre@yahoo.com> * chore: implement some coderabbit suggestions Signed-off-by: etorreborre <etorreborre@yahoo.com> * chore: rebase on main Signed-off-by: etorreborre <etorreborre@yahoo.com> * test: pass around the best chain for a generated tree of headers to write assertions Signed-off-by: etorreborre <etorreborre@yahoo.com> --------- Signed-off-by: etorreborre <etorreborre@yahoo.com>
1 parent baba192 commit 0918356

23 files changed

Lines changed: 438 additions & 1393 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/amaru-consensus/src/consensus/headers_tree/data_generation/actions.rs

Lines changed: 92 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
use crate::consensus::errors::ConsensusError;
2424
use crate::consensus::headers_tree::Tracker::{Me, SomePeer};
2525
use crate::consensus::headers_tree::data_generation::SelectionResult::{Back, Forward};
26-
use crate::consensus::headers_tree::data_generation::any_tree_of_headers;
26+
use crate::consensus::headers_tree::data_generation::any_tree_of_headers_and_best_chain;
2727
use crate::consensus::headers_tree::tree::Tree;
2828
use crate::consensus::headers_tree::{HeadersTree, Tracker};
2929
use crate::consensus::stages::select_chain::RollbackChainSelection::RollbackBeyondLimit;
@@ -44,6 +44,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
4444
use std::cmp::Reverse;
4545
use std::collections::BTreeMap;
4646
use std::fmt::{Debug, Display, Formatter};
47+
use std::str::FromStr;
4748
use std::sync::Arc;
4849

4950
/// This data type models the events sent by the ChainSync mini-protocol with simplified data for the tests.
@@ -315,6 +316,40 @@ pub fn random_walk(
315316
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
316317
pub struct Ratio(pub u32, pub u32);
317318

319+
impl Display for Ratio {
320+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
321+
write!(f, "{}/{}", self.0, self.1)
322+
}
323+
}
324+
325+
impl FromStr for Ratio {
326+
type Err = String;
327+
328+
fn from_str(s: &str) -> Result<Self, Self::Err> {
329+
let parts: Vec<&str> = s.split('/').collect();
330+
if parts.len() != 2 {
331+
return Err("Ratio must be in the form 'numerator/denominator'".to_string());
332+
}
333+
let numerator = parts[0]
334+
.parse::<u32>()
335+
.map_err(|_| format!("Invalid numerator in ratio: {}", parts[0]))?;
336+
let denominator = parts[1]
337+
.parse::<u32>()
338+
.map_err(|_| format!("Invalid denominator in ratio: {}", parts[1]))?;
339+
340+
if denominator == 0 {
341+
return Err("Ratio denominator must be > 0".to_string());
342+
}
343+
if numerator > denominator {
344+
return Err(format!(
345+
"Ratio must be <= 1.0, got {}/{}",
346+
numerator, denominator
347+
));
348+
}
349+
Ok(Ratio(numerator, denominator))
350+
}
351+
}
352+
318353
/// Generate random walks for a fixed number of peers on a given tree of headers.
319354
///
320355
/// The returned list of actions is transposed so that the actions from different peers are interleaved.
@@ -342,25 +377,34 @@ pub fn generate_random_walks(
342377
.collect()
343378
}
344379

345-
/// Generate a random list of actions, for a fixed number of peers, with:
380+
/// Generate a random list of actions, for a number of peers.
346381
///
347-
/// - A tree of headers of depth `depth`.
348-
/// - A `max_length` for how far rollback actions can go in the past.
382+
/// We first generate a tree of headers of depth `depth` with a given `branching_ratio`
383+
/// (the probability that a branch is created from an original spine of headers).
349384
///
350-
/// If we use `max_length` < depth we will generate test cases where the `HeadersTree` will
351-
/// have to be truncated after a number of roll forwards.
385+
/// Then we execute a random walk on that tree for `peers_nb` peers, with a given `rollback_ratio`,
386+
/// which is how often a given peer will rollback.
352387
///
353388
/// Important note: this function returns a prop test strategy but the random walks are generated
354389
/// using a `StdRng` generator. This makes the generator reproducible, because the `StdGenerator`
355390
/// is given a seed controlled by `proptest` but this makes the resulting list of actions non-shrinkable.
356391
///
357392
pub fn any_select_chains(
393+
peers_nb: usize,
358394
depth: usize,
359395
rollback_ratio: Ratio,
360-
) -> impl Strategy<Value = Vec<Action>> {
361-
any_tree_of_headers(depth, Ratio(1, 2)).prop_flat_map(move |tree| {
362-
(1..u64::MAX).prop_map(move |seed| generate_random_walks(&tree, 5, rollback_ratio, seed))
363-
})
396+
branching_ratio: Ratio,
397+
) -> impl Strategy<Value = (Vec<Action>, Chain)> {
398+
any_tree_of_headers_and_best_chain(depth, branching_ratio).prop_flat_map(
399+
move |(tree, best_chain)| {
400+
(1..u64::MAX).prop_map(move |seed| {
401+
(
402+
generate_random_walks(&tree, peers_nb, rollback_ratio, seed),
403+
best_chain.clone(),
404+
)
405+
})
406+
},
407+
)
364408
}
365409

366410
/// Create an empty `HeadersTree` handling chains of maximum length `max_length` and
@@ -476,12 +520,15 @@ pub fn execute_json_actions(
476520
}
477521

478522
/// Type alias for a chain of headers tracked by a peer
479-
type Chain = Vec<BlockHeader>;
523+
pub type Chain = Vec<BlockHeader>;
480524

481525
/// This function computes the chains sent by each peer from a list of actions.
482526
/// Once all the actions have been executed it returns the chains that are the longest.
483527
///
484-
/// The return value is a list of lists of chains, because it returns one list of chains per action.
528+
/// The return value is a list of lists of chains:
529+
///
530+
/// - For each action we produce the resulting best chains.
531+
/// - So that the last list of chains is the best chains after executing all the actions.
485532
///
486533
pub fn make_best_chains_from_actions(actions: &Vec<Action>) -> Vec<Vec<Chain>> {
487534
let mut all_best_chains: Vec<Vec<Chain>> = vec![];
@@ -584,6 +631,8 @@ where
584631

585632
#[cfg(test)]
586633
mod tests {
634+
use super::*;
635+
587636
#[test]
588637
fn transpose_works() {
589638
let rows = vec![
@@ -600,7 +649,37 @@ mod tests {
600649
vec![3, 8],
601650
vec![9],
602651
];
603-
let result = super::transpose(rows);
652+
let result = transpose(rows);
604653
assert_eq!(result, expected);
605654
}
655+
656+
#[test]
657+
fn parse_ratio_works() {
658+
let ratio: Ratio = "3/4".parse().unwrap();
659+
assert_eq!(ratio, Ratio(3, 4));
660+
661+
let ratio: super::Ratio = "0/1".parse().unwrap();
662+
assert_eq!(ratio, Ratio(0, 1));
663+
664+
let ratio: super::Ratio = "1/1".parse().unwrap();
665+
assert_eq!(ratio, Ratio(1, 1));
666+
}
667+
668+
#[test]
669+
fn parse_ratio_with_errors() {
670+
let err = "3".parse::<Ratio>().unwrap_err();
671+
assert_eq!(err, "Ratio must be in the form 'numerator/denominator'");
672+
673+
let err = "a/2".parse::<Ratio>().unwrap_err();
674+
assert_eq!(err, "Invalid numerator in ratio: a");
675+
676+
let err = "1/b".parse::<Ratio>().unwrap_err();
677+
assert_eq!(err, "Invalid denominator in ratio: b");
678+
679+
let err = "1/0".parse::<Ratio>().unwrap_err();
680+
assert_eq!(err, "Ratio denominator must be > 0");
681+
682+
let err = "5/4".parse::<Ratio>().unwrap_err();
683+
assert_eq!(err, "Ratio must be <= 1.0, got 5/4");
684+
}
606685
}

crates/amaru-consensus/src/consensus/headers_tree/data_generation/data_generation.rs

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
//!
2222
2323
use crate::consensus::headers_tree::HeadersTree;
24-
use crate::consensus::headers_tree::data_generation::Ratio;
24+
use crate::consensus::headers_tree::data_generation::{Chain, Ratio};
2525
use crate::consensus::headers_tree::tree::Tree;
2626
use amaru_kernel::peer::Peer;
2727
use amaru_kernel::{Bytes, HEADER_HASH_SIZE, Header, HeaderHash};
@@ -33,23 +33,46 @@ use rand::prelude::StdRng;
3333
use rand::{Rng, RngCore, SeedableRng};
3434
use std::sync::Arc;
3535

36+
/// Return a `proptest` Strategy producing a random `Tree<BlockHeader>` of a given depth
37+
/// Additionally, we return the best chain corresponding to the spine of the tree.
38+
pub fn any_tree_of_headers_and_best_chain(
39+
depth: usize,
40+
branching_ratio: Ratio,
41+
) -> impl Strategy<Value = (Tree<BlockHeader>, Chain)> {
42+
(0..u64::MAX)
43+
.prop_map(move |seed| generate_header_tree_and_best_chain(depth, seed, branching_ratio))
44+
}
45+
3646
/// Return a `proptest` Strategy producing a random `Tree<BlockHeader>` of a given depth
3747
pub fn any_tree_of_headers(
3848
depth: usize,
3949
branching_ratio: Ratio,
4050
) -> impl Strategy<Value = Tree<BlockHeader>> {
41-
(0..u64::MAX).prop_map(move |seed| generate_header_tree(depth, seed, branching_ratio))
51+
any_tree_of_headers_and_best_chain(depth, branching_ratio).prop_map(|(tree, _)| tree)
4252
}
4353

4454
/// Generate a tree of headers of a given depth.
4555
/// A seed is used to control the random generation of subtrees on top of a spine of length `depth`.
46-
pub fn generate_header_tree(depth: usize, seed: u64, branching_ratio: Ratio) -> Tree<BlockHeader> {
56+
///
57+
/// We also return the chain corresponding to the spine of the tree. This is the best chain.
58+
pub fn generate_header_tree_and_best_chain(
59+
depth: usize,
60+
seed: u64,
61+
branching_ratio: Ratio,
62+
) -> (Tree<BlockHeader>, Chain) {
4763
let mut rng = StdRng::seed_from_u64(seed);
4864

4965
let root = generate_header(1, 1, None, &mut rng);
5066
let mut root_tree = Tree::make_leaf(&root);
51-
generate_header_subtree(&mut rng, &mut root_tree, depth - 1, branching_ratio);
52-
root_tree
67+
let mut spine = generate_header_subtree(&mut rng, &mut root_tree, depth - 1, branching_ratio);
68+
spine.insert(0, root);
69+
(root_tree, spine)
70+
}
71+
72+
/// Generate a tree of headers of a given depth.
73+
/// A seed is used to control the random generation of subtrees on top of a spine of length `depth`.
74+
pub fn generate_header_tree(depth: usize, seed: u64, branching_ratio: Ratio) -> Tree<BlockHeader> {
75+
generate_header_tree_and_best_chain(depth, seed, branching_ratio).0
5376
}
5477

5578
/// Given a random generator and a tree:
@@ -66,7 +89,7 @@ fn generate_header_subtree(
6689
tree: &mut Tree<BlockHeader>,
6790
depth: usize,
6891
branching_ratio: Ratio,
69-
) {
92+
) -> Chain {
7093
let header_body = tree.value.header_body().clone();
7194
let mut spine = generate_headers(
7295
depth,
@@ -90,6 +113,7 @@ fn generate_header_subtree(
90113
generate_header_subtree(rng, current, other_branch_depth, branching_ratio);
91114
}
92115
}
116+
spine
93117
}
94118

95119
/// Generate a chain of headers anchored at a given header.

crates/amaru-consensus/src/consensus/headers_tree/headers_tree.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1284,11 +1284,13 @@ mod tests {
12841284
const MAX_LENGTH: usize = 5;
12851285
const TEST_CASES_NB: u32 = 1000;
12861286
const ROLLBACK_RATIO: Ratio = Ratio(1, 2);
1287+
const BRANCHING_RATIO: Ratio = Ratio(1, 2);
12871288

12881289
proptest! {
12891290
#![proptest_config(config_begin().no_shrink().with_cases(TEST_CASES_NB).end())]
12901291
#[test]
1291-
fn run_chain_selection(actions in any_select_chains(DEPTH, ROLLBACK_RATIO)) {
1292+
fn run_chain_selection(generated in any_select_chains(DEPTH, 5, ROLLBACK_RATIO, BRANCHING_RATIO)) {
1293+
let actions = generated.0;
12921294
let results = execute_actions(MAX_LENGTH, &actions, false).unwrap();
12931295
let actual_chains = make_best_chains_from_results(&results);
12941296
let expected_chains = make_best_chains_from_actions(&actions);

crates/amaru-consensus/src/consensus/stages/receive_header.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ pub async fn stage(
4444
raw_header,
4545
span,
4646
} => {
47+
// TODO: check the point vs the deserialized header point and invalidate if they don't match
48+
// then simplify and don't pass the point separately
4749
let header = match decode_header(&point, raw_header.as_slice()) {
4850
Ok(header) => {
4951
if header.point() != point {
@@ -282,6 +284,15 @@ mod tests {
282284
Ok(())
283285
}
284286

287+
#[test]
288+
fn decode_header_on_generated_header() {
289+
let header = run(any_header());
290+
let raw_header = cbor::to_vec(header.clone()).unwrap();
291+
let point = header.point();
292+
let decoded = decode_header(&point, &raw_header).unwrap();
293+
assert_eq!(header, decoded);
294+
}
295+
285296
// HELPERS
286297

287298
fn make_state() -> State {

crates/amaru-ouroboros-traits/src/is_header/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ impl BlockHeader {
134134
&self.header.header_body
135135
}
136136

137+
pub fn parent_hash(&self) -> Option<HeaderHash> {
138+
self.header.header_body.prev_hash
139+
}
140+
137141
fn recompute_hash(&mut self) {
138142
self.hash = Hasher::<{ HEADER_HASH_SIZE * 8 }>::hash_cbor(&self.header);
139143
}

crates/amaru-ouroboros-traits/src/is_header/tests.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
use super::*;
1616
use proptest::prelude::*;
1717
use proptest::strategy::ValueTree;
18-
use proptest::test_runner::TestRunner;
18+
use proptest::test_runner::{Config, RngSeed, TestRunner};
1919

2020
/// Make a mostly empty Header with the given block_number, slot and previous hash
2121
pub fn make_header(block_number: u64, slot: u64, prev_hash: Option<HeaderHash>) -> Header {
@@ -121,3 +121,15 @@ pub fn run<T>(s: impl Strategy<Value = T>) -> T {
121121
let mut runner = TestRunner::default();
122122
s.new_tree(&mut runner).unwrap().current()
123123
}
124+
125+
/// Run a strategy with a seed provided by a random generator
126+
/// and return the generated value, panicking if generation fails.
127+
#[expect(clippy::unwrap_used)]
128+
pub fn run_with_rng<T, RNG: Rng>(rng: &mut RNG, s: impl Strategy<Value = T>) -> T {
129+
let config = Config {
130+
rng_seed: RngSeed::Fixed(rng.random()),
131+
..Default::default()
132+
};
133+
let mut runner = TestRunner::new(config);
134+
s.new_tree(&mut runner).unwrap().current()
135+
}

crates/amaru/src/stages/build_stage_graph.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,9 @@ pub fn build_stage_graph(
5959
// TODO: currently only validate_header errors, will need to grow into all error handling
6060
let validation_errors_stage = network.stage(
6161
"validation_errors",
62-
async |_, error: ValidationFailed, eff| {
62+
async |_, error: ValidationFailed, _eff| {
6363
tracing::error!(%error, "stage error");
64-
// TODO: implement specific actions once we have an upstream network
65-
// termination here will tear down the entire stage graph
66-
eff.terminate().await
64+
// TODO: disconnect bad behaving peers
6765
},
6866
);
6967

simulation/amaru-sim/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ tracing.workspace = true
3535
# Internal dependencies ───────────────────────────────────────────────────────┐
3636
amaru.workspace = true
3737
amaru-kernel = { workspace = true, features = ["test-utils"] }
38-
amaru-consensus.workspace = true
38+
amaru-consensus = { workspace = true, features = ["test-utils"] }
39+
amaru-ouroboros-traits = { workspace = true, features = ["test-utils"] }
3940
amaru-ouroboros.workspace = true
4041
amaru-slot-arithmetic.workspace = true
4142

0 commit comments

Comments
 (0)