Skip to content
Open
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
2 changes: 2 additions & 0 deletions crates/bitcoind_rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ bdk_core = { path = "../core", version = "0.6.1", default-features = false }
bdk_bitcoind_rpc = { path = "." }
bdk_testenv = { path = "../testenv" }
bdk_chain = { path = "../chain" }
serde = "1"
serde_json = "1"
Comment on lines +27 to +28

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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};


[features]
default = ["std"]
Expand Down
92 changes: 90 additions & 2 deletions crates/bitcoind_rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@
//! To only get block updates (exclude mempool transactions), the caller can use
//! [`Emitter::next_block`] until it returns `Ok(None)` (which means the chain tip is reached). A
//! separate method, [`Emitter::mempool`] can be used to emit the whole mempool.
//!
//! # Trust in the RPC connection
//!
//! The recommended practice is to connect only to a `bitcoind` node that **you run and control**.
//! Bitcoin Core's RPC is a privileged, administrative interface rather than a public API. A wallet
//! inherently trusts its chain data source for the validity of blocks and transactions, for
//! confirmation status, and for privacy.
//!
//! If you must reach a node over a network, tunnel the connection (for example over SSH, a VPN, or
//! a Tor hidden service) and never expose RPC to the public internet. As a defense mechanism, the
//! emitter verifies that a fetched transaction's computed txid matches the one requested, but this
//! is not a substitute for connecting to a node you trust.
#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
#![warn(missing_docs)]

Expand Down Expand Up @@ -151,6 +163,12 @@ where
/// `sync_time` is in unix seconds.
///
/// This is the no-std version of [`mempool`](Self::mempool).
///
/// # Errors

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: I would add smthg like this in mempool() so callers also have this information.

/// See [`mempool_at`](Self::mempool_at#errors) for errors.

///
/// Returns [`bitcoincore_rpc::Error::UnexpectedStructure`] if the node returns a transaction
/// body whose computed txid does not match the requested txid. A single mismatch fails the
/// whole poll. Retry on the next call.
pub fn mempool_at(&mut self, sync_time: u64) -> Result<MempoolEvent, bitcoincore_rpc::Error> {
let client = &*self.client;

Expand Down Expand Up @@ -179,6 +197,10 @@ where
let tx = match self.mempool_snapshot.get(&txid) {
Some(tx) => tx.clone(),
None => match client.get_raw_transaction(&txid, None) {
// Reject a tx whose computed txid does not match the requested one.
Ok(tx) if tx.compute_txid() != txid => {
return Some(Err(bitcoincore_rpc::Error::UnexpectedStructure));
}
Ok(tx) => {
let tx = Arc::new(tx);
self.mempool_snapshot.insert(txid, tx.clone());
Expand Down Expand Up @@ -466,8 +488,12 @@ impl BitcoindRpcErrorExt for bitcoincore_rpc::Error {
mod test {
use crate::{Emitter, NO_EXPECTED_MEMPOOL_TXS};
use bdk_chain::local_chain::LocalChain;
use bdk_testenv::{anyhow, TestEnv};
use bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, Txid, WScriptHash};
use bdk_core::CheckPoint;
use bdk_testenv::{anyhow, utils::new_tx, TestEnv};
use bitcoin::{
hashes::Hash, Address, Amount, BlockHash, ScriptBuf, Transaction, Txid, WScriptHash,
};
use bitcoincore_rpc::{Error, RpcApi};
use std::collections::HashSet;

#[test]
Expand Down Expand Up @@ -536,4 +562,66 @@ mod test {

Ok(())
}

//A fetched mempool tx whose body does not match the requested txid is rejected.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

#[test]
fn mismatched_tx_body_is_rejected_and_not_cached() {
struct LyingNode {
announced: Txid,
tip_hash: BlockHash,
served: Transaction,
}

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())
}
}
Comment on lines +575 to +599

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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


let served = new_tx(0);
let announced = new_tx(1).compute_txid();
assert_ne!(
announced,
served.compute_txid(),
"announced txid must differ from the served body",
);

let node = LyingNode {
announced,
tip_hash: BlockHash::from_byte_array([2u8; 32]),
served,
};
let last_cp = CheckPoint::new(0, BlockHash::all_zeros());
let mut emitter = Emitter::new(&node, last_cp, 0, NO_EXPECTED_MEMPOOL_TXS);

let result = emitter.mempool_at(0);
assert!(
matches!(result, Err(Error::UnexpectedStructure)),
"mismatched tx body must be rejected, got {result:?}",
);
assert!(
!emitter.mempool_snapshot.contains_key(&announced),
"rejected tx must not be cached under the announced txid",
);
}
}
Loading