Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion crates/floresta-chain/src/extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ mod tests {
unimplemented!()
}

fn acc(&self) -> Stump {
fn get_acc(&self, _block: Option<BlockHash>) -> Result<Stump, Self::Error> {
unimplemented!()
}
}
Expand Down
29 changes: 17 additions & 12 deletions crates/floresta-chain/src/pruned_utreexo/chain_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -941,9 +941,6 @@ impl<PersistedState: ChainStore> ChainState<PersistedState> {
None => Ok(true),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would be nice to have tests for the arbitrary block acc.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

}
}
pub fn acc(&self) -> Stump {
read_lock!(self).acc.to_owned()
}
/// Returns the next required work for the next block, usually it's just the last block's target
/// but if we are in a retarget period, it's calculated from the last 2016 blocks.
fn get_next_required_work(
Expand Down Expand Up @@ -1042,8 +1039,18 @@ impl<PersistedState: ChainStore> BlockchainInterface for ChainState<PersistedSta
self.get_branch_work(header)
}

fn acc(&self) -> Stump {
read_lock!(self).acc.to_owned()
fn get_acc(&self, block: Option<BlockHash>) -> Result<Stump, Self::Error> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same thing about Result<Option<T>> I've mentioned a couple times.

match block {
Some(hash) => {
let height = self
.get_block_height(&hash)?
.ok_or(BlockchainError::BlockNotPresent)?;
Comment thread
jaoleal marked this conversation as resolved.

self.get_roots_for_block(height)?
.ok_or(BlockchainError::UnknownUtreexoAcc)
}
None => Ok(read_lock!(self).acc.to_owned()),
}
}

fn get_fork_point(&self, block: BlockHash) -> Result<BlockHash, Self::Error> {
Expand Down Expand Up @@ -1239,10 +1246,6 @@ impl<PersistedState: ChainStore> UpdatableChainstate for ChainState<PersistedSta
self.reorg(new_tip)
}

fn get_acc(&self) -> Stump {
self.acc()
}

fn mark_block_as_valid(&self, block: BlockHash) -> Result<(), BlockchainError> {
let header = self.get_disk_block_header(&block)?;
let height = header.try_height()?;
Expand Down Expand Up @@ -1365,7 +1368,7 @@ impl<PersistedState: ChainStore> UpdatableChainstate for ChainState<PersistedSta
.then(|| inputs.clone());

self.validate_block_no_acc(block, height, inputs)?;
let acc = Consensus::update_acc(&self.acc(), block, height, proof, del_hashes)?;
let acc = Consensus::update_acc(&self.get_acc(None)?, block, height, proof, del_hashes)?;

self.update_view(height, &block.header, acc)?;

Expand Down Expand Up @@ -1766,7 +1769,9 @@ mod test {
== bhash!("45c74beefa2a110715377e023d4260168b4cafbb0891f3b0869aea30867acc87")
{
// This is the block we will reorg to
fork_acc = chain.acc();
fork_acc = chain
.get_acc(None)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Perhaps a get_acc and get_tip_acc would be better to avoid all those None there.

.expect("tip acc should always be present");
}
}

Expand All @@ -1789,7 +1794,7 @@ mod test {

assert_eq!(chain.get_best_block().unwrap(), expected);
assert_eq!(
chain.acc(),
chain.get_acc(None).unwrap(),
fork_acc,
"The accumulator should not change when accepting headers only",
);
Expand Down
6 changes: 6 additions & 0 deletions crates/floresta-chain/src/pruned_utreexo/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ pub enum BlockchainError {
/// The Utreexo proof for this block is invalid.
InvalidUtreexoProof,

/// A block without known utreexo acc was requested
UnknownUtreexoAcc,

/// Error whilst interacting with the [accumulator](rustreexo::stump::Stump).
AccumulatorError(StumpError),

Expand Down Expand Up @@ -83,6 +86,9 @@ impl Display for BlockchainError {
write!(f, "The block contains invalid transaction(s): {e}")
}
Self::InvalidUtreexoProof => write!(f, "The Utreexo proof for this block is invalid"),
Self::UnknownUtreexoAcc => {
write!(f, "A block without known utreexo acc was requested")
}
Self::AccumulatorError(e) => {
write!(f, "Error whilst interacting with the accumulator: {e:?}")
}
Expand Down
14 changes: 4 additions & 10 deletions crates/floresta-chain/src/pruned_utreexo/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ pub trait BlockchainInterface {
/// Returns this chain's params
fn get_params(&self) -> bitcoin::params::Params;

/// Returns our current acc
fn acc(&self) -> Stump;
/// Returns the acc for the given block, on None it will return the tip acc.
fn get_acc(&self, block: Option<BlockHash>) -> Result<Stump, Self::Error>;

/// Returns the amount of [`Work`] associated with a given chain tip
fn get_work(&self, tip: BlockHash) -> Result<Work, Self::Error>;
Expand Down Expand Up @@ -188,8 +188,6 @@ pub trait UpdatableChainstate {
/// This mimics the behaviour of checking every block before this block, and continues
/// from this point
fn mark_chain_as_assumed(&self, acc: Stump, tip: BlockHash) -> Result<bool, BlockchainError>;
/// Returns the current accumulator
fn get_acc(&self) -> Stump;
}

#[derive(Debug, Clone)]
Expand All @@ -205,10 +203,6 @@ impl<T: UpdatableChainstate> UpdatableChainstate for Arc<T> {
T::flush(self)
}

fn get_acc(&self) -> Stump {
T::get_acc(self)
}

fn toggle_ibd(&self, is_ibd: bool) {
T::toggle_ibd(self, is_ibd)
}
Expand Down Expand Up @@ -276,8 +270,8 @@ impl<T: BlockchainInterface> BlockchainInterface for Arc<T> {
T::get_params(self)
}

fn acc(&self) -> Stump {
T::acc(self)
fn get_acc(&self, block: Option<BlockHash>) -> Result<Stump, Self::Error> {
T::get_acc(self, block)
}

fn get_block(&self, hash: &BlockHash) -> Result<Block, Self::Error> {
Expand Down
8 changes: 2 additions & 6 deletions crates/floresta-chain/src/pruned_utreexo/partial_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,10 +273,6 @@ impl UpdatableChainstate for PartialChainState {
self.inner().current_acc.roots.clone()
}

fn get_acc(&self) -> Stump {
self.inner().current_acc.clone()
}

//these are no-ops, you can call them, but they won't do anything

fn flush(&self) -> Result<(), BlockchainError> {
Expand Down Expand Up @@ -333,8 +329,8 @@ impl BlockchainInterface for PartialChainState {
self.inner().chain_params().params
}

fn acc(&self) -> Stump {
self.inner().current_acc.clone()
fn get_acc(&self, _block: Option<BlockHash>) -> Result<Stump, Self::Error> {
Ok(self.inner().current_acc.clone())
}

fn get_height(&self) -> Result<u32, Self::Error> {
Expand Down
63 changes: 54 additions & 9 deletions crates/floresta-node/src/json_rpc/blockchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use corepc_types::v30::GetDeploymentInfo;
use floresta_chain::buried_deployments_for;
use floresta_chain::extensions::HeaderExt;
use floresta_chain::extensions::WorkExt;
use floresta_wire::block_proof::TipProof;
use miniscript::descriptor::checksum;
use serde_json::Value;
use serde_json::json;
Expand All @@ -38,6 +39,8 @@ use super::server::RpcChain;
use super::server::RpcImpl;
use crate::json_rpc::res::GetBlockRes;
use crate::json_rpc::res::RescanConfidence;
use crate::json_rpc::res::VerifyUtxoChainTipInclusionProofRes;
use crate::json_rpc::res::VerifyUtxoChainTipInclusionProofVerbose;

impl<Blockchain: RpcChain> RpcImpl<Blockchain> {
async fn get_block_inner(&self, hash: BlockHash) -> Result<Block, JsonRpcError> {
Expand Down Expand Up @@ -243,15 +246,12 @@ impl<Blockchain: RpcChain> RpcImpl<Blockchain> {
.calculate_chain_work(&self.chain)?
.to_string_hex();
let latest_block_time = latest_header.time;
let leaf_count = self.chain.acc().leaves as u32;
let root_count = self.chain.acc().roots.len() as u32;
let root_hashes = self
.chain
.acc()
.roots
.into_iter()
.map(|r| r.to_string())
.collect();

let acc = self.chain.get_acc(None).map_err(|_| JsonRpcError::Chain)?;
let leaf_count = acc.leaves as u32;
let root_count = acc.roots.len() as u32;

let root_hashes = acc.roots.into_iter().map(|r| r.to_string()).collect();

let validated_blocks = self.chain.get_validation_index().unwrap();

Expand Down Expand Up @@ -769,4 +769,49 @@ impl<Blockchain: RpcChain> RpcImpl<Blockchain> {
.map_err(|e| JsonRpcError::Wallet(e.to_string()))?;
Ok(descriptors)
}

pub(super) fn verify_utxo_chain_tip_inclusion_proof(
&self,
proof: TipProof,
verbosity: u8,
blockhash: Option<BlockHash>,
) -> Result<VerifyUtxoChainTipInclusionProofRes, JsonRpcError> {
// The hash we got querying for the given blockhash
let internal_hash = blockhash.unwrap_or(self.get_best_block_hash()?);
Copy link
Copy Markdown
Contributor

@Micah-Shallom Micah-Shallom May 26, 2026

Choose a reason for hiding this comment

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

rust will compute the argument in unwrap_or before it knows whether blockhash is Some or None...this is bad bcus if get_best_block_hash fails.... the user sees that error even though they provided a hash that would have worked

let internal_hash = match blockhash {
    Some(h) => h,
    None => self.get_best_block_hash()?,
};

or u could use unwrap_or_else

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

get_best_block_hash() should be logically infallible, but the suggestion makes sense.

Ill add about get_best_block_hash() case to our error handling overhaul.


if proof.proved_at_hash != internal_hash {
return Err(JsonRpcError::InvalidProof(format!(
"Possibly stale proof. Got {internal_hash} internally but proof was generated at block {}",
proof.proved_at_hash
)));
};

let stump = self
.chain
.get_acc(blockhash)
.map_err(|_| JsonRpcError::Chain)?;

let is_valid = stump
.verify(&proof.proof, &proof.hashes_proven)
.map_err(|e| JsonRpcError::InvalidProof(format!("Proof verification failed: {e:?}")))?;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Proof error implement Display, no?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

StumpError doesn’t implement std::fmt::Display


match verbosity {
0 => Ok(VerifyUtxoChainTipInclusionProofRes::Zero(is_valid)),
1 => Ok(VerifyUtxoChainTipInclusionProofRes::One(
VerifyUtxoChainTipInclusionProofVerbose {
valid: is_valid,
proved_at_hash: proof.proved_at_hash.to_string(),
targets: proof.proof.targets,
num_proof_hashes: proof.proof.hashes.len(),
proof_hashes: proof.proof.hashes.iter().map(ToString::to_string).collect(),
hashes_proven: proof
.hashes_proven
.iter()
.map(ToString::to_string)
.collect(),
},
)),
_ => Err(JsonRpcError::InvalidVerbosityLevel),
}
}
}
39 changes: 39 additions & 0 deletions crates/floresta-node/src/json_rpc/res.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,41 @@ pub struct RpcError {
#[derive(Debug, Deserialize, Serialize)]
pub struct GetTxOutProof(pub Vec<u8>);

#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
/// Return type for `verifyutxochaintipinclusionproof`, supports both its verbose version and non-verbose.
///
/// The non-verbose version tells whether a proof is valid given the internal utreexo accumulator.
pub enum VerifyUtxoChainTipInclusionProofRes {
/// No verbosity, tells whether a proof is valid.
Zero(bool),

/// Verbosity one, with more detailed information about the proof
One(VerifyUtxoChainTipInclusionProofVerbose),
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We could have a verbosity level wrapper like:

#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum VerbosityLevel<Z: Debug, Serialize, Deserialize , O: Debug, Serialize, Deserialize> {
     Zero(Z),
     One(O),
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Perhaps this suggestion belongs to #1076 and should be applied together with the input type ?

The current implementation follows the same pattern as the other methods that have verbosity, and we could work this suggestion for them all in a single pr.


#[derive(Debug, Serialize, Deserialize)]
/// Return type for `verifyutxochaintipinclusionproof`
pub struct VerifyUtxoChainTipInclusionProofVerbose {
/// Whether this proof is valid
pub valid: bool,

/// The block hash that this proof was proved.
pub proved_at_hash: String,

/// The targets that this proof is proving.
pub targets: Vec<u64>,

/// Hashes count.
pub num_proof_hashes: usize,

/// Proof hashes.
pub proof_hashes: Vec<String>,

/// Which of the hashes were proven.
pub hashes_proven: Vec<String>,
}

#[derive(Debug)]
pub enum JsonRpcError {
/// There was a rescan request but we do not have any addresses in the watch-only wallet.
Expand Down Expand Up @@ -244,6 +279,9 @@ pub enum JsonRpcError {

/// A numeric conversion overflows, e.g., u64 to u32
ConversionOverflow(String),

/// This error is returned when a proof is well-formed but invalid (stale or verification failed)
InvalidProof(String),
}

impl_error_from!(JsonRpcError, MempoolError, MempoolAccept);
Expand Down Expand Up @@ -302,6 +340,7 @@ impl Display for JsonRpcError {
write!(f, "Could not send transaction to mempool due to {e}")
}
JsonRpcError::ConversionOverflow(e) => write!(f, "Numeric conversion overflow: {e}"),
JsonRpcError::InvalidProof(e) => write!(f, "Invalid proof: {e}"),
}
}
}
Expand Down
20 changes: 20 additions & 0 deletions crates/floresta-node/src/json_rpc/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use core::net::SocketAddr;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Instant;

Expand Down Expand Up @@ -35,6 +36,7 @@ use floresta_compact_filters::network_filters::NetworkFilters;
use floresta_watch_only::AddressCache;
use floresta_watch_only::CachedTransaction;
use floresta_watch_only::kv_database::KvDatabase;
use floresta_wire::block_proof::TipProof;
use floresta_wire::node_interface::NodeInterface;
use serde_json::Value;
use serde_json::json;
Expand Down Expand Up @@ -435,6 +437,22 @@ async fn handle_json_rpc_request(
.list_descriptors()
.map(|v| serde_json::to_value(v).unwrap()),

"verifyutxochaintipinclusionproof" => {
let proof_str = get_string(&params, 0, "proof")?;

let proof = TipProof::from_str(&proof_str)
.map_err(|e| JsonRpcError::InvalidProof(e.to_string()))?;

let verbosity: u8 =
get_optional_field(&params, 1, "verbosity", get_numeric)?.unwrap_or(0);

let blockhash: Option<BlockHash> =
get_optional_field(&params, 2, "blockhash", get_hash)?;

state
.verify_utxo_chain_tip_inclusion_proof(proof, verbosity, blockhash)
.map(|v| serde_json::to_value(v).unwrap())
}
_ => {
let error = JsonRpcError::MethodNotFound;
Err(error)
Expand Down Expand Up @@ -465,6 +483,7 @@ fn get_http_error_code(err: &JsonRpcError) -> u16 {
| JsonRpcError::ChainWorkOverflow
| JsonRpcError::ConversionOverflow(_)
| JsonRpcError::MempoolAccept(_)
| JsonRpcError::InvalidProof(_)
| JsonRpcError::Wallet(_) => 400,

// idunnolol
Expand Down Expand Up @@ -506,6 +525,7 @@ fn get_json_rpc_error_code(err: &JsonRpcError) -> i32 {
| JsonRpcError::ChainWorkOverflow
| JsonRpcError::ConversionOverflow(_)
| JsonRpcError::Wallet(_)
| JsonRpcError::InvalidProof(_)
| JsonRpcError::MempoolAccept(_) => -32600,

// server error
Expand Down
Loading