feat!: LocalChain with hardwired genesis checkpoint

This ensures that `LocalChain` will always have a tip. The `ChainOracle`
trait's `get_chain_tip` method no longer needs to return an option.
This commit is contained in:
志宇
2023-10-12 16:55:32 +08:00
parent d6a0cf0795
commit 5998a22819
19 changed files with 562 additions and 452 deletions

View File

@@ -23,6 +23,7 @@ macro_rules! local_chain {
[ $(($height:expr, $block_hash:expr)), * ] => {{
#[allow(unused_mut)]
bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $block_hash).into()),*].into_iter().collect())
.expect("chain must have genesis block")
}};
}
@@ -32,8 +33,8 @@ macro_rules! chain_update {
#[allow(unused_mut)]
bdk_chain::local_chain::Update {
tip: bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $hash).into()),*].into_iter().collect())
.tip()
.expect("must have tip"),
.expect("chain must have genesis block")
.tip(),
introduce_older_blocks: true,
}
}};

View File

@@ -1,7 +1,7 @@
#[macro_use]
mod common;
use std::collections::{BTreeMap, BTreeSet};
use std::collections::BTreeSet;
use bdk_chain::{
indexed_tx_graph::{self, IndexedTxGraph},
@@ -9,9 +9,7 @@ use bdk_chain::{
local_chain::LocalChain,
tx_graph, BlockId, ChainPosition, ConfirmationHeightAnchor,
};
use bitcoin::{
secp256k1::Secp256k1, BlockHash, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut,
};
use bitcoin::{secp256k1::Secp256k1, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut};
use miniscript::Descriptor;
/// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented
@@ -112,11 +110,8 @@ fn insert_relevant_txs() {
fn test_list_owned_txouts() {
// Create Local chains
let local_chain = LocalChain::from(
(0..150)
.map(|i| (i as u32, h!("random")))
.collect::<BTreeMap<u32, BlockHash>>(),
);
let local_chain = LocalChain::from_blocks((0..150).map(|i| (i as u32, h!("random"))).collect())
.expect("must have genesis hash");
// Initiate IndexedTxGraph

View File

@@ -1,4 +1,6 @@
use bdk_chain::local_chain::{CannotConnectError, ChangeSet, InsertBlockError, LocalChain, Update};
use bdk_chain::local_chain::{
AlterCheckPointError, CannotConnectError, ChangeSet, LocalChain, Update,
};
use bitcoin::BlockHash;
#[macro_use]
@@ -68,10 +70,10 @@ fn update_local_chain() {
[
TestLocalChain {
name: "add first tip",
chain: local_chain![],
chain: local_chain![(0, h!("A"))],
update: chain_update![(0, h!("A"))],
exp: ExpectedResult::Ok {
changeset: &[(0, Some(h!("A")))],
changeset: &[],
init_changeset: &[(0, Some(h!("A")))],
},
},
@@ -86,18 +88,18 @@ fn update_local_chain() {
},
TestLocalChain {
name: "two disjoint chains cannot merge",
chain: local_chain![(0, h!("A"))],
update: chain_update![(1, h!("B"))],
chain: local_chain![(0, h!("_")), (1, h!("A"))],
update: chain_update![(0, h!("_")), (2, h!("B"))],
exp: ExpectedResult::Err(CannotConnectError {
try_include_height: 0,
try_include_height: 1,
}),
},
TestLocalChain {
name: "two disjoint chains cannot merge (existing chain longer)",
chain: local_chain![(1, h!("A"))],
update: chain_update![(0, h!("B"))],
chain: local_chain![(0, h!("_")), (2, h!("A"))],
update: chain_update![(0, h!("_")), (1, h!("B"))],
exp: ExpectedResult::Err(CannotConnectError {
try_include_height: 1,
try_include_height: 2,
}),
},
TestLocalChain {
@@ -111,54 +113,54 @@ fn update_local_chain() {
},
// Introduce an older checkpoint (B)
// | 0 | 1 | 2 | 3
// chain | C D
// update | B C
// chain | _ C D
// update | _ B C
TestLocalChain {
name: "can introduce older checkpoint",
chain: local_chain![(2, h!("C")), (3, h!("D"))],
update: chain_update![(1, h!("B")), (2, h!("C"))],
chain: local_chain![(0, h!("_")), (2, h!("C")), (3, h!("D"))],
update: chain_update![(0, h!("_")), (1, h!("B")), (2, h!("C"))],
exp: ExpectedResult::Ok {
changeset: &[(1, Some(h!("B")))],
init_changeset: &[(1, Some(h!("B"))), (2, Some(h!("C"))), (3, Some(h!("D")))],
init_changeset: &[(0, Some(h!("_"))), (1, Some(h!("B"))), (2, Some(h!("C"))), (3, Some(h!("D")))],
},
},
// Introduce an older checkpoint (A) that is not directly behind PoA
// | 2 | 3 | 4
// chain | B C
// update | A C
// | 0 | 2 | 3 | 4
// chain | _ B C
// update | _ A C
TestLocalChain {
name: "can introduce older checkpoint 2",
chain: local_chain![(3, h!("B")), (4, h!("C"))],
update: chain_update![(2, h!("A")), (4, h!("C"))],
chain: local_chain![(0, h!("_")), (3, h!("B")), (4, h!("C"))],
update: chain_update![(0, h!("_")), (2, h!("A")), (4, h!("C"))],
exp: ExpectedResult::Ok {
changeset: &[(2, Some(h!("A")))],
init_changeset: &[(2, Some(h!("A"))), (3, Some(h!("B"))), (4, Some(h!("C")))],
init_changeset: &[(0, Some(h!("_"))), (2, Some(h!("A"))), (3, Some(h!("B"))), (4, Some(h!("C")))],
}
},
// Introduce an older checkpoint (B) that is not the oldest checkpoint
// | 1 | 2 | 3
// chain | A C
// update | B C
// | 0 | 1 | 2 | 3
// chain | _ A C
// update | _ B C
TestLocalChain {
name: "can introduce older checkpoint 3",
chain: local_chain![(1, h!("A")), (3, h!("C"))],
update: chain_update![(2, h!("B")), (3, h!("C"))],
chain: local_chain![(0, h!("_")), (1, h!("A")), (3, h!("C"))],
update: chain_update![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
exp: ExpectedResult::Ok {
changeset: &[(2, Some(h!("B")))],
init_changeset: &[(1, Some(h!("A"))), (2, Some(h!("B"))), (3, Some(h!("C")))],
init_changeset: &[(0, Some(h!("_"))), (1, Some(h!("A"))), (2, Some(h!("B"))), (3, Some(h!("C")))],
}
},
// Introduce two older checkpoints below the PoA
// | 1 | 2 | 3
// chain | C
// update | A B C
// | 0 | 1 | 2 | 3
// chain | _ C
// update | _ A B C
TestLocalChain {
name: "introduce two older checkpoints below PoA",
chain: local_chain![(3, h!("C"))],
update: chain_update![(1, h!("A")), (2, h!("B")), (3, h!("C"))],
chain: local_chain![(0, h!("_")), (3, h!("C"))],
update: chain_update![(0, h!("_")), (1, h!("A")), (2, h!("B")), (3, h!("C"))],
exp: ExpectedResult::Ok {
changeset: &[(1, Some(h!("A"))), (2, Some(h!("B")))],
init_changeset: &[(1, Some(h!("A"))), (2, Some(h!("B"))), (3, Some(h!("C")))],
init_changeset: &[(0, Some(h!("_"))), (1, Some(h!("A"))), (2, Some(h!("B"))), (3, Some(h!("C")))],
},
},
TestLocalChain {
@@ -172,45 +174,46 @@ fn update_local_chain() {
},
// B and C are in both chain and update
// | 0 | 1 | 2 | 3 | 4
// chain | B C
// update | A B C D
// chain | _ B C
// update | _ A B C D
// This should succeed with the point of agreement being C and A should be added in addition.
TestLocalChain {
name: "two points of agreement",
chain: local_chain![(1, h!("B")), (2, h!("C"))],
update: chain_update![(0, h!("A")), (1, h!("B")), (2, h!("C")), (3, h!("D"))],
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"))],
exp: ExpectedResult::Ok {
changeset: &[(0, Some(h!("A"))), (3, Some(h!("D")))],
changeset: &[(1, Some(h!("A"))), (4, Some(h!("D")))],
init_changeset: &[
(0, Some(h!("A"))),
(1, Some(h!("B"))),
(2, Some(h!("C"))),
(3, Some(h!("D"))),
(0, Some(h!("_"))),
(1, Some(h!("A"))),
(2, Some(h!("B"))),
(3, Some(h!("C"))),
(4, Some(h!("D"))),
],
},
},
// Update and chain does not connect:
// | 0 | 1 | 2 | 3 | 4
// chain | B C
// update | A B D
// chain | _ B C
// update | _ A B D
// This should fail as we cannot figure out whether C & D are on the same chain
TestLocalChain {
name: "update and chain does not connect",
chain: local_chain![(1, h!("B")), (2, h!("C"))],
update: chain_update![(0, h!("A")), (1, h!("B")), (3, h!("D"))],
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"))],
exp: ExpectedResult::Err(CannotConnectError {
try_include_height: 2,
try_include_height: 3,
}),
},
// Transient invalidation:
// | 0 | 1 | 2 | 3 | 4 | 5
// chain | A B C E
// update | A B' C' D
// chain | _ B C E
// update | _ B' C' D
// 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",
chain: local_chain![(0, h!("A")), (2, h!("B")), (3, h!("C")), (5, h!("E"))],
update: chain_update![(0, h!("A")), (2, h!("B'")), (3, h!("C'")), (4, h!("D"))],
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"))],
exp: ExpectedResult::Ok {
changeset: &[
(2, Some(h!("B'"))),
@@ -219,7 +222,7 @@ fn update_local_chain() {
(5, None),
],
init_changeset: &[
(0, Some(h!("A"))),
(0, Some(h!("_"))),
(2, Some(h!("B'"))),
(3, Some(h!("C'"))),
(4, Some(h!("D"))),
@@ -228,13 +231,13 @@ fn update_local_chain() {
},
// Transient invalidation:
// | 0 | 1 | 2 | 3 | 4
// chain | B C E
// update | B' C' D
// chain | _ B C E
// update | _ B' C' D
// 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",
chain: local_chain![(1, h!("B")), (2, h!("C")), (4, h!("E"))],
update: chain_update![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))],
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"))],
exp: ExpectedResult::Ok {
changeset: &[
(1, Some(h!("B'"))),
@@ -243,6 +246,7 @@ fn update_local_chain() {
(4, None)
],
init_changeset: &[
(0, Some(h!("_"))),
(1, Some(h!("B'"))),
(2, Some(h!("C'"))),
(3, Some(h!("D"))),
@@ -250,16 +254,16 @@ fn update_local_chain() {
},
},
// Transient invalidation:
// | 0 | 1 | 2 | 3 | 4
// chain | A B C E
// update | B' C' D
// | 0 | 1 | 2 | 3 | 4 | 5
// chain | _ A B C E
// update | _ B' C' D
// 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",
chain: local_chain![(0, h!("A")), (1, h!("B")), (2, h!("C")), (4, h!("E"))],
update: chain_update![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))],
exp: ExpectedResult::Err(CannotConnectError { try_include_height: 0 }),
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 }),
},
// Introduce blocks between two points of agreement
// | 0 | 1 | 2 | 3 | 4 | 5
@@ -294,44 +298,44 @@ fn local_chain_insert_block() {
struct TestCase {
original: LocalChain,
insert: (u32, BlockHash),
expected_result: Result<ChangeSet, InsertBlockError>,
expected_result: Result<ChangeSet, AlterCheckPointError>,
expected_final: LocalChain,
}
let test_cases = [
TestCase {
original: local_chain![],
original: local_chain![(0, h!("_"))],
insert: (5, h!("block5")),
expected_result: Ok([(5, Some(h!("block5")))].into()),
expected_final: local_chain![(5, h!("block5"))],
expected_final: local_chain![(0, h!("_")), (5, h!("block5"))],
},
TestCase {
original: local_chain![(3, h!("A"))],
original: local_chain![(0, h!("_")), (3, h!("A"))],
insert: (4, h!("B")),
expected_result: Ok([(4, Some(h!("B")))].into()),
expected_final: local_chain![(3, h!("A")), (4, h!("B"))],
expected_final: local_chain![(0, h!("_")), (3, h!("A")), (4, h!("B"))],
},
TestCase {
original: local_chain![(4, h!("B"))],
original: local_chain![(0, h!("_")), (4, h!("B"))],
insert: (3, h!("A")),
expected_result: Ok([(3, Some(h!("A")))].into()),
expected_final: local_chain![(3, h!("A")), (4, h!("B"))],
expected_final: local_chain![(0, h!("_")), (3, h!("A")), (4, h!("B"))],
},
TestCase {
original: local_chain![(2, h!("K"))],
original: local_chain![(0, h!("_")), (2, h!("K"))],
insert: (2, h!("K")),
expected_result: Ok([].into()),
expected_final: local_chain![(2, h!("K"))],
expected_final: local_chain![(0, h!("_")), (2, h!("K"))],
},
TestCase {
original: local_chain![(2, h!("K"))],
original: local_chain![(0, h!("_")), (2, h!("K"))],
insert: (2, h!("J")),
expected_result: Err(InsertBlockError {
expected_result: Err(AlterCheckPointError {
height: 2,
original_hash: h!("K"),
update_hash: h!("J"),
update_hash: Some(h!("J")),
}),
expected_final: local_chain![(2, h!("K"))],
expected_final: local_chain![(0, h!("_")), (2, h!("K"))],
},
];

View File

@@ -511,11 +511,13 @@ fn test_calculate_fee_on_coinbase() {
// where b0 and b1 spend a0, c0 and c1 spend b0, d0 spends c1, etc.
#[test]
fn test_walk_ancestors() {
let local_chain: LocalChain = (0..=20)
.map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes())))
.collect::<BTreeMap<u32, BlockHash>>()
.into();
let tip = local_chain.tip().expect("must have tip");
let local_chain = LocalChain::from_blocks(
(0..=20)
.map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes())))
.collect(),
)
.expect("must contain genesis hash");
let tip = local_chain.tip();
let tx_a0 = Transaction {
input: vec![TxIn {
@@ -839,11 +841,13 @@ fn test_descendants_no_repeat() {
#[test]
fn test_chain_spends() {
let local_chain: LocalChain = (0..=100)
.map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes())))
.collect::<BTreeMap<u32, BlockHash>>()
.into();
let tip = local_chain.tip().expect("must have tip");
let local_chain = LocalChain::from_blocks(
(0..=100)
.map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes())))
.collect(),
)
.expect("must have genesis hash");
let tip = local_chain.tip();
// The parent tx contains 2 outputs. Which are spent by one confirmed and one unconfirmed tx.
// The parent tx is confirmed at block 95.
@@ -1078,7 +1082,7 @@ fn test_missing_blocks() {
g
},
chain: {
let mut c = LocalChain::default();
let (mut c, _) = LocalChain::from_genesis_hash(h!("genesis"));
for (height, hash) in chain {
let _ = c.insert_block(BlockId {
height: *height,

View File

@@ -39,10 +39,7 @@ fn test_tx_conflict_handling() {
(5, h!("F")),
(6, h!("G"))
);
let chain_tip = local_chain
.tip()
.map(|cp| cp.block_id())
.unwrap_or_default();
let chain_tip = local_chain.tip().block_id();
let scenarios = [
Scenario {