Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions changelog.d/zero-prev-header.md
Original file line number Diff line number Diff line change
@@ -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`.
27 changes: 26 additions & 1 deletion crates/rbitcoin-net/src/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -1516,6 +1517,13 @@ impl ChainHub {
) -> Result<Fk, NetError> {
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
Expand Down Expand Up @@ -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);
}

Expand Down
19 changes: 19 additions & 0 deletions crates/rbitcoin-net/src/peer_header_dos_journey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions docs/external_findings/030-header-accept.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Loading