Skip to content
Draft
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
10 changes: 10 additions & 0 deletions bin/floresta-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ fn do_request(cmd: &Cli, client: Client) -> anyhow::Result<String> {
Ok(match cmd.methods.clone() {
// Handle each possible RPC method and serialize the result to a pretty JSON string
Methods::GetBlockchainInfo => serde_json::to_string_pretty(&client.get_blockchain_info()?)?,
Methods::GetChainTips => serde_json::to_string_pretty(&client.get_chain_tips()?)?,
Methods::GetBlockHash { height } => {
serde_json::to_string_pretty(&client.get_block_hash(height)?)?
}
Expand Down Expand Up @@ -289,6 +290,15 @@ pub enum Methods {
verbosity: Option<u32>,
},

#[doc = include_str!("../../../doc/rpc/getchaintips.md")]
#[command(
name = "getchaintips",
about = "Return information about all known tips in the block tree.",
long_about = Some(include_str!("../../../doc/rpc/getchaintips.md")),
disable_help_subcommand = true
)]
GetChainTips,

/// Returns information about the peers we are connected to
#[command(name = "getpeerinfo")]
GetPeerInfo,
Expand Down
3 changes: 2 additions & 1 deletion crates/floresta-chain/src/extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ mod tests {
use super::*;
use crate::BlockConsumer;
use crate::BlockchainError;
use crate::ChainTipInfo;
use crate::UtxoData;

#[derive(Debug)]
Expand Down Expand Up @@ -409,7 +410,7 @@ mod tests {
unimplemented!()
}

fn get_chain_tips(&self) -> Result<Vec<BlockHash>, Self::Error> {
fn get_chain_tips(&self) -> Result<Vec<ChainTipInfo>, Self::Error> {
unimplemented!()
}

Expand Down
91 changes: 84 additions & 7 deletions crates/floresta-chain/src/pruned_utreexo/chain_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ use tracing::warn;
use super::chain_state_builder::BlockchainBuilderError;
use super::chain_state_builder::ChainStateBuilder;
use super::chainparams::ChainParams;
use super::chainstore::ChainTipInfo;
use super::chainstore::ChainTipStatus;
use super::chainstore::DiskBlockHeader;
use super::consensus::Consensus;
use super::error::BlockValidationErrors;
Expand Down Expand Up @@ -301,8 +303,13 @@ impl<PersistedState: ChainStore> ChainState<PersistedState> {
let mut header = DiskBlockHeader::HeadersOnly(*new_tip, height);

while !self.is_genesis(&header) && header.block_hash() != fork_point {
let disk_header = DiskBlockHeader::HeadersOnly(*header, height);
let hash = disk_header.block_hash();
let hash = header.block_hash();

// Preserve FullyValid status for blocks we already validated.
let disk_header = match self.get_disk_block_header(&hash) {
Ok(DiskBlockHeader::FullyValid(h, _)) => DiskBlockHeader::FullyValid(h, height),
_ => DiskBlockHeader::HeadersOnly(*header, height),
};

self.update_header_and_index(&disk_header, hash, height)?;

Expand Down Expand Up @@ -393,6 +400,7 @@ impl<PersistedState: ChainStore> ChainState<PersistedState> {
// This method should only be called after we validate the new branch
fn reorg(&self, new_tip: BlockHeader) -> Result<(), BlockchainError> {
let current_best_block = self.get_block_header(&self.get_best_block()?.1)?;
let old_best_hash = self.get_best_block()?.1;
let fork_point = self.find_fork_point(&new_tip)?;

self.mark_chain_as_inactive(&current_best_block, fork_point.block_hash())?;
Expand All @@ -404,6 +412,60 @@ impl<PersistedState: ChainStore> ChainState<PersistedState> {
self.change_active_chain(&new_tip, validation_index, depth);
self.reorg_acc(&fork_point)?;

// Update alternative_tips: remove any tips that are now ancestors of
// the new active chain, and add the old active tip.
//
// Collect hashes on the new active chain (tip → fork point) before
// acquiring the write lock to avoid re-entrant lock issues.
let mut new_chain_hashes = alloc::collections::BTreeSet::new();
{
let mut h = new_tip;
let fork_hash = fork_point.block_hash();
while h.block_hash() != fork_hash && !self.is_genesis(&h) {
new_chain_hashes.insert(h.block_hash());
h = *self.get_ancestor(&h)?;
}
}
// Check whether old_best_hash is already an ancestor of any
// remaining alternative tip. If so, it is not a true leaf tip and
// should not be added (mirrors Bitcoin Core which identifies tips as
// blocks with no children).
let old_best_is_ancestor = {
let inner = read_lock!(self);
inner.best_block.alternative_tips.iter().any(|&tip_hash| {
if new_chain_hashes.contains(&tip_hash) {
return false; // will be removed
}
// Walk ancestors of this alt tip looking for old_best_hash
let mut h = tip_hash;
loop {
if h == old_best_hash {
return true;
}
match inner.chainstore.get_header(&h) {
Ok(Some(dh)) => {
let prev = dh.prev_blockhash;
if prev == h {
return false; // genesis
}
h = prev;
}
_ => return false,
}
}
})
};
{
let mut inner = write_lock!(self);
inner
.best_block
.alternative_tips
.retain(|h| !new_chain_hashes.contains(h));
if !old_best_is_ancestor {
inner.best_block.alternative_tips.push(old_best_hash);
}
}

Ok(())
}

Expand Down Expand Up @@ -985,12 +1047,23 @@ impl<PersistedState: ChainStore> BlockchainInterface for ChainState<PersistedSta
Consensus::update_acc(&acc, block, height, proof, del_hashes)
}

fn get_chain_tips(&self) -> Result<Vec<BlockHash>, Self::Error> {
fn get_chain_tips(&self) -> Result<Vec<ChainTipInfo>, Self::Error> {
let inner = read_lock!(self);
let mut tips = Vec::new();

tips.push(inner.best_block.best_block);
tips.extend(inner.best_block.alternative_tips.iter());
let best = inner.best_block.best_block;

let mut tips = vec![ChainTipInfo {
hash: best,
status: ChainTipStatus::Active,
}];

for &hash in &inner.best_block.alternative_tips {
let status = match inner.chainstore.get_header(&hash)? {
Some(DiskBlockHeader::InvalidChain(_)) => ChainTipStatus::Invalid,
Some(DiskBlockHeader::HeadersOnly(_, _)) => ChainTipStatus::HeadersOnly,
_ => ChainTipStatus::ValidFork,
};
tips.push(ChainTipInfo { hash, status });
}

Ok(tips)
}
Expand Down Expand Up @@ -1204,6 +1277,10 @@ impl<PersistedState: ChainStore> UpdatableChainstate for ChainState<PersistedSta
let height = self.get_disk_block_header(&block)?.try_height()?;
let current_height = self.get_height()?;

// Remember the current best block so we can track it as an alternative
// (invalid) tip after rolling back.
let old_best = self.get_best_block()?.1;

// Mark all blocks after this one as invalid
for h in height..=current_height {
let hash = self.get_block_hash(h)?;
Expand Down
26 changes: 26 additions & 0 deletions crates/floresta-chain/src/pruned_utreexo/chainstore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,32 @@ impl Encodable for DiskBlockHeader {
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// The validation status of a chain tip, as tracked by the chain backend
pub enum ChainTipStatus {
/// The current best chain tip
Active,

/// A fully validated fork that is not the active chain
ValidFork,

/// Headers received but blocks not yet fully validated
HeadersOnly,

/// The chain contains at least one invalid block
Invalid,
}

#[derive(Debug, Clone, PartialEq, Eq)]
/// A chain tip with its block hash and validation status
pub struct ChainTipInfo {
/// The block hash of this chain tip
pub hash: BlockHash,

/// The validation status of this chain tip
pub status: ChainTipStatus,
}

#[derive(Clone, Debug, PartialEq, Eq)]
/// Internal representation of the chain we are in
pub struct BestChain {
Expand Down
13 changes: 10 additions & 3 deletions crates/floresta-chain/src/pruned_utreexo/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use rustreexo::node_hash::BitcoinNodeHash;
use rustreexo::proof::Proof;
use rustreexo::stump::Stump;

use self::chainstore::ChainTipInfo;
use self::partial_chain::PartialChainState;
use crate::prelude::*;
use crate::pruned_utreexo::utxo_data::UtxoData;
Expand Down Expand Up @@ -103,8 +104,14 @@ pub trait BlockchainInterface {
del_hashes: Vec<sha256::Hash>,
) -> Result<Stump, Self::Error>;

/// Returns all known chain tips, including the best one and forks
fn get_chain_tips(&self) -> Result<Vec<BlockHash>, Self::Error>;
/// Returns all known chain tips, including the active tip and any forks.
///
/// Each returned [ChainTipInfo] contains the tip's block hash and its
/// [ChainTipStatus], which reflects the validation state of that branch
/// (e.g. active, valid fork, headers-only, or invalid).
///
/// The first element is always the active chain tip.
fn get_chain_tips(&self) -> Result<Vec<ChainTipInfo>, Self::Error>;

/// Validates a block according to Bitcoin's rules, without modifying our chain
fn validate_block(
Expand Down Expand Up @@ -335,7 +342,7 @@ impl<T: BlockchainInterface> BlockchainInterface for Arc<T> {
T::update_acc(self, acc, block, height, proof, del_hashes)
}

fn get_chain_tips(&self) -> Result<Vec<BlockHash>, Self::Error> {
fn get_chain_tips(&self) -> Result<Vec<ChainTipInfo>, Self::Error> {
T::get_chain_tips(self)
}

Expand Down
3 changes: 2 additions & 1 deletion crates/floresta-chain/src/pruned_utreexo/partial_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use rustreexo::stump::Stump;
use tracing::info;

use super::chainparams::ChainParams;
use super::chainstore::ChainTipInfo;
use super::consensus::Consensus;
use super::error::BlockValidationErrors;
use super::error::BlockchainError;
Expand Down Expand Up @@ -395,7 +396,7 @@ impl BlockchainInterface for PartialChainState {
unimplemented!("PartialChainState::get_block_header")
}

fn get_chain_tips(&self) -> Result<Vec<BlockHash>, Self::Error> {
fn get_chain_tips(&self) -> Result<Vec<ChainTipInfo>, Self::Error> {
unimplemented!("PartialChainState::get_chain_tips")
}

Expand Down
111 changes: 110 additions & 1 deletion crates/floresta-node/src/json_rpc/blockchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use corepc_types::v30::GetBlockVerboseOne;
use corepc_types::ScriptPubkey;
use floresta_chain::extensions::HeaderExt;
use floresta_chain::extensions::WorkExt;
use floresta_chain::ChainTipStatus as ChainLevelStatus;
use miniscript::descriptor::checksum;
use serde_json::json;
use serde_json::Value;
Expand All @@ -29,6 +30,8 @@ use super::res::GetTxOutProof;
use super::res::JsonRpcError;
use super::server::RpcChain;
use super::server::RpcImpl;
use crate::json_rpc::res::ChainTip;
use crate::json_rpc::res::ChainTipStatus;
use crate::json_rpc::res::GetBlockRes;
use crate::json_rpc::res::RescanConfidence;

Expand Down Expand Up @@ -310,7 +313,113 @@ impl<Blockchain: RpcChain> RpcImpl<Blockchain> {

// getblockstats
// getchainstates
// getchaintips

pub(super) fn get_chain_tips(&self) -> Result<Vec<ChainTip>, JsonRpcError> {
let tips = self
.chain
.get_chain_tips()
.map_err(|_| JsonRpcError::Chain)?;

let (best_height, _) = self
.chain
.get_best_block()
.map_err(|_| JsonRpcError::Chain)?;

tips.into_iter()
.map(|tip| {
let status = match tip.status {
ChainLevelStatus::Active => ChainTipStatus::Active,
ChainLevelStatus::ValidFork => ChainTipStatus::ValidFork,
ChainLevelStatus::HeadersOnly => ChainTipStatus::HeadersOnly,
ChainLevelStatus::Invalid => ChainTipStatus::Invalid,
};

if matches!(tip.status, ChainLevelStatus::Active) {
return Ok(ChainTip {
height: best_height,
hash: tip.hash.to_string(),
branchlen: 0,
status,
});
}

// For tips with known height (ValidFork, HeadersOnly), we
// can look up height and fork point directly. For Invalid
// tips the DiskBlockHeader doesn't store the height, so we
// walk back through ancestors until we find one whose height
// is known and derive the tip height from the distance.
let tip_height = match self
.chain
.get_block_height(&tip.hash)
.map_err(|_| JsonRpcError::Chain)?
{
Some(h) => h,
None => {
// Walk ancestors to compute height
let mut hash = tip.hash;
let mut depth = 0u32;
loop {
let header = self
.chain
.get_block_header(&hash)
.map_err(|_| JsonRpcError::Chain)?;
let parent = header.prev_blockhash;
depth += 1;
if let Some(parent_h) = self
.chain
.get_block_height(&parent)
.map_err(|_| JsonRpcError::Chain)?
{
break parent_h + depth;
}
hash = parent;
}
}
};

// Find the fork point: the most recent common ancestor with
// the active chain. Walk back through ancestors and check
// whether each block is actually on the active chain by
// comparing get_block_hash(height) with the block's hash.
// This mirrors Bitcoin Core's FindFork behaviour.
let fork_height = {
let mut hash = tip.hash;
let mut h = tip_height;
loop {
// A block is on the active chain when its height is
// within the active chain and the hash at that height
// matches its own hash.
if h <= best_height {
if let Ok(active_hash) = self.chain.get_block_hash(h) {
if active_hash == hash {
break h;
}
}
}
if h == 0 {
break 0;
}
let header = match self.chain.get_block_header(&hash) {
Ok(hdr) => hdr,
Err(_) => break 0,
};
hash = header.prev_blockhash;
h -= 1;
}
};

let branchlen = tip_height.saturating_sub(fork_height);

Ok(ChainTip {
height: tip_height,
hash: tip.hash.to_string(),
branchlen,
status,
})
})
.collect()
}

// getchaintxstats
// getdeploymentinfo
// getdifficulty
Expand Down
Loading
Loading