From 76c9c83d28b947384dc57126e58f83e7c2095c5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=BE=D0=BB=D0=BB=D0=B8?= Date: Wed, 10 Jun 2026 22:39:55 +0300 Subject: [PATCH] integration-tests: verify partial-donation coinbase outputs in mined block --- integration-tests/lib/template_provider.rs | 13 ++- integration-tests/tests/pool_solo_mining.rs | 91 ++++++++++++++++++++- 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/integration-tests/lib/template_provider.rs b/integration-tests/lib/template_provider.rs index 712f86356..5929fb2fc 100644 --- a/integration-tests/lib/template_provider.rs +++ b/integration-tests/lib/template_provider.rs @@ -5,7 +5,7 @@ use std::{ path::PathBuf, process::{Child, Command, Stdio}, }; -use stratum_apps::stratum_core::bitcoin::{Address, Amount, Txid}; +use stratum_apps::stratum_core::bitcoin::{Address, Amount, Block, BlockHash, Txid}; use tracing::warn; use crate::utils::{fs_utils, http, tarball}; @@ -300,6 +300,12 @@ impl BitcoinCore { Ok(block_hash) } + /// Fetch the full block (header + txdata) for the given hash. + pub fn get_block(&self, hash: BlockHash) -> Result { + let block = self.bitcoind.client.get_block(hash)?; + Ok(block) + } + /// Return the IPC socket path for connecting to this node. pub fn ipc_socket_path(&self) -> PathBuf { let network_dir = if self.is_signet { "signet" } else { "regtest" }; @@ -441,6 +447,11 @@ impl TemplateProvider { self.bitcoin_core.get_best_block_hash() } + /// Fetch the full block (header + txdata) for the given hash. + pub fn get_block(&self, hash: BlockHash) -> Result { + self.bitcoin_core.get_block(hash) + } + /// Return the sv2 port that sv2-tp is listening on. pub fn sv2_port(&self) -> u16 { self.sv2_port diff --git a/integration-tests/tests/pool_solo_mining.rs b/integration-tests/tests/pool_solo_mining.rs index b19ca78df..731e2ecc6 100644 --- a/integration-tests/tests/pool_solo_mining.rs +++ b/integration-tests/tests/pool_solo_mining.rs @@ -28,7 +28,7 @@ use integration_tests_sv2::{ POOL_COINBASE_REWARD_ADDRESS, *, }; use stratum_apps::stratum_core::{ - bitcoin::{consensus::deserialize, params::TESTNET4, Address, Transaction}, + bitcoin::{consensus::deserialize, params::TESTNET4, Address, BlockHash, Transaction}, common_messages_sv2::*, mining_sv2::*, parsers_sv2::{self, AnyMessage, Mining}, @@ -1294,3 +1294,92 @@ async fn pool_solo_mining_partial_donation() { shutdown_all!(pool); } + +#[tokio::test] +async fn pool_solo_mining_partial_donation_end_to_end() { + start_tracing(); + let (tp, tp_addr) = start_template_provider(Some(5), DifficultyLevel::Low); + tp.fund_wallet().unwrap(); + let (pool, pool_addr, _) = start_pool(sv2_tp_config(tp_addr), vec![], vec![], false).await; + + let (_address, mempool_txid) = tp.create_mempool_transaction().unwrap(); + let start_hash: BlockHash = tp + .get_best_block_hash() + .unwrap() + .parse() + .expect("invalid block hash"); + + let donate_identity = format!("sri/donate/5/{}/worker.1", MINER_COINBASE_REWARD_ADDR); + let (translator, tproxy_addr, _) = start_sv2_translator_with_user_identity( + &[pool_addr], + false, + vec![], + vec![], + None, + donate_identity, + false, + false, + ) + .await; + let (_minerd_process, _minerd_addr) = start_minerd(tproxy_addr, None, None, false).await; + + // Mine until a block includes the mempool transaction, proving the round-trip + // worked against a template carrying real txdata (non-empty merkle path). Each + // poll walks the chain back to the starting tip so no mined block is missed. + let timeout = tokio::time::Duration::from_secs(60); + let poll_interval = tokio::time::Duration::from_secs(2); + let start_time = tokio::time::Instant::now(); + let block = 'outer: loop { + if start_time.elapsed() > timeout { + panic!("no block including the mempool transaction was mined within 60 seconds"); + } + tokio::time::sleep(poll_interval).await; + let mut cursor: BlockHash = tp + .get_best_block_hash() + .unwrap() + .parse() + .expect("invalid block hash"); + while cursor != start_hash { + let block = tp.get_block(cursor).expect("failed to fetch mined block"); + if block + .txdata + .iter() + .any(|tx| tx.compute_txid() == mempool_txid) + { + break 'outer block; + } + cursor = block.header.prev_blockhash; + } + }; + + let coinbase = &block.txdata[0]; + assert_eq!( + coinbase.output.len(), + 3, + "coinbase should have 3 outputs (pool 5%, miner 95%, witness commitment)" + ); + + let payout = extract_payout_info(coinbase); + assert_eq!( + payout.addresses.len(), + 2, + "coinbase should pay exactly two addresses" + ); + assert_eq!( + payout.addresses[0], POOL_COINBASE_REWARD_ADDRESS, + "pool payout should be the first coinbase output" + ); + assert_eq!( + payout.addresses[1], MINER_COINBASE_REWARD_ADDR, + "miner payout should be the second coinbase output" + ); + assert_payout_percentage( + &payout, + &[ + (POOL_COINBASE_REWARD_ADDRESS.to_string(), 5.0), + (MINER_COINBASE_REWARD_ADDR.to_string(), 95.0), + ], + ); + + shutdown_all!(pool, translator); +}