Skip to content

Commit 1f30c7d

Browse files
committed
Add Rpc interface
bitcoin/bitcoin#32297 introduced the RPC interface for `bitcoin-cli`, allowing it to execute RPC commands over IPC. This commit adds the corresponding Cap'n Proto schema and includes a test that queries `getblockcount`.
1 parent 4d5381d commit 1f30c7d

6 files changed

Lines changed: 77 additions & 0 deletions

File tree

build.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ fn main() {
66
.file("capnp/echo.capnp")
77
.file("capnp/init.capnp")
88
.file("capnp/mining.capnp")
9+
.file("capnp/rpc.capnp")
910
.file("capnp/proxy.capnp")
1011
.run()
1112
.unwrap();

capnp/init.capnp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ $Cxx.namespace("ipc::capnp::messages");
1010
using Proxy = import "proxy.capnp";
1111
using Echo = import "echo.capnp";
1212
using Mining = import "mining.capnp";
13+
using Rpc = import "rpc.capnp";
1314

1415
interface Init $Proxy.wrap("interfaces::Init") {
1516
construct @0 (threadMap: Proxy.ThreadMap) -> (threadMap :Proxy.ThreadMap);
1617
makeEcho @1 (context :Proxy.Context) -> (result :Echo.Echo);
1718
makeMiningOld2 @2 () -> ();
1819
makeMining @3 (context :Proxy.Context) -> (result :Mining.Mining);
20+
makeRpc @4 (context :Proxy.Context) -> (result :Rpc.Rpc);
1921
}

capnp/rpc.capnp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright (c) 2025 The Bitcoin Core developers
2+
# Distributed under the MIT software license, see the accompanying
3+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
@0x9c3505dc45e146ac;
6+
7+
using Cxx = import "/capnp/c++.capnp";
8+
$Cxx.namespace("ipc::capnp::messages");
9+
10+
using Common = import "common.capnp";
11+
using Proxy = import "proxy.capnp";
12+
13+
interface Rpc $Proxy.wrap("interfaces::Rpc") {
14+
executeRpc @0 (context :Proxy.Context, request :Text, uri :Text, user :Text) -> (result :Text);
15+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ capnp::generated_code!(pub mod common_capnp);
33
capnp::generated_code!(pub mod echo_capnp);
44
capnp::generated_code!(pub mod proxy_capnp);
55
capnp::generated_code!(pub mod mining_capnp);
6+
capnp::generated_code!(pub mod rpc_capnp);
67

78
pub extern crate capnp;
89
pub extern crate capnp_rpc;

tests/test.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ mod bitcoin_core_wallet_util;
77

88
use bitcoin_core_util::{
99
destroy_template, make_block_template, mempool_tx_count, with_init_client, with_mining_client,
10+
with_rpc_client,
1011
};
1112
use bitcoin_core_wallet_util::{
1213
bitcoin_test_wallet, create_mempool_self_transfer, ensure_wallet_loaded_and_funded,
1314
};
15+
use serde_json::{Value, json};
1416

1517
#[tokio::test]
1618
#[serial_test::parallel]
@@ -40,6 +42,41 @@ async fn integration() {
4042
.await;
4143
}
4244

45+
/// Test the RPC interface by calling `getblockcount`
46+
#[tokio::test]
47+
#[serial_test::parallel]
48+
async fn rpc_query_getblockcount() {
49+
with_rpc_client(|_client, thread, rpc| async move {
50+
let mut execute_rpc_request = rpc.execute_rpc_request();
51+
execute_rpc_request
52+
.get()
53+
.get_context()
54+
.unwrap()
55+
.set_thread(thread.clone());
56+
let j: Value = json!({
57+
"method": "getblockcount",
58+
"params": [],
59+
"id": "test",
60+
"jsonrpc": "2.0"
61+
});
62+
execute_rpc_request.get().set_request(j.to_string());
63+
let exec_rpc_response = execute_rpc_request.send().promise.await.unwrap();
64+
let result = exec_rpc_response
65+
.get()
66+
.unwrap()
67+
.get_result()
68+
.unwrap()
69+
.to_string()
70+
.unwrap();
71+
let v: Value = serde_json::from_str(&result)
72+
.map_err(|e| format!("failed to parse rpc response as JSON: {e}"))
73+
.unwrap();
74+
// Tests are boostrapped with 101 blocks. See `ensure_bootstrap_chain_ready()`
75+
assert_eq!(101, v["result"]);
76+
})
77+
.await;
78+
}
79+
4380
/// Calling the deprecated makeMiningOld2 (@2) should return an error from the
4481
/// server. Cap'n Proto requires sequential ordinals so this placeholder cannot
4582
/// be removed, but the server intentionally rejects it.

tests/util/bitcoin_core.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use bitcoin_capnp_types::{
1111
init_capnp::init,
1212
mining_capnp::{block_template, mining},
1313
proxy_capnp::{thread, thread_map},
14+
rpc_capnp::rpc,
1415
};
1516
use capnp_rpc::{RpcSystem, rpc_twoparty_capnp::Side, twoparty::VatNetwork};
1617
use futures::io::BufReader;
@@ -88,6 +89,18 @@ where
8889
.await;
8990
}
9091

92+
pub async fn with_rpc_client<F, Fut>(f: F)
93+
where
94+
F: FnOnce(init::Client, thread::Client, rpc::Client) -> Fut,
95+
Fut: Future<Output = ()>,
96+
{
97+
with_init_client(|client, thread| async move {
98+
let rpc = make_rpc(&client, &thread).await;
99+
f(client, thread, rpc).await;
100+
})
101+
.await;
102+
}
103+
91104
pub async fn connect_unix_stream(
92105
path: impl AsRef<Path>,
93106
) -> VatNetwork<BufReader<Compat<OwnedReadHalf>>> {
@@ -151,6 +164,14 @@ pub async fn make_mining(init: &init::Client, thread: &thread::Client) -> mining
151164
resp.get().unwrap().get_result().unwrap()
152165
}
153166

167+
/// Obtain a Rpc client from an Init client.
168+
pub async fn make_rpc(init: &init::Client, thread: &thread::Client) -> rpc::Client {
169+
let mut req = init.make_rpc_request();
170+
req.get().get_context().unwrap().set_thread(thread.clone());
171+
let resp = req.send().promise.await.unwrap();
172+
resp.get().unwrap().get_result().unwrap()
173+
}
174+
154175
/// Create a new block template with default options and no cooldown.
155176
///
156177
/// The node must have height > 16. At height <= 16 the BIP34 height push

0 commit comments

Comments
 (0)