Consensus optimizations by Sansh2356 · Pull Request #438 · braidpool/braidpool · GitHub
Skip to content

Consensus optimizations#438

Closed
Sansh2356 wants to merge 7 commits into
devfrom
consensus-optimizations
Closed

Consensus optimizations#438
Sansh2356 wants to merge 7 commits into
devfrom
consensus-optimizations

Conversation

@Sansh2356

@Sansh2356 Sansh2356 commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

This is segregation of #299 with reduced diff .

mcelrath and others added 5 commits April 16, 2026 21:16
Merges two significant features:
- Heuristic cohorts refactor (close-the-loop): New optimized Braid API with
  index, geneses, orphanage, MicrosecondTimestamp, algorithms module
- IBD support (sprint-agents): Initial Block Download manager, get_beads_after,
  check_genesis_beads, prepare_bead_tuple_data

Key changes:
- AddBeadStatus: Added alias variants for compatibility (DuplicateBead/DagAlreadyContainsBead,
  ParentsMissing/ParentsNotYetReceived)
- Braid: Added compatibility methods (bead_index_mapping(), genesis_beads(),
  check_genesis_beads(), insert_genesis_beads(), get_beads_after())
- Bead: Added hash() method
- Timestamps: Unified on MicrosecondTimestamp throughout
- SwarmCommand: Added InitiateIBD variant
- DB: Uses pre-computed JSON approach from sprint-agents

All 90 tests pass.
Copilot AI review requested due to automatic review settings April 17, 2026 17:17

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the braid consensus logic into a dedicated algorithms module and introduces a microsecond-precision timestamp type, updating serialization, RPC paths, and test utilities to match the new data structures and optimized consensus operations.

Changes:

  • Introduce MicrosecondTimestamp and switch committed/uncommitted metadata timestamp fields + consensus encoding to microseconds.
  • Replace legacy consensus_functions with braid::algorithms and add pure algorithm test coverage driven by JSON fixtures.
  • Update RPC, DB handler interfaces, and test utilities to use the new braid indices/maps and bead hash accessor.

Reviewed changes

Copilot reviewed 20 out of 22 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
node/src/utils/timestamp.rs Adds MicrosecondTimestamp with serde + bitcoin consensus encode/decode support.
node/src/utils/test_utils.rs Adds test macros/builders + JSON fixture loader helpers for braid algorithm tests.
node/src/utils/mod.rs Exposes timestamp module and updates test bead timestamp creation to microseconds.
node/src/uncommitted_metadata/mod.rs Switches broadcast timestamp to MicrosecondTimestamp and updates consensus encode/decode.
node/src/rpc_server.rs Updates RPC endpoints to new braid APIs and algorithm helpers; improves server start error propagation and test server spawning.
node/src/main.rs Adapts main control flow to updated AddBeadStatus variants and new braid index structures; improves RPC startup error reporting.
node/src/lib.rs Switches internal timestamp construction to microseconds and updates braid index handling paths.
node/src/error.rs Removes unused BraidError type and its Display/Error impls.
node/src/db/db_handlers.rs Updates DB handler interfaces and bead tuple prep to new braid index/relatives types; adapts timestamp types.
node/src/committed_metadata/tests.rs Updates committed-metadata tests to use MicrosecondTimestamp and new test util paths.
node/src/committed_metadata/mod.rs Switches committed metadata timestamps/TimeVec to microseconds and updates consensus encode/decode.
node/src/braid/mod.rs Major braid refactor: introduces parents/children maps, orphanage, caches, extend strategies, and updated extend/get-beads logic.
node/src/braid/algorithms.rs New pure consensus/graph algorithms (cohorts, reverse, work calc, highest-work-path).
node/src/braid/algorithm_tests.rs Adds extensive pure algorithm tests, including JSON-fixture based verification.
node/src/behaviour/tests.rs Updates behaviour tests for new timestamp type and bead hash accessor.
node/src/behaviour/mod.rs Minor import cleanup.
node/src/bead/tests.rs Updates bead tests to use MicrosecondTimestamp and new test utils import path.
node/src/bead/mod.rs Adds Bead::hash() helper for consistent hash access.
Cargo.toml Adds criterion to workspace dependencies.
.gitignore Updates ignored files/directories.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread node/src/db/db_handlers.rs Outdated
Comment on lines +372 to +376
bead.committed_metadata.start_timestamp =
MedianTimePast::from_u32(row.get::<u32, _>("start_timestamp")).unwrap();
MicrosecondTimestamp::from_secs(row.get::<u32, _>("start_timestamp"));

