Skip to content

Commit faf75b5

Browse files
committed
feat(node): NodeState owns BlockTree; apply_block inserts header into tree
Op: extend
1 parent 4b95900 commit faf75b5

2 files changed

Lines changed: 70 additions & 0 deletions

File tree

crates/node/src/import.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,34 @@ mod tests {
162162
Ok(())
163163
}
164164

165+
#[test]
166+
fn two_block_import_grows_block_tree_to_two_headers() -> Result<()> {
167+
let genesis_bytes = hex_decode(REGTEST_GENESIS_HEX)?;
168+
let mut cursor = std::io::Cursor::new(genesis_bytes.as_slice());
169+
let mut follow_up = Block::consensus_decode(&mut cursor)?;
170+
follow_up.header.prev_blockhash = follow_up.block_hash();
171+
follow_up.txdata[0].input[0].script_sig = bitcoin::ScriptBuf::from_bytes(vec![1, 1]);
172+
follow_up.header.merkle_root = follow_up
173+
.compute_merkle_root()
174+
.ok_or_else(|| anyhow::anyhow!("follow-up block should have merkle root"))?;
175+
mine_block_to_declared_target(&mut follow_up)?;
176+
177+
let mut follow_up_bytes = Vec::new();
178+
follow_up.consensus_encode(&mut follow_up_bytes)?;
179+
180+
let dir = tempdir()?;
181+
let mut config = crate::Config::default_for_network(crate::Network::Regtest);
182+
config.data_dir = dir.path().join("node");
183+
config.p2p_listen.clear();
184+
let state = NodeState::open(config)?;
185+
186+
let _genesis = import_block(&state, &genesis_bytes)?;
187+
let _follow_up = import_block(&state, &follow_up_bytes)?;
188+
189+
assert_eq!(state.block_tree().read().len(), 2);
190+
Ok(())
191+
}
192+
165193
#[test]
166194
fn import_rejects_block_with_no_coinbase() -> Result<()> {
167195
let genesis_bytes = hex_decode(REGTEST_GENESIS_HEX)?;

crates/node/src/state.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ pub enum ApplyError {
4848
/// Consensus validation rejected the block.
4949
#[error("consensus: {0}")]
5050
Consensus(#[from] bitcoin_rs_consensus::ConsensusError),
51+
/// Block-tree insertion rejected the header.
52+
#[error("chain: {0}")]
53+
Chain(#[from] bitcoin_rs_chain::ChainError),
5154
/// UTXO commit failed during block apply.
5255
#[error("utxo commit: {0}")]
5356
UtxoCommit(#[from] bitcoin_rs_utxo::UtxoError),
@@ -157,6 +160,7 @@ pub struct NodeState {
157160
utxo: Arc<UtxoSet>,
158161
mempool: Arc<RwLock<Mempool>>,
159162
chain_tip: Arc<ArcSwapOption<TipSnapshot>>,
163+
block_tree: Arc<RwLock<bitcoin_rs_chain::BlockTree>>,
160164
blocks: Arc<RwLock<Vec<BlockRecord>>>,
161165
transactions: Arc<RwLock<HashMap<Txid, Transaction>>>,
162166
network: Arc<RwLock<NetworkState>>,
@@ -175,6 +179,7 @@ impl NodeState {
175179
let utxo = Arc::new(UtxoSet::new());
176180
let mempool = Arc::new(RwLock::new(Mempool::new(MempoolLimits::default())));
177181
let chain_tip = Arc::new(ArcSwapOption::empty());
182+
let block_tree = Arc::new(RwLock::new(bitcoin_rs_chain::BlockTree::new()));
178183
let blocks = Arc::new(RwLock::new(Vec::new()));
179184
let transactions = Arc::new(RwLock::new(HashMap::new()));
180185
let network = Arc::new(RwLock::new(NetworkState::default()));
@@ -192,6 +197,7 @@ impl NodeState {
192197
utxo,
193198
mempool,
194199
chain_tip,
200+
block_tree,
195201
blocks,
196202
transactions,
197203
network,
@@ -236,6 +242,12 @@ impl NodeState {
236242
Arc::clone(&self.chain_tip)
237243
}
238244

245+
/// Returns the shared block-tree handle.
246+
#[must_use]
247+
pub fn block_tree(&self) -> Arc<RwLock<bitcoin_rs_chain::BlockTree>> {
248+
Arc::clone(&self.block_tree)
249+
}
250+
239251
/// Returns the shared block-records handle exposed to RPC handlers.
240252
#[must_use]
241253
pub fn blocks(&self) -> Arc<RwLock<Vec<BlockRecord>>> {
@@ -352,6 +364,11 @@ impl NodeState {
352364
// Contextual consensus checks (BIP30 + BIP34) using the resolved height.
353365
self.check_bip30_and_bip34(block, height)?;
354366

367+
// Persist the header into the in-memory block tree; it is the source of
368+
// truth for header height, chainwork, and parent linkage. We dual-write
369+
// `chain_tip` below until readers migrate to `block_tree()`.
370+
self.insert_active_header(block)?;
371+
355372
let mut changes = BlockChanges::default();
356373
for tx in &block.txdata {
357374
let txid = tx.compute_txid();
@@ -411,6 +428,13 @@ impl NodeState {
411428
Ok(tip)
412429
}
413430

431+
fn insert_active_header(&self, block: &bitcoin::Block) -> core::result::Result<(), ApplyError> {
432+
self.block_tree
433+
.write()
434+
.insert_header(block.header, bitcoin_rs_chain::node::NodeStatus::Active)?;
435+
Ok(())
436+
}
437+
414438
fn check_bip30_and_bip34(
415439
&self,
416440
block: &bitcoin::Block,
@@ -491,6 +515,24 @@ mod tests {
491515
Ok(())
492516
}
493517

518+
#[test]
519+
fn open_constructs_empty_block_tree() -> anyhow::Result<()> {
520+
use tempfile::tempdir;
521+
522+
let dir = tempdir()?;
523+
let mut config = crate::Config::default_for_network(crate::Network::Regtest);
524+
config.data_dir = dir.path().join("node");
525+
config.p2p_listen.clear();
526+
let state = NodeState::open(config)?;
527+
let tree = state.block_tree();
528+
529+
assert!(
530+
tree.read().is_empty(),
531+
"freshly opened tree has zero headers"
532+
);
533+
Ok(())
534+
}
535+
494536
#[test]
495537
fn open_constructs_full_rpc_handle_set() -> anyhow::Result<()> {
496538
use tempfile::tempdir;

0 commit comments

Comments
 (0)