From 68cb1b3e8b52315ca415d2521054cfb45d1b7621 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:44:33 +0000 Subject: [PATCH 1/5] Initial plan From 3eb39a6e0f558713dca545ccc838043fd051484c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:49:50 +0000 Subject: [PATCH 2/5] Add mining example Co-authored-by: Sjors <10217+Sjors@users.noreply.github.com> --- examples/mining.rs | 176 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 examples/mining.rs diff --git a/examples/mining.rs b/examples/mining.rs new file mode 100644 index 0000000..40c644d --- /dev/null +++ b/examples/mining.rs @@ -0,0 +1,176 @@ +//! Connect to a running Bitcoin Core node via IPC and exercise the +//! Mining interface: query chain state, create a block template, and +//! inspect its contents. +//! +//! # Prerequisites +//! +//! Start Bitcoin Core in regtest mode with IPC enabled and generate +//! at least 17 blocks so that `createNewBlock` succeeds: +//! +//! ```sh +//! bitcoind -regtest -ipcbind=unix +//! bitcoin-cli -regtest createwallet miner +//! bitcoin-cli -regtest -generate 17 +//! ``` +//! +//! # Usage +//! +//! ```sh +//! cargo run --example mining +//! ``` + +use std::path::PathBuf; + +use bitcoin_capnp_types::{ + init_capnp::init, + mining_capnp::{self, block_template, mining}, + proxy_capnp::{thread, thread_map}, +}; +use capnp_rpc::{RpcSystem, rpc_twoparty_capnp::Side, twoparty::VatNetwork}; +use futures::io::BufReader; +use tokio::net::UnixStream; +use tokio::task::LocalSet; +use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt}; + +fn unix_socket_path() -> PathBuf { + let home_dir = std::env::var("HOME") + .expect("HOME not set") + .parse::() + .unwrap(); + let bitcoin_dir = if cfg!(target_os = "macos") { + home_dir + .join("Library") + .join("Application Support") + .join("Bitcoin") + } else { + home_dir.join(".bitcoin") + }; + bitcoin_dir.join("regtest").join("node.sock") +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let path = unix_socket_path(); + println!("Connecting to {}", path.display()); + + // Connect to the Unix socket exposed by Bitcoin Core. + let stream = UnixStream::connect(&path).await?; + let (reader, writer) = stream.into_split(); + let rpc_network = VatNetwork::new( + BufReader::new(reader.compat()), + futures::io::BufWriter::new(writer.compat_write()), + Side::Client, + Default::default(), + ); + + let mut rpc_system = RpcSystem::new(Box::new(rpc_network), None); + let client: init::Client = rpc_system.bootstrap(Side::Server); + + LocalSet::new() + .run_until(async move { + tokio::task::spawn_local(rpc_system); + + // Bootstrap: obtain a thread handle for RPC context. + let construct_resp = client.construct_request().send().promise.await?; + let thread_map: thread_map::Client = + construct_resp.get()?.get_thread_map()?; + let thread_resp = thread_map.make_thread_request().send().promise.await?; + let thread: thread::Client = thread_resp.get()?.get_result()?; + + // Create a Mining client. + let mut req = client.make_mining_request(); + req.get().get_context()?.set_thread(thread.clone()); + let resp = req.send().promise.await?; + let mining: mining::Client = resp.get()?.get_result()?; + + // ── Chain queries ─────────────────────────────────────── + + // isTestChain + let mut req = mining.is_test_chain_request(); + req.get().get_context()?.set_thread(thread.clone()); + let resp = req.send().promise.await?; + println!("Test chain: {}", resp.get()?.get_result()); + + // isInitialBlockDownload + let mut req = mining.is_initial_block_download_request(); + req.get().get_context()?.set_thread(thread.clone()); + let resp = req.send().promise.await?; + println!("Initial block download: {}", resp.get()?.get_result()); + + // getTip + let mut req = mining.get_tip_request(); + req.get().get_context()?.set_thread(thread.clone()); + let resp = req.send().promise.await?; + let results = resp.get()?; + if results.get_has_result() { + let tip = results.get_result()?; + let tip_hash: Vec = tip.get_hash()?.to_vec(); + println!( + "Tip: height={} hash={}", + tip.get_height(), + tip_hash + .iter() + .rev() + .map(|b| format!("{b:02x}")) + .collect::() + ); + } + + // ── Block template ────────────────────────────────────── + + // createNewBlock + let mut req = mining.create_new_block_request(); + req.get().get_context()?.set_thread(thread.clone()); + req.get().set_cooldown(false); + let resp = req.send().promise.await?; + let template: block_template::Client = resp.get()?.get_result()?; + + // getBlockHeader — 80-byte raw header. + let mut req = template.get_block_header_request(); + req.get().get_context()?.set_thread(thread.clone()); + let resp = req.send().promise.await?; + let header = resp.get()?.get_result()?; + println!( + "Block header ({} bytes): {}", + header.len(), + header.iter().map(|b| format!("{b:02x}")).collect::() + ); + + // getTxFees + let mut req = template.get_tx_fees_request(); + req.get().get_context()?.set_thread(thread.clone()); + let resp = req.send().promise.await?; + let fees = resp.get()?.get_result()?; + let total_fees: i64 = fees.iter().sum(); + println!("Transactions: {} (total fees: {total_fees} sat)", fees.len()); + + // getCoinbaseTx + let mut req = template.get_coinbase_tx_request(); + req.get().get_context()?.set_thread(thread.clone()); + let resp = req.send().promise.await?; + let coinbase = resp.get()?.get_result()?; + println!( + "Block reward remaining: {} sat (max: {} sat)", + coinbase.get_block_reward_remaining(), + mining_capnp::MAX_MONEY + ); + + // getCoinbaseMerklePath + let mut req = template.get_coinbase_merkle_path_request(); + req.get().get_context()?.set_thread(thread.clone()); + let resp = req.send().promise.await?; + let merkle_path = resp.get()?.get_result()?; + println!("Coinbase merkle path: {} hash(es)", merkle_path.len() / 32); + + // Clean up the template. + let mut req = template.destroy_request(); + req.get().get_context()?.set_thread(thread.clone()); + req.send().promise.await?; + println!("Template destroyed"); + + Ok::<(), Box>(()) + }) + .await?; + + Ok(()) +} From ec1ad117c40c9668e7f6ede6f573c2ebc513820d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:50:53 +0000 Subject: [PATCH 3/5] Add CI step to run mining example Co-authored-by: Sjors <10217+Sjors@users.noreply.github.com> --- .github/workflows/ci.yml | 2 ++ examples/mining.rs | 5 +---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a38418..7687593 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,3 +37,5 @@ jobs: run: cd bitcoin && ./build/bin/bitcoin rpc -regtest -rpcwait generatetodescriptor 101 "raw(51)" - name: Run Test Suite run: cargo test + - name: Run Example + run: cargo run --example mining diff --git a/examples/mining.rs b/examples/mining.rs index 40c644d..930e5bd 100644 --- a/examples/mining.rs +++ b/examples/mining.rs @@ -33,10 +33,7 @@ use tokio::task::LocalSet; use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt}; fn unix_socket_path() -> PathBuf { - let home_dir = std::env::var("HOME") - .expect("HOME not set") - .parse::() - .unwrap(); + let home_dir = PathBuf::from(std::env::var("HOME").expect("HOME not set")); let bitcoin_dir = if cfg!(target_os = "macos") { home_dir .join("Library") From 993402428a192ef7f1eb4032fd7de7ac76371d6f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:51:24 +0000 Subject: [PATCH 4/5] Add waitNext and interruptWait to mining example Co-authored-by: Sjors <10217+Sjors@users.noreply.github.com> --- examples/mining.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/examples/mining.rs b/examples/mining.rs index 930e5bd..9bc8c34 100644 --- a/examples/mining.rs +++ b/examples/mining.rs @@ -159,6 +159,30 @@ async fn main() -> Result<(), Box> { let merkle_path = resp.get()?.get_result()?; println!("Coinbase merkle path: {} hash(es)", merkle_path.len() / 32); + // waitNext — wait briefly for mempool updates (short timeout). + println!("Waiting for mempool updates..."); + let mut req = template.wait_next_request(); + req.get().get_context()?.set_thread(thread.clone()); + { + let mut opts = req.get().init_options(); + opts.set_timeout(100.0); // 100 ms + opts.set_fee_threshold(mining_capnp::MAX_MONEY); + } + let resp = req.send().promise.await?; + if resp.get()?.has_result() { + println!("Updated template available"); + } else { + println!("No mempool updates within timeout"); + } + + // interruptWait — signal the template to stop waiting. + template + .interrupt_wait_request() + .send() + .promise + .await?; + println!("Interrupted wait"); + // Clean up the template. let mut req = template.destroy_request(); req.get().get_context()?.set_thread(thread.clone()); From 0c4aa6be8d317a750d47fd112c89587461c9b64b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:52:16 +0000 Subject: [PATCH 5/5] Align example docs with README (use generatetodescriptor) Co-authored-by: Sjors <10217+Sjors@users.noreply.github.com> --- examples/mining.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/mining.rs b/examples/mining.rs index 9bc8c34..194cd90 100644 --- a/examples/mining.rs +++ b/examples/mining.rs @@ -9,8 +9,7 @@ //! //! ```sh //! bitcoind -regtest -ipcbind=unix -//! bitcoin-cli -regtest createwallet miner -//! bitcoin-cli -regtest -generate 17 +//! bitcoin-cli -regtest generatetodescriptor 17 "raw(51)" //! ``` //! //! # Usage