bead.uncommitted_metadata.broadcast_timestamp =
MedianTimePast::from_u32(row.get::<u32, _>("broadcast_timestamp")).unwrap();
MicrosecondTimestamp::from_secs(row.get::<u32, _>("broadcast_timestamp"));

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

start_timestamp and broadcast_timestamp are loaded from the DB as u32 seconds and converted via MicrosecondTimestamp::from_secs(...). The schema declares these columns as 64-bit unix epoch in microseconds, and the rest of this PR moves consensus timestamps to microseconds. These should be read as i64/u64 microseconds and converted with from_micros (and insertion should store microseconds too) to avoid precision loss and inconsistent units.

Copilot uses AI. Check for mistakes.
Comment thread node/src/db/db_handlers.rs Outdated
Comment on lines +241 to +244
parent_timestamp_tuples.push((
parent_bead as u64,
bead_id as u64,
current_parent_timestamp.to_u32().to_u64().unwrap(), // FIXME: loses precision

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prepare_bead_tuple_data serializes parent timestamps using current_parent_timestamp.to_u32().to_u64(), which truncates microseconds down to seconds (and can overflow/truncate for large values). If the DB/API expects microseconds (as the new timestamp type suggests), serialize as_micros() instead (or keep everything in seconds consistently across schema + code).

Suggested change
parent_timestamp_tuples.push((
parent_bead as u64,
bead_id as u64,
current_parent_timestamp.to_u32().to_u64().unwrap(), // FIXME: loses precision
let current_parent_timestamp_micros =
u64::try_from(current_parent_timestamp.as_micros()).map_err(|_| {
anyhow::Error::msg("parent timestamp in microseconds exceeds u64 range")
})?;
parent_timestamp_tuples.push((
parent_bead as u64,
bead_id as u64,
current_parent_timestamp_micros,

Copilot uses AI. Check for mistakes.
Comment thread node/src/braid/mod.rs
Comment on lines +182 to +190
/// Returns the average orphanage occupancy over the last `interval`, using the event history.
/// Keeps all events needed for overlapping windows; prunes older ones beyond the maximum lookback observed.
pub fn orphanage_occupancy(&mut self, interval: Duration) -> Option<f64> {
let now = Instant::now();
self.occupancy_event(now);

let start = now
.checked_sub(interval)
.unwrap_or_else(|| Instant::now() - Duration::from_secs(0));

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

orphanage_occupancy’s doc comment says it “prunes older ones”, but the implementation never removes old entries from occupancy_events, so this vector can grow without bound over time. Also, unwrap_or_else(|| Instant::now() - Duration::from_secs(0)) should likely use the already-captured now for consistency. Consider pruning events older than start (or a configured max lookback) after computing the average.

Copilot uses AI. Check for mistakes.
Comment thread Cargo.toml
strum = { version = "0.26", features = ["derive"] }
if-addrs = "0.13"
anyhow = "1.0.100"
criterion = { version = "0.5", features = ["html_reports"] }

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

criterion is added under [workspace.dependencies], which makes it available (and potentially pulled in) for normal builds of workspace crates. If it’s only intended for benchmarks, it should typically live under dev/bench dependencies (e.g., [dev-dependencies] in the benchmarking crate or a dedicated bench crate) to avoid increasing compile times and dependency surface for production builds.

Suggested change
criterion = { version = "0.5", features = ["html_reports"] }

Copilot uses AI. Check for mistakes.
let extra_nonce_2 = u32::consensus_decode(r)?;
let broadcast_timestamp = Time::from_consensus(u32::consensus_decode(r).unwrap()).unwrap();
let broadcast_timestamp = MicrosecondTimestamp::consensus_decode(r)?;
let signature = Signature::from_str(&String::consensus_decode(r).unwrap()).unwrap();

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consensus_decode uses unwrap() when decoding the signature (String::consensus_decode(...).unwrap() and Signature::from_str(...).unwrap()). Malformed input can panic the process instead of returning a bitcoin::consensus::Error. Use ?/error mapping so decode failures are reported as errors, not panics.

Suggested change
let signature = Signature::from_str(&String::consensus_decode(r).unwrap()).unwrap();
let signature_str = String::consensus_decode(r)?;
let signature = Signature::from_str(&signature_str)
.map_err(|_| Error::ParseFailed("invalid ECDSA signature"))?;

Copilot uses AI. Check for mistakes.
Comment on lines 127 to 129
let comm_pub_key = PublicKey::from_slice(&Vec::<u8>::consensus_decode(r).unwrap()).unwrap();
let min_target = CompactTarget::consensus_decode(r).unwrap();
let weak_target = CompactTarget::consensus_decode(r).unwrap();

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CommittedMetadata::consensus_decode still has multiple unwrap() calls (Vec::<u8>::consensus_decode(...).unwrap(), PublicKey::from_slice(...).unwrap(), and CompactTarget::consensus_decode(r).unwrap()). Any malformed/short input can panic during decoding. Prefer propagating bitcoin::consensus::Error via ? (and mapping conversion errors) to keep decoding panic-free.

Suggested change
let comm_pub_key = PublicKey::from_slice(&Vec::<u8>::consensus_decode(r).unwrap()).unwrap();
let min_target = CompactTarget::consensus_decode(r).unwrap();
let weak_target = CompactTarget::consensus_decode(r).unwrap();
let comm_pub_key_bytes = Vec::<u8>::consensus_decode(r)?;
let comm_pub_key =
PublicKey::from_slice(&comm_pub_key_bytes).map_err(|_| Error::ParseFailed("invalid public key"))?;
let min_target = CompactTarget::consensus_decode(r)?;
let weak_target = CompactTarget::consensus_decode(r)?;

Copilot uses AI. Check for mistakes.
Comment on lines 99 to 108
fn consensus_encode<W: Write + ?Sized>(&self, w: &mut W) -> Result<usize, io::Error> {
let mut len = 0;
let mut parent_hashes: Vec<BeadHash> = self.parents.iter().cloned().collect();
parent_hashes.sort(); // for deterministic ordering
len += self.transaction_ids.consensus_encode(w)?;
len += hashset_to_vec_deterministic(&self.parents).consensus_encode(w)?;
len += parent_hashes.consensus_encode(w)?;
len += self.parent_bead_timestamps.consensus_encode(w)?;
len += self.payout_address.consensus_encode(w)?;
len += self
.start_timestamp
.to_consensus_u32()
.consensus_encode(w)?;
len += self.start_timestamp.consensus_encode(w)?;
let pubkey_bytes = self.comm_pub_key.to_vec();

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change switches consensus encoding for timestamps from 32-bit seconds to 64-bit microseconds (e.g., start_timestamp.consensus_encode now encodes a u64, and TimeVec encodes u64 micros per entry). That is a wire-format / consensus breaking change for any previously serialized beads. If backward compatibility is required, this needs an explicit versioning/migration strategy (network upgrade flag, dual decode path, or protocol version bump).

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +29
/// Get the timestamp as seconds since epoch (truncated)
pub fn as_secs(self) -> u32 {
(self.0 / 1_000_000) as u32

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MicrosecondTimestamp::as_secs truncates to u32 via a cast. For timestamps beyond u32::MAX seconds (year ~2106), this will silently wrap/truncate and produce incorrect times. Consider returning u64 seconds, or using a checked/saturating conversion and documenting/handling out-of-range values.

Suggested change
/// Get the timestamp as seconds since epoch (truncated)
pub fn as_secs(self) -> u32 {
(self.0 / 1_000_000) as u32
/// Get the timestamp as seconds since epoch (truncated).
///
/// If the timestamp exceeds `u32::MAX` seconds, this method saturates at `u32::MAX`
/// instead of silently wrapping/truncating via a narrowing cast.
pub fn as_secs(self) -> u32 {
let secs = self.0 / 1_000_000;
u32::try_from(secs).unwrap_or(u32::MAX)

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +17
// A macro for making parents and children relationships for testing like:
// parents: relatives!(0 => [], 1 => [0])
#[macro_export]
macro_rules! relatives {
() => {
std::collections::HashMap::new()
};
($($k:expr => [$($v:expr),*]),* $(,)?) => {
std::collections::HashMap::from([$(($k, [$($v),*].into_iter().collect()),)*])
};
}

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

relatives! is #[macro_export] but not gated behind #[cfg(test)], while this module is included from utils::mod in non-test builds. That leaks a test helper macro into production builds / downstream crates. Consider adding #[cfg(test)] (or removing macro_export and re-exporting only for tests) so it’s only available to test code.

Copilot uses AI. Check for mistakes.
Comment thread node/src/utils/test_utils.rs
@Sansh2356 Sansh2356 mentioned this pull request May 12, 2026
37 tasks
@mcelrath

Copy link
Copy Markdown
Collaborator

@mcelrath mcelrath closed this May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants