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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ capnp-rpc = "0.25.0"
capnpc = "0.25.0"

[dev-dependencies]
bitcoin = { git = "https://github.com/rust-bitcoin/rust-bitcoin", package = "bitcoin", tag = "bitcoin-0.33.0-beta" }
bitcoin-primitives = { git = "https://github.com/rust-bitcoin/rust-bitcoin", package = "bitcoin-primitives", tag = "bitcoin-0.33.0-beta" }
encoding = { git = "https://github.com/rust-bitcoin/rust-bitcoin", package = "bitcoin-consensus-encoding", tag = "bitcoin-0.33.0-beta" }
futures = "0.3.0"
Expand Down
1 change: 1 addition & 0 deletions capnp/mining.capnp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface Mining $Proxy.wrap("interfaces::Mining") {
createNewBlock @4 (context :Proxy.Context, options: BlockCreateOptions, cooldown: Bool = true) -> (result: BlockTemplate);
checkBlock @5 (context :Proxy.Context, block: Data, options: BlockCheckOptions) -> (reason: Text, debug: Text, result: Bool);
interrupt @6 () -> ();
submitBlock @7 (context :Proxy.Context, block: Data) -> (reason: Text, debug: Text, result: Bool);
}

interface BlockTemplate $Proxy.wrap("interfaces::BlockTemplate") {
Expand Down
264 changes: 236 additions & 28 deletions tests/test.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,47 @@
use bitcoin_capnp_types::mining_capnp;
use bitcoin_capnp_types::{mining_capnp, proxy_capnp::thread};

#[path = "util/bitcoin_core.rs"]
mod bitcoin_core_util;
#[path = "util/bitcoin_core_wallet.rs"]
mod bitcoin_core_wallet_util;
mod util;

use bitcoin_core_util::{
use util::bitcoin_core::{
destroy_template, make_block_template, mempool_tx_count, with_init_client, with_mining_client,
};
use bitcoin_core_wallet_util::{
use util::bitcoin_core_wallet::{
bitcoin_test_wallet, create_mempool_self_transfer, ensure_wallet_loaded_and_funded,
};
use util::block::{block_solution, block_with_pow};

struct SubmitBlockOutcome {
accepted: bool,
reason: String,
debug: String,
}

async fn submit_block(
mining: &mining_capnp::mining::Client,
thread: &thread::Client,
block: &[u8],
) -> SubmitBlockOutcome {
let mut req = mining.submit_block_request();
req.get().get_context().unwrap().set_thread(thread.clone());
req.get().set_block(block);
let resp = req.send().promise.await.unwrap();
let results = resp.get().unwrap();
SubmitBlockOutcome {
accepted: results.get_result(),
reason: results.get_reason().unwrap().to_string().unwrap(),
debug: results.get_debug().unwrap().to_string().unwrap(),
}
}

async fn get_template_block(
template: &mining_capnp::block_template::Client,
thread: &thread::Client,
) -> Vec<u8> {
let mut req = template.get_block_request();
req.get().get_context().unwrap().set_thread(thread.clone());
let resp = req.send().promise.await.unwrap();
resp.get().unwrap().get_result().unwrap().to_vec()
}

#[tokio::test]
#[serial_test::parallel]
Expand Down Expand Up @@ -84,7 +115,10 @@ async fn mining_basic_queries() {
let mut req = mining.is_initial_block_download_request();
req.get().get_context().unwrap().set_thread(thread.clone());
let resp = req.send().promise.await.unwrap();
let _ibd: bool = resp.get().unwrap().get_result();
let _ibd = resp
.get()
.expect("isInitialBlockDownload response should decode")
.get_result();

// getTip
let mut req = mining.get_tip_request();
Expand Down Expand Up @@ -154,13 +188,21 @@ async fn mining_block_template_inspection() {
let mut req = template.get_tx_fees_request();
req.get().get_context().unwrap().set_thread(thread.clone());
let resp = req.send().promise.await.unwrap();
let _fees = resp.get().unwrap().get_result().unwrap();
let _fees = resp
.get()
.expect("getTxFees response should decode")
.get_result()
.expect("getTxFees response should contain fees");

// getTxSigops
let mut req = template.get_tx_sigops_request();
req.get().get_context().unwrap().set_thread(thread.clone());
let resp = req.send().promise.await.unwrap();
let _sigops = resp.get().unwrap().get_result().unwrap();
let _sigops = resp
.get()
.expect("getTxSigops response should decode")
.get_result()
.expect("getTxSigops response should contain sigops");

// getCoinbaseTx — inspect every CoinbaseTx field
let mut req = template.get_coinbase_tx_request();
Expand All @@ -174,17 +216,25 @@ async fn mining_block_template_inspection() {
!script_sig_prefix.is_empty(),
"scriptSigPrefix must contain at least the block height"
);
let _witness = coinbase.get_witness().unwrap();
let _witness = coinbase
.get_witness()
.expect("coinbase witness should decode");
let reward: i64 = coinbase.get_block_reward_remaining();
assert!(reward > 0 && reward <= mining_capnp::MAX_MONEY);
let _required_outputs = coinbase.get_required_outputs().unwrap();
let _required_outputs = coinbase
.get_required_outputs()
.expect("coinbase required outputs should decode");
let _lock_time: u32 = coinbase.get_lock_time();

// getCoinbaseMerklePath
let mut req = template.get_coinbase_merkle_path_request();
req.get().get_context().unwrap().set_thread(thread.clone());
let resp = req.send().promise.await.unwrap();
let _merkle_path = resp.get().unwrap().get_result().unwrap();
let _merkle_path = resp
.get()
.expect("getCoinbaseMerklePath response should decode")
.get_result()
.expect("getCoinbaseMerklePath response should contain a merkle path");

destroy_template(&template, &thread).await;
})
Expand All @@ -208,15 +258,19 @@ async fn mining_block_template_lifecycle() {
opts.set_fee_threshold(mining_capnp::MAX_MONEY);
}
let resp = req.send().promise.await.unwrap();
let _has_next = resp.get().unwrap().has_result();
let results = resp.get().expect("waitNext response should decode");
assert!(
!results.has_result(),
"waitNext should time out without a new template"
);

// interruptWait — should not crash.
template
.interrupt_wait_request()
.send()
.promise
.await
.unwrap();
.expect("interruptWait should not fail");

// submitSolution — garbage coinbase should be rejected.
// This mutates the template, so we do it right before destroy.
Expand All @@ -235,6 +289,158 @@ async fn mining_block_template_lifecycle() {
.await;
}

/// submitSolution with a solved template block should be accepted.
#[tokio::test]
#[serial_test::serial]
async fn mining_block_template_submit_solution_resolved_and_duplicate() {
with_mining_client(|_client, thread, mining| async move {
let template = make_block_template(&mining, &thread).await;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

IIUC this will generate a regtest block, for which the blockhash will be essentially pseudorandom

at regtest genesis difficulty, any random sha256d hash has 50% chance of passing the PoW diff target

so this test will fail non-determinstically for reasons that have nothing to do with the correctness of the underlying submitBlock implementation that we're trying to validate

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is now deterministic.


let block = get_template_block(&template, &thread).await;
let block = block_with_pow(&block, true);
let solution = block_solution(&block);

let mut req = template.submit_solution_request();
{
let mut params = req.get();
params.set_version(solution.version);
params.set_timestamp(solution.timestamp);
params.set_nonce(solution.nonce);
params.set_coinbase(&solution.coinbase);
params.get_context().unwrap().set_thread(thread.clone());
}
let resp = req.send().promise.await.unwrap();
assert!(
resp.get().unwrap().get_result(),
"solved template solution must be accepted"
);

// A duplicate block currently returns true. bitcoin/bitcoin#34672 may
// change this to false, and this coverage should catch that change.
let mut req = template.submit_solution_request();
{
let mut params = req.get();
params.set_version(solution.version);
params.set_timestamp(solution.timestamp);
params.set_nonce(solution.nonce);
params.set_coinbase(&solution.coinbase);
params.get_context().unwrap().set_thread(thread.clone());
}
let resp = req.send().promise.await.unwrap();
assert!(
resp.get().unwrap().get_result(),
"duplicate template solution currently returns true"
);

destroy_template(&template, &thread).await;
})
.await;
}

/// submitBlock with insufficient PoW should be rejected.
#[tokio::test]
#[serial_test::serial]
async fn mining_submit_block_insufficient_pow() {
with_mining_client(|_client, thread, mining| async move {
let template = make_block_template(&mining, &thread).await;

let block = get_template_block(&template, &thread).await;
let block = block_with_pow(&block, false);

let outcome = submit_block(&mining, &thread, &block).await;
assert!(
!outcome.accepted,
"block with insufficient PoW must not be accepted"
);
assert_eq!(outcome.reason, "high-hash");
assert_eq!(outcome.debug, "proof of work failed");

destroy_template(&template, &thread).await;
})
.await;
}

/// submitBlock with invalid contents should be rejected even with sufficient PoW.
#[tokio::test]
#[serial_test::serial]
async fn mining_submit_block_invalid() {
with_mining_client(|_client, thread, mining| async move {
let template = make_block_template(&mining, &thread).await;

let block = get_template_block(&template, &thread).await;
let mut block = block_with_pow(&block, true);
// Corrupt the serialized block after solving its header. This keeps
// the PoW valid while making the header's Merkle root stale.
*block
.last_mut()
.expect("serialized block must not be empty") ^= 1;

let outcome = submit_block(&mining, &thread, &block).await;
assert!(
!outcome.accepted,
"invalid block with sufficient PoW must not be accepted"
);
assert_eq!(outcome.reason, "bad-txnmrklroot");
assert_eq!(outcome.debug, "hashMerkleRoot mismatch");

destroy_template(&template, &thread).await;
})
.await;
}

/// submitBlock with a solved template block should be accepted.
#[tokio::test]
#[serial_test::serial]
async fn mining_submit_block_resolved() {
with_mining_client(|_client, thread, mining| async move {
let template = make_block_template(&mining, &thread).await;

let block = get_template_block(&template, &thread).await;
let block = block_with_pow(&block, true);

let outcome = submit_block(&mining, &thread, &block).await;
assert!(
outcome.accepted,
"solved template block must be accepted: reason={}, debug={}",
outcome.reason, outcome.debug
);
assert_eq!(outcome.reason, "");
assert_eq!(outcome.debug, "");

destroy_template(&template, &thread).await;
})
.await;
}

/// submitBlock with a duplicate solved block should be rejected.
#[tokio::test]
#[serial_test::serial]
async fn mining_submit_block_duplicate() {
with_mining_client(|_client, thread, mining| async move {
let template = make_block_template(&mining, &thread).await;

let block = get_template_block(&template, &thread).await;
let block = block_with_pow(&block, true);

let outcome = submit_block(&mining, &thread, &block).await;
assert!(
outcome.accepted,
"first solved block submission must be accepted: reason={}, debug={}",
outcome.reason, outcome.debug
);
assert_eq!(outcome.reason, "");
assert_eq!(outcome.debug, "");

let outcome = submit_block(&mining, &thread, &block).await;
assert!(!outcome.accepted, "duplicate block must not be accepted");
assert_eq!(outcome.reason, "duplicate");
assert_eq!(outcome.debug, "");

destroy_template(&template, &thread).await;
})
.await;
}

/// checkBlock with a template block payload, and interrupt.
#[tokio::test]
// Serialized because interrupt() can affect other in-flight mining waits.
Expand All @@ -243,14 +449,7 @@ async fn mining_check_block_and_interrupt() {
with_mining_client(|_client, thread, mining| async move {
let template = make_block_template(&mining, &thread).await;

let mut get_block_req = template.get_block_request();
get_block_req
.get()
.get_context()
.unwrap()
.set_thread(thread.clone());
let get_block_resp = get_block_req.send().promise.await.unwrap();
let block = get_block_resp.get().unwrap().get_result().unwrap().to_vec();
let block = get_template_block(&template, &thread).await;

// checkBlock should either error or return a response.
let mut req = mining.check_block_request();
Expand All @@ -264,10 +463,14 @@ async fn mining_check_block_and_interrupt() {
let result = req.send().promise.await;
match result {
Ok(resp) => {
let results = resp.get().unwrap();
let _valid: bool = results.get_result();
let _reason = results.get_reason().unwrap();
let _debug = results.get_debug().unwrap();
let results = resp.get().expect("checkBlock response should decode");
let _valid = results.get_result();
let _reason = results
.get_reason()
.expect("checkBlock response should contain reason");
let _debug = results
.get_debug()
.expect("checkBlock response should contain debug");
}
Err(_) => {
// Server may reject validation/deserialization.
Expand All @@ -277,7 +480,12 @@ async fn mining_check_block_and_interrupt() {
destroy_template(&template, &thread).await;

// interrupt — should not crash.
mining.interrupt_request().send().promise.await.unwrap();
mining
.interrupt_request()
.send()
.promise
.await
.expect("interrupt should not fail");
})
.await;
}
Expand Down
2 changes: 1 addition & 1 deletion tests/util/bitcoin_core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::{
sync::Once,
};

use crate::bitcoin_core_wallet_util::{
use crate::util::bitcoin_core_wallet::{
bitcoin_rpc_json, bitcoin_test_wallet, ensure_wallet_loaded, mine_blocks_to_new_address,
};
use bitcoin_capnp_types::{
Expand Down
Loading
Loading