diff --git a/changelog.d/zero-prev-header.md b/changelog.d/zero-prev-header.md new file mode 100644 index 0000000000..c594bb79d3 --- /dev/null +++ b/changelog.d/zero-prev-header.md @@ -0,0 +1,8 @@ +Fixed + +- **A header with an all-zero parent is stored only for genesis.** Any + other such header was written to the header store with no proof-of-work + or context check, from an unrequested block, a `headers` message, or + `submitheader`. New rows are now refused, as in Core. An unrequested + `block` with such a header gets the usual misbehavior penalty, and + `submitheader` returns `Must submit previous header`. diff --git a/crates/rbitcoin-net/src/chain.rs b/crates/rbitcoin-net/src/chain.rs index b960f8a644..a93f0e5d81 100644 --- a/crates/rbitcoin-net/src/chain.rs +++ b/crates/rbitcoin-net/src/chain.rs @@ -1409,7 +1409,8 @@ impl ChainHub { let prev = header.prev_blockhash; let prev_bytes = prev.to_byte_array(); // Same for the parent, plus a body parked with no header row yet. - let prev_known = prev_bytes == [0u8; 32] + // An all-zero parent is known only to genesis. + let prev_known = (prev_bytes == [0u8; 32] && hash == self.params.genesis_hash) || self .query .get_header_by_hash(&prev_bytes) @@ -1516,6 +1517,13 @@ impl ChainHub { ) -> Result { let prev_bytes = header.prev_blockhash.to_byte_array(); let prev_fk = if prev_bytes == [0u8; 32] { + // Only genesis has no parent. Any other all-zero parent is + // missing, as in Core, and gets no row. + if header.block_hash() != self.params.genesis_hash { + return Err(NetError::Consensus( + "header parent unknown — ensure parent before child".into(), + )); + } Fk::NULL } else if let Some(n) = in_batch.get(&prev_bytes) { n.fk @@ -6064,6 +6072,23 @@ mod tests { .to_string() .contains("header parent unknown")); assert_eq!(hub.query.store().header_count(), after_ok); + // Only genesis has an all-zero parent. Any other zero-parent header + // is an orphan, as in Core, and writes no row. + let mut zero = b4.header; + zero.prev_blockhash = BlockHash::from_byte_array([0u8; 32]); + rbitcoin_consensus::grind_regtest_pow(&mut zero); + let err = hub.ensure_header(&zero).unwrap_err(); + assert!(err.to_string().contains("header parent unknown"), "{err}"); + let err = hub.ensure_headers_batch(&[zero, b4.header]).unwrap_err(); + assert!(err.to_string().contains("header parent unknown"), "{err}"); + assert_eq!( + hub.process_submitted_header(&zero).unwrap_err(), + "Must submit previous header" + ); + assert_eq!(hub.query.store().header_count(), after_ok); + assert!(!hub.knows_header(&zero.block_hash())); + hub.ensure_header(&rbitcoin_consensus::genesis_block(&hub.params).header) + .expect("genesis still writes its zero-parent row"); let _ = std::fs::remove_dir_all(&dir); } diff --git a/crates/rbitcoin-net/src/peer_header_dos_journey.rs b/crates/rbitcoin-net/src/peer_header_dos_journey.rs index 32f1964544..a95b63e282 100644 --- a/crates/rbitcoin-net/src/peer_header_dos_journey.rs +++ b/crates/rbitcoin-net/src/peer_header_dos_journey.rs @@ -727,6 +727,25 @@ async fn header_reject_punishes_except_time(hub: &crate::chain::ChainHub) { "an unrequested header with bad pow must disconnect" ); + // Core: a missing parent is `prev-blk-not-found` and Misbehaving. An + // all-zero parent is missing too unless the block is genesis. + let mut zero_prev = rbitcoin_consensus::mine_regtest_paying( + gen, + tip_time.saturating_add(2), + next_h, + op_true(), + vec![], + ); + zero_prev.header.prev_blockhash = zero_stop(); + rbitcoin_consensus::grind_regtest_pow(&mut zero_prev.header); + let mut follow = PeerFollowState::new(); + push(hub, &out_tx, &mut follow, None, NetworkMessage::Block(zero_prev.clone())).await; + assert!( + follow.ban_score >= BAN_SCORE_THRESHOLD, + "an unrequested block with an all-zero parent must disconnect" + ); + assert!(!hub.knows_header(&zero_prev.block_hash())); + let now = u32::try_from( std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) diff --git a/docs/external_findings/030-header-accept.md b/docs/external_findings/030-header-accept.md index 2e84790342..3a6ccc45f7 100644 --- a/docs/external_findings/030-header-accept.md +++ b/docs/external_findings/030-header-accept.md @@ -9,6 +9,13 @@ accepted. A non-genesis block with an all-zero previous hash is not held and does not disconnect the tip. Header work is summed with a checked add. A zero `nBits` target is not passed to `Header::work`. +Header intake also refuses a new zero-parent non-genesis row, from a +single header, a `headers` batch, or `submitheader`. An unrequested block +with such a header gets the usual misbehavior penalty. Rows written by +older versions stay in the store. + **Regression:** `rbitcoin-net` `chain::tests::hostile_peer_session`, +`chain::tests::ensure_headers_batch_chain_and_missing_parent`, +`peer::tests::peer_header_dos_and_self_announce`, `most_work::tests::sum_work_and_work_better`