Skip to content

[RPCSAGA] verifyutxochaintipinclusionproof rpc#1085

Open
jaoleal wants to merge 5 commits into
getfloresta:masterfrom
jaoleal:rpcsaga_verifyutxochaintipinclusionproof
Open

[RPCSAGA] verifyutxochaintipinclusionproof rpc#1085
jaoleal wants to merge 5 commits into
getfloresta:masterfrom
jaoleal:rpcsaga_verifyutxochaintipinclusionproof

Conversation

@jaoleal
Copy link
Copy Markdown
Member

@jaoleal jaoleal commented May 25, 2026

Description and Notes

Superseeds #819.

Implement Utreexod's verifyutxochaintipinclusionproof

To implement the feature of specifying a blockhash to control the block which we should verify the proof against i had to make some changes on our blockchain backend only to expose the option.

We had three main external call points to fetch the acc from the blockchain, always the tip, BlockchainInterface::acc(), ChainState::acc() and UpdatableChainstate::get_acc(), now they are centralized into BlockChainInterface::get_acc().

This PR also includes extensive tests, unit tests for the new request structure parsing and integration tests for the rpc itself.

@jaoleal jaoleal requested a review from moisesPompilio May 25, 2026 13:46
@jaoleal jaoleal self-assigned this May 25, 2026
@jaoleal jaoleal added this to Floresta May 25, 2026
@github-project-automation github-project-automation Bot moved this to Backlog in Floresta May 25, 2026
@jaoleal jaoleal added this to the Q2/2026 milestone May 25, 2026
@jaoleal jaoleal moved this from Backlog to Needs review in Floresta May 25, 2026
@jaoleal jaoleal force-pushed the rpcsaga_verifyutxochaintipinclusionproof branch from b03c945 to 854b248 Compare May 25, 2026 13:59
@luisschwab
Copy link
Copy Markdown
Member

Needs rebase

@Davidson-Souza
Copy link
Copy Markdown
Member

NACK on renaming. This RPC will be used by integrations that wishes to have utreexo support, we shouldn't make utreexo support harder by fracturing the [pretty small] ecosystem of utreexo nodes.

@jaoleal jaoleal force-pushed the rpcsaga_verifyutxochaintipinclusionproof branch from 854b248 to 0116df4 Compare May 25, 2026 17:47
@jaoleal
Copy link
Copy Markdown
Member Author

jaoleal commented May 25, 2026

NACK on renaming. This RPC will be used by integrations that wishes to have utreexo support, we shouldn't make utreexo support harder by fracturing the [pretty small] ecosystem of utreexo nodes.

makes sense if the idea is to stick with the reference name but, the point is that verifyutxochaintipinclusionproof is bad naming, the original utreexod command also accepts Blockhash targeting so no chaintip and, the utxo and inclusion is redundant.

Ive been thinking on a alias feature for the rpc, with it i could fix this naming and build other interesting shortcuts without raising inconsistency in the common interface

@jaoleal jaoleal force-pushed the rpcsaga_verifyutxochaintipinclusionproof branch 2 times, most recently from a0f981f to 1ea4355 Compare May 25, 2026 18:54
@jaoleal
Copy link
Copy Markdown
Member Author

jaoleal commented May 25, 2026

Rebased.
Renamed the rpc to verifyutxochaintipinclusionproof.
Fixed a typo in the docs.

@jaoleal jaoleal requested a review from Davidson-Souza May 25, 2026 18:57
@Davidson-Souza Davidson-Souza added the ecosystem support Enable interoperability, compatibility and practical integration with the broader Bitcoin ecosystem label May 25, 2026
Comment thread crates/floresta-chain/src/pruned_utreexo/chain_state.rs
Comment thread crates/floresta-chain/src/pruned_utreexo/error.rs Outdated
Comment thread crates/floresta-node/src/json_rpc/blockchain.rs Outdated
Comment thread crates/floresta-wire/src/p2p_wire/node/running_ctx.rs

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

Comment thread crates/floresta-node/src/json_rpc/request.rs Outdated
}
}

impl Decodable for TipProof {
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.

Fuzz for this and the FromStr above.

Comment on lines +236 to +242
// Hashes proven (u32 LE count + 32-byte hashes)
let num_proven = u32::consensus_decode(reader)? as usize;
if num_proven > MAX_INPUTS_PER_BLOCK {
return Err(bitcoin::consensus::encode::Error::ParseFailed(
"Too many proven hashes",
));
}
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.

Why didn't you use read_bounded_len?

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.

The value im trying to extract is a u32, AFAIK read_bounded_len will only work for VarInt

Comment thread crates/floresta-node/src/json_rpc/request.rs Outdated
Comment thread crates/floresta-node/src/json_rpc/request.rs Outdated
@jaoleal jaoleal changed the title [RPCSAGA] verifyproof rpc [RPCSAGA] verifyutxochaintipinclusionproof rpc May 26, 2026
@jaoleal jaoleal force-pushed the rpcsaga_verifyutxochaintipinclusionproof branch 3 times, most recently from b6479f0 to 1454d31 Compare May 26, 2026 17:51
@jaoleal jaoleal requested a review from Davidson-Souza May 26, 2026 17:53
@jaoleal
Copy link
Copy Markdown
Member Author

jaoleal commented May 26, 2026

Applied @Davidson-Souza suggestions.

  • Removed unwraps where applies.
  • whitespace on TipProof
  • use get_block_height
  • Fix MAX_PROOF_SIZE_BYTES and reflect changes on integration tests.
  • Still working on the fuzz entries.

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.

@jaoleal jaoleal force-pushed the rpcsaga_verifyutxochaintipinclusionproof branch 2 times, most recently from 820a8e8 to ec080e5 Compare May 27, 2026 18:58
Comment on lines +9 to +11
fuzz_target!(|data: &[u8]| {
let _ = deserialize::<TipProof>(data);
});
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.

Try to rtt this

@jaoleal jaoleal force-pushed the rpcsaga_verifyutxochaintipinclusionproof branch from ec080e5 to 4461dc9 Compare May 27, 2026 19:50
@jaoleal
Copy link
Copy Markdown
Member Author

jaoleal commented May 27, 2026

Done @Davidson-Souza

@jaoleal jaoleal requested a review from Davidson-Souza May 27, 2026 19:54
@jaoleal jaoleal requested a review from Micah-Shallom May 27, 2026 19:54
jaoleal added 5 commits May 29, 2026 11:35
Rename `acc()` to `get_acc(block: Option<BlockHash>)` in the
`BlockchainInterface` trait, enabling callers to retrieve the
accumulator state for a specific historical block. When `None` is
passed, the current tip accumulator is returned (preserving the
previous behavior).

- Implement historical lookup in `ChainState` via
  `get_disk_block_header` + `get_roots_for_block`
- Remove the duplicate `get_acc()` from `UpdatableChainstate`
- Update all `acc()` call sites to use BlockchainInterface::get_acc().
Implement verification of Utreexo accumulator inclusion proofs for
chain tip UTXOs. Accepts hex-encoded proofs from utreexod's
proveutxochaintipinclusion RPC and validates them against the
internal Utreexo accumulator.

Includes:
- TipProof type with consensus decoding and hex parsing
- Verbosity 0: returns boolean validation result
- Verbosity 1: returns detailed proof information
- Stale proof detection (chain tip mismatch)
- Optional blockhash parameter for historical verification
- DoS protection (4MB max proof size)
- Comprehensive parsing unit tests
- RPC documentation
Add the `verifyutxochaintipinclusionproof` subcommand and its RPC client method to
`FlorestaRPC`. Accepts a hex proof string, optional verbosity
level (0 or 1), and optional blockhash parameter.
Comprehensive functional test suite for the verifyutxochaintipinclusionproof RPC:
- Valid proof with verbosity 0 and 1
- Explicit blockhash verification
- Stale proof with blockhash fallback
- Invalid hex, malformed proof, oversized proof
- Invalid verbosity level
…uzz targets

Add two fuzz targets for the TipProof type:
- tip_proof_des: raw bytes deserialization via consensus_decode
- tip_proof_from_str: hex string parsing via FromStr
- tip_proof_roundtrip: roundtrip symmetry
@jaoleal jaoleal force-pushed the rpcsaga_verifyutxochaintipinclusionproof branch from 4461dc9 to 0d995fc Compare May 29, 2026 14:36
@jaoleal
Copy link
Copy Markdown
Member Author

jaoleal commented May 29, 2026

Rebased

// 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.


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.

@@ -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.


/// 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.

///
/// <https://bitcoin.stackexchange.com/questions/85752/maximum-number-of-inputs-per-transaction>
const MAX_INPUTS_PER_BLOCK: usize = 24_386;
pub const MAX_INPUTS_PER_BLOCK: usize = 24_386;
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.

Don't think this needs to be public

Comment on lines +73 to +86
#[derive(Debug, Clone, PartialEq, Eq)]
/// A chain-tip inclusion proof as serialized by utreexod.
///
/// Responses to `proveutxochaintipinclusion` utreexod RPC.
pub struct TipProof {
/// The block hash at which this proof was generated.
pub proved_at_hash: BlockHash,

/// The Utreexo accumulator proof (targets + sibling hashes).
pub proof: Proof,

/// The raw leaf hashes that were proven.
pub hashes_proven: Vec<BitcoinNodeHash>,
}
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.

Think I already mentioned this, but this seems offtopic for wire. This struct should be in rpc

}

fuzz_target!(|data: &[u8]| {
if let Ok(inp) = Inputs::arbitrary(&mut Unstructured::new(data)) {
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.

This isn't really what I've suggested. Look at the *rtt targets we already have.

@Davidson-Souza
Copy link
Copy Markdown
Member

Davidson-Souza commented Jun 5, 2026

target/debug/floresta-cli -n signet verifyutxochaintipinclusionproof 401fceaf938fd4235fdea312efe90a425831d261ceeb72345e157d201400000002ff9c30f60300000080ff268cfd00000000e008cedbb4415f242d45e04ae1f6f8264b9e16ab71879dee04a3838c53f27f2538f8bf781c81f44b98e169f4ea60667932df76280583780cf6afb3fbdac2c76cfc88725fd9c3ac26289fc6fcfe6e48bc9bf95d6e75cadd0c8aaf8e707f8e7cf2ede2ca309cef93015c47d15218d1857efabadc4ddf69616df09a2f09a68fd885a876b75e088751ceff203a7a9d6fe1f5b36259618992379b02aa69ca3a2365aeefe95d1c5e5fea603271e306cf2cb06847ba977946af6d3a60bd5c72e6668dd9eee008f225077f59e79b3511768157a92afef3c848e762bbd562cba1c338ffc21748911e68e98e10ebf67263013ae29dff1c381d7b8d76053aa2e9fb8ed3711162e40200000020f485fc2303e3c17789059bb6661b5430b1805e48d745474d46c5adea74790ca6fdc8cb530d2d831b31831c84be8eed0deb908c8bbdb0199aa503ff15fdc2a9 1 00000011924dba80c9983c7a0f292599e583f9cfb113d2f7c18616278cf9bb0b

Error: JsonRpc returned an error RPC error response: RpcError { code: -32600, message: "Invalid proof: Possibly stale proof. Got 00000011924dba80c9983c7a0f292599e583f9cfb113d2f7c18616278cf9bb0b internally but proof was generated at block 00000014207d155e3472ebce61d23158420ae9ef12a3de5f23d48f93afce1f40", data: None }

This proof was generated by utreexod.

Edit: It looks like the problem are coinbase transactions, for some reason.

@jaoleal
Copy link
Copy Markdown
Member Author

jaoleal commented Jun 5, 2026

Can you share the command that you used to generate the proof ?

@Davidson-Souza
Copy link
Copy Markdown
Member

Can you share the command that you used to generate the proof ?

utreexoctl --signet proveutxochaintipinclusion '["4d584b391d27ee951fafe64827724d7af0f3f589bfecd7011d5f9c8fabf0357f", "914523bb6e0a5b53ea29315d87ed43fde1826e1a44b83ce5fa863d2fd55f5a33"]' '[2, 0]' 0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ecosystem support Enable interoperability, compatibility and practical integration with the broader Bitcoin ecosystem

Projects

Status: Needs review

Development

Successfully merging this pull request may close these issues.

4 participants