From 844c91b2f8a63fdf3d5a062b485bc5128fca404e Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 7 Jul 2026 19:51:20 +0200 Subject: [PATCH 01/11] ci: update integration workflow runner versions Update the existing integration workflow to actions/checkout@v6 and macos-15. Behavior changes: - Existing integration tests run on macos-15 instead of macos-latest. --- .github/workflows/integration-tests.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/integration-tests.yaml b/.github/workflows/integration-tests.yaml index 68bb35be9..03d33f204 100644 --- a/.github/workflows/integration-tests.yaml +++ b/.github/workflows/integration-tests.yaml @@ -15,14 +15,14 @@ jobs: matrix: os: - ubuntu-latest - - macos-latest + - macos-15 include: - os: ubuntu-latest target: x86_64-unknown-linux-gnu steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install Rust uses: dtolnay/rust-toolchain@1.88 @@ -32,7 +32,7 @@ jobs: run: sudo apt-get install -y capnproto libcapnp-dev - name: Install capnp dependencies (macOS) - if: matrix.os == 'macos-latest' + if: matrix.os == 'macos-15' run: brew install capnp # Install cargo-binstall so tool installs below use prebuilt binaries. From 3d921fa5e7b8ea891b3729de32ba9a052daef4d5 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 7 Jul 2026 19:39:33 +0200 Subject: [PATCH 02/11] test: group BitcoinCoreIpc integration tests Move BitcoinCoreIpc-only integration tests into bitcoin_core_ipc_integration. Behavior changes: - Test names and assertions stay the same. --- .../tests/bitcoin_core_ipc_integration.rs | 236 +++++++++++++++++- integration-tests/tests/jd_integration.rs | 73 ------ .../tests/jdc_fallback_to_solo.rs | 86 ------- integration-tests/tests/pool_integration.rs | 69 ----- 4 files changed, 235 insertions(+), 229 deletions(-) diff --git a/integration-tests/tests/bitcoin_core_ipc_integration.rs b/integration-tests/tests/bitcoin_core_ipc_integration.rs index 431a264c2..dcd2fcab6 100644 --- a/integration-tests/tests/bitcoin_core_ipc_integration.rs +++ b/integration-tests/tests/bitcoin_core_ipc_integration.rs @@ -1,10 +1,16 @@ use integration_tests_sv2::{ interceptor::{IgnoreMessage, MessageDirection}, + mock_roles::{MockDownstream, WithSetup}, template_provider::DifficultyLevel, *, }; use jd_client_sv2::config::ConfigJDCMode; -use stratum_apps::stratum_core::{common_messages_sv2::*, job_declaration_sv2::*}; +use stratum_apps::stratum_core::{ + common_messages_sv2::*, + job_declaration_sv2::*, + mining_sv2::*, + parsers_sv2::{AnyMessage, Mining}, +}; // Pool propagates block via IPC #[tokio::test] @@ -166,3 +172,231 @@ async fn jdc_solo_mining_with_bitcoin_core_ipc() { } } } + +// launch a JDC (with Bitcoin Core IPC) connected to a Pool/JDS and then triggers a fallback to solo +// then it mines a block using solo and verifies the block was propagated +// meant to avoid regressions like https://github.com/stratum-mining/sv2-apps/issues/466 +#[tokio::test] +async fn jdc_fallback_to_solo_mines_block_with_bitcoin_core_ipc() { + start_tracing(); + let bitcoin_core = start_bitcoin_core_latest(DifficultyLevel::Low); + let current_block_hash = bitcoin_core.get_best_block_hash().unwrap(); + + let (pool, pool_addr, jds_addr, _) = + start_pool_with_jds(&bitcoin_core, vec![], vec![], false).await; + let (jdc_jds_sniffer, jdc_jds_sniffer_addr) = start_sniffer( + "jdc-fallback-bitcoin-core-jds", + jds_addr, + false, + vec![], + None, + ); + let (jdc, jdc_addr, _) = start_jdc( + &[(pool_addr, jdc_jds_sniffer_addr)], + ipc_config( + bitcoin_core.data_dir().clone(), + bitcoin_core.is_signet(), + None, + ), + vec![], + vec![], + false, + None, + ); + + // assert JDC-JDS connection is established + { + jdc_jds_sniffer + .wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_SETUP_CONNECTION) + .await; + jdc_jds_sniffer + .wait_for_message_type( + MessageDirection::ToDownstream, + MESSAGE_TYPE_SETUP_CONNECTION_SUCCESS, + ) + .await; + jdc_jds_sniffer + .wait_for_message_type( + MessageDirection::ToUpstream, + MESSAGE_TYPE_ALLOCATE_MINING_JOB_TOKEN, + ) + .await; + jdc_jds_sniffer + .wait_for_message_type( + MessageDirection::ToDownstream, + MESSAGE_TYPE_ALLOCATE_MINING_JOB_TOKEN_SUCCESS, + ) + .await; + } + + // trigger JDC fallback + pool.shutdown().await; + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + + let (tproxy, tproxy_addr, _) = + start_sv2_translator(&[jdc_addr], false, vec![], vec![], None, false).await; + let (_minerd_process, _minerd_addr) = start_minerd(tproxy_addr, None, None, false).await; + + let timeout = tokio::time::Duration::from_secs(60); + let poll_interval = tokio::time::Duration::from_secs(2); + let start_time = tokio::time::Instant::now(); + + // assert JDC was able to propagate a block while doing solo + loop { + tokio::time::sleep(poll_interval).await; + let new_block_hash = bitcoin_core.get_best_block_hash().unwrap(); + if new_block_hash != current_block_hash { + shutdown_all!(jdc, tproxy); + return; + } + if start_time.elapsed() > timeout { + panic!( + "JDC fallback to solo with BitcoinCoreIpc should have propagated a new block \ + within {} seconds", + timeout.as_secs() + ); + } + } +} + +// This test verifies that Pool rejects OpenExtendedMiningChannel when +// REQUIRES_STANDARD_JOBS is set during SetupConnection. +#[tokio::test] +async fn pool_require_standard_jobs_set_rejects_open_extended_mining_channel() { + start_tracing(); + let bitcoin_core = start_bitcoin_core_latest(DifficultyLevel::Low); + let (pool, pool_addr, _) = start_pool( + ipc_config( + bitcoin_core.data_dir().clone(), + bitcoin_core.is_signet(), + None, + ), + vec![], + vec![], + false, + ) + .await; + + let (sniffer, sniffer_addr) = start_sniffer("sniffer", pool_addr, false, vec![], None); + // SetupConnection flags: 0b0001 == REQUIRES_STANDARD_JOBS. + let mock_downstream = MockDownstream::new( + sniffer_addr, + WithSetup::yes_with_defaults(Protocol::MiningProtocol, 0b0001), + ); + let send_to_pool = mock_downstream.start().await; + + sniffer + .wait_for_message_type_and_clean_queue( + MessageDirection::ToDownstream, + MESSAGE_TYPE_SETUP_CONNECTION_SUCCESS, + ) + .await; + + let open_extended_mining_channel = AnyMessage::Mining(Mining::OpenExtendedMiningChannel( + OpenExtendedMiningChannel { + request_id: 100u32.into(), + user_identity: "user_identity".try_into().unwrap(), + nominal_hash_rate: 1000.0, + max_target: vec![0xff; 32].try_into().unwrap(), + min_extranonce_size: 8, + }, + )); + send_to_pool + .send(open_extended_mining_channel) + .await + .unwrap(); + + sniffer + .wait_for_message_type( + MessageDirection::ToDownstream, + MESSAGE_TYPE_OPEN_MINING_CHANNEL_ERROR, + ) + .await; + + let error = loop { + match sniffer.next_message_from_upstream() { + Some((_, AnyMessage::Mining(Mining::OpenMiningChannelError(msg)))) => break msg, + _ => continue, + } + }; + + assert_eq!( + error.error_code.as_utf8_or_hex(), + ERROR_CODE_OPEN_MINING_CHANNEL_EXTENDED_CHANNELS_NOT_SUPPORTED_FOR_STANDARD_JOBS + ); + + pool.shutdown().await; +} + +// This test verifies that JDC rejects OpenExtendedMiningChannel when +// REQUIRES_STANDARD_JOBS is set during SetupConnection. +#[tokio::test] +async fn jdc_require_standard_jobs_set_rejects_open_extended_mining_channel() { + start_tracing(); + let bitcoin_core = start_bitcoin_core_latest(DifficultyLevel::Low); + let (pool, pool_addr, jds_addr, _) = + start_pool_with_jds(&bitcoin_core, vec![], vec![], true).await; + + let (jdc, jdc_addr, _) = start_jdc( + &[(pool_addr, jds_addr)], + ipc_config( + bitcoin_core.data_dir().clone(), + bitcoin_core.is_signet(), + None, + ), + vec![], + vec![], + false, + None, + ); + + let (sniffer, sniffer_addr) = start_sniffer("sniffer", jdc_addr, false, vec![], None); + // SetupConnection flags: 0b0001 == REQUIRES_STANDARD_JOBS. + let mock_downstream = MockDownstream::new( + sniffer_addr, + WithSetup::yes_with_defaults(Protocol::MiningProtocol, 0b0001), + ); + let send_to_jdc = mock_downstream.start().await; + + sniffer + .wait_for_message_type_and_clean_queue( + MessageDirection::ToDownstream, + MESSAGE_TYPE_SETUP_CONNECTION_SUCCESS, + ) + .await; + + let open_extended_mining_channel = AnyMessage::Mining(Mining::OpenExtendedMiningChannel( + OpenExtendedMiningChannel { + request_id: 100u32.into(), + user_identity: "user_identity".try_into().unwrap(), + nominal_hash_rate: 1000.0, + max_target: vec![0xff; 32].try_into().unwrap(), + min_extranonce_size: 8, + }, + )); + send_to_jdc + .send(open_extended_mining_channel) + .await + .unwrap(); + + sniffer + .wait_for_message_type( + MessageDirection::ToDownstream, + MESSAGE_TYPE_OPEN_MINING_CHANNEL_ERROR, + ) + .await; + + let error = loop { + match sniffer.next_message_from_upstream() { + Some((_, AnyMessage::Mining(Mining::OpenMiningChannelError(msg)))) => break msg, + _ => continue, + } + }; + + assert_eq!( + error.error_code.as_utf8_or_hex(), + ERROR_CODE_OPEN_MINING_CHANNEL_EXTENDED_CHANNELS_NOT_SUPPORTED_FOR_STANDARD_JOBS + ); + + shutdown_all!(jdc, pool); +} diff --git a/integration-tests/tests/jd_integration.rs b/integration-tests/tests/jd_integration.rs index 5f7f6b514..e77742378 100644 --- a/integration-tests/tests/jd_integration.rs +++ b/integration-tests/tests/jd_integration.rs @@ -1271,79 +1271,6 @@ async fn jdc_require_standard_jobs_set_does_not_group_standard_channels() { shutdown_all!(jdc, pool); } -// This test verifies that JDC rejects OpenExtendedMiningChannel when -// REQUIRES_STANDARD_JOBS is set during SetupConnection. -#[tokio::test] -async fn jdc_require_standard_jobs_set_rejects_open_extended_mining_channel() { - start_tracing(); - let bitcoin_core = start_bitcoin_core_latest(DifficultyLevel::Low); - let (pool, pool_addr, jds_addr, _) = - start_pool_with_jds(&bitcoin_core, vec![], vec![], true).await; - - let (jdc, jdc_addr, _) = start_jdc( - &[(pool_addr, jds_addr)], - ipc_config( - bitcoin_core.data_dir().clone(), - bitcoin_core.is_signet(), - None, - ), - vec![], - vec![], - false, - None, - ); - - let (sniffer, sniffer_addr) = start_sniffer("sniffer", jdc_addr, false, vec![], None); - // SetupConnection flags: 0b0001 == REQUIRES_STANDARD_JOBS. - let mock_downstream = MockDownstream::new( - sniffer_addr, - WithSetup::yes_with_defaults(Protocol::MiningProtocol, 0b0001), - ); - let send_to_jdc = mock_downstream.start().await; - - sniffer - .wait_for_message_type_and_clean_queue( - MessageDirection::ToDownstream, - MESSAGE_TYPE_SETUP_CONNECTION_SUCCESS, - ) - .await; - - let open_extended_mining_channel = AnyMessage::Mining(Mining::OpenExtendedMiningChannel( - OpenExtendedMiningChannel { - request_id: 100u32.into(), - user_identity: "user_identity".try_into().unwrap(), - nominal_hash_rate: 1000.0, - max_target: vec![0xff; 32].try_into().unwrap(), - min_extranonce_size: 8, - }, - )); - send_to_jdc - .send(open_extended_mining_channel) - .await - .unwrap(); - - sniffer - .wait_for_message_type( - MessageDirection::ToDownstream, - MESSAGE_TYPE_OPEN_MINING_CHANNEL_ERROR, - ) - .await; - - let error = loop { - match sniffer.next_message_from_upstream() { - Some((_, AnyMessage::Mining(Mining::OpenMiningChannelError(msg)))) => break msg, - _ => continue, - } - }; - - assert_eq!( - error.error_code.as_utf8_or_hex(), - ERROR_CODE_OPEN_MINING_CHANNEL_EXTENDED_CHANNELS_NOT_SUPPORTED_FOR_STANDARD_JOBS - ); - - shutdown_all!(jdc, pool); -} - // Verifies that when the primary pool disconnects, JDC falls back to the next upstream entry // and sends *that* upstream's `user_identity` in `AllocateMiningJobToken` — not the primary's. #[tokio::test] diff --git a/integration-tests/tests/jdc_fallback_to_solo.rs b/integration-tests/tests/jdc_fallback_to_solo.rs index f64616510..7258448fd 100644 --- a/integration-tests/tests/jdc_fallback_to_solo.rs +++ b/integration-tests/tests/jdc_fallback_to_solo.rs @@ -1,92 +1,6 @@ use integration_tests_sv2::{interceptor::MessageDirection, template_provider::DifficultyLevel, *}; use stratum_apps::stratum_core::{common_messages_sv2::*, job_declaration_sv2::*}; -// launch a JDC (with Bitcoin Core IPC) connected to a Pool/JDS and then triggers a fallback to solo -// then it mines a block using solo and verifies the block was propagated -// meant to avoid regressions like https://github.com/stratum-mining/sv2-apps/issues/466 -#[tokio::test] -async fn jdc_fallback_to_solo_mines_block_with_bitcoin_core_ipc() { - start_tracing(); - let bitcoin_core = start_bitcoin_core_latest(DifficultyLevel::Low); - let current_block_hash = bitcoin_core.get_best_block_hash().unwrap(); - - let (pool, pool_addr, jds_addr, _) = - start_pool_with_jds(&bitcoin_core, vec![], vec![], false).await; - let (jdc_jds_sniffer, jdc_jds_sniffer_addr) = start_sniffer( - "jdc-fallback-bitcoin-core-jds", - jds_addr, - false, - vec![], - None, - ); - let (jdc, jdc_addr, _) = start_jdc( - &[(pool_addr, jdc_jds_sniffer_addr)], - ipc_config( - bitcoin_core.data_dir().clone(), - bitcoin_core.is_signet(), - None, - ), - vec![], - vec![], - false, - None, - ); - - // assert JDC-JDS connection is established - { - jdc_jds_sniffer - .wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_SETUP_CONNECTION) - .await; - jdc_jds_sniffer - .wait_for_message_type( - MessageDirection::ToDownstream, - MESSAGE_TYPE_SETUP_CONNECTION_SUCCESS, - ) - .await; - jdc_jds_sniffer - .wait_for_message_type( - MessageDirection::ToUpstream, - MESSAGE_TYPE_ALLOCATE_MINING_JOB_TOKEN, - ) - .await; - jdc_jds_sniffer - .wait_for_message_type( - MessageDirection::ToDownstream, - MESSAGE_TYPE_ALLOCATE_MINING_JOB_TOKEN_SUCCESS, - ) - .await; - } - - // trigger JDC fallback - pool.shutdown().await; - tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; - - let (tproxy, tproxy_addr, _) = - start_sv2_translator(&[jdc_addr], false, vec![], vec![], None, false).await; - let (_minerd_process, _minerd_addr) = start_minerd(tproxy_addr, None, None, false).await; - - let timeout = tokio::time::Duration::from_secs(60); - let poll_interval = tokio::time::Duration::from_secs(2); - let start_time = tokio::time::Instant::now(); - - // assert JDC was able to propagate a block while doing solo - loop { - tokio::time::sleep(poll_interval).await; - let new_block_hash = bitcoin_core.get_best_block_hash().unwrap(); - if new_block_hash != current_block_hash { - shutdown_all!(jdc, tproxy); - return; - } - if start_time.elapsed() > timeout { - panic!( - "JDC fallback to solo with BitcoinCoreIpc should have propagated a new block \ - within {} seconds", - timeout.as_secs() - ); - } - } -} - // launch a JDC (with Sv2 TP over TCP) connected to a Pool/JDS and then triggers a fallback to solo // then it mines a block using solo and verifies the block was propagated // meant to avoid regressions like https://github.com/stratum-mining/sv2-apps/issues/466 diff --git a/integration-tests/tests/pool_integration.rs b/integration-tests/tests/pool_integration.rs index ea420fb60..8e93f982b 100644 --- a/integration-tests/tests/pool_integration.rs +++ b/integration-tests/tests/pool_integration.rs @@ -1029,72 +1029,3 @@ async fn pool_require_standard_jobs_set_does_not_group_standard_channels() { } pool.shutdown().await; } - -// This test verifies that Pool rejects OpenExtendedMiningChannel when -// REQUIRES_STANDARD_JOBS is set during SetupConnection. -#[tokio::test] -async fn pool_require_standard_jobs_set_rejects_open_extended_mining_channel() { - start_tracing(); - let bitcoin_core = start_bitcoin_core_latest(DifficultyLevel::Low); - let (pool, pool_addr, _) = start_pool( - ipc_config( - bitcoin_core.data_dir().clone(), - bitcoin_core.is_signet(), - None, - ), - vec![], - vec![], - false, - ) - .await; - - let (sniffer, sniffer_addr) = start_sniffer("sniffer", pool_addr, false, vec![], None); - // SetupConnection flags: 0b0001 == REQUIRES_STANDARD_JOBS. - let mock_downstream = MockDownstream::new( - sniffer_addr, - WithSetup::yes_with_defaults(Protocol::MiningProtocol, 0b0001), - ); - let send_to_pool = mock_downstream.start().await; - - sniffer - .wait_for_message_type_and_clean_queue( - MessageDirection::ToDownstream, - MESSAGE_TYPE_SETUP_CONNECTION_SUCCESS, - ) - .await; - - let open_extended_mining_channel = AnyMessage::Mining(Mining::OpenExtendedMiningChannel( - OpenExtendedMiningChannel { - request_id: 100u32.into(), - user_identity: "user_identity".try_into().unwrap(), - nominal_hash_rate: 1000.0, - max_target: vec![0xff; 32].try_into().unwrap(), - min_extranonce_size: 8, - }, - )); - send_to_pool - .send(open_extended_mining_channel) - .await - .unwrap(); - - sniffer - .wait_for_message_type( - MessageDirection::ToDownstream, - MESSAGE_TYPE_OPEN_MINING_CHANNEL_ERROR, - ) - .await; - - let error = loop { - match sniffer.next_message_from_upstream() { - Some((_, AnyMessage::Mining(Mining::OpenMiningChannelError(msg)))) => break msg, - _ => continue, - } - }; - - assert_eq!( - error.error_code.as_utf8_or_hex(), - ERROR_CODE_OPEN_MINING_CHANNEL_EXTENDED_CHANNELS_NOT_SUPPORTED_FOR_STANDARD_JOBS - ); - - pool.shutdown().await; -} From 6bd4a82977454522f37f3e80fc5c3832c95ab224 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 7 Jul 2026 19:53:15 +0200 Subject: [PATCH 03/11] test: move template-provider mempool test Move test_create_mempool_transaction into template_provider_integration. Behavior changes: - The test name and assertions stay the same. --- integration-tests/lib/template_provider.rs | 15 --------------- .../tests/template_provider_integration.rs | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/integration-tests/lib/template_provider.rs b/integration-tests/lib/template_provider.rs index 9fa14b820..2188e3da2 100644 --- a/integration-tests/lib/template_provider.rs +++ b/integration-tests/lib/template_provider.rs @@ -506,18 +506,3 @@ impl Drop for TemplateProvider { // bitcoin-node is managed by corepc-node::Node and will be cleaned up automatically } } - -#[cfg(test)] -mod tests { - use super::{DifficultyLevel, TemplateProvider}; - use crate::utils::get_available_address; - - #[tokio::test] - async fn test_create_mempool_transaction() { - let address = get_available_address(); - let port = address.port(); - let tp = TemplateProvider::start(port, 1, DifficultyLevel::Low); - assert!(tp.fund_wallet().is_ok()); - assert!(tp.create_mempool_transaction().is_ok()); - } -} diff --git a/integration-tests/tests/template_provider_integration.rs b/integration-tests/tests/template_provider_integration.rs index 78615bd2e..325a06b0a 100644 --- a/integration-tests/tests/template_provider_integration.rs +++ b/integration-tests/tests/template_provider_integration.rs @@ -1,4 +1,17 @@ -use integration_tests_sv2::{template_provider::DifficultyLevel, *}; +use integration_tests_sv2::{ + template_provider::{DifficultyLevel, TemplateProvider}, + utils::get_available_address, + *, +}; + +#[tokio::test] +async fn test_create_mempool_transaction() { + let address = get_available_address(); + let port = address.port(); + let tp = TemplateProvider::start(port, 1, DifficultyLevel::Low); + assert!(tp.fund_wallet().is_ok()); + assert!(tp.create_mempool_transaction().is_ok()); +} #[tokio::test] async fn tp_low_diff() { From 170775f1bba87abf7870118232773d0f9c48fc51 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 7 Jul 2026 15:48:54 +0200 Subject: [PATCH 04/11] ci: build integration test binaries once per OS Compile integration-test binaries once per OS and run tests from the archived build. Behavior changes: - The new build job uploads a nextest archive per OS and does not run tests. - Test jobs download that archive instead of recompiling. --- .../actions/run-archived-nextest/action.yml | 46 ++++++++++++++ .github/actions/setup-nextest/action.yml | 21 +++++++ .github/workflows/integration-tests.yaml | 61 +++++++++++++------ 3 files changed, 111 insertions(+), 17 deletions(-) create mode 100644 .github/actions/run-archived-nextest/action.yml create mode 100644 .github/actions/setup-nextest/action.yml diff --git a/.github/actions/run-archived-nextest/action.yml b/.github/actions/run-archived-nextest/action.yml new file mode 100644 index 000000000..273164ecc --- /dev/null +++ b/.github/actions/run-archived-nextest/action.yml @@ -0,0 +1,46 @@ +name: Run archived cargo-nextest profile +description: Install cargo-nextest, download the compiled archive, and run one profile. + +inputs: + profile: + description: nextest profile to run. + required: true + github-token: + description: GitHub token used by cargo-binstall for release downloads. + required: true + bitcoin-core-version: + description: Bitcoin Core version selected by the matrix entry. + required: false + default: "" + bitcoin-core-binary: + description: Source-built Bitcoin Core binary path. + required: false + default: "" + +runs: + using: composite + steps: + - name: Install Rust + uses: dtolnay/rust-toolchain@1.88 + + - name: Install cargo-nextest + uses: ./.github/actions/setup-nextest + with: + github-token: ${{ inputs.github-token }} + + - name: Download compiled test archive + uses: actions/download-artifact@v8 + with: + name: integration-test-binaries-${{ runner.os }} + + - name: Run archived integration tests + shell: bash + env: + BITCOIN_CORE_VERSION: ${{ inputs.bitcoin-core-version }} + BITCOIN_CORE_BINARY: ${{ inputs.bitcoin-core-binary }} + run: | + RUST_BACKTRACE=1 RUST_LOG=debug cargo nextest run \ + --profile "${{ inputs.profile }}" \ + --archive-file "integration-test-binaries-${{ runner.os }}.tar.zst" \ + --workspace-remap "${{ github.workspace }}/integration-tests" \ + --nocapture diff --git a/.github/actions/setup-nextest/action.yml b/.github/actions/setup-nextest/action.yml new file mode 100644 index 000000000..0d7509250 --- /dev/null +++ b/.github/actions/setup-nextest/action.yml @@ -0,0 +1,21 @@ +name: Setup cargo-nextest +description: Install cargo-nextest from upstream release artifacts. + +inputs: + github-token: + description: GitHub token used by cargo-binstall for release downloads. + required: true + +runs: + using: composite + steps: + - name: Install cargo-binstall + uses: cargo-bins/cargo-binstall@v1.20.0 + with: + version: "1.20.0" + + - name: Install cargo-nextest + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github-token }} + run: cargo binstall --no-confirm --disable-telemetry --disable-strategies quick-install --locked cargo-nextest@0.9.100 diff --git a/.github/workflows/integration-tests.yaml b/.github/workflows/integration-tests.yaml index 03d33f204..cef0a822f 100644 --- a/.github/workflows/integration-tests.yaml +++ b/.github/workflows/integration-tests.yaml @@ -9,16 +9,14 @@ on: name: Integration Tests jobs: - ci: + build: + name: Build integration test binaries (${{ matrix.os }}) runs-on: ${{ matrix.os }} strategy: matrix: os: - ubuntu-latest - macos-15 - include: - - os: ubuntu-latest - target: x86_64-unknown-linux-gnu steps: - name: Checkout repository @@ -27,6 +25,16 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@1.88 + - name: Cache Rust build artifacts + uses: actions/cache@v6.1.0 + with: + path: | + ~/.cargo/git + ~/.cargo/registry + integration-tests/target + key: integration-tests-cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} + restore-keys: integration-tests-cargo-${{ runner.os }}- + - name: Install capnp dependencies (Ubuntu) if: matrix.os == 'ubuntu-latest' run: sudo apt-get install -y capnproto libcapnp-dev @@ -35,19 +43,38 @@ jobs: if: matrix.os == 'macos-15' run: brew install capnp - # Install cargo-binstall so tool installs below use prebuilt binaries. - - name: Install cargo-binstall - uses: cargo-bins/cargo-binstall@v1.20.0 - with: - version: "1.20.0" - - # Install cargo-nextest via cargo-binstall (no source build). - # Keep quick-install disabled to only use upstream release artifacts. - name: Install cargo-nextest - env: - GITHUB_TOKEN: ${{ github.token }} - run: cargo binstall --no-confirm --disable-telemetry --disable-strategies quick-install --locked cargo-nextest@0.9.100 + uses: ./.github/actions/setup-nextest + with: + github-token: ${{ github.token }} - - name: Integration Tests + - name: Build integration test binaries run: | - RUST_BACKTRACE=1 RUST_LOG=debug cargo nextest run --manifest-path=integration-tests/Cargo.toml --nocapture + cargo nextest archive --manifest-path=integration-tests/Cargo.toml --archive-file integration-test-binaries-${{ runner.os }}.tar.zst + + - name: Upload compiled test archive + uses: actions/upload-artifact@v7 + with: + name: integration-test-binaries-${{ runner.os }} + path: integration-test-binaries-${{ runner.os }}.tar.zst + if-no-files-found: error + + ci: + name: Integration tests (${{ matrix.os }}) + needs: build + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + - macos-15 + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Run archived integration tests + uses: ./.github/actions/run-archived-nextest + with: + profile: default + github-token: ${{ github.token }} From 3a2fea832056bd2e5a8514e73634e87fe4af9796 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 7 Jul 2026 14:52:44 +0200 Subject: [PATCH 05/11] test: select Bitcoin Core release from environment Select Bitcoin Core release tests from BITCOIN_CORE_VERSION instead of always using the latest release. Behavior changes: - v30.x and v31.x choose their matching IPC ABI and release tarball. - Version-specific IPC tests skip when another Core version is selected. --- integration-tests/lib/mod.rs | 4 +- integration-tests/lib/template_provider.rs | 37 +++++++++++++++++-- .../tests/bitcoin_core_ipc_jdp_io.rs | 9 ++++- .../tests/bitcoin_core_ipc_tdp_io.rs | 8 +++- 4 files changed, 51 insertions(+), 7 deletions(-) diff --git a/integration-tests/lib/mod.rs b/integration-tests/lib/mod.rs index 297b400a0..364b33a7b 100644 --- a/integration-tests/lib/mod.rs +++ b/integration-tests/lib/mod.rs @@ -75,7 +75,7 @@ pub fn ipc_config( } else { BitcoinNetwork::Regtest }; - let version = BITCOIN_CORE_LATEST; + let version = selected_bitcoin_core_version(); TemplateProviderType::BitcoinCoreIpc { version, @@ -193,7 +193,7 @@ pub fn start_template_provider( } pub fn start_bitcoin_core_latest(difficulty_level: DifficultyLevel) -> BitcoinCore { - start_bitcoin_core(difficulty_level, BITCOIN_CORE_LATEST) + start_bitcoin_core(difficulty_level, selected_bitcoin_core_version()) } pub fn start_bitcoin_core( diff --git a/integration-tests/lib/template_provider.rs b/integration-tests/lib/template_provider.rs index 2188e3da2..c5d5207ea 100644 --- a/integration-tests/lib/template_provider.rs +++ b/integration-tests/lib/template_provider.rs @@ -60,6 +60,29 @@ fn get_bitcoin_core_filename(os: &str, arch: &str, bitcoin_core_version: &str) - pub const BITCOIN_CORE_LATEST: BitcoinCoreVersion = BitcoinCoreVersion::V31X; +fn parse_ipc_version(version: &str) -> Option { + let version = version.strip_prefix('v').unwrap_or(version); + let major = version.split('.').next().unwrap_or(version); + match major { + "30" => Some(BitcoinCoreVersion::V30X), + "31" => Some(BitcoinCoreVersion::V31X), + _ => None, + } +} + +pub fn selected_bitcoin_core_version() -> BitcoinCoreVersion { + if let Ok(version) = env::var("BITCOIN_CORE_VERSION") { + return parse_ipc_version(&version) + .unwrap_or_else(|| panic!("Unsupported BITCOIN_CORE_VERSION release: {version}")); + } + + BITCOIN_CORE_LATEST +} + +pub fn should_run_bitcoin_core_version(version: BitcoinCoreVersion) -> bool { + env::var("BITCOIN_CORE_VERSION").is_err() || selected_bitcoin_core_version() == version +} + fn release_version(version: BitcoinCoreVersion) -> &'static str { match version { BitcoinCoreVersion::V30X => BITCOIN_CORE_V30X, @@ -67,6 +90,13 @@ fn release_version(version: BitcoinCoreVersion) -> &'static str { } } +fn selected_release_version(ipc_version: BitcoinCoreVersion) -> String { + match env::var("BITCOIN_CORE_VERSION") { + Ok(version) => version.strip_prefix('v').unwrap_or(&version).to_owned(), + _ => release_version(ipc_version).to_owned(), + } +} + /// Represents the consensus difficulty level of the network. /// /// Low: regtest mode (every share is a block) @@ -175,10 +205,10 @@ impl BitcoinCore { } // Download and setup Bitcoin Core with IPC support - let bitcoin_core_version = release_version(node_version); + let bitcoin_core_version = selected_release_version(node_version); let os = env::consts::OS; let arch = env::consts::ARCH; - let bitcoin_filename = get_bitcoin_core_filename(os, arch, bitcoin_core_version); + let bitcoin_filename = get_bitcoin_core_filename(os, arch, &bitcoin_core_version); let bitcoin_home = bin_dir.join(format!("bitcoin-{bitcoin_core_version}")); let bitcoin_node_bin = bitcoin_home.join("libexec").join("bitcoin-node"); let bitcoin_cli_bin = bitcoin_home.join("bin").join("bitcoin-cli"); @@ -363,7 +393,8 @@ pub struct TemplateProvider { impl TemplateProvider { /// Start a new [`TemplateProvider`] instance with Bitcoin Core and standalone sv2-tp. pub fn start(port: u16, sv2_interval: u32, difficulty_level: DifficultyLevel) -> Self { - let bitcoin_core = BitcoinCore::start(port, difficulty_level, BITCOIN_CORE_LATEST); + let bitcoin_core = + BitcoinCore::start(port, difficulty_level, selected_bitcoin_core_version()); let current_dir: PathBuf = std::env::current_dir().expect("failed to read current dir"); let bin_dir = current_dir.join("template-provider"); diff --git a/integration-tests/tests/bitcoin_core_ipc_jdp_io.rs b/integration-tests/tests/bitcoin_core_ipc_jdp_io.rs index dc5eb573d..5933b3cc4 100644 --- a/integration-tests/tests/bitcoin_core_ipc_jdp_io.rs +++ b/integration-tests/tests/bitcoin_core_ipc_jdp_io.rs @@ -12,7 +12,8 @@ use async_channel::Sender; use integration_tests_sv2::{ - start_bitcoin_core, start_tracing, template_provider::DifficultyLevel, + start_bitcoin_core, start_tracing, + template_provider::{should_run_bitcoin_core_version, DifficultyLevel}, }; use std::time::Duration; use stratum_apps::{ @@ -36,11 +37,17 @@ use stratum_apps::{ #[tokio::test] async fn jdp_io_integration_v30x() { + if !should_run_bitcoin_core_version(BitcoinCoreVersion::V30X) { + return; + } assert_jdp_io_integration_for_version(BitcoinCoreVersion::V30X).await; } #[tokio::test] async fn jdp_io_integration_v31x() { + if !should_run_bitcoin_core_version(BitcoinCoreVersion::V31X) { + return; + } assert_jdp_io_integration_for_version(BitcoinCoreVersion::V31X).await; } diff --git a/integration-tests/tests/bitcoin_core_ipc_tdp_io.rs b/integration-tests/tests/bitcoin_core_ipc_tdp_io.rs index 7c2bfe0f7..a575452c0 100644 --- a/integration-tests/tests/bitcoin_core_ipc_tdp_io.rs +++ b/integration-tests/tests/bitcoin_core_ipc_tdp_io.rs @@ -13,7 +13,7 @@ use async_channel::{Receiver, Sender}; use integration_tests_sv2::{ start_bitcoin_core, start_tracing, - template_provider::{BitcoinCore, DifficultyLevel}, + template_provider::{should_run_bitcoin_core_version, BitcoinCore, DifficultyLevel}, }; use std::time::{Duration, Instant}; use stratum_apps::{ @@ -33,11 +33,17 @@ use stratum_apps::{ #[tokio::test] async fn tdp_io_integration_v30x() { + if !should_run_bitcoin_core_version(BitcoinCoreVersion::V30X) { + return; + } assert_tdp_io_integration(BitcoinCoreVersion::V30X).await; } #[tokio::test] async fn tdp_io_integration_v31x() { + if !should_run_bitcoin_core_version(BitcoinCoreVersion::V31X) { + return; + } assert_tdp_io_integration(BitcoinCoreVersion::V31X).await; } From 08e72bbb4c066ba416eaffd323d87785cfa3ec69 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 7 Jul 2026 15:47:58 +0200 Subject: [PATCH 06/11] test: support source-built Bitcoin Core binaries Allow BITCOIN_CORE_VERSION values like bitcoin/bitcoin@master to use BITCOIN_CORE_BINARY instead of a release tarball. Behavior changes: - Source entries skip release downloads. - BITCOIN_CORE_IPC_VERSION can override the IPC ABI; otherwise source entries use the latest supported ABI. --- integration-tests/lib/template_provider.rs | 44 ++++++++++++++++++---- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/integration-tests/lib/template_provider.rs b/integration-tests/lib/template_provider.rs index c5d5207ea..5ea1eb8a9 100644 --- a/integration-tests/lib/template_provider.rs +++ b/integration-tests/lib/template_provider.rs @@ -2,7 +2,7 @@ use corepc_node::{types::GetBlockchainInfo, Conf, ConnectParams, Node}; use std::{ env, fs::create_dir_all, - path::PathBuf, + path::{Path, PathBuf}, process::{Child, Command, Stdio}, }; use stratum_apps::{ @@ -71,9 +71,16 @@ fn parse_ipc_version(version: &str) -> Option { } pub fn selected_bitcoin_core_version() -> BitcoinCoreVersion { - if let Ok(version) = env::var("BITCOIN_CORE_VERSION") { + if let Ok(version) = env::var("BITCOIN_CORE_IPC_VERSION") { return parse_ipc_version(&version) - .unwrap_or_else(|| panic!("Unsupported BITCOIN_CORE_VERSION release: {version}")); + .unwrap_or_else(|| panic!("Unsupported BITCOIN_CORE_IPC_VERSION: {version}")); + } + + if let Ok(version) = env::var("BITCOIN_CORE_VERSION") { + if !version.contains('@') { + return parse_ipc_version(&version) + .unwrap_or_else(|| panic!("Unsupported BITCOIN_CORE_VERSION release: {version}")); + } } BITCOIN_CORE_LATEST @@ -92,11 +99,32 @@ fn release_version(version: BitcoinCoreVersion) -> &'static str { fn selected_release_version(ipc_version: BitcoinCoreVersion) -> String { match env::var("BITCOIN_CORE_VERSION") { - Ok(version) => version.strip_prefix('v').unwrap_or(&version).to_owned(), + Ok(version) if !version.contains('@') => { + version.strip_prefix('v').unwrap_or(&version).to_owned() + } _ => release_version(ipc_version).to_owned(), } } +fn source_bitcoin_core_binary() -> Option { + let binary = env::var("BITCOIN_CORE_BINARY").ok()?; + if !env::var("BITCOIN_CORE_VERSION") + .map(|version| version.contains('@')) + .unwrap_or(false) + { + return None; + } + Some(PathBuf::from(binary)) +} + +fn ensure_executable_exists(path: &Path, env_var: &str) { + assert!( + path.exists(), + "Cannot find {} specified by {env_var}", + path.display() + ); +} + /// Represents the consensus difficulty level of the network. /// /// Low: regtest mode (every share is a block) @@ -204,16 +232,18 @@ impl BitcoinCore { } } - // Download and setup Bitcoin Core with IPC support + // Download and setup Bitcoin Core with IPC support, or use a source-built binary. let bitcoin_core_version = selected_release_version(node_version); let os = env::consts::OS; let arch = env::consts::ARCH; let bitcoin_filename = get_bitcoin_core_filename(os, arch, &bitcoin_core_version); let bitcoin_home = bin_dir.join(format!("bitcoin-{bitcoin_core_version}")); - let bitcoin_node_bin = bitcoin_home.join("libexec").join("bitcoin-node"); + let bitcoin_node_bin = source_bitcoin_core_binary() + .inspect(|path| ensure_executable_exists(path, "BITCOIN_CORE_BINARY")) + .unwrap_or_else(|| bitcoin_home.join("libexec").join("bitcoin-node")); let bitcoin_cli_bin = bitcoin_home.join("bin").join("bitcoin-cli"); - if !bitcoin_node_bin.exists() { + if source_bitcoin_core_binary().is_none() && !bitcoin_node_bin.exists() { let tarball_bytes = match env::var("BITCOIN_CORE_TARBALL_FILE") { Ok(path) => tarball::read_from_file(&path), Err(_) => { From e0647db3fcf10e8864cd81076a73de984d7c68eb Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 7 Jul 2026 18:02:42 +0200 Subject: [PATCH 07/11] test: add Bitcoin Core master IPC bindings Add bitcoin-capnp-types master and a unix_capnp::master wrapper using shared v31x sources. Behavior changes: - Master IPC bindings are available but not selected yet. --- bitcoin-core-sv2/Cargo.toml | 3 +++ bitcoin-core-sv2/src/lib.rs | 1 + bitcoin-core-sv2/src/unix_capnp/master/mod.rs | 14 ++++++++++++++ bitcoin-core-sv2/src/unix_capnp/mod.rs | 1 + .../v31x/job_declaration_protocol/error.rs | 2 +- .../v31x/job_declaration_protocol/handlers.rs | 8 ++------ .../v31x/job_declaration_protocol/mod.rs | 10 +++------- .../v31x/job_declaration_protocol/monitors.rs | 4 ++-- bitcoin-core-sv2/src/unix_capnp/v31x/mod.rs | 2 ++ .../v31x/template_distribution_protocol/error.rs | 2 +- .../template_distribution_protocol/handlers.rs | 4 +--- .../v31x/template_distribution_protocol/mod.rs | 4 ++-- .../template_distribution_protocol/monitors.rs | 4 ++-- .../template_data.rs | 4 ++-- integration-tests/Cargo.lock | 13 ++++++++++++- miner-apps/Cargo.lock | 13 ++++++++++++- pool-apps/Cargo.lock | 13 ++++++++++++- stratum-apps/Cargo.lock | 13 ++++++++++++- 18 files changed, 85 insertions(+), 30 deletions(-) create mode 100644 bitcoin-core-sv2/src/unix_capnp/master/mod.rs diff --git a/bitcoin-core-sv2/Cargo.toml b/bitcoin-core-sv2/Cargo.toml index fc9c065d0..49fac19d4 100644 --- a/bitcoin-core-sv2/Cargo.toml +++ b/bitcoin-core-sv2/Cargo.toml @@ -23,6 +23,9 @@ bitcoin_capnp_types_v30 = { package = "bitcoin-capnp-types", version = "0.1.2" } # Bitcoin Core v31.x IPC dependencies bitcoin_capnp_types_v31 = { package = "bitcoin-capnp-types", version = "0.2.1" } +# Bitcoin Core master IPC dependencies +bitcoin_capnp_types_master = { package = "bitcoin-capnp-types", git = "https://github.com/2140-dev/bitcoin-capnp-types", branch = "master" } + # fetching from github enables synchronizing development workflows across sv2-apps and stratum repos # it MUST be changed before bitcoin-core-sv2 is published to crates.io # with the proper version of stratum-core being fetched from crates.io as well diff --git a/bitcoin-core-sv2/src/lib.rs b/bitcoin-core-sv2/src/lib.rs index a3842b310..b088f598a 100644 --- a/bitcoin-core-sv2/src/lib.rs +++ b/bitcoin-core-sv2/src/lib.rs @@ -19,6 +19,7 @@ //! factories with enum dispatch across backend versions. //! - [`unix_capnp::v30x`] contains the Bitcoin Core v30.x IPC implementation. //! - [`unix_capnp::v31x`] contains the Bitcoin Core v31.x IPC implementation. +//! - [`unix_capnp::master`] contains the Bitcoin Core master IPC implementation. //! //! ## Flavor direction //! diff --git a/bitcoin-core-sv2/src/unix_capnp/master/mod.rs b/bitcoin-core-sv2/src/unix_capnp/master/mod.rs new file mode 100644 index 000000000..79852cd8d --- /dev/null +++ b/bitcoin-core-sv2/src/unix_capnp/master/mod.rs @@ -0,0 +1,14 @@ +//! Bitcoin Core master IPC implementation modules. +//! +//! This namespace reuses the v31.x backend source with Bitcoin Core master capnp bindings. If +//! master diverges, add only the changed module here and keep unchanged modules path-imported. + +#![allow(clippy::duplicate_mod)] + +pub(crate) use bitcoin_capnp_types_master as capnp_types; + +#[path = "../v31x/job_declaration_protocol/mod.rs"] +pub mod job_declaration_protocol; + +#[path = "../v31x/template_distribution_protocol/mod.rs"] +pub mod template_distribution_protocol; diff --git a/bitcoin-core-sv2/src/unix_capnp/mod.rs b/bitcoin-core-sv2/src/unix_capnp/mod.rs index 9ef418203..3bd7c93e6 100644 --- a/bitcoin-core-sv2/src/unix_capnp/mod.rs +++ b/bitcoin-core-sv2/src/unix_capnp/mod.rs @@ -7,5 +7,6 @@ //! Due to `capnp-rpc` `!Send` internals, these runtimes must execute inside a //! [`tokio::task::LocalSet`]. +pub mod master; pub mod v30x; pub mod v31x; diff --git a/bitcoin-core-sv2/src/unix_capnp/v31x/job_declaration_protocol/error.rs b/bitcoin-core-sv2/src/unix_capnp/v31x/job_declaration_protocol/error.rs index 972c28c82..5e7882d31 100644 --- a/bitcoin-core-sv2/src/unix_capnp/v31x/job_declaration_protocol/error.rs +++ b/bitcoin-core-sv2/src/unix_capnp/v31x/job_declaration_protocol/error.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use stratum_core::bitcoin::consensus; -use bitcoin_capnp_types_v31::capnp; +use super::super::capnp_types::capnp; /// Errors from the [`crate::unix_capnp::v31x::job_declaration_protocol::BitcoinCoreSv2JDP`] layer. #[derive(Debug)] diff --git a/bitcoin-core-sv2/src/unix_capnp/v31x/job_declaration_protocol/handlers.rs b/bitcoin-core-sv2/src/unix_capnp/v31x/job_declaration_protocol/handlers.rs index 4d4e0a821..e6f442a6f 100644 --- a/bitcoin-core-sv2/src/unix_capnp/v31x/job_declaration_protocol/handlers.rs +++ b/bitcoin-core-sv2/src/unix_capnp/v31x/job_declaration_protocol/handlers.rs @@ -1,11 +1,7 @@ //! Handlers for Bitcoin Core v31.x Sv2 Job Declaration Protocol via capnp over UNIX socket. -use crate::{ - common::job_declaration_protocol::io::{JdResponse, ValidationContext}, - unix_capnp::v31x::job_declaration_protocol::{ - BitcoinCoreSv2JDP, mempool::decode_bip34_height_from_coinbase_script_sig, - }, -}; +use super::{BitcoinCoreSv2JDP, mempool::decode_bip34_height_from_coinbase_script_sig}; +use crate::common::job_declaration_protocol::io::{JdResponse, ValidationContext}; use stratum_core::{ bitcoin::{ Block, Transaction, TxMerkleNode, Txid, Wtxid, diff --git a/bitcoin-core-sv2/src/unix_capnp/v31x/job_declaration_protocol/mod.rs b/bitcoin-core-sv2/src/unix_capnp/v31x/job_declaration_protocol/mod.rs index 524dbfd12..ff1d2ec08 100644 --- a/bitcoin-core-sv2/src/unix_capnp/v31x/job_declaration_protocol/mod.rs +++ b/bitcoin-core-sv2/src/unix_capnp/v31x/job_declaration_protocol/mod.rs @@ -1,12 +1,9 @@ //! Module for interacting with Bitcoin Core v31.x via Sv2 Job Declaration Protocol via capnp over //! UNIX socket. -use crate::{ - common::job_declaration_protocol::io::JdRequest, - unix_capnp::v31x::job_declaration_protocol::{ - error::BitcoinCoreSv2JDPError, mempool::MempoolMirror, - }, -}; +use self::{error::BitcoinCoreSv2JDPError, mempool::MempoolMirror}; +use super::capnp_types as bitcoin_capnp_types; +use crate::common::job_declaration_protocol::io::JdRequest; use async_channel::Receiver; use bitcoin_capnp_types::{ capnp, @@ -17,7 +14,6 @@ use bitcoin_capnp_types::{ }, proxy_capnp::{thread::Client as ThreadIpcClient, thread_map::Client as ThreadMapIpcClient}, }; -use bitcoin_capnp_types_v31 as bitcoin_capnp_types; use std::{cell::RefCell, path::Path, rc::Rc}; use stratum_core::bitcoin::{Block, consensus::deserialize}; use tokio::net::UnixStream; diff --git a/bitcoin-core-sv2/src/unix_capnp/v31x/job_declaration_protocol/monitors.rs b/bitcoin-core-sv2/src/unix_capnp/v31x/job_declaration_protocol/monitors.rs index b4167f053..8466e0f6f 100644 --- a/bitcoin-core-sv2/src/unix_capnp/v31x/job_declaration_protocol/monitors.rs +++ b/bitcoin-core-sv2/src/unix_capnp/v31x/job_declaration_protocol/monitors.rs @@ -1,8 +1,8 @@ //! Background monitors for Bitcoin Core v31.x Sv2 Job Declaration Protocol via capnp over UNIX //! socket. -use crate::unix_capnp::v31x::job_declaration_protocol::BitcoinCoreSv2JDP; -use bitcoin_capnp_types_v31::capnp; +use super::super::capnp_types::capnp; +use super::BitcoinCoreSv2JDP; use tokio::task::JoinHandle; use tracing::{debug, error, warn}; diff --git a/bitcoin-core-sv2/src/unix_capnp/v31x/mod.rs b/bitcoin-core-sv2/src/unix_capnp/v31x/mod.rs index 2242ef82e..14ca2214a 100644 --- a/bitcoin-core-sv2/src/unix_capnp/v31x/mod.rs +++ b/bitcoin-core-sv2/src/unix_capnp/v31x/mod.rs @@ -6,5 +6,7 @@ //! It is wired against `bitcoin_capnp_types_v31`, which re-exports the matching `capnp` //! and `capnp-rpc` APIs. +pub(crate) use bitcoin_capnp_types_v31 as capnp_types; + pub mod job_declaration_protocol; pub mod template_distribution_protocol; diff --git a/bitcoin-core-sv2/src/unix_capnp/v31x/template_distribution_protocol/error.rs b/bitcoin-core-sv2/src/unix_capnp/v31x/template_distribution_protocol/error.rs index d76e0eba1..af55be189 100644 --- a/bitcoin-core-sv2/src/unix_capnp/v31x/template_distribution_protocol/error.rs +++ b/bitcoin-core-sv2/src/unix_capnp/v31x/template_distribution_protocol/error.rs @@ -6,7 +6,7 @@ use stratum_core::bitcoin::{ block::ValidationError, consensus, consensus::encode::Error as ConsensusEncodeError, }; -use bitcoin_capnp_types_v31::capnp; +use super::super::capnp_types::capnp; /// Error type for [`crate::BitcoinCoreSv2TDP`] #[derive(Debug)] diff --git a/bitcoin-core-sv2/src/unix_capnp/v31x/template_distribution_protocol/handlers.rs b/bitcoin-core-sv2/src/unix_capnp/v31x/template_distribution_protocol/handlers.rs index ab08dfd86..9caa12025 100644 --- a/bitcoin-core-sv2/src/unix_capnp/v31x/template_distribution_protocol/handlers.rs +++ b/bitcoin-core-sv2/src/unix_capnp/v31x/template_distribution_protocol/handlers.rs @@ -1,8 +1,6 @@ //! Handlers for Bitcoin Core v31.x Sv2 Template Distribution Protocol via capnp over UNIX socket. -use crate::unix_capnp::v31x::template_distribution_protocol::{ - BitcoinCoreSv2TDP, error::BitcoinCoreSv2TDPError, -}; +use super::{BitcoinCoreSv2TDP, error::BitcoinCoreSv2TDPError}; use stratum_core::{ parsers_sv2::TemplateDistribution, template_distribution_sv2::{ diff --git a/bitcoin-core-sv2/src/unix_capnp/v31x/template_distribution_protocol/mod.rs b/bitcoin-core-sv2/src/unix_capnp/v31x/template_distribution_protocol/mod.rs index 2073363ae..0e08bf636 100644 --- a/bitcoin-core-sv2/src/unix_capnp/v31x/template_distribution_protocol/mod.rs +++ b/bitcoin-core-sv2/src/unix_capnp/v31x/template_distribution_protocol/mod.rs @@ -1,7 +1,8 @@ //! Module for interacting with Bitcoin Core v31.x via Sv2 Template Distribution Protocol via //! capnp over UNIX socket. -use crate::unix_capnp::v31x::template_distribution_protocol::template_data::TemplateData; +use self::template_data::TemplateData; +use super::capnp_types as bitcoin_capnp_types; use async_channel::{Receiver, Sender}; use bitcoin_capnp_types::{ capnp, @@ -17,7 +18,6 @@ use bitcoin_capnp_types::{ }, proxy_capnp::{thread::Client as ThreadIpcClient, thread_map::Client as ThreadMapIpcClient}, }; -use bitcoin_capnp_types_v31 as bitcoin_capnp_types; use capnp::capability::Request; use error::BitcoinCoreSv2TDPError; use std::{ diff --git a/bitcoin-core-sv2/src/unix_capnp/v31x/template_distribution_protocol/monitors.rs b/bitcoin-core-sv2/src/unix_capnp/v31x/template_distribution_protocol/monitors.rs index 9d2bee772..8659f57cb 100644 --- a/bitcoin-core-sv2/src/unix_capnp/v31x/template_distribution_protocol/monitors.rs +++ b/bitcoin-core-sv2/src/unix_capnp/v31x/template_distribution_protocol/monitors.rs @@ -1,9 +1,9 @@ //! Background monitors for Bitcoin Core v31.x Sv2 Template Distribution Protocol via capnp over //! UNIX socket. -use crate::unix_capnp::v31x::template_distribution_protocol::BitcoinCoreSv2TDP; +use super::BitcoinCoreSv2TDP; -use bitcoin_capnp_types_v31::capnp; +use super::super::capnp_types::capnp; use stratum_core::parsers_sv2::TemplateDistribution; use tracing::{debug, error, info, warn}; diff --git a/bitcoin-core-sv2/src/unix_capnp/v31x/template_distribution_protocol/template_data.rs b/bitcoin-core-sv2/src/unix_capnp/v31x/template_distribution_protocol/template_data.rs index 87a7bf74b..5b5966a44 100644 --- a/bitcoin-core-sv2/src/unix_capnp/v31x/template_distribution_protocol/template_data.rs +++ b/bitcoin-core-sv2/src/unix_capnp/v31x/template_distribution_protocol/template_data.rs @@ -1,13 +1,13 @@ //! Template-data helpers for Bitcoin Core v31.x Sv2 Template Distribution Protocol via capnp over //! UNIX socket. -use crate::unix_capnp::v31x::template_distribution_protocol::error::TemplateDataError; +use super::error::TemplateDataError; +use super::super::capnp_types as bitcoin_capnp_types; use bitcoin_capnp_types::{ mining_capnp::block_template::Client as BlockTemplateIpcClient, proxy_capnp::{thread::Client as ThreadIpcClient, thread_map::Client as ThreadMapIpcClient}, }; -use bitcoin_capnp_types_v31 as bitcoin_capnp_types; use std::{fs::File, io::Write, path::Path}; use stratum_core::bitcoin::{ Target, Transaction, TxOut, diff --git a/integration-tests/Cargo.lock b/integration-tests/Cargo.lock index d72747484..dd1d92612 100644 --- a/integration-tests/Cargo.lock +++ b/integration-tests/Cargo.lock @@ -1072,6 +1072,16 @@ dependencies = [ "capnpc", ] +[[package]] +name = "bitcoin-capnp-types" +version = "0.2.1" +source = "git+https://github.com/2140-dev/bitcoin-capnp-types?branch=master#93d33e47720cccff822be4fee950ada8e9578538" +dependencies = [ + "capnp", + "capnp-rpc", + "capnpc", +] + [[package]] name = "bitcoin-internals" version = "0.3.0" @@ -1103,7 +1113,8 @@ version = "0.4.0" dependencies = [ "async-channel 1.9.0", "bitcoin-capnp-types 0.1.2", - "bitcoin-capnp-types 0.2.1", + "bitcoin-capnp-types 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bitcoin-capnp-types 0.2.1 (git+https://github.com/2140-dev/bitcoin-capnp-types?branch=master)", "stratum-core", "tokio", "tokio-util", diff --git a/miner-apps/Cargo.lock b/miner-apps/Cargo.lock index be79dc9c6..1177ce08e 100644 --- a/miner-apps/Cargo.lock +++ b/miner-apps/Cargo.lock @@ -884,6 +884,16 @@ dependencies = [ "capnpc", ] +[[package]] +name = "bitcoin-capnp-types" +version = "0.2.1" +source = "git+https://github.com/2140-dev/bitcoin-capnp-types?branch=master#93d33e47720cccff822be4fee950ada8e9578538" +dependencies = [ + "capnp", + "capnp-rpc", + "capnpc", +] + [[package]] name = "bitcoin-internals" version = "0.2.0" @@ -917,7 +927,8 @@ version = "0.4.0" dependencies = [ "async-channel", "bitcoin-capnp-types 0.1.2", - "bitcoin-capnp-types 0.2.1", + "bitcoin-capnp-types 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bitcoin-capnp-types 0.2.1 (git+https://github.com/2140-dev/bitcoin-capnp-types?branch=master)", "stratum-core", "tokio", "tokio-util", diff --git a/pool-apps/Cargo.lock b/pool-apps/Cargo.lock index 902f8d41c..1a3c5e950 100644 --- a/pool-apps/Cargo.lock +++ b/pool-apps/Cargo.lock @@ -318,6 +318,16 @@ dependencies = [ "capnpc", ] +[[package]] +name = "bitcoin-capnp-types" +version = "0.2.1" +source = "git+https://github.com/2140-dev/bitcoin-capnp-types?branch=master#93d33e47720cccff822be4fee950ada8e9578538" +dependencies = [ + "capnp", + "capnp-rpc", + "capnpc", +] + [[package]] name = "bitcoin-internals" version = "0.2.0" @@ -351,7 +361,8 @@ version = "0.4.0" dependencies = [ "async-channel", "bitcoin-capnp-types 0.1.2", - "bitcoin-capnp-types 0.2.1", + "bitcoin-capnp-types 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bitcoin-capnp-types 0.2.1 (git+https://github.com/2140-dev/bitcoin-capnp-types?branch=master)", "stratum-core", "tokio", "tokio-util", diff --git a/stratum-apps/Cargo.lock b/stratum-apps/Cargo.lock index cbc7f9c38..deb3cd2e2 100644 --- a/stratum-apps/Cargo.lock +++ b/stratum-apps/Cargo.lock @@ -819,6 +819,16 @@ dependencies = [ "capnpc", ] +[[package]] +name = "bitcoin-capnp-types" +version = "0.2.1" +source = "git+https://github.com/2140-dev/bitcoin-capnp-types?branch=master#93d33e47720cccff822be4fee950ada8e9578538" +dependencies = [ + "capnp", + "capnp-rpc", + "capnpc", +] + [[package]] name = "bitcoin-internals" version = "0.2.0" @@ -852,7 +862,8 @@ version = "0.4.0" dependencies = [ "async-channel", "bitcoin-capnp-types 0.1.2", - "bitcoin-capnp-types 0.2.1", + "bitcoin-capnp-types 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bitcoin-capnp-types 0.2.1 (git+https://github.com/2140-dev/bitcoin-capnp-types?branch=master)", "stratum-core", "tokio", "tokio-util", From 933d8b4586c2efbb0b1f235a59a73f53318bce3b Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 7 Jul 2026 15:54:18 +0200 Subject: [PATCH 08/11] ci: build Bitcoin Core master once per OS Add a per-OS job that builds bitcoin/bitcoin@master and uploads bitcoin-node. Behavior changes: - Source-built Core binaries are produced once per OS for test jobs. - Source builds restore and save a per-OS ccache cache. - Bitcoin Core C/C++ compilation uses ccache. --- .../actions/classify-bitcoin-core/action.yml | 41 ++++++++++++++ .github/workflows/integration-tests.yaml | 56 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 .github/actions/classify-bitcoin-core/action.yml diff --git a/.github/actions/classify-bitcoin-core/action.yml b/.github/actions/classify-bitcoin-core/action.yml new file mode 100644 index 000000000..dc4f87ce8 --- /dev/null +++ b/.github/actions/classify-bitcoin-core/action.yml @@ -0,0 +1,41 @@ +name: Classify Bitcoin Core version +description: Classify a Bitcoin Core matrix entry as a release tag or source ref. + +inputs: + version: + description: Bitcoin Core release tag or repo@ref source entry. + required: true + +outputs: + cache-key: + description: Artifact-safe cache key for the matrix entry. + value: ${{ steps.classify.outputs.cache-key }} + source: + description: Whether the matrix entry points to a source ref. + value: ${{ steps.classify.outputs.source }} + repo: + description: GitHub repository for source entries. + value: ${{ steps.classify.outputs.repo }} + ref: + description: Git ref for source entries. + value: ${{ steps.classify.outputs.ref }} + +runs: + using: composite + steps: + - name: Classify Bitcoin Core version + id: classify + shell: bash + run: | + bitcoin_core_version="${{ inputs.version }}" + cache_key="${bitcoin_core_version//[^A-Za-z0-9_.-]/-}" + { + echo "cache-key=$cache_key" + if [[ "$bitcoin_core_version" == *@* ]]; then + echo "source=true" + echo "repo=${bitcoin_core_version%@*}" + echo "ref=${bitcoin_core_version#*@}" + else + echo "source=false" + fi + } >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/integration-tests.yaml b/.github/workflows/integration-tests.yaml index cef0a822f..51f1bf960 100644 --- a/.github/workflows/integration-tests.yaml +++ b/.github/workflows/integration-tests.yaml @@ -59,6 +59,62 @@ jobs: path: integration-test-binaries-${{ runner.os }}.tar.zst if-no-files-found: error + bitcoin-core: + name: Build Bitcoin Core (${{ matrix.os }}, ${{ matrix.bitcoin_core_version }}) + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + - macos-15 + bitcoin_core_version: + - bitcoin/bitcoin@master + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Classify Bitcoin Core version + id: bitcoin-core + uses: ./.github/actions/classify-bitcoin-core + with: + version: ${{ matrix.bitcoin_core_version }} + + - name: Install Bitcoin Core build dependencies (Ubuntu) + if: matrix.os == 'ubuntu-latest' + run: sudo apt-get install -y build-essential cmake pkgconf python3 libevent-dev libboost-dev libsqlite3-dev capnproto libcapnp-dev ccache + + - name: Install Bitcoin Core build dependencies (macOS) + if: matrix.os == 'macos-15' + run: brew install cmake boost pkgconf libevent sqlite capnp ccache + + - name: ccache + uses: actions/cache@v6.1.0 + with: + path: ~/.cache/ccache + key: ccache-bitcoin-${{ runner.os }}-${{ github.sha }} + restore-keys: ccache-bitcoin-${{ runner.os }}- + + - name: Checkout Bitcoin Core source + run: | + git clone --filter=blob:none --no-checkout "https://github.com/${{ steps.bitcoin-core.outputs.repo }}.git" bitcoin + git -C bitcoin fetch --depth 1 origin "${{ steps.bitcoin-core.outputs.ref }}" + git -C bitcoin checkout --detach FETCH_HEAD + + - name: Build Bitcoin Core + run: | + cd bitcoin + export CCACHE_DIR="$HOME/.cache/ccache" + cmake -B build -DENABLE_WALLET=ON -DBUILD_TESTS=OFF -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache + cmake --build build -j "$(nproc 2>/dev/null || sysctl -n hw.ncpu)" + + - name: Upload Bitcoin Core binary + uses: actions/upload-artifact@v7 + with: + name: bitcoin-core-${{ runner.os }}-${{ steps.bitcoin-core.outputs.cache-key }} + path: bitcoin/build/bin/bitcoin-node + if-no-files-found: error + ci: name: Integration tests (${{ matrix.os }}) needs: build From 57700e4e7cbec7dbeb0230d865418ded11a81036 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 7 Jul 2026 15:55:00 +0200 Subject: [PATCH 09/11] ci: test against Bitcoin Core master Run the integration test job against bitcoin/bitcoin@master using the source-built bitcoin-node artifact, driven through a shared setup-bitcoin-integration-test action. Behavior changes: - BITCOIN_CORE_VERSION=bitcoin/bitcoin@master selects the Master IPC adapter. - The test archive build updates bitcoin-capnp-types master before compiling. --- .../setup-bitcoin-core-binary/action.yml | 42 +++++++++++++++++++ .../setup-bitcoin-integration-test/action.yml | 38 +++++++++++++++++ .github/workflows/integration-tests.yaml | 21 +++++++++- .../common/job_declaration_protocol/mod.rs | 19 ++++++++- bitcoin-core-sv2/src/common/mod.rs | 12 +++--- .../template_distribution_protocol/mod.rs | 21 +++++++++- integration-tests/lib/template_provider.rs | 17 +++++--- .../tests/bitcoin_core_ipc_jdp_io.rs | 8 ++++ .../tests/bitcoin_core_ipc_tdp_io.rs | 8 ++++ 9 files changed, 172 insertions(+), 14 deletions(-) create mode 100644 .github/actions/setup-bitcoin-core-binary/action.yml create mode 100644 .github/actions/setup-bitcoin-integration-test/action.yml diff --git a/.github/actions/setup-bitcoin-core-binary/action.yml b/.github/actions/setup-bitcoin-core-binary/action.yml new file mode 100644 index 000000000..ea2d0f242 --- /dev/null +++ b/.github/actions/setup-bitcoin-core-binary/action.yml @@ -0,0 +1,42 @@ +name: Setup Bitcoin Core binary +description: Download and prepare a source-built Bitcoin Core binary for tests. + +inputs: + source: + description: Whether the current Bitcoin Core matrix entry is source-built. + required: true + cache-key: + description: Artifact-safe key for the Bitcoin Core source build. + required: true + +runs: + using: composite + steps: + - name: Install Bitcoin Core runtime dependencies + if: inputs.source == 'true' + shell: bash + run: | + case "$RUNNER_OS" in + Linux) + sudo apt-get install -y libevent-dev libboost-dev libsqlite3-dev + ;; + macOS) + brew install boost libevent sqlite + ;; + *) + echo "Unsupported runner OS: $RUNNER_OS" >&2 + exit 1 + ;; + esac + + - name: Download Bitcoin Core binary + if: inputs.source == 'true' + uses: actions/download-artifact@v8 + with: + name: bitcoin-core-${{ runner.os }}-${{ inputs.cache-key }} + path: bitcoin-core + + - name: Prepare Bitcoin Core binary + if: inputs.source == 'true' + shell: bash + run: chmod +x bitcoin-core/bitcoin-node diff --git a/.github/actions/setup-bitcoin-integration-test/action.yml b/.github/actions/setup-bitcoin-integration-test/action.yml new file mode 100644 index 000000000..97050173f --- /dev/null +++ b/.github/actions/setup-bitcoin-integration-test/action.yml @@ -0,0 +1,38 @@ +name: Setup Bitcoin integration test runtime +description: Install IPC runtime dependencies and prepare a Bitcoin Core source binary when needed. + +inputs: + bitcoin-core-version: + description: Bitcoin Core release tag or repo@ref source entry. + required: true + +runs: + using: composite + steps: + - name: Install capnp dependencies + shell: bash + run: | + case "$RUNNER_OS" in + Linux) + sudo apt-get install -y capnproto libcapnp-dev + ;; + macOS) + brew install capnp + ;; + *) + echo "Unsupported runner OS: $RUNNER_OS" >&2 + exit 1 + ;; + esac + + - name: Classify Bitcoin Core version + id: bitcoin-core + uses: ./.github/actions/classify-bitcoin-core + with: + version: ${{ inputs.bitcoin-core-version }} + + - name: Setup Bitcoin Core binary + uses: ./.github/actions/setup-bitcoin-core-binary + with: + source: ${{ steps.bitcoin-core.outputs.source }} + cache-key: ${{ steps.bitcoin-core.outputs.cache-key }} diff --git a/.github/workflows/integration-tests.yaml b/.github/workflows/integration-tests.yaml index 51f1bf960..68faf1c9b 100644 --- a/.github/workflows/integration-tests.yaml +++ b/.github/workflows/integration-tests.yaml @@ -48,6 +48,9 @@ jobs: with: github-token: ${{ github.token }} + - name: Update Bitcoin Core master IPC bindings + run: cargo update --manifest-path=integration-tests/Cargo.toml 'git+https://github.com/2140-dev/bitcoin-capnp-types?branch=master#0.2.1' + - name: Build integration test binaries run: | cargo nextest archive --manifest-path=integration-tests/Cargo.toml --archive-file integration-test-binaries-${{ runner.os }}.tar.zst @@ -116,21 +119,35 @@ jobs: if-no-files-found: error ci: - name: Integration tests (${{ matrix.os }}) - needs: build + name: Integration tests (${{ matrix.os }}, ${{ matrix.bitcoin_core_version }}) + needs: + - build + - bitcoin-core runs-on: ${{ matrix.os }} strategy: matrix: os: - ubuntu-latest - macos-15 + bitcoin_core_version: + - bitcoin/bitcoin@master + include: + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu steps: - name: Checkout repository uses: actions/checkout@v6 + - name: Setup Bitcoin integration runtime + uses: ./.github/actions/setup-bitcoin-integration-test + with: + bitcoin-core-version: ${{ matrix.bitcoin_core_version }} + - name: Run archived integration tests uses: ./.github/actions/run-archived-nextest with: profile: default github-token: ${{ github.token }} + bitcoin-core-version: ${{ matrix.bitcoin_core_version }} + bitcoin-core-binary: ${{ github.workspace }}/bitcoin-core/bitcoin-node diff --git a/bitcoin-core-sv2/src/common/job_declaration_protocol/mod.rs b/bitcoin-core-sv2/src/common/job_declaration_protocol/mod.rs index d2b45671f..e53eedca0 100644 --- a/bitcoin-core-sv2/src/common/job_declaration_protocol/mod.rs +++ b/bitcoin-core-sv2/src/common/job_declaration_protocol/mod.rs @@ -14,12 +14,13 @@ pub mod io; use crate::{ common::{BitcoinCoreSv2Error, BitcoinCoreSv2Protocol, BitcoinCoreVersion}, - unix_capnp::{v30x, v31x}, + unix_capnp::{master, v30x, v31x}, }; use async_channel::Receiver; use io::JdRequest; use std::path::Path; pub use tokio_util::sync::CancellationToken; +use tracing::info; /// Version-agnostic JDP runtime handle. /// @@ -28,6 +29,7 @@ pub use tokio_util::sync::CancellationToken; pub enum BitcoinCoreSv2JDP { V30X(v30x::job_declaration_protocol::BitcoinCoreSv2JDP), V31X(v31x::job_declaration_protocol::BitcoinCoreSv2JDP), + Master(master::job_declaration_protocol::BitcoinCoreSv2JDP), } impl BitcoinCoreSv2JDP { @@ -35,6 +37,7 @@ impl BitcoinCoreSv2JDP { match self { Self::V30X(runtime) => runtime.run().await, Self::V31X(runtime) => runtime.run().await, + Self::Master(runtime) => runtime.run().await, } } } @@ -75,5 +78,19 @@ where .map_err(|error| { BitcoinCoreSv2JDPError::from_debug(version, BitcoinCoreSv2Protocol::JDP, error) }), + BitcoinCoreVersion::Master => { + info!("Using Bitcoin Core master JDP adapter backed by master IPC bindings"); + master::job_declaration_protocol::BitcoinCoreSv2JDP::new( + bitcoin_core_unix_socket_path, + incoming_requests, + cancellation_token, + ready_tx, + ) + .await + .map(BitcoinCoreSv2JDP::Master) + .map_err(|error| { + BitcoinCoreSv2JDPError::from_debug(version, BitcoinCoreSv2Protocol::JDP, error) + }) + } } } diff --git a/bitcoin-core-sv2/src/common/mod.rs b/bitcoin-core-sv2/src/common/mod.rs index ab537dd03..52fbe09c8 100644 --- a/bitcoin-core-sv2/src/common/mod.rs +++ b/bitcoin-core-sv2/src/common/mod.rs @@ -10,13 +10,15 @@ use std::fmt; pub enum BitcoinCoreVersion { V30X, V31X, + Master, } impl BitcoinCoreVersion { - pub const fn as_major(self) -> u8 { + pub const fn as_label(self) -> &'static str { match self { - Self::V30X => 30, - Self::V31X => 31, + Self::V30X => "30", + Self::V31X => "31", + Self::Master => "master", } } } @@ -78,9 +80,9 @@ impl fmt::Display for BitcoinCoreSv2Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "failed to initialize bitcoin_core_sv2 {} for v{}: {}", + "failed to initialize bitcoin_core_sv2 {} for {}: {}", self.protocol.as_str(), - self.version.as_major(), + self.version.as_label(), self.details ) } diff --git a/bitcoin-core-sv2/src/common/template_distribution_protocol/mod.rs b/bitcoin-core-sv2/src/common/template_distribution_protocol/mod.rs index 78b8f9633..f34560718 100644 --- a/bitcoin-core-sv2/src/common/template_distribution_protocol/mod.rs +++ b/bitcoin-core-sv2/src/common/template_distribution_protocol/mod.rs @@ -14,12 +14,13 @@ use crate::{ common::{BitcoinCoreSv2Error, BitcoinCoreSv2Protocol, BitcoinCoreVersion}, - unix_capnp::{v30x, v31x}, + unix_capnp::{master, v30x, v31x}, }; use async_channel::{Receiver, Sender}; use std::path::Path; use stratum_core::parsers_sv2::TemplateDistribution; pub use tokio_util::sync::CancellationToken; +use tracing::info; /// Version-agnostic TDP runtime handle. /// @@ -28,6 +29,7 @@ pub use tokio_util::sync::CancellationToken; pub enum BitcoinCoreSv2TDP { V30X(v30x::template_distribution_protocol::BitcoinCoreSv2TDP), V31X(v31x::template_distribution_protocol::BitcoinCoreSv2TDP), + Master(master::template_distribution_protocol::BitcoinCoreSv2TDP), } impl BitcoinCoreSv2TDP { @@ -35,6 +37,7 @@ impl BitcoinCoreSv2TDP { match self { Self::V30X(runtime) => runtime.run().await, Self::V31X(runtime) => runtime.run().await, + Self::Master(runtime) => runtime.run().await, } } } @@ -82,5 +85,21 @@ where .map_err(|error| { BitcoinCoreSv2TDPError::from_debug(version, BitcoinCoreSv2Protocol::TDP, error) }), + BitcoinCoreVersion::Master => { + info!("Using Bitcoin Core master TDP adapter backed by master IPC bindings"); + master::template_distribution_protocol::BitcoinCoreSv2TDP::new( + bitcoin_core_unix_socket_path, + fee_threshold, + min_interval, + incoming_messages, + outgoing_messages, + global_cancellation_token, + ) + .await + .map(BitcoinCoreSv2TDP::Master) + .map_err(|error| { + BitcoinCoreSv2TDPError::from_debug(version, BitcoinCoreSv2Protocol::TDP, error) + }) + } } } diff --git a/integration-tests/lib/template_provider.rs b/integration-tests/lib/template_provider.rs index 5ea1eb8a9..5c0992b82 100644 --- a/integration-tests/lib/template_provider.rs +++ b/integration-tests/lib/template_provider.rs @@ -61,6 +61,10 @@ fn get_bitcoin_core_filename(os: &str, arch: &str, bitcoin_core_version: &str) - pub const BITCOIN_CORE_LATEST: BitcoinCoreVersion = BitcoinCoreVersion::V31X; fn parse_ipc_version(version: &str) -> Option { + if version.contains('@') || version == "master" { + return Some(BitcoinCoreVersion::Master); + } + let version = version.strip_prefix('v').unwrap_or(version); let major = version.split('.').next().unwrap_or(version); match major { @@ -77,23 +81,26 @@ pub fn selected_bitcoin_core_version() -> BitcoinCoreVersion { } if let Ok(version) = env::var("BITCOIN_CORE_VERSION") { - if !version.contains('@') { - return parse_ipc_version(&version) - .unwrap_or_else(|| panic!("Unsupported BITCOIN_CORE_VERSION release: {version}")); - } + return parse_ipc_version(&version) + .unwrap_or_else(|| panic!("Unsupported BITCOIN_CORE_VERSION release: {version}")); } BITCOIN_CORE_LATEST } pub fn should_run_bitcoin_core_version(version: BitcoinCoreVersion) -> bool { - env::var("BITCOIN_CORE_VERSION").is_err() || selected_bitcoin_core_version() == version + if env::var("BITCOIN_CORE_VERSION").is_err() { + return version != BitcoinCoreVersion::Master; + } + + selected_bitcoin_core_version() == version } fn release_version(version: BitcoinCoreVersion) -> &'static str { match version { BitcoinCoreVersion::V30X => BITCOIN_CORE_V30X, BitcoinCoreVersion::V31X => BITCOIN_CORE_V31X, + BitcoinCoreVersion::Master => BITCOIN_CORE_V31X, } } diff --git a/integration-tests/tests/bitcoin_core_ipc_jdp_io.rs b/integration-tests/tests/bitcoin_core_ipc_jdp_io.rs index 5933b3cc4..a9afb8d8c 100644 --- a/integration-tests/tests/bitcoin_core_ipc_jdp_io.rs +++ b/integration-tests/tests/bitcoin_core_ipc_jdp_io.rs @@ -51,6 +51,14 @@ async fn jdp_io_integration_v31x() { assert_jdp_io_integration_for_version(BitcoinCoreVersion::V31X).await; } +#[tokio::test] +async fn jdp_io_integration_master() { + if !should_run_bitcoin_core_version(BitcoinCoreVersion::Master) { + return; + } + assert_jdp_io_integration_for_version(BitcoinCoreVersion::Master).await; +} + async fn assert_jdp_io_integration_for_version(version: BitcoinCoreVersion) { start_tracing(); diff --git a/integration-tests/tests/bitcoin_core_ipc_tdp_io.rs b/integration-tests/tests/bitcoin_core_ipc_tdp_io.rs index a575452c0..1343961f7 100644 --- a/integration-tests/tests/bitcoin_core_ipc_tdp_io.rs +++ b/integration-tests/tests/bitcoin_core_ipc_tdp_io.rs @@ -47,6 +47,14 @@ async fn tdp_io_integration_v31x() { assert_tdp_io_integration(BitcoinCoreVersion::V31X).await; } +#[tokio::test] +async fn tdp_io_integration_master() { + if !should_run_bitcoin_core_version(BitcoinCoreVersion::Master) { + return; + } + assert_tdp_io_integration(BitcoinCoreVersion::Master).await; +} + async fn assert_tdp_io_integration(version: BitcoinCoreVersion) { start_tracing(); From fdaf5f8f0edf019846b9769495c8d99dd931f04b Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 7 Jul 2026 15:50:26 +0200 Subject: [PATCH 10/11] ci: matrix integration tests across Bitcoin Core versions Add v30.2 and v31.0 release legs beside bitcoin/bitcoin@master. Behavior changes: - CI covers v30.2, v31.0, and master on each OS. - Standalone sv2-tp uses v1.0.6 for v30.x and v1.1.0 for v31.x/master. --- .github/workflows/integration-tests.yaml | 2 + integration-tests/lib/template_provider.rs | 47 ++++++++++++++----- .../tests/bitcoin_core_ipc_integration.rs | 12 ++--- 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/.github/workflows/integration-tests.yaml b/.github/workflows/integration-tests.yaml index 68faf1c9b..ff910667c 100644 --- a/.github/workflows/integration-tests.yaml +++ b/.github/workflows/integration-tests.yaml @@ -130,6 +130,8 @@ jobs: - ubuntu-latest - macos-15 bitcoin_core_version: + - v30.2 + - v31.0 - bitcoin/bitcoin@master include: - os: ubuntu-latest diff --git a/integration-tests/lib/template_provider.rs b/integration-tests/lib/template_provider.rs index 5c0992b82..4e2dd1986 100644 --- a/integration-tests/lib/template_provider.rs +++ b/integration-tests/lib/template_provider.rs @@ -13,7 +13,8 @@ use tracing::warn; use crate::utils::{fs_utils, http, tarball}; -const VERSION_SV2_TP: &str = "1.1.0"; +const VERSION_SV2_TP_V30X: &str = "1.0.6"; +const VERSION_SV2_TP_V31X: &str = "1.1.0"; const BITCOIN_CORE_V30X: &str = "30.2"; const BITCOIN_CORE_V31X: &str = "31.0"; /// Allow static signet fixtures to leave IBD without freezing Bitcoin Core's @@ -28,17 +29,17 @@ const BITCOIN_CORE_V31X: &str = "31.0"; /// stale-tip IBD threshold; it does not change Bitcoin Core's clock. const SIGNET_FIXTURE_MAX_TIP_AGE_SECS: u64 = 100 * 365 * 24 * 60 * 60; -fn get_sv2_tp_filename(os: &str, arch: &str) -> String { +fn get_sv2_tp_filename(os: &str, arch: &str, sv2_tp_version: &str) -> String { match (os, arch) { ("macos", "aarch64") => { - format!("sv2-tp-{VERSION_SV2_TP}-arm64-apple-darwin.tar.gz") + format!("sv2-tp-{sv2_tp_version}-arm64-apple-darwin.tar.gz") } ("macos", "x86_64") => { - format!("sv2-tp-{VERSION_SV2_TP}-x86_64-apple-darwin.tar.gz") + format!("sv2-tp-{sv2_tp_version}-x86_64-apple-darwin.tar.gz") } - ("linux", "x86_64") => format!("sv2-tp-{VERSION_SV2_TP}-x86_64-linux-gnu.tar.gz"), - ("linux", "aarch64") => format!("sv2-tp-{VERSION_SV2_TP}-aarch64-linux-gnu.tar.gz"), - _ => format!("sv2-tp-{VERSION_SV2_TP}-x86_64-apple-darwin.tar.gz"), + ("linux", "x86_64") => format!("sv2-tp-{sv2_tp_version}-x86_64-linux-gnu.tar.gz"), + ("linux", "aarch64") => format!("sv2-tp-{sv2_tp_version}-aarch64-linux-gnu.tar.gz"), + _ => format!("sv2-tp-{sv2_tp_version}-x86_64-apple-darwin.tar.gz"), } } @@ -104,6 +105,22 @@ fn release_version(version: BitcoinCoreVersion) -> &'static str { } } +fn sv2_tp_version(version: BitcoinCoreVersion) -> &'static str { + match version { + BitcoinCoreVersion::V30X => VERSION_SV2_TP_V30X, + BitcoinCoreVersion::V31X => VERSION_SV2_TP_V31X, + BitcoinCoreVersion::Master => VERSION_SV2_TP_V31X, + } +} + +fn sv2_tp_template_interval_arg(version: BitcoinCoreVersion, sv2_interval: u32) -> String { + match version { + BitcoinCoreVersion::V30X => format!("-sv2interval={sv2_interval}"), + BitcoinCoreVersion::V31X => format!("-templateinterval={sv2_interval}"), + BitcoinCoreVersion::Master => format!("-templateinterval={sv2_interval}"), + } +} + fn selected_release_version(ipc_version: BitcoinCoreVersion) -> String { match env::var("BITCOIN_CORE_VERSION") { Ok(version) if !version.contains('@') => { @@ -430,8 +447,8 @@ pub struct TemplateProvider { impl TemplateProvider { /// Start a new [`TemplateProvider`] instance with Bitcoin Core and standalone sv2-tp. pub fn start(port: u16, sv2_interval: u32, difficulty_level: DifficultyLevel) -> Self { - let bitcoin_core = - BitcoinCore::start(port, difficulty_level, selected_bitcoin_core_version()); + let bitcoin_core_version = selected_bitcoin_core_version(); + let bitcoin_core = BitcoinCore::start(port, difficulty_level, bitcoin_core_version); let current_dir: PathBuf = std::env::current_dir().expect("failed to read current dir"); let bin_dir = current_dir.join("template-provider"); @@ -439,8 +456,9 @@ impl TemplateProvider { // Download and setup sv2-tp binary let os = env::consts::OS; let arch = env::consts::ARCH; - let sv2_tp_filename = get_sv2_tp_filename(os, arch); - let sv2_tp_home = bin_dir.join(format!("sv2-tp-{VERSION_SV2_TP}")); + let sv2_tp_version = sv2_tp_version(bitcoin_core_version); + let sv2_tp_filename = get_sv2_tp_filename(os, arch, sv2_tp_version); + let sv2_tp_home = bin_dir.join(format!("sv2-tp-{sv2_tp_version}")); let sv2_tp_bin = sv2_tp_home.join("bin").join("sv2-tp"); if !sv2_tp_bin.exists() { @@ -452,7 +470,7 @@ impl TemplateProvider { env::var("SV2TP_DOWNLOAD_ENDPOINT").unwrap_or_else(|_| { "https://github.com/stratum-mining/sv2-tp/releases/download".to_owned() }); - let url = format!("{download_endpoint}/v{VERSION_SV2_TP}/{sv2_tp_filename}"); + let url = format!("{download_endpoint}/v{sv2_tp_version}/{sv2_tp_filename}"); http::make_get_request(&url, 5) } }; @@ -486,7 +504,10 @@ impl TemplateProvider { .arg(network) .arg(format!("-datadir={}", datadir.display())) .arg(format!("-sv2port={port}")) - .arg(format!("-templateinterval={sv2_interval}")) + .arg(sv2_tp_template_interval_arg( + bitcoin_core_version, + sv2_interval, + )) .arg("-sv2feedelta=0") .arg("-debug=sv2") .arg("-loglevel=sv2:trace") diff --git a/integration-tests/tests/bitcoin_core_ipc_integration.rs b/integration-tests/tests/bitcoin_core_ipc_integration.rs index dcd2fcab6..8df282125 100644 --- a/integration-tests/tests/bitcoin_core_ipc_integration.rs +++ b/integration-tests/tests/bitcoin_core_ipc_integration.rs @@ -55,10 +55,10 @@ async fn pool_propagates_block_with_bitcoin_core_ipc() { #[tokio::test] async fn jdc_propagates_block_with_bitcoin_core_ipc() { start_tracing(); - let (tp, _tp_addr) = start_template_provider(None, DifficultyLevel::Low); - let current_block_hash = tp.get_best_block_hash().unwrap(); + let bitcoin_core = start_bitcoin_core_latest(DifficultyLevel::Low); + let current_block_hash = bitcoin_core.get_best_block_hash().unwrap(); let (pool, pool_addr, jds_addr, _) = - start_pool_with_jds(tp.bitcoin_core(), vec![], vec![], false).await; + start_pool_with_jds(&bitcoin_core, vec![], vec![], false).await; let ignore_push_solution = IgnoreMessage::new(MessageDirection::ToUpstream, MESSAGE_TYPE_PUSH_SOLUTION); let (sniffer, sniffer_addr) = start_sniffer( @@ -71,8 +71,8 @@ async fn jdc_propagates_block_with_bitcoin_core_ipc() { let (jdc, jdc_addr, _) = start_jdc( &[(pool_addr, sniffer_addr)], ipc_config( - tp.bitcoin_core().data_dir().clone(), - tp.bitcoin_core().is_signet(), + bitcoin_core.data_dir().clone(), + bitcoin_core.is_signet(), None, ), vec![], @@ -109,7 +109,7 @@ async fn jdc_propagates_block_with_bitcoin_core_ipc() { let start_time = tokio::time::Instant::now(); loop { tokio::time::sleep(poll_interval).await; - let new_block_hash = tp.get_best_block_hash().unwrap(); + let new_block_hash = bitcoin_core.get_best_block_hash().unwrap(); if new_block_hash != current_block_hash { sniffer .assert_message_not_present( From 620bff08e67b201f6dad280e9f4564b880904e3f Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 7 Jul 2026 20:50:50 +0200 Subject: [PATCH 11/11] ci: split integration tests by runtime dependency and role Split archived integration tests into node-free, BitcoinCoreIpc, and standalone sv2-tp jobs. Behavior changes: - Node-free tests run once per OS. - BitcoinCoreIpc and standalone sv2-tp tests stay in the Bitcoin Core version matrix. The standalone sv2-tp bucket would run ~77 tests serially (~23 minutes), dwarfing the other jobs, so it is further split into three roughly equal-duration buckets, grouped by role so a failing job points at a subsystem: - ci-sv2-tp-jd: jd_*, jdc_*, jds_* (~6.5 min) - ci-sv2-tp-translator: translator, monitoring (~8.5 min) - ci-sv2-tp-other: pool, sv1, sniffer, ... (~7.5 min) Durations are based on per-test timings from a previous CI run. ci-sv2-tp-other is a catch-all (not one of the other profiles), so new test binaries run there by default without touching the filters. --- .github/workflows/integration-tests.yaml | 135 ++++++++++++++++++++++- integration-tests/.config/nextest.toml | 24 +++- 2 files changed, 155 insertions(+), 4 deletions(-) diff --git a/.github/workflows/integration-tests.yaml b/.github/workflows/integration-tests.yaml index ff910667c..d2389d49e 100644 --- a/.github/workflows/integration-tests.yaml +++ b/.github/workflows/integration-tests.yaml @@ -118,8 +118,137 @@ jobs: path: bitcoin/build/bin/bitcoin-node if-no-files-found: error - ci: - name: Integration tests (${{ matrix.os }}, ${{ matrix.bitcoin_core_version }}) + ci-no-bitcoin: + name: Integration tests without Bitcoin Core (${{ matrix.os }}) + needs: + - build + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + - macos-15 + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Run archived integration tests + uses: ./.github/actions/run-archived-nextest + with: + profile: ci-no-bitcoin + github-token: ${{ github.token }} + + ci-bitcoin-core-ipc: + name: Integration tests BitcoinCoreIpc (${{ matrix.os }}, ${{ matrix.bitcoin_core_version }}) + needs: + - build + - bitcoin-core + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + - macos-15 + bitcoin_core_version: + - v30.2 + - v31.0 + - bitcoin/bitcoin@master + include: + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Bitcoin integration runtime + uses: ./.github/actions/setup-bitcoin-integration-test + with: + bitcoin-core-version: ${{ matrix.bitcoin_core_version }} + + - name: Run archived integration tests + uses: ./.github/actions/run-archived-nextest + with: + profile: ci-bitcoin-core-ipc + github-token: ${{ github.token }} + bitcoin-core-version: ${{ matrix.bitcoin_core_version }} + bitcoin-core-binary: ${{ github.workspace }}/bitcoin-core/bitcoin-node + + ci-sv2-tp-jd: + name: sv2-tp jd (${{ matrix.os }}, ${{ matrix.bitcoin_core_version }}) + needs: + - build + - bitcoin-core + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + - macos-15 + bitcoin_core_version: + - v30.2 + - v31.0 + - bitcoin/bitcoin@master + include: + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Bitcoin integration runtime + uses: ./.github/actions/setup-bitcoin-integration-test + with: + bitcoin-core-version: ${{ matrix.bitcoin_core_version }} + + - name: Run archived integration tests + uses: ./.github/actions/run-archived-nextest + with: + profile: ci-sv2-tp-jd + github-token: ${{ github.token }} + bitcoin-core-version: ${{ matrix.bitcoin_core_version }} + bitcoin-core-binary: ${{ github.workspace }}/bitcoin-core/bitcoin-node + + ci-sv2-tp-translator: + name: sv2-tp translator (${{ matrix.os }}, ${{ matrix.bitcoin_core_version }}) + needs: + - build + - bitcoin-core + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + - macos-15 + bitcoin_core_version: + - v30.2 + - v31.0 + - bitcoin/bitcoin@master + include: + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Bitcoin integration runtime + uses: ./.github/actions/setup-bitcoin-integration-test + with: + bitcoin-core-version: ${{ matrix.bitcoin_core_version }} + + - name: Run archived integration tests + uses: ./.github/actions/run-archived-nextest + with: + profile: ci-sv2-tp-translator + github-token: ${{ github.token }} + bitcoin-core-version: ${{ matrix.bitcoin_core_version }} + bitcoin-core-binary: ${{ github.workspace }}/bitcoin-core/bitcoin-node + + ci-sv2-tp-other: + name: sv2-tp other (${{ matrix.os }}, ${{ matrix.bitcoin_core_version }}) needs: - build - bitcoin-core @@ -149,7 +278,7 @@ jobs: - name: Run archived integration tests uses: ./.github/actions/run-archived-nextest with: - profile: default + profile: ci-sv2-tp-other github-token: ${{ github.token }} bitcoin-core-version: ${{ matrix.bitcoin_core_version }} bitcoin-core-binary: ${{ github.workspace }}/bitcoin-core/bitcoin-node diff --git a/integration-tests/.config/nextest.toml b/integration-tests/.config/nextest.toml index 5f5d26a09..7492d66de 100644 --- a/integration-tests/.config/nextest.toml +++ b/integration-tests/.config/nextest.toml @@ -14,4 +14,26 @@ slow-timeout = { period = "60s", terminate-after = 2 } # display status for all levels (pass, fail, flaky, slow, etc) status-level = "all" -final-status-level = "all" \ No newline at end of file +final-status-level = "all" + +[profile.ci-no-bitcoin] +default-filter = 'binary(=integration_tests_sv2) | binary(=mining_device_fast_hasher_equivalence)' + +[profile.ci-bitcoin-core-ipc] +default-filter = 'binary(~bitcoin_core_ipc)' + +# The standalone sv2-tp tests are split into three roughly equal-duration +# buckets, grouped by role. New test binaries run in ci-sv2-tp-other unless +# their name matches one of the more specific buckets below. + +# Job declaration stack: jd_*, jdc_*, jds_* +# (bitcoin_core_ipc_jdp_io also matches ~jd, but belongs to ci-bitcoin-core-ipc) +[profile.ci-sv2-tp-jd] +default-filter = 'binary(~jd) and not binary(~bitcoin_core_ipc)' + +[profile.ci-sv2-tp-translator] +default-filter = 'binary(~translator) | binary(~monitoring)' + +# Catch-all for the remaining sv2-tp tests (pool, sv1, sniffer, extensions, ...) +[profile.ci-sv2-tp-other] +default-filter = 'not (binary(~jd) | binary(~translator) | binary(~monitoring) | binary(=integration_tests_sv2) | binary(=mining_device_fast_hasher_equivalence) | binary(~bitcoin_core_ipc))'