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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ before 1.0).

### Changed

- **Algo-review P7/P8/P10/P16:** leftover TipOnly fence snapshot is
`Arc` (COW on extend). Densify assign resumes after the BQ-ready
prefix. BIP324 v2 decode is command+payload (no sha256d checksum, no
v1 reframe). Fence-tip BIP113 MTP is an 11-slot ring. Closed
[`docs/algo-review.md`](docs/algo-review.md) P7, P8, P10, P16
([#245](https://github.com/reardencode/rbitcoin/pull/245)).

- **Algo-review P1–P3:** BIP339 wtxid inv is a `TxGraph` map (no mempool
scan under the lock). Best-chain cumulative work is a RAM prefix
(~32 B × tip), not a genesis walk per unrequested body / `chainwork`.
Expand Down
51 changes: 51 additions & 0 deletions crates/rbitcoin-consensus/src/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ pub fn validate_header(
/// header MTP). Heights still above tip with no plan are retryable load
/// incomplete — not permanent `BadPrev` (that silently split batches to n=1).
pub fn median_time_past(query: &Query, height: Height) -> Result<u32, ConsensusError> {
if let Some((n, buf)) = query.store().mtp_times_at(height) {
return Ok(median_time_past_times(&buf[..n as usize]));
}
let mut times = Vec::with_capacity(11);
let start = height.0.saturating_sub(10);
let tip = query.tip_height().map(|h| h.0).unwrap_or(0);
Expand All @@ -106,6 +109,9 @@ pub fn median_time_past(query: &Query, height: Height) -> Result<u32, ConsensusE
/// MTP from the confirmed chain only (write structural). Tip-ahead heights
/// must be carried on [`crate::confirm_run`] `Prepared::prev_mtp`.
pub fn median_time_past_store(query: &Query, height: Height) -> Result<u32, ConsensusError> {
if let Some((n, buf)) = query.store().mtp_times_at(height) {
return Ok(median_time_past_times(&buf[..n as usize]));
}
let mut times = Vec::with_capacity(11);
let start = height.0.saturating_sub(10);
for h in start..=height.0 {
Expand Down Expand Up @@ -220,6 +226,12 @@ mod median_time_past_tests {
let mtp = median_time_past(&q, Height(2)).unwrap();
// times: 1000, 1010, 1020 → middle 1010
assert_eq!(mtp, 1010);
let (n, buf) = q
.store()
.mtp_times_at(Height(2))
.expect("ring covers confirmed tip");
assert_eq!(n, 3);
assert_eq!(median_time_past_times(&buf[..3]), 1010);

// Height above tip with no plan → incomplete load error (not BadPrev).
let err = median_time_past(&q, Height(5)).unwrap_err();
Expand All @@ -242,6 +254,45 @@ mod median_time_past_tests {
let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn sequential_tip_mtp_matches_ring_and_survives_pop() {
let (dir, q) = temp_q();
let mut prev = Fk::NULL;
let mut parent_hash: Option<[u8; 32]> = None;
let mut times = Vec::new();
for h in 0..12u32 {
let (hdr, ta) = coinbase(h, prev, parent_hash);
times.push(hdr.timestamp);
parent_hash = Some(hdr.hash);
prev = q.connect_block(Height(h), &hdr, &[ta]).unwrap();
}
let want11 = median_time_past_times(&times[1..]);
assert_eq!(median_time_past_store(&q, Height(11)).unwrap(), want11);
assert_eq!(median_time_past(&q, Height(11)).unwrap(), want11);
let (n, buf) = q.store().mtp_times_at(Height(11)).expect("ring at tip");
assert_eq!(n, 11);
assert_eq!(median_time_past_times(&buf[..11]), want11);
assert!(
q.store().mtp_times_at(Height(5)).is_none(),
"historical MTP is not the tip ring"
);
assert_eq!(
median_time_past_store(&q, Height(5)).unwrap(),
median_time_past_times(&times[0..=5])
);

q.disconnect_tip().unwrap();
let want10 = median_time_past_times(&times[0..=10]);
assert_eq!(median_time_past_store(&q, Height(10)).unwrap(), want10);
let (n, buf) = q
.store()
.mtp_times_at(Height(10))
.expect("ring rebuilt after pop");
assert_eq!(median_time_past_times(&buf[..n as usize]), want10);

let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn validate_header_genesis_and_bad_prev() {
let (dir, q) = temp_q();
Expand Down
141 changes: 95 additions & 46 deletions crates/rbitcoin-net/src/codec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
//! blocking worker. Never deserialize multi‑MB `block` payloads on the async
//! I/O worker.

use bitcoin::consensus::deserialize;
use bitcoin::consensus::encode::Decodable;
use bitcoin::p2p::message::{CommandString, NetworkMessage, RawNetworkMessage};
use bitcoin::p2p::Magic;

Expand Down Expand Up @@ -49,7 +49,6 @@ pub struct FramedMessage {
pub magic: Magic,
/// 12-byte null-padded command (wire form).
pub command: [u8; 12],
pub checksum: [u8; 4],
pub payload: Vec<u8>,
}

Expand Down Expand Up @@ -107,30 +106,21 @@ impl FramedMessage {
Some(bitcoin::BlockHash::from_byte_array(dig.to_byte_array()))
}

/// CPU-heavy: checksum + payload deserialize into [`RawNetworkMessage`].
/// Deserialize the application payload (no v1 header, no checksum).
///
/// Extra bytes / unknown command → [`NetworkMessage::Unknown`].
pub fn decode(self) -> RawNetworkMessage {
let mut full = Vec::with_capacity(24 + self.payload.len());
full.extend_from_slice(self.magic.to_bytes().as_ref());
full.extend_from_slice(&self.command);
full.extend_from_slice(&(self.payload.len() as u32).to_le_bytes());
full.extend_from_slice(&self.checksum);
full.extend_from_slice(&self.payload);

match deserialize::<RawNetworkMessage>(&full) {
Ok(msg) => msg,
Err(_e) => {
// Real peers send extensions/padding that can trip strict payload
// checks (e.g. "extra bytes after network message payload").
// Bytes are already framed correctly — surface as Unknown.
let cmd = command_from_header(&self.command);
RawNetworkMessage::new(
self.magic,
NetworkMessage::Unknown {
command: cmd,
payload: self.payload,
},
)
}
let cmd = command_from_header(&self.command);
let mut sl = self.payload.as_slice();
match decode_cmd_payload(cmd.as_ref(), &mut sl) {
Ok(Some(msg)) if sl.is_empty() => RawNetworkMessage::new(self.magic, msg),
_ => RawNetworkMessage::new(
self.magic,
NetworkMessage::Unknown {
command: cmd,
payload: self.payload,
},
),
}
}

Expand Down Expand Up @@ -168,6 +158,82 @@ pub(crate) fn command_bytes_ok(cmd12: &[u8]) -> bool {
any
}

fn decode_cmd_payload(
cmd: &str,
d: &mut &[u8],
) -> Result<Option<NetworkMessage>, bitcoin::consensus::encode::Error> {
fn one<T: Decodable>(
d: &mut &[u8],
f: fn(T) -> NetworkMessage,
) -> Result<NetworkMessage, bitcoin::consensus::encode::Error> {
Ok(f(Decodable::consensus_decode(d)?))
}
Ok(Some(match cmd {
"verack" => NetworkMessage::Verack,
"sendheaders" => NetworkMessage::SendHeaders,
"getaddr" => NetworkMessage::GetAddr,
"mempool" => NetworkMessage::MemPool,
"filterclear" => NetworkMessage::FilterClear,
"wtxidrelay" => NetworkMessage::WtxidRelay,
"sendaddrv2" => NetworkMessage::SendAddrV2,
"version" => one(d, NetworkMessage::Version)?,
"addr" => one(d, NetworkMessage::Addr)?,
"inv" => one(d, NetworkMessage::Inv)?,
"getdata" => one(d, NetworkMessage::GetData)?,
"notfound" => one(d, NetworkMessage::NotFound)?,
"getblocks" => one(d, NetworkMessage::GetBlocks)?,
"getheaders" => one(d, NetworkMessage::GetHeaders)?,
"block" => one(d, NetworkMessage::Block)?,
"tx" => one(d, NetworkMessage::Tx)?,
"ping" => one(d, NetworkMessage::Ping)?,
"pong" => one(d, NetworkMessage::Pong)?,
"merkleblock" => one(d, NetworkMessage::MerkleBlock)?,
"filterload" => one(d, NetworkMessage::FilterLoad)?,
"filteradd" => one(d, NetworkMessage::FilterAdd)?,
"getcfilters" => one(d, NetworkMessage::GetCFilters)?,
"cfilter" => one(d, NetworkMessage::CFilter)?,
"getcfheaders" => one(d, NetworkMessage::GetCFHeaders)?,
"cfheaders" => one(d, NetworkMessage::CFHeaders)?,
"getcfcheckpt" => one(d, NetworkMessage::GetCFCheckpt)?,
"cfcheckpt" => one(d, NetworkMessage::CFCheckpt)?,
"reject" => one(d, NetworkMessage::Reject)?,
"alert" => one(d, NetworkMessage::Alert)?,
"sendcmpct" => one(d, NetworkMessage::SendCmpct)?,
"cmpctblock" => one(d, NetworkMessage::CmpctBlock)?,
"getblocktxn" => one(d, NetworkMessage::GetBlockTxn)?,
"blocktxn" => one(d, NetworkMessage::BlockTxn)?,
"addrv2" => one(d, NetworkMessage::AddrV2)?,
"feefilter" => {
let fee: i64 = Decodable::consensus_decode(d)?;
let upper: i64 = bitcoin::Amount::MAX_MONEY
.to_sat()
.try_into()
.expect("Amount::MAX_MONEY < i64::MAX");
if fee < 0 || fee > upper {
return Err(bitcoin::consensus::encode::Error::ParseFailed(
"feefilter value out of range",
));
}
NetworkMessage::FeeFilter(fee)
}
"headers" => {
let n = bitcoin::consensus::encode::VarInt::consensus_decode(d)?.0 as usize;
let mut hs = Vec::with_capacity(n.min(16 * 1024));
for _ in 0..n {
hs.push(bitcoin::block::Header::consensus_decode(d)?);
let txn: u8 = Decodable::consensus_decode(d)?;
if txn != 0 {
return Err(bitcoin::consensus::encode::Error::ParseFailed(
"Headers message should not contain transactions",
));
}
}
NetworkMessage::Headers(hs)
}
_ => return Ok(None),
}))
}

fn command_from_header(cmd12: &[u8]) -> CommandString {
let end = cmd12.iter().position(|&b| b == 0).unwrap_or(12);
let s = std::str::from_utf8(&cmd12[..end]).unwrap_or("unknown");
Expand All @@ -187,35 +253,25 @@ mod tests {
}

#[test]
fn frame_decode_verack_via_synthetic() {
use bitcoin::hashes::Hash as _;
let magic = signet_magic();
let payload = Vec::<u8>::new();
let dig = bitcoin::hashes::sha256d::Hash::hash(&payload);
let ba = dig.to_byte_array();
fn frame_decode_verack_ignores_checksum() {
let frame = FramedMessage {
magic,
magic: signet_magic(),
command: *b"verack\0\0\0\0\0\0",
checksum: [ba[0], ba[1], ba[2], ba[3]],
payload,
payload: Vec::new(),
};
assert!(!frame.decode_is_cpu_heavy());
assert!(matches!(frame.decode().payload(), NetworkMessage::Verack));
}

#[test]
fn block_hash_from_header_matches_full_block() {
use bitcoin::hashes::Hash as _;
let magic = Magic::from(Network::Bitcoin);
let genesis = genesis_block(Network::Bitcoin);
let want = genesis.block_hash();
let payload = serialize(&genesis);
let dig = bitcoin::hashes::sha256d::Hash::hash(&payload);
let ba = dig.to_byte_array();
let frame = FramedMessage {
magic,
command: *b"block\0\0\0\0\0\0\0",
checksum: [ba[0], ba[1], ba[2], ba[3]],
payload,
};
assert!(frame.is_block());
Expand Down Expand Up @@ -250,16 +306,12 @@ mod tests {

#[test]
fn frame_helpers_ping_headers_notfound_and_encode_cost() {
use bitcoin::hashes::Hash as _;
let magic = signet_magic();
let nonce: u64 = 0x1122_3344_5566_7788;
let payload = nonce.to_le_bytes().to_vec();
let dig = bitcoin::hashes::sha256d::Hash::hash(&payload);
let ba = dig.to_byte_array();
let ping = FramedMessage {
magic,
command: *b"ping\0\0\0\0\0\0\0\0",
checksum: [ba[0], ba[1], ba[2], ba[3]],
payload: payload.clone(),
};
assert!(ping.is_ping());
Expand All @@ -269,15 +321,13 @@ mod tests {
let short = FramedMessage {
magic,
command: *b"ping\0\0\0\0\0\0\0\0",
checksum: [0; 4],
payload: vec![1, 2, 3],
};
assert!(short.ping_nonce().is_none());

let headers = FramedMessage {
magic,
command: *b"headers\0\0\0\0\0",
checksum: [0; 4],
payload: vec![],
};
assert!(headers.is_headers());
Expand All @@ -287,7 +337,6 @@ mod tests {
let nf = FramedMessage {
magic,
command: *b"notfound\0\0\0\0",
checksum: [0; 4],
payload: vec![],
};
assert!(nf.is_notfound());
Expand All @@ -297,7 +346,6 @@ mod tests {
let bad = FramedMessage {
magic,
command: *b"block\0\0\0\0\0\0\0",
checksum: [0; 4],
payload: vec![0u8; 10],
};
match bad.decode().payload() {
Expand All @@ -310,6 +358,7 @@ mod tests {
assert!(!encode_is_cpu_heavy(&NetworkMessage::Verack));
assert!(encode_is_cpu_heavy(&NetworkMessage::Headers(vec![])));
// Small inv is cheap; large is heavy.
use bitcoin::hashes::Hash as _;
use bitcoin::p2p::message_blockdata::Inventory;
let small = NetworkMessage::Inv(vec![Inventory::Block(
bitcoin::BlockHash::from_byte_array([0; 32]),
Expand Down
Loading
Loading