Skip to content

Commit b8b099f

Browse files
committed
feat(node): apply_block runs non-contextual consensus checks via verify_block_rules_borrowed
Op: extend
1 parent 2fc1be7 commit b8b099f

2 files changed

Lines changed: 64 additions & 5 deletions

File tree

crates/node/src/import.rs

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ pub fn import_block(state: &NodeState, block_bytes: &[u8]) -> Result<ImportOutco
4949
#[cfg(test)]
5050
mod tests {
5151
use super::*;
52-
use bitcoin::TxMerkleNode;
5352
use bitcoin::consensus::Encodable as _;
5453
use tempfile::tempdir;
5554

@@ -100,7 +99,9 @@ mod tests {
10099
let mut cursor = std::io::Cursor::new(genesis_bytes.as_slice());
101100
let mut follow_up = Block::consensus_decode(&mut cursor)?;
102101
follow_up.header.prev_blockhash = follow_up.block_hash();
103-
follow_up.header.merkle_root = TxMerkleNode::from_byte_array([1_u8; 32]);
102+
follow_up.header.merkle_root = follow_up
103+
.compute_merkle_root()
104+
.ok_or_else(|| anyhow::anyhow!("follow-up block should have merkle root"))?;
104105
follow_up.header.nonce = follow_up.header.nonce.wrapping_add(1);
105106

106107
let mut follow_up_bytes = Vec::new();
@@ -123,6 +124,47 @@ mod tests {
123124
Ok(())
124125
}
125126

127+
#[test]
128+
fn import_rejects_block_with_no_coinbase() -> Result<()> {
129+
let genesis_bytes = hex_decode(REGTEST_GENESIS_HEX)?;
130+
let mut cursor = std::io::Cursor::new(genesis_bytes.as_slice());
131+
let mut block = Block::consensus_decode(&mut cursor)?;
132+
block.txdata[0].input[0].previous_output = bitcoin::OutPoint {
133+
txid: bitcoin::Txid::from_byte_array([1_u8; 32]),
134+
vout: 0,
135+
};
136+
let merkle_root = block
137+
.compute_merkle_root()
138+
.ok_or_else(|| anyhow::anyhow!("mutated block should have merkle root"))?;
139+
block.header.merkle_root = merkle_root;
140+
141+
let mut block_bytes = Vec::new();
142+
block.consensus_encode(&mut block_bytes)?;
143+
144+
let dir = tempdir()?;
145+
let mut config = crate::Config::default_for_network(crate::Network::Regtest);
146+
config.data_dir = dir.path().join("node");
147+
config.p2p_listen.clear();
148+
let state = NodeState::open(config)?;
149+
150+
let Err(error) = import_block(&state, &block_bytes) else {
151+
anyhow::bail!("block without coinbase should be rejected");
152+
};
153+
154+
assert!(
155+
error.chain().any(
156+
|cause| cause.downcast_ref::<bitcoin_rs_consensus::ConsensusError>()
157+
== Some(&bitcoin_rs_consensus::ConsensusError::MissingCoinbase)
158+
),
159+
"error chain should contain MissingCoinbase: {error:?}"
160+
);
161+
assert!(
162+
state.chain_tip().load().is_none(),
163+
"rejected block must not advance chain tip"
164+
);
165+
Ok(())
166+
}
167+
126168
fn hex_decode(hex: &str) -> Result<Vec<u8>> {
127169
let mut bytes = Vec::with_capacity(hex.len() / 2);
128170
let chars: Vec<char> = hex.chars().collect();

crates/node/src/state.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ pub enum ApplyError {
3939
/// Height arithmetic overflowed `u32::MAX`.
4040
#[error("height overflow at tip {0}")]
4141
HeightOverflow(u32),
42+
/// Consensus validation rejected the block.
43+
#[error("consensus: {0}")]
44+
Consensus(#[from] bitcoin_rs_consensus::ConsensusError),
4245
/// UTXO commit failed during block apply.
4346
#[error("utxo commit: {0}")]
4447
UtxoCommit(#[from] bitcoin_rs_utxo::UtxoError),
@@ -274,13 +277,13 @@ impl NodeState {
274277
crate::crash_recovery::write_meta(self, &meta)
275278
}
276279

277-
/// Synthetically applies `block` as the next tip without consensus validation.
280+
/// Applies `block` as the next tip after non-contextual consensus checks.
278281
///
279282
/// This is the v1 contract: the block hash is taken from the decoded
280283
/// header, the new height is `current_tip.height + 1` (or zero when no
281284
/// tip is published yet), chainwork is approximated by accumulating the
282285
/// block header's own work onto the prior tip's chainwork, and the block
283-
/// is stored in `blocks` for RPC consumers. Real consensus validation,
286+
/// is stored in `blocks` for RPC consumers. Contextual consensus checks,
284287
/// BIP30 / BIP34 / soft-fork checks, BIP9 deployment state, and reorg
285288
/// planning land in follow-up turns.
286289
///
@@ -300,7 +303,7 @@ impl NodeState {
300303
bitcoin_rs_chain::node::ChainWork::from_be_bytes(block.header.work().to_be_bytes());
301304

302305
let prior = self.chain_tip.load_full();
303-
let (height, chainwork) = match prior {
306+
let (height, chainwork) = match prior.as_deref() {
304307
Some(tip) => {
305308
if tip.hash != prev_hash {
306309
return Err(ApplyError::PrevHashMismatch {
@@ -317,6 +320,20 @@ impl NodeState {
317320
None => (0_u32, header_work),
318321
};
319322

323+
let prev_tip_state = match prior.as_deref() {
324+
Some(tip) => bitcoin_rs_consensus::rust_path::TipState {
325+
height: Some(tip.height),
326+
block_hash: None,
327+
median_time_past: 0,
328+
},
329+
None => bitcoin_rs_consensus::rust_path::TipState {
330+
height: None,
331+
block_hash: None,
332+
median_time_past: 0,
333+
},
334+
};
335+
bitcoin_rs_consensus::verify_block::verify_block_rules_borrowed(block, &prev_tip_state)?;
336+
320337
let mut changes = BlockChanges::default();
321338
for tx in &block.txdata {
322339
let txid = tx.compute_txid();

0 commit comments

Comments
 (0)