Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2316 +/- ##
=======================================
Coverage 78.84% 78.85%
=======================================
Files 31 31
Lines 6060 6062 +2
Branches 288 289 +1
=======================================
+ Hits 4778 4780 +2
Misses 1203 1203
Partials 79 79
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
The MSRV job is failing because of a transitive dependency Happy to open a tiny PR for the pin if that's preferred. |
- Add `tx.compute_txid() == txid` check to validate that fetched raw mempool transaction hashes against their requested txid before caching in Emitter::mempool_at. Returns `Error::UnexpectedStructure` for a mismatch. - Add a regression test for the above check. - Document that the crate assumes a trusted `bitcoind` connection.
2141610 to
3fba250
Compare
Dmenec
left a comment
There was a problem hiding this comment.
Approach ACK
I was thinking if the validation should retry (maybe 3 times at most) when failing so you don't throw the whole poll, but I think in this specific scenario it would be more annoying than useful.
Not sure if compute_txid cover the witness, so maybe a node could still serve a different witness for the same txid? If not, maybe worth a word in the doc so it doesn't read as the whole tx being verified.
Also, checking the code I noticed that next_block fetches blocks by hash but never checks the returned block actually hashes to what was requested:
bdk/crates/bitcoind_rpc/src/lib.rs
Lines 282 to 291 in 3fba250
I think in the same way we should check the block hash, merkle root and witness commitment. Checking only the hash isn't enough, since it only covers the header, so a node could serve the genuine header with forged txs. I saw your benchmarks in #2283 so, as much, it should be a few ms per block. Given the scope was narrowed there, maybe better as a follow-up?
Also, just a few nits I've seen through
| Ok(()) | ||
| } | ||
|
|
||
| //A fetched mempool tx whose body does not match the requested txid is rejected. |
There was a problem hiding this comment.
| //A fetched mempool tx whose body does not match the requested txid is rejected. | |
| // A fetched mempool tx whose body does not match the requested txid is rejected. |
| /// | ||
| /// This is the no-std version of [`mempool`](Self::mempool). | ||
| /// | ||
| /// # Errors |
There was a problem hiding this comment.
nit: I would add smthg like this in mempool() so callers also have this information.
/// See [`mempool_at`](Self::mempool_at#errors) for errors.
| impl RpcApi for LyingNode { | ||
| fn call<T: for<'a> serde::de::Deserialize<'a>>( | ||
| &self, | ||
| _cmd: &str, | ||
| _args: &[serde_json::Value], | ||
| ) -> Result<T, Error> { | ||
| unreachable!() | ||
| } | ||
| fn get_block_count(&self) -> Result<u64, Error> { | ||
| Ok(100) | ||
| } | ||
| fn get_block_hash(&self, _height: u64) -> Result<BlockHash, Error> { | ||
| Ok(self.tip_hash) | ||
| } | ||
| fn get_raw_mempool(&self) -> Result<Vec<Txid>, Error> { | ||
| Ok(vec![self.announced]) | ||
| } | ||
| fn get_raw_transaction( | ||
| &self, | ||
| _txid: &Txid, | ||
| _block_hash: Option<&BlockHash>, | ||
| ) -> Result<Transaction, Error> { | ||
| Ok(self.served.clone()) | ||
| } | ||
| } |
There was a problem hiding this comment.
nit: since all RpcApi methods default to call, the mock could implement only call and match on the command name. Reads like a table and gets rid of the unreachable!()
fn call<T: for<'a> serde::de::Deserialize<'a>>(
&self,
cmd: &str,
_args: &[serde_json::Value],
) -> Result<T, Error> {
let value = match cmd {
"getblockcount" => serde_json::json!(100),
"getblockhash" => serde_json::to_value(self.tip_hash)?,
"getrawmempool" => serde_json::to_value([self.announced])?,
"getrawtransaction" => serialize_hex(&self.served).into(),
_ => unimplemented!("unexpected RPC call {cmd}"),
};
Ok(serde_json::from_value(value)?)
}should need use bitcoin::consensus::encode::serialize_hex; in the test module
| serde = "1" | ||
| serde_json = "1" |
There was a problem hiding this comment.
nit: these deps aren't needed, bitcoincore_rpc already re-exports both. In the test module this should be enough
use bitcoincore_rpc::jsonrpc::{serde, serde_json};
Description
This PR adds a check to validate fetched raw mempool transaction hashes against the requested txid before caching in Emitter::mempool_at, preventing cache poisoning and state corruption from unverified RPC responses. It also adds a module level documentation that
bdk_bitcoind_rpcassumes connection to a trustedbitcoindnode.This addresses only the missing txid-validation part of #2282, following discussions on #2283.
Notes to the reviewers
tx.compute_txid() != txidreturns the existingError::UnexpectedStructureon mismatch, so it is non-breaking.Changelog notice
Emitter::mempool/mempool_at: verify that a fetched transaction's computed txid matches the requested txid before caching, rejecting a mismatch withError::UnexpectedStructure.Checklists
All Submissions:
New Features:
Bugfixes: