Skip to content

Commit 3c9b2a2

Browse files
committed
add tests using Arnauds synthetic chain
1 parent 5379528 commit 3c9b2a2

13 files changed

Lines changed: 792 additions & 17 deletions

File tree

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ target/
1414
*.pdb
1515

1616
# Default database location for the ledger
17-
ledger.db
17+
/*ledger.db/
1818

1919
# Default database location for the consensus
20-
chain.db
20+
/*chain.db/
2121

2222
# Insta not-yet reviewed snapshots
2323
*.snap.new

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ default-members = ["crates/*"]
1515
resolver = "2"
1616

1717
[workspace.dependencies]
18+
acto = { version = "0.7.2", features = ["tokio"] }
1819
anyhow = "1.0.95"
1920
async-trait = "0.1.83"
2021
bech32 = "0.11.0"

Makefile

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ AMARU_PEER_ADDRESS ?= 127.0.0.1:3000
33
HASKELL_NODE_CONFIG_DIR ?= cardano-node-config
44
DEMO_TARGET_EPOCH ?= 173
55
HASKELL_NODE_CONFIG_SOURCE := https://book.world.dev.cardano.org/environments
6+
DB_PREFIX ?= amaru
7+
LISTEN_ADDRESS ?= 0.0.0.0:0
8+
9+
LEDGER_DIR = $(DB_PREFIX)-ledger
10+
CHAIN_DIR = $(DB_PREFIX)-chain
611

712
.PHONY: help bootstrap run import-snapshots import-headers import-nonces download-haskell-config
813

@@ -35,32 +40,44 @@ download-haskell-config: ## Download Cardano Haskell configuration for $NETWORK
3540

3641
import-snapshots: snapshots ## Import PreProd snapshots for demo
3742
cargo run -- import-ledger-state \
43+
--ledger-dir $(LEDGER_DIR) \
3844
--snapshot $^/69206375.6f99b5f3deaeae8dc43fce3db2f3cd36ad8ed174ca3400b5b1bed76fdf248912.cbor \
3945
--snapshot $^/69638382.5da6ba37a4a07df015c4ea92c880e3600d7f098b97e73816f8df04bbb5fad3b7.cbor \
4046
--snapshot $^/70070379.d6fe6439aed8bddc10eec22c1575bf0648e4a76125387d9e985e9a3f8342870d.cbor
4147

4248
import-headers: ## Import headers from $AMARU_PEER_ADDRESS for demo
4349
cargo run -- import-headers \
50+
--chain-dir $(CHAIN_DIR) \
4451
--peer-address ${AMARU_PEER_ADDRESS} \
4552
--starting-point 69638365.4ec0f5a78431fdcc594eab7db91aff7dfd91c13cc93e9fbfe70cd15a86fadfb2 \
4653
--count 2
4754
cargo run -- import-headers \
55+
--chain-dir $(CHAIN_DIR) \
4856
--peer-address ${AMARU_PEER_ADDRESS} \
4957
--starting-point 70070331.076218aa483344e34620d3277542ecc9e7b382ae2407a60e177bc3700548364c \
5058
--count 2
5159

5260
import-nonces: ## Import PreProd nonces for demo
5361
cargo run -- import-nonces \
62+
--chain-dir $(CHAIN_DIR) \
5463
--at 70070379.d6fe6439aed8bddc10eec22c1575bf0648e4a76125387d9e985e9a3f8342870d \
5564
--active a7c4477e9fcfd519bf7dcba0d4ffe35a399125534bc8c60fa89ff6b50a060a7a \
5665
--candidate 74fe03b10c4f52dd41105a16b5f6a11015ec890a001a5253db78a779fe43f6b6 \
5766
--evolving 24bb737ee28652cd99ca41f1f7be568353b4103d769c6e1ddb531fc874dd6718 \
5867
--tail 5da6ba37a4a07df015c4ea92c880e3600d7f098b97e73816f8df04bbb5fad3b7
5968

60-
bootstrap: import-headers import-nonces import-snapshots ## Bootstrap the node
69+
clear-db: ## Clear the database
70+
rm -rf $(LEDGER_DIR) $(CHAIN_DIR)
71+
72+
bootstrap: clear-db import-headers import-nonces import-snapshots ## Bootstrap the node
6173

6274
dev: ## Compile and run for development with default options
63-
cargo run -- daemon --peer-address=$(AMARU_PEER_ADDRESS) --network=$(NETWORK)
75+
cargo run -- daemon \
76+
--ledger-dir $(LEDGER_DIR) \
77+
--chain-dir $(CHAIN_DIR) \
78+
--peer-address=$(AMARU_PEER_ADDRESS) \
79+
--network=$(NETWORK) \
80+
--listen-address=$(LISTEN_ADDRESS)
6481

6582
test-e2e: ## Run snapshot tests, assuming snapshots are available.
6683
cargo test -p amaru -- --ignored

crates/amaru-consensus/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ documentation.workspace = true
1313
rust-version.workspace = true
1414

1515
[dependencies]
16+
acto.workspace = true
1617
async-trait.workspace = true
1718
gasket = { workspace = true, features = ["derive"] }
1819
pallas-codec.workspace = true
@@ -30,12 +31,13 @@ amaru-kernel.workspace = true
3031
amaru-ledger.workspace = true
3132
amaru-ouroboros.workspace = true
3233
amaru-ouroboros-traits.workspace = true
33-
acto = { version = "0.7.2", features = ["tokio"] }
3434

3535
[dev-dependencies]
3636
envpath.workspace = true
3737
hex.workspace = true
3838
insta.workspace = true
39+
minicbor.workspace = true
3940
proptest.workspace = true
4041
rand.workspace = true
42+
serde_json.workspace = true
4143
tempfile.workspace = true

crates/amaru-consensus/src/chain_forward.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,6 @@ pub type UpstreamPort = gasket::messaging::InputPort<BlockValidationResult>;
3939

4040
pub const EVENT_TARGET: &str = "amaru::consensus::chain_forward";
4141

42-
// FIXME: should be default for an incoming configuration, eventually.
43-
pub const LISTEN_ADDRESS: &str = "0.0.0.0:3001";
44-
4542
/// Forwarding stage of the consensus where blocks are stored and made
4643
/// available to downstream peers.
4744
///
@@ -55,16 +52,22 @@ pub struct ForwardStage {
5552
pub upstream: UpstreamPort,
5653
pub network_magic: u64,
5754
pub runtime: AcTokio,
55+
pub listen_address: String,
5856
}
5957

6058
impl ForwardStage {
61-
pub fn new(store: Arc<Mutex<dyn ChainStore<Header>>>, network_magic: u64) -> Self {
59+
pub fn new(
60+
store: Arc<Mutex<dyn ChainStore<Header>>>,
61+
network_magic: u64,
62+
listen_address: &str,
63+
) -> Self {
6264
let runtime = AcTokio::new("consensus.forward", 1).unwrap();
6365
Self {
6466
store,
6567
upstream: Default::default(),
6668
network_magic,
6769
runtime,
70+
listen_address: listen_address.to_string(),
6871
}
6972
}
7073
}
@@ -90,7 +93,7 @@ impl Drop for Worker {
9093
#[async_trait::async_trait(?Send)]
9194
impl gasket::framework::Worker<ForwardStage> for Worker {
9295
async fn bootstrap(stage: &ForwardStage) -> Result<Self, WorkerError> {
93-
let server = TcpListener::bind(LISTEN_ADDRESS).await.or_panic()?;
96+
let server = TcpListener::bind(&stage.listen_address).await.or_panic()?;
9497
let (tx, incoming_peers) = mpsc::channel(10);
9598

9699
let clients = stage
@@ -265,3 +268,6 @@ async fn client_supervisor(
265268

266269
mod client_protocol;
267270
mod client_state;
271+
272+
#[cfg(test)]
273+
mod tests;

crates/amaru-consensus/src/chain_forward/client_protocol.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@ async fn chain_sync(
105105
return Err(ClientError::EarlyRequestNext);
106106
};
107107

108-
let Some((intersection, client_at)) = find_headers_between(&store, &tip.0, &req).await else {
108+
let Some((intersection, client_at)) = find_headers_between(&*store.lock().await, &tip.0, &req)
109+
else {
109110
server.send_intersect_not_found(tip).await?;
110111
return Err(ClientError::NoIntersection);
111112
};

crates/amaru-consensus/src/chain_forward/client_state.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use pallas_network::miniprotocols::{chainsync::Tip, Point};
55
use std::{collections::VecDeque, sync::Arc};
66
use tokio::sync::Mutex;
77

8-
#[derive(Debug, Clone)]
8+
#[derive(Debug, Clone, PartialEq, Eq)]
99
pub(super) enum ClientOp {
1010
Backward(Point),
1111
Forward(Header),
@@ -72,14 +72,20 @@ impl ClientState {
7272
/// Otherwise returns Some(headers) where headers is a list of headers leading from
7373
/// the first found point in the past of `start_point` matching a point from `points`
7474
/// up to `start_point`.
75-
pub(super) async fn find_headers_between(
76-
store: &Arc<Mutex<dyn ChainStore<Header>>>,
75+
pub(super) fn find_headers_between(
76+
store: &dyn ChainStore<Header>,
7777
start_point: &Point,
7878
points: &[Point],
7979
) -> Option<(Vec<ClientOp>, Tip)> {
80-
let store = store.lock().await;
8180
let start_header = store.load_header(&hash_point(start_point))?;
8281

82+
if points.contains(start_point) {
83+
return Some((
84+
vec![],
85+
Tip(start_point.clone(), start_header.block_height()),
86+
));
87+
}
88+
8389
// Find the first point that is in the past of start_point
8490
let mut current_header = start_header;
8591
let mut headers = vec![ClientOp::Forward(current_header.clone())];
@@ -101,6 +107,7 @@ pub(super) async fn find_headers_between(
101107
}
102108
}
103109

110+
// FIXME: syncing a new node will get to this point, but the procedure should probably use snapshots.
104111
None // Reached genesis without finding any matching point
105112
}
106113

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
use crate::chain_forward::client_state::find_headers_between;
2+
use crate::consensus::store::{ChainStore, Nonces, StoreError};
3+
use amaru_kernel::{Hash, Header};
4+
use pallas_network::miniprotocols::chainsync::Tip;
5+
use pallas_network::miniprotocols::Point;
6+
use std::{collections::HashMap, fs::File, path::Path, str::FromStr};
7+
8+
impl ChainStore<Header> for HashMap<Hash<32>, Header> {
9+
fn load_header(&self, hash: &Hash<32>) -> Option<Header> {
10+
self.get(hash).cloned()
11+
}
12+
13+
fn store_header(&mut self, hash: &Hash<32>, header: &Header) -> Result<(), StoreError> {
14+
self.insert(*hash, header.clone());
15+
Ok(())
16+
}
17+
18+
fn get_nonces(&self, _header: &Hash<32>) -> Option<Nonces> {
19+
todo!()
20+
}
21+
22+
fn put_nonces(&mut self, _header: &Hash<32>, _nonces: Nonces) -> Result<(), StoreError> {
23+
todo!()
24+
}
25+
}
26+
27+
fn mk_store(path: impl AsRef<Path>) -> HashMap<Hash<32>, Header> {
28+
let f = File::open(path).unwrap();
29+
let json: serde_json::Value = serde_json::from_reader(f).unwrap();
30+
let headers = json
31+
.pointer("/stakePools/chains")
32+
.unwrap()
33+
.as_array()
34+
.unwrap();
35+
36+
let mut store = HashMap::new();
37+
38+
for header in headers {
39+
let hash = header.pointer("/hash").unwrap().as_str().unwrap();
40+
let header = header.pointer("/header").unwrap().as_str().unwrap();
41+
let header = hex::decode(header).unwrap();
42+
store.insert(hash.parse().unwrap(), minicbor::decode(&header).unwrap());
43+
}
44+
45+
store
46+
}
47+
48+
fn hash(s: &str) -> Hash<32> {
49+
Hash::<32>::from_str(s).unwrap()
50+
}
51+
52+
fn hex(s: &str) -> Vec<u8> {
53+
hex::decode(s).unwrap()
54+
}
55+
56+
const CHAIN_41: &str = "tests/data/chain41.json";
57+
58+
const TIP_41: &str = "fcb4a51804f14f3f5b5ad841199b557aed0187280f7855736bdb153b0d202bb6";
59+
const TIP_41_SLOT: u64 = 990;
60+
const TIP_41_HEIGHT: u64 = 47;
61+
62+
const LOST_41: &str = "bd41b102018a21e068d504e64b282512a3b7d5c3883b743aa070ad9244691125";
63+
const LOST_41_SLOT: u64 = 188;
64+
65+
const WINNER_41: &str = "66c90f54f9073cfc03a334f5b15b1617f6bf6fe6c892fad8368e16abe20b0f4f";
66+
const WINNER_41_SLOT: u64 = 187;
67+
const WINNER_41_HEIGHT: u64 = 8;
68+
69+
const BRANCH_41: &str = "64565f22fb23476baaa6f82e0e2d68636ceadabded697099fb376c23226bdf03";
70+
const BRANCH_41_SLOT: u64 = 142;
71+
const BRANCH_41_HEIGHT: u64 = 7;
72+
73+
const ROOT_41_SLOT: u64 = 31;
74+
75+
fn point(slot: u64, hash: &str) -> Point {
76+
Point::Specific(slot, hex(hash))
77+
}
78+
79+
#[test]
80+
fn test_mk_store() {
81+
let store = mk_store(CHAIN_41);
82+
assert_eq!(store.len(), 48);
83+
84+
let mut current = hash(TIP_41);
85+
let mut chain = store.get(&current).unwrap().clone();
86+
87+
assert_eq!(chain.header_body.slot, TIP_41_SLOT);
88+
89+
while chain.header_body.prev_hash.is_some() {
90+
current = chain.header_body.prev_hash.unwrap();
91+
chain = store.get(&current).unwrap().clone();
92+
}
93+
94+
assert_eq!(chain.header_body.slot, ROOT_41_SLOT);
95+
}
96+
97+
#[test]
98+
fn find_headers_between_tip_and_tip() {
99+
let store = mk_store(CHAIN_41);
100+
101+
let tip = point(TIP_41_SLOT, TIP_41);
102+
let points = [point(TIP_41_SLOT, TIP_41)];
103+
104+
let (ops, Tip(p, h)) = find_headers_between(&store, &tip, &points).unwrap();
105+
assert_eq!((ops, p, h), (vec![], tip, 47));
106+
}
107+
108+
#[test]
109+
fn find_headers_between_tip_and_branch() {
110+
let store = mk_store(CHAIN_41);
111+
112+
let tip = point(TIP_41_SLOT, TIP_41);
113+
let points = [point(BRANCH_41_SLOT, BRANCH_41)];
114+
let peer = point(BRANCH_41_SLOT, BRANCH_41);
115+
116+
let (ops, Tip(p, h)) = find_headers_between(&store, &tip, &points).unwrap();
117+
assert_eq!(
118+
(ops.len() as u64, p, h),
119+
(TIP_41_HEIGHT - BRANCH_41_HEIGHT, peer, BRANCH_41_HEIGHT)
120+
);
121+
}
122+
123+
#[test]
124+
fn find_headers_between_tip_and_branches() {
125+
let store = mk_store(CHAIN_41);
126+
127+
let tip = point(TIP_41_SLOT, TIP_41);
128+
// Note that the below scheme does not match the documented behaviour, which shall pick the first from
129+
// the list that is on the same chain. But that doesn't make sense to me at all.
130+
let points = [
131+
point(BRANCH_41_SLOT, BRANCH_41), // this will lose to the (taller) winner
132+
point(LOST_41_SLOT, LOST_41), // this is not on the same chain
133+
point(WINNER_41_SLOT, WINNER_41), // this is the winner after the branch
134+
];
135+
let peer = point(WINNER_41_SLOT, WINNER_41);
136+
137+
let (ops, Tip(p, h)) = find_headers_between(&store, &tip, &points).unwrap();
138+
assert_eq!(
139+
(ops.len() as u64, p, h),
140+
(TIP_41_HEIGHT - WINNER_41_HEIGHT, peer, WINNER_41_HEIGHT)
141+
);
142+
}
143+
144+
#[test]
145+
fn find_headers_between_tip_and_lost() {
146+
let store = mk_store(CHAIN_41);
147+
148+
let tip = point(TIP_41_SLOT, TIP_41);
149+
let points = [point(LOST_41_SLOT, LOST_41)];
150+
151+
let result = find_headers_between(&store, &tip, &points);
152+
assert!(result.is_none(), "{result:?}");
153+
}

0 commit comments

Comments
 (0)