2024-05-08 14:36:52 +02:00
|
|
|
#![cfg(feature = "miniscript")]
|
|
|
|
|
2024-04-05 16:43:13 +08:00
|
|
|
use std::ops::{Bound, RangeBounds};
|
|
|
|
|
2024-01-10 17:42:03 +08:00
|
|
|
use bdk_chain::{
|
|
|
|
local_chain::{
|
2024-01-11 00:05:04 +08:00
|
|
|
AlterCheckPointError, ApplyHeaderError, CannotConnectError, ChangeSet, CheckPoint,
|
2024-04-17 10:02:12 +08:00
|
|
|
LocalChain, MissingGenesisError,
|
2024-01-10 17:42:03 +08:00
|
|
|
},
|
|
|
|
BlockId,
|
2023-10-12 16:55:32 +08:00
|
|
|
};
|
2024-01-11 00:05:04 +08:00
|
|
|
use bitcoin::{block::Header, hashes::Hash, BlockHash};
|
2024-04-05 16:43:13 +08:00
|
|
|
use proptest::prelude::*;
|
2023-04-20 15:29:20 +08:00
|
|
|
|
|
|
|
#[macro_use]
|
|
|
|
mod common;
|
|
|
|
|
2023-07-19 17:42:52 +08:00
|
|
|
#[derive(Debug)]
|
|
|
|
struct TestLocalChain<'a> {
|
|
|
|
name: &'static str,
|
|
|
|
chain: LocalChain,
|
2024-04-17 10:02:12 +08:00
|
|
|
update: CheckPoint,
|
2023-07-19 17:42:52 +08:00
|
|
|
exp: ExpectedResult<'a>,
|
2023-04-20 15:29:20 +08:00
|
|
|
}
|
|
|
|
|
2023-07-19 17:42:52 +08:00
|
|
|
#[derive(Debug, PartialEq)]
|
|
|
|
enum ExpectedResult<'a> {
|
|
|
|
Ok {
|
|
|
|
changeset: &'a [(u32, Option<BlockHash>)],
|
|
|
|
init_changeset: &'a [(u32, Option<BlockHash>)],
|
|
|
|
},
|
|
|
|
Err(CannotConnectError),
|
2023-04-20 15:29:20 +08:00
|
|
|
}
|
|
|
|
|
2023-07-19 17:42:52 +08:00
|
|
|
impl<'a> TestLocalChain<'a> {
|
|
|
|
fn run(mut self) {
|
|
|
|
println!("[TestLocalChain] test: {}", self.name);
|
|
|
|
let got_changeset = match self.chain.apply_update(self.update) {
|
|
|
|
Ok(changeset) => changeset,
|
|
|
|
Err(got_err) => {
|
|
|
|
assert_eq!(
|
|
|
|
ExpectedResult::Err(got_err),
|
|
|
|
self.exp,
|
|
|
|
"{}: unexpected error",
|
|
|
|
self.name
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
};
|
2023-04-20 15:29:20 +08:00
|
|
|
|
2023-07-19 17:42:52 +08:00
|
|
|
match self.exp {
|
|
|
|
ExpectedResult::Ok {
|
|
|
|
changeset,
|
|
|
|
init_changeset,
|
|
|
|
} => {
|
|
|
|
assert_eq!(
|
|
|
|
got_changeset,
|
|
|
|
changeset.iter().cloned().collect(),
|
|
|
|
"{}: unexpected changeset",
|
|
|
|
self.name
|
|
|
|
);
|
|
|
|
assert_eq!(
|
|
|
|
self.chain.initial_changeset(),
|
|
|
|
init_changeset.iter().cloned().collect(),
|
|
|
|
"{}: unexpected initial changeset",
|
|
|
|
self.name
|
|
|
|
);
|
|
|
|
}
|
|
|
|
ExpectedResult::Err(err) => panic!(
|
|
|
|
"{}: expected error ({}), got non-error result: {:?}",
|
|
|
|
self.name, err, got_changeset
|
|
|
|
),
|
|
|
|
}
|
|
|
|
}
|
2023-04-20 15:29:20 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2023-07-19 17:42:52 +08:00
|
|
|
fn update_local_chain() {
|
|
|
|
[
|
|
|
|
TestLocalChain {
|
|
|
|
name: "add first tip",
|
2023-10-12 16:55:32 +08:00
|
|
|
chain: local_chain![(0, h!("A"))],
|
2023-07-19 17:42:52 +08:00
|
|
|
update: chain_update![(0, h!("A"))],
|
|
|
|
exp: ExpectedResult::Ok {
|
2023-10-12 16:55:32 +08:00
|
|
|
changeset: &[],
|
2023-07-19 17:42:52 +08:00
|
|
|
init_changeset: &[(0, Some(h!("A")))],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
TestLocalChain {
|
|
|
|
name: "add second tip",
|
|
|
|
chain: local_chain![(0, h!("A"))],
|
|
|
|
update: chain_update![(0, h!("A")), (1, h!("B"))],
|
|
|
|
exp: ExpectedResult::Ok {
|
|
|
|
changeset: &[(1, Some(h!("B")))],
|
|
|
|
init_changeset: &[(0, Some(h!("A"))), (1, Some(h!("B")))],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
TestLocalChain {
|
|
|
|
name: "two disjoint chains cannot merge",
|
2023-10-12 16:55:32 +08:00
|
|
|
chain: local_chain![(0, h!("_")), (1, h!("A"))],
|
|
|
|
update: chain_update![(0, h!("_")), (2, h!("B"))],
|
2023-07-19 17:42:52 +08:00
|
|
|
exp: ExpectedResult::Err(CannotConnectError {
|
2023-10-12 16:55:32 +08:00
|
|
|
try_include_height: 1,
|
2023-07-19 17:42:52 +08:00
|
|
|
}),
|
|
|
|
},
|
|
|
|
TestLocalChain {
|
|
|
|
name: "two disjoint chains cannot merge (existing chain longer)",
|
2023-10-12 16:55:32 +08:00
|
|
|
chain: local_chain![(0, h!("_")), (2, h!("A"))],
|
|
|
|
update: chain_update![(0, h!("_")), (1, h!("B"))],
|
2023-07-19 17:42:52 +08:00
|
|
|
exp: ExpectedResult::Err(CannotConnectError {
|
2023-10-12 16:55:32 +08:00
|
|
|
try_include_height: 2,
|
2023-07-19 17:42:52 +08:00
|
|
|
}),
|
|
|
|
},
|
|
|
|
TestLocalChain {
|
|
|
|
name: "duplicate chains should merge",
|
|
|
|
chain: local_chain![(0, h!("A"))],
|
|
|
|
update: chain_update![(0, h!("A"))],
|
|
|
|
exp: ExpectedResult::Ok {
|
|
|
|
changeset: &[],
|
|
|
|
init_changeset: &[(0, Some(h!("A")))],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
// Introduce an older checkpoint (B)
|
|
|
|
// | 0 | 1 | 2 | 3
|
2023-10-12 16:55:32 +08:00
|
|
|
// chain | _ C D
|
|
|
|
// update | _ B C
|
2023-07-19 17:42:52 +08:00
|
|
|
TestLocalChain {
|
|
|
|
name: "can introduce older checkpoint",
|
2023-10-12 16:55:32 +08:00
|
|
|
chain: local_chain![(0, h!("_")), (2, h!("C")), (3, h!("D"))],
|
|
|
|
update: chain_update![(0, h!("_")), (1, h!("B")), (2, h!("C"))],
|
2023-07-19 17:42:52 +08:00
|
|
|
exp: ExpectedResult::Ok {
|
|
|
|
changeset: &[(1, Some(h!("B")))],
|
2023-10-12 16:55:32 +08:00
|
|
|
init_changeset: &[(0, Some(h!("_"))), (1, Some(h!("B"))), (2, Some(h!("C"))), (3, Some(h!("D")))],
|
2023-07-19 17:42:52 +08:00
|
|
|
},
|
|
|
|
},
|
|
|
|
// Introduce an older checkpoint (A) that is not directly behind PoA
|
2023-10-12 16:55:32 +08:00
|
|
|
// | 0 | 2 | 3 | 4
|
|
|
|
// chain | _ B C
|
|
|
|
// update | _ A C
|
2023-07-19 17:42:52 +08:00
|
|
|
TestLocalChain {
|
|
|
|
name: "can introduce older checkpoint 2",
|
2023-10-12 16:55:32 +08:00
|
|
|
chain: local_chain![(0, h!("_")), (3, h!("B")), (4, h!("C"))],
|
|
|
|
update: chain_update![(0, h!("_")), (2, h!("A")), (4, h!("C"))],
|
2023-07-19 17:42:52 +08:00
|
|
|
exp: ExpectedResult::Ok {
|
|
|
|
changeset: &[(2, Some(h!("A")))],
|
2023-10-12 16:55:32 +08:00
|
|
|
init_changeset: &[(0, Some(h!("_"))), (2, Some(h!("A"))), (3, Some(h!("B"))), (4, Some(h!("C")))],
|
2023-07-19 17:42:52 +08:00
|
|
|
}
|
|
|
|
},
|
|
|
|
// Introduce an older checkpoint (B) that is not the oldest checkpoint
|
2023-10-12 16:55:32 +08:00
|
|
|
// | 0 | 1 | 2 | 3
|
|
|
|
// chain | _ A C
|
|
|
|
// update | _ B C
|
2023-07-19 17:42:52 +08:00
|
|
|
TestLocalChain {
|
|
|
|
name: "can introduce older checkpoint 3",
|
2023-10-12 16:55:32 +08:00
|
|
|
chain: local_chain![(0, h!("_")), (1, h!("A")), (3, h!("C"))],
|
|
|
|
update: chain_update![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
|
2023-07-19 17:42:52 +08:00
|
|
|
exp: ExpectedResult::Ok {
|
|
|
|
changeset: &[(2, Some(h!("B")))],
|
2023-10-12 16:55:32 +08:00
|
|
|
init_changeset: &[(0, Some(h!("_"))), (1, Some(h!("A"))), (2, Some(h!("B"))), (3, Some(h!("C")))],
|
2023-07-19 17:42:52 +08:00
|
|
|
}
|
|
|
|
},
|
|
|
|
// Introduce two older checkpoints below the PoA
|
2023-10-12 16:55:32 +08:00
|
|
|
// | 0 | 1 | 2 | 3
|
|
|
|
// chain | _ C
|
|
|
|
// update | _ A B C
|
2023-07-19 17:42:52 +08:00
|
|
|
TestLocalChain {
|
|
|
|
name: "introduce two older checkpoints below PoA",
|
2023-10-12 16:55:32 +08:00
|
|
|
chain: local_chain![(0, h!("_")), (3, h!("C"))],
|
|
|
|
update: chain_update![(0, h!("_")), (1, h!("A")), (2, h!("B")), (3, h!("C"))],
|
2023-07-19 17:42:52 +08:00
|
|
|
exp: ExpectedResult::Ok {
|
|
|
|
changeset: &[(1, Some(h!("A"))), (2, Some(h!("B")))],
|
2023-10-12 16:55:32 +08:00
|
|
|
init_changeset: &[(0, Some(h!("_"))), (1, Some(h!("A"))), (2, Some(h!("B"))), (3, Some(h!("C")))],
|
2023-07-19 17:42:52 +08:00
|
|
|
},
|
|
|
|
},
|
|
|
|
TestLocalChain {
|
|
|
|
name: "fix blockhash before agreement point",
|
|
|
|
chain: local_chain![(0, h!("im-wrong")), (1, h!("we-agree"))],
|
|
|
|
update: chain_update![(0, h!("fix")), (1, h!("we-agree"))],
|
|
|
|
exp: ExpectedResult::Ok {
|
|
|
|
changeset: &[(0, Some(h!("fix")))],
|
|
|
|
init_changeset: &[(0, Some(h!("fix"))), (1, Some(h!("we-agree")))],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
// B and C are in both chain and update
|
|
|
|
// | 0 | 1 | 2 | 3 | 4
|
2023-10-12 16:55:32 +08:00
|
|
|
// chain | _ B C
|
|
|
|
// update | _ A B C D
|
2023-07-19 17:42:52 +08:00
|
|
|
// This should succeed with the point of agreement being C and A should be added in addition.
|
|
|
|
TestLocalChain {
|
|
|
|
name: "two points of agreement",
|
2023-10-12 16:55:32 +08:00
|
|
|
chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
|
|
|
|
update: chain_update![(0, h!("_")), (1, h!("A")), (2, h!("B")), (3, h!("C")), (4, h!("D"))],
|
2023-07-19 17:42:52 +08:00
|
|
|
exp: ExpectedResult::Ok {
|
2023-10-12 16:55:32 +08:00
|
|
|
changeset: &[(1, Some(h!("A"))), (4, Some(h!("D")))],
|
2023-07-19 17:42:52 +08:00
|
|
|
init_changeset: &[
|
2023-10-12 16:55:32 +08:00
|
|
|
(0, Some(h!("_"))),
|
|
|
|
(1, Some(h!("A"))),
|
|
|
|
(2, Some(h!("B"))),
|
|
|
|
(3, Some(h!("C"))),
|
|
|
|
(4, Some(h!("D"))),
|
2023-07-19 17:42:52 +08:00
|
|
|
],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
// Update and chain does not connect:
|
|
|
|
// | 0 | 1 | 2 | 3 | 4
|
2023-10-12 16:55:32 +08:00
|
|
|
// chain | _ B C
|
|
|
|
// update | _ A B D
|
2023-07-19 17:42:52 +08:00
|
|
|
// This should fail as we cannot figure out whether C & D are on the same chain
|
|
|
|
TestLocalChain {
|
|
|
|
name: "update and chain does not connect",
|
2023-10-12 16:55:32 +08:00
|
|
|
chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
|
|
|
|
update: chain_update![(0, h!("_")), (1, h!("A")), (2, h!("B")), (4, h!("D"))],
|
2023-07-19 17:42:52 +08:00
|
|
|
exp: ExpectedResult::Err(CannotConnectError {
|
2023-10-12 16:55:32 +08:00
|
|
|
try_include_height: 3,
|
2023-07-19 17:42:52 +08:00
|
|
|
}),
|
|
|
|
},
|
|
|
|
// Transient invalidation:
|
|
|
|
// | 0 | 1 | 2 | 3 | 4 | 5
|
2023-10-12 16:55:32 +08:00
|
|
|
// chain | _ B C E
|
|
|
|
// update | _ B' C' D
|
2023-07-19 17:42:52 +08:00
|
|
|
// This should succeed and invalidate B,C and E with point of agreement being A.
|
|
|
|
TestLocalChain {
|
|
|
|
name: "transitive invalidation applies to checkpoints higher than invalidation",
|
2023-10-12 16:55:32 +08:00
|
|
|
chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C")), (5, h!("E"))],
|
|
|
|
update: chain_update![(0, h!("_")), (2, h!("B'")), (3, h!("C'")), (4, h!("D"))],
|
2023-07-19 17:42:52 +08:00
|
|
|
exp: ExpectedResult::Ok {
|
|
|
|
changeset: &[
|
|
|
|
(2, Some(h!("B'"))),
|
|
|
|
(3, Some(h!("C'"))),
|
|
|
|
(4, Some(h!("D"))),
|
|
|
|
(5, None),
|
|
|
|
],
|
|
|
|
init_changeset: &[
|
2023-10-12 16:55:32 +08:00
|
|
|
(0, Some(h!("_"))),
|
2023-07-19 17:42:52 +08:00
|
|
|
(2, Some(h!("B'"))),
|
|
|
|
(3, Some(h!("C'"))),
|
|
|
|
(4, Some(h!("D"))),
|
|
|
|
],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
// Transient invalidation:
|
|
|
|
// | 0 | 1 | 2 | 3 | 4
|
2023-10-12 16:55:32 +08:00
|
|
|
// chain | _ B C E
|
|
|
|
// update | _ B' C' D
|
2023-07-19 17:42:52 +08:00
|
|
|
// This should succeed and invalidate B, C and E with no point of agreement
|
|
|
|
TestLocalChain {
|
|
|
|
name: "transitive invalidation applies to checkpoints higher than invalidation no point of agreement",
|
2023-10-12 16:55:32 +08:00
|
|
|
chain: local_chain![(0, h!("_")), (1, h!("B")), (2, h!("C")), (4, h!("E"))],
|
|
|
|
update: chain_update![(0, h!("_")), (1, h!("B'")), (2, h!("C'")), (3, h!("D"))],
|
2023-07-19 17:42:52 +08:00
|
|
|
exp: ExpectedResult::Ok {
|
|
|
|
changeset: &[
|
|
|
|
(1, Some(h!("B'"))),
|
|
|
|
(2, Some(h!("C'"))),
|
|
|
|
(3, Some(h!("D"))),
|
|
|
|
(4, None)
|
|
|
|
],
|
|
|
|
init_changeset: &[
|
2023-10-12 16:55:32 +08:00
|
|
|
(0, Some(h!("_"))),
|
2023-07-19 17:42:52 +08:00
|
|
|
(1, Some(h!("B'"))),
|
|
|
|
(2, Some(h!("C'"))),
|
|
|
|
(3, Some(h!("D"))),
|
|
|
|
],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
// Transient invalidation:
|
2023-10-12 16:55:32 +08:00
|
|
|
// | 0 | 1 | 2 | 3 | 4 | 5
|
|
|
|
// chain | _ A B C E
|
|
|
|
// update | _ B' C' D
|
2023-07-19 17:42:52 +08:00
|
|
|
// This should fail since although it tells us that B and C are invalid it doesn't tell us whether
|
|
|
|
// A was invalid.
|
|
|
|
TestLocalChain {
|
|
|
|
name: "invalidation but no connection",
|
2023-10-12 16:55:32 +08:00
|
|
|
chain: local_chain![(0, h!("_")), (1, h!("A")), (2, h!("B")), (3, h!("C")), (5, h!("E"))],
|
|
|
|
update: chain_update![(0, h!("_")), (2, h!("B'")), (3, h!("C'")), (4, h!("D"))],
|
|
|
|
exp: ExpectedResult::Err(CannotConnectError { try_include_height: 1 }),
|
2023-07-19 17:42:52 +08:00
|
|
|
},
|
|
|
|
// Introduce blocks between two points of agreement
|
|
|
|
// | 0 | 1 | 2 | 3 | 4 | 5
|
|
|
|
// chain | A B D E
|
|
|
|
// update | A C E F
|
|
|
|
TestLocalChain {
|
|
|
|
name: "introduce blocks between two points of agreement",
|
|
|
|
chain: local_chain![(0, h!("A")), (1, h!("B")), (3, h!("D")), (4, h!("E"))],
|
|
|
|
update: chain_update![(0, h!("A")), (2, h!("C")), (4, h!("E")), (5, h!("F"))],
|
|
|
|
exp: ExpectedResult::Ok {
|
|
|
|
changeset: &[
|
|
|
|
(2, Some(h!("C"))),
|
|
|
|
(5, Some(h!("F"))),
|
|
|
|
],
|
|
|
|
init_changeset: &[
|
|
|
|
(0, Some(h!("A"))),
|
|
|
|
(1, Some(h!("B"))),
|
|
|
|
(2, Some(h!("C"))),
|
|
|
|
(3, Some(h!("D"))),
|
|
|
|
(4, Some(h!("E"))),
|
|
|
|
(5, Some(h!("F"))),
|
|
|
|
],
|
|
|
|
},
|
|
|
|
},
|
2023-12-29 18:23:42 +08:00
|
|
|
// Allow update that is shorter than original chain
|
|
|
|
// | 0 | 1 | 2 | 3 | 4 | 5
|
|
|
|
// chain | A C D E F
|
|
|
|
// update | A C D'
|
|
|
|
TestLocalChain {
|
|
|
|
name: "allow update that is shorter than original chain",
|
|
|
|
chain: local_chain![(0, h!("_")), (2, h!("C")), (3, h!("D")), (4, h!("E")), (5, h!("F"))],
|
|
|
|
update: chain_update![(0, h!("_")), (2, h!("C")), (3, h!("D'"))],
|
|
|
|
exp: ExpectedResult::Ok {
|
|
|
|
changeset: &[
|
|
|
|
(3, Some(h!("D'"))),
|
|
|
|
(4, None),
|
|
|
|
(5, None),
|
|
|
|
],
|
|
|
|
init_changeset: &[
|
|
|
|
(0, Some(h!("_"))),
|
|
|
|
(2, Some(h!("C"))),
|
|
|
|
(3, Some(h!("D'"))),
|
|
|
|
],
|
|
|
|
},
|
|
|
|
},
|
2023-07-19 17:42:52 +08:00
|
|
|
]
|
|
|
|
.into_iter()
|
|
|
|
.for_each(TestLocalChain::run);
|
2023-04-20 15:29:20 +08:00
|
|
|
}
|
2023-05-03 15:01:39 +08:00
|
|
|
|
|
|
|
#[test]
|
2023-07-19 17:42:52 +08:00
|
|
|
fn local_chain_insert_block() {
|
2023-05-03 15:01:39 +08:00
|
|
|
struct TestCase {
|
|
|
|
original: LocalChain,
|
|
|
|
insert: (u32, BlockHash),
|
2023-10-12 16:55:32 +08:00
|
|
|
expected_result: Result<ChangeSet, AlterCheckPointError>,
|
2023-05-03 15:01:39 +08:00
|
|
|
expected_final: LocalChain,
|
|
|
|
}
|
|
|
|
|
|
|
|
let test_cases = [
|
|
|
|
TestCase {
|
2023-10-12 16:55:32 +08:00
|
|
|
original: local_chain![(0, h!("_"))],
|
2023-05-03 15:01:39 +08:00
|
|
|
insert: (5, h!("block5")),
|
|
|
|
expected_result: Ok([(5, Some(h!("block5")))].into()),
|
2023-10-12 16:55:32 +08:00
|
|
|
expected_final: local_chain![(0, h!("_")), (5, h!("block5"))],
|
2023-05-03 15:01:39 +08:00
|
|
|
},
|
|
|
|
TestCase {
|
2023-10-12 16:55:32 +08:00
|
|
|
original: local_chain![(0, h!("_")), (3, h!("A"))],
|
2023-05-03 15:01:39 +08:00
|
|
|
insert: (4, h!("B")),
|
|
|
|
expected_result: Ok([(4, Some(h!("B")))].into()),
|
2023-10-12 16:55:32 +08:00
|
|
|
expected_final: local_chain![(0, h!("_")), (3, h!("A")), (4, h!("B"))],
|
2023-05-03 15:01:39 +08:00
|
|
|
},
|
|
|
|
TestCase {
|
2023-10-12 16:55:32 +08:00
|
|
|
original: local_chain![(0, h!("_")), (4, h!("B"))],
|
2023-05-03 15:01:39 +08:00
|
|
|
insert: (3, h!("A")),
|
|
|
|
expected_result: Ok([(3, Some(h!("A")))].into()),
|
2023-10-12 16:55:32 +08:00
|
|
|
expected_final: local_chain![(0, h!("_")), (3, h!("A")), (4, h!("B"))],
|
2023-05-03 15:01:39 +08:00
|
|
|
},
|
|
|
|
TestCase {
|
2023-10-12 16:55:32 +08:00
|
|
|
original: local_chain![(0, h!("_")), (2, h!("K"))],
|
2023-05-03 15:01:39 +08:00
|
|
|
insert: (2, h!("K")),
|
|
|
|
expected_result: Ok([].into()),
|
2023-10-12 16:55:32 +08:00
|
|
|
expected_final: local_chain![(0, h!("_")), (2, h!("K"))],
|
2023-05-03 15:01:39 +08:00
|
|
|
},
|
|
|
|
TestCase {
|
2023-10-12 16:55:32 +08:00
|
|
|
original: local_chain![(0, h!("_")), (2, h!("K"))],
|
2023-05-03 15:01:39 +08:00
|
|
|
insert: (2, h!("J")),
|
2023-10-12 16:55:32 +08:00
|
|
|
expected_result: Err(AlterCheckPointError {
|
2023-05-03 15:01:39 +08:00
|
|
|
height: 2,
|
|
|
|
original_hash: h!("K"),
|
2023-10-12 16:55:32 +08:00
|
|
|
update_hash: Some(h!("J")),
|
2023-05-03 15:01:39 +08:00
|
|
|
}),
|
2023-10-12 16:55:32 +08:00
|
|
|
expected_final: local_chain![(0, h!("_")), (2, h!("K"))],
|
2023-05-03 15:01:39 +08:00
|
|
|
},
|
|
|
|
];
|
|
|
|
|
|
|
|
for (i, t) in test_cases.into_iter().enumerate() {
|
|
|
|
let mut chain = t.original;
|
|
|
|
assert_eq!(
|
|
|
|
chain.insert_block(t.insert.into()),
|
|
|
|
t.expected_result,
|
|
|
|
"[{}] unexpected result when inserting block",
|
|
|
|
i,
|
|
|
|
);
|
|
|
|
assert_eq!(chain, t.expected_final, "[{}] unexpected final chain", i,);
|
|
|
|
}
|
|
|
|
}
|
2024-01-15 17:48:36 +08:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn local_chain_disconnect_from() {
|
|
|
|
struct TestCase {
|
|
|
|
name: &'static str,
|
|
|
|
original: LocalChain,
|
|
|
|
disconnect_from: (u32, BlockHash),
|
|
|
|
exp_result: Result<ChangeSet, MissingGenesisError>,
|
|
|
|
exp_final: LocalChain,
|
|
|
|
}
|
|
|
|
|
|
|
|
let test_cases = [
|
|
|
|
TestCase {
|
|
|
|
name: "try_replace_genesis_should_fail",
|
|
|
|
original: local_chain![(0, h!("_"))],
|
|
|
|
disconnect_from: (0, h!("_")),
|
|
|
|
exp_result: Err(MissingGenesisError),
|
|
|
|
exp_final: local_chain![(0, h!("_"))],
|
|
|
|
},
|
|
|
|
TestCase {
|
|
|
|
name: "try_replace_genesis_should_fail_2",
|
|
|
|
original: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
|
|
|
|
disconnect_from: (0, h!("_")),
|
|
|
|
exp_result: Err(MissingGenesisError),
|
|
|
|
exp_final: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
|
|
|
|
},
|
|
|
|
TestCase {
|
|
|
|
name: "from_does_not_exist",
|
|
|
|
original: local_chain![(0, h!("_")), (3, h!("C"))],
|
|
|
|
disconnect_from: (2, h!("B")),
|
|
|
|
exp_result: Ok(ChangeSet::default()),
|
|
|
|
exp_final: local_chain![(0, h!("_")), (3, h!("C"))],
|
|
|
|
},
|
|
|
|
TestCase {
|
|
|
|
name: "from_has_different_blockhash",
|
|
|
|
original: local_chain![(0, h!("_")), (2, h!("B"))],
|
|
|
|
disconnect_from: (2, h!("not_B")),
|
|
|
|
exp_result: Ok(ChangeSet::default()),
|
|
|
|
exp_final: local_chain![(0, h!("_")), (2, h!("B"))],
|
|
|
|
},
|
|
|
|
TestCase {
|
|
|
|
name: "disconnect_one",
|
|
|
|
original: local_chain![(0, h!("_")), (2, h!("B"))],
|
|
|
|
disconnect_from: (2, h!("B")),
|
|
|
|
exp_result: Ok(ChangeSet::from_iter([(2, None)])),
|
|
|
|
exp_final: local_chain![(0, h!("_"))],
|
|
|
|
},
|
|
|
|
TestCase {
|
|
|
|
name: "disconnect_three",
|
|
|
|
original: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C")), (4, h!("D"))],
|
|
|
|
disconnect_from: (2, h!("B")),
|
|
|
|
exp_result: Ok(ChangeSet::from_iter([(2, None), (3, None), (4, None)])),
|
|
|
|
exp_final: local_chain![(0, h!("_"))],
|
|
|
|
},
|
|
|
|
];
|
|
|
|
|
|
|
|
for (i, t) in test_cases.into_iter().enumerate() {
|
|
|
|
println!("Case {}: {}", i, t.name);
|
|
|
|
|
|
|
|
let mut chain = t.original;
|
|
|
|
let result = chain.disconnect_from(t.disconnect_from.into());
|
|
|
|
assert_eq!(
|
|
|
|
result, t.exp_result,
|
|
|
|
"[{}:{}] unexpected changeset result",
|
|
|
|
i, t.name
|
|
|
|
);
|
|
|
|
assert_eq!(
|
|
|
|
chain, t.exp_final,
|
|
|
|
"[{}:{}] unexpected final chain",
|
|
|
|
i, t.name
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2024-01-10 17:42:03 +08:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn checkpoint_from_block_ids() {
|
|
|
|
struct TestCase<'a> {
|
|
|
|
name: &'a str,
|
|
|
|
blocks: &'a [(u32, BlockHash)],
|
|
|
|
exp_result: Result<(), Option<(u32, BlockHash)>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
let test_cases = [
|
|
|
|
TestCase {
|
|
|
|
name: "in_order",
|
|
|
|
blocks: &[(0, h!("A")), (1, h!("B")), (3, h!("D"))],
|
|
|
|
exp_result: Ok(()),
|
|
|
|
},
|
|
|
|
TestCase {
|
|
|
|
name: "with_duplicates",
|
|
|
|
blocks: &[(1, h!("B")), (2, h!("C")), (2, h!("C'"))],
|
|
|
|
exp_result: Err(Some((2, h!("C")))),
|
|
|
|
},
|
|
|
|
TestCase {
|
|
|
|
name: "not_in_order",
|
|
|
|
blocks: &[(1, h!("B")), (3, h!("D")), (2, h!("C"))],
|
|
|
|
exp_result: Err(Some((3, h!("D")))),
|
|
|
|
},
|
|
|
|
TestCase {
|
|
|
|
name: "empty",
|
|
|
|
blocks: &[],
|
|
|
|
exp_result: Err(None),
|
|
|
|
},
|
|
|
|
TestCase {
|
|
|
|
name: "single",
|
|
|
|
blocks: &[(21, h!("million"))],
|
|
|
|
exp_result: Ok(()),
|
|
|
|
},
|
|
|
|
];
|
|
|
|
|
|
|
|
for (i, t) in test_cases.into_iter().enumerate() {
|
|
|
|
println!("running test case {}: '{}'", i, t.name);
|
|
|
|
let result = CheckPoint::from_block_ids(
|
|
|
|
t.blocks
|
|
|
|
.iter()
|
|
|
|
.map(|&(height, hash)| BlockId { height, hash }),
|
|
|
|
);
|
|
|
|
match t.exp_result {
|
|
|
|
Ok(_) => {
|
|
|
|
assert!(result.is_ok(), "[{}:{}] should be Ok", i, t.name);
|
|
|
|
let result_vec = {
|
|
|
|
let mut v = result
|
|
|
|
.unwrap()
|
|
|
|
.into_iter()
|
|
|
|
.map(|cp| (cp.height(), cp.hash()))
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
v.reverse();
|
|
|
|
v
|
|
|
|
};
|
|
|
|
assert_eq!(
|
|
|
|
&result_vec, t.blocks,
|
|
|
|
"[{}:{}] not equal to original block ids",
|
|
|
|
i, t.name
|
|
|
|
);
|
|
|
|
}
|
|
|
|
Err(exp_last) => {
|
|
|
|
assert!(result.is_err(), "[{}:{}] should be Err", i, t.name);
|
|
|
|
let err = result.unwrap_err();
|
|
|
|
assert_eq!(
|
|
|
|
err.as_ref()
|
|
|
|
.map(|last_cp| (last_cp.height(), last_cp.hash())),
|
|
|
|
exp_last,
|
|
|
|
"[{}:{}] error's last cp height should be {:?}, got {:?}",
|
|
|
|
i,
|
|
|
|
t.name,
|
|
|
|
exp_last,
|
|
|
|
err
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-01-11 00:05:04 +08:00
|
|
|
|
2024-03-06 13:04:12 +08:00
|
|
|
#[test]
|
|
|
|
fn checkpoint_query() {
|
|
|
|
struct TestCase {
|
|
|
|
chain: LocalChain,
|
|
|
|
/// The heights we want to call [`CheckPoint::query`] with, represented as an inclusive
|
|
|
|
/// range.
|
|
|
|
///
|
|
|
|
/// If a [`CheckPoint`] exists at that height, we expect [`CheckPoint::query`] to return
|
|
|
|
/// it. If not, [`CheckPoint::query`] should return `None`.
|
|
|
|
query_range: (u32, u32),
|
|
|
|
}
|
|
|
|
|
|
|
|
let test_cases = [
|
|
|
|
TestCase {
|
|
|
|
chain: local_chain![(0, h!("_")), (1, h!("A"))],
|
|
|
|
query_range: (0, 2),
|
|
|
|
},
|
|
|
|
TestCase {
|
|
|
|
chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
|
|
|
|
query_range: (0, 3),
|
|
|
|
},
|
|
|
|
];
|
|
|
|
|
|
|
|
for t in test_cases.into_iter() {
|
|
|
|
let tip = t.chain.tip();
|
|
|
|
for h in t.query_range.0..=t.query_range.1 {
|
|
|
|
let query_result = tip.get(h);
|
|
|
|
|
|
|
|
// perform an exhausitive search for the checkpoint at height `h`
|
|
|
|
let exp_hash = t
|
|
|
|
.chain
|
|
|
|
.iter_checkpoints()
|
|
|
|
.find(|cp| cp.height() == h)
|
|
|
|
.map(|cp| cp.hash());
|
|
|
|
|
|
|
|
match query_result {
|
|
|
|
Some(cp) => {
|
|
|
|
assert_eq!(Some(cp.hash()), exp_hash);
|
|
|
|
assert_eq!(cp.height(), h);
|
|
|
|
}
|
|
|
|
None => assert!(exp_hash.is_none()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-15 14:37:11 +08:00
|
|
|
#[test]
|
|
|
|
fn checkpoint_insert() {
|
|
|
|
struct TestCase<'a> {
|
|
|
|
/// The name of the test.
|
|
|
|
name: &'a str,
|
|
|
|
/// The original checkpoint chain to call [`CheckPoint::insert`] on.
|
|
|
|
chain: &'a [(u32, BlockHash)],
|
|
|
|
/// The `block_id` to insert.
|
|
|
|
to_insert: (u32, BlockHash),
|
|
|
|
/// The expected final checkpoint chain after calling [`CheckPoint::insert`].
|
|
|
|
exp_final_chain: &'a [(u32, BlockHash)],
|
|
|
|
}
|
|
|
|
|
|
|
|
let test_cases = [
|
|
|
|
TestCase {
|
|
|
|
name: "insert_above_tip",
|
|
|
|
chain: &[(1, h!("a")), (2, h!("b"))],
|
|
|
|
to_insert: (4, h!("d")),
|
|
|
|
exp_final_chain: &[(1, h!("a")), (2, h!("b")), (4, h!("d"))],
|
|
|
|
},
|
|
|
|
TestCase {
|
|
|
|
name: "insert_already_exists_expect_no_change",
|
|
|
|
chain: &[(1, h!("a")), (2, h!("b")), (3, h!("c"))],
|
|
|
|
to_insert: (2, h!("b")),
|
|
|
|
exp_final_chain: &[(1, h!("a")), (2, h!("b")), (3, h!("c"))],
|
|
|
|
},
|
|
|
|
TestCase {
|
|
|
|
name: "insert_in_middle",
|
|
|
|
chain: &[(2, h!("b")), (4, h!("d")), (5, h!("e"))],
|
|
|
|
to_insert: (3, h!("c")),
|
|
|
|
exp_final_chain: &[(2, h!("b")), (3, h!("c")), (4, h!("d")), (5, h!("e"))],
|
|
|
|
},
|
|
|
|
TestCase {
|
|
|
|
name: "replace_one",
|
|
|
|
chain: &[(3, h!("c")), (4, h!("d")), (5, h!("e"))],
|
|
|
|
to_insert: (5, h!("E")),
|
|
|
|
exp_final_chain: &[(3, h!("c")), (4, h!("d")), (5, h!("E"))],
|
|
|
|
},
|
|
|
|
TestCase {
|
|
|
|
name: "insert_conflict_should_evict",
|
|
|
|
chain: &[(3, h!("c")), (4, h!("d")), (5, h!("e")), (6, h!("f"))],
|
|
|
|
to_insert: (4, h!("D")),
|
|
|
|
exp_final_chain: &[(3, h!("c")), (4, h!("D"))],
|
|
|
|
},
|
|
|
|
];
|
|
|
|
|
|
|
|
fn genesis_block() -> impl Iterator<Item = BlockId> {
|
|
|
|
core::iter::once((0, h!("_"))).map(BlockId::from)
|
|
|
|
}
|
|
|
|
|
|
|
|
for (i, t) in test_cases.into_iter().enumerate() {
|
|
|
|
println!("Running [{}] '{}'", i, t.name);
|
|
|
|
|
|
|
|
let chain = CheckPoint::from_block_ids(
|
|
|
|
genesis_block().chain(t.chain.iter().copied().map(BlockId::from)),
|
|
|
|
)
|
|
|
|
.expect("test formed incorrectly, must construct checkpoint chain");
|
|
|
|
|
|
|
|
let exp_final_chain = CheckPoint::from_block_ids(
|
|
|
|
genesis_block().chain(t.exp_final_chain.iter().copied().map(BlockId::from)),
|
|
|
|
)
|
|
|
|
.expect("test formed incorrectly, must construct checkpoint chain");
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
chain.insert(t.to_insert.into()),
|
|
|
|
exp_final_chain,
|
|
|
|
"unexpected final chain"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-11 00:05:04 +08:00
|
|
|
#[test]
|
|
|
|
fn local_chain_apply_header_connected_to() {
|
|
|
|
fn header_from_prev_blockhash(prev_blockhash: BlockHash) -> Header {
|
|
|
|
Header {
|
|
|
|
version: bitcoin::block::Version::default(),
|
|
|
|
prev_blockhash,
|
|
|
|
merkle_root: bitcoin::hash_types::TxMerkleNode::all_zeros(),
|
|
|
|
time: 0,
|
|
|
|
bits: bitcoin::CompactTarget::default(),
|
|
|
|
nonce: 0,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct TestCase {
|
|
|
|
name: &'static str,
|
|
|
|
chain: LocalChain,
|
|
|
|
header: Header,
|
|
|
|
height: u32,
|
|
|
|
connected_to: BlockId,
|
|
|
|
exp_result: Result<Vec<(u32, Option<BlockHash>)>, ApplyHeaderError>,
|
|
|
|
}
|
|
|
|
|
|
|
|
let test_cases = [
|
|
|
|
{
|
2024-04-17 10:44:26 +08:00
|
|
|
let header = header_from_prev_blockhash(h!("_"));
|
2024-01-11 00:05:04 +08:00
|
|
|
let hash = header.block_hash();
|
2024-04-17 10:44:26 +08:00
|
|
|
let height = 1;
|
2024-01-11 00:05:04 +08:00
|
|
|
let connected_to = BlockId { height, hash };
|
|
|
|
TestCase {
|
|
|
|
name: "connected_to_self_header_applied_to_self",
|
|
|
|
chain: local_chain![(0, h!("_")), (height, hash)],
|
|
|
|
header,
|
|
|
|
height,
|
|
|
|
connected_to,
|
|
|
|
exp_result: Ok(vec![]),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
let prev_hash = h!("A");
|
|
|
|
let prev_height = 1;
|
|
|
|
let header = header_from_prev_blockhash(prev_hash);
|
|
|
|
let hash = header.block_hash();
|
|
|
|
let height = prev_height + 1;
|
|
|
|
let connected_to = BlockId {
|
|
|
|
height: prev_height,
|
|
|
|
hash: prev_hash,
|
|
|
|
};
|
|
|
|
TestCase {
|
|
|
|
name: "connected_to_prev_header_applied_to_self",
|
|
|
|
chain: local_chain![(0, h!("_")), (prev_height, prev_hash)],
|
|
|
|
header,
|
|
|
|
height,
|
|
|
|
connected_to,
|
|
|
|
exp_result: Ok(vec![(height, Some(hash))]),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
let header = header_from_prev_blockhash(BlockHash::all_zeros());
|
|
|
|
let hash = header.block_hash();
|
|
|
|
let height = 0;
|
|
|
|
let connected_to = BlockId { height, hash };
|
|
|
|
TestCase {
|
|
|
|
name: "genesis_applied_to_self",
|
|
|
|
chain: local_chain![(0, hash)],
|
|
|
|
header,
|
|
|
|
height,
|
|
|
|
connected_to,
|
|
|
|
exp_result: Ok(vec![]),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
let header = header_from_prev_blockhash(h!("Z"));
|
|
|
|
let height = 10;
|
|
|
|
let hash = header.block_hash();
|
|
|
|
let prev_height = height - 1;
|
|
|
|
let prev_hash = header.prev_blockhash;
|
|
|
|
TestCase {
|
|
|
|
name: "connect_at_connected_to",
|
|
|
|
chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
|
|
|
|
header,
|
|
|
|
height: 10,
|
|
|
|
connected_to: BlockId {
|
|
|
|
height: 3,
|
|
|
|
hash: h!("C"),
|
|
|
|
},
|
|
|
|
exp_result: Ok(vec![(prev_height, Some(prev_hash)), (height, Some(hash))]),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
let prev_hash = h!("A");
|
|
|
|
let prev_height = 1;
|
|
|
|
let header = header_from_prev_blockhash(prev_hash);
|
|
|
|
let connected_to = BlockId {
|
|
|
|
height: prev_height,
|
|
|
|
hash: h!("not_prev_hash"),
|
|
|
|
};
|
|
|
|
TestCase {
|
|
|
|
name: "inconsistent_prev_hash",
|
|
|
|
chain: local_chain![(0, h!("_")), (prev_height, h!("not_prev_hash"))],
|
|
|
|
header,
|
|
|
|
height: prev_height + 1,
|
|
|
|
connected_to,
|
|
|
|
exp_result: Err(ApplyHeaderError::InconsistentBlocks),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
let prev_hash = h!("A");
|
|
|
|
let prev_height = 1;
|
|
|
|
let header = header_from_prev_blockhash(prev_hash);
|
|
|
|
let height = prev_height + 1;
|
|
|
|
let connected_to = BlockId {
|
|
|
|
height,
|
|
|
|
hash: h!("not_current_hash"),
|
|
|
|
};
|
|
|
|
TestCase {
|
|
|
|
name: "inconsistent_current_block",
|
|
|
|
chain: local_chain![(0, h!("_")), (height, h!("not_current_hash"))],
|
|
|
|
header,
|
|
|
|
height,
|
|
|
|
connected_to,
|
|
|
|
exp_result: Err(ApplyHeaderError::InconsistentBlocks),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
let header = header_from_prev_blockhash(h!("B"));
|
|
|
|
let height = 3;
|
|
|
|
let connected_to = BlockId {
|
|
|
|
height: 4,
|
|
|
|
hash: h!("D"),
|
|
|
|
};
|
|
|
|
TestCase {
|
|
|
|
name: "connected_to_is_greater",
|
|
|
|
chain: local_chain![(0, h!("_")), (2, h!("B"))],
|
|
|
|
header,
|
|
|
|
height,
|
|
|
|
connected_to,
|
|
|
|
exp_result: Err(ApplyHeaderError::InconsistentBlocks),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
];
|
|
|
|
|
|
|
|
for (i, t) in test_cases.into_iter().enumerate() {
|
|
|
|
println!("running test case {}: '{}'", i, t.name);
|
|
|
|
let mut chain = t.chain;
|
|
|
|
let result = chain.apply_header_connected_to(&t.header, t.height, t.connected_to);
|
|
|
|
let exp_result = t
|
|
|
|
.exp_result
|
|
|
|
.map(|cs| cs.iter().cloned().collect::<ChangeSet>());
|
|
|
|
assert_eq!(result, exp_result, "[{}:{}] unexpected result", i, t.name);
|
|
|
|
}
|
|
|
|
}
|
2024-04-05 16:43:13 +08:00
|
|
|
|
|
|
|
fn generate_height_range_bounds(
|
|
|
|
height_upper_bound: u32,
|
|
|
|
) -> impl Strategy<Value = (Bound<u32>, Bound<u32>)> {
|
|
|
|
fn generate_height_bound(height_upper_bound: u32) -> impl Strategy<Value = Bound<u32>> {
|
|
|
|
prop_oneof![
|
|
|
|
(0..height_upper_bound).prop_map(Bound::Included),
|
|
|
|
(0..height_upper_bound).prop_map(Bound::Excluded),
|
|
|
|
Just(Bound::Unbounded),
|
|
|
|
]
|
|
|
|
}
|
|
|
|
(
|
|
|
|
generate_height_bound(height_upper_bound),
|
|
|
|
generate_height_bound(height_upper_bound),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn generate_checkpoints(max_height: u32, max_count: usize) -> impl Strategy<Value = CheckPoint> {
|
|
|
|
proptest::collection::btree_set(1..max_height, 0..max_count).prop_map(|mut heights| {
|
|
|
|
heights.insert(0); // must have genesis
|
|
|
|
CheckPoint::from_block_ids(heights.into_iter().map(|height| {
|
|
|
|
let hash = bitcoin::hashes::Hash::hash(height.to_le_bytes().as_slice());
|
|
|
|
BlockId { height, hash }
|
|
|
|
}))
|
|
|
|
.expect("blocks must be in order as it comes from btreeset")
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
proptest! {
|
|
|
|
#![proptest_config(ProptestConfig {
|
|
|
|
..Default::default()
|
|
|
|
})]
|
|
|
|
|
|
|
|
/// Ensure that [`CheckPoint::range`] returns the expected checkpoint heights by comparing it
|
|
|
|
/// against a more primitive approach.
|
|
|
|
#[test]
|
|
|
|
fn checkpoint_range(
|
|
|
|
range in generate_height_range_bounds(21_000),
|
|
|
|
cp in generate_checkpoints(21_000, 2100)
|
|
|
|
) {
|
|
|
|
let exp_heights = cp.iter().map(|cp| cp.height()).filter(|h| range.contains(h)).collect::<Vec<u32>>();
|
|
|
|
let heights = cp.range(range).map(|cp| cp.height()).collect::<Vec<u32>>();
|
|
|
|
prop_assert_eq!(heights, exp_heights);
|
|
|
|
}
|
|
|
|
}
|