diff --git a/.github/workflows/clippy-lint.yaml b/.github/workflows/clippy-lint.yaml index 5c11d83da8..e73915f245 100644 --- a/.github/workflows/clippy-lint.yaml +++ b/.github/workflows/clippy-lint.yaml @@ -24,7 +24,7 @@ jobs: - uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: stable + toolchain: 1.89 override: true components: clippy - name: Run Clippy on different workspaces and crates diff --git a/.github/workflows/coverage-protocols.yaml b/.github/workflows/coverage-protocols.yaml index a52bed95a6..dc0a96b0de 100644 --- a/.github/workflows/coverage-protocols.yaml +++ b/.github/workflows/coverage-protocols.yaml @@ -1,7 +1,7 @@ name: Protocol test Coverage on: - pull_request: + push: branches: - main @@ -53,6 +53,14 @@ jobs: flags: codec_sv2-coverage token: ${{ secrets.CODECOV_TOKEN }} + - name: Upload channels_sv2-coverage to codecov.io + uses: codecov/codecov-action@v4 + with: + directory: ./protocols/target/tarpaulin-reports/channels-sv2-coverage + file: ./protocols/target/tarpaulin-reports/channels-sv2-coverage/cobertura.xml + flags: channels_sv2-coverage + token: ${{ secrets.CODECOV_TOKEN }} + - name: Upload common_messages_sv2-coverage to codecov.io uses: codecov/codecov-action@v4 with: @@ -85,6 +93,14 @@ jobs: flags: noise_sv2-coverage token: ${{ secrets.CODECOV_TOKEN }} + - name: Upload parsers_sv2-coverage to codecov.io + uses: codecov/codecov-action@v4 + with: + directory: ./protocols/target/tarpaulin-reports/parsers-sv2-coverage + file: ./protocols/target/tarpaulin-reports/parsers-sv2-coverage/cobertura.xml + flags: parsers_sv2-coverage + token: ${{ secrets.CODECOV_TOKEN }} + - name: Upload roles_logic_sv2-coverage to codecov.io uses: codecov/codecov-action@v4 with: @@ -101,14 +117,6 @@ jobs: flags: v1-coverage token: ${{ secrets.CODECOV_TOKEN }} - - name: Upload sv2_ffi-coverage to codecov.io - uses: codecov/codecov-action@v4 - with: - directory: ./protocols/target/tarpaulin-reports/sv2-ffi-coverage - file: ./protocols/target/tarpaulin-reports/sv2-ffi-coverage/cobertura.xml - flags: sv2_ffi-coverage - token: ${{ secrets.CODECOV_TOKEN }} - - name: Upload template_distribution_sv2-coverage to codecov.io uses: codecov/codecov-action@v4 with: diff --git a/.github/workflows/coverage-roles.yaml b/.github/workflows/coverage-roles.yaml index 33e7497bb6..f777de8de7 100644 --- a/.github/workflows/coverage-roles.yaml +++ b/.github/workflows/coverage-roles.yaml @@ -1,7 +1,7 @@ name: Roles test Coverage on: - pull_request: + push: branches: - main @@ -53,14 +53,6 @@ jobs: flags: mining_device-coverage token: ${{ secrets.CODECOV_TOKEN }} - - name: Upload mining_proxy_sv2-coverage to codecov.io - uses: codecov/codecov-action@v4 - with: - directory: ./roles/target/tarpaulin-reports/mining-proxy-coverage - file: ./roles/target/tarpaulin-reports/mining-proxy-coverage/cobertura.xml - flags: mining_proxy_sv2-coverage - token: ${{ secrets.CODECOV_TOKEN }} - - name: Upload pool_sv2-coverage to codecov.io uses: codecov/codecov-action@v4 with: diff --git a/.github/workflows/coverage-utils.yaml b/.github/workflows/coverage-utils.yaml index 1b6c0156d0..6ff5e4f3a5 100644 --- a/.github/workflows/coverage-utils.yaml +++ b/.github/workflows/coverage-utils.yaml @@ -1,7 +1,7 @@ name: Util Test Coverage on: - pull_request: + push: branches: - main diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 1d0fe6f812..15df72e603 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -49,6 +49,16 @@ jobs: cd protocols/v2/binary-sv2 cargo doc --features with_buffer_pool + - name: Rust Docs crate channels_sv2 + run: | + cd protocols/v2/channels-sv2 + cargo doc + + - name: Rust Docs crate parsers_sv2 + run: | + cd protocols/v2/parsers-sv2 + cargo doc + - name: Rust Docs crate framing_sv2 run: | cd protocols/v2/framing-sv2 @@ -84,11 +94,6 @@ jobs: cd protocols/v2/subprotocols/template-distribution cargo doc - - name: Rust Docs crate sv2_ffi - run: | - cd protocols/v2/sv2-ffi - cargo doc - - name: Rust Docs crate roles_logic_sv2 run: | diff --git a/.github/workflows/integration-tests.yaml b/.github/workflows/integration-tests.yaml index 0829786c7e..b2c95722ba 100644 --- a/.github/workflows/integration-tests.yaml +++ b/.github/workflows/integration-tests.yaml @@ -24,10 +24,9 @@ jobs: toolchain: stable override: true - - name: Roles Integration Tests - run: | - RUST_BACKTRACE=1 RUST_LOG=debug cargo test --manifest-path=test/integration-tests/Cargo.toml --verbose --test '*' -- --nocapture + - name: Install cargo-nextest + run: cargo install cargo-nextest --locked - - name: SV1 Integration Tests + - name: Integration Tests run: | - RUST_BACKTRACE=1 RUST_LOG=debug cargo test --manifest-path=test/integration-tests/Cargo.toml --verbose --test 'sv1' --features sv1 -- --nocapture + RUST_BACKTRACE=1 RUST_LOG=debug cargo nextest run --manifest-path=test/integration-tests/Cargo.toml --features sv1 --nocapture diff --git a/.github/workflows/release-libs.yaml b/.github/workflows/release-libs.yaml index 20f3e5df2e..6f257aa37b 100644 --- a/.github/workflows/release-libs.yaml +++ b/.github/workflows/release-libs.yaml @@ -29,38 +29,51 @@ jobs: - name: Login run: cargo login ${{ secrets.CRATES_IO_DEPLOY_KEY }} - - name: Publish crate common + # Base dependencies with no local dependencies + - name: Publish crate config_helpers_sv2 run: | - ./scripts/release-libs.sh common - + ./scripts/release-libs.sh roles/roles-utils/config-helpers + - name: Publish crate buffer_sv2 run: | ./scripts/release-libs.sh utils/buffer - - name: Publish crate binary_sv2 derive_codec + - name: Publish crate error-handling run: | - ./scripts/release-libs.sh protocols/v2/binary-sv2/derive_codec + ./scripts/release-libs.sh utils/error-handling + + - name: Publish crate key-utils + run: | + ./scripts/release-libs.sh utils/key-utils + - name: Publish crate noise_sv2 + run: | + ./scripts/release-libs.sh protocols/v2/noise-sv2 + + # binary_sv2 (depends on buffer_sv2) - name: Publish crate binary_sv2 codec run: | ./scripts/release-libs.sh protocols/v2/binary-sv2/codec + - name: Publish crate binary_sv2 derive_codec + run: | + ./scripts/release-libs.sh protocols/v2/binary-sv2/derive_codec + - name: Publish crate binary_sv2 run: | ./scripts/release-libs.sh protocols/v2/binary-sv2 + # framing_sv2(depends on binary_sv2, buffer_sv2, noise_sv2) - name: Publish crate framing_sv2 run: | ./scripts/release-libs.sh protocols/v2/framing-sv2 - - name: Publish crate noise_sv2 - run: | - ./scripts/release-libs.sh protocols/v2/noise-sv2 - + # codec_sv2 (depends on framing_sv2, noise_sv2, binary_sv2, buffer_sv2, key-utils) - name: Publish crate codec_sv2 run: | ./scripts/release-libs.sh protocols/v2/codec-sv2 + # Subprotocols (depend on binary_sv2) - name: Publish crate common_messages run: | ./scripts/release-libs.sh protocols/v2/subprotocols/common-messages @@ -77,34 +90,51 @@ jobs: run: | ./scripts/release-libs.sh protocols/v2/subprotocols/template-distribution - - name: Publish crate sv2_ffi + # channels_sv2 (depends on binary_sv2, common_messages_sv2, mining_sv2, template_distribution_sv2, job_declaration_sv2) + - name: Publish crate channels_sv2 run: | - ./scripts/release-libs.sh protocols/v2/sv2-ffi + ./scripts/release-libs.sh protocols/v2/channels-sv2 - - name: Publish crate roles_logic_sv2 + # parsers_sv2 (depends on binary_sv2, framing_sv2, common_messages, mining, template_distribution, job_declaration) + - name: Publish crate parsers_sv2 run: | - ./scripts/release-libs.sh protocols/v2/roles-logic-sv2 + ./scripts/release-libs.sh protocols/v2/parsers-sv2 + # sv1_api (depends on binary_sv2) - name: Publish crate v1 run: | ./scripts/release-libs.sh protocols/v1 - - name: Publish crate bip32-key-derivation + # stratum_translation (depends on binary_sv2, mining_sv2, channels_sv2, v1) + - name: Publish crate stratum_translation run: | - ./scripts/release-libs.sh utils/bip32-key-derivation + ./scripts/release-libs.sh roles/roles-utils/stratum-translation - - name: Publish crate error-handling - run: | - ./scripts/release-libs.sh utils/error-handling - - - name: Publish crate key-utils + # Roles logic (depends on codec_sv2 and subprotocols) + - name: Publish crate roles_logic_sv2 run: | - ./scripts/release-libs.sh utils/key-utils + ./scripts/release-libs.sh protocols/v2/roles-logic-sv2 + # Network helpers (depends on codec_sv2, sv1_api) - name: Publish crate network_helpers_sv2 run: | ./scripts/release-libs.sh roles/roles-utils/network-helpers + # Common (depends on roles_logic_sv2 and network_helpers_sv2) + - name: Publish crate common + run: | + ./scripts/release-libs.sh common + + # handlers_sv2 (depends on parsers_sv2, binary_sv2, common_messages_sv2, mining_sv2, template_distribution_sv2, job_declaration_sv2) + - name: Publish crate handlers_sv2 + run: | + ./scripts/release-libs.sh protocols/v2/handlers-sv2 + + # Utilities that depend on stratum-common + - name: Publish crate bip32-key-derivation + run: | + ./scripts/release-libs.sh utils/bip32-key-derivation + - name: Publish crate rpc_sv2 run: | - ./scripts/release-libs.sh roles/roles-utils/rpc + ./scripts/release-libs.sh roles/roles-utils/rpc \ No newline at end of file diff --git a/.github/workflows/rust-msrv.yaml b/.github/workflows/rust-msrv.yaml index 8c2f8d27d5..8e892c586b 100644 --- a/.github/workflows/rust-msrv.yaml +++ b/.github/workflows/rust-msrv.yaml @@ -27,20 +27,20 @@ jobs: - name: Build Protocols run: cargo build --manifest-path=protocols/Cargo.toml - name: Build Roles - run: cargo build --manifest-path=roles/Cargo.toml + run: cargo build --locked --manifest-path=roles/Cargo.toml - name: Build Utils - run: cargo build --manifest-path=utils/Cargo.toml + run: cargo build --locked --manifest-path=utils/Cargo.toml - name: Build Integration Tests - run: cargo build --manifest-path=test/integration-tests/Cargo.toml - + run: cargo build --locked --manifest-path=test/integration-tests/Cargo.toml + # also check test compilation without running tests - name: Check Test Compilation for Benches run: cargo test --manifest-path=benches/Cargo.toml --no-run - name: Check Test Compilation for Protocols run: cargo test --manifest-path=protocols/Cargo.toml --no-run - name: Check Test Compilation for Roles - run: cargo test --manifest-path=roles/Cargo.toml --no-run + run: cargo test --locked --manifest-path=roles/Cargo.toml --no-run - name: Check Test Compilation for Utils - run: cargo test --manifest-path=utils/Cargo.toml --no-run + run: cargo test --locked --manifest-path=utils/Cargo.toml --no-run - name: Check Test Compilation for Integration Tests - run: cargo test --manifest-path=test/integration-tests/Cargo.toml --no-run \ No newline at end of file + run: cargo test --locked --manifest-path=test/integration-tests/Cargo.toml --no-run diff --git a/.github/workflows/semver-check.yaml b/.github/workflows/semver-check.yaml index 30596e25b0..5154c7499c 100644 --- a/.github/workflows/semver-check.yaml +++ b/.github/workflows/semver-check.yaml @@ -85,14 +85,22 @@ jobs: working-directory: protocols/v2/subprotocols/template-distribution run: cargo semver-checks - - name: Run semver checks for protocols/v2/sv2-ffi - working-directory: protocols/v2/sv2-ffi - run: cargo semver-checks - - name: Run semver checks for protocols/v2/roles-logic-sv2 working-directory: protocols/v2/roles-logic-sv2 run: cargo semver-checks --default-features + - name: Run semver checks for protocols/v2/channels-sv2 + working-directory: protocols/v2/channels-sv2 + run: cargo semver-checks + + - name: Run semver checks for protocols/v2/parsers-sv2 + working-directory: protocols/v2/parsers-sv2 + run: cargo semver-checks + + - name: Run semver checks for protocols/v2/handlers-sv2 + working-directory: protocols/v2/handlers-sv2 + run: cargo semver-checks + - name: Run semver checks for protocols/v1 working-directory: protocols/v1 run: cargo semver-checks @@ -109,6 +117,11 @@ jobs: working-directory: utils/key-utils run: cargo semver-checks + # TODO: Uncomment this when the stratum-translation crate is published to crates.io + #- name: Run semver checks for roles/roles-utils/stratum-translation + # working-directory: roles/roles-utils/stratum-translation + # run: cargo semver-checks + - name: Run semver checks for roles/roles-utils/network-helpers working-directory: roles/roles-utils/network-helpers run: cargo semver-checks @@ -116,3 +129,7 @@ jobs: - name: Run semver checks for roles/roles-utils/rpc working-directory: roles/roles-utils/rpc run: cargo semver-checks + + - name: Run semver checks for roles/roles-utils/config-helpers + working-directory: roles/roles-utils/config-helpers + run: cargo semver-checks \ No newline at end of file diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 734c4552d7..19a67a4f1a 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -11,11 +11,11 @@ jobs: strategy: matrix: os: - - macos-13 + - macos-latest - ubuntu-latest include: - - os: macos-13 - target: x86_64-apple-darwin + - os: macos-latest + target: aarch64-apple-darwin - os: ubuntu-latest target: x86_64-unknown-linux-musl @@ -44,6 +44,19 @@ jobs: run: | cargo run --manifest-path=protocols/v2/framing-sv2/Cargo.toml --example sv2_frame + - name: Run codec-sv2 examples + run: | + cargo run --manifest-path=protocols/v2/codec-sv2/Cargo.toml --example unencrypted + cargo run --manifest-path=protocols/v2/codec-sv2/Cargo.toml --example encrypted --features=noise_sv2 + + - name: Run binary-sv2 examples + run: | + cargo run --manifest-path=protocols/v2/binary-sv2/Cargo.toml --example encode_decode + + - name: Run noise-sv2 examples + run: | + cargo run --manifest-path=protocols/v2/noise-sv2/Cargo.toml --example handshake + - name: fuzz tests run: | if [ ${{ matrix.os }} == "ubuntu-latest" ]; then @@ -62,6 +75,7 @@ jobs: cargo test --manifest-path=utils/Cargo.toml cargo test --manifest-path=roles/roles-utils/config-helpers/Cargo.toml cargo test --manifest-path=roles/roles-utils/network-helpers/Cargo.toml sv1_connection::tests::test_sv1_connection --features sv1 + cargo test --manifest-path=roles/roles-utils/stratum-translation/Cargo.toml - name: Property based testing run: | diff --git a/.gitignore b/.gitignore index 6720e2bde7..b475baf9f5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ */**/target /protocols/guix-example/guix-example.h /protocols/Cargo.lock +/common/Cargo.lock +/protocols/v2/binary-sv2/derive_codec/Cargo.lock /benches/Cargo.lock /ignore /vendor/ed25519-dalek/target @@ -18,5 +20,8 @@ cobertura.xml /examples/*/Cargo.lock /scripts/sv2.h /test/integration-tests/template-provider +/test/integration-tests/minerd **/template-provider stratum-message-generator +*.log +**/minerd \ No newline at end of file diff --git a/README.md b/README.md index ce735b11ac..0dc2c554f6 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ SRI is fully open-source, community-developed, independent of any single entity, To get started with the Stratum V2 Reference Implementation (SRI), please follow the detailed setup instructions available on the official website: -[Getting Started with Stratum V2](https://stratumprotocol.org/getting-started/) +[Getting Started with Stratum V2](https://stratumprotocol.org/blog/getting-started/) This guide provides all the necessary information on prerequisites, installation, and configuration to help you begin using, testing or contributing to SRI. @@ -61,7 +61,6 @@ The library is modular to address different use-cases and desired functionality. - Pools supporting SV2 can deploy the open source binary crate (`roles/pool`) to offer their clients (miners participating in said pool) an SV2-compatible pool. - The Rust helper library provides a suite of tools for mining pools to build custom SV2 compatible pool implementations. -- The C library provides a set of FFI bindings to the Rust helper library for miners to integrate SV2 into their existing firmware stack. ## 🛣 Roadmap @@ -84,10 +83,9 @@ The goals of this project are to provide: - Pools supporting SV2 - Mining-device/hashrate producers integrating SV2 into their firmware - Bitcoin nodes implementing Template Provider to build the `blocktemplate` -2. The above Rust primitives as a C library available for use in other languages via FFI. -3. A set of helpers built on top of the above primitives and the external Bitcoin-related Rust crates for anyone to implement the SV2 roles. -4. An open-source implementation of a SV2 proxy for miners. -5. An open-source implementation of a SV2 pool for mining pool operators. +2. A set of helpers built on top of the above primitives and the external Bitcoin-related Rust crates for anyone to implement the SV2 roles. +3. An open-source implementation of a SV2 proxy for miners. +4. An open-source implementation of a SV2 pool for mining pool operators. ## 💻 Contribute @@ -120,10 +118,10 @@ Email us at: stratumv2@gmail.com SRI contributors are independently, financially supported by following entities:

- - - - + + + +

## 📖 License diff --git a/benches/Cargo.toml b/benches/Cargo.toml index 7bc179f3fb..4cfd8f0161 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -4,7 +4,7 @@ version = "1.0.1" edition = "2021" [dependencies] -stratum-common = { path = "../common", features=["bitcoin"]} +bitcoin = { version = "0.32.5", optional = true } async-std={version = "1.10.0", features = ["attributes"]} criterion = "0.5.1" async-channel = "1.4.0" diff --git a/benches/benches/src/sv1/lib/client.rs b/benches/benches/src/sv1/lib/client.rs index 3ef5fc38ef..e59f4086f2 100644 --- a/benches/benches/src/sv1/lib/client.rs +++ b/benches/benches/src/sv1/lib/client.rs @@ -3,7 +3,6 @@ //! messages. It also provides a trait implementation for handling server messages and managing //! client state. -use std::fmt::Write; use v1::{ client_to_server, error::Error, @@ -27,7 +26,7 @@ pub struct Client { impl Client { pub fn new(client_id: u32) -> Client { - let client = Client { + Client { client_id, extranonce1: extranonce_from_hex("00000000"), extranonce2_size: 4, @@ -37,9 +36,7 @@ impl Client { last_notify: None, sented_authorize_request: vec![], authorized: vec![], - }; - - client + } } // this is what we want to benchmark @@ -262,6 +259,6 @@ mod utils { } pub fn encode_hex(bytes: &[u8]) -> String { - bytes.iter().map(|b| format!("{:02x}", b)).collect() + bytes.iter().map(|b| format!("{b:02x}")).collect() } } diff --git a/common/Cargo.lock b/common/Cargo.lock deleted file mode 100644 index 83e061fb92..0000000000 --- a/common/Cargo.lock +++ /dev/null @@ -1,205 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "base58ck" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" -dependencies = [ - "bitcoin-internals", - "bitcoin_hashes", -] - -[[package]] -name = "bech32" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" - -[[package]] -name = "bitcoin" -version = "0.32.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026" -dependencies = [ - "base58ck", - "bech32", - "bitcoin-internals", - "bitcoin-io", - "bitcoin-units", - "bitcoin_hashes", - "hex-conservative", - "hex_lit", - "secp256k1 0.29.1", -] - -[[package]] -name = "bitcoin-internals" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" - -[[package]] -name = "bitcoin-io" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" - -[[package]] -name = "bitcoin-units" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" -dependencies = [ - "bitcoin-internals", -] - -[[package]] -name = "bitcoin_hashes" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" -dependencies = [ - "bitcoin-io", - "hex-conservative", -] - -[[package]] -name = "cc" -version = "1.0.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "hex-conservative" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "hex_lit" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" - -[[package]] -name = "libc" -version = "0.2.154" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "secp256k1" -version = "0.28.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" -dependencies = [ - "rand", - "secp256k1-sys 0.9.2", -] - -[[package]] -name = "secp256k1" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" -dependencies = [ - "bitcoin_hashes", - "secp256k1-sys 0.10.1", -] - -[[package]] -name = "secp256k1-sys" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" -dependencies = [ - "cc", -] - -[[package]] -name = "secp256k1-sys" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" -dependencies = [ - "cc", -] - -[[package]] -name = "stratum-common" -version = "2.0.0" -dependencies = [ - "bitcoin", - "secp256k1 0.28.2", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" diff --git a/common/Cargo.toml b/common/Cargo.toml index abbac337c5..648c2a818d 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,11 +1,15 @@ [package] name = "stratum-common" -version = "2.0.0" -edition = "2018" +version = "4.0.1" +edition = "2021" description = "SV2 pool role" license = "MIT OR Apache-2.0" repository = "https://github.com/stratum-mining/stratum" [dependencies] -bitcoin = {version="0.32.5",optional=true} -secp256k1 = { version = "0.28.2", default-features = false, features =["alloc","rand","rand-std"] } +roles_logic_sv2 = { path = "../protocols/v2/roles-logic-sv2", version = "5.0.0" } +network_helpers_sv2 = { path = "../roles/roles-utils/network-helpers", version = "4.0.1", features = ["with_buffer_pool"], optional = true } + +[features] +with_network_helpers = ["dep:network_helpers_sv2"] +sv1 = ["network_helpers_sv2/sv1"] diff --git a/common/src/lib.rs b/common/src/lib.rs index 242c7acb35..eca6226f3f 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -2,6 +2,7 @@ //! //! `stratum_common` is a utility crate designed to centralize //! and manage the shared dependencies and utils across stratum crates. -#[cfg(feature = "bitcoin")] -pub use bitcoin; -pub use secp256k1; + +#[cfg(feature = "with_network_helpers")] +pub use network_helpers_sv2; +pub use roles_logic_sv2; diff --git a/examples/interop-cpp-no-cargo/.gitignore b/examples/interop-cpp-no-cargo/.gitignore deleted file mode 100644 index 858137540d..0000000000 --- a/examples/interop-cpp-no-cargo/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/sv2.h -/a.out -/libsv2_ffi.a -/deps diff --git a/examples/interop-cpp-no-cargo/README.md b/examples/interop-cpp-no-cargo/README.md deleted file mode 100644 index 6db6b3c1e6..0000000000 --- a/examples/interop-cpp-no-cargo/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# C++ interop no cargo - -An example of how to build the code in ../../interop-cpp/ without using cargo. - -`./run.sh` will build and run the example diff --git a/examples/interop-cpp-no-cargo/run.sh b/examples/interop-cpp-no-cargo/run.sh deleted file mode 100755 index 5648647112..0000000000 --- a/examples/interop-cpp-no-cargo/run.sh +++ /dev/null @@ -1,35 +0,0 @@ -#! /bin/sh - -touch libsv2_ffi.a -touch a.out - -# CLEAN -rm -f libsv2_ffi.a -rm -f a.out -rm -f sv2.h - -./rust-build-script.sh ../../protocols/v2/ ../../utils/ - -g++ -I ../../protocols/v2/sv2-ffi ../interop-cpp/template-provider/template-provider.cpp libsv2_ffi.a -lpthread -ldl - -./a.out & -provider_pid=$! -sleep 1 # wait for provider to start listening - -cargo run --manifest-path ../interop-cpp/Cargo.toml & -run_pid=$! - -# If there is a first argument sleep for that long -if [ -n "$1" ]; then - sleep "$1" - - if ps -p $provider_pid > /dev/null && ps -p $run_pid > /dev/null - then - echo "Success!" - kill $provider_pid - kill $run_pid - else - echo "Failure!!!" - exit 1 - fi -fi diff --git a/examples/interop-cpp-no-cargo/rust-build-script.sh b/examples/interop-cpp-no-cargo/rust-build-script.sh deleted file mode 100755 index 927fdbfee6..0000000000 --- a/examples/interop-cpp-no-cargo/rust-build-script.sh +++ /dev/null @@ -1,162 +0,0 @@ -#! /bin/sh - -set -ex - -ROOT=$1 -UTILS=$2 - -DEPS="./deps" - -rm -rf $DEPS - -mkdir $DEPS - -rustc \ - --crate-name buffer_sv2 \ - --edition=2018 \ - "$UTILS"/buffer/src/lib.rs \ - --error-format=json \ - --json=diagnostic-rendered-ansi,artifacts \ - --crate-type lib \ - --emit=dep-info,metadata,link \ - -C opt-level=3 \ - -C embed-bitcode=no \ - --out-dir $DEPS \ - -L dependency=$DEPS \ - -rustc \ - --crate-name binary_codec_sv2 \ - --edition=2018 \ - $ROOT/binary-sv2/codec/src/lib.rs \ - --error-format=json \ - --json=diagnostic-rendered-ansi \ - --crate-type lib \ - --emit=dep-info,metadata,link \ - -C embed-bitcode=no \ - -C debug-assertions=off \ - --out-dir $DEPS \ - -L dependency=$DEPS - -rustc \ - --crate-name binary_codec_sv2 \ - --edition=2018 \ - $ROOT/binary-sv2/codec/src/lib.rs \ - --error-format=json \ - --json=diagnostic-rendered-ansi,artifacts \ - --crate-type lib \ - --emit=dep-info,metadata,link \ - -C opt-level=3 \ - -C embed-bitcode=no \ - --out-dir $DEPS \ - -L dependency=$DEPS - -rustc \ - --crate-name derive_codec_sv2 \ - --edition=2018 \ - $ROOT/binary-sv2/derive_codec/src/lib.rs \ - --error-format=json \ - --json=diagnostic-rendered-ansi \ - --crate-type proc-macro \ - --emit=dep-info,link \ - -C prefer-dynamic \ - -C embed-bitcode=no \ - -C debug-assertions=off \ - --out-dir $DEPS \ - -L dependency=$DEPS \ - --extern binary_codec_sv2=$DEPS/libbinary_codec_sv2.rlib \ - --extern proc_macro - -rustc \ - --crate-name binary_sv2 \ - --edition=2018 \ - $ROOT/binary-sv2/src/lib.rs \ - --error-format=json \ - --json=diagnostic-rendered-ansi,artifacts \ - --crate-type lib \ - --emit=dep-info,metadata,link \ - -C opt-level=3 \ - -C embed-bitcode=no \ - --cfg 'feature="default"' \ - --out-dir $DEPS \ - -L dependency=$DEPS \ - --extern binary_codec_sv2=$DEPS/libbinary_codec_sv2.rmeta \ - --extern derive_codec_sv2=$DEPS/libderive_codec_sv2.so - -rustc \ - --crate-name framing_sv2 \ - --edition=2018 \ - $ROOT/framing-sv2/src/lib.rs \ - --error-format=json \ - --json=diagnostic-rendered-ansi,artifacts \ - --crate-type lib \ - --emit=dep-info,metadata,link \ - -C opt-level=3 \ - -C embed-bitcode=no \ - --out-dir $DEPS \ - -L dependency=$DEPS \ - --extern binary_sv2=$DEPS/libbinary_sv2.rmeta \ - --extern const_sv2=$DEPS/libconst_sv2.rmeta - -rustc \ - --crate-name common_messages_sv2 \ - --edition=2018 \ - $ROOT/subprotocols/common-messages/src/lib.rs \ - --error-format=json \ - --json=diagnostic-rendered-ansi \ - --crate-type lib \ - --emit=dep-info,metadata,link \ - -C opt-level=3 \ - -C embed-bitcode=no \ - --out-dir $DEPS \ - -L dependency=$DEPS \ - --extern binary_sv2=$DEPS/libbinary_sv2.rmeta \ - --extern const_sv2=$DEPS/libconst_sv2.rmeta - -rustc \ - --crate-name template_distribution_sv2 \ - --edition=2018 \ - $ROOT/subprotocols/template-distribution/src/lib.rs \ - --error-format=json \ - --json=diagnostic-rendered-ansi \ - --crate-type lib \ - --emit=dep-info,metadata,link \ - -C opt-level=3 \ - -C embed-bitcode=no \ - --out-dir $DEPS \ - -L dependency=$DEPS \ - --extern binary_sv2=$DEPS/libbinary_sv2.rmeta \ - --extern const_sv2=$DEPS/libconst_sv2.rmeta - -rustc \ - --crate-name codec_sv2 \ - --edition=2018 \ - $ROOT/codec-sv2/src/lib.rs \ - --error-format=json \ - --json=diagnostic-rendered-ansi \ - --crate-type lib \ - --emit=dep-info,metadata,link \ - -C opt-level=3 \ - -C embed-bitcode=no \ - --out-dir $DEPS \ - -L dependency=$DEPS \ - --extern binary_sv2=$DEPS/libbinary_sv2.rmeta \ - --extern const_sv2=$DEPS/libconst_sv2.rmeta \ - --extern framing_sv2=$DEPS/libframing_sv2.rmeta \ - --extern buffer_sv2=$DEPS/libbuffer_sv2.rmeta - -rustc \ - --crate-name sv2_ffi \ - --edition=2018 \ - $ROOT/sv2-ffi/src/lib.rs \ - --error-format=json \ - --json=diagnostic-rendered-ansi \ - --crate-type staticlib \ - -C opt-level=3 \ - -C embed-bitcode=no \ - --out-dir ./ \ - -L dependency=$DEPS \ - --extern binary_sv2=$DEPS/libbinary_sv2.rlib \ - --extern codec_sv2=$DEPS/libcodec_sv2.rlib \ - --extern common_messages_sv2=$DEPS/libcommon_messages_sv2.rlib \ - --extern const_sv2=$DEPS/libconst_sv2.rlib \ - --extern template_distribution_sv2=$DEPS/libtemplate_distribution_sv2.rlib diff --git a/examples/interop-cpp/.gitignore b/examples/interop-cpp/.gitignore deleted file mode 100644 index 83a6f8cea6..0000000000 --- a/examples/interop-cpp/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/sv2.h -/a.out -/libsv2_ffi.a diff --git a/examples/interop-cpp/Cargo.toml b/examples/interop-cpp/Cargo.toml deleted file mode 100644 index 2b4bd73af3..0000000000 --- a/examples/interop-cpp/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "interop-cpp" -version = "0.1.0" -authors = ["The Stratum V2 Developers"] -edition = "2018" -publish = false - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -codec_sv2 = { path = "../../protocols/v2/codec-sv2" } -stratum_common = { path = "../../common" } -binary_sv2 = { path = "../../protocols/v2/binary-sv2" } -common_messages_sv2 = { path = "../../protocols/v2/subprotocols/common-messages" } -template_distribution_sv2 = { path = "../../protocols/v2/subprotocols/template-distribution" } diff --git a/examples/interop-cpp/README.md b/examples/interop-cpp/README.md deleted file mode 100644 index 37da8d5495..0000000000 --- a/examples/interop-cpp/README.md +++ /dev/null @@ -1,182 +0,0 @@ -# C++ interop - -This crate provides an example of how to use the Rust Sv2 `Decoder` and `Encoder` from C++. - -To run the example: `./run.sh`. - -The example is composed by a Rust "downstream node" that keep sending a -[`common_messages_sv2::SetupConnection`] message to a C++ "upstream node" that receive the message -and keep answering with a [`common_messages_sv2::SetupConnectionError`]. - -The Rust codec is exported as a C static library by the crate [sv2-ffi](../../protocols/v2/sv2-ffi). - -## Intro - -### Header file - -The [header file](../../protocols/v2/sv2-ffi/sv2.h) is generated with `cbindgen`. - -Rust enums definition are transformed by `cbingen` in: -```c -struct [Rust_enum_name] { - union class Tag { - [union_element_1_name] - [union_element_2_name] - ... - } - - struct [union_element_1_name]_Body { - [inner_union_element_name_if_any_1] _0; - [inner_union_element_name_if_any_2] _1; - ... - } - - struct [union_element_2_name]_Body { - [inner_union_element_name_if_any_1] _0; - [inner_union_element_name_if_any_2] _1; - ... - } - - ... - - union { - [union_element_1_name]_Body [union_element_1_name] - [union_element_2_name]_Body [union_element_2_name] - ... - } - -} -``` - -For example the below Rust enum: -```Rust -#[repr(C)] -pub enum CResult { - Ok(T), - Err(E), -} -``` - -Will be transformed in: -```c -template -struct CResult { - enum class Tag { - Ok, - Err, - }; - - struct Ok_Body { - T _0; - }; - - struct Err_Body { - E _0; - }; - - Tag tag; - union { - Ok_Body ok; - Err_Body err; - }; -}; -``` - -### Conventions - -#### Memory -All the memory used shared struct/enums (also when borrowed) is allocated by Rust. - -When C++ take ownership of a Sv2 message the message must be manually dropped. - -#### Enums -In order to pattern match against a Rust defined enum from C++: -``` -CResult < CSv2Message, Sv2Error > frame = next_frame(decoder); - -switch (frame.tag) { - -case CResult < CSv2Message, Sv2Error > ::Tag::Ok: - on_success(frame.ok._0); - cout << "\n"; - cout << "START PARSING NEW FRAME"; - cout << "\n"; - send_setup_connection_error(new_socket); - break; -case CResult < CSv2Message, Sv2Error > ::Tag::Err: - on_error(frame.err._0); - break; -}; -``` - -### `CVec` -[`binary_sv2::binary_codec_sv2::CVec`] is used to share bytes buffers between Rust and C++. - -A `CVec` can be either "borrowed" or "owned" if is on or the other depend by the method that we -use to construct it. - -* (borrowed) [`binary_sv2::binary_codec_sv2::CVec::as_shared_buffer`]: used when we need to fill a Rust - allocated buffer from C++. This method does not guarantee anything about the pointed memory - and the user must enforce that the Rust side does not free the pointed memory while the - C++ part is using it - A `CVec` constructed with this method must not be freed by C++ (this is enforced by the fact that - the function used to free the `CVec` is not exported in the C library) -* (owned) `&[u8].into::()`: used to copy the contents of the `&[u8]` to a `CVec`. - It must be dropped from C++ via [`sv2_ffi::drop_sv2_message`] -* (owned) [`binary_sv2::binary_codec_sv2::cvec_from_buffer`]: used when a `CVec` must be created in C++, - is used to construct a [`sv2_ffi::CSv2Message`] that will be dropped as usual with - [`sv2_ffi::drop_sv2_message`] -* (owned) `CVec2.into::>()`, see `CVec2` section -* (owned) `Inner.into::()`: same as `&[u8].into::()` - -### `CVec2` -A `CVec2` is a vector of `CVec`'s. It is always allocated in Rust, is used only as field of Sv2 messages, and is -dropped when the Sv2 message gets dropped. - -## Memory management - -### Decoder - -[`sv2_ffi::DecoderWrapper`] is instantiated in C++ via [`sv2_ffi::new_decoder`]. -There is no need to drop it as it will live for the entire life of the program. - -[`sv2_ffi::get_writable`] returns a `CVec` and is Rust allocated memory that C++ can fill with the -socket content. The `CVec` is "borrowed" (`&[u8].into::()`) so it will be automatically -dropped by Rust. - -[`sv2_ffi::next_frame`] is used if a complete Sv2 frame is available, it returns a [`sv2_ffi::CSv2Message`]. -The message can contain one or more "owned" `CVec`'s, so it must be manually dropped via -[`sv2_ffi::drop_sv2_message`]. - - -### Encoder - -[`sv2_ffi::EncoderWrapper`] is instantiated in C++ via [`sv2_ffi::new_encoder`]. -There is no need to drop it as it will live for the entire life of the program. - -A [`sv2_ffi::CSv2Message`] can be constructed in C++ ([here is an example](template-provider/template-provider.cpp#67)) -if the message contains one or more `CVec`'s, then the content of the `CVec` must be copied in a Rust allocated -`CVec` with [`binary_sv2::binary_codec_sv2::cvec_from_buffer`]. The message must be dropped with -[`sv2_ffi::drop_sv2_message`]. - -[`sv2_ffi::encode`] encodes a [`sv2_ffi::CSv2Message`] as an encoded Sv2 frame in a buffer internal -to [`sv2_ffi::EncoderWrapper`]. The buffer contents are returned as a "borrowed" `CVec`. After -that, C++ has copied it and it must free the encoder with [`sv2_ffi::flush_encoder`]. -This is necessary because the encoder will reuse the internal buffer to encode the next message with -[`sv2_ffi::flush_encoder`]. We let the encoder know that the content of the internal buffer has been copied -and can be overwritten. - - -## Decode Sv2 messages in C++ - -1. Instantiate a decoder with [`sv2_ffi::new_decoder`] -2. Fill the decoder, copying the input bytes in the buffer returned by [`sv2_ffi::get_writable`] -3. If the above buffer is full, call [`sv2_ffi::next_frame`]. If the decoder has enough bytes to - decode an Sv2 frame it will return a `CSv2Message`, otherwise it returns 2. - -## Encode Sv2 messages in C++ - -1. Instantiate an encoder with [`sv2_ffi::new_encoder`] -2. Call [`sv2_ffi::encode`] with a valid `CSv2Message` -3. Copy the returned encoded frame where needed -4. Call [`sv2_ffi::flush_encoder`] to let the encoder know that the encoded frame has been copied \ No newline at end of file diff --git a/examples/interop-cpp/run.sh b/examples/interop-cpp/run.sh deleted file mode 100755 index 31f914e5d9..0000000000 --- a/examples/interop-cpp/run.sh +++ /dev/null @@ -1,41 +0,0 @@ -#! /bin/sh - -touch libsv2_ffi.a -touch a.out - -# CLEAN -rm -f libsv2_ffi.a -rm -f a.out -rm -f sv2.h - -cargo build \ - --manifest-path=../../protocols/Cargo.toml \ - --release \ - -p sv2_ffi && \ - cp ../../protocols/target/release/libsv2_ffi.a ./ - -../../scripts/build_header.sh ../../protocols && mv ../../scripts/sv2.h . - -g++ -I ./ ./template-provider/template-provider.cpp libsv2_ffi.a -lpthread -ldl - -./a.out & -provider_pid=$! -sleep 1 # wait for provider to start listening -cargo run & -run_pid=$! - -# If there is a first argument sleep for that long -if [ -n "$1" ]; then - sleep "$1" - - if ps -p $provider_pid > /dev/null && ps -p $run_pid > /dev/null - then - echo "Success!" - kill $provider_pid - kill $run_pid - else - echo "Failure!!!" - exit 1 - fi -fi - diff --git a/examples/interop-cpp/src/main.rs b/examples/interop-cpp/src/main.rs deleted file mode 100644 index 06e13e08f6..0000000000 --- a/examples/interop-cpp/src/main.rs +++ /dev/null @@ -1,136 +0,0 @@ -fn main() -> Result<(), std::io::Error> { - use main_::main; - main() -} - -mod main_ { - use codec_sv2::{Encoder, StandardDecoder, StandardSv2Frame}; - use common_messages_sv2::{Protocol, SetupConnection, SetupConnectionError}; - use std::{ - convert::{TryFrom, TryInto}, - io::{Read, Write}, - net::TcpStream, - }; - use stratum_common::{ - CHANNEL_BIT_SETUP_CONNECTION, MESSAGE_TYPE_SETUP_CONNECTION, - MESSAGE_TYPE_SETUP_CONNECTION_ERROR, - }; - - use binary_sv2::{ - decodable::{DecodableField, FieldMarker}, - encodable::EncodableField, - from_bytes, Deserialize, Error, - }; - - #[derive(Clone, Debug)] - pub enum Sv2Message<'a> { - SetupConnection(SetupConnection<'a>), - SetupConnectionError(SetupConnectionError<'a>), - } - - impl binary_sv2::GetSize for Sv2Message<'_> { - fn get_size(&self) -> usize { - match self { - Sv2Message::SetupConnection(a) => a.get_size(), - Sv2Message::SetupConnectionError(a) => a.get_size(), - } - } - } - - impl<'decoder> Deserialize<'decoder> for Sv2Message<'decoder> { - fn get_structure(_v: &[u8]) -> std::result::Result, binary_sv2::Error> { - unimplemented!() - } - fn from_decoded_fields( - _v: Vec>, - ) -> std::result::Result { - unimplemented!() - } - } - - impl<'a> TryFrom<(u8, &'a mut [u8])> for Sv2Message<'a> { - type Error = Error; - - fn try_from(v: (u8, &'a mut [u8])) -> Result { - let msg_type = v.0; - match msg_type { - MESSAGE_TYPE_SETUP_CONNECTION => { - let message: SetupConnection<'a> = from_bytes(v.1)?; - Ok(Sv2Message::SetupConnection(message)) - } - MESSAGE_TYPE_SETUP_CONNECTION_ERROR => { - let message: SetupConnectionError<'a> = from_bytes(v.1)?; - Ok(Sv2Message::SetupConnectionError(message)) - } - _ => panic!(), - } - } - } - - impl<'decoder> From> for EncodableField<'decoder> { - fn from(m: Sv2Message<'decoder>) -> Self { - match m { - Sv2Message::SetupConnection(a) => a.into(), - Sv2Message::SetupConnectionError(a) => a.into(), - } - } - } - - pub fn main() -> Result<(), std::io::Error> { - let mut encoder = Encoder::::new(); - - let setup_connection = SetupConnection { - protocol: Protocol::TemplateDistributionProtocol, - min_version: 2, - max_version: 2, - flags: 0, - endpoint_host: "0.0.0.0".to_string().into_bytes().try_into().unwrap(), - endpoint_port: 8081, - vendor: "Bitmain".to_string().into_bytes().try_into().unwrap(), - hardware_version: "901".to_string().into_bytes().try_into().unwrap(), - firmware: "abcX".to_string().into_bytes().try_into().unwrap(), - device_id: "89567".to_string().into_bytes().try_into().unwrap(), - }; - - let setup_connection = StandardSv2Frame::from_message( - setup_connection, - MESSAGE_TYPE_SETUP_CONNECTION, - 0, - CHANNEL_BIT_SETUP_CONNECTION, - ) - .unwrap(); - let setup_connection = encoder.encode(setup_connection).unwrap(); - - #[allow(deprecated)] - std::thread::sleep_ms(2000); - - let mut stream = TcpStream::connect("0.0.0.0:8080")?; - - let mut decoder = StandardDecoder::>::new(); - - loop { - #[allow(deprecated)] - std::thread::sleep_ms(500); - - stream.write_all(setup_connection)?; - - loop { - let buffer = decoder.writable(); - stream.read_exact(buffer).unwrap(); - if let Ok(mut f) = decoder.next_frame() { - let msg_type = f.get_header().unwrap().msg_type(); - let payload = f.payload(); - let message: Sv2Message = (msg_type, payload).try_into().unwrap(); - match message { - Sv2Message::SetupConnection(_) => panic!(), - Sv2Message::SetupConnectionError(m) => { - println!("RUST MESSAGE RECEIVED"); - println!(" {}", std::str::from_utf8(m.error_code.as_ref()).unwrap()); - } - } - break; - } - } - } - } -} diff --git a/examples/interop-cpp/template-provider/template-provider.cpp b/examples/interop-cpp/template-provider/template-provider.cpp deleted file mode 100644 index 5bd1f12aa8..0000000000 --- a/examples/interop-cpp/template-provider/template-provider.cpp +++ /dev/null @@ -1,171 +0,0 @@ -#include - -#include - -#include - -#include - -#include - -#include - -#include - -#include - -using namespace std; -#define PORT 8080 - -void on_success(CSv2Message message) { - cout << "PARSED FRAME:\n"; - cout << " Message version: "; - cout << message.setup_connection._0.min_version; - cout << "\n"; - switch (message.tag) { - - case CSv2Message::Tag::SetupConnection: - - switch (message.setup_connection._0.protocol) { - case Protocol::MiningProtocol: - cout << "MiningProtocol \n"; - drop_sv2_message(message); - break; - case Protocol::TemplateDistributionProtocol: - - cout << " Protocol: TDP \n"; - cout << " Vendor: "; - cout << message.setup_connection._0.vendor.data; - cout << "\n"; - cout << " H Version: "; - cout << message.setup_connection._0.hardware_version.data; - cout << "\n"; - cout << " Firmware: "; - cout << message.setup_connection._0.firmware.data; - cout << "\n"; - cout << " Device ID: "; - cout << message.setup_connection._0.device_id.data; - cout << "\n"; - - drop_sv2_message(message); - break; - } - } -} - -void on_error(Sv2Error error) { - switch (error.tag) { - case Sv2Error::Tag::MissingBytes: - cout << "Waiting for the remaining part of the frame \n"; - break; - default: - cout << "An unkwon error occured \n"; - break; - } -} - -void send_setup_connection_error(int socket, EncoderWrapper *encoder) { - const char* error = "connection can not be created"; - uint8_t* error_ = (uint8_t*) error; - - CVec error_code = cvec_from_buffer(error_, strlen(error)); - CSetupConnectionError message; - message.flags = 0; - message.error_code = error_code; - - CSv2Message response; - response.tag = CSv2Message::Tag::SetupConnectionError; - response.setup_connection_error._0 = message; - - CResult encoded = encode(&response, encoder); - switch (encoded.tag) { - - case CResult < CVec, Sv2Error > ::Tag::Ok: - cout << "sending connection setup error \n"; - write(socket, encoded.ok._0.data, encoded.ok._0.len); - drop_sv2_message(response); - flush_encoder(encoder); - break; - case CResult < CVec, Sv2Error > ::Tag::Err: - cout << "Some error occurred \n"; - break; - }; - - //char *hello = "Hello"; - //write(socket, hello, strlen(hello)); -} - -int main() { - int server_fd, new_socket, valread; - struct sockaddr_in address; - int opt = 1; - int addrlen = sizeof(address); - - // Creating socket file descriptor - if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { - perror("socket failed"); - exit(EXIT_FAILURE); - } - - // Forcefully attaching socket to the port 8080 - if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, & - opt, sizeof(opt))) { - perror("setsockopt"); - exit(EXIT_FAILURE); - } - address.sin_family = AF_INET; - address.sin_addr.s_addr = INADDR_ANY; - address.sin_port = htons(PORT); - - // Forcefully attaching socket to the port 8080 - if (bind(server_fd, (struct sockaddr * ) & address, - sizeof(address)) < 0) { - perror("bind failed"); - exit(EXIT_FAILURE); - } - if (listen(server_fd, 3) < 0) { - perror("listen"); - exit(EXIT_FAILURE); - } - if ((new_socket = accept(server_fd, (struct sockaddr * ) & address, - (socklen_t * ) & addrlen)) < 0) { - perror("accept"); - exit(EXIT_FAILURE); - } - - // Istanciate Sv2 decoder - DecoderWrapper * decoder = new_decoder(); - EncoderWrapper * encoder = new_encoder(); - - int byte_read = 0; - - while (true) { - CVec buffer = get_writable(decoder); - - while (byte_read < buffer.len) { - byte_read += read(new_socket, buffer.data, (buffer.len - byte_read)); - - } - - byte_read = 0; - CResult < CSv2Message, Sv2Error > frame = next_frame(decoder); - - - switch (frame.tag) { - - case CResult < CSv2Message, Sv2Error > ::Tag::Ok: - on_success(frame.ok._0); - cout << "\n"; - cout << "START PARSING NEW FRAME"; - cout << "\n"; - send_setup_connection_error(new_socket, encoder); - break; - case CResult < CSv2Message, Sv2Error > ::Tag::Err: - on_error(frame.err._0); - break; - }; - } - - return 0; -} - diff --git a/examples/ping-pong-encrypted/Cargo.toml b/examples/ping-pong-encrypted/Cargo.toml index 8b3099a018..cf9e99fe51 100644 --- a/examples/ping-pong-encrypted/Cargo.toml +++ b/examples/ping-pong-encrypted/Cargo.toml @@ -11,7 +11,7 @@ binary_sv2 = { path = "../../protocols/v2/binary-sv2" } codec_sv2 = { path = "../../protocols/v2/codec-sv2", features = [ "noise_sv2" ] } noise_sv2 = { path = "../../protocols/v2/noise-sv2" } key-utils = { version = "^1.0.0", path = "../../utils/key-utils" } -network_helpers_sv2 = { version = "3.0.0", path = "../../roles/roles-utils/network-helpers" } +network_helpers_sv2 = { version = "4.0.0", path = "../../roles/roles-utils/network-helpers" } rand = "0.8" tokio = { version = "1.44.1", features = [ "full" ] } async-channel = "1.5.1" diff --git a/protocols/Cargo.toml b/protocols/Cargo.toml index 92b84df262..cd287c3493 100644 --- a/protocols/Cargo.toml +++ b/protocols/Cargo.toml @@ -14,8 +14,10 @@ members = [ "v2/subprotocols/template-distribution", "v2/subprotocols/mining", "v2/subprotocols/job-declaration", - "v2/sv2-ffi", "v2/roles-logic-sv2", + "v2/channels-sv2", + "v2/parsers-sv2", + "v2/handlers-sv2", ] [profile.dev] diff --git a/protocols/v1/Cargo.toml b/protocols/v1/Cargo.toml index 91bf3dc2d3..ff3e569436 100644 --- a/protocols/v1/Cargo.toml +++ b/protocols/v1/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "sv1_api" -version = "1.0.1" +version = "2.1.1" authors = ["The Stratum V2 Developers"] -edition = "2018" +edition = "2021" readme = "README.md" description = "API for bridging SV1 miners to SV2 pools" documentation = "https://docs.rs/sv1_api" @@ -20,7 +20,7 @@ hex = "0.4.3" serde = { version = "1.0.89", default-features = false, features = ["derive", "alloc"] } serde_json = { version = "1.0.64", default-features = false, features = ["alloc"] } tracing = {version = "0.1"} -binary_sv2 = { path = "../v2/binary-sv2", version = "^3.0.0" } +binary_sv2 = { path = "../v2/binary-sv2", version = "^4.0.0" } [dev-dependencies] quickcheck = "1" diff --git a/protocols/v1/examples/client_and_server.rs b/protocols/v1/examples/client_and_server.rs index e99f047693..4812ed0c3e 100644 --- a/protocols/v1/examples/client_and_server.rs +++ b/protocols/v1/examples/client_and_server.rs @@ -86,12 +86,12 @@ fn server_pool_listen(listener: TcpListener) { loop { match listener.accept() { Ok((stream, addr)) => { - println!("SERVER - Accepting from: {}", addr); + println!("SERVER - Accepting from: {addr}"); let server = Server::new(stream); let _ = Arc::new(Mutex::new(server)); } Err(e) => { - eprintln!("SERVER - Accept error: {}", e); + eprintln!("SERVER - Accept error: {e}"); break; } } @@ -139,7 +139,7 @@ impl Server<'_> { thread::spawn(move || loop { if let Ok(mut self_) = cloned.try_lock() { if let Ok(line) = self_.receiver_incoming.try_recv() { - println!("SERVER - message: {}", line); + println!("SERVER - message: {line}"); let message: Result = serde_json::from_str(&line); if let Ok(message) = message { if let Ok(Some(resp)) = self_.handle_message(message) { @@ -171,7 +171,7 @@ impl Server<'_> { run_time -= notify_time as i32; if run_time <= 0 { - println!("Test Success - ran for {} seconds", TEST_DURATION); + println!("Test Success - ran for {TEST_DURATION} seconds"); exit(0) } } @@ -305,7 +305,7 @@ impl Client<'static> { thread::sleep(Duration::from_secs(1)); match TcpStream::connect(socket) { Ok(st) => { - println!("CLIENT - connected to server at {}", socket); + println!("CLIENT - connected to server at {socket}"); let (sender_incoming, receiver_incoming) = mpsc::channel::(); let (sender_outgoing, receiver_outgoing) = mpsc::channel::(); @@ -560,7 +560,7 @@ impl<'a> IsClient<'a> for Client<'a> { &mut self, message: Message, ) -> Result, Error<'a>> { - println!("{:?}", message); + println!("{message:?}"); Ok(None) } } @@ -621,7 +621,7 @@ mod utils { pub fn encode_hex(bytes: &[u8]) -> String { let mut s = String::with_capacity(bytes.len() * 2); for &b in bytes { - write!(&mut s, "{:02x}", b).unwrap(); + write!(&mut s, "{b:02x}").unwrap(); } s } diff --git a/protocols/v1/src/error.rs b/protocols/v1/src/error.rs index a4dbaa342d..bd1365af36 100644 --- a/protocols/v1/src/error.rs +++ b/protocols/v1/src/error.rs @@ -31,6 +31,8 @@ pub enum Error<'a> { /// Errors if server does not recognize the client's `id`. UnknownID(u64), InvalidVersionMask(HexU32Be), + /// Errors when an unexpected or unsupported message/method is called. + UnexpectedMessage(String), } impl std::fmt::Display for Error<'_> { @@ -38,15 +40,14 @@ impl std::fmt::Display for Error<'_> { match self { Error::BadBytesConvert(ref e) => write!( f, - "Bad U256 or B032 conversion (U256 length must be exactly 32 bytes; B032 length must be <= 32 bytes): {:?}", - e + "Bad U256 or B032 conversion (U256 length must be exactly 32 bytes; B032 length must be <= 32 bytes): {e:?}" ), - Error::BTCHashError(ref e) => write!(f, "Bitcoin Hashes Error: `{:?}`", e), - Error::HexError(ref e) => write!(f, "Bad hex encode/decode: `{:?}`", e), + Error::BTCHashError(ref e) => write!(f, "Bitcoin Hashes Error: `{e:?}`"), + Error::HexError(ref e) => write!(f, "Bad hex encode/decode: `{e:?}`"), Error::IncorrectClientStatus(s) => { - write!(f, "Client status is incompatible with message: `{}`", s) + write!(f, "Client status is incompatible with message: `{s}`") } - Error::Infallible(ref e) => write!(f, "Infallible error{:?}", e), + Error::Infallible(ref e) => write!(f, "Infallible error{e:?}"), Error::InvalidJsonRpcMessageKind => write!( f, "Server received a `json_rpc` response when it should only receive requests" @@ -54,8 +55,7 @@ impl std::fmt::Display for Error<'_> { Error::InvalidReceiver(ref e) => write!( f, "Client received an invalid message that was intended to be sent from the - client to the server, NOT from the server to the client. Invalid message: `{:?}`", - e + client to the server, NOT from the server to the client. Invalid message: `{e:?}`" ), Error::InvalidSubmission => { write!(f, "Server received an invalid `mining.submit` message.") @@ -63,17 +63,16 @@ impl std::fmt::Display for Error<'_> { Error::Method(ref e) => { write!( f, - "Error converting valid `json_rpc` SV1 message: `{:?}`", - e + "Error converting valid `json_rpc` SV1 message: `{e:?}`" ) } Error::UnauthorizedClient(id) => write!( f, - "Client with id `{}` expected to be authorized but is unauthorized.", - id + "Client with id `{id}` expected to be authorized but is unauthorized." ), - Error::UnknownID(e) => write!(f, "Server did not recognize the client id: `{}`.", e), + Error::UnknownID(e) => write!(f, "Server did not recognize the client id: `{e}`."), Error::InvalidVersionMask(e) => write!(f, "First 3 bits of version rolling mask must be 0 and last 13 bits of version rolling mask must be 0. Version rolling mask is: `{:b}`.", e.0), + Error::UnexpectedMessage(method) => write!(f, "Unexpected or unsupported message/method called: `{method}`."), } } } diff --git a/protocols/v1/src/json_rpc.rs b/protocols/v1/src/json_rpc.rs index f468ece909..16e43f99fc 100644 --- a/protocols/v1/src/json_rpc.rs +++ b/protocols/v1/src/json_rpc.rs @@ -26,14 +26,10 @@ impl Message { impl Display for Message { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Message::StandardRequest(sr) => write!(f, "{:?}", sr.method), - Message::Notification(n) => write!(f, "{:?}", n.method), - Message::OkResponse(_) => write!(f, "\"result\": true"), - Message::ErrorResponse(r) => write!( - f, - "\"result\": false, \"error\": {:?}", - r.error.as_ref().unwrap().message - ), + Message::StandardRequest(sr) => write!(f, "{}", sr), + Message::Notification(n) => write!(f, "{}", n), + Message::OkResponse(r) => write!(f, "{}", r), + Message::ErrorResponse(r) => write!(f, "{}", r), } } } @@ -45,12 +41,33 @@ pub struct StandardRequest { pub params: serde_json::Value, } +impl fmt::Display for StandardRequest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let params = + serde_json::to_string_pretty(&self.params).unwrap_or_else(|_| self.params.to_string()); + write!( + f, + "{{ id: {}, method: {}, params: {} }}", + self.id, self.method, params + ) + } +} + #[derive(Clone, Serialize, Deserialize, Debug)] pub struct Notification { pub method: String, pub params: serde_json::Value, } +impl fmt::Display for Notification { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let params = + serde_json::to_string_pretty(&self.params).unwrap_or_else(|_| self.params.to_string()); + + write!(f, "{{ method: \"{}\", params: {} }}", self.method, params) + } +} + #[derive(Clone, Serialize, Deserialize, Debug)] pub struct Response { pub id: u64, @@ -58,6 +75,23 @@ pub struct Response { pub result: serde_json::Value, } +impl fmt::Display for Response { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let result = + serde_json::to_string_pretty(&self.result).unwrap_or_else(|_| self.result.to_string()); + + if let Some(err) = &self.error { + write!( + f, + "{{ id: {}, error: {:?}, result: {} }}", + self.id, err, result + ) + } else { + write!(f, "{{ id: {}, result: {} }}", self.id, result) + } + } +} + #[derive(Clone, Serialize, Deserialize, Debug)] pub struct JsonRpcError { pub code: i32, // json do not specify precision which one should be used? diff --git a/protocols/v1/src/lib.rs b/protocols/v1/src/lib.rs index b803c14228..c05035c18b 100644 --- a/protocols/v1/src/lib.rs +++ b/protocols/v1/src/lib.rs @@ -1,5 +1,5 @@ #![allow(clippy::result_unit_err)] -//! Startum V1 application protocol: +//! Stratum V1 application protocol: //! //! json-rpc has two types of messages: **request** and **response**. //! A request message can be either a **notification** or a **standard message**. @@ -227,9 +227,9 @@ pub trait IsServer<'a> { // {"params":["00003000"], "id":null, "method": "mining.set_version_mask"} // fn update_version_rolling_mask - fn notify(&mut self) -> Result; + fn notify(&mut self) -> Result>; - fn handle_set_difficulty(&mut self, value: f64) -> Result { + fn handle_set_difficulty(&mut self, value: f64) -> Result> { let set_difficulty = server_to_client::SetDifficulty { value }; Ok(set_difficulty.into()) } @@ -421,7 +421,7 @@ pub trait IsClient<'a> { fn status(&self) -> ClientStatus; - fn last_notify(&self) -> Option; + fn last_notify(&self) -> Option>; /// Check if the given user_name has been authorized by the server #[allow(clippy::ptr_arg)] @@ -464,7 +464,7 @@ pub trait IsClient<'a> { id: u64, name: String, password: String, - ) -> Result { + ) -> Result> { match self.status() { ClientStatus::Init => Err(Error::IncorrectClientStatus("mining.authorize".to_string())), _ => Ok(client_to_server::Authorize { id, name, password }.into()), @@ -559,7 +559,7 @@ mod tests { true } - fn notify(&mut self) -> Result { + fn notify(&mut self) -> Result> { Ok(json_rpc::Message::StandardRequest( json_rpc::StandardRequest { id: 1, diff --git a/protocols/v1/src/methods/client_to_server.rs b/protocols/v1/src/methods/client_to_server.rs index 6d44d9c8a9..b7780fb7f1 100644 --- a/protocols/v1/src/methods/client_to_server.rs +++ b/protocols/v1/src/methods/client_to_server.rs @@ -226,12 +226,12 @@ impl Arbitrary for Submit<'static> { fn arbitrary(g: &mut Gen) -> Self { let mut extra = Vec::::arbitrary(g); extra.resize(32, 0); - println!("\nEXTRA: {:?}\n", extra); + println!("\nEXTRA: {extra:?}\n"); let bits = Option::::arbitrary(g); - println!("\nBITS: {:?}\n", bits); + println!("\nBITS: {bits:?}\n"); let extra: Extranonce = extra.try_into().unwrap(); let bits = bits.map(HexU32Be); - println!("\nBITS: {:?}\n", bits); + println!("\nBITS: {bits:?}\n"); Submit { user_name: String::arbitrary(g), job_id: String::arbitrary(g), @@ -248,12 +248,12 @@ impl Arbitrary for Submit<'static> { #[quickcheck_macros::quickcheck] fn submit_from_to_json_rpc(submit: Submit<'static>) -> bool { let message = Into::::into(submit.clone()); - println!("\nMESSAGE: {:?}\n", message); + println!("\nMESSAGE: {message:?}\n"); let request = match message { Message::StandardRequest(s) => s, _ => panic!(), }; - println!("\nREQUEST: {:?}\n", request); + println!("\nREQUEST: {request:?}\n"); submit == TryInto::::try_into(request).unwrap() } @@ -319,9 +319,7 @@ impl TryFrom for Subscribe<'_> { [JString(a), Null, JString(_), Null] => (a.into(), None), // bosminer subscribe message [JString(a), Null] => (a.into(), None), - [JString(a), JString(b)] => { - (a.into(), Some(Extranonce::try_from(hex::decode(b)?)?)) - } + [JString(a), JString(b)] => (a.into(), Some(Extranonce::try_from(b.as_str())?)), [JString(a)] => (a.into(), None), [] => ("".to_string(), None), _ => return Err(ParsingMethodError::wrong_args_from_value(msg.params)), @@ -716,3 +714,36 @@ fn test_version_extension_with_no_bit_count() { _ => panic!(), }; } + +#[test] +fn test_subscribe_with_odd_length_extranonce() { + // Test that odd-length hex strings (with leading zeroes) are handled correctly + let client_message = r#"{"id":1, + "method": "mining.subscribe", + "params":["test-agent", "abc"] + }"#; + let client_message: StandardRequest = serde_json::from_str(client_message).unwrap(); + let subscribe = Subscribe::try_from(client_message).unwrap(); + + // Should successfully parse odd-length hex string by prepending "0" + assert_eq!(subscribe.agent_signature, "test-agent"); + assert!(subscribe.extranonce1.is_some()); + let extranonce = subscribe.extranonce1.unwrap(); + assert_eq!(extranonce.0.inner_as_ref(), &[0x0a, 0xbc]); // "0abc" -> [10, 188] +} + +#[test] +fn test_subscribe_with_even_length_extranonce() { + // Test that even-length hex strings work as before + let client_message = r#"{"id":1, + "method": "mining.subscribe", + "params":["test-agent", "abcd"] + }"#; + let client_message: StandardRequest = serde_json::from_str(client_message).unwrap(); + let subscribe = Subscribe::try_from(client_message).unwrap(); + + assert_eq!(subscribe.agent_signature, "test-agent"); + assert!(subscribe.extranonce1.is_some()); + let extranonce = subscribe.extranonce1.unwrap(); + assert_eq!(extranonce.0.inner_as_ref(), &[0xab, 0xcd]); // "abcd" -> [171, 205] +} diff --git a/protocols/v1/src/methods/server_to_client.rs b/protocols/v1/src/methods/server_to_client.rs index 009a41bea8..d4cf61d5f2 100644 --- a/protocols/v1/src/methods/server_to_client.rs +++ b/protocols/v1/src/methods/server_to_client.rs @@ -563,7 +563,7 @@ fn configure_response_parsing_all_fields() { }"#; let client_response = serde_json::from_str(client_response_str).unwrap(); let server_configure = Configure::try_from(&client_response).unwrap(); - println!("{:?}", server_configure); + println!("{server_configure:?}"); let version_rolling = server_configure.version_rolling.unwrap(); assert!(version_rolling.version_rolling); @@ -584,7 +584,7 @@ fn configure_response_parsing_no_vr_min_bit_count() { }"#; let client_response = serde_json::from_str(client_response_str).unwrap(); let server_configure = Configure::try_from(&client_response).unwrap(); - println!("{:?}", server_configure); + println!("{server_configure:?}"); let version_rolling = server_configure.version_rolling.unwrap(); assert!(version_rolling.version_rolling); diff --git a/protocols/v1/src/utils.rs b/protocols/v1/src/utils.rs index bcd4a206c6..ec4d960764 100644 --- a/protocols/v1/src/utils.rs +++ b/protocols/v1/src/utils.rs @@ -44,7 +44,7 @@ impl<'a> From> for Value { /// FIXME: find a nicer solution fn hex_decode(s: &str) -> Result, Error<'static>> { if s.len() % 2 != 0 { - Ok(hex::decode(format!("0{}", s))?) + Ok(hex::decode(format!("0{s}"))?) } else { Ok(hex::decode(s)?) } diff --git a/protocols/v2/binary-sv2/Cargo.toml b/protocols/v2/binary-sv2/Cargo.toml index d054a210d5..053093370a 100644 --- a/protocols/v2/binary-sv2/Cargo.toml +++ b/protocols/v2/binary-sv2/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "binary_sv2" -version = "3.0.0" +version = "4.0.0" authors = ["The Stratum V2 Developers"] -edition = "2018" +edition = "2021" readme = "README.md" description = "Sv2 data format" documentation = "https://docs.rs/binary_sv2" @@ -14,7 +14,7 @@ keywords = ["stratum", "mining", "bitcoin", "protocol"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -binary_codec_sv2 = { path = "codec", version = "^2.0.0" } +binary_codec_sv2 = { path = "codec", version = "^3.0.0" } derive_codec_sv2 = { path = "derive_codec", version = "^1.0.0" } [features] diff --git a/protocols/v2/binary-sv2/codec/Cargo.toml b/protocols/v2/binary-sv2/codec/Cargo.toml index abc883f326..5b70970e5f 100644 --- a/protocols/v2/binary-sv2/codec/Cargo.toml +++ b/protocols/v2/binary-sv2/codec/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "binary_codec_sv2" -version = "2.0.0" +version = "3.0.0" authors = ["The Stratum V2 Developers"] -edition = "2018" +edition = "2021" readme = "README.md" description = "Sv2 data format" documentation = "https://docs.rs/binary_codec_sv2" diff --git a/protocols/v2/binary-sv2/codec/README.md b/protocols/v2/binary-sv2/codec/README.md index 9189991c13..8ee2a55008 100644 --- a/protocols/v2/binary-sv2/codec/README.md +++ b/protocols/v2/binary-sv2/codec/README.md @@ -12,8 +12,7 @@ - **Comprehensive Encoding and Decoding**: Provides traits (`Encodable`, `Decodable`) for converting between Rust and SV2 data types/structures. - **Support for Complex Data Structures**: Handles primitives, nested structures, and protocol-specific types like `U24`, `U256`,`Str0255` and rest. -- **Error Handling**: Robust mechanisms for managing encoding/decoding failures, including size mismatches and invalid data. -- **Cross-Language Compatibility**: Utilities like `CVec` and `CError` ensure smooth integration with other programming languages. +- **Error Handling**: Robust mechanisms for managing encoding/decoding failures, including size mismatches and invalid data. - **`no_std` Compatibility**: Fully supports constrained environments without the Rust standard library. ## Sv2 Type Mapping diff --git a/protocols/v2/binary-sv2/codec/src/datatypes/copy_data_types.rs b/protocols/v2/binary-sv2/codec/src/datatypes/copy_data_types.rs index d9ca0a4abd..56db4a79f5 100644 --- a/protocols/v2/binary-sv2/codec/src/datatypes/copy_data_types.rs +++ b/protocols/v2/binary-sv2/codec/src/datatypes/copy_data_types.rs @@ -165,7 +165,7 @@ impl_sv2_for_unsigned!(f32); /// Represents a 24-bit unsigned integer (`U24`), supporting SV2 serialization and deserialization. /// Only first 3 bytes of a u32 is considered to get the SV2 value, and rest are ignored (in little /// endian). -#[repr(C)] + #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct U24(pub(crate) u32); diff --git a/protocols/v2/binary-sv2/codec/src/datatypes/non_copy_data_types/inner.rs b/protocols/v2/binary-sv2/codec/src/datatypes/non_copy_data_types/inner.rs index 5feee50a94..0e41134429 100644 --- a/protocols/v2/binary-sv2/codec/src/datatypes/non_copy_data_types/inner.rs +++ b/protocols/v2/binary-sv2/codec/src/datatypes/non_copy_data_types/inner.rs @@ -61,7 +61,7 @@ use std::io::{Error as E, Read, Write}; // - `HEADERSIZE`: The size of the header, which is used for types that require a prefix to // describe the content's length. // - `MAXSIZE`: The maximum allowable size for the data. -#[repr(C)] + #[derive(Debug)] pub enum Inner< 'a, diff --git a/protocols/v2/binary-sv2/codec/src/datatypes/non_copy_data_types/mod.rs b/protocols/v2/binary-sv2/codec/src/datatypes/non_copy_data_types/mod.rs index f9f2d55997..fc2c3d75e6 100644 --- a/protocols/v2/binary-sv2/codec/src/datatypes/non_copy_data_types/mod.rs +++ b/protocols/v2/binary-sv2/codec/src/datatypes/non_copy_data_types/mod.rs @@ -31,9 +31,9 @@ #[cfg(feature = "prop_test")] use quickcheck::{Arbitrary, Gen}; -use alloc::string::String; #[cfg(feature = "prop_test")] use alloc::vec::Vec; +use alloc::{borrow::ToOwned, fmt, string::String}; mod inner; mod seq_inner; @@ -74,6 +74,239 @@ pub type B064K<'a> = Inner<'a, false, 1, 2, { u16::MAX as usize }>; /// represented using the `Inner` type with a 3-byte header. pub type B016M<'a> = Inner<'a, false, 1, 3, { 2_usize.pow(24) - 1 }>; +impl fmt::Display for U32AsRef<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let inner = self.inner_as_ref(); + write!( + f, + "U32AsRef({})", + u32::from_le_bytes([inner[0], inner[1], inner[2], inner[3]]) + ) + } +} + +impl fmt::Display for Sv2Option<'_, u32> { + // internally Sv2Option is pub struct Sv2Option<'a, T>(pub Vec, PhantomData<&'a T>); + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let inner = self.to_owned().into_inner(); + match inner { + Some(value) => write!(f, "Sv2Option({value})"), + None => write!(f, "Sv2Option(None)"), + } + } +} + +impl B0255<'_> { + pub fn as_hex(&self) -> String { + let inner = self + .inner_as_ref() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + format!("B0255({inner})") + } +} + +impl Str0255<'_> { + /// Returns the value as a UTF-8 string if possible, otherwise as a hex string prefixed with 0x. + pub fn as_utf8_or_hex(&self) -> String { + match core::str::from_utf8(self.inner_as_ref()) { + Ok(s) => alloc::string::String::from(s), + Err(_) => format!( + "0x{}", + self.inner_as_ref() + .iter() + .map(|b| format!("{b:02x}")) + .collect::() + ), + } + } +} + +impl fmt::Display for B064K<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let inner = self + .inner_as_ref() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + write!(f, "B064K({inner})") + } +} + +impl fmt::Display for U256<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let inner = self + .inner_as_ref() + .iter() + .rev() + .map(|byte| format!("{byte:02x}")) + .collect::(); + write!(f, "U256({inner})") + } +} + +impl fmt::Display for Seq0255<'_, U256<'_>> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let len = self.0.len(); + let as_hex = |item: &U256<'_>| { + item.inner_as_ref() + .iter() + .rev() + .map(|byte| format!("{byte:02x}")) + .collect::() + }; + write!(f, "Seq0255 write!(f, "[]"), + 1 => write!(f, "{}]", as_hex(&self.0[0])), + 2 => write!(f, "{}, {}]", as_hex(&self.0[0]), as_hex(&self.0[1])), + 3 => write!( + f, + "[{}, {}, {}]", + as_hex(&self.0[0]), + as_hex(&self.0[1]), + as_hex(&self.0[2]) + ), + _ => write!( + f, + "[{}, {}, ... , {}, {}]", + as_hex(&self.0[0]), + as_hex(&self.0[1]), + as_hex(&self.0[len - 2]), + as_hex(&self.0[len - 1]) + ), + } + } +} + +impl fmt::Display for Seq064K<'_, B016M<'_>> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let len = self.0.len(); + + let as_hex = |item: &B016M<'_>| { + let hex: String = item + .inner_as_ref() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect(); + + if hex.len() > 500 { + format!("{}…", &hex[..500], hex.len() - 500) + } else { + hex + } + }; + + write!(f, "Seq064K write!(f, "[]"), + 1 => write!(f, "[{}]", as_hex(&self.0[0])), + 2 => write!(f, "[{}, {}]", as_hex(&self.0[0]), as_hex(&self.0[1])), + 3 => write!( + f, + "[{}, {}, {}]", + as_hex(&self.0[0]), + as_hex(&self.0[1]), + as_hex(&self.0[2]) + ), + _ => write!( + f, + "[{}, {}, … , {}, {}]", + as_hex(&self.0[0]), + as_hex(&self.0[1]), + as_hex(&self.0[len - 2]), + as_hex(&self.0[len - 1]) + ), + } + } +} + +impl fmt::Display for Seq064K<'_, U256<'_>> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let len = self.0.len(); + let as_hex = |item: &U256<'_>| { + item.inner_as_ref() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::() + }; + write!(f, "Seq064K write!(f, "[]"), + 1 => write!(f, "[{}]", as_hex(&self.0[0])), + 2 => write!(f, "[{}, {}]", as_hex(&self.0[0]), as_hex(&self.0[1])), + 3 => write!( + f, + "[{}, {}, {}]", + as_hex(&self.0[0]), + as_hex(&self.0[1]), + as_hex(&self.0[2]) + ), + _ => write!( + f, + "[{}, {}, ... , {}, {}]", + as_hex(&self.0[0]), + as_hex(&self.0[1]), + as_hex(&self.0[len - 2]), + as_hex(&self.0[len - 1]) + ), + } + } +} + +impl fmt::Display for Seq064K<'_, u16> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let len = self.0.len(); + write!(f, "Seq064K write!(f, "[]"), + 1 => write!(f, "[{}]", self.0[0]), + 2 => write!(f, "[{}, {}]", self.0[0], self.0[1]), + 3 => write!(f, "[{}, {}, {}]", self.0[0], self.0[1], self.0[2]), + _ => write!( + f, + "[{}, {}, ... , {}, {}]", + self.0[0], + self.0[1], + self.0[len - 2], + self.0[len - 1] + ), + } + } +} +impl fmt::Display for Seq064K<'_, u32> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let len = self.0.len(); + write!(f, "Seq064K write!(f, "[]"), + 1 => write!(f, "[{}]", self.0[0]), + 2 => write!(f, "[{}, {}]", self.0[0], self.0[1]), + 3 => write!(f, "[{}, {}, {}]", self.0[0], self.0[1], self.0[2]), + _ => write!( + f, + "[{}, {}, ... , {}, {}]", + self.0[0], + self.0[1], + self.0[len - 2], + self.0[len - 1] + ), + } + } +} + +impl fmt::Display for B032<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let item = self + .inner_as_ref() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + write!(f, "B032({item})") + } +} + impl From<[u8; 32]> for U256<'_> { fn from(v: [u8; 32]) -> Self { Inner::Owned(v.into()) diff --git a/protocols/v2/binary-sv2/codec/src/datatypes/non_copy_data_types/seq_inner.rs b/protocols/v2/binary-sv2/codec/src/datatypes/non_copy_data_types/seq_inner.rs index 53d5b3f75e..8cd71a8dca 100644 --- a/protocols/v2/binary-sv2/codec/src/datatypes/non_copy_data_types/seq_inner.rs +++ b/protocols/v2/binary-sv2/codec/src/datatypes/non_copy_data_types/seq_inner.rs @@ -113,7 +113,7 @@ use std::io::Read; /// [`Seq0255`] represents a sequence with a maximum length of 255 elements. /// This structure uses a generic type `T` and a lifetime parameter `'a`. -#[repr(C)] + #[derive(Debug, Clone, Eq, PartialEq)] pub struct Seq0255<'a, T>(pub Vec, PhantomData<&'a T>); @@ -447,7 +447,7 @@ impl<'a, const ISFIXED: bool, const SIZE: usize, const HEADERSIZE: usize, const } /// The lifetime 'a is defined. -#[repr(C)] + #[derive(Debug, Clone, Eq, PartialEq)] pub struct Sv2Option<'a, T>(pub Vec, PhantomData<&'a T>); diff --git a/protocols/v2/binary-sv2/codec/src/lib.rs b/protocols/v2/binary-sv2/codec/src/lib.rs index 36ed68fe15..cac81dacf3 100644 --- a/protocols/v2/binary-sv2/codec/src/lib.rs +++ b/protocols/v2/binary-sv2/codec/src/lib.rs @@ -49,11 +49,6 @@ //! - Size mismatches during encoding/decoding //! - Invalid data representations, such as non-boolean values interpreted as booleans. //! -//! # Cross-Language Interoperability -//! -//! To support foreign function interface (FFI) use cases, the module includes `CError` and `CVec` -//! types that represent SV2 data and errors in a format suitable for cross-language compatibility. -//! //! # Build Options //! //! Supports optional features like `no_std` for environments without standard library support. @@ -63,16 +58,6 @@ //! - With the `no_std` feature enabled, I/O-related errors use a simplified `IoError` //! representation. //! - Standard I/O errors (`std::io::Error`) are used when `no_std` is disabled. -//! -//! # FFI Interoperability -//! -//! Provides utilities for FFI (Foreign Function Interface) to enable data passing between Rust and -//! other languages. Includes: -//! - `CVec`: Represents a byte vector for safe passing between C and Rust. -//! - `CError`: A C-compatible error type. -//! - `CVec2`: Manages collections of `CVec` objects across FFI boundaries. -//! -//! Facilitates integration of SV2 functionality into cross-language projects. #![cfg_attr(feature = "no_std", no_std)] @@ -305,166 +290,6 @@ impl From for Error { } } -/// `CError` is a foreign function interface (FFI)-compatible version of the `Error` enum to -/// facilitate cross-language compatibility. -#[repr(C)] -#[derive(Debug)] -pub enum CError { - /// Indicates an attempt to read beyond a valid range. - OutOfBound, - - /// Raised when a non-binary value is interpreted as a boolean. - NotABool(u8), - - /// Occurs when an unexpected size mismatch arises during a write operation, specifying - /// expected and actual sizes. - WriteError(usize, usize), - - /// Signifies an overflow condition where a `u32` exceeds the maximum allowable `u24` value. - U24TooBig(u32), - - /// Reports a size mismatch for a signature, such as when it does not match the expected size. - InvalidSignatureSize(usize), - - /// Raised when a `u256` value is invalid, typically due to size discrepancies. - InvalidU256(usize), - - /// Indicates an invalid `u24` representation. - InvalidU24(u32), - - /// Error indicating that a byte array exceeds the maximum allowed size for `B0255`. - InvalidB0255Size(usize), - - /// Error indicating that a byte array exceeds the maximum allowed size for `B064K`. - InvalidB064KSize(usize), - - /// Error indicating that a byte array exceeds the maximum allowed size for `B016M`. - InvalidB016MSize(usize), - - /// Raised when a sequence size exceeds `0255`. - InvalidSeq0255Size(usize), - - /// Error when trying to encode a non-primitive data type. - NonPrimitiveTypeCannotBeEncoded, - - /// Generic conversion error related to primitive types. - PrimitiveConversionError, - - /// Error occurring during decoding due to conversion issues. - DecodableConversionError, - - /// Error triggered when a decoder is used without initialization. - UnInitializedDecoder, - - #[cfg(not(feature = "no_std"))] - /// Represents I/O-related errors, compatible with `no_std` mode where specific error types may - /// vary. - IoError(E), - - #[cfg(feature = "no_std")] - /// Represents I/O-related errors, compatible with `no_std` mode. - IoError, - - /// Raised when an unexpected mismatch occurs during read operations, specifying expected and - /// actual read sizes. - ReadError(usize, usize), - - /// Used as a marker error for fields that should remain void or empty. - VoidFieldMarker, - - /// Signifies a value overflow based on protocol restrictions, containing details about - /// fixed/variable size, maximum size allowed, and the offending value details. - ValueExceedsMaxSize(bool, usize, usize, usize, CVec, usize), - - /// Triggered when a sequence type (`Seq0255`, `Seq064K`) exceeds its maximum allowable size. - SeqExceedsMaxSize, - - /// Raised when no valid decodable field is provided during decoding. - NoDecodableFieldPassed, - - /// Error for protocol-specific invalid values. - ValueIsNotAValidProtocol(u8), - - /// Raised when an unsupported or unknown message type is encountered. - UnknownMessageType(u8), - - /// Indicates a protocol constraint violation where `Sv2Option` unexpectedly contains multiple - /// elements. - Sv2OptionHaveMoreThenOneElement(u8), -} - -impl From for CError { - fn from(e: Error) -> CError { - match e { - Error::OutOfBound => CError::OutOfBound, - Error::NotABool(u) => CError::NotABool(u), - Error::WriteError(u1, u2) => CError::WriteError(u1, u2), - Error::U24TooBig(u) => CError::U24TooBig(u), - Error::InvalidSignatureSize(u) => CError::InvalidSignatureSize(u), - Error::InvalidU256(u) => CError::InvalidU256(u), - Error::InvalidU24(u) => CError::InvalidU24(u), - Error::InvalidB0255Size(u) => CError::InvalidB0255Size(u), - Error::InvalidB064KSize(u) => CError::InvalidB064KSize(u), - Error::InvalidB016MSize(u) => CError::InvalidB016MSize(u), - Error::InvalidSeq0255Size(u) => CError::InvalidSeq0255Size(u), - Error::NonPrimitiveTypeCannotBeEncoded => CError::NonPrimitiveTypeCannotBeEncoded, - Error::PrimitiveConversionError => CError::PrimitiveConversionError, - Error::DecodableConversionError => CError::DecodableConversionError, - Error::UnInitializedDecoder => CError::UnInitializedDecoder, - #[cfg(not(feature = "no_std"))] - Error::IoError(e) => CError::IoError(e), - #[cfg(feature = "no_std")] - Error::IoError => CError::IoError, - Error::ReadError(u1, u2) => CError::ReadError(u1, u2), - Error::VoidFieldMarker => CError::VoidFieldMarker, - Error::ValueExceedsMaxSize(isfixed, size, headersize, maxsize, bad_value, bad_len) => { - let bv1: &[u8] = bad_value.as_ref(); - let bv: CVec = bv1.into(); - CError::ValueExceedsMaxSize(isfixed, size, headersize, maxsize, bv, bad_len) - } - Error::SeqExceedsMaxSize => CError::SeqExceedsMaxSize, - Error::NoDecodableFieldPassed => CError::NoDecodableFieldPassed, - Error::ValueIsNotAValidProtocol(u) => CError::ValueIsNotAValidProtocol(u), - Error::UnknownMessageType(u) => CError::UnknownMessageType(u), - Error::Sv2OptionHaveMoreThenOneElement(u) => CError::Sv2OptionHaveMoreThenOneElement(u), - } - } -} - -impl Drop for CError { - fn drop(&mut self) { - match self { - Self::OutOfBound => (), - Self::NotABool(_) => (), - Self::WriteError(_, _) => (), - Self::U24TooBig(_) => (), - Self::InvalidSignatureSize(_) => (), - Self::InvalidU256(_) => (), - Self::InvalidU24(_) => (), - Self::InvalidB0255Size(_) => (), - Self::InvalidB064KSize(_) => (), - Self::InvalidB016MSize(_) => (), - Self::InvalidSeq0255Size(_) => (), - Self::NonPrimitiveTypeCannotBeEncoded => (), - Self::PrimitiveConversionError => (), - Self::DecodableConversionError => (), - Self::UnInitializedDecoder => (), - #[cfg(not(feature = "no_std"))] - Self::IoError(_) => (), - #[cfg(feature = "no_std")] - Self::IoError => (), - Self::ReadError(_, _) => (), - Self::VoidFieldMarker => (), - Self::ValueExceedsMaxSize(_, _, _, _, cvec, _) => free_vec(cvec), - Self::SeqExceedsMaxSize => (), - Self::NoDecodableFieldPassed => (), - Self::ValueIsNotAValidProtocol(_) => (), - Self::UnknownMessageType(_) => (), - Self::Sv2OptionHaveMoreThenOneElement(_) => (), - }; - } -} - /// Vec is used as the Sv2 type Bytes impl GetSize for Vec { fn get_size(&self) -> usize { @@ -480,249 +305,8 @@ impl From> for EncodableField<'_> { } #[cfg(feature = "with_buffer_pool")] -impl<'a> From for EncodableField<'a> { +impl From for EncodableField<'_> { fn from(_v: buffer_sv2::Slice) -> Self { unreachable!() } } - -/// A struct to facilitate transferring a `Vec` across FFI boundaries. -#[repr(C)] -#[derive(Debug, Clone, Copy)] -pub struct CVec { - data: *mut u8, - len: usize, - capacity: usize, -} - -impl CVec { - /// Returns a mutable slice of the contained data. - /// - /// # Safety - /// - /// The caller must ensure that the data pointed to by `self.data` - /// remains valid for the duration of the returned slice. - pub fn as_mut_slice(&mut self) -> &mut [u8] { - unsafe { core::slice::from_raw_parts_mut(self.data, self.len) } - } - - /// Fills a buffer allocated in Rust from C. - /// - /// # Safety - /// - /// Constructs a `CVec` without taking ownership of the pointed buffer. If the owner drops the - /// buffer, the `CVec` will point to invalid memory. - #[allow(clippy::wrong_self_convention)] - pub fn as_shared_buffer(v: &mut [u8]) -> Self { - let (data, len) = (v.as_mut_ptr(), v.len()); - Self { - data, - len, - capacity: len, - } - } -} - -impl From<&[u8]> for CVec { - fn from(v: &[u8]) -> Self { - let mut buffer: Vec = vec![0; v.len()]; - buffer.copy_from_slice(v); - - // Get the length, first, then the pointer (doing it the other way around **currently** - // doesn't cause UB, but it may be unsound due to unclear (to me, at least) guarantees of - // the std lib) - let len = buffer.len(); - let ptr = buffer.as_mut_ptr(); - core::mem::forget(buffer); - - CVec { - data: ptr, - len, - capacity: len, - } - } -} - -/// Creates a `CVec` from a buffer that was allocated in C. -/// -/// # Safety -/// The caller must ensure that the buffer is valid and that -/// the data length does not exceed the allocated size. -#[no_mangle] -pub unsafe extern "C" fn cvec_from_buffer(data: *const u8, len: usize) -> CVec { - let input = core::slice::from_raw_parts(data, len); - - let mut buffer: Vec = vec![0; len]; - buffer.copy_from_slice(input); - - // Get the length, first, then the pointer (doing it the other way around **currently** doesn't - // cause UB, but it may be unsound due to unclear (to me, at least) guarantees of the std lib) - let len = buffer.len(); - let ptr = buffer.as_mut_ptr(); - core::mem::forget(buffer); - - CVec { - data: ptr, - len, - capacity: len, - } -} - -/// A struct to manage a collection of `CVec` objects across FFI boundaries. -#[repr(C)] -#[derive(Debug, Clone, Copy)] -pub struct CVec2 { - data: *mut CVec, - len: usize, - capacity: usize, -} - -impl CVec2 { - /// `as_mut_slice`: helps to get a mutable slice - pub fn as_mut_slice(&mut self) -> &mut [CVec] { - unsafe { core::slice::from_raw_parts_mut(self.data, self.len) } - } -} -impl From for Vec { - fn from(v: CVec2) -> Self { - unsafe { Vec::from_raw_parts(v.data, v.len, v.capacity) } - } -} - -/// Frees the underlying memory of a `CVec`. -pub fn free_vec(buf: &mut CVec) { - let _: Vec = unsafe { Vec::from_raw_parts(buf.data, buf.len, buf.capacity) }; -} - -/// Frees the underlying memory of a `CVec2` and all its elements. -pub fn free_vec_2(buf: &mut CVec2) { - let vs: Vec = unsafe { Vec::from_raw_parts(buf.data, buf.len, buf.capacity) }; - for mut s in vs { - free_vec(&mut s) - } -} - -impl<'a, const A: bool, const B: usize, const C: usize, const D: usize> - From> for CVec -{ - fn from(v: datatypes::Inner<'a, A, B, C, D>) -> Self { - let (ptr, len, cap): (*mut u8, usize, usize) = match v { - datatypes::Inner::Ref(inner) => { - // Data is copied in a vector that then will be forgetted from the allocator, - // cause the owner of the data is going to be dropped by rust - let mut inner: Vec = inner.into(); - - // Get the length, first, then the pointer (doing it the other way around - // **currently** doesn't cause UB, but it may be unsound due to unclear (to me, at - // least) guarantees of the std lib) - let len = inner.len(); - let cap = inner.capacity(); - let ptr = inner.as_mut_ptr(); - core::mem::forget(inner); - - (ptr, len, cap) - } - datatypes::Inner::Owned(mut inner) => { - // Get the length, first, then the pointer (doing it the other way around - // **currently** doesn't cause UB, but it may be unsound due to unclear (to me, at - // least) guarantees of the std lib) - let len = inner.len(); - let cap = inner.capacity(); - let ptr = inner.as_mut_ptr(); - core::mem::forget(inner); - - (ptr, len, cap) - } - }; - Self { - data: ptr, - len, - capacity: cap, - } - } -} - -/// Initializes an empty `CVec2`. -/// -/// # Safety -/// The caller is responsible for freeing the `CVec2` when it is no longer needed. -#[no_mangle] -pub unsafe extern "C" fn init_cvec2() -> CVec2 { - let mut buffer = Vec::::new(); - - // Get the length, first, then the pointer (doing it the other way around **currently** doesn't - // cause UB, but it may be unsound due to unclear (to me, at least) guarantees of the std lib) - let len = buffer.len(); - let ptr = buffer.as_mut_ptr(); - core::mem::forget(buffer); - - CVec2 { - data: ptr, - len, - capacity: len, - } -} - -/// Adds a `CVec` to a `CVec2`. -/// -/// # Safety -/// The caller must ensure no duplicate `CVec`s are added, as duplicates may -/// lead to double-free errors when the message is dropped. -#[no_mangle] -pub unsafe extern "C" fn cvec2_push(cvec2: &mut CVec2, cvec: CVec) { - let mut buffer: Vec = Vec::from_raw_parts(cvec2.data, cvec2.len, cvec2.capacity); - buffer.push(cvec); - - let len = buffer.len(); - let ptr = buffer.as_mut_ptr(); - core::mem::forget(buffer); - - cvec2.data = ptr; - cvec2.len = len; - cvec2.capacity = len; -} - -impl<'a, T: Into> From> for CVec2 { - fn from(v: Seq0255<'a, T>) -> Self { - let mut v: Vec = v.0.into_iter().map(|x| x.into()).collect(); - // Get the length, first, then the pointer (doing it the other way around **currently** - // doesn't cause UB, but it may be unsound due to unclear (to me, at least) guarantees of - // the std lib) - let len = v.len(); - let capacity = v.capacity(); - let data = v.as_mut_ptr(); - core::mem::forget(v); - Self { - data, - len, - capacity, - } - } -} -impl<'a, T: Into> From> for CVec2 { - fn from(v: Seq064K<'a, T>) -> Self { - let mut v: Vec = v.0.into_iter().map(|x| x.into()).collect(); - // Get the length, first, then the pointer (doing it the other way around **currently** - // doesn't cause UB, but it may be unsound due to unclear (to me, at least) guarantees of - // the std lib) - let len = v.len(); - let capacity = v.capacity(); - let data = v.as_mut_ptr(); - core::mem::forget(v); - Self { - data, - len, - capacity, - } - } -} - -/// Exported FFI functions for interoperability with C code for u24 -#[no_mangle] -pub extern "C" fn _c_export_u24(_a: U24) {} -/// Exported FFI functions for interoperability with C code for CVec -#[no_mangle] -pub extern "C" fn _c_export_cvec(_a: CVec) {} -/// Exported FFI functions for interoperability with C code for CVec2 -#[no_mangle] -pub extern "C" fn _c_export_cvec2(_a: CVec2) {} diff --git a/protocols/v2/binary-sv2/derive_codec/Cargo.lock b/protocols/v2/binary-sv2/derive_codec/Cargo.lock deleted file mode 100644 index 1f6ad16fb5..0000000000 --- a/protocols/v2/binary-sv2/derive_codec/Cargo.lock +++ /dev/null @@ -1,12 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -[[package]] -name = "derive_codec" -version = "0.1.0" -dependencies = [ - "parse", -] - -[[package]] -name = "parse" -version = "0.1.0" diff --git a/protocols/v2/binary-sv2/derive_codec/Cargo.toml b/protocols/v2/binary-sv2/derive_codec/Cargo.toml index e12f0c3506..59e8891335 100644 --- a/protocols/v2/binary-sv2/derive_codec/Cargo.toml +++ b/protocols/v2/binary-sv2/derive_codec/Cargo.toml @@ -2,7 +2,7 @@ name = "derive_codec_sv2" version = "1.1.1" authors = ["The Stratum V2 Developers"] -edition = "2018" +edition = "2021" readme = "README.md" description = "Derive macro for Sv2 binary format serializer and deserializer" documentation = "https://docs.rs/derive_codec_sv2" @@ -14,7 +14,7 @@ keywords = ["stratum", "mining", "bitcoin", "protocol"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -binary_codec_sv2 = { path="../codec", version = "^2.0.0" } +binary_codec_sv2 = { path="../codec", version = "^3.0.0" } [lib] proc-macro = true diff --git a/protocols/v2/binary-sv2/derive_codec/src/lib.rs b/protocols/v2/binary-sv2/derive_codec/src/lib.rs index 06be328cde..37da055224 100644 --- a/protocols/v2/binary-sv2/derive_codec/src/lib.rs +++ b/protocols/v2/binary-sv2/derive_codec/src/lib.rs @@ -342,10 +342,10 @@ fn get_struct_properties(item: TokenStream) -> ParsedStruct { break; } TokenTree::Punct(p) => { - struct_generics = format!("{}{}", struct_generics, p); + struct_generics = format!("{struct_generics}{p}"); } TokenTree::Ident(i) => { - struct_generics = format!("{}{}", struct_generics, i); + struct_generics = format!("{struct_generics}{i}"); } // Never executed at runtime it ok to panic _ => panic!("Struct {} has no fields", struct_name), diff --git a/protocols/v2/binary-sv2/src/lib.rs b/protocols/v2/binary-sv2/src/lib.rs index 61d490f059..95ec5152e7 100644 --- a/protocols/v2/binary-sv2/src/lib.rs +++ b/protocols/v2/binary-sv2/src/lib.rs @@ -620,8 +620,7 @@ mod test { c: 32, }; let len = expected.get_size(); - let mut buffer = Vec::new(); - buffer.resize(len, 0); + let mut buffer = vec![0; len]; to_writer(expected.clone(), &mut buffer).unwrap(); let deserialized: Test = from_bytes(&mut buffer[..]).unwrap(); assert_eq!(deserialized, expected); diff --git a/protocols/v2/channels-sv2/Cargo.toml b/protocols/v2/channels-sv2/Cargo.toml new file mode 100644 index 0000000000..0f866d1637 --- /dev/null +++ b/protocols/v2/channels-sv2/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "channels_sv2" +version = "2.0.0" +authors = ["The Stratum V2 Developers"] +edition = "2021" +readme = "README.md" +description = "Sv2 Channel Primitives" +documentation = "https://docs.rs/channels_sv2" +license = "MIT OR Apache-2.0" +repository = "https://github.com/stratum-mining/stratum" +homepage = "https://stratumprotocol.org" +keywords = ["stratum", "mining", "bitcoin", "protocol"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +binary_sv2 = { path = "../binary-sv2", version = "^4.0.0" } +common_messages_sv2 = { path = "../subprotocols/common-messages", version = "^6.0.0" } +mining_sv2 = { path = "../subprotocols/mining", version = "^5.0.0" } +template_distribution_sv2 = { path = "../subprotocols/template-distribution", version = "^4.0.0" } +job_declaration_sv2 = { path = "../subprotocols/job-declaration", version = "^5.0.0" } +tracing = { version = "0.1"} +bitcoin = { version = "0.32.5" } +primitive-types = "0.13.1" +hashbrown = { version = "0.15.5", optional = true } + + +[features] +default = [] +no_std = ["hashbrown"] diff --git a/protocols/v2/channels-sv2/README.md b/protocols/v2/channels-sv2/README.md new file mode 100644 index 0000000000..e4aa86fe5c --- /dev/null +++ b/protocols/v2/channels-sv2/README.md @@ -0,0 +1,17 @@ +# `channels_sv2` + +[![crates.io](https://img.shields.io/crates/v/channels_sv2.svg)](https://crates.io/crates/channels_sv2) +[![docs.rs](https://docs.rs/channels_sv2/badge.svg)](https://docs.rs/channels_sv2) +[![rustc+](https://img.shields.io/badge/rustc-1.75.0%2B-lightgrey.svg)](https://blog.rust-lang.org/2023/12/28/Rust-1.75.0.html) +[![license](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](https://github.com/stratum-mining/stratum/blob/main/LICENSE.md) +[![codecov](https://codecov.io/gh/stratum-mining/stratum/branch/main/graph/badge.svg?flag=channels_sv2-coverage)](https://codecov.io/gh/stratum-mining/stratum) + +`channels_sv2` provides primitives and abstractions for Stratum V2 (Sv2) Channels. + +This crate implements the core channel management functionality for both mining clients and servers, including standard, extended and group channels, and share accounting mechanisms. + +The `client` module is compatible with `no_std` environments. To enable this mode, build the crate with the `no_std` feature. In this configuration, standard library collections are replaced with the `hashbrown` crate, together with `core` and `alloc`, allowing the module to be used in embedded or constrained contexts. + +```bash +cargo build --features no_std +``` diff --git a/protocols/v2/channels-sv2/src/bip141.rs b/protocols/v2/channels-sv2/src/bip141.rs new file mode 100644 index 0000000000..6f7f08a8dc --- /dev/null +++ b/protocols/v2/channels-sv2/src/bip141.rs @@ -0,0 +1,186 @@ +//! Provides functionality to strip coinbase_tx_prefix and coinbase_tx_suffix from bip141 marker, +//! flag and witness data. +extern crate alloc; + +use alloc::vec::Vec; +use bitcoin::{blockdata::transaction::Transaction, consensus::Decodable}; +use mining_sv2::MAX_EXTRANONCE_LEN; + +use bitcoin::io::Cursor; + +const MARKER_FLAG_OFFSET: usize = 4; +const MARKER_FLAG_LEN: usize = 2; +const WITNESS_COUNT_LEN: usize = 1; +const WITNESS_LEN_LEN: usize = 1; +const WITNESS_DATA_LEN: usize = 32; +const LOCKTIME_LEN: usize = 4; + +#[derive(Debug)] +pub enum StripBip141Error { + FailedToDeserializeCoinbaseVersion, + FailedToDeserializeCoinbaseInputs, + FailedToDeserializeCoinbaseOutputs, + FailedToDeserializeCoinbaseLockTime, +} + +/// Tries to strip the bip141 marker, flag and witness data from `coinbase_tx_prefix` and +/// `coinbase_tx_suffix` of a `NewExtendedMiningJob`. +/// +/// This helps calculate the coinbase `txid` (instead of `wtxid`) for merkle root calculation, and +/// it is particularly helpful for translation to Sv1. +/// +/// If the coinbase transaction is already stripped of bip141, returns `Ok(None)`. +/// If the coinbase transaction is not stripped, returns `Ok(Some((Vec, Vec)))` with the +/// stripped `coinbase_tx_prefix` and `coinbase_tx_suffix`. +#[allow(clippy::type_complexity)] +pub fn try_strip_bip141( + coinbase_tx_prefix: &[u8], + coinbase_tx_suffix: &[u8], +) -> Result, Vec)>, StripBip141Error> { + let mut encoded = Vec::with_capacity( + coinbase_tx_prefix.len() + coinbase_tx_suffix.len() + MAX_EXTRANONCE_LEN, + ); + encoded.extend_from_slice(coinbase_tx_prefix); + encoded.extend_from_slice(&[0; MAX_EXTRANONCE_LEN]); + encoded.extend_from_slice(coinbase_tx_suffix); + + let mut decoder = Cursor::new(encoded); + let coinbase = Transaction { + version: Decodable::consensus_decode(&mut decoder) + .map_err(|_| StripBip141Error::FailedToDeserializeCoinbaseVersion)?, + input: Decodable::consensus_decode(&mut decoder) + .map_err(|_| StripBip141Error::FailedToDeserializeCoinbaseInputs)?, + output: Decodable::consensus_decode(&mut decoder) + .map_err(|_| StripBip141Error::FailedToDeserializeCoinbaseOutputs)?, + lock_time: Decodable::consensus_decode(&mut decoder) + .map_err(|_| StripBip141Error::FailedToDeserializeCoinbaseLockTime)?, + }; + + if coinbase.compute_txid().to_raw_hash() == coinbase.compute_wtxid().to_raw_hash() { + return Ok(None); + } + + // strip bip141 marker and flag bytes from coinbase_tx_prefix + let mut coinbase_tx_prefix_stripped_bip141 = coinbase_tx_prefix[0..MARKER_FLAG_OFFSET].to_vec(); + coinbase_tx_prefix_stripped_bip141 + .extend_from_slice(&coinbase_tx_prefix[MARKER_FLAG_OFFSET + MARKER_FLAG_LEN..]); + + // strip bip141 witness bytes from coinbase_tx_suffix + let locktime_position = coinbase_tx_suffix.len() - LOCKTIME_LEN; + + // strip witness count, witness length and witness data + let mut coinbase_tx_suffix_stripped_bip141 = coinbase_tx_suffix + [..locktime_position - WITNESS_COUNT_LEN - WITNESS_LEN_LEN - WITNESS_DATA_LEN] + .to_vec(); + coinbase_tx_suffix_stripped_bip141.extend_from_slice(&coinbase_tx_suffix[locktime_position..]); + + Ok(Some(( + coinbase_tx_prefix_stripped_bip141, + coinbase_tx_suffix_stripped_bip141, + ))) +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_try_strip_bip141_sri_stripped() { + // taken from a job sent by SRI that already stripped bip141 + let coinbase_tx_prefix = [ + 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 60, 2, 69, 8, 0, 22, 47, 83, 116, 114, 97, + 116, 117, 109, 32, 86, 50, 32, 83, 82, 73, 32, 80, 111, 111, 108, 47, 47, 32, + ] + .to_vec(); + let coinbase_tx_suffix = [ + 254, 255, 255, 255, 2, 0, 242, 5, 42, 1, 0, 0, 0, 22, 0, 20, 235, 225, 183, 220, 194, + 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194, 8, 252, 0, 0, 0, 0, 0, 0, + 0, 0, 38, 106, 36, 170, 33, 169, 237, 226, 246, 28, 63, 113, 209, 222, 253, 63, 169, + 153, 223, 163, 105, 83, 117, 92, 105, 6, 137, 121, 153, 98, 180, 139, 235, 216, 54, + 151, 78, 140, 249, 68, 8, 0, 0, + ] + .to_vec(); + + let result = try_strip_bip141(&coinbase_tx_prefix, &coinbase_tx_suffix) + .expect("failed trying to strip bip141"); + + assert!(result.is_none()); + } + + #[test] + fn test_try_strip_bip141_braiins() { + // taken from a job sent by Braiins (which strips bip141) + let coinbase_tx_prefix = [ + 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 76, 3, 206, 226, 13, 15, 47, 115, 108, 117, + 115, 104, 47, 183, 0, 23, 4, 15, 174, 96, 163, 250, 190, 109, 109, 207, 170, 255, 170, + 165, 167, 227, 112, 68, 27, 6, 55, 218, 219, 107, 176, 134, 175, 81, 46, 136, 141, 22, + 52, 201, 113, 254, 3, 148, 98, 86, 129, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 146, 162, 70, 0, + ] + .to_vec(); + let coinbase_tx_suffix = [ + 255, 255, 255, 255, 3, 194, 35, 192, 18, 0, 0, 0, 0, 23, 169, 20, 31, 12, 187, 236, + 139, 196, 201, 69, 228, 225, 98, 73, 177, 30, 238, 145, 30, 222, 213, 95, 135, 0, 0, 0, + 0, 0, 0, 0, 0, 38, 106, 36, 170, 33, 169, 237, 255, 27, 83, 239, 4, 153, 53, 158, 142, + 142, 137, 160, 34, 151, 40, 92, 185, 56, 40, 203, 144, 158, 95, 20, 155, 139, 53, 163, + 229, 145, 31, 81, 0, 0, 0, 0, 0, 0, 0, 0, 43, 106, 41, 82, 83, 75, 66, 76, 79, 67, 75, + 58, 188, 174, 124, 112, 138, 29, 26, 158, 194, 209, 140, 216, 148, 128, 27, 238, 106, + 189, 119, 214, 62, 100, 213, 246, 143, 66, 110, 12, 0, 120, 89, 116, 0, 0, 0, 0, + ] + .to_vec(); + + let result = try_strip_bip141(&coinbase_tx_prefix, &coinbase_tx_suffix) + .expect("failed trying to strip bip141"); + + assert!(result.is_none()); + } + + #[test] + fn test_try_strip_bip141_sri_not_yet_stripped() { + // taken from SRI before bip141 was stripped + let coinbase_tx_prefix = [ + 2, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 60, 2, 69, 8, 0, 22, 47, 83, 116, + 114, 97, 116, 117, 109, 32, 86, 50, 32, 83, 82, 73, 32, 80, 111, 111, 108, 47, 47, 32, + ] + .to_vec(); + let coinbase_tx_suffix = [ + 254, 255, 255, 255, 2, 0, 242, 5, 42, 1, 0, 0, 0, 22, 0, 20, 235, 225, 183, 220, 194, + 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194, 8, 252, 0, 0, 0, 0, 0, 0, + 0, 0, 38, 106, 36, 170, 33, 169, 237, 226, 246, 28, 63, 113, 209, 222, 253, 63, 169, + 153, 223, 163, 105, 83, 117, 92, 105, 6, 137, 121, 153, 98, 180, 139, 235, 216, 54, + 151, 78, 140, 249, 1, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 68, 8, 0, 0, + ] + .to_vec(); + + let (coinbase_tx_prefix_stripped_bip141, coinbase_tx_suffix_stripped_bip141) = + try_strip_bip141(&coinbase_tx_prefix, &coinbase_tx_suffix) + .expect("failed trying to strip bip141") + .unwrap(); + + assert_eq!( + coinbase_tx_prefix_stripped_bip141, + [ + 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 60, 2, 69, 8, 0, 22, 47, 83, 116, + 114, 97, 116, 117, 109, 32, 86, 50, 32, 83, 82, 73, 32, 80, 111, 111, 108, 47, 47, + 32 + ] + .to_vec() + ); + assert_eq!( + coinbase_tx_suffix_stripped_bip141, + [ + 254, 255, 255, 255, 2, 0, 242, 5, 42, 1, 0, 0, 0, 22, 0, 20, 235, 225, 183, 220, + 194, 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194, 8, 252, 0, 0, 0, + 0, 0, 0, 0, 0, 38, 106, 36, 170, 33, 169, 237, 226, 246, 28, 63, 113, 209, 222, + 253, 63, 169, 153, 223, 163, 105, 83, 117, 92, 105, 6, 137, 121, 153, 98, 180, 139, + 235, 216, 54, 151, 78, 140, 249, 68, 8, 0, 0 + ] + .to_vec() + ); + } +} diff --git a/protocols/v2/channels-sv2/src/chain_tip.rs b/protocols/v2/channels-sv2/src/chain_tip.rs new file mode 100644 index 0000000000..236be0edc8 --- /dev/null +++ b/protocols/v2/channels-sv2/src/chain_tip.rs @@ -0,0 +1,62 @@ +//! # Chain Tip +use binary_sv2::U256; +use mining_sv2::SetNewPrevHash as SetNewPrevHashMp; +use template_distribution_sv2::SetNewPrevHash as SetNewPrevHashTdp; + +/// An abstraction over the chain tip, carrying information from `SetNewPrevHash` messages. +/// +/// Used for: +/// - creating non-future jobs +/// - validating shares. +#[derive(Debug, Clone)] +pub struct ChainTip { + prev_hash: U256<'static>, + nbits: u32, + min_ntime: u32, +} + +impl ChainTip { + /// Constructs a new `ChainTip` instance. + pub fn new(prev_hash: U256<'static>, nbits: u32, min_ntime: u32) -> Self { + Self { + prev_hash, + nbits, + min_ntime, + } + } + + /// Retrieves the hash of the previous block + pub fn prev_hash(&self) -> U256<'static> { + self.prev_hash.clone() + } + + /// Retrieves the network difficulty for the current block + pub fn nbits(&self) -> u32 { + self.nbits + } + + /// Retrieves the smallest nTime value available for hashing + pub fn min_ntime(&self) -> u32 { + self.min_ntime + } +} + +impl From> for ChainTip { + fn from(set_new_prev_hash: SetNewPrevHashTdp) -> Self { + let set_new_prev_hash_static = set_new_prev_hash.into_static(); + let prev_hash = set_new_prev_hash_static.prev_hash; + let nbits = set_new_prev_hash_static.n_bits; + let min_ntime = set_new_prev_hash_static.header_timestamp; + Self::new(prev_hash, nbits, min_ntime) + } +} + +impl From> for ChainTip { + fn from(set_new_prev_hash: SetNewPrevHashMp) -> Self { + let set_new_prev_hash_static = set_new_prev_hash.into_static(); + let prev_hash = set_new_prev_hash_static.prev_hash; + let nbits = set_new_prev_hash_static.nbits; + let min_ntime = set_new_prev_hash_static.min_ntime; + Self::new(prev_hash, nbits, min_ntime) + } +} diff --git a/protocols/v2/channels-sv2/src/client/error.rs b/protocols/v2/channels-sv2/src/client/error.rs new file mode 100644 index 0000000000..0f84d8ec62 --- /dev/null +++ b/protocols/v2/channels-sv2/src/client/error.rs @@ -0,0 +1,49 @@ +//! # Channel Error Types +//! +//! This module defines error types for different channel contexts: extended, standard, +//! and group channels. Each error type represents specific categories of failures that +//! can occur during channel operations. + +use crate::bip141::StripBip141Error; + +/// Errors that can occur within an **extended channel** context. +/// +/// These include conditions where the extranonce prefix exceeds allowed limits +/// or a referenced job ID is not recognized by the channel. +#[derive(Debug)] +pub enum ExtendedChannelError { + /// The provided extranonce prefix exceeds the maximum allowed size. + NewExtranoncePrefixTooLarge, + + /// The specified job ID was not found in the extended channel. + JobIdNotFound, + FailedToTryToStripBip141(StripBip141Error), + FailedToStripBip141, + FailedToSerializeToB064K, + FailedToDeserializeCoinbaseOutputs, + ChannelIdMismatch, + RequestIdMismatch, + NoChainTip, + ChainTipMismatch, +} + +/// Errors that can occur within a **standard channel** context. +/// +/// These cover scenarios such as missing job IDs or an oversized extranonce prefix. +#[derive(Debug)] +pub enum StandardChannelError { + /// The specified job ID was not found in the standard channel. + JobIdNotFound, + + /// The provided extranonce prefix exceeds the maximum allowed size. + NewExtranoncePrefixTooLarge, +} + +/// Errors that can occur within a **group channel** context. +/// +/// Currently includes only job ID lookup failures. +#[derive(Debug)] +pub enum GroupChannelError { + /// The specified job ID was not found in the group channel. + JobIdNotFound, +} diff --git a/protocols/v2/roles-logic-sv2/src/channels/client/extended.rs b/protocols/v2/channels-sv2/src/client/extended.rs similarity index 66% rename from protocols/v2/roles-logic-sv2/src/channels/client/extended.rs rename to protocols/v2/channels-sv2/src/client/extended.rs index 48fbc827f3..b39978b40d 100644 --- a/protocols/v2/roles-logic-sv2/src/channels/client/extended.rs +++ b/protocols/v2/channels-sv2/src/client/extended.rs @@ -1,52 +1,62 @@ -//! Mining Client abstraction over the state of a Sv2 Extended Channel +//! # Sv2 Extended Channel - Mining Client Abstraction +//! +//! This module provides an abstraction over the state of an [Sv2](https://stratumprotocol.org/specification) +//! **Extended Channel** within a mining client. +extern crate alloc; +use super::HashMap; use crate::{ - channels::{ - chain_tip::ChainTip, - client::{ - error::ExtendedChannelError, - share_accounting::{ShareAccounting, ShareValidationError, ShareValidationResult}, - }, + bip141::try_strip_bip141, + chain_tip::ChainTip, + client::{ + error::ExtendedChannelError, + share_accounting::{ShareAccounting, ShareValidationError, ShareValidationResult}, }, - utils::{bytes_to_hex, merkle_root_from_path, target_to_difficulty, u256_to_block_hash}, + merkle_root::merkle_root_from_path, + target::{bytes_to_hex, target_to_difficulty, u256_to_block_hash}, }; -use binary_sv2::Sv2Option; -use mining_sv2::{ - NewExtendedMiningJob, SetNewPrevHash as SetNewPrevHashMp, SubmitSharesExtended, Target, - MAX_EXTRANONCE_LEN, -}; -use std::{collections::HashMap, convert::TryInto}; -use stratum_common::bitcoin::{ - blockdata::block::{Header, Version}, +use alloc::{format, string::String, vec, vec::Vec}; +use binary_sv2::{self, Sv2Option}; +use bitcoin::{ + absolute::LockTime, + blockdata::block::{Header, Version as BlockVersion}, + consensus::{serialize, Decodable}, hashes::sha256d::Hash, - CompactTarget, Target as BitcoinTarget, + transaction::Version, + CompactTarget, OutPoint, Sequence, Target as BitcoinTarget, Transaction, TxIn, TxOut, Witness, +}; +use mining_sv2::{ + NewExtendedMiningJob, SetCustomMiningJob, SetCustomMiningJobSuccess, + SetNewPrevHash as SetNewPrevHashMp, SubmitSharesExtended, Target, MAX_EXTRANONCE_LEN, }; use tracing::debug; -// ExtendedJob is a tuple of: -// - the NewExtendedMiningJob message -// - the extranonce_prefix associated with the channel at the time of job creation +/// A type alias representing an extended mining job tied to a specific `extranonce_prefix`. +/// +/// Extended jobs allow Merkle root rolling, providing broader control over the search space. +/// Each job includes: +/// - A [`NewExtendedMiningJob`] message +/// - The `extranonce_prefix` in use when the job was created pub type ExtendedJob<'a> = (NewExtendedMiningJob<'a>, Vec); -/// Mining Client abstraction over the state of a Sv2 Extended Channel. +/// Mining Client abstraction for the state management of an Sv2 Extended Channel. /// -/// It keeps track of: -/// - the channel's unique `channel_id` -/// - the channel's `user_identity` -/// - the channel's unique `extranonce_prefix` -/// - the channel's rollable extranonce size -/// - the channel's target -/// - the channel's nominal hashrate -/// - the channel's version rolling -/// - the channel's future jobs (indexed by `job_id`, to be activated upon receipt of a -/// `SetNewPrevHash` message) -/// - the channel's active job -/// - the channel's past jobs (which were active jobs under the current chain tip, indexed by -/// `job_id`) -/// - the channel's stale jobs (which were past and active jobs under the previous chain tip, -/// indexed by `job_id`) -/// - the channel's share accounting (as seen by the client) -/// - the channel's chain tip +/// This struct encapsulates all channel-specific state for a mining client, including: +/// - The channel's unique `channel_id`. +/// - The channel's `user_identity` as seen by upstream. +/// - The channel's unique `extranonce_prefix`. +/// - The size of the rollable portion of the extranonce. +/// - The channel's current target. +/// - The channel's nominal hashrate. +/// - Whether version rolling is supported (see [BIP 320](https://github.com/bitcoin/bips/blob/master/bip-0320.mediawiki)). +/// - Future jobs (indexed by `job_id`) to be activated by a [`SetNewPrevHash`](SetNewPrevHashMp) +/// message. +/// - The currently active job. +/// - Past jobs (previously active under the current chain tip, indexed by `job_id`). +/// - Stale jobs (previously active and past jobs under the previous chain tip, indexed by +/// `job_id`). +/// - Share accounting for the channel (as tracked by the client). +/// - The channel's current chain tip. #[derive(Clone, Debug)] pub struct ExtendedChannel<'a> { channel_id: u32, @@ -68,6 +78,7 @@ pub struct ExtendedChannel<'a> { } impl<'a> ExtendedChannel<'a> { + /// Constructs a new [`ExtendedChannel`]. pub fn new( channel_id: u32, user_identity: String, @@ -94,27 +105,44 @@ impl<'a> ExtendedChannel<'a> { } } + /// Returns the unique `channel_id` of this channel. pub fn get_channel_id(&self) -> u32 { self.channel_id } + /// Returns the `user_identity` used by the upstream node to identify this client. pub fn get_user_identity(&self) -> &String { &self.user_identity } + /// Returns the bytes representing the first part of the `extranonce`. pub fn get_extranonce_prefix(&self) -> &Vec { &self.extranonce_prefix } + /// Returns `true` if the channel supports version rolling as per [BIP 320](https://github.com/bitcoin/bips/blob/master/bip-0320.mediawiki). pub fn is_version_rolling(&self) -> bool { self.version_rolling } - /// Sets the extranonce prefix. + /// Returns a reference to the current [`ChainTip`], if any. + pub fn get_chain_tip(&self) -> Option<&ChainTip> { + self.chain_tip.as_ref() + } + + /// Sets the [`ChainTip`]. + pub fn set_chain_tip(&mut self, chain_tip: ChainTip) { + self.chain_tip = Some(chain_tip); + } + + /// Sets a new extranonce prefix for the channel. /// - /// Note: after this, all new jobs will be associated with the new extranonce prefix. - /// Jobs created before this call will remain associated with the previous extranonce prefix, - /// and share validation will be done accordingly. + /// After this change, all new jobs will use the new extranonce prefix. + /// Jobs created before this call retain the previous extranonce prefix, + /// and share validation is performed accordingly. + /// + /// Returns an error if the new prefix violates the minimum rollable extranonce size established + /// at channel creation. pub fn set_extranonce_prefix( &mut self, new_extranonce_prefix: Vec, @@ -135,43 +163,80 @@ impl<'a> ExtendedChannel<'a> { Ok(()) } + /// Returns the available size, in bytes, of the rollable portion of the extranonce. pub fn get_rollable_extranonce_size(&self) -> u16 { self.rollable_extranonce_size } + /// Returns a reference to the current [`Target`] for this channel. pub fn get_target(&self) -> &Target { &self.target } + /// Sets a new [`Target`] for the channel. pub fn set_target(&mut self, new_target: Target) { self.target = new_target; } + /// Returns the cumulative nominal hashrate for the channel, in h/s. pub fn get_nominal_hashrate(&self) -> f32 { self.nominal_hashrate } + /// Returns a reference to the currently active job, if any. pub fn get_active_job(&self) -> Option<&ExtendedJob<'a>> { self.active_job.as_ref() } + /// Returns a reference to all future jobs for this channel. pub fn get_future_jobs(&self) -> &HashMap> { &self.future_jobs } + /// Returns a reference to all past jobs for this channel. pub fn get_past_jobs(&self) -> &HashMap> { &self.past_jobs } + /// Returns a reference to all stale jobs for this channel. pub fn get_stale_jobs(&self) -> &HashMap> { &self.stale_jobs } - /// Called when a `NewExtendedMiningJob` message is received from upstream. + /// Returns a reference to the [`ShareAccounting`] for this channel. + pub fn get_share_accounting(&self) -> &ShareAccounting { + &self.share_accounting + } + + /// Handles a [`NewExtendedMiningJob`] message received from upstream. + /// + /// - If [`NewExtendedMiningJob::min_ntime`] is empty, the job is considered a future job and + /// added to the future jobs list (see [`get_future_jobs`](ExtendedChannel::get_future_jobs)). + /// - Otherwise, the job is activated and previous active job moves to the past jobs list. pub fn on_new_extended_mining_job( &mut self, - new_extended_mining_job: NewExtendedMiningJob<'a>, - ) { + mut new_extended_mining_job: NewExtendedMiningJob<'a>, + ) -> Result<(), ExtendedChannelError> { + // try to strip bip141 bytes from coinbase_tx_prefix and coinbase_tx_suffix, if they are + // present + let new_extended_mining_job = match try_strip_bip141( + new_extended_mining_job.coinbase_tx_prefix.inner_as_ref(), + new_extended_mining_job.coinbase_tx_suffix.inner_as_ref(), + ) + .map_err(ExtendedChannelError::FailedToTryToStripBip141)? + { + Some((coinbase_tx_prefix_stripped_bip141, coinbase_tx_suffix_stripped_bip141)) => { + new_extended_mining_job.coinbase_tx_prefix = coinbase_tx_prefix_stripped_bip141 + .try_into() + .map_err(|_| ExtendedChannelError::FailedToSerializeToB064K)?; + new_extended_mining_job.coinbase_tx_suffix = coinbase_tx_suffix_stripped_bip141 + .try_into() + .map_err(|_| ExtendedChannelError::FailedToSerializeToB064K)?; + new_extended_mining_job + } + None => new_extended_mining_job, + }; + match new_extended_mining_job.min_ntime.clone().into_inner() { Some(_min_ntime) => { if let Some(active_job) = self.active_job.clone() { @@ -186,19 +251,147 @@ impl<'a> ExtendedChannel<'a> { ); } } + + Ok(()) } - /// Called when a `SetNewPrevHash` message is received from upstream. + /// Handles a `SetCustomMiningJobSuccess` message from upstream. + /// Requires the corresponding `SetCustomMiningJob`. /// - /// If the job_id addressed in the `SetNewPrevHash` is not a future job, - /// returns an error. + /// To be used by a Sv2 Job Declarator Client + pub fn on_set_custom_mining_job_success( + &mut self, + set_custom_mining_job: SetCustomMiningJob<'a>, + set_custom_mining_job_success: SetCustomMiningJobSuccess, + ) -> Result<(), ExtendedChannelError> { + if set_custom_mining_job.channel_id != set_custom_mining_job_success.channel_id + || set_custom_mining_job.channel_id != self.channel_id + { + return Err(ExtendedChannelError::ChannelIdMismatch); + } + + if set_custom_mining_job.request_id != set_custom_mining_job_success.request_id { + return Err(ExtendedChannelError::RequestIdMismatch); + } + + let Some(chain_tip) = self.chain_tip.clone() else { + return Err(ExtendedChannelError::NoChainTip); + }; + + if set_custom_mining_job.min_ntime != chain_tip.min_ntime() + || set_custom_mining_job.prev_hash != chain_tip.prev_hash() + || set_custom_mining_job.nbits != chain_tip.nbits() + { + return Err(ExtendedChannelError::ChainTipMismatch); + } + + let deserialized_outputs = Vec::::consensus_decode( + &mut set_custom_mining_job + .coinbase_tx_outputs + .inner_as_ref() + .to_vec() + .as_slice(), + ) + .map_err(|_| ExtendedChannelError::FailedToDeserializeCoinbaseOutputs)?; + + let mut script_sig = vec![]; + script_sig.extend_from_slice(set_custom_mining_job.coinbase_prefix.inner_as_ref()); + script_sig.extend_from_slice(&[0; MAX_EXTRANONCE_LEN]); + + let tx_in = TxIn { + previous_output: OutPoint::null(), + script_sig: script_sig.into(), + sequence: Sequence(set_custom_mining_job.coinbase_tx_input_n_sequence), + witness: Witness::from(vec![vec![0; 32]]), /* note: 32 bytes of zeros is only safe to + * assume now, this could change in future + * soft forks */ + }; + + let coinbase = Transaction { + version: Version::non_standard(set_custom_mining_job.coinbase_tx_version as i32), + lock_time: LockTime::from_consensus(set_custom_mining_job.coinbase_tx_locktime), + input: vec![tx_in], + output: deserialized_outputs, + }; + + let serialized_coinbase = serialize(&coinbase); + + let prefix_index = 4 // tx version + + 2 // segwit + + 1 // number of inputs + + 32 // prev OutPoint + + 4 // index + + 1 // bytes in script + + set_custom_mining_job.coinbase_prefix.inner_as_ref().len(); + + let coinbase_tx_prefix = serialized_coinbase[0..prefix_index].to_vec(); + + let suffix_index = prefix_index + MAX_EXTRANONCE_LEN; + + let coinbase_tx_suffix = serialized_coinbase[suffix_index..].to_vec(); + + // strip bip141 bytes from coinbase_tx_prefix and coinbase_tx_suffix + let (coinbase_tx_prefix_stripped_bip141, coinbase_tx_suffix_stripped_bip141) = + try_strip_bip141(&coinbase_tx_prefix, &coinbase_tx_suffix) + .map_err(ExtendedChannelError::FailedToTryToStripBip141)? + .ok_or(ExtendedChannelError::FailedToStripBip141)?; + + let new_extended_mining_job = NewExtendedMiningJob { + channel_id: set_custom_mining_job.channel_id, + job_id: set_custom_mining_job_success.job_id, + min_ntime: Sv2Option::new(Some(set_custom_mining_job.min_ntime)), + version: set_custom_mining_job.version, + version_rolling_allowed: self.version_rolling, + coinbase_tx_prefix: coinbase_tx_prefix_stripped_bip141 + .try_into() + .map_err(|_| ExtendedChannelError::FailedToSerializeToB064K)?, + coinbase_tx_suffix: coinbase_tx_suffix_stripped_bip141 + .try_into() + .map_err(|_| ExtendedChannelError::FailedToSerializeToB064K)?, + merkle_path: set_custom_mining_job.merkle_path, + }; + + if let Some(active_job) = self.active_job.clone() { + self.past_jobs.insert(active_job.0.job_id, active_job); + } + self.active_job = Some((new_extended_mining_job, self.extranonce_prefix.clone())); + + Ok(()) + } + + /// Handles a [`ChainTip`] update. /// - /// If the job_id addressed in the `SetNewPrevHash` is a future job, - /// it is "activated" and set as the active job. + /// To be used by a Sv2 Job Declarator Client, which should never receive a + /// [`SetNewPrevHash`](SetNewPrevHashMp) (Mining Protocol) message, or will most likely + /// ignore it if it does. /// - /// All past jobs are marked as stale, so that shares are not propagated. + /// So a [`SetNewPrevHash`](template_distribution_sv2::SetNewPrevHash) (Template Distribution + /// Protocol) message should be converted into a [`ChainTip`] and passed to this function. + pub fn on_chain_tip_update(&mut self, chain_tip: ChainTip) -> Result<(), ExtendedChannelError> { + self.chain_tip = Some(chain_tip); + + // all other future jobs are now useless + self.future_jobs.clear(); + + // mark all past jobs as stale, so that shares are not propagated + self.stale_jobs = self.past_jobs.clone(); + + // clear past jobs, as we're no longer going to propagate shares for them + self.past_jobs.clear(); + + // clear seen shares, as shares for past chain tip will be rejected as stale + self.share_accounting.flush_seen_shares(); + + Ok(()) + } + + /// Handles a [`SetNewPrevHash`](SetNewPrevHashMp) message from upstream. /// - /// The chain tip information is not kept in the channel state. + /// - If the referenced `job_id` is not a future job, returns an error. + /// - If it is a future job, activates it as the current job. + /// - Marks all past jobs as stale and clears them. + /// - Clears all seen shares as shares for the previous chain tip will be rejected as stale. + /// - Updates the chain tip for the channel. pub fn on_set_new_prev_hash( &mut self, set_new_prev_hash: SetNewPrevHashMp<'a>, @@ -225,25 +418,17 @@ impl<'a> ExtendedChannel<'a> { // clear seen shares, as shares for past chain tip will be rejected as stale self.share_accounting.flush_seen_shares(); - let set_new_prev_hash_static = set_new_prev_hash.into_static(); - let new_chain_tip = ChainTip::new( - set_new_prev_hash_static.prev_hash, - set_new_prev_hash_static.nbits, - set_new_prev_hash_static.min_ntime, - ); - self.chain_tip = Some(new_chain_tip); + self.chain_tip = Some(set_new_prev_hash.into()); Ok(()) } - /// Validates a share, to be used before submission upstream. - /// - /// Updates the channel state with the result of the share validation. + /// Validates a share prior to submission upstream. /// - /// - Allows the mining client to avoid propagating stale, duplicate or low-diff shares. - /// - Allows the mining client to know whether a block was found on some share. - /// - Allows the mining client to keep a local version of the share accounting for comparison - /// with the acknowledgements coming from the upstream server. + /// Updates channel state with the share validation result: + /// - Prevents propagation of stale, duplicate, or low-difficulty shares. + /// - Indicates whether a block was found from the share. + /// - Maintains local share accounting for later reconciliation with upstream acknowledgements. pub fn validate_share( &mut self, share: SubmitSharesExtended, @@ -313,7 +498,7 @@ impl<'a> ExtendedChannel<'a> { // create the header for validation let header = Header { - version: Version::from_consensus(share.version as i32), + version: BlockVersion::from_consensus(share.version as i32), prev_blockhash: u256_to_block_hash(prev_hash.clone()), merkle_root: (*Hash::from_bytes_ref(&merkle_root)).into(), time: share.ntime, @@ -378,7 +563,7 @@ impl<'a> ExtendedChannel<'a> { #[cfg(test)] mod tests { - use crate::channels::client::{ + use crate::client::{ extended::ExtendedChannel, share_accounting::{ShareValidationError, ShareValidationResult}, }; @@ -420,8 +605,8 @@ mod tests { version: 536870912, version_rolling_allowed: true, coinbase_tx_prefix: vec![ - 2, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 34, 82, 0, + 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 34, 82, 0, ] .try_into() .unwrap(), @@ -430,15 +615,16 @@ mod tests { 194, 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194, 8, 252, 0, 0, 0, 0, 0, 0, 0, 0, 38, 106, 36, 170, 33, 169, 237, 226, 246, 28, 63, 113, 209, 222, 253, 63, 169, 153, 223, 163, 105, 83, 117, 92, 105, 6, 137, 121, 153, 98, 180, 139, - 235, 216, 54, 151, 78, 140, 249, 1, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 235, 216, 54, 151, 78, 140, 249, 0, 0, 0, 0, ] .try_into() .unwrap(), merkle_path: vec![].try_into().unwrap(), }; - channel.on_new_extended_mining_job(future_job.clone()); + channel + .on_new_extended_mining_job(future_job.clone()) + .unwrap(); assert_eq!(channel.get_future_jobs().len(), 1); assert_eq!(channel.get_active_job(), None); @@ -502,8 +688,8 @@ mod tests { version: 536870912, version_rolling_allowed: true, coinbase_tx_prefix: vec![ - 2, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 34, 82, 0, + 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 34, 82, 0, ] .try_into() .unwrap(), @@ -512,15 +698,16 @@ mod tests { 194, 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194, 8, 252, 0, 0, 0, 0, 0, 0, 0, 0, 38, 106, 36, 170, 33, 169, 237, 226, 246, 28, 63, 113, 209, 222, 253, 63, 169, 153, 223, 163, 105, 83, 117, 92, 105, 6, 137, 121, 153, 98, 180, 139, - 235, 216, 54, 151, 78, 140, 249, 1, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 235, 216, 54, 151, 78, 140, 249, 0, 0, 0, 0, ] .try_into() .unwrap(), merkle_path: vec![].try_into().unwrap(), }; - channel.on_new_extended_mining_job(active_job.clone()); + channel + .on_new_extended_mining_job(active_job.clone()) + .unwrap(); assert_eq!(channel.get_future_jobs().len(), 0); assert_eq!( @@ -531,7 +718,9 @@ mod tests { let mut new_active_job = active_job.clone(); new_active_job.job_id = 2; - channel.on_new_extended_mining_job(new_active_job.clone()); + channel + .on_new_extended_mining_job(new_active_job.clone()) + .unwrap(); assert_eq!(channel.get_future_jobs().len(), 0); assert_eq!( @@ -590,7 +779,9 @@ mod tests { merkle_path: vec![].try_into().unwrap(), }; - channel.on_new_extended_mining_job(future_job.clone()); + channel + .on_new_extended_mining_job(future_job.clone()) + .unwrap(); // network target: 7fffff0000000000000000000000000000000000000000000000000000000000 let nbits = 545259519; @@ -682,7 +873,9 @@ mod tests { merkle_path: vec![].try_into().unwrap(), }; - channel.on_new_extended_mining_job(future_job.clone()); + channel + .on_new_extended_mining_job(future_job.clone()) + .unwrap(); // network target: 000000000000d7c0000000000000000000000000000000000000000000000000 let nbits = 453040064; @@ -777,7 +970,9 @@ mod tests { merkle_path: vec![].try_into().unwrap(), }; - channel.on_new_extended_mining_job(future_job.clone()); + channel + .on_new_extended_mining_job(future_job.clone()) + .unwrap(); // network target: 000000000000d7c0000000000000000000000000000000000000000000000000 let nbits: u32 = 453040064; diff --git a/protocols/v2/roles-logic-sv2/src/channels/client/group.rs b/protocols/v2/channels-sv2/src/client/group.rs similarity index 51% rename from protocols/v2/roles-logic-sv2/src/channels/client/group.rs rename to protocols/v2/channels-sv2/src/client/group.rs index ded10529ed..2eba05bbce 100644 --- a/protocols/v2/roles-logic-sv2/src/channels/client/group.rs +++ b/protocols/v2/channels-sv2/src/client/group.rs @@ -1,34 +1,40 @@ -//! Abstraction over the state of a Sv2 Group Channel, as seen by a Mining Client - -use crate::channels::client::error::GroupChannelError; - -use std::collections::{HashMap, HashSet}; +//! Sv2 Group Channel - Mining Client Abstraction. +//! +//! This module provides the [`GroupChannel`] struct, which acts as a mining client's +//! abstraction over the state of a Sv2 group channel. It tracks group-level job state +//! and associated standard channels, but delegates share validation and job lifecycle +//! to standard channels. +use super::{HashMap, HashSet}; +use crate::client::error::GroupChannelError; use mining_sv2::{NewExtendedMiningJob, SetNewPrevHash as SetNewPrevHashMp}; -/// Mining Client abstraction over the state of a Sv2 Group Channel. +/// Mining Client abstraction over the state of an Sv2 Group Channel. /// -/// It keeps track of: +/// Tracks: /// - the group channel's unique `group_channel_id` -/// - the group channel's `standard_channel_ids` (indexed by `channel_id`) -/// - the group channel's future jobs (indexed by `job_id`, to be activated upon receipt of a -/// `SetNewPrevHash` message) -/// - the group channel's active job +/// - associated `standard_channel_ids` (indexed by `channel_id`) +/// - future jobs (indexed by `job_id`, to be activated upon receipt of a +/// [`SetNewPrevHash`](SetNewPrevHashMp) message) +/// - active job /// -/// Since share validation happens at the Standard Channel level, we don't really keep track of: -/// - the group channel's past jobs -/// - the group channel's stale jobs -/// - the group channel's share validation state +/// Does **not** track: +/// - past or stale jobs +/// - share validation state (handled per-standard channel) #[derive(Debug, Clone)] pub struct GroupChannel<'a> { + /// Unique identifier for the group channel group_channel_id: u32, + /// Set of channel IDs associated with this group channel standard_channel_ids: HashSet, - // future jobs are indexed with job_id (u32) + /// Future jobs, indexed by job_id, waiting to be activated future_jobs: HashMap>, + /// Currently active mining job for the group channel active_job: Option>, } impl<'a> GroupChannel<'a> { + /// Creates a new [`GroupChannel`] with the given group_channel_id. pub fn new(group_channel_id: u32) -> Self { Self { group_channel_id, @@ -38,34 +44,42 @@ impl<'a> GroupChannel<'a> { } } + /// Adds a [`StandardChannel`](crate::client::standard::StandardChannel) to the group channel + /// by referencing its `channel_id`. pub fn add_standard_channel_id(&mut self, standard_channel_id: u32) { self.standard_channel_ids.insert(standard_channel_id); } + /// Removes a [`StandardChannel`](crate::client::standard::StandardChannel) from the group + /// channel by its `channel_id`. pub fn remove_standard_channel_id(&mut self, standard_channel_id: u32) { self.standard_channel_ids.remove(&standard_channel_id); } + /// Returns the group channel ID. pub fn get_group_channel_id(&self) -> u32 { self.group_channel_id } + /// Returns a reference to all standard channel IDs associated with this group channel. pub fn get_standard_channel_ids(&self) -> &HashSet { &self.standard_channel_ids } + /// Returns a reference to the current active job, if any. pub fn get_active_job(&self) -> Option<&NewExtendedMiningJob<'a>> { self.active_job.as_ref() } + /// Returns a reference to all future jobs indexed by job_id. pub fn get_future_jobs(&self) -> &HashMap> { &self.future_jobs } - /// Called when a `NewExtendedMiningJob` message is received from upstream. + /// Handles a newly received [`NewExtendedMiningJob`] message from upstream. /// - /// If the job is a future job, it is added to the `future_jobs` map. - /// If the job is an active job, it is set as the active job. + /// - If `min_ntime` is present, sets this job as active. + /// - If `min_ntime` is empty, stores it as a future job. pub fn on_new_extended_mining_job( &mut self, new_extended_mining_job: NewExtendedMiningJob<'a>, @@ -81,15 +95,12 @@ impl<'a> GroupChannel<'a> { } } - /// Called when a `SetNewPrevHash` message is received from upstream. - /// - /// If there is some future job matching the `job_id` that `SetNewPrevHash` points to, - /// this future job is "activated" and set as the active job. + /// Handles an upstream [`SetNewPrevHash`](SetNewPrevHashMp) message. /// - /// If there is not future job matching the `job_id` that `SetNewPrevHash` points to, - /// returns an error. + /// Activates the future job matching `job_id` from the message, making it the active job. + /// Clears all other future jobs. /// - /// All other future jobs are cleared. + /// Returns `Err(GroupChannelError::JobIdNotFound)` if no matching job found. pub fn on_set_new_prev_hash( &mut self, set_new_prev_hash: SetNewPrevHashMp<'a>, diff --git a/protocols/v2/channels-sv2/src/client/mod.rs b/protocols/v2/channels-sv2/src/client/mod.rs new file mode 100644 index 0000000000..a7b11f7ae7 --- /dev/null +++ b/protocols/v2/channels-sv2/src/client/mod.rs @@ -0,0 +1,23 @@ +//! Sv2 channels - Mining Clients Abstractions. +//! +//! The `client` module is compatible with `no_std` environments. To enable this mode, build the +//! crate with the `no_std` feature. In this configuration, standard library collections are +//! replaced with the `hashbrown` crate, together with `core` and `alloc`, allowing the module to be +//! used in embedded or constrained contexts. + +pub mod error; +pub mod extended; +pub mod group; +pub mod share_accounting; +pub mod standard; + +// Type aliases that switch between `std::collections` and `hashbrown` +// depending on whether the `no_std` feature is enabled. +#[cfg(not(feature = "no_std"))] +type HashMap = std::collections::HashMap; +#[cfg(not(feature = "no_std"))] +type HashSet = std::collections::HashSet; +#[cfg(feature = "no_std")] +type HashMap = hashbrown::HashMap; +#[cfg(feature = "no_std")] +type HashSet = hashbrown::HashSet; diff --git a/protocols/v2/channels-sv2/src/client/share_accounting.rs b/protocols/v2/channels-sv2/src/client/share_accounting.rs new file mode 100644 index 0000000000..bd9f1de5ca --- /dev/null +++ b/protocols/v2/channels-sv2/src/client/share_accounting.rs @@ -0,0 +1,132 @@ +//! Share Validation - Mining Client Abstraction. +//! +//! This module provides types and logic for validating mining shares, tracking share +//! statistics, and reporting share validation results and errors. These abstractions +//! are intended for use in Mining Clients. + +use super::HashSet; +use bitcoin::hashes::sha256d::Hash; + +/// The outcome of share validation, as seen by a Mining Client. +/// +/// - `Valid`: The share is valid and accepted. +/// - `BlockFound`: The submitted share resulted in a new block being found. +#[derive(Debug)] +pub enum ShareValidationResult { + Valid, + BlockFound, +} + +/// Possible errors encountered during share validation. +/// +/// - `Invalid`: The share is malformed or not valid. +/// - `Stale`: The share refers to an outdated job or block tip. +/// - `InvalidJobId`: The job ID referenced by the share is not recognized. +/// - `DoesNotMeetTarget`: The share does not meet the required target difficulty. +/// - `VersionRollingNotAllowed`: Version rolling is not permitted for this channel/job. +/// - `DuplicateShare`: The share has already been submitted (detected by hash). +/// - `NoChainTip`: The chain tip is unknown or unavailable. +#[derive(Debug)] +pub enum ShareValidationError { + Invalid, + Stale, + InvalidJobId, + DoesNotMeetTarget, + VersionRollingNotAllowed, + DuplicateShare, + NoChainTip, +} + +/// Tracks share validation state for a specific channel (Extended or Standard). +/// +/// Used only on Mining Clients. +/// Keeps statistics and state for shares submitted through the channel: +/// - last received share's sequence number +/// - total accepted shares +/// - cumulative work from accepted shares +/// - hashes of seen shares (for duplicate detection) +/// - highest difficulty seen in accepted shares +#[derive(Clone, Debug)] +pub struct ShareAccounting { + last_share_sequence_number: u32, + shares_accepted: u32, + share_work_sum: u64, + seen_shares: HashSet, + best_diff: f64, +} + +impl Default for ShareAccounting { + fn default() -> Self { + Self::new() + } +} + +impl ShareAccounting { + /// Creates a new [`ShareAccounting`] instance, initializing all statistics to zero. + pub fn new() -> Self { + Self { + last_share_sequence_number: 0, + shares_accepted: 0, + share_work_sum: 0, + seen_shares: HashSet::new(), + best_diff: 0.0, + } + } + + /// Updates the accounting state with a newly accepted share. + /// + /// - Increments share count and total work. + /// - Updates last share sequence number. + /// - Records share hash to detect duplicates. + pub fn update_share_accounting( + &mut self, + share_work: u64, + share_sequence_number: u32, + share_hash: Hash, + ) { + self.last_share_sequence_number = share_sequence_number; + self.shares_accepted += 1; + self.share_work_sum += share_work; + self.seen_shares.insert(share_hash); + } + + /// Clears the set of seen share hashes. + /// + /// Should be called on every chain tip update + /// to prevent unbounded memory growth. + pub fn flush_seen_shares(&mut self) { + self.seen_shares.clear(); + } + + /// Returns the sequence number of the last share received. + pub fn get_last_share_sequence_number(&self) -> u32 { + self.last_share_sequence_number + } + + /// Returns the total number of shares accepted. + pub fn get_shares_accepted(&self) -> u32 { + self.shares_accepted + } + + /// Returns the cumulative work of all accepted shares. + pub fn get_share_work_sum(&self) -> u64 { + self.share_work_sum + } + + /// Checks if the given share hash has already been seen (duplicate detection). + pub fn is_share_seen(&self, share_hash: Hash) -> bool { + self.seen_shares.contains(&share_hash) + } + + /// Returns the highest difficulty among all accepted shares. + pub fn get_best_diff(&self) -> f64 { + self.best_diff + } + + /// Updates the best difficulty if the new difficulty is higher than the current best. + pub fn update_best_diff(&mut self, diff: f64) { + if diff > self.best_diff { + self.best_diff = diff; + } + } +} diff --git a/protocols/v2/roles-logic-sv2/src/channels/client/standard.rs b/protocols/v2/channels-sv2/src/client/standard.rs similarity index 83% rename from protocols/v2/roles-logic-sv2/src/channels/client/standard.rs rename to protocols/v2/channels-sv2/src/client/standard.rs index b23a561e87..a0d0294601 100644 --- a/protocols/v2/roles-logic-sv2/src/channels/client/standard.rs +++ b/protocols/v2/channels-sv2/src/client/standard.rs @@ -1,45 +1,47 @@ -//! Abstraction over the state of a Sv2 Standard Channel, as seen by a Mining Client - +//! Sv2 Standard Channel - Mining Client Abstraction. +//! +//! This module provides the [`StandardChannel`] struct, which models the state of a mining +//! client's Sv2 Standard Channel. It tracks channel-level job management, share accounting, +//! and chain tip state, enabling share validation and mining job lifecycle management. + +extern crate alloc; +use super::HashMap; use crate::{ - channels::{ - chain_tip::ChainTip, - client::{ - error::StandardChannelError, - share_accounting::{ShareAccounting, ShareValidationError, ShareValidationResult}, - }, + chain_tip::ChainTip, + client::{ + error::StandardChannelError, + share_accounting::{ShareAccounting, ShareValidationError, ShareValidationResult}, }, - utils::{bytes_to_hex, merkle_root_from_path, target_to_difficulty, u256_to_block_hash}, -}; -use binary_sv2::Sv2Option; -use mining_sv2::{ - NewExtendedMiningJob, NewMiningJob, SetNewPrevHash as SetNewPrevHashMp, SubmitSharesStandard, - Target, MAX_EXTRANONCE_LEN, + merkle_root::merkle_root_from_path, + target::{bytes_to_hex, target_to_difficulty, u256_to_block_hash}, }; -use std::{collections::HashMap, convert::TryInto}; -use stratum_common::bitcoin::{ +use alloc::{format, string::String, vec::Vec}; +use binary_sv2::{self, Sv2Option}; +use bitcoin::{ blockdata::block::{Header, Version}, hashes::sha256d::Hash, CompactTarget, Target as BitcoinTarget, }; +use mining_sv2::{ + NewExtendedMiningJob, NewMiningJob, SetNewPrevHash as SetNewPrevHashMp, SubmitSharesStandard, + Target, MAX_EXTRANONCE_LEN, +}; use tracing::debug; /// Mining Client abstraction over the state of a Sv2 Standard Channel. /// -/// It keeps track of: -/// - the channel's unique `channel_id` -/// - the channel's `user_identity` -/// - the channel's unique `extranonce_prefix` -/// - the channel's target -/// - the channel's nominal hashrate -/// - the channel's future jobs (indexed by `job_id`, to be activated upon receipt of a -/// `NewMiningJob` message) -/// - the channel's active job -/// - the channel's past jobs (which were active jobs under the current chain tip, indexed by -/// `job_id`) -/// - the channel's stale jobs (which were past and active jobs under the previous chain tip, -/// indexed by `job_id`) -/// - the channel's share accounting (as seen by the client) -/// - the channel's chain tip +/// Tracks: +/// - unique channel ID +/// - user identity string +/// - unique extranonce prefix +/// - channel target +/// - nominal hashrate in h/s +/// - future mining jobs (indexed by job_id, activated upon [`NewMiningJob`] receipt) +/// - active mining job +/// - past jobs (active jobs under current chain tip, indexed by job_id) +/// - stale jobs (jobs from previous chain tip, indexed by job_id) +/// - share accounting state +/// - chain tip state #[derive(Debug, Clone)] pub struct StandardChannel<'a> { channel_id: u32, @@ -56,6 +58,7 @@ pub struct StandardChannel<'a> { } impl<'a> StandardChannel<'a> { + /// Creates a new [`StandardChannel`] instance with provided channel parameters. pub fn new( channel_id: u32, user_identity: String, @@ -78,14 +81,31 @@ impl<'a> StandardChannel<'a> { } } + /// Returns the channel ID. pub fn get_channel_id(&self) -> u32 { self.channel_id } + /// Returns the user identity string associated with this channel. pub fn get_user_identity(&self) -> &String { &self.user_identity } + /// Returns the latest chain tip information, if any. + pub fn get_chain_tip(&self) -> Option<&ChainTip> { + self.chain_tip.as_ref() + } + + /// Sets the [`ChainTip`] + pub fn set_chain_tip(&mut self, chain_tip: ChainTip) { + self.chain_tip = Some(chain_tip); + } + + /// Sets the extranonce prefix for the channel. + /// + /// All new jobs will use the new extranonce prefix. Jobs created before + /// this call will continue using their previous prefix for share validation. + /// Returns an error if the prefix is too large. pub fn set_extranonce_prefix( &mut self, extranonce_prefix: Vec, @@ -95,46 +115,60 @@ impl<'a> StandardChannel<'a> { } self.extranonce_prefix = extranonce_prefix; - Ok(()) } + /// Returns the bytes representing the first part of the extranonce. pub fn get_extranonce_prefix(&self) -> &Vec { &self.extranonce_prefix } + /// Returns the current target for the channel. pub fn get_target(&self) -> &Target { &self.target } + /// Sets a new target for the channel. pub fn set_target(&mut self, target: Target) { self.target = target; } + /// Returns the nominal hashrate of the channel in h/s. pub fn get_nominal_hashrate(&self) -> f32 { self.nominal_hashrate } + /// Returns all future jobs for this channel. + /// + /// The list is cleared once a [`StandardChannel::on_set_new_prev_hash`] is processed. pub fn get_future_jobs(&self) -> &HashMap> { &self.future_jobs } + /// Returns the currently active job, if any. pub fn get_active_job(&self) -> Option<&NewMiningJob<'a>> { self.active_job.as_ref() } + /// Returns all past jobs for the channel (active jobs under current chain tip). pub fn get_past_jobs(&self) -> &HashMap> { &self.past_jobs } + /// Returns all stale jobs for the channel (jobs from previous chain tip). pub fn get_stale_jobs(&self) -> &HashMap> { &self.stale_jobs } - /// Called when the Group Channel receives a new extended job. + /// Returns the share accounting state for this channel. + pub fn get_share_accounting(&self) -> &ShareAccounting { + &self.share_accounting + } + + /// Handles a new group channel job by converting it into a standard job + /// and activating it in this channel's context. /// - /// Essentially converts the extended job into a standard job (with the current channel's - /// extranonce_prefix) and then calls `on_new_mining_job` to update the channel state. + /// The new job is constructed using the current extranonce prefix. pub fn on_new_group_channel_job(&mut self, new_extended_mining_job: NewExtendedMiningJob<'a>) { let merkle_root = merkle_root_from_path( new_extended_mining_job.coinbase_tx_prefix.inner_as_ref(), @@ -157,11 +191,14 @@ impl<'a> StandardChannel<'a> { self.on_new_mining_job(new_mining_job); } - /// Called when a `NewMiningJob` message is received from upstream. + /// Handles a newly received [`NewMiningJob`] message from upstream. + /// + /// - If `min_ntime` is present, the job is activated and replaces the current active job. + /// - If `min_ntime` is empty, the job is added to future jobs. + /// - If an active job exists, it is moved to past jobs on activation. pub fn on_new_mining_job(&mut self, new_mining_job: NewMiningJob<'a>) { match new_mining_job.min_ntime.clone().into_inner() { Some(_min_ntime) => { - println!(); if let Some(active_job) = self.active_job.as_ref() { self.past_jobs.insert(active_job.job_id, active_job.clone()); } @@ -174,17 +211,14 @@ impl<'a> StandardChannel<'a> { } } - /// Called when a `SetNewPrevHash` message is received from upstream. - /// - /// If the job_id addressed in the `SetNewPrevHash` is not a future job, - /// returns an error. - /// - /// If the job_id addressed in the `SetNewPrevHash` is a future job, - /// it is "activated" and set as the active job. + /// Handles an upstream [`SetNewPrevHash`](SetNewPrevHashMp) message. /// - /// All past jobs are marked as stale, so that shares are not propagated. - /// - /// The chain tip information is not kept in the channel state. + /// - Activates the matching future job as the new active job. + /// - Clears all future jobs. + /// - Marks all past jobs as stale (they are no longer valid for share propagation). + /// - Clears past jobs and the set of seen shares (to avoid memory growth and stale share + /// submissions). + /// - Updates chain tip information. Returns error if no matching future job found. pub fn on_set_new_prev_hash( &mut self, set_new_prev_hash: SetNewPrevHashMp<'a>, @@ -209,25 +243,18 @@ impl<'a> StandardChannel<'a> { // clear seen shares, as shares for past chain tip will be rejected as stale self.share_accounting.flush_seen_shares(); - let set_new_prev_hash_static = set_new_prev_hash.into_static(); - let new_chain_tip = ChainTip::new( - set_new_prev_hash_static.prev_hash, - set_new_prev_hash_static.nbits, - set_new_prev_hash_static.min_ntime, - ); - self.chain_tip = Some(new_chain_tip); + self.chain_tip = Some(set_new_prev_hash.into()); Ok(()) } - /// Validates a share, to be used before submission upstream. - /// - /// Updates the channel state with the result of the share validation. + /// Validates a share before submission upstream. /// - /// - Allows the mining client to avoid propagating stale, duplicate or low-diff shares. - /// - Allows the mining client to know whether a block was found on some share. - /// - Allows the mining client to keep a local version of the share accounting for comparison - /// with the acknowledgements coming from the upstream server. + /// - Checks if the share refers to an active or past job; rejects stale jobs. + /// - Verifies the share meets the channel target, is not a duplicate, and is not stale. + /// - Updates share accounting state based on validation result. + /// - Returns whether the share is valid or resulted in a block being found. + /// - Returns error describing why share is not valid. pub fn validate_share( &mut self, share: SubmitSharesStandard, @@ -338,7 +365,7 @@ impl<'a> StandardChannel<'a> { #[cfg(test)] mod tests { - use crate::channels::client::{ + use crate::client::{ share_accounting::{ShareValidationError, ShareValidationResult}, standard::StandardChannel, }; diff --git a/protocols/v2/channels-sv2/src/lib.rs b/protocols/v2/channels-sv2/src/lib.rs new file mode 100644 index 0000000000..9a84a58317 --- /dev/null +++ b/protocols/v2/channels-sv2/src/lib.rs @@ -0,0 +1,30 @@ +//! # Stratum V2 Channels +//! +//! `channels_sv2` provides primitives and abstractions for Stratum V2 (Sv2) Channels. +//! +//! This crate implements the core channel management functionality for both mining clients and +//! servers, including standard, extended, and group channels, and share accounting mechanisms. +//! +//! ## Features +//! +//! - Channel primitives for SV2 mining protocol +//! - Channel management for mining servers and clients +//! - Standard, extended, and group channel support +//! - Share accounting +//! - Job store abstractions +//! - [`client`] module is `no_std` compatible. To enable it build the crate with `no_std` feature. +#![cfg_attr(feature = "no_std", no_std)] + +#[cfg(not(feature = "no_std"))] +pub mod server; + +#[cfg(not(feature = "no_std"))] +pub mod outputs; + +pub mod bip141; +pub mod chain_tip; +pub mod client; +pub mod merkle_root; +pub mod target; +pub mod vardiff; +pub use vardiff::{classic::VardiffState, Vardiff}; diff --git a/protocols/v2/channels-sv2/src/merkle_root.rs b/protocols/v2/channels-sv2/src/merkle_root.rs new file mode 100644 index 0000000000..92feb4db29 --- /dev/null +++ b/protocols/v2/channels-sv2/src/merkle_root.rs @@ -0,0 +1,75 @@ +extern crate alloc; + +use alloc::vec::Vec; +use bitcoin::{ + consensus, + hashes::{sha256d::Hash as DHash, Hash}, + Transaction, +}; +use tracing::error; + +/// Computes the Merkle root from coinbase transaction components and a path of transaction hashes. +/// +/// Validates and deserializes a coinbase transaction before building the 32-byte Merkle root. +/// Returns [`None`] is the arguments are invalid. +/// +/// ## Components +/// * `coinbase_tx_prefix`: First part of the coinbase transaction (the part before the extranonce). +/// Should be converted from [`binary_sv2::B064K`]. +/// * `coinbase_tx_suffix`: Coinbase transaction suffix (the part after the extranonce). Should be +/// converted from [`binary_sv2::B064K`]. +/// * `extranonce`: Extra nonce space. Should be converted from [`binary_sv2::B032`] and padded with +/// zeros if not `32` bytes long. +/// * `path`: List of transaction hashes. Should be converted from [`binary_sv2::U256`]. +pub fn merkle_root_from_path>( + coinbase_tx_prefix: &[u8], + coinbase_tx_suffix: &[u8], + extranonce: &[u8], + path: &[T], +) -> Option> { + let mut coinbase = + Vec::with_capacity(coinbase_tx_prefix.len() + coinbase_tx_suffix.len() + extranonce.len()); + coinbase.extend_from_slice(coinbase_tx_prefix); + coinbase.extend_from_slice(extranonce); + coinbase.extend_from_slice(coinbase_tx_suffix); + let coinbase: Transaction = match consensus::deserialize(&coinbase[..]) { + Ok(trans) => trans, + Err(e) => { + error!("ERROR: {}", e); + return None; + } + }; + + let coinbase_id: [u8; 32] = *coinbase.compute_txid().as_ref(); + + Some(merkle_root_from_path_(coinbase_id, path).to_vec()) +} + +/// Computes the Merkle root from a validated coinbase transaction and a path of transaction +/// hashes. +/// +/// If the `path` is empty, the coinbase transaction hash (`coinbase_id`) is returned as the root. +/// +/// ## Components +/// * `coinbase_id`: Coinbase transaction hash. +/// * `path`: List of transaction hashes. Should be converted from [`binary_sv2::U256`]. +pub fn merkle_root_from_path_>(coinbase_id: [u8; 32], path: &[T]) -> [u8; 32] { + match path.len() { + 0 => coinbase_id, + _ => reduce_path(coinbase_id, path), + } +} + +// Computes the Merkle root by iteratively combining the coinbase transaction hash with each +// transaction hash in the `path`. +// +// Handles the core logic of combining hashes using the Bitcoin double-SHA256 hashing algorithm. +fn reduce_path>(coinbase_id: [u8; 32], path: &[T]) -> [u8; 32] { + let mut root = coinbase_id; + for node in path { + let to_hash = [&root[..], node.as_ref()].concat(); + let hash = DHash::hash(&to_hash); + root = *hash.as_ref(); + } + root +} diff --git a/protocols/v2/channels-sv2/src/outputs.rs b/protocols/v2/channels-sv2/src/outputs.rs new file mode 100644 index 0000000000..ffb9c214f7 --- /dev/null +++ b/protocols/v2/channels-sv2/src/outputs.rs @@ -0,0 +1,39 @@ +//! Utilities to deserialize outputs. +use bitcoin::{ + consensus::{deserialize, Decodable}, + transaction::TxOut, +}; +use std::io::Cursor; + +#[derive(Debug)] +pub struct OutputsDeserializationError; + +/// Deserializes a vector of serialized outputs into a vector of `TxOut`s. +/// +/// Only to be used for deserializing outputs from a `NewTemplate` message, as it asserts the +/// expected number of outputs. +/// +/// Not suitable for deserializing outputs from a `SetCustomMiningJob` message or +/// `AllocateMiningJobToken.Success`. +pub fn deserialize_template_outputs( + serialized_outputs: Vec, + coinbase_tx_outputs_count: u32, +) -> Result, OutputsDeserializationError> { + let mut cursor = Cursor::new(serialized_outputs); + + (0..coinbase_tx_outputs_count) + .map(|_| TxOut::consensus_decode(&mut cursor).map_err(|_| OutputsDeserializationError)) + .collect() +} + +/// Deserializes a vector of serialized outputs into a vector of TxOuts. +/// +/// Does not assert the expected number of outputs. +pub fn deserialize_outputs( + serialized_outputs: Vec, +) -> Result, OutputsDeserializationError> { + match deserialize(serialized_outputs.as_slice()) { + Ok(outputs) => Ok(outputs), + Err(_) => Err(OutputsDeserializationError), + } +} diff --git a/protocols/v2/roles-logic-sv2/src/channels/server/error.rs b/protocols/v2/channels-sv2/src/server/error.rs similarity index 85% rename from protocols/v2/roles-logic-sv2/src/channels/server/error.rs rename to protocols/v2/channels-sv2/src/server/error.rs index f56a99b3ca..ce38de0522 100644 --- a/protocols/v2/roles-logic-sv2/src/channels/server/error.rs +++ b/protocols/v2/channels-sv2/src/server/error.rs @@ -1,4 +1,6 @@ -use crate::channels::server::jobs::error::JobFactoryError; +//! # Channel Error Types + +use crate::server::jobs::error::JobFactoryError; #[derive(Debug)] pub enum ExtendedChannelError { @@ -27,4 +29,5 @@ pub enum StandardChannelError { NewExtranoncePrefixTooLarge, JobFactoryError(JobFactoryError), ChainTipNotSet, + FailedToConvertToStandardJob, } diff --git a/protocols/v2/roles-logic-sv2/src/channels/server/extended.rs b/protocols/v2/channels-sv2/src/server/extended.rs similarity index 79% rename from protocols/v2/roles-logic-sv2/src/channels/server/extended.rs rename to protocols/v2/channels-sv2/src/server/extended.rs index 136c2a6ddc..71d33e4ca1 100644 --- a/protocols/v2/roles-logic-sv2/src/channels/server/extended.rs +++ b/protocols/v2/channels-sv2/src/server/extended.rs @@ -1,27 +1,63 @@ -//! Mining Server abstraction over the state of a Sv2 Extended Channel +//! # SV2 Extended Channel - Mining Server Abstraction. +//! +//! This module defines the [`ExtendedChannel`] struct, which provides an abstraction of a SV2 +//! extended channel as maintained by a mining pool server. +//! +//! ## Responsibilities +//! +//! `ExtendedChannel` is responsible for managing all the state associated with an SV2 extended +//! channel, including: +//! +//! - **Channel Parameters**: Holds the unique `channel_id`, `user_identity`, `extranonce_prefix`, +//! and other parameters negotiated during channel opening. +//! - **Target Difficulty**: Manages the target difficulty (`target`) and maximum allowed target +//! (`requested_max_target`), based on client requests and nominal hashrate. +//! - **Job Lifecycle Management**: Stores jobs received from new templates or custom job messages, +//! including: +//! - Future jobs (indexed by `template_id`) +//! - Active job (currently being mined) +//! - Past and stale jobs (for share validation over time) +//! - **Share Validation and Accounting**: Validates shares submitted by the miner, updating +//! internal accounting and detecting duplicates or stale submissions. Determines if a share meets +//! the channel or network target and responds accordingly. +//! - **Chain Tip Management**: Tracks the latest known chain tip (previous hash, timestamp, and +//! target) for constructing headers and validating shares. +//! - **Version Rolling**: Honors server configuration on whether version rolling is permitted, +//! validating submitted BIP320 header versions accordingly. +//! +//! ## Usage +//! +//! This struct is intended for use on the **pool server side** or by SV2-compliant job declaration +//! clients. It encapsulates logic for responding to SV2 messages such as `NewTemplate`, +//! `SetNewPrevHash`, `SetCustomMiningJob`, and `SubmitSharesExtended`. +//! +//! ## Notes +//! +//! - Only one active job is allowed at a time. Jobs from a previous chain tip become stale when a +//! new chain tip is set. +//! - Share acknowledgment logic is tied to a configured batch size (e.g., every `N` valid shares). +//! - Extranonce validation supports dynamic updates of `extranonce_prefix` but enforces consistency +//! with previously agreed parameters. use crate::{ - channels::{ - chain_tip::ChainTip, - server::{ - error::ExtendedChannelError, - jobs::{extended::ExtendedJob, factory::JobFactory, JobOrigin}, - share_accounting::{ShareAccounting, ShareValidationError, ShareValidationResult}, - }, - }, - utils::{ - bytes_to_hex, hash_rate_to_target, merkle_root_from_path, target_to_difficulty, - u256_to_block_hash, + chain_tip::ChainTip, + merkle_root::merkle_root_from_path, + server::{ + error::ExtendedChannelError, + jobs::{extended::ExtendedJob, factory::JobFactory, job_store::JobStore, JobOrigin}, + share_accounting::{ShareAccounting, ShareValidationError, ShareValidationResult}, }, + target::{bytes_to_hex, hash_rate_to_target, target_to_difficulty, u256_to_block_hash}, }; -use mining_sv2::{SetCustomMiningJob, SubmitSharesExtended, Target, MAX_EXTRANONCE_LEN}; -use std::{collections::HashMap, convert::TryInto}; -use stratum_common::bitcoin::{ +use binary_sv2::{self}; +use bitcoin::{ blockdata::block::{Header, Version}, hashes::sha256d::Hash, transaction::TxOut, CompactTarget, Target as BitcoinTarget, }; +use mining_sv2::{SetCustomMiningJob, SubmitSharesExtended, Target, MAX_EXTRANONCE_LEN}; +use std::{collections::HashMap, convert::TryInto, marker::PhantomData}; use template_distribution_sv2::{NewTemplate, SetNewPrevHash as SetNewPrevHashTdp}; use tracing::debug; @@ -46,8 +82,11 @@ use tracing::debug; /// - the channel's share validation state /// - the channel's job factory /// - the channel's chain tip -#[derive(Clone, Debug)] -pub struct ExtendedChannel<'a> { +#[derive(Debug)] +pub struct ExtendedChannel<'a, J> +where + J: JobStore>, +{ channel_id: u32, user_identity: String, extranonce_prefix: Vec, @@ -55,24 +94,30 @@ pub struct ExtendedChannel<'a> { requested_max_target: Target, target: Target, // todo: try to use Target from rust-bitcoin nominal_hashrate: f32, - // maps template_id to job_id on future jobs - future_template_to_job_id: HashMap, - // future jobs are indexed with job_id (u32) - future_jobs: HashMap>, - active_job: Option>, - // past jobs are indexed with job_id (u32) - past_jobs: HashMap>, - // stale jobs are indexed with job_id (u32) - stale_jobs: HashMap>, + job_store: J, job_factory: JobFactory, share_accounting: ShareAccounting, expected_share_per_minute: f32, chain_tip: Option, + phantom: PhantomData<&'a ()>, } -impl<'a> ExtendedChannel<'a> { +impl<'a, J> ExtendedChannel<'a, J> +where + J: JobStore>, +{ + /// Constructor of `ExtendedChannel` for a Sv2 Pool Server. + /// Not meant for usage on a Sv2 Job Declaration Client. + /// + /// Initializes the extended channel state with the provided parameters, including channel + /// identifiers, difficulty targets, share accounting, and job management. + /// Returns an error if target/difficulty parameters are invalid or extranonce prefix + /// requirements are not met. + /// + /// For non-JD jobs, `pool_tag_string` is added to the coinbase scriptSig in between `/` + /// and `//` delimiters: `/pool_tag_string//` #[allow(clippy::too_many_arguments)] - pub fn new( + pub fn new_for_pool( channel_id: u32, user_identity: String, extranonce_prefix: Vec, @@ -82,6 +127,81 @@ impl<'a> ExtendedChannel<'a> { requested_min_rollable_extranonce_size: u16, share_batch_size: usize, expected_share_per_minute: f32, + job_store: J, + pool_tag_string: String, + ) -> Result { + Self::new( + channel_id, + user_identity, + extranonce_prefix, + max_target, + nominal_hashrate, + version_rolling_allowed, + requested_min_rollable_extranonce_size, + share_batch_size, + expected_share_per_minute, + job_store, + Some(pool_tag_string), + None, + ) + } + + /// Constructor of `ExtendedChannel` for a Sv2 Job Declaration Client. + /// Not meant for usage on a Sv2 Pool Server. + /// + /// Initializes the extended channel state with the provided parameters, including channel + /// identifiers, difficulty targets, share accounting, and job management. + /// Returns an error if target/difficulty parameters are invalid or extranonce prefix + /// requirements are not met. + /// + /// The `pool_tag_string` and `miner_tag_string` are added to the coinbase scriptSig in between + /// `/` delimiters: `/pool_tag_string/miner_tag_string/` + #[allow(clippy::too_many_arguments)] + pub fn new_for_job_declaration_client( + channel_id: u32, + user_identity: String, + extranonce_prefix: Vec, + max_target: Target, + nominal_hashrate: f32, + version_rolling_allowed: bool, + requested_min_rollable_extranonce_size: u16, + share_batch_size: usize, + expected_share_per_minute: f32, + job_store: J, + pool_tag_string: Option, + miner_tag_string: String, + ) -> Result { + Self::new( + channel_id, + user_identity, + extranonce_prefix, + max_target, + nominal_hashrate, + version_rolling_allowed, + requested_min_rollable_extranonce_size, + share_batch_size, + expected_share_per_minute, + job_store, + pool_tag_string, + Some(miner_tag_string), + ) + } + + // private constructor + #[allow(clippy::too_many_arguments)] + fn new( + channel_id: u32, + user_identity: String, + extranonce_prefix: Vec, + max_target: Target, + nominal_hashrate: f32, + version_rolling_allowed: bool, + requested_min_rollable_extranonce_size: u16, + share_batch_size: usize, + expected_share_per_minute: f32, + job_store: J, + pool_tag: Option, + miner_tag: Option, ) -> Result { let target_u256 = match hash_rate_to_target(nominal_hashrate.into(), expected_share_per_minute.into()) { @@ -111,45 +231,52 @@ impl<'a> ExtendedChannel<'a> { requested_max_target: max_target, target, nominal_hashrate, - future_template_to_job_id: HashMap::new(), - future_jobs: HashMap::new(), - active_job: None, - past_jobs: HashMap::new(), - stale_jobs: HashMap::new(), - job_factory: JobFactory::new(version_rolling_allowed), + job_store, + job_factory: JobFactory::new(version_rolling_allowed, pool_tag, miner_tag), share_accounting: ShareAccounting::new(share_batch_size), expected_share_per_minute, chain_tip: None, + phantom: PhantomData, }) } + /// Returns the unique channel ID for this channel. pub fn get_channel_id(&self) -> u32 { self.channel_id } + /// Returns the user identity string associated with this channel. pub fn get_user_identity(&self) -> &String { &self.user_identity } + /// Returns the extranonce prefix bytes for this channel. pub fn get_extranonce_prefix(&self) -> &Vec { &self.extranonce_prefix } + /// Returns the current chain tip, if set. pub fn get_chain_tip(&self) -> Option<&ChainTip> { self.chain_tip.as_ref() } + /// Returns the expected number of shares per minute configured for this channel. + pub fn get_shares_per_minute(&self) -> f32 { + self.expected_share_per_minute + } + /// Only for testing purposes, not meant to be used in real apps. #[cfg(test)] fn set_chain_tip(&mut self, chain_tip: ChainTip) { self.chain_tip = Some(chain_tip); } - /// Sets the extranonce prefix. + /// Updates the extranonce prefix for this channel. /// - /// Note: after this, all new jobs will be associated with the new extranonce prefix. - /// Jobs created before this call will remain associated with the previous extranonce prefix, - /// and share validation will be done accordingly. + /// After this call, all newly created jobs will reference the new prefix. + /// Jobs created before the update will continue to use the previous prefix, + /// and share validation will be performed accordingly. + /// Returns an error if the new prefix violates minimum rollable extranonce size. pub fn set_extranonce_prefix( &mut self, extranonce_prefix: Vec, @@ -170,29 +297,52 @@ impl<'a> ExtendedChannel<'a> { Ok(()) } + /// Returns the number of bytes available for the rollable portion of the extranonce. pub fn get_rollable_extranonce_size(&self) -> u16 { self.rollable_extranonce_size } + /// Returns the requested maximum target for this channel. pub fn get_requested_max_target(&self) -> &Target { &self.requested_max_target } + /// Returns the current target for this channel. pub fn get_target(&self) -> &Target { &self.target } + /// Updates the current target for this channel. + pub fn set_target(&mut self, target: Target) { + self.target = target; + } + + /// Returns the mapping of future template IDs to job IDs. pub fn get_future_template_to_job_id(&self) -> &HashMap { - &self.future_template_to_job_id + self.job_store.get_future_template_to_job_id() } + /// Returns the nominal hashrate for this channel. pub fn get_nominal_hashrate(&self) -> f32 { self.nominal_hashrate } - /// Updates the channel's nominal hashrate and target. + /// Updates the nominal hashrate for this channel. + pub fn set_nominal_hashrate(&mut self, hashrate: f32) { + self.nominal_hashrate = hashrate; + } + + /// Updates channel configuration with a new nominal hashrate. + /// + /// Adjusts target difficulty and internal state. Returns an error if + /// any input parameters are invalid or constraints are violated. /// - /// If requested_max_target is None, we use the cached value in the channel state. + /// This can be used in two scenarios: + /// - Client sent `UpdateChannel` message, which contains a `requested_max_target` parameter + /// that's also used as input. + /// - vardiff algorithm estimated a new nominal hashrate, in which case `requested_max_target` + /// is `None` and we use the value from the channel state (that was set either during channel + /// opening or some previous `UpdateChannel` message). pub fn update_channel( &mut self, new_nominal_hashrate: f32, @@ -247,19 +397,19 @@ impl<'a> ExtendedChannel<'a> { Ok(()) } - + /// Returns the currently active job, if any. pub fn get_active_job(&self) -> Option<&ExtendedJob<'a>> { - self.active_job.as_ref() + self.job_store.get_active_job() } - + /// Returns all future jobs for this channel. pub fn get_future_jobs(&self) -> &HashMap> { - &self.future_jobs + self.job_store.get_future_jobs() } - + /// Returns all past jobs for this channel. pub fn get_past_jobs(&self) -> &HashMap> { - &self.past_jobs + self.job_store.get_past_jobs() } - + /// Returns a reference to the share accounting state for this channel. pub fn get_share_accounting(&self) -> &ShareAccounting { &self.share_accounting } @@ -288,10 +438,7 @@ impl<'a> ExtendedChannel<'a> { coinbase_reward_outputs, ) .map_err(ExtendedChannelError::JobFactoryError)?; - let new_job_id = new_job.get_job_id(); - self.future_jobs.insert(new_job_id, new_job); - self.future_template_to_job_id - .insert(template.template_id, new_job_id); + self.job_store.add_future_job(template.template_id, new_job); } false => { match self.chain_tip.clone() { @@ -308,15 +455,7 @@ impl<'a> ExtendedChannel<'a> { coinbase_reward_outputs, ) .map_err(ExtendedChannelError::JobFactoryError)?; - // if there's already some active job, move it to the past jobs - // and set the new job as the active job - if let Some(active_job) = self.active_job.take() { - self.past_jobs.insert(active_job.get_job_id(), active_job); - self.active_job = Some(new_job); - } else { - // if there's no active job, simply set the new job as the active job - self.active_job = Some(new_job); - } + self.job_store.add_active_job(new_job); } } } @@ -328,67 +467,38 @@ impl<'a> ExtendedChannel<'a> { /// Updates the channel state with a new `SetNewPrevHash` message (Template Distribution /// Protocol variant). /// - /// If there are no future jobs, returns an error. - /// If there is some future job matching the `template_id`` that `SetNewPrevHash` points to, - /// this future job is "activated" and set as the active job. + /// If there are future jobs in the Job Store, it activates the future job matching the + /// `template_id` and sets it as the active job. /// - /// All past jobs are cleared. + /// If there are future jobs in the Job Store, but the template id is not found, returns an + /// error. /// - /// The chain tip information is not kept in the channel state. + /// All past jobs are cleared. pub fn on_set_new_prev_hash( &mut self, set_new_prev_hash: SetNewPrevHashTdp<'a>, ) -> Result<(), ExtendedChannelError> { - match self.future_jobs.is_empty() { + // extended channels dedicated to custom work don't need to keep track of future jobs + match self.job_store.get_future_jobs().is_empty() { true => { - return Err(ExtendedChannelError::TemplateIdNotFound); + self.job_store.mark_past_jobs_as_stale(); } false => { // the SetNewPrevHash message was addressed to a specific future template - let future_job_id = self - .future_template_to_job_id - .remove(&set_new_prev_hash.template_id) - .ok_or(ExtendedChannelError::TemplateIdNotFound)?; - - // move currently active job to past jobs (so it can be marked as stale) - let currently_active_job = self.active_job.take(); - if let Some(active_job) = currently_active_job { - self.past_jobs.insert(active_job.get_job_id(), active_job); + if !self.job_store.activate_future_job( + set_new_prev_hash.template_id, + set_new_prev_hash.header_timestamp, + ) { + return Err(ExtendedChannelError::TemplateIdNotFound); } - - // activate the future job - let mut activated_job = self - .future_jobs - .remove(&future_job_id) - .expect("future job must exist"); - - activated_job.activate(set_new_prev_hash.header_timestamp); - - self.active_job = Some(activated_job); - - self.future_jobs.clear(); - self.future_template_to_job_id.clear(); } } - // mark all past jobs as stale, so that shares can be rejected with the appropriate error - // code - self.stale_jobs = self.past_jobs.clone(); - - // clear past jobs, as we're no longer going to validate shares for them - self.past_jobs.clear(); - // clear seen shares, as shares for past chain tip will be rejected as stale self.share_accounting.flush_seen_shares(); // update the chain tip - let set_new_prev_hash_static = set_new_prev_hash.into_static(); - let new_chain_tip = ChainTip::new( - set_new_prev_hash_static.prev_hash, - set_new_prev_hash_static.n_bits, - set_new_prev_hash_static.header_timestamp, - ); - self.chain_tip = Some(new_chain_tip); + self.chain_tip = Some(set_new_prev_hash.into()); Ok(()) } @@ -398,6 +508,9 @@ impl<'a> ExtendedChannel<'a> { /// If there is an active job, it is moved to the past jobs. /// The new custom mining job is then set as the active job. /// + /// Assumes SetCustomMiningJob.{prev_hash, nbits, min_ntime} have already been validated. + /// Updates the channel's `ChainTip``. + /// /// Returns the job id of the new custom mining job. /// /// To be used by a Sv2 Pool Server upon receiving a `SetCustomMiningJob` message. @@ -407,16 +520,23 @@ impl<'a> ExtendedChannel<'a> { ) -> Result { let new_job = self .job_factory - .new_custom_job(set_custom_mining_job, self.extranonce_prefix.clone()) + .new_extended_job_from_custom_job( + set_custom_mining_job.clone(), + self.extranonce_prefix.clone(), + ) .map_err(ExtendedChannelError::JobFactoryError)?; let job_id = new_job.get_job_id(); - if let Some(active_job) = self.active_job.take() { - self.past_jobs.insert(active_job.get_job_id(), active_job); - } + self.job_store.add_active_job(new_job); - self.active_job = Some(new_job); + // update the chain tip + let set_custom_mining_job_static = set_custom_mining_job.into_static(); + let prev_hash = set_custom_mining_job_static.prev_hash; + let nbits = set_custom_mining_job_static.nbits; + let min_ntime = set_custom_mining_job_static.min_ntime; + let new_chain_tip = ChainTip::new(prev_hash, nbits, min_ntime); + self.chain_tip = Some(new_chain_tip); Ok(job_id) } @@ -432,15 +552,15 @@ impl<'a> ExtendedChannel<'a> { // check if job_id is active job let is_active_job = self - .active_job - .as_ref() + .job_store + .get_active_job() .is_some_and(|job| job.get_job_id() == job_id); // check if job_id is past job - let is_past_job = self.past_jobs.contains_key(&job_id); + let is_past_job = self.job_store.get_past_jobs().contains_key(&job_id); // check if job_id is stale job - let is_stale_job = self.stale_jobs.contains_key(&job_id); + let is_stale_job = self.job_store.get_stale_jobs().contains_key(&job_id); if is_stale_job { return Err(ShareValidationError::Stale); @@ -452,11 +572,19 @@ impl<'a> ExtendedChannel<'a> { } let job = if is_active_job { - self.active_job.as_ref().expect("active job must exist") + self.job_store + .get_active_job() + .expect("active job must exist") } else if is_past_job { - self.past_jobs.get(&job_id).expect("past job must exist") + self.job_store + .get_past_jobs() + .get(&job_id) + .expect("past job must exist") } else { - self.stale_jobs.get(&job_id).expect("stale job must exist") + self.job_store + .get_stale_jobs() + .get(&job_id) + .expect("stale job must exist") }; let extranonce_prefix = job.get_extranonce_prefix(); @@ -470,8 +598,8 @@ impl<'a> ExtendedChannel<'a> { // - job coinbase_tx_suffix // - job merkle_path let merkle_root: [u8; 32] = merkle_root_from_path( - job.get_coinbase_tx_prefix().inner_as_ref(), - job.get_coinbase_tx_suffix().inner_as_ref(), + &job.get_coinbase_tx_prefix_without_bip141(), + &job.get_coinbase_tx_suffix_without_bip141(), full_extranonce.as_ref(), &job.get_merkle_path().inner_as_ref(), ) @@ -539,9 +667,9 @@ impl<'a> ExtendedChannel<'a> { ); let mut coinbase = vec![]; - coinbase.extend(job.get_coinbase_tx_prefix().inner_as_ref()); - coinbase.extend(full_extranonce); - coinbase.extend(job.get_coinbase_tx_suffix().inner_as_ref()); + coinbase.extend(job.get_coinbase_tx_prefix_with_bip141()); + coinbase.extend(full_extranonce.clone()); + coinbase.extend(job.get_coinbase_tx_suffix_with_bip141()); match job.get_origin() { JobOrigin::NewTemplate(template) => { @@ -595,18 +723,19 @@ impl<'a> ExtendedChannel<'a> { #[cfg(test)] mod tests { - use crate::channels::{ + use crate::{ chain_tip::ChainTip, server::{ error::ExtendedChannelError, extended::ExtendedChannel, + jobs::job_store::DefaultJobStore, share_accounting::{ShareValidationError, ShareValidationResult}, }, }; use binary_sv2::Sv2Option; + use bitcoin::{transaction::TxOut, Amount, ScriptBuf}; use mining_sv2::{NewExtendedMiningJob, SubmitSharesExtended, Target, MAX_EXTRANONCE_LEN}; use std::convert::TryInto; - use stratum_common::bitcoin::{transaction::TxOut, Amount, ScriptBuf}; use template_distribution_sv2::{NewTemplate, SetNewPrevHash}; const SATS_AVAILABLE_IN_TEMPLATE: u64 = 5000000000; @@ -629,6 +758,7 @@ mod tests { let version_rolling_allowed = true; let rollable_extranonce_size = (MAX_EXTRANONCE_LEN - extranonce_prefix.len()) as u16; let share_batch_size = 100; + let job_store = DefaultJobStore::new(); let mut channel = ExtendedChannel::new( channel_id, @@ -640,6 +770,9 @@ mod tests { rollable_extranonce_size, share_batch_size, expected_share_per_minute, + job_store, + None, + None, ) .unwrap(); @@ -704,8 +837,8 @@ mod tests { version: 536870912, version_rolling_allowed: true, coinbase_tx_prefix: vec![ - 2, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 34, 82, 0, + 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 39, 82, 0, 3, 47, 47, 47, 32, ] .try_into() .unwrap(), @@ -714,8 +847,7 @@ mod tests { 194, 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194, 8, 252, 0, 0, 0, 0, 0, 0, 0, 0, 38, 106, 36, 170, 33, 169, 237, 226, 246, 28, 63, 113, 209, 222, 253, 63, 169, 153, 223, 163, 105, 83, 117, 92, 105, 6, 137, 121, 153, 98, 180, 139, - 235, 216, 54, 151, 78, 140, 249, 1, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 235, 216, 54, 151, 78, 140, 249, 0, 0, 0, 0, ] .try_into() .unwrap(), @@ -777,6 +909,7 @@ mod tests { let version_rolling_allowed = true; let rollable_extranonce_size = (MAX_EXTRANONCE_LEN - extranonce_prefix.len()) as u16; let share_batch_size = 100; + let job_store = DefaultJobStore::new(); let mut channel = ExtendedChannel::new( channel_id, @@ -788,6 +921,9 @@ mod tests { rollable_extranonce_size, share_batch_size, expected_share_per_minute, + job_store, + None, + None, ) .unwrap(); @@ -854,8 +990,8 @@ mod tests { version: 536870912, version_rolling_allowed: true, coinbase_tx_prefix: vec![ - 2, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 34, 82, 0, + 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 39, 82, 0, 3, 47, 47, 47, 32, ] .try_into() .unwrap(), @@ -864,8 +1000,7 @@ mod tests { 194, 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194, 8, 252, 0, 0, 0, 0, 0, 0, 0, 0, 38, 106, 36, 170, 33, 169, 237, 226, 246, 28, 63, 113, 209, 222, 253, 63, 169, 153, 223, 163, 105, 83, 117, 92, 105, 6, 137, 121, 153, 98, 180, 139, - 235, 216, 54, 151, 78, 140, 249, 1, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 235, 216, 54, 151, 78, 140, 249, 0, 0, 0, 0, ] .try_into() .unwrap(), @@ -875,14 +1010,6 @@ mod tests { assert_eq!(active_job.get_job_message(), &expected_job); } - #[test] - fn test_custom_job_creation_flow() { - // todo: assert that a SetCustomMiningJob leads to - // the correct NewExtendedMiningJob message - // we should wait until the following spec cleanup is finished - // https://github.com/stratum-mining/sv2-spec/issues/133 - } - #[test] fn test_coinbase_reward_outputs_sum_above_template_value() { // note: @@ -902,6 +1029,7 @@ mod tests { let version_rolling_allowed = true; let rollable_extranonce_size = (MAX_EXTRANONCE_LEN - extranonce_prefix.len()) as u16; let share_batch_size = 100; + let job_store = DefaultJobStore::new(); let mut channel = ExtendedChannel::new( channel_id, @@ -913,6 +1041,9 @@ mod tests { rollable_extranonce_size, share_batch_size, expected_share_per_minute, + job_store, + None, + None, ) .unwrap(); @@ -977,6 +1108,7 @@ mod tests { let version_rolling_allowed = true; let rollable_extranonce_size = (MAX_EXTRANONCE_LEN - extranonce_prefix.len()) as u16; let share_batch_size = 100; + let job_store = DefaultJobStore::new(); let mut channel = ExtendedChannel::new( channel_id, @@ -988,6 +1120,9 @@ mod tests { rollable_extranonce_size, share_batch_size, expected_share_per_minute, + job_store, + None, + None, ) .unwrap(); @@ -1045,14 +1180,14 @@ mod tests { .on_new_template(template.clone(), coinbase_reward_outputs) .unwrap(); - // this share has hash 00009a270ad03f1256312c7f196ab1a66bf8951f282fc75d9c81393cbb6427a8 + // this share has hash 564e724a9eb5716f7eec638e5aeed595f45643bb57913c9445bafdf28d8be022 // which satisfies network target // 7fffff0000000000000000000000000000000000000000000000000000000000 let share_valid_block = SubmitSharesExtended { channel_id, sequence_number: 0, job_id: 1, - nonce: 741057, + nonce: 0, ntime: 1745596971, version: 536870912, extranonce: vec![1, 0, 0, 0, 0].try_into().unwrap(), @@ -1083,6 +1218,7 @@ mod tests { let version_rolling_allowed = true; let rollable_extranonce_size = (MAX_EXTRANONCE_LEN - extranonce_prefix.len()) as u16; let share_batch_size = 100; + let job_store = DefaultJobStore::new(); let mut channel = ExtendedChannel::new( channel_id, @@ -1094,6 +1230,9 @@ mod tests { rollable_extranonce_size, share_batch_size, expected_share_per_minute, + job_store, + None, + None, ) .unwrap(); @@ -1151,7 +1290,7 @@ mod tests { .on_new_template(template.clone(), coinbase_reward_outputs) .unwrap(); - // this share has hash 6f33ea329093baa13e37d11b3afa91960f8d84f0ec064c1376522548c0852d79 + // this share has hash efc366b8401ff88cd581644fd935f42dd66348a08dd1ccf2f2f0dcbbbf989300 // which does not meet the channel target // 000aebbc990fff5144366f000aebbc990fff5144366f000aebbc990fff514435 let share_low_diff = SubmitSharesExtended { @@ -1192,6 +1331,7 @@ mod tests { let version_rolling_allowed = true; let rollable_extranonce_size = (MAX_EXTRANONCE_LEN - extranonce_prefix.len()) as u16; let share_batch_size = 100; + let job_store = DefaultJobStore::new(); let mut channel = ExtendedChannel::new( channel_id, @@ -1203,6 +1343,9 @@ mod tests { rollable_extranonce_size, share_batch_size, expected_share_per_minute, + job_store, + None, + None, ) .unwrap(); @@ -1261,7 +1404,7 @@ mod tests { .on_new_template(template.clone(), coinbase_reward_outputs) .unwrap(); - // this share has hash 0001099d7c957a0502952177aada0254921f04306a174543389263d1dd487cce + // this share has hash 00003126092fcbc15f05fbdf7e38dd468249e4e473fb5286caba164d8206f7f4 // which does meet the channel target // 0001179d9861a761ffdadd11c307c4fc04eea3a418f7d687584e4434af158205 // but does not meet network target @@ -1270,7 +1413,7 @@ mod tests { channel_id, sequence_number: 1, job_id: 1, - nonce: 159386, + nonce: 109053, ntime: 1745611105, version: 536870912, extranonce: vec![1, 0, 0, 0, 0].try_into().unwrap(), @@ -1285,7 +1428,7 @@ mod tests { channel_id, sequence_number: 2, job_id: 1, - nonce: 159386, + nonce: 109053, ntime: 1745611105, version: 536870912, extranonce: vec![1, 0, 0, 0, 0].try_into().unwrap(), @@ -1311,6 +1454,7 @@ mod tests { let version_rolling_allowed = true; let rollable_extranonce_size = (MAX_EXTRANONCE_LEN - extranonce_prefix.len()) as u16; let share_batch_size = 100; + let job_store = DefaultJobStore::new(); // this is the most permissive possible max_target let max_target: Target = [0xff; 32].into(); @@ -1326,6 +1470,9 @@ mod tests { rollable_extranonce_size, share_batch_size, expected_share_per_minute, + job_store, + None, + None, ) .unwrap(); @@ -1405,6 +1552,7 @@ mod tests { let version_rolling_allowed = true; let rollable_extranonce_size = (MAX_EXTRANONCE_LEN - extranonce_prefix.len()) as u16; let share_batch_size = 100; + let job_store = DefaultJobStore::new(); let mut channel = ExtendedChannel::new( channel_id, @@ -1416,6 +1564,9 @@ mod tests { rollable_extranonce_size, share_batch_size, expected_share_per_minute, + job_store, + None, + None, ) .unwrap(); diff --git a/protocols/v2/roles-logic-sv2/src/channels/server/group.rs b/protocols/v2/channels-sv2/src/server/group.rs similarity index 70% rename from protocols/v2/roles-logic-sv2/src/channels/server/group.rs rename to protocols/v2/channels-sv2/src/server/group.rs index 71569c0633..fc30d1a365 100644 --- a/protocols/v2/roles-logic-sv2/src/channels/server/group.rs +++ b/protocols/v2/channels-sv2/src/server/group.rs @@ -1,16 +1,50 @@ -//! Abstraction over the state of a Sv2 Group Channel, as seen by a Mining Server -use crate::channels::{ +//! Sv2 Group Channel - Mining Server Abstraction. +//! +//! This module defines the [`GroupChannel`] struct, which provides an abstraction of a Stratum V2 +//! (SV2) group channel as maintained by a mining server. +//! +//! A group channel represents a logical grouping of standard channels, allowing multiple mining +//! entities to share jobs. It manages job distribution and activation for all +//! associated standard channels, but delegates share validation and accounting to those standard +//! channels. +//! +//! ## Responsibilities +//! +//! `GroupChannel` is responsible for managing the state associated with an SV2 group channel, +//! including: +//! +//! - **Group Channel ID**: Holds the unique `group_channel_id`. +//! - **Standard Channel Management**: Tracks the set of associated standard channel IDs, allowing +//! for dynamic addition and removal. +//! - **Job Factory and Store**: Manages creation and storage of jobs (future and active) using the +//! job factory and job store abstractions. +//! - **Job Lifecycle Management**: Stores jobs received from new templates, including: +//! - Future jobs (indexed by `template_id`) +//! - Active job (currently being mined) +//! - **Chain Tip Management**: Tracks the latest known chain tip (block height, previous hash, +//! timestamp, and target) for constructing headers and activating jobs. +//! +//! ## Notes +//! +//! - Share validation and accounting is handled at the standard channel level, not in the group +//! channel. +//! - Past and stale jobs are not tracked in this abstraction. +//! - Extranonce prefix management is deferred to standard channels; group jobs use an empty prefix. + +use crate::{ chain_tip::ChainTip, server::{ error::GroupChannelError, - jobs::{extended::ExtendedJob, factory::JobFactory}, + jobs::{extended::ExtendedJob, factory::JobFactory, job_store::JobStore}, }, }; -use stratum_common::bitcoin::transaction::TxOut; +use bitcoin::transaction::TxOut; +use std::{ + collections::{HashMap, HashSet}, + marker::PhantomData, +}; use template_distribution_sv2::{NewTemplate, SetNewPrevHash as SetNewPrevHashTdp}; -use std::collections::{HashMap, HashSet}; - /// Abstraction of a Group Channel. /// /// It keeps track of: @@ -26,48 +60,97 @@ use std::collections::{HashMap, HashSet}; /// - the group channel's past jobs /// - the group channel's stale jobs /// - the group channel's share validation state -#[derive(Debug, Clone)] -pub struct GroupChannel<'a> { +#[derive(Debug)] +pub struct GroupChannel<'a, J> +where + J: JobStore>, +{ group_channel_id: u32, standard_channel_ids: HashSet, job_factory: JobFactory, - // maps template_id to job_id on future jobs - future_template_to_job_id: HashMap, - // future jobs are indexed with job_id (u32) - future_jobs: HashMap>, - active_job: Option>, + job_store: J, chain_tip: Option, + phantom: PhantomData<&'a ()>, } -impl<'a> GroupChannel<'a> { - pub fn new(group_channel_id: u32) -> Self { +impl<'a, J> GroupChannel<'a, J> +where + J: JobStore>, +{ + /// Constructor of `GroupChannel` for a Sv2 Pool Server. + /// Not meant for usage on a Sv2 Job Declaration Client. + /// + /// Initializes the group channel state with the provided group channel ID and job store. + /// The job factory is initialized with version rolling enabled. + /// + /// For non-JD jobs, `pool_tag_string` is added to the coinbase scriptSig in between `/` + /// and `//` delimiters: `/pool_tag_string//` + pub fn new_for_pool(group_channel_id: u32, job_store: J, pool_tag_string: String) -> Self { + Self::new(group_channel_id, job_store, Some(pool_tag_string), None) + } + + /// Constructor of `GroupChannel` for a Sv2 Job Declaration Client. + /// Not meant for usage on a Sv2 Pool Server. + /// + /// Initializes the extended channel state with the provided parameters, including channel + /// identifiers, difficulty targets, share accounting, and job management. + /// Returns an error if target/difficulty parameters are invalid or extranonce prefix + /// requirements are not met. + /// + /// The `pool_tag_string` and `miner_tag_string` are added to the coinbase scriptSig in between + /// `/` delimiters: `/pool_tag_string/miner_tag_string/` + pub fn new_for_job_declaration_client( + group_channel_id: u32, + job_store: J, + pool_tag_string: Option, + miner_tag_string: String, + ) -> Self { + Self::new( + group_channel_id, + job_store, + pool_tag_string, + Some(miner_tag_string), + ) + } + + // private constructor + fn new( + group_channel_id: u32, + job_store: J, + pool_tag: Option, + miner_tag: Option, + ) -> Self { Self { group_channel_id, standard_channel_ids: HashSet::new(), - job_factory: JobFactory::new(true), - future_template_to_job_id: HashMap::new(), - future_jobs: HashMap::new(), - active_job: None, + job_factory: JobFactory::new(true, pool_tag, miner_tag), + job_store, chain_tip: None, + phantom: PhantomData, } } + /// Adds a standard channel ID to this group channel. pub fn add_standard_channel_id(&mut self, standard_channel_id: u32) { self.standard_channel_ids.insert(standard_channel_id); } + /// Removes a standard channel ID from this group channel. pub fn remove_standard_channel_id(&mut self, standard_channel_id: u32) { self.standard_channel_ids.remove(&standard_channel_id); } + /// Returns the unique group channel ID for this group channel. pub fn get_group_channel_id(&self) -> u32 { self.group_channel_id } + /// Returns a reference to the set of standard channel IDs associated with this group channel. pub fn get_standard_channel_ids(&self) -> &HashSet { &self.standard_channel_ids } + /// Returns the current chain tip, if set. pub fn get_chain_tip(&self) -> Option<&ChainTip> { self.chain_tip.as_ref() } @@ -78,22 +161,26 @@ impl<'a> GroupChannel<'a> { self.chain_tip = Some(chain_tip); } + /// Returns the currently active job, if any. pub fn get_active_job(&self) -> Option<&ExtendedJob<'a>> { - self.active_job.as_ref() + self.job_store.get_active_job() } + /// Returns the mapping of future template IDs to job IDs. pub fn get_future_template_to_job_id(&self) -> &HashMap { - &self.future_template_to_job_id + self.job_store.get_future_template_to_job_id() } + /// Returns all future jobs for this group channel. pub fn get_future_jobs(&self) -> &HashMap> { - &self.future_jobs + self.job_store.get_future_jobs() } /// Updates the group channel state with a new template. /// /// If the template is a future template, the chain tip is not used. /// If the template is not a future template, the chain tip must be set. + /// Returns an error if a non-future job cannot be created due to missing chain tip. pub fn on_new_template( &mut self, template: NewTemplate<'a>, @@ -113,10 +200,7 @@ impl<'a> GroupChannel<'a> { coinbase_reward_outputs, ) .map_err(GroupChannelError::JobFactoryError)?; - let new_job_id = new_job.get_job_id(); - self.future_jobs.insert(new_job_id, new_job); - self.future_template_to_job_id - .insert(template.template_id, new_job_id); + self.job_store.add_future_job(template.template_id, new_job); } false => { match self.chain_tip.clone() { @@ -135,7 +219,7 @@ impl<'a> GroupChannel<'a> { coinbase_reward_outputs, ) .map_err(GroupChannelError::JobFactoryError)?; - self.active_job = Some(new_job); + self.job_store.add_active_job(new_job); } } } @@ -143,51 +227,32 @@ impl<'a> GroupChannel<'a> { Ok(()) } - /// Updates the channel state with a new `SetNewPrevHash` message (Template Distribution - /// Protocol variant). + /// Updates the group channel state with a new [`SetNewPrevHash`](SetNewPrevHashTdp) message + /// (Template Distribution Protocol variant). /// - /// If there is some future job matching the `template_id`` that `SetNewPrevHash` points to, + /// If there is a future job matching the `template_id` specified in `SetNewPrevHash`, /// this future job is "activated" and set as the active job. /// - /// The chain tip information is not kept in the channel state. + /// Updates the chain tip for the group channel. + /// Returns an error if no matching future job is found. pub fn on_set_new_prev_hash( &mut self, set_new_prev_hash: SetNewPrevHashTdp<'a>, ) -> Result<(), GroupChannelError> { - match self.future_jobs.is_empty() { + match self.job_store.get_future_jobs().is_empty() { true => { return Err(GroupChannelError::TemplateIdNotFound); } false => { - // the SetNewPrevHash message was addressed to a specific future template - let future_job_id = self - .future_template_to_job_id - .remove(&set_new_prev_hash.template_id) - .ok_or(GroupChannelError::TemplateIdNotFound)?; - - // activate the future job - let mut activated_job = self - .future_jobs - .remove(&future_job_id) - .expect("future job must exist"); - - activated_job.activate(set_new_prev_hash.header_timestamp); - - self.active_job = Some(activated_job); - - self.future_jobs.clear(); - self.future_template_to_job_id.clear(); + self.job_store.activate_future_job( + set_new_prev_hash.template_id, + set_new_prev_hash.header_timestamp, + ); } } // update the chain tip - let set_new_prev_hash_static = set_new_prev_hash.into_static(); - let new_chain_tip = ChainTip::new( - set_new_prev_hash_static.prev_hash, - set_new_prev_hash_static.n_bits, - set_new_prev_hash_static.header_timestamp, - ); - self.chain_tip = Some(new_chain_tip); + self.chain_tip = Some(set_new_prev_hash.into()); Ok(()) } @@ -195,11 +260,14 @@ impl<'a> GroupChannel<'a> { #[cfg(test)] mod tests { - use crate::channels::{chain_tip::ChainTip, server::group::GroupChannel}; + use crate::{ + chain_tip::ChainTip, + server::{group::GroupChannel, jobs::job_store::DefaultJobStore}, + }; use binary_sv2::Sv2Option; + use bitcoin::{transaction::TxOut, Amount, ScriptBuf}; use mining_sv2::NewExtendedMiningJob; use std::convert::TryInto; - use stratum_common::bitcoin::{transaction::TxOut, Amount, ScriptBuf}; use template_distribution_sv2::{NewTemplate, SetNewPrevHash}; const SATS_AVAILABLE_IN_TEMPLATE: u64 = 5000000000; @@ -210,8 +278,8 @@ mod tests { // the messages on this test were collected from a sane message flow // we use them as test vectors to assert correct behavior of job creation let group_channel_id = 1; - - let mut group_channel = GroupChannel::new(group_channel_id); + let job_store = DefaultJobStore::new(); + let mut group_channel = GroupChannel::new(group_channel_id, job_store, None, None); let template = NewTemplate { template_id: 1, @@ -274,8 +342,8 @@ mod tests { version: 536870912, version_rolling_allowed: true, coinbase_tx_prefix: vec![ - 2, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 34, 82, 0, + 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 39, 82, 0, 3, 47, 47, 47, 32, ] .try_into() .unwrap(), @@ -284,8 +352,7 @@ mod tests { 194, 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194, 8, 252, 0, 0, 0, 0, 0, 0, 0, 0, 38, 106, 36, 170, 33, 169, 237, 226, 246, 28, 63, 113, 209, 222, 253, 63, 169, 153, 223, 163, 105, 83, 117, 92, 105, 6, 137, 121, 153, 98, 180, 139, - 235, 216, 54, 151, 78, 140, 249, 1, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 235, 216, 54, 151, 78, 140, 249, 0, 0, 0, 0, ] .try_into() .unwrap(), @@ -338,7 +405,8 @@ mod tests { // we use them as test vectors to assert correct behavior of job creation let group_channel_id = 1; - let mut group_channel = GroupChannel::new(group_channel_id); + let job_store = DefaultJobStore::new(); + let mut group_channel = GroupChannel::new(group_channel_id, job_store, None, None); let ntime = 1746839905; let prev_hash = [ @@ -400,8 +468,8 @@ mod tests { version: 536870912, version_rolling_allowed: true, coinbase_tx_prefix: vec![ - 2, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 34, 82, 0, + 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 39, 82, 0, 3, 47, 47, 47, 32, ] .try_into() .unwrap(), @@ -410,8 +478,7 @@ mod tests { 194, 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194, 8, 252, 0, 0, 0, 0, 0, 0, 0, 0, 38, 106, 36, 170, 33, 169, 237, 226, 246, 28, 63, 113, 209, 222, 253, 63, 169, 153, 223, 163, 105, 83, 117, 92, 105, 6, 137, 121, 153, 98, 180, 139, - 235, 216, 54, 151, 78, 140, 249, 1, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 235, 216, 54, 151, 78, 140, 249, 0, 0, 0, 0, ] .try_into() .unwrap(), @@ -428,7 +495,8 @@ mod tests { // we use them as test vectors to assert correct behavior of job creation let group_channel_id = 1; - let mut group_channel = GroupChannel::new(group_channel_id); + let job_store = DefaultJobStore::new(); + let mut group_channel = GroupChannel::new(group_channel_id, job_store, None, None); let template = NewTemplate { template_id: 1, diff --git a/protocols/v2/channels-sv2/src/server/jobs/error.rs b/protocols/v2/channels-sv2/src/server/jobs/error.rs new file mode 100644 index 0000000000..945e2a13c1 --- /dev/null +++ b/protocols/v2/channels-sv2/src/server/jobs/error.rs @@ -0,0 +1,32 @@ +//! # Job Error Types + +#[derive(Debug)] +pub enum ExtendedJobError { + FailedToDeserializeCoinbase, + FailedToDeserializeCoinbaseOutputs, + CoinbaseInputCountMismatch, + FailedToSerializeCoinbaseOutputs, + FailedToSerializeCoinbasePrefix, + FailedToConvertToStandardJob, + FailedToCalculateMerkleRoot, + FutureJobNotAllowed, + InvalidMinNTime, +} + +pub enum StandardJobError { + FailedToDeserializeCoinbaseOutputs, +} + +#[derive(Debug)] +pub enum JobFactoryError { + FailedToStripBip141, + FailedToSerializeCoinbaseOutputs, + FailedToSerializeCoinbasePrefix, + InvalidTemplate(String), + DeserializeCoinbaseOutputsError, + CoinbaseTxPrefixError, + CoinbaseTxSuffixError, + CoinbaseOutputsSumOverflow, + InvalidCoinbaseOutputsSum, + ChainTipRequired, +} diff --git a/protocols/v2/channels-sv2/src/server/jobs/extended.rs b/protocols/v2/channels-sv2/src/server/jobs/extended.rs new file mode 100644 index 0000000000..dc26d7d07d --- /dev/null +++ b/protocols/v2/channels-sv2/src/server/jobs/extended.rs @@ -0,0 +1,208 @@ +use super::Job; +use crate::{ + merkle_root::merkle_root_from_path, + outputs::deserialize_template_outputs, + server::jobs::{error::ExtendedJobError, standard::StandardJob, JobOrigin}, +}; +use binary_sv2::{Seq0255, Sv2Option, U256}; +use bitcoin::transaction::TxOut; +use mining_sv2::{NewExtendedMiningJob, NewMiningJob, SetCustomMiningJob}; +use std::convert::TryInto; +use template_distribution_sv2::NewTemplate; + +/// Abstraction of an extended mining job with: +/// - the `NewTemplate` OR `SetCustomMiningJob` message that originated it +/// - the extranonce prefix associated with the channel at the time of job creation +/// - all coinbase outputs (spendable + unspendable) associated with the job +/// - the `NewExtendedMiningJob` message to be sent across the wire +/// +/// Please note that `coinbase_tx_prefix` and `coinbase_tx_suffix` are stored in memory with bip141 +/// data (marker, flag and witness). That makes it easy to reconstruct the segwit coinbase while +/// trying to propagate a block. +/// +/// However, the `coinbase_tx_prefix` and `coinbase_tx_suffix` contained in the +/// `NewExtendedMiningJob` message to be sent across the wire DO NOT contain bip141 data. +/// That makes it easy to calculate the coinbase `txid` (instead of `wtxid`) for merkle root +/// calculation. +#[derive(Debug, Clone)] +pub struct ExtendedJob<'a> { + origin: JobOrigin<'a>, + extranonce_prefix: Vec, + coinbase_outputs: Vec, + coinbase_tx_prefix_with_bip141: Vec, + coinbase_tx_suffix_with_bip141: Vec, + job_message: NewExtendedMiningJob<'a>, +} + +impl Job for ExtendedJob<'_> { + fn get_job_id(&self) -> u32 { + self.job_message.job_id + } + + fn activate(&mut self, min_ntime: u32) { + self.activate(min_ntime); + } +} + +impl<'a> ExtendedJob<'a> { + /// Creates a new job from a template. + /// + /// `additional_coinbase_outputs` are added to the coinbase outputs coming from the template. + pub fn from_template( + template: NewTemplate<'a>, + extranonce_prefix: Vec, + additional_coinbase_outputs: Vec, + coinbase_tx_prefix: Vec, + coinbase_tx_suffix: Vec, + job_message: NewExtendedMiningJob<'a>, + ) -> Result { + let template_coinbase_outputs = deserialize_template_outputs( + template.coinbase_tx_outputs.to_vec(), + template.coinbase_tx_outputs_count, + ) + .map_err(|_| ExtendedJobError::FailedToDeserializeCoinbaseOutputs)?; + + let mut coinbase_outputs = vec![]; + coinbase_outputs.extend(additional_coinbase_outputs); + coinbase_outputs.extend(template_coinbase_outputs); + + Ok(Self { + origin: JobOrigin::NewTemplate(template), + extranonce_prefix, + coinbase_outputs, + coinbase_tx_prefix_with_bip141: coinbase_tx_prefix, + coinbase_tx_suffix_with_bip141: coinbase_tx_suffix, + job_message, + }) + } + /// Creates a new extended job from a custom mining job message. + /// + /// Used for jobs originating from [`SetCustomMiningJob`] messages. + pub fn from_custom_job( + custom_job: SetCustomMiningJob<'a>, + extranonce_prefix: Vec, + coinbase_outputs: Vec, + coinbase_tx_prefix: Vec, + coinbase_tx_suffix: Vec, + job_message: NewExtendedMiningJob<'a>, + ) -> Self { + Self { + origin: JobOrigin::SetCustomMiningJob(custom_job), + extranonce_prefix, + coinbase_outputs, + coinbase_tx_prefix_with_bip141: coinbase_tx_prefix, + coinbase_tx_suffix_with_bip141: coinbase_tx_suffix, + job_message, + } + } + + /// Converts the `ExtendedJob` into a `StandardJob`. + /// + /// Only possible if the job was created from a `NewTemplate`. + /// Jobs created from `SetCustomMiningJob` cannot be converted + pub fn into_standard_job( + self, + channel_id: u32, + extranonce_prefix: Vec, + ) -> Result, ExtendedJobError> { + // here we can only convert extended jobs that were created from a template + let template = match self.get_origin() { + JobOrigin::NewTemplate(template) => template, + JobOrigin::SetCustomMiningJob(_) => { + return Err(ExtendedJobError::FailedToConvertToStandardJob); + } + }; + + let merkle_root = merkle_root_from_path( + &self.get_coinbase_tx_prefix_without_bip141(), + &self.get_coinbase_tx_suffix_without_bip141(), + &extranonce_prefix, + &self.get_merkle_path().inner_as_ref(), + ) + .ok_or(ExtendedJobError::FailedToCalculateMerkleRoot)? + .try_into() + .map_err(|_| ExtendedJobError::FailedToCalculateMerkleRoot)?; + + let standard_job_message = NewMiningJob { + channel_id, + job_id: self.get_job_id(), + merkle_root, + version: self.get_version(), + min_ntime: self.get_min_ntime(), + }; + + let standard_job = StandardJob::from_template( + template.clone(), + extranonce_prefix, + self.get_coinbase_outputs().clone(), + standard_job_message, + ) + .map_err(|_| ExtendedJobError::FailedToConvertToStandardJob)?; + + Ok(standard_job) + } + + /// Returns the job ID for this job. + pub fn get_job_id(&self) -> u32 { + self.job_message.job_id + } + + /// Returns the origin message for this job (template or custom job). + pub fn get_origin(&self) -> &JobOrigin<'a> { + &self.origin + } + + /// Returns the coinbase transaction without for this job without BIP141 data. + pub fn get_coinbase_tx_prefix_without_bip141(&self) -> Vec { + self.job_message.coinbase_tx_prefix.inner_as_ref().to_vec() + } + + /// Returns the coinbase transaction suffix for this job without BIP141 data. + pub fn get_coinbase_tx_suffix_without_bip141(&self) -> Vec { + self.job_message.coinbase_tx_suffix.inner_as_ref().to_vec() + } + + /// Returns the extranonce prefix used for this job. + pub fn get_extranonce_prefix(&self) -> &Vec { + &self.extranonce_prefix + } + /// Returns all coinbase outputs for this job. + pub fn get_coinbase_outputs(&self) -> &Vec { + &self.coinbase_outputs + } + /// Returns the [`NewExtendedMiningJob`] message for this job. + pub fn get_job_message(&self) -> &NewExtendedMiningJob<'a> { + &self.job_message + } + /// Returns the merkle path for this job. + pub fn get_merkle_path(&self) -> &Seq0255<'a, U256<'a>> { + &self.job_message.merkle_path + } + /// Returns the minimum ntime for this job (if set). + pub fn get_min_ntime(&self) -> Sv2Option<'a, u32> { + self.job_message.min_ntime.clone() + } + /// Returns the block version for this job. + pub fn get_version(&self) -> u32 { + self.job_message.version + } + /// Returns true if version rolling is allowed for this job. + pub fn version_rolling_allowed(&self) -> bool { + self.job_message.version_rolling_allowed + } + + pub fn get_coinbase_tx_prefix_with_bip141(&self) -> Vec { + self.coinbase_tx_prefix_with_bip141.clone() + } + + pub fn get_coinbase_tx_suffix_with_bip141(&self) -> Vec { + self.coinbase_tx_suffix_with_bip141.clone() + } + + /// Activates the job, setting the `min_ntime` field of the `NewExtendedMiningJob` message. + /// + /// To be used while activating future jobs upon updating channel `ChainTip` state. + pub fn activate(&mut self, min_ntime: u32) { + self.job_message.min_ntime = Sv2Option::new(Some(min_ntime)); + } +} diff --git a/protocols/v2/channels-sv2/src/server/jobs/factory.rs b/protocols/v2/channels-sv2/src/server/jobs/factory.rs new file mode 100644 index 0000000000..a3e05ada71 --- /dev/null +++ b/protocols/v2/channels-sv2/src/server/jobs/factory.rs @@ -0,0 +1,859 @@ +//! Abstraction of a factory for creating Sv2 Extended or Standard Jobs. +//! +//! This module provides the [`JobFactory`] struct, which enables the creation +//! of uniquely identified Extended and Standard mining jobs as required by +//! Stratum V2 (SV2) mining servers. It manages job ID assignment, construction +//! of coinbase transactions, and correct association of template and custom job +//! parameters. +//! +//! ## Responsibilities +//! +//! - **Job ID Generation**: Ensures all jobs have unique IDs per factory instance. +//! - **Job Construction**: Builds Extended and Standard jobs from SV2 templates and custom job +//! messages, assembling all required coinbase transaction data and metadata. +//! - **Coinbase Output Validation**: Verifies that coinbase outputs match SV2 template constraints +//! and protocol rules. +//! - **Version Rolling**: Tracks version rolling allowance for created jobs. +//! +//! ## Usage +//! +//! Designed for mining server implementations. Use `JobFactory` to generate jobs in response to +//! incoming SV2 messages (`NewTemplate`, `SetCustomMiningJob`), ensuring protocol correctness and +//! uniqueness of job IDs. +use crate::{ + bip141::try_strip_bip141, + chain_tip::ChainTip, + merkle_root::merkle_root_from_path, + outputs::deserialize_template_outputs, + server::jobs::{error::*, extended::ExtendedJob, standard::StandardJob}, +}; +use binary_sv2::{Sv2Option, B0255}; +use bitcoin::{ + absolute::LockTime, + blockdata::witness::Witness, + consensus::{serialize, Decodable}, + transaction::{OutPoint, Transaction, TxIn, TxOut, Version}, + Amount, Sequence, +}; +use mining_sv2::{NewExtendedMiningJob, NewMiningJob, SetCustomMiningJob, MAX_EXTRANONCE_LEN}; +use std::convert::TryInto; +use template_distribution_sv2::NewTemplate; + +#[derive(Debug, PartialEq, Eq, Clone)] +struct JobIdFactory { + state: u32, +} + +impl JobIdFactory { + /// Creates a new [`Id`] instance initialized to `0`. + fn new() -> Self { + Self { state: 0 } + } + + /// Increments then returns the internal state on a new ID. + fn next(&mut self) -> u32 { + self.state += 1; + self.state + } +} + +/// A Factory for creating Extended or Standard Jobs. +/// +/// Ensures unique job ids. +/// +/// Enables creation of new Extended Jobs from NewTemplate and SetCustomMiningJob messages. +/// +/// Enables creation of new Standard Jobs from NewTemplate messages. +#[derive(Debug, Clone)] +pub struct JobFactory { + job_id_factory: JobIdFactory, + version_rolling_allowed: bool, + pool_tag_string: Option, + miner_tag_string: Option, +} + +impl JobFactory { + /// Creates a new [`JobFactory`] instance. + /// + /// The `pool_tag_string` and `miner_tag_string` are optional and will be added to the coinbase + /// scriptSig. + /// + /// Version rolling is always allowed for standard jobs, so the `version_rolling_allowed` + /// parameter is only relevant for creating extended jobs. + pub fn new( + version_rolling_allowed: bool, + pool_tag_string: Option, + miner_tag_string: Option, + ) -> Self { + Self { + job_id_factory: JobIdFactory::new(), + version_rolling_allowed, + pool_tag_string, + miner_tag_string, + } + } + + /// Returns a byte vector with the OP_PUSHBYTES opcode and the pool+miner tag. + /// + /// The character `/` is used as a delimiter. + /// + /// If no pool or miner tag is provided, the delimiters are still added. + pub fn op_pushbytes_pool_miner_tag(&self) -> Result, JobFactoryError> { + let mut pool_miner_tag = vec![]; + pool_miner_tag.extend_from_slice(b"/"); + if let Some(pool_tag_string) = &self.pool_tag_string { + pool_miner_tag.extend_from_slice(pool_tag_string.as_bytes()); + } + pool_miner_tag.extend_from_slice(b"/"); + if let Some(miner_tag_string) = &self.miner_tag_string { + pool_miner_tag.extend_from_slice(miner_tag_string.as_bytes()); + } + pool_miner_tag.extend_from_slice(b"/"); + + // Create the proper OP_PUSHBYTES opcode based on data length + let op_pushbytes = match pool_miner_tag.len() { + // 100 bytes are available for scriptSig + // subtract 5 for BIP34 + // subtract 1 for OP_PUSHBYTES opcode before pool/miner tag + // subtract 1+32 for extranonce (OP_PUSHBYTES_32 + 32 bytes) + len @ 1..=61 => len as u8, + _ => return Err(JobFactoryError::CoinbaseTxPrefixError), + }; + + let mut op_pushbytes_pool_miner_tag = vec![]; + op_pushbytes_pool_miner_tag.push(op_pushbytes); + op_pushbytes_pool_miner_tag.extend_from_slice(&pool_miner_tag); + + Ok(op_pushbytes_pool_miner_tag) + } + + /// Creates a new job from a template. + /// + /// This job (and related shares) is fully committed to: + /// - The template + /// - The additional coinbase outputs (added to the outputs coming from the template) + /// - The extranonce prefix of the channel at the time of job creation + /// + /// The optional `ChainTip` defines whether the job will be future or not. + /// + /// Version rolling is always allowed for standard jobs, so the `version_rolling_allowed` + /// parameter is ignored. + /// + /// It's up to the caller to ensure that the sum of `additional_coinbase_outputs` is equal to + /// available template revenue. Returns an error otherwise. + pub fn new_standard_job<'a>( + &mut self, + channel_id: u32, + chain_tip: Option, + extranonce_prefix: Vec, + template: NewTemplate<'a>, + additional_coinbase_outputs: Vec, + ) -> Result, JobFactoryError> { + let coinbase_outputs_sum = additional_coinbase_outputs + .iter() + .map(|o| o.value.to_sat()) + .sum::(); + if coinbase_outputs_sum != template.coinbase_tx_value_remaining { + return Err(JobFactoryError::InvalidCoinbaseOutputsSum); + } + + let job_id = self.job_id_factory.next(); + + let version = template.version; + + let coinbase_tx_prefix = + self.coinbase_tx_prefix(template.clone(), additional_coinbase_outputs.clone())?; + let coinbase_tx_suffix = + self.coinbase_tx_suffix(template.clone(), additional_coinbase_outputs.clone())?; + let merkle_path = template.merkle_path.clone(); + let merkle_root = merkle_root_from_path( + &coinbase_tx_prefix, + &coinbase_tx_suffix, + &extranonce_prefix, + &merkle_path.inner_as_ref(), + ) + .expect("merkle root must be valid") + .try_into() + .expect("merkle root must be 32 bytes"); + + let job_message = match template.future_template { + true => NewMiningJob { + channel_id, + job_id, + min_ntime: Sv2Option::new(None), + version, + merkle_root, + }, + false => { + let min_ntime = match chain_tip { + Some(chain_tip) => Some(chain_tip.min_ntime()), + None => return Err(JobFactoryError::ChainTipRequired), + }; + + NewMiningJob { + channel_id, + job_id, + min_ntime: Sv2Option::new(min_ntime), + version, + merkle_root, + } + } + }; + + let job = StandardJob::from_template( + template, + extranonce_prefix, + additional_coinbase_outputs, + job_message, + ) + .map_err(|_| JobFactoryError::DeserializeCoinbaseOutputsError)?; + + Ok(job) + } + + /// Creates a new job from a template. + /// + /// This job (and related shares) is fully committed to: + /// - The template + /// - The additional coinbase outputs (added to the outputs coming from the template) + /// - The extranonce prefix of the channel at the time of job creation + /// + /// The optional `ChainTip` defines whether the job will be future or not. + /// + /// It's up to the caller to ensure that the sum of `additional_coinbase_outputs` is equal to + /// available template revenue. Returns an error otherwise. + pub fn new_extended_job<'a>( + &mut self, + channel_id: u32, + chain_tip: Option, + extranonce_prefix: Vec, + template: NewTemplate<'a>, + additional_coinbase_outputs: Vec, + ) -> Result, JobFactoryError> { + let coinbase_outputs_sum = additional_coinbase_outputs + .iter() + .map(|o| o.value.to_sat()) + .sum::(); + if coinbase_outputs_sum != template.coinbase_tx_value_remaining { + return Err(JobFactoryError::InvalidCoinbaseOutputsSum); + } + + let job_id = self.job_id_factory.next(); + + let version = template.version; + + let coinbase_tx_prefix = + self.coinbase_tx_prefix(template.clone(), additional_coinbase_outputs.clone())?; + let coinbase_tx_suffix = + self.coinbase_tx_suffix(template.clone(), additional_coinbase_outputs.clone())?; + + // strip bip141 bytes from coinbase_tx_prefix and coinbase_tx_suffix + let (coinbase_tx_prefix_stripped_bip141, coinbase_tx_suffix_stripped_bip141) = + try_strip_bip141(&coinbase_tx_prefix, &coinbase_tx_suffix) + .map_err(|_| JobFactoryError::FailedToStripBip141)? + .ok_or(JobFactoryError::FailedToStripBip141)?; + + let merkle_path = template.merkle_path.clone(); + + let job_message = match template.future_template { + true => NewExtendedMiningJob { + channel_id, + job_id, + min_ntime: Sv2Option::new(None), + version, + version_rolling_allowed: self.version_rolling_allowed, + merkle_path, + coinbase_tx_prefix: coinbase_tx_prefix_stripped_bip141 + .try_into() + .map_err(|_| JobFactoryError::CoinbaseTxPrefixError)?, + coinbase_tx_suffix: coinbase_tx_suffix_stripped_bip141 + .try_into() + .map_err(|_| JobFactoryError::CoinbaseTxSuffixError)?, + }, + false => { + let min_ntime = match chain_tip { + Some(chain_tip) => Some(chain_tip.min_ntime()), + None => return Err(JobFactoryError::ChainTipRequired), + }; + NewExtendedMiningJob { + channel_id, + job_id, + min_ntime: Sv2Option::new(min_ntime), + version, + version_rolling_allowed: self.version_rolling_allowed, + merkle_path, + coinbase_tx_prefix: coinbase_tx_prefix_stripped_bip141 + .try_into() + .map_err(|_| JobFactoryError::CoinbaseTxPrefixError)?, + coinbase_tx_suffix: coinbase_tx_suffix_stripped_bip141 + .try_into() + .map_err(|_| JobFactoryError::CoinbaseTxSuffixError)?, + } + } + }; + + let job = ExtendedJob::from_template( + template, + extranonce_prefix, + additional_coinbase_outputs, + coinbase_tx_prefix, + coinbase_tx_suffix, + job_message, + ) + .map_err(|_| JobFactoryError::DeserializeCoinbaseOutputsError)?; + + Ok(job) + } + + /// Creates a new coinbase_tx_prefix and coinbase_tx_suffix from a template. + /// + /// To be used by a Sv2 Job Declarator Client to create a `DeclareMiningJob` message. + /// + /// It's up to the caller to ensure that the sum of `additional_coinbase_outputs` + /// is equal to available template revenue. Returns an error otherwise. + pub fn new_coinbase_tx_prefix_and_suffix( + &self, + template: NewTemplate<'_>, + additional_coinbase_outputs: Vec, + ) -> Result<(Vec, Vec), JobFactoryError> { + let coinbase_outputs_sum = additional_coinbase_outputs + .iter() + .map(|o| o.value.to_sat()) + .sum::(); + if coinbase_outputs_sum != template.coinbase_tx_value_remaining { + return Err(JobFactoryError::InvalidCoinbaseOutputsSum); + } + + let coinbase_tx_prefix = + self.coinbase_tx_prefix(template.clone(), additional_coinbase_outputs.clone())?; + let coinbase_tx_suffix = + self.coinbase_tx_suffix(template.clone(), additional_coinbase_outputs.clone())?; + Ok((coinbase_tx_prefix, coinbase_tx_suffix)) + } + + /// Creates a new `SetCustomMiningJob` message from a template. + /// + /// To be used by a Sv2 Job Declarator Client. + /// + /// It's up to the caller to ensure that the sum of the additional coinbase outputs is equal to + /// available template revenue. + pub fn new_custom_job<'a>( + &self, + channel_id: u32, + request_id: u32, + token: B0255<'a>, + chain_tip: ChainTip, + template: NewTemplate<'a>, + additional_coinbase_outputs: Vec, + ) -> Result, JobFactoryError> { + let coinbase_outputs_sum = additional_coinbase_outputs + .iter() + .map(|o| o.value.to_sat()) + .sum::(); + if coinbase_outputs_sum != template.coinbase_tx_value_remaining { + return Err(JobFactoryError::InvalidCoinbaseOutputsSum); + } + + let template_outputs = deserialize_template_outputs( + template.coinbase_tx_outputs.to_vec(), + template.coinbase_tx_outputs_count, + ) + .map_err(|_| JobFactoryError::DeserializeCoinbaseOutputsError)?; + + let mut coinbase_tx_outputs = vec![]; + coinbase_tx_outputs.extend_from_slice(additional_coinbase_outputs.as_slice()); + coinbase_tx_outputs.extend_from_slice(template_outputs.as_slice()); + + let serialized_outputs = serialize(&coinbase_tx_outputs); + + let mut coinbase_prefix = vec![]; + coinbase_prefix.extend_from_slice(&template.coinbase_prefix.to_vec()); + coinbase_prefix.extend_from_slice(&self.op_pushbytes_pool_miner_tag()?); + coinbase_prefix.push(MAX_EXTRANONCE_LEN as u8); // OP_PUSHBYTES_32 (for the extranonce) + + let set_custom_mining_job = SetCustomMiningJob { + channel_id, + request_id, + token, + version: template.version, + prev_hash: chain_tip.prev_hash(), + min_ntime: chain_tip.min_ntime(), + nbits: chain_tip.nbits(), + coinbase_tx_version: template.coinbase_tx_version, + coinbase_prefix: coinbase_prefix + .try_into() + .map_err(|_| JobFactoryError::FailedToSerializeCoinbasePrefix)?, + coinbase_tx_input_n_sequence: template.coinbase_tx_input_sequence, + coinbase_tx_outputs: serialized_outputs + .try_into() + .map_err(|_| JobFactoryError::FailedToSerializeCoinbaseOutputs)?, + coinbase_tx_locktime: template.coinbase_tx_locktime, + merkle_path: template.merkle_path.clone(), + }; + + Ok(set_custom_mining_job) + } + + /// Creates a new Extended Job from a SetCustomMiningJob message. + /// + /// Assumes that the SetCustomMiningJob message has already been validated. + /// + /// To be used by Extended Channels on a Sv2 Pool Server. + pub fn new_extended_job_from_custom_job<'a>( + &mut self, + set_custom_mining_job: SetCustomMiningJob<'a>, + extranonce_prefix: Vec, + ) -> Result, JobFactoryError> { + let serialized_outputs = set_custom_mining_job + .coinbase_tx_outputs + .inner_as_ref() + .to_vec(); + + let coinbase_outputs = Vec::::consensus_decode(&mut serialized_outputs.as_slice()) + .map_err(|_| JobFactoryError::DeserializeCoinbaseOutputsError)?; + + let job_id = self.job_id_factory.next(); + + let version = set_custom_mining_job.version; + + let coinbase_tx_prefix = self.custom_coinbase_tx_prefix(set_custom_mining_job.clone())?; + let coinbase_tx_suffix = self.custom_coinbase_tx_suffix(set_custom_mining_job.clone())?; + + // strip bip141 bytes from coinbase_tx_prefix and coinbase_tx_suffix + let (coinbase_tx_prefix_stripped_bip141, coinbase_tx_suffix_stripped_bip141) = + try_strip_bip141(&coinbase_tx_prefix, &coinbase_tx_suffix) + .map_err(|_| JobFactoryError::FailedToStripBip141)? + .ok_or(JobFactoryError::FailedToStripBip141)?; + + let merkle_path = set_custom_mining_job.merkle_path.clone().into_static(); + + let job_message = NewExtendedMiningJob { + channel_id: set_custom_mining_job.channel_id, + job_id, + min_ntime: Sv2Option::new(Some(set_custom_mining_job.min_ntime)), + version, + version_rolling_allowed: self.version_rolling_allowed, + coinbase_tx_prefix: coinbase_tx_prefix_stripped_bip141 + .clone() + .try_into() + .map_err(|_| JobFactoryError::CoinbaseTxPrefixError)?, + coinbase_tx_suffix: coinbase_tx_suffix_stripped_bip141 + .clone() + .try_into() + .map_err(|_| JobFactoryError::CoinbaseTxSuffixError)?, + merkle_path, + }; + + let job = ExtendedJob::from_custom_job( + set_custom_mining_job, + extranonce_prefix, + coinbase_outputs, + coinbase_tx_prefix, + coinbase_tx_suffix, + job_message, + ); + + Ok(job) + } +} + +// impl block with private methods +impl JobFactory { + // build a coinbase transaction from a SetCustomMiningJob + // this is only used to extract coinbase_tx_prefix and coinbase_tx_suffix from the custom + // coinbase + fn custom_coinbase(&self, m: SetCustomMiningJob<'_>) -> Result { + let deserialized_outputs = Vec::::consensus_decode( + &mut m.coinbase_tx_outputs.inner_as_ref().to_vec().as_slice(), + ) + .map_err(|_| JobFactoryError::DeserializeCoinbaseOutputsError)?; + + let mut script_sig = vec![]; + script_sig.extend_from_slice(m.coinbase_prefix.inner_as_ref()); + script_sig.extend_from_slice(&[0; MAX_EXTRANONCE_LEN]); + + // Create transaction input + let tx_in = TxIn { + previous_output: OutPoint::null(), + script_sig: script_sig.into(), + sequence: Sequence(m.coinbase_tx_input_n_sequence), + witness: Witness::from(vec![vec![0; 32]]), /* note: 32 bytes of zeros is only safe to + * assume now, this could change in future + * soft forks */ + }; + + Ok(Transaction { + version: Version::non_standard(m.coinbase_tx_version as i32), + lock_time: LockTime::from_consensus(m.coinbase_tx_locktime), + input: vec![tx_in], + output: deserialized_outputs, + }) + } + + fn custom_coinbase_tx_prefix( + &self, + m: SetCustomMiningJob<'_>, + ) -> Result, JobFactoryError> { + let coinbase = self.custom_coinbase(m.clone())?; + let serialized_coinbase = serialize(&coinbase); + + let index = 4 // tx version + + 2 // segwit + + 1 // number of inputs + + 32 // prev OutPoint + + 4 // index + + 1 // bytes in script + + m.coinbase_prefix.inner_as_ref().len(); + + let coinbase_tx_prefix = serialized_coinbase[0..index].to_vec(); + + Ok(coinbase_tx_prefix) + } + + fn custom_coinbase_tx_suffix( + &self, + m: SetCustomMiningJob<'_>, + ) -> Result, JobFactoryError> { + let coinbase = self.custom_coinbase(m.clone())?; + let serialized_coinbase = serialize(&coinbase); + + // Calculate full extranonce size + let full_extranonce_size = MAX_EXTRANONCE_LEN; + + let index = 4 // tx version + + 2 // segwit + + 1 // number of inputs + + 32 // prev OutPoint + + 4 // index + + 1 // bytes in script + + m.coinbase_prefix.inner_as_ref().len() + + full_extranonce_size; + + let coinbase_tx_suffix = serialized_coinbase[index..].to_vec(); + + Ok(coinbase_tx_suffix) + } + + // build a coinbase transaction from some template in the JobFactory + fn coinbase( + &self, + template: NewTemplate<'_>, + coinbase_reward_outputs: Vec, + ) -> Result { + // check that the sum of the additional coinbase outputs is equal to the value remaining in + // the active template + let mut coinbase_reward_outputs_sum = Amount::from_sat(0); + for output in coinbase_reward_outputs.iter() { + coinbase_reward_outputs_sum = coinbase_reward_outputs_sum + .checked_add(output.value) + .ok_or(JobFactoryError::CoinbaseOutputsSumOverflow)?; + } + + if template.coinbase_tx_value_remaining < coinbase_reward_outputs_sum.to_sat() { + return Err(JobFactoryError::InvalidCoinbaseOutputsSum); + } + + let mut outputs = vec![]; + + for output in coinbase_reward_outputs.iter() { + outputs.push(output.clone()); + } + + let mut template_outputs = deserialize_template_outputs( + template.coinbase_tx_outputs.to_vec(), + template.coinbase_tx_outputs_count, + ) + .map_err(|_| JobFactoryError::DeserializeCoinbaseOutputsError)?; + + outputs.append(&mut template_outputs); + + let op_pushbytes_pool_miner_tag = self.op_pushbytes_pool_miner_tag()?; + + let mut script_sig = vec![]; + script_sig.extend_from_slice(&template.coinbase_prefix.to_vec()); + script_sig.extend_from_slice(&op_pushbytes_pool_miner_tag); + script_sig.push(MAX_EXTRANONCE_LEN as u8); // OP_PUSHBYTES_32 (for the extranonce) + script_sig.extend_from_slice(&[0; MAX_EXTRANONCE_LEN]); + + let tx_in = TxIn { + previous_output: OutPoint::null(), + script_sig: script_sig.into(), + sequence: Sequence(template.coinbase_tx_input_sequence), + witness: Witness::from(vec![vec![0; 32]]), /* note: 32 bytes of zeros is only safe to + * assume now, this could change in future + * soft forks */ + }; + + Ok(Transaction { + version: Version::non_standard(template.coinbase_tx_version as i32), + lock_time: LockTime::from_consensus(template.coinbase_tx_locktime), + input: vec![tx_in], + output: outputs, + }) + } + + fn coinbase_tx_prefix( + &self, + template: NewTemplate<'_>, + coinbase_reward_outputs: Vec, + ) -> Result, JobFactoryError> { + let coinbase = self.coinbase(template.clone(), coinbase_reward_outputs)?; + let serialized_coinbase = serialize(&coinbase); + + // Calculate the full pool/miner tag length including delimiters and OP_PUSHBYTES opcode + let pool_miner_tag_len = 1 // OP_PUSHBYTES opcode + + 3 // three "/" delimiters + + self.pool_tag_string.as_ref().map_or(0, |s| s.len()) + + self.miner_tag_string.as_ref().map_or(0, |s| s.len()); + + let index = 4 // tx version + + 2 // segwit bytes + + 1 // number of inputs + + 32 // prev OutPoint + + 4 // index + + 1 // bytes in script + + template.coinbase_prefix.len() + + pool_miner_tag_len + + 1; // OP_PUSHBYTES_32 (for the extranonce) + + let coinbase_tx_prefix = serialized_coinbase[0..index].to_vec(); + + Ok(coinbase_tx_prefix) + } + + fn coinbase_tx_suffix( + &self, + template: NewTemplate<'_>, + coinbase_reward_outputs: Vec, + ) -> Result, JobFactoryError> { + let coinbase = self.coinbase(template.clone(), coinbase_reward_outputs)?; + let serialized_coinbase = serialize(&coinbase); + + // Calculate the full pool/miner tag length including delimiters and OP_PUSHBYTES opcode + let pool_miner_tag_len = 1 // OP_PUSHBYTES opcode + + 3 // three "/" delimiters + + self.pool_tag_string.as_ref().map_or(0, |s| s.len()) + + self.miner_tag_string.as_ref().map_or(0, |s| s.len()); + + // 32 bytes + let full_extranonce_size = MAX_EXTRANONCE_LEN; + + let coinbase_tx_suffix = serialized_coinbase[4 // tx version + + 2 // segwit bytes + + 1 // number of inputs + + 32 // prev OutPoint + + 4 // index + + 1 // bytes in script + + template.coinbase_prefix.len() + + pool_miner_tag_len + + 1 // OP_PUSHBYTES_32 (for the extranonce) + + full_extranonce_size..] + .to_vec(); + + Ok(coinbase_tx_suffix) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bitcoin::ScriptBuf; + use template_distribution_sv2::NewTemplate; + + #[test] + fn test_new_pool_job() { + let mut job_factory = JobFactory::new(true, Some("Stratum V2 SRI Pool".to_string()), None); + + // note: + // the messages on this test were collected from a sane message flow + // we use them as test vectors to assert correct behavior of job creation + + let template = NewTemplate { + template_id: 1, + future_template: true, + version: 536870912, + coinbase_tx_version: 2, + coinbase_prefix: vec![82, 0].try_into().unwrap(), + coinbase_tx_input_sequence: 4294967295, + coinbase_tx_value_remaining: 5000000000, + coinbase_tx_outputs_count: 1, + coinbase_tx_outputs: vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 38, 106, 36, 170, 33, 169, 237, 226, 246, 28, 63, 113, 209, + 222, 253, 63, 169, 153, 223, 163, 105, 83, 117, 92, 105, 6, 137, 121, 153, 98, 180, + 139, 235, 216, 54, 151, 78, 140, 249, + ] + .try_into() + .unwrap(), + coinbase_tx_locktime: 0, + merkle_path: vec![].try_into().unwrap(), + }; + + // match the original script format used to generate the coinbase_reward_outputs for the + // expected job + let pubkey_hash = [ + 235, 225, 183, 220, 194, 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194, + 8, 252, + ]; + let mut script_bytes = vec![0]; // SegWit version 0 + script_bytes.push(20); // Push 20 bytes (length of pubkey hash) + script_bytes.extend_from_slice(&pubkey_hash); + let script = ScriptBuf::from(script_bytes); + let coinbase_reward_outputs = vec![TxOut { + value: Amount::from_sat(5000000000), + script_pubkey: script, + }]; + + // match the original extranonce_prefix used to generate the expected job + let extranonce_prefix = [ + 83, 116, 114, 97, 116, 117, 109, 32, 86, 50, 32, 83, 82, 73, 32, 80, 111, 111, 108, 0, + 0, 0, 0, 0, 0, 0, 1, + ] + .to_vec(); + + let job = job_factory + .new_extended_job( + 1, + None, + extranonce_prefix, + template, + coinbase_reward_outputs, + ) + .unwrap(); + + // we know that the provided template should generate this job + let expected_job = NewExtendedMiningJob { + channel_id: 1, + job_id: 1, + min_ntime: Sv2Option::new(None), + version: 536870912, + version_rolling_allowed: true, + // contains scriptSig with /Stratum V2 SRI Pool// + coinbase_tx_prefix: vec![ + 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 58, 82, 0, 22, 47, 83, 116, 114, 97, + 116, 117, 109, 32, 86, 50, 32, 83, 82, 73, 32, 80, 111, 111, 108, 47, 47, 32, + ] + .try_into() + .unwrap(), + coinbase_tx_suffix: vec![ + 255, 255, 255, 255, 2, 0, 242, 5, 42, 1, 0, 0, 0, 22, 0, 20, 235, 225, 183, 220, + 194, 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194, 8, 252, 0, 0, 0, + 0, 0, 0, 0, 0, 38, 106, 36, 170, 33, 169, 237, 226, 246, 28, 63, 113, 209, 222, + 253, 63, 169, 153, 223, 163, 105, 83, 117, 92, 105, 6, 137, 121, 153, 98, 180, 139, + 235, 216, 54, 151, 78, 140, 249, 0, 0, 0, 0, + ] + .try_into() + .unwrap(), + merkle_path: vec![].try_into().unwrap(), + }; + + assert_eq!(job.get_job_message(), &expected_job); + } + + #[test] + fn test_new_extended_job_from_custom_job() { + let jdc_job_factory = JobFactory::new( + true, + Some("Stratum V2 SRI Pool".to_string()), + Some("Stratum V2 SRI Miner".to_string()), + ); + + let extranonce_prefix = [ + 83, 116, 114, 97, 116, 117, 109, 32, 86, 50, 32, 83, 82, 73, 32, 80, 111, 111, 108, 0, + 0, 0, 0, 0, 0, 0, 1, + ] + .to_vec(); + + let template = NewTemplate { + template_id: 1, + future_template: false, + version: 536870912, + coinbase_tx_version: 2, + coinbase_prefix: vec![82, 0].try_into().unwrap(), + coinbase_tx_input_sequence: 4294967295, + coinbase_tx_value_remaining: 5000000000, + coinbase_tx_outputs_count: 1, + coinbase_tx_outputs: vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 38, 106, 36, 170, 33, 169, 237, 226, 246, 28, 63, 113, 209, + 222, 253, 63, 169, 153, 223, 163, 105, 83, 117, 92, 105, 6, 137, 121, 153, 98, 180, + 139, 235, 216, 54, 151, 78, 140, 249, + ] + .try_into() + .unwrap(), + coinbase_tx_locktime: 0, + merkle_path: vec![].try_into().unwrap(), + }; + + // match the original script format used to generate the coinbase_reward_outputs for the + // expected job + let pubkey_hash = [ + 235, 225, 183, 220, 194, 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194, + 8, 252, + ]; + let mut script_bytes = vec![0]; // SegWit version 0 + script_bytes.push(20); // Push 20 bytes (length of pubkey hash) + script_bytes.extend_from_slice(&pubkey_hash); + let script = ScriptBuf::from(script_bytes); + let coinbase_reward_outputs = vec![TxOut { + value: Amount::from_sat(5000000000), + script_pubkey: script, + }]; + + let chain_tip = ChainTip::new( + [ + 200, 53, 253, 129, 214, 31, 43, 84, 179, 58, 58, 76, 128, 213, 24, 53, 38, 144, + 205, 88, 172, 20, 251, 22, 217, 141, 21, 221, 21, 0, 0, 0, + ] + .into(), + 503543726, + 1746839905, + ); + + let set_custom_mining_job = jdc_job_factory + .new_custom_job( + 1, + 1, + vec![0].try_into().unwrap(), + chain_tip, + template, + coinbase_reward_outputs, + ) + .unwrap(); + + let mut pool_job_factory = + JobFactory::new(true, Some("Stratum V2 SRI Pool".to_string()), None); + + let custom_job = pool_job_factory + .new_extended_job_from_custom_job(set_custom_mining_job, extranonce_prefix) + .unwrap(); + + let expected_job = NewExtendedMiningJob { + channel_id: 1, + job_id: 1, + min_ntime: Sv2Option::new(Some(1746839905)), + version: 536870912, + version_rolling_allowed: true, + // contains scriptSig with /Stratum V2 SRI Pool/Stratum V2 SRI Miner/ + coinbase_tx_prefix: vec![ + 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 78, 82, 0, 42, 47, 83, 116, 114, 97, + 116, 117, 109, 32, 86, 50, 32, 83, 82, 73, 32, 80, 111, 111, 108, 47, 83, 116, 114, + 97, 116, 117, 109, 32, 86, 50, 32, 83, 82, 73, 32, 77, 105, 110, 101, 114, 47, 32, + ] + .try_into() + .unwrap(), + coinbase_tx_suffix: vec![ + 255, 255, 255, 255, 2, 0, 242, 5, 42, 1, 0, 0, 0, 22, 0, 20, 235, 225, 183, 220, + 194, 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194, 8, 252, 0, 0, 0, + 0, 0, 0, 0, 0, 38, 106, 36, 170, 33, 169, 237, 226, 246, 28, 63, 113, 209, 222, + 253, 63, 169, 153, 223, 163, 105, 83, 117, 92, 105, 6, 137, 121, 153, 98, 180, 139, + 235, 216, 54, 151, 78, 140, 249, 0, 0, 0, 0, + ] + .try_into() + .unwrap(), + merkle_path: vec![].try_into().unwrap(), + }; + + assert_eq!(custom_job.get_job_message(), &expected_job); + } +} diff --git a/protocols/v2/channels-sv2/src/server/jobs/job_store.rs b/protocols/v2/channels-sv2/src/server/jobs/job_store.rs new file mode 100644 index 0000000000..71ba232de9 --- /dev/null +++ b/protocols/v2/channels-sv2/src/server/jobs/job_store.rs @@ -0,0 +1,168 @@ +//! Abstractions for job storage and lifecycle management in SV2 mining channels. +//! +//! This module provides the [`JobStore`] trait and a default implementation for +//! tracking mining job states (future, active, past, stale) for SV2 Extended and Standard channels. +//! +//! ## Responsibilities +//! +//! - **Job Storage**: Manages collections of jobs indexed by job ID and template ID. +//! - **Job Activation**: Handles transitions between future, active, past, and stale jobs. +//! - **Template Mapping**: Tracks mappings from template IDs to job IDs for future jobs. +//! - **Lifecycle Management**: Ensures correct state transitions when activating jobs or updating +//! chain tips. +//! +//! ## Usage +//! +//! Use the [`JobStore`] trait for custom job store implementations, or the [`DefaultJobStore`] +//! for standard job lifecycle management in mining channel abstractions. + +use std::{collections::HashMap, fmt::Debug}; + +use super::Job; + +/// Trait for job lifecycle management in mining channels. +/// +/// Types implementing `JobStore` must support tracking and transitioning jobs through various +/// states (future, active, past, stale), and provide access to job collections and mappings. +pub trait JobStore: Send + Sync + Debug { + /// Adds a future job associated with a template ID. + /// Returns the new job's ID. + fn add_future_job(&mut self, template_id: u64, job: T) -> u32; + + /// Adds an active job, moving the previous active job (if any) to past jobs. + fn add_active_job(&mut self, job: T); + + /// Activates a future job given by template ID and header timestamp. + /// Returns `true` if successful, `false` if not found. + fn activate_future_job(&mut self, template_id: u64, prev_hash_header_timestamp: u32) -> bool; + + /// Marks all past jobs as stale, so that shares can be rejected with the appropriate error + /// code + fn mark_past_jobs_as_stale(&mut self); + + /// Returns the mapping from future template IDs to job IDs. + fn get_future_template_to_job_id(&self) -> &HashMap; + + /// Returns the currently active job, if any. + fn get_active_job(&self) -> Option<&T>; + + /// Returns all future jobs, indexed by job ID. + fn get_future_jobs(&self) -> &HashMap; + + /// Returns all past jobs (previously active jobs), indexed by job ID. + fn get_past_jobs(&self) -> &HashMap; + + /// Returns all stale jobs (jobs from previous chain tip), indexed by job ID. + fn get_stale_jobs(&self) -> &HashMap; +} + +/// Default implementation of [`JobStore`] for tracking mining job states in SV2 channels. +/// +/// Maintains collections for future, active, past, and stale jobs, and tracks template-to-job ID +/// mappings for future job activation. +#[derive(Debug)] +pub struct DefaultJobStore { + future_template_to_job_id: HashMap, + // Future jobs are indexed with job_id (u32) + future_jobs: HashMap, + active_job: Option, + // Past jobs are indexed with job_id (u32) + past_jobs: HashMap, + // Stale jobs are indexed with job_id (u32) + stale_jobs: HashMap, +} + +impl DefaultJobStore { + /// Creates a new empty job store. + pub fn new() -> Self { + Self { + future_template_to_job_id: HashMap::new(), + future_jobs: HashMap::new(), + active_job: None, + past_jobs: HashMap::new(), + stale_jobs: HashMap::new(), + } + } +} + +impl Default for DefaultJobStore { + fn default() -> Self { + Self::new() + } +} + +impl JobStore for DefaultJobStore { + fn add_future_job(&mut self, template_id: u64, new_job: T) -> u32 { + let new_job_id = new_job.get_job_id(); + self.future_jobs.insert(new_job_id, new_job); + self.future_template_to_job_id + .insert(template_id, new_job_id); + new_job_id + } + + fn add_active_job(&mut self, job: T) { + // Move currently active job to past jobs (so it can be marked as stale) + if let Some(active_job) = self.active_job.take() { + self.past_jobs.insert(active_job.get_job_id(), active_job); + } + // Set the new active job + self.active_job = Some(job); + } + + fn activate_future_job(&mut self, template_id: u64, prev_hash_header_timestamp: u32) -> bool { + let mut future_job = + if let Some(job_id) = self.future_template_to_job_id.remove(&template_id) { + if let Some(job) = self.future_jobs.remove(&job_id) { + job + } else { + return false; + } + } else { + return false; + }; + + // Move currently active job to past jobs (so it can be marked as stale) + if let Some(active_job) = self.active_job.take() { + self.past_jobs.insert(active_job.get_job_id(), active_job); + } + + // Activate the future job + future_job.activate(prev_hash_header_timestamp); + self.active_job = Some(future_job); + self.future_jobs.clear(); + self.future_template_to_job_id.clear(); + + self.mark_past_jobs_as_stale(); + + true + } + + fn mark_past_jobs_as_stale(&mut self) { + // Mark all past jobs as stale, so that shares can be rejected with the appropriate error + // code + self.stale_jobs = self.past_jobs.clone(); + + // Clear past jobs, as we're no longer going to validate shares for them + self.past_jobs.clear(); + } + + fn get_future_template_to_job_id(&self) -> &HashMap { + &self.future_template_to_job_id + } + + fn get_active_job(&self) -> Option<&T> { + self.active_job.as_ref() + } + + fn get_future_jobs(&self) -> &HashMap { + &self.future_jobs + } + + fn get_past_jobs(&self) -> &HashMap { + &self.past_jobs + } + + fn get_stale_jobs(&self) -> &HashMap { + &self.stale_jobs + } +} diff --git a/protocols/v2/channels-sv2/src/server/jobs/mod.rs b/protocols/v2/channels-sv2/src/server/jobs/mod.rs new file mode 100644 index 0000000000..32c9cd98f8 --- /dev/null +++ b/protocols/v2/channels-sv2/src/server/jobs/mod.rs @@ -0,0 +1,46 @@ +//! Mining job construction - Mining Server Abstraction. +//! +//! This module provides submodules and traits for representing, constructing, and managing +//! Extended and Standard mining jobs in Stratum V2 (SV2) mining servers. +//! +//! ## Responsibilities +//! +//! - **Error Handling**: See [`error`] submodule for job-related error types. +//! - **Extended Jobs**: See [`extended`] submodule for SV2 extended job implementation. +//! - **Standard Jobs**: See [`standard`] submodule for SV2 standard job implementation. +//! - **Job Factories**: See [`factory`] for job creation logic and unique job ID assignment. +//! - **Job Storage**: See [`job_store`] for job lifecycle management and storage abstractions. +//! - **Job Origin Tracking**: Tracks job origin (template or custom job message). +//! - **Job Trait**: Unified trait for all mining job types, supporting activation and job ID +//! retrieval. +//! +//! ## Usage +//! +//! Use these abstractions for implementing mining server job management and SV2 protocol logic. + +pub mod error; +pub mod extended; +pub mod factory; +pub mod job_store; +pub mod standard; + +use mining_sv2::SetCustomMiningJob; +use template_distribution_sv2::NewTemplate; + +#[derive(Clone, Debug, PartialEq)] +pub enum JobOrigin<'a> { + NewTemplate(NewTemplate<'a>), + SetCustomMiningJob(SetCustomMiningJob<'a>), +} + +/// Trait for mining job types in SV2 mining servers. +/// +/// Types implementing `Job` must provide a unique job ID and support activation upon chain tip +/// update. +pub trait Job: Send + Sync { + /// Returns the unique job ID for this job. + fn get_job_id(&self) -> u32; + + /// Activates the job for a new chain tip or prev_hash header timestamp. + fn activate(&mut self, prev_hash_header_timestamp: u32); +} diff --git a/protocols/v2/channels-sv2/src/server/jobs/standard.rs b/protocols/v2/channels-sv2/src/server/jobs/standard.rs new file mode 100644 index 0000000000..7efc109ff7 --- /dev/null +++ b/protocols/v2/channels-sv2/src/server/jobs/standard.rs @@ -0,0 +1,118 @@ +//! Abstraction of a standard mining job for SV2 mining servers. +//! +//! This module provides the [`StandardJob`] struct, which encapsulates all the state and +//! protocol-relevant data for a standard mining job as handled by a mining server. +//! +//! ## Responsibilities +//! +//! - **Origin Tracking**: Captures the originating `NewTemplate` message and extranonce prefix at +//! creation time. +//! - **Coinbase Outputs Management**: Combines spendable and unspendable coinbase outputs from the +//! template and additional outputs. +//! - **Wire-format Message**: Stores the protocol wire-format `NewMiningJob` message for downstream +//! communication. +//! - **Lifecycle Management**: Supports activation and state transitions of jobs, including +//! future/non-future status. +//! +//! ## Usage +//! +//! Use this struct when creating, activating, or managing standard mining jobs in SV2-compliant +//! mining servers. + +use crate::{ + outputs::deserialize_template_outputs, + server::jobs::{error::StandardJobError, Job}, +}; +use binary_sv2::{Sv2Option, U256}; +use bitcoin::transaction::TxOut; +use mining_sv2::NewMiningJob; +use template_distribution_sv2::NewTemplate; + +/// Abstraction of a standard mining job with: +/// - the `NewTemplate` message that originated it +/// - the extranonce prefix associated with the channel at the time of job creation +/// - all coinbase outputs (spendable + unspendable) associated with the job +/// - the `NewMiningJob` message to be sent across the wire +#[derive(Debug, Clone)] +pub struct StandardJob<'a> { + template: NewTemplate<'a>, + extranonce_prefix: Vec, + coinbase_outputs: Vec, + job_message: NewMiningJob<'a>, +} + +impl Job for StandardJob<'_> { + /// Returns the job ID for this job. + fn get_job_id(&self) -> u32 { + self.job_message.job_id + } + + /// Activates the job by setting the minimum ntime field. + fn activate(&mut self, min_ntime: u32) { + self.activate(min_ntime); + } +} + +impl<'a> StandardJob<'a> { + /// Creates a new standard job from a template. + /// + /// Combines coinbase outputs from the template and any additional outputs. + /// Returns an error if coinbase outputs cannot be deserialized. + pub fn from_template( + template: NewTemplate<'a>, + extranonce_prefix: Vec, + additional_coinbase_outputs: Vec, + job_message: NewMiningJob<'a>, + ) -> Result { + let template_coinbase_outputs = deserialize_template_outputs( + template.coinbase_tx_outputs.to_vec(), + template.coinbase_tx_outputs_count, + ) + .map_err(|_| StandardJobError::FailedToDeserializeCoinbaseOutputs)?; + + let mut coinbase_outputs = vec![]; + coinbase_outputs.extend(additional_coinbase_outputs); + coinbase_outputs.extend(template_coinbase_outputs); + + Ok(Self { + template, + extranonce_prefix, + coinbase_outputs, + job_message, + }) + } + /// Returns the job ID for this job. + pub fn get_job_id(&self) -> u32 { + self.job_message.job_id + } + /// Returns all coinbase outputs (spendable and unspendable) for this job. + pub fn get_coinbase_outputs(&self) -> &Vec { + &self.coinbase_outputs + } + /// Returns the extranonce prefix used for this job. + pub fn get_extranonce_prefix(&self) -> &Vec { + &self.extranonce_prefix + } + /// Returns the `NewMiningJob` message for this job. + pub fn get_job_message(&self) -> &NewMiningJob<'a> { + &self.job_message + } + /// Returns the originating `NewTemplate` message for this job. + pub fn get_template(&self) -> &NewTemplate<'a> { + &self.template + } + /// Returns the merkle root for this job. + pub fn get_merkle_root(&self) -> &U256<'a> { + &self.job_message.merkle_root + } + /// Returns true if the job is a future job (not yet activated). + pub fn is_future(&self) -> bool { + self.job_message.min_ntime.clone().into_inner().is_none() + } + /// Activates the job by setting the minimum ntime field. + /// + /// Should be called when activating future jobs. + pub fn activate(&mut self, min_ntime: u32) { + self.job_message.min_ntime = Sv2Option::new(Some(min_ntime)); + } +} diff --git a/protocols/v2/roles-logic-sv2/src/channels/server/mod.rs b/protocols/v2/channels-sv2/src/server/mod.rs similarity index 64% rename from protocols/v2/roles-logic-sv2/src/channels/server/mod.rs rename to protocols/v2/channels-sv2/src/server/mod.rs index 5ac040b6e3..06319cf60f 100644 --- a/protocols/v2/roles-logic-sv2/src/channels/server/mod.rs +++ b/protocols/v2/channels-sv2/src/server/mod.rs @@ -1,4 +1,4 @@ -//! Abstractions for channels to be used by mining servers. +//! Sv2 channels - Mining Servers Abstraction. pub mod error; pub mod extended; diff --git a/protocols/v2/channels-sv2/src/server/share_accounting.rs b/protocols/v2/channels-sv2/src/server/share_accounting.rs new file mode 100644 index 0000000000..4369b1610a --- /dev/null +++ b/protocols/v2/channels-sv2/src/server/share_accounting.rs @@ -0,0 +1,172 @@ +//! Share Validation - Mining Server Abstraction. +//! +//! This module provides types and logic for validating mining shares and tracking share accounting +//! state on a mining server. It is used to determine the outcome of submitted shares, track +//! duplicate submissions, maintain batch acknowledgment state, and compute share statistics for +//! downstream Stratum V2 (SV2) messaging. +//! +//! ## Responsibilities +//! +//! - **Share Validation Result**: Encapsulates the result of validating a mining share, including +//! success, batch acknowledgment, and block discovery. +//! - **Share Validation Error**: Enumerates possible failure reasons when validating a share. +//! - **Share Accounting**: Tracks per-channel share statistics, acknowledges batches, detects +//! duplicate shares, and maintains best difficulty found. +//! +//! ## Usage +//! +//! Intended for use within mining server implementations that process SV2 share submissions and +//! issue `SubmitShares.Success` messages. Not intended for use by mining clients. + +use bitcoin::hashes::sha256d::Hash; +use std::collections::HashSet; + +/// The outcome of share validation, from the perspective of a Mining Server. +/// +/// The [`ShareValidationResult::ValidWithAcknowledgement`] variant carries: +/// - `last_sequence_number` (as `u32`) +/// - `new_submits_accepted_count` (as `u32`) +/// - `new_shares_sum` (as `u64`) +/// +/// which are used to craft `SubmitShares.Success` Sv2 messages. +/// +/// The [`ShareValidationResult::BlockFound`] variant carries: +/// - `template_id` (as `Option`) +/// - `coinbase` (as `Vec`) +/// +/// where `template_id` is `None` if the share is for a custom job. +#[derive(Debug)] +pub enum ShareValidationResult { + /// The share is valid and accepted. + Valid, + /// The share is valid and triggers a batch acknowledgment. + /// Contains: + /// - `last_sequence_number`: The sequence number of the last accepted share in the batch. + /// - `new_submits_accepted_count`: The number of new shares accepted in this batch. + /// - `new_shares_sum`: The total work contributed by shares in this batch. + ValidWithAcknowledgement(u32, u32, u64), + /// The share solves a block. + /// Contains: + /// - `template_id`: The template ID associated with the job, or `None` for custom jobs. + /// - `coinbase`: The serialized coinbase transaction for the block. + BlockFound(Option, Vec), +} + +/// The error variants that can occur during share validation. +#[derive(Debug)] +pub enum ShareValidationError { + /// The share is invalid for unspecified reasons. + Invalid, + /// The share is stale due to chain tip changes. + Stale, + /// The submitted job ID does not refer to any known job for this channel. + InvalidJobId, + /// The share does not meet the required target difficulty. + DoesNotMeetTarget, + /// The submitted share attempts version rolling when not allowed. + VersionRollingNotAllowed, + /// The share is a duplicate of a previously accepted share. + DuplicateShare, + /// The coinbase transaction was invalid or malformed. + InvalidCoinbase, + /// No chain tip is set for the channel (required for share validation). + NoChainTip, +} + +/// The state of share validation in the context of some specific channel (either Extended or +/// Standard). +/// +/// This struct manages per-channel share statistics, batch acknowledgment, duplicate detection, +/// and difficulty tracking. Only meant for usage on Mining Servers. +#[derive(Clone, Debug)] +pub struct ShareAccounting { + last_share_sequence_number: u32, + shares_accepted: u32, + share_work_sum: u64, + share_batch_size: usize, + seen_shares: HashSet, + best_diff: f64, +} + +impl ShareAccounting { + /// Constructs a new `ShareAccounting` instance for a channel. + /// + /// `share_batch_size` controls how many accepted shares trigger a batch acknowledgment. + pub fn new(share_batch_size: usize) -> Self { + Self { + last_share_sequence_number: 0, + shares_accepted: 0, + share_work_sum: 0, + share_batch_size, + seen_shares: HashSet::new(), + best_diff: 0.0, + } + } + + /// Updates internal accounting for a newly accepted share. + /// + /// - Increments total shares accepted and work sum. + /// - Updates last accepted sequence number. + /// - Records the share hash to detect duplicates. + pub fn update_share_accounting( + &mut self, + share_work: u64, + share_sequence_number: u32, + share_hash: Hash, + ) { + self.last_share_sequence_number = share_sequence_number; + self.shares_accepted += 1; + self.share_work_sum += share_work; + self.seen_shares.insert(share_hash); + } + + /// Clears the set of seen share hashes. + /// + /// Should be called on every chain tip update to avoid unbounded growth of memory + /// and allow new shares for the new tip. + pub fn flush_seen_shares(&mut self) { + self.seen_shares.clear(); + } + + /// Returns the sequence number of the last accepted share. + pub fn get_last_share_sequence_number(&self) -> u32 { + self.last_share_sequence_number + } + + /// Returns the total number of shares accepted on this channel. + pub fn get_shares_accepted(&self) -> u32 { + self.shares_accepted + } + + /// Returns the sum of work contributed by all accepted shares. + pub fn get_share_work_sum(&self) -> u64 { + self.share_work_sum + } + + /// Returns the configured batch size for share acknowledgments. + pub fn get_share_batch_size(&self) -> usize { + self.share_batch_size + } + + /// Returns true if the current count of accepted shares triggers an acknowledgment. + pub fn should_acknowledge(&self) -> bool { + self.shares_accepted % self.share_batch_size as u32 == 0 + } + + /// Checks if the share hash has already been accepted (duplicate detection). + pub fn is_share_seen(&self, share_hash: Hash) -> bool { + self.seen_shares.contains(&share_hash) + } + + /// Returns the highest difficulty found among accepted shares. + pub fn get_best_diff(&self) -> f64 { + self.best_diff + } + + /// Updates the best difficulty if the new value is higher. + pub fn update_best_diff(&mut self, diff: f64) { + if diff > self.best_diff { + self.best_diff = diff; + } + } +} diff --git a/protocols/v2/roles-logic-sv2/src/channels/server/standard.rs b/protocols/v2/channels-sv2/src/server/standard.rs similarity index 77% rename from protocols/v2/roles-logic-sv2/src/channels/server/standard.rs rename to protocols/v2/channels-sv2/src/server/standard.rs index 789b76d380..986b864f8c 100644 --- a/protocols/v2/roles-logic-sv2/src/channels/server/standard.rs +++ b/protocols/v2/channels-sv2/src/server/standard.rs @@ -1,18 +1,51 @@ -//! Abstraction over the state of a Sv2 Standard Channel, as seen by a Mining Server +//! Sv2 Standard Channel - Mining Server Abstraction. +//! +//! This module provides the [`StandardChannel`] struct, which models and manages the state of a +//! Stratum V2 (SV2) standard channel as maintained on a mining server. +//! +//! ## Responsibilities +//! +//! `StandardChannel` is responsible for managing all the state associated with an SV2 standard +//! channel, including: +//! +//! - **Channel Parameters**: Unique `channel_id`, `user_identity`, `extranonce_prefix`, maximum +//! target, nominal hashrate, and other properties negotiated at channel opening. +//! - **Target Difficulty**: Maintains both the requested maximum target and the current working +//! target for the channel, recalculated as hashrate or share rate changes. +//! - **Job Lifecycle Management**: Manages active, future, past, and stale jobs, including +//! activation on new chain tips and template updates. +//! - **Share Validation and Accounting**: Validates submitted shares, updates share accounting +//! state, detects duplicates, and manages batch acknowledgements for SV2 `SubmitShares.Success` +//! responses. +//! - **Chain Tip Management**: Tracks the latest known chain tip (block height, previous hash, +//! timestamp, and target) for constructing headers and validating shares. +//! +//! ## Usage +//! +//! Intended for use by pool servers or SV2-compliant job declaration clients (JDC), not by mining +//! devices or proxies. Encapsulates logic for handling SV2 messages such as `NewTemplate`, +//! `SetNewPrevHash`, and `SubmitSharesStandard`. +//! +//! ## Notes +//! +//! - Only one active job is allowed at a time. When a chain tip updates, jobs from the previous tip +//! become stale and are tracked accordingly. +//! - Share batch acknowledgment logic is tied to the configured batch size. +//! - Extranonce prefix updates must be consistent with SV2 protocol constraints. +//! - Job lifecycle and share accounting are managed on a per-channel basis. use crate::{ - channels::{ - chain_tip::ChainTip, - server::{ - error::StandardChannelError, - jobs::{factory::JobFactory, standard::StandardJob}, - share_accounting::{ShareAccounting, ShareValidationError, ShareValidationResult}, + chain_tip::ChainTip, + server::{ + error::StandardChannelError, + jobs::{ + extended::ExtendedJob, factory::JobFactory, job_store::JobStore, standard::StandardJob, }, + share_accounting::{ShareAccounting, ShareValidationError, ShareValidationResult}, }, - utils::{bytes_to_hex, hash_rate_to_target, target_to_difficulty, u256_to_block_hash}, + target::{bytes_to_hex, hash_rate_to_target, target_to_difficulty, u256_to_block_hash}, }; -use mining_sv2::{SubmitSharesStandard, Target, MAX_EXTRANONCE_LEN}; -use std::{collections::HashMap, convert::TryInto}; -use stratum_common::bitcoin::{ +use binary_sv2::{self}; +use bitcoin::{ absolute::LockTime, blockdata::{ block::{Header, Version}, @@ -23,6 +56,8 @@ use stratum_common::bitcoin::{ transaction::{OutPoint, Transaction, TxIn, TxOut, Version as TxVersion}, CompactTarget, Sequence, Target as BitcoinTarget, }; +use mining_sv2::{SubmitSharesStandard, Target, MAX_EXTRANONCE_LEN}; +use std::{collections::HashMap, convert::TryInto, marker::PhantomData}; use template_distribution_sv2::{NewTemplate, SetNewPrevHash}; use tracing::debug; @@ -44,31 +79,105 @@ use tracing::debug; /// indexed by `job_id`) /// - the channel's job factory /// - the channel's chain tip -#[derive(Debug, Clone)] -pub struct StandardChannel<'a> { +#[derive(Debug)] +pub struct StandardChannel<'a, J> +where + J: JobStore>, +{ pub channel_id: u32, user_identity: String, extranonce_prefix: Vec, requested_max_target: Target, target: Target, nominal_hashrate: f32, - // maps template_id to job_id on future jobs - future_template_to_job_id: HashMap, - // future jobs are indexed with job_id (u32) - future_jobs: HashMap>, - active_job: Option>, - // past jobs are indexed with job_id (u32) - past_jobs: HashMap>, - // stale jobs are indexed with job_id (u32) - stale_jobs: HashMap>, share_accounting: ShareAccounting, expected_share_per_minute: f32, + job_store: J, job_factory: JobFactory, chain_tip: Option, + phantom: PhantomData<&'a ()>, } -impl<'a> StandardChannel<'a> { - pub fn new( +impl<'a, J> StandardChannel<'a, J> +where + J: JobStore>, +{ + /// Constructor of `StandardChannel` for a Sv2 Pool Server. + /// Not meant for usage on a Sv2 Job Declaration Client. + /// + /// Initializes the standard channel state with the provided parameters, including channel + /// identifiers, difficulty targets, share accounting, and job management. + /// Returns an error if target/difficulty parameters are invalid or extranonce prefix + /// requirements are not met. + /// + /// For non-JD jobs, `pool_tag_string` is added to the coinbase scriptSig in between `/` + /// and `//` delimiters: `/pool_tag_string//` + #[allow(clippy::too_many_arguments)] + pub fn new_for_pool( + channel_id: u32, + user_identity: String, + extranonce_prefix: Vec, + requested_max_target: Target, + nominal_hashrate: f32, + share_batch_size: usize, + expected_share_per_minute: f32, + job_store: J, + pool_tag_string: String, + ) -> Result { + Self::new( + channel_id, + user_identity, + extranonce_prefix, + requested_max_target, + nominal_hashrate, + share_batch_size, + expected_share_per_minute, + job_store, + Some(pool_tag_string), + None, + ) + } + + /// Constructor of `StandardChannel` for a Sv2 Job Declaration Client. + /// Not meant for usage on a Sv2 Pool Server. + /// + /// Initializes the extended channel state with the provided parameters, including channel + /// identifiers, difficulty targets, share accounting, and job management. + /// Returns an error if target/difficulty parameters are invalid or extranonce prefix + /// requirements are not met. + /// + /// The `pool_tag_string` and `miner_tag_string` are added to the coinbase scriptSig in between + /// `/` delimiters: `/pool_tag_string/miner_tag_string/` + #[allow(clippy::too_many_arguments)] + pub fn new_for_job_declaration_client( + channel_id: u32, + user_identity: String, + extranonce_prefix: Vec, + requested_max_target: Target, + nominal_hashrate: f32, + share_batch_size: usize, + expected_share_per_minute: f32, + job_store: J, + pool_tag_string: Option, + miner_tag_string: String, + ) -> Result { + Self::new( + channel_id, + user_identity, + extranonce_prefix, + requested_max_target, + nominal_hashrate, + share_batch_size, + expected_share_per_minute, + job_store, + pool_tag_string, + Some(miner_tag_string), + ) + } + + // private constructor + #[allow(clippy::too_many_arguments)] + fn new( channel_id: u32, user_identity: String, extranonce_prefix: Vec, @@ -76,6 +185,9 @@ impl<'a> StandardChannel<'a> { nominal_hashrate: f32, share_batch_size: usize, expected_share_per_minute: f32, + job_store: J, + pool_tag_string: Option, + miner_tag_string: Option, ) -> Result { let calculated_target = match hash_rate_to_target(nominal_hashrate.into(), expected_share_per_minute.into()) { @@ -98,30 +210,33 @@ impl<'a> StandardChannel<'a> { requested_max_target, target, nominal_hashrate, - future_template_to_job_id: HashMap::new(), - future_jobs: HashMap::new(), - active_job: None, - past_jobs: HashMap::new(), - stale_jobs: HashMap::new(), share_accounting: ShareAccounting::new(share_batch_size), expected_share_per_minute, - job_factory: JobFactory::new(true), + job_factory: JobFactory::new(true, pool_tag_string, miner_tag_string), chain_tip: None, + job_store, + phantom: PhantomData, }) } + /// Returns the unique channel ID for this channel. pub fn get_channel_id(&self) -> u32 { self.channel_id } + /// Returns the user identity string for this channel. pub fn get_user_identity(&self) -> &String { &self.user_identity } + /// Returns the extranonce prefix bytes. pub fn get_extranonce_prefix(&self) -> &Vec { &self.extranonce_prefix } + /// Sets a new extranonce prefix for this channel. + /// + /// Returns an error if the new prefix is too large. pub fn set_extranonce_prefix( &mut self, extranonce_prefix: Vec, @@ -134,30 +249,38 @@ impl<'a> StandardChannel<'a> { Ok(()) } - + /// Updates the current target for this channel. pub fn set_target(&mut self, target: Target) { self.target = target; } - + /// Updates the nominal hashrate for this channel. pub fn set_nominal_hashrate(&mut self, nominal_hashrate: f32) { self.nominal_hashrate = nominal_hashrate; } - + /// Returns the requested maximum target for this channel. pub fn get_requested_max_target(&self) -> &Target { &self.requested_max_target } - + /// Returns the current target for this channel. pub fn get_target(&self) -> &Target { &self.target } - + /// Returns the nominal hashrate for this channel. pub fn get_nominal_hashrate(&self) -> f32 { self.nominal_hashrate } - /// Updates the channel's nominal hashrate and target. + /// Updates channel configuration with a new nominal hashrate. /// - /// If requested_max_target is None, we use the cached value in the channel state. + /// Adjusts target difficulty and internal state. Returns an error if + /// any input parameters are invalid or constraints are violated. + /// + /// This can be used in two scenarios: + /// - Client sent `UpdateChannel` message, which contains a `requested_max_target` parameter + /// that's also used as input. + /// - vardiff algorithm estimated a new nominal hashrate, in which case `requested_max_target` + /// is `None` and we use the value from the channel state (that was set either during channel + /// opening or some previous `UpdateChannel` message). pub fn update_channel( &mut self, nominal_hashrate: f32, @@ -211,27 +334,36 @@ impl<'a> StandardChannel<'a> { self.requested_max_target = requested_max_target; Ok(()) } - + /// Returns the currently active job, if any. pub fn get_active_job(&self) -> Option<&StandardJob<'a>> { - self.active_job.as_ref() + self.job_store.get_active_job() } - + /// Returns the mapping of future template IDs to job IDs. pub fn get_future_template_to_job_id(&self) -> &HashMap { - &self.future_template_to_job_id + self.job_store.get_future_template_to_job_id() } + /// Returns all future jobs for this channel. pub fn get_future_jobs(&self) -> &HashMap> { - &self.future_jobs + self.job_store.get_future_jobs() } + /// Returns all past jobs for this channel. pub fn get_past_jobs(&self) -> &HashMap> { - &self.past_jobs + self.job_store.get_past_jobs() } + /// Returns all stale jobs for this channel. pub fn get_stale_jobs(&self) -> &HashMap> { - &self.stale_jobs + self.job_store.get_stale_jobs() } + /// Returns the expected number of shares per minute for this channel. + pub fn get_shares_per_minute(&self) -> f32 { + self.expected_share_per_minute + } + + /// Returns the current chain tip, if set. pub fn get_chain_tip(&self) -> Option<&ChainTip> { self.chain_tip.as_ref() } @@ -242,6 +374,7 @@ impl<'a> StandardChannel<'a> { self.chain_tip = Some(chain_tip); } + /// Returns a reference to the share accounting state for this channel. pub fn get_share_accounting(&self) -> &ShareAccounting { &self.share_accounting } @@ -253,6 +386,10 @@ impl<'a> StandardChannel<'a> { /// /// Only meant for usage on a Sv2 Pool Server or a Sv2 Job Declaration Client, /// but not on mining clients such as Mining Devices or Proxies. + /// + /// Only meant to be used in case we want to broadcast standard jobs. + /// In case we want to broadcast extended jobs via group channel, use `on_group_channel_job` + /// instead. pub fn on_new_template( &mut self, template: NewTemplate<'a>, @@ -270,10 +407,7 @@ impl<'a> StandardChannel<'a> { coinbase_reward_outputs, ) .map_err(StandardChannelError::JobFactoryError)?; - let new_job_id = new_job.get_job_id(); - self.future_jobs.insert(new_job_id, new_job); - self.future_template_to_job_id - .insert(template.template_id, new_job_id); + self.job_store.add_future_job(template.template_id, new_job); } false => { match self.chain_tip.clone() { @@ -290,15 +424,7 @@ impl<'a> StandardChannel<'a> { coinbase_reward_outputs, ) .map_err(StandardChannelError::JobFactoryError)?; - - // if there's already some active job, move it to the past jobs - // and set the new job as the active job - if let Some(active_job) = self.active_job.take() { - self.past_jobs.insert(active_job.get_job_id(), active_job); - self.active_job = Some(new_job); - } else { - self.active_job = Some(new_job); - } + self.job_store.add_active_job(new_job); } } } @@ -307,76 +433,66 @@ impl<'a> StandardChannel<'a> { Ok(()) } + /// Used as an alternative to `on_new_template` when an extended job is meant to be broadcast + /// to the group channel, instead of multiple standard jobs to diffferent standard channels. + /// + /// We use this method to update the channel state, so it can validate share from the job that + /// was broadcasted to the group channel. + pub fn on_group_channel_job( + &mut self, + extended_job: ExtendedJob<'a>, + ) -> Result<(), StandardChannelError> { + let standard_job = extended_job + .into_standard_job(self.channel_id, self.extranonce_prefix.clone()) + .map_err(|_| StandardChannelError::FailedToConvertToStandardJob)?; + + match standard_job.is_future() { + true => { + self.job_store + .add_future_job(standard_job.get_template().template_id, standard_job); + } + false => { + self.job_store.add_active_job(standard_job); + } + } + + Ok(()) + } + /// Updates the channel state with a new `SetNewPrevHash` message. /// /// If there are no future jobs, returns an error. /// If there are future jobs, the active job is set to the job with the given `template_id`. /// /// All past jobs are cleared. - /// - /// The chain tip information is not kept in the channel state. pub fn on_set_new_prev_hash( &mut self, set_new_prev_hash: SetNewPrevHash<'a>, ) -> Result<(), StandardChannelError> { - match self.future_jobs.is_empty() { + match self.job_store.get_future_jobs().is_empty() { true => { return Err(StandardChannelError::TemplateIdNotFound); } false => { - // the SetNewPrevHash message was addressed to a specific future template - if !self - .future_template_to_job_id - .contains_key(&set_new_prev_hash.template_id) - { + if !self.job_store.activate_future_job( + set_new_prev_hash.template_id, + set_new_prev_hash.header_timestamp, + ) { return Err(StandardChannelError::TemplateIdNotFound); } - - // move currently active job to past jobs (so it can be marked as stale) - let currently_active_job = self.active_job.take(); - if let Some(active_job) = currently_active_job { - self.past_jobs.insert(active_job.get_job_id(), active_job); - } - - let future_job_id = self - .future_template_to_job_id - .remove(&set_new_prev_hash.template_id) - .expect("future job must exist"); - - // activate the future job - let mut activated_job = self - .future_jobs - .remove(&future_job_id) - .expect("future job must exist"); - - activated_job.activate(set_new_prev_hash.header_timestamp); - - self.active_job = Some(activated_job); } } - // mark all past jobs as stale, so that shares can be rejected with the appropriate error - // code - self.stale_jobs = self.past_jobs.clone(); - - // clear past jobs, as we're no longer going to validate shares for them - self.past_jobs.clear(); - // update the chain tip - let set_new_prev_hash_static = set_new_prev_hash.into_static(); - let new_chain_tip = ChainTip::new( - set_new_prev_hash_static.prev_hash, - set_new_prev_hash_static.n_bits, - set_new_prev_hash_static.header_timestamp, - ); - self.chain_tip = Some(new_chain_tip); + self.chain_tip = Some(set_new_prev_hash.into()); Ok(()) } - /// Validates a share. + /// Validates a submitted share and updates accounting state. /// - /// Updates the channel state with the result of the share validation. + /// Returns the result of share validation, including block found, valid share, duplicate, or + /// error if the share is stale or does not meet target. pub fn validate_share( &mut self, share: SubmitSharesStandard, @@ -385,15 +501,15 @@ impl<'a> StandardChannel<'a> { // check if job_id is active job let is_active_job = self - .active_job - .as_ref() + .job_store + .get_active_job() .is_some_and(|job| job.get_job_id() == job_id); // check if job_id is past job - let is_past_job = self.past_jobs.contains_key(&job_id); + let is_past_job = self.job_store.get_past_jobs().contains_key(&job_id); // check if job_id is stale job - let is_stale_job = self.stale_jobs.contains_key(&job_id); + let is_stale_job = self.job_store.get_stale_jobs().contains_key(&job_id); if is_stale_job { return Err(ShareValidationError::Stale); @@ -405,11 +521,19 @@ impl<'a> StandardChannel<'a> { } let job = if is_active_job { - self.active_job.as_ref().expect("active job must exist") + self.job_store + .get_active_job() + .expect("active job must exist") } else if is_past_job { - self.past_jobs.get(&job_id).expect("past job must exist") + self.job_store + .get_past_jobs() + .get(&job_id) + .expect("past job must exist") } else { - self.stale_jobs.get(&job_id).expect("stale job must exist") + self.job_store + .get_stale_jobs() + .get(&job_id) + .expect("stale job must exist") }; let merkle_root: [u8; 32] = job @@ -466,7 +590,14 @@ impl<'a> StandardChannel<'a> { hash.to_raw_hash(), ); + let op_pushbytes_pool_miner_tag = self + .job_factory + .op_pushbytes_pool_miner_tag() + .map_err(|_| ShareValidationError::InvalidCoinbase)?; + let mut script_sig = job.get_template().coinbase_prefix.to_vec(); + script_sig.extend(op_pushbytes_pool_miner_tag); + script_sig.push(MAX_EXTRANONCE_LEN as u8); // OP_PUSHBYTES_32 (for the extranonce) script_sig.extend(job.get_extranonce_prefix()); let tx_in = TxIn { @@ -531,18 +662,19 @@ impl<'a> StandardChannel<'a> { #[cfg(test)] mod tests { - use crate::channels::{ + use crate::{ chain_tip::ChainTip, server::{ error::StandardChannelError, + jobs::{job_store::DefaultJobStore, standard::StandardJob}, share_accounting::{ShareValidationError, ShareValidationResult}, standard::StandardChannel, }, }; use binary_sv2::Sv2Option; + use bitcoin::{transaction::TxOut, Amount, ScriptBuf}; use mining_sv2::{NewMiningJob, SubmitSharesStandard, Target}; use std::convert::TryInto; - use stratum_common::bitcoin::{transaction::TxOut, Amount, ScriptBuf}; use template_distribution_sv2::{NewTemplate, SetNewPrevHash as SetNewPrevHashTdp}; const SATS_AVAILABLE_IN_TEMPLATE: u64 = 5000000000; @@ -565,6 +697,7 @@ mod tests { let nominal_hashrate = 10.0; let share_batch_size = 100; let expected_share_per_minute = 1.0; + let job_store = DefaultJobStore::::new(); let mut standard_channel = StandardChannel::new( standard_channel_id, @@ -574,6 +707,9 @@ mod tests { nominal_hashrate, share_batch_size, expected_share_per_minute, + job_store, + None, + None, ) .unwrap(); @@ -622,8 +758,8 @@ mod tests { channel_id: standard_channel_id, job_id: 1, merkle_root: [ - 189, 200, 25, 246, 119, 73, 34, 42, 209, 112, 237, 50, 169, 71, 163, 192, 24, 84, - 56, 86, 147, 71, 243, 44, 18, 107, 167, 169, 169, 66, 186, 98, + 213, 241, 108, 144, 69, 96, 29, 8, 222, 2, 135, 14, 213, 87, 81, 21, 140, 98, 42, + 221, 221, 174, 219, 248, 106, 52, 168, 88, 18, 146, 186, 71, ] .into(), version: 536870912, @@ -689,6 +825,8 @@ mod tests { let share_batch_size = 100; let expected_share_per_minute = 1.0; + let job_store = DefaultJobStore::::new(); + let mut standard_channel = StandardChannel::new( standard_channel_id, user_identity, @@ -697,6 +835,9 @@ mod tests { nominal_hashrate, share_batch_size, expected_share_per_minute, + job_store, + None, + None, ) .unwrap(); @@ -753,8 +894,8 @@ mod tests { channel_id: standard_channel_id, job_id: 1, merkle_root: [ - 189, 200, 25, 246, 119, 73, 34, 42, 209, 112, 237, 50, 169, 71, 163, 192, 24, 84, - 56, 86, 147, 71, 243, 44, 18, 107, 167, 169, 169, 66, 186, 98, + 213, 241, 108, 144, 69, 96, 29, 8, 222, 2, 135, 14, 213, 87, 81, 21, 140, 98, 42, + 221, 221, 174, 219, 248, 106, 52, 168, 88, 18, 146, 186, 71, ] .into(), version: 536870912, @@ -788,6 +929,8 @@ mod tests { let share_batch_size = 100; let expected_share_per_minute = 1.0; + let job_store = DefaultJobStore::::new(); + let mut standard_channel = StandardChannel::new( standard_channel_id, user_identity, @@ -796,6 +939,9 @@ mod tests { nominal_hashrate, share_batch_size, expected_share_per_minute, + job_store, + None, + None, ) .unwrap(); @@ -853,14 +999,14 @@ mod tests { let active_standard_job = standard_channel.get_active_job().unwrap(); - // this share has hash 40b4c57b2c65052bbe1092e556146ad78cdd9e5ffaeff856a0eb54ee7b816da7 + // this share has hash 3c34f63de61283c907b68e3127146d7d11f1fb14e50020a8317a292d11e2dab6 // which satisfied the network target // 7fffff0000000000000000000000000000000000000000000000000000000000 let share_valid_block = SubmitSharesStandard { channel_id: standard_channel_id, sequence_number: 0, job_id: active_standard_job.get_job_id(), - nonce: 3, + nonce: 0, ntime: 1745596932, version: 536870912, }; @@ -889,6 +1035,8 @@ mod tests { let share_batch_size = 100; let expected_share_per_minute = 1.0; + let job_store = DefaultJobStore::::new(); + let mut standard_channel = StandardChannel::new( standard_channel_id, user_identity, @@ -897,6 +1045,9 @@ mod tests { nominal_hashrate, share_batch_size, expected_share_per_minute, + job_store, + None, + None, ) .unwrap(); @@ -993,6 +1144,8 @@ mod tests { let share_batch_size = 100; let expected_share_per_minute = 1.0; + let job_store = DefaultJobStore::::new(); + let mut standard_channel = StandardChannel::new( standard_channel_id, user_identity, @@ -1001,6 +1154,9 @@ mod tests { nominal_hashrate, share_batch_size, expected_share_per_minute, + job_store, + None, + None, ) .unwrap(); @@ -1058,7 +1214,7 @@ mod tests { .on_new_template(template.clone(), coinbase_reward_outputs) .unwrap(); - // this share has hash 000010dcb838b589e5b0365350425ea82f368d330616f783d32dadf9b497bd02 + // this share has hash 0000d603073772ba60af5922486242a6adb74cdf5baec768c7bd684977852cd8 // which does meet the channel target // 0001179d9861a761ffdadd11c307c4fc04eea3a418f7d687584e4434af158205 // but does not meet network target @@ -1067,7 +1223,7 @@ mod tests { channel_id: standard_channel_id, sequence_number: 1, job_id: 1, - nonce: 31978, + nonce: 134870, ntime: 1745611105, version: 536870912, }; @@ -1088,7 +1244,7 @@ mod tests { let expected_share_per_minute = 1.0; let initial_hashrate = 10.0; let share_batch_size = 100; - + let job_store = DefaultJobStore::::new(); // this is the most permissive possible max_target let max_target: Target = [0xff; 32].into(); @@ -1101,6 +1257,9 @@ mod tests { initial_hashrate, share_batch_size, expected_share_per_minute, + job_store, + None, + None, ) .unwrap(); @@ -1178,6 +1337,7 @@ mod tests { let expected_share_per_minute = 1.0; let nominal_hashrate = 1_000.0; let share_batch_size = 100; + let job_store = DefaultJobStore::::new(); let mut channel = StandardChannel::new( channel_id, @@ -1187,6 +1347,9 @@ mod tests { nominal_hashrate, share_batch_size, expected_share_per_minute, + job_store, + None, + None, ) .unwrap(); diff --git a/protocols/v2/channels-sv2/src/target.rs b/protocols/v2/channels-sv2/src/target.rs new file mode 100644 index 0000000000..305b25c530 --- /dev/null +++ b/protocols/v2/channels-sv2/src/target.rs @@ -0,0 +1,234 @@ +//! Helper functions related to [`Target`] + +extern crate alloc; +use alloc::string::String; +use binary_sv2::U256; +use bitcoin::{hash_types::BlockHash, hashes::Hash}; +use core::{cmp::max, fmt::Write, ops::Div}; +use mining_sv2::Target; +use primitive_types::U256 as U256Primitive; +/// Converts a `Target` to a `f64` difficulty. +pub fn target_to_difficulty(target: Target) -> f64 { + // Genesis block target: 0x00000000ffff0000000000000000000000000000000000000000000000000000 + // (in little endian) + let max_target_bytes = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, + 0x00, 0x00, + ]; + let max_target = U256Primitive::from_little_endian(&max_target_bytes); + + // Convert input target to U256Primitive + let target_u256: U256<'static> = target.into(); + let mut target_bytes = [0u8; 32]; + target_bytes.copy_from_slice(target_u256.inner_as_ref()); + let target = U256Primitive::from_little_endian(&target_bytes); + + // Calculate difficulty = max_target / target + // We need to handle the full 256-bit values properly + // Convert to f64 by taking the ratio of the most significant bits + let max_target_high = (max_target >> 128).low_u128() as f64; + let max_target_low = max_target.low_u128() as f64; + let target_high = (target >> 128).low_u128() as f64; + let target_low = target.low_u128() as f64; + + // Combine high and low parts with appropriate scaling + let max_target_f64 = max_target_high * (2.0f64.powi(128)) + max_target_low; + let target_f64 = target_high * (2.0f64.powi(128)) + target_low; + + max_target_f64 / target_f64 +} + +/// Converts a `u256` to a [`BlockHash`] type. +pub fn u256_to_block_hash(v: U256<'static>) -> BlockHash { + let hash: [u8; 32] = v.to_vec().try_into().unwrap(); + let hash = Hash::from_slice(&hash).unwrap(); + BlockHash::from_raw_hash(hash) +} + +/// Helper function to format bytes as hex string +/// useful for visualizing targets +pub fn bytes_to_hex(bytes: &[u8]) -> String { + let mut s = String::with_capacity(bytes.len() * 2); + for &b in bytes { + write!(&mut s, "{b:02x}") + .expect("Writing hex bytes to pre-allocated string should never fail"); + } + s +} + +/// Calculates the mining target threshold for a mining device based on its hashrate (H/s) and +/// desired share frequency (shares/min). +/// +/// Determines the maximum hash value (target), in big endian, that a mining device can produce to +/// find a valid share. The target is derived from the miner's hashrate and the expected number of +/// shares per minute, aligning the miner's workload with the upstream's (e.g. pool's) share +/// frequency requirements. +/// +/// Typically used during connection setup to assign a starting target based on the mining device's +/// reported hashrate and to recalculate during runtime when a mining device's hashrate changes, +/// ensuring they submit shares at the desired rate. +/// +/// ## Formula +/// ```text +/// t = (2^256 - sh) / (sh + 1) +/// ``` +/// +/// Where: +/// - `h`: Mining device hashrate (H/s). +/// - `s`: Shares per second `60 / shares/min` (s). +/// - `sh`: `h * s`, the mining device's work over `s` seconds. +/// +/// According to \[1] and \[2], it is possible to model the probability of finding a block with +/// a random variable X whose distribution is negative hypergeometric \[3]. Such a variable is +/// characterized as follows: +/// +/// Say that there are `n` (`2^256`) elements (possible hash values), of which `t` (values <= +/// target) are defined as success and the remaining as failures. The variable `X` has co-domain +/// the positive integers, and `X=k` is the event where element are drawn one after the other, +/// without replacement, and only the `k`th element is successful. The expected value of this +/// variable is `(n-t)/(t+1)`. So, on average, a miner has to perform `(2^256-t)/(t+1)` hashes +/// before finding hash whose value is below the target `t`. +/// +/// If the pool wants, on average, a share every `s` seconds, then, on average, the miner has to +/// perform `h*s` hashes before finding one that is smaller than the target, where `h` is the +/// miner's hashrate. Therefore, `s*h= (2^256-t)/(t+1)`. If we consider `h` the global Bitcoin's +/// hashrate, `s = 600` seconds and `t` the Bitcoin global target, then, for all the blocks we +/// tried, the two members of the equations have the same order of magnitude and, most of the +/// cases, they coincide with the first two digits. +/// +/// We take this as evidence of the correctness of our calculations. Thus, if the pool wants on +/// average a share every `s` seconds from a miner with hashrate `h`, then the target `t` for the +/// miner is `t = (2^256-sh)/(sh+1)`. +/// +/// \[1] [https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3399742](https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3399742) +/// +/// \[2] [https://www.zora.uzh.ch/id/eprint/173483/1/SSRN-id3399742-2.pdf](https://www.zora.uzh.ch/id/eprint/173483/1/SSRN-id3399742-2.pdf) +/// +/// \[3] [https://en.wikipedia.org/wiki/Negative_hypergeometric_distribution](https://en.wikipedia.org/wiki/Negative_hypergeometric_distribution) +pub fn hash_rate_to_target( + hashrate: f64, + share_per_min: f64, +) -> Result, HashRateToTargetError> { + // checks that we are not dividing by zero + if share_per_min == 0.0 { + return Err(HashRateToTargetError::DivisionByZero); + } + if share_per_min.is_sign_negative() { + return Err(HashRateToTargetError::NegativeInput); + }; + if hashrate.is_sign_negative() { + return Err(HashRateToTargetError::NegativeInput); + }; + + // if we want 5 shares per minute, this means that s=60/5=12 seconds interval between shares + // this quantity will be at the numerator, so we multiply the result by 100 again later + let shares_occurrency_frequence = 60_f64 / share_per_min; + + let h_times_s = hashrate * shares_occurrency_frequence; + let h_times_s = h_times_s as u128; + + // We calculate the denominator: h*s+1 + // the denominator is h*s+1, where h*s is an u128, so always positive. + // this means that the denominator can never be zero + // we add 100 in place of 1 because h*s is actually h*s*100, we in order to simplify later we + // must calculate (h*s+1)*100 + let h_times_s_plus_one = max(h_times_s, h_times_s + 1); + + let h_times_s_plus_one = from_u128_to_u256(h_times_s_plus_one); + let denominator = h_times_s_plus_one; + + // We calculate the numerator: 2^256-sh + let two_to_256_minus_one = [255_u8; 32]; + let two_to_256_minus_one = U256Primitive::from_big_endian(two_to_256_minus_one.as_ref()); + + let mut h_times_s_array = [0u8; 32]; + h_times_s_array[16..].copy_from_slice(&h_times_s.to_be_bytes()); + let numerator = two_to_256_minus_one - U256Primitive::from_big_endian(h_times_s_array.as_ref()); + + let mut target = numerator.div(denominator).to_big_endian(); + target.reverse(); + Ok(U256::<'static>::from(target)) +} + +/// Converts a `u128` to a [`U256`]. +pub fn from_u128_to_u256(input: u128) -> U256Primitive { + let input: [u8; 16] = input.to_be_bytes(); + let mut be_bytes = [0_u8; 32]; + for (i, b) in input.iter().enumerate() { + be_bytes[16 + i] = *b; + } + U256Primitive::from_big_endian(be_bytes.as_ref()) +} + +#[derive(Debug)] +pub enum HashRateToTargetError { + DivisionByZero, + NegativeInput, +} + +#[derive(Debug)] +pub enum InputError { + NegativeInput, + DivisionByZero, + ArithmeticOverflow, +} + +/// Calculates the hashrate (H/s) required to produce a specific number of shares per minute for a +/// given mining target (big endian). +/// +/// It is the inverse of [`hash_rate_to_target`], enabling backward calculations to estimate a +/// mining device's performance from its submitted shares. +/// +/// Typically used to calculate the mining device's effective hashrate during runtime based on the +/// submitted shares and the assigned target, also helps detect changes in miner performance and +/// recalibrate the target (using [`hash_rate_to_target`]) if necessary. +/// +/// ## Formula +/// ```text +/// h = (2^256 - t) / (s * (t + 1)) +/// ``` +/// +/// Where: +/// - `h`: Mining device hashrate (H/s). +/// - `t`: Target threshold. +/// - `s`: Shares per minute. +pub fn hash_rate_from_target(target: U256<'static>, share_per_min: f64) -> Result { + // checks that we are not dividing by zero + if share_per_min == 0.0 { + return Err(InputError::DivisionByZero); + } + if share_per_min.is_sign_negative() { + return Err(InputError::NegativeInput); + } + let mut target_arr: [u8; 32] = [0; 32]; + let slice: &mut [u8] = &mut target_arr; + slice.copy_from_slice(target.inner_as_ref()); + target_arr.reverse(); + let target = U256Primitive::from_big_endian(target_arr.as_ref()); + // we calculate the numerator 2^256-t + // note that [255_u8,;32] actually is 2^256 -1, but 2^256 -t = (2^256-1) - (t-1) + let max_target = [255_u8; 32]; + let max_target = U256Primitive::from_big_endian(max_target.as_ref()); + let numerator = max_target - (target - U256Primitive::one()); + // now we calculate the denominator s(t+1) + // *100 here to move the fractional bit up so we can make this an int later + let shares_occurrency_frequence = 60_f64 / (share_per_min) * 100.0; + // note that t+1 cannot be zero because t unsigned. Therefore the denominator is zero if and + // only if s is zero. + let shares_occurrency_frequence = shares_occurrency_frequence as u128; + if shares_occurrency_frequence == 0_u128 { + return Err(InputError::DivisionByZero); + } + let shares_occurrency_frequence = from_u128_to_u256(shares_occurrency_frequence); + let target_plus_one = U256Primitive::from_big_endian(target_arr.as_ref()) + .checked_add(U256Primitive::one()) + .ok_or(InputError::ArithmeticOverflow)?; + let denominator = target_plus_one + .checked_mul(shares_occurrency_frequence) + .and_then(|e| e.checked_div(U256Primitive::from(100))) + .ok_or(InputError::ArithmeticOverflow)?; + let result = numerator.div(denominator).low_u128(); + // we multiply back by 100 so that it cancels with the same factor at the denominator + Ok(result as f64) +} diff --git a/protocols/v2/roles-logic-sv2/src/vardiff/classic.rs b/protocols/v2/channels-sv2/src/vardiff/classic.rs similarity index 98% rename from protocols/v2/roles-logic-sv2/src/vardiff/classic.rs rename to protocols/v2/channels-sv2/src/vardiff/classic.rs index 86cbdbcd9d..587be5cf2c 100644 --- a/protocols/v2/roles-logic-sv2/src/vardiff/classic.rs +++ b/protocols/v2/channels-sv2/src/vardiff/classic.rs @@ -1,6 +1,6 @@ -use crate::utils::hash_rate_from_target; +use crate::target::hash_rate_from_target; use mining_sv2::Target; -use tracing::{debug, warn}; +use tracing::debug; /// Default minimum hashrate (H/s) if not specified. const DEFAULT_MIN_HASHRATE: f32 = 1.0; @@ -132,7 +132,7 @@ impl Vardiff for VardiffState { ) { Ok(hashrate) => hashrate as f32, Err(e) => { - warn!( + debug!( target: "vardiff", "Target->Hashrate conversion failed: {:?}. Falling back using previous hashrate and realized_shares_per_minute", e ); diff --git a/protocols/v2/roles-logic-sv2/src/vardiff/error.rs b/protocols/v2/channels-sv2/src/vardiff/error.rs similarity index 100% rename from protocols/v2/roles-logic-sv2/src/vardiff/error.rs rename to protocols/v2/channels-sv2/src/vardiff/error.rs diff --git a/protocols/v2/roles-logic-sv2/src/vardiff/mod.rs b/protocols/v2/channels-sv2/src/vardiff/mod.rs similarity index 96% rename from protocols/v2/roles-logic-sv2/src/vardiff/mod.rs rename to protocols/v2/channels-sv2/src/vardiff/mod.rs index e5976c6dfe..ff8a9e6682 100644 --- a/protocols/v2/roles-logic-sv2/src/vardiff/mod.rs +++ b/protocols/v2/channels-sv2/src/vardiff/mod.rs @@ -8,7 +8,7 @@ pub mod error; pub mod test; /// Trait defining the interface for a Vardiff implementation. -pub trait Vardiff: Debug + Send { +pub trait Vardiff: Debug + Send + Sync { /// Gets the timestamp of the last update. fn last_update_timestamp(&self) -> u64; diff --git a/protocols/v2/roles-logic-sv2/src/vardiff/test/classic.rs b/protocols/v2/channels-sv2/src/vardiff/test/classic.rs similarity index 98% rename from protocols/v2/roles-logic-sv2/src/vardiff/test/classic.rs rename to protocols/v2/channels-sv2/src/vardiff/test/classic.rs index eab55aabcd..8aa7689c32 100644 --- a/protocols/v2/roles-logic-sv2/src/vardiff/test/classic.rs +++ b/protocols/v2/channels-sv2/src/vardiff/test/classic.rs @@ -2,7 +2,7 @@ use crate::vardiff::test::{ simulate_shares_and_wait, TEST_MIN_ALLOWED_HASHRATE, TEST_SHARES_PER_MINUTE, }; -use crate::{utils::hash_rate_to_target, vardiff::VardiffError, VardiffState}; +use crate::{target::hash_rate_to_target, vardiff::VardiffError, VardiffState}; use super::{ test_increment_and_reset_shares, test_try_vardiff_low_hashrate_decrease_target, diff --git a/protocols/v2/roles-logic-sv2/src/vardiff/test/mod.rs b/protocols/v2/channels-sv2/src/vardiff/test/mod.rs similarity index 98% rename from protocols/v2/roles-logic-sv2/src/vardiff/test/mod.rs rename to protocols/v2/channels-sv2/src/vardiff/test/mod.rs index 8ea22f02bd..eb79b5f918 100644 --- a/protocols/v2/roles-logic-sv2/src/vardiff/test/mod.rs +++ b/protocols/v2/channels-sv2/src/vardiff/test/mod.rs @@ -5,7 +5,7 @@ use std::{thread, time::Duration}; mod classic; use super::Vardiff; -use crate::utils::hash_rate_to_target; +use crate::target::hash_rate_to_target; use mining_sv2::Target; pub const TEST_INITIAL_HASHRATE: f32 = 1000.0; @@ -79,8 +79,7 @@ pub fn test_try_vardiff_stable_hashrate_minimal_change_or_no_change( if let Some(new_hashrate) = result { let diff_percentage = ((new_hashrate - initial_hashrate).abs() / initial_hashrate) * 100.0; println!( - "Stable hashrate test: new hashrate {}, initial {}, diff_pct {}", - new_hashrate, initial_hashrate, diff_percentage + "Stable hashrate test: new hashrate {new_hashrate}, initial {initial_hashrate}, diff_pct {diff_percentage}" ); assert!( diff_percentage < 20.0, @@ -119,7 +118,7 @@ pub fn test_try_vardiff_low_hashrate_decrease_target(vardiff: &mut V let target: Target = hash_rate_to_target(new_hashrate.into(), TEST_SHARES_PER_MINUTE.into()) .unwrap() .into(); - println!("target: {:?}", target); + println!("target: {target:?}"); assert!( target < initial_target, "Target should become harder (larger value)" diff --git a/protocols/v2/codec-sv2/Cargo.toml b/protocols/v2/codec-sv2/Cargo.toml index 5e5108fbb4..8e55188ced 100644 --- a/protocols/v2/codec-sv2/Cargo.toml +++ b/protocols/v2/codec-sv2/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "codec_sv2" -version = "2.1.0" +version = "3.0.1" authors = ["The Stratum V2 Developers"] -edition = "2018" +edition = "2021" readme = "README.md" description = "Sv2 data format" documentation = "https://docs.rs/codec_sv2" @@ -14,8 +14,7 @@ keywords = ["stratum", "mining", "bitcoin", "protocol"] [dependencies] framing_sv2 = { path = "../../../protocols/v2/framing-sv2", version = "^5.0.0" } noise_sv2 = { path = "../../../protocols/v2/noise-sv2", default-features = false, optional = true, version = "^1.0.0" } -binary_sv2 = { path = "../../../protocols/v2/binary-sv2", version = "^3.0.0" } -stratum-common = { version = "2.0.0", path = "../../../common" } +binary_sv2 = { path = "../../../protocols/v2/binary-sv2", version = "^4.0.0" } buffer_sv2 = { path = "../../../utils/buffer", version = "^2.0.0" } rand = { version = "0.8.5", default-features = false } tracing = { version = "0.1", optional = true } diff --git a/protocols/v2/codec-sv2/examples/encrypted.rs b/protocols/v2/codec-sv2/examples/encrypted.rs index f006d5d2bc..a28898796f 100644 --- a/protocols/v2/codec-sv2/examples/encrypted.rs +++ b/protocols/v2/codec-sv2/examples/encrypted.rs @@ -24,14 +24,16 @@ use codec_sv2::{ }; #[cfg(feature = "noise_sv2")] use key_utils::{Secp256k1PublicKey, Secp256k1SecretKey}; + +#[cfg(feature = "noise_sv2")] +use noise_sv2::{ELLSWIFT_ENCODING_SIZE, INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_SIZE}; + use std::convert::TryInto; #[cfg(feature = "noise_sv2")] use std::{ io::{Read, Write}, net::{TcpListener, TcpStream}, }; -#[cfg(feature = "noise_sv2")] -use stratum_common::{ELLSWIFT_ENCODING_SIZE, INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_SIZE}; // Arbitrary message type. // Supported Sv2 message types are listed in the [Sv2 Spec Message diff --git a/protocols/v2/codec-sv2/src/decoder.rs b/protocols/v2/codec-sv2/src/decoder.rs index 8498f02075..1675c77983 100644 --- a/protocols/v2/codec-sv2/src/decoder.rs +++ b/protocols/v2/codec-sv2/src/decoder.rs @@ -184,6 +184,19 @@ impl<'a, T: Serialize + GetSize + Deserialize<'a>, B: IsBuffer + AeadBuffer> Wit } } + /// Returns the number of bytes expected in the next read operation. + /// + /// This value indicates how many more bytes are required to complete the + /// current Noise-encrypted frame. It is used to determine the exact size + /// of the writable buffer that should be passed to the underlying stream + /// during reading. + /// + /// The returned length dynamically updates as data is received and processed, + /// and ensures that we only read as much as needed to complete the frame. + pub fn writable_len(&self) -> usize { + self.missing_noise_b + } + /// Provides a writable buffer for receiving incoming Noise-encrypted Sv2 data. /// /// This buffer is used to store incoming data, and its size is adjusted based on the number diff --git a/protocols/v2/codec-sv2/src/error.rs b/protocols/v2/codec-sv2/src/error.rs index 60137647e5..17f518d09a 100644 --- a/protocols/v2/codec-sv2/src/error.rs +++ b/protocols/v2/codec-sv2/src/error.rs @@ -1,8 +1,8 @@ //! # Error Handling and Result Types //! //! This module defines error types and utilities for handling errors in the `codec_sv2` module. -//! It includes the [`Error`] enum for representing various errors, a C-compatible [`CError`] enum -//! for FFI, and a `Result` type alias for convenience. +//! It includes the [`Error`] enum for representing various errors and a `Result` type alias for +//! convenience. use core::fmt; use framing_sv2::Error as FramingError; @@ -62,10 +62,10 @@ impl fmt::Display for Error { use Error::*; match self { #[cfg(feature = "noise_sv2")] - AeadError(e) => write!(f, "Aead Error: `{:?}`", e), - BinarySv2Error(e) => write!(f, "Binary Sv2 Error: `{:?}`", e), - FramingError(e) => write!(f, "Framing error in codec: `{:?}`", e), - FramingSv2Error(e) => write!(f, "Framing Sv2 Error: `{:?}`", e), + AeadError(e) => write!(f, "Aead Error: `{e:?}`"), + BinarySv2Error(e) => write!(f, "Binary Sv2 Error: `{e:?}`"), + FramingError(e) => write!(f, "Framing error in codec: `{e:?}`"), + FramingSv2Error(e) => write!(f, "Framing Sv2 Error: `{e:?}`"), #[cfg(feature = "noise_sv2")] InvalidStepForInitiator => write!( f, @@ -76,9 +76,9 @@ impl fmt::Display for Error { f, "This noise handshake step can not be executed by a responder" ), - MissingBytes(u) => write!(f, "Missing `{}` Noise bytes", u), + MissingBytes(u) => write!(f, "Missing `{u}` Noise bytes"), #[cfg(feature = "noise_sv2")] - NoiseSv2Error(e) => write!(f, "Noise SV2 Error: `{:?}`", e), + NoiseSv2Error(e) => write!(f, "Noise SV2 Error: `{e:?}`"), #[cfg(feature = "noise_sv2")] NotInHandShakeState => write!( f, @@ -116,90 +116,3 @@ impl From for Error { Error::NoiseSv2Error(e) } } - -/// C-compatible enumeration of possible errors in the `codec_sv2` module. -/// -/// This enum mirrors the [`Error`] enum but is designed to be used in C code through FFI. It -/// represents the same set of errors as [`Error`], making them accessible to C programs. -#[repr(C)] -#[derive(Debug)] -pub enum CError { - /// AEAD (`snow`) error in the Noise protocol. - AeadError, - - /// Binary Sv2 data format error. - BinarySv2Error, - - /// Framing Sv2 error. - FramingError, - - /// Framing Sv2 error. - FramingSv2Error, - - /// Invalid step for initiator in the Noise protocol. - InvalidStepForInitiator, - - /// Invalid step for responder in the Noise protocol. - InvalidStepForResponder, - - /// Missing bytes in the Noise protocol. - MissingBytes(usize), - - /// Sv2 Noise protocol error. - NoiseSv2Error, - - /// Noise protocol is not in the expected handshake state. - NotInHandShakeState, - - /// Unexpected state in the Noise protocol. - UnexpectedNoiseState, -} - -/// Force `cbindgen` to create a header for [`CError`]. -/// -/// It ensures that [`CError`] is included in the generated C header file. This function is not -/// meant to be called and will panic if called. Its only purpose is to make [`CError`] visible to -/// `cbindgen`. -#[no_mangle] -pub extern "C" fn export_cerror() -> CError { - unimplemented!() -} - -impl From for CError { - fn from(e: Error) -> CError { - match e { - #[cfg(feature = "noise_sv2")] - Error::AeadError(_) => CError::AeadError, - Error::BinarySv2Error(_) => CError::BinarySv2Error, - Error::FramingSv2Error(_) => CError::FramingSv2Error, - Error::FramingError(_) => CError::FramingError, - #[cfg(feature = "noise_sv2")] - Error::InvalidStepForInitiator => CError::InvalidStepForInitiator, - #[cfg(feature = "noise_sv2")] - Error::InvalidStepForResponder => CError::InvalidStepForResponder, - Error::MissingBytes(u) => CError::MissingBytes(u), - #[cfg(feature = "noise_sv2")] - Error::NoiseSv2Error(_) => CError::NoiseSv2Error, - #[cfg(feature = "noise_sv2")] - Error::NotInHandShakeState => CError::NotInHandShakeState, - Error::UnexpectedNoiseState => CError::UnexpectedNoiseState, - } - } -} - -impl Drop for CError { - fn drop(&mut self) { - match self { - CError::AeadError => (), - CError::BinarySv2Error => (), - CError::FramingError => (), - CError::FramingSv2Error => (), - CError::InvalidStepForInitiator => (), - CError::InvalidStepForResponder => (), - CError::MissingBytes(_) => (), - CError::NoiseSv2Error => (), - CError::NotInHandShakeState => (), - CError::UnexpectedNoiseState => (), - }; - } -} diff --git a/protocols/v2/codec-sv2/src/lib.rs b/protocols/v2/codec-sv2/src/lib.rs index a4a1e995a1..37fe3f27ea 100644 --- a/protocols/v2/codec-sv2/src/lib.rs +++ b/protocols/v2/codec-sv2/src/lib.rs @@ -46,7 +46,7 @@ mod decoder; mod encoder; pub mod error; -pub use error::{CError, Error, Result}; +pub use error::{Error, Result}; pub use decoder::{StandardEitherFrame, StandardSv2Frame}; @@ -67,6 +67,8 @@ pub use noise_sv2::{self, Initiator, NoiseCodec, Responder}; pub use buffer_sv2; +pub use binary_sv2; + pub use framing_sv2::{self, framing::handshake_message_to_frame as h2f}; /// Represents the role in the Noise handshake process, either as an initiator or a responder. @@ -79,7 +81,7 @@ pub use framing_sv2::{self, framing::handshake_message_to_frame as h2f}; /// process accordingly. #[allow(clippy::large_enum_variant)] #[cfg(feature = "noise_sv2")] -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum HandshakeRole { /// The initiator role in the Noise handshake process. /// @@ -103,7 +105,7 @@ pub enum HandshakeRole { /// [`State::HandShake`] and finally to transport mode [`State::Transport`] as the encryption /// handshake is completed. #[cfg(feature = "noise_sv2")] -#[derive(Debug)] +#[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] pub enum State { /// The codec has not been initialized yet. diff --git a/protocols/v2/framing-sv2/Cargo.toml b/protocols/v2/framing-sv2/Cargo.toml index c5af6f2cfe..003c4be089 100644 --- a/protocols/v2/framing-sv2/Cargo.toml +++ b/protocols/v2/framing-sv2/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "framing_sv2" -version = "5.1.0" +version = "5.0.1" authors = ["The Stratum V2 Developers"] -edition = "2018" +edition = "2021" readme = "README.md" description = "Sv2 frames" documentation = "https://docs.rs/framing_sv2" @@ -14,8 +14,7 @@ keywords = ["stratum", "mining", "bitcoin", "protocol"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -stratum-common = { path = "../../../common", version = "^2.0.0" } -binary_sv2 = { path = "../../../protocols/v2/binary-sv2", version = "^3.0.0" } +binary_sv2 = { path = "../../../protocols/v2/binary-sv2", version = "^4.0.0" } buffer_sv2 = { path = "../../../utils/buffer", optional=true, version = "^2.0.0" } noise_sv2 = { path = "../../../protocols/v2/noise-sv2", version = "^1.0.0" } diff --git a/protocols/v2/framing-sv2/src/error.rs b/protocols/v2/framing-sv2/src/error.rs index bf6941da50..557af6bb06 100644 --- a/protocols/v2/framing-sv2/src/error.rs +++ b/protocols/v2/framing-sv2/src/error.rs @@ -23,7 +23,7 @@ impl fmt::Display for Error { use Error::*; match self { BinarySv2Error(ref e) => { - write!(f, "BinarySv2Error: `{:?}`", e) + write!(f, "BinarySv2Error: `{e:?}`") } ExpectedHandshakeFrame => { write!(f, "Expected `HandshakeFrame`, received `Sv2Frame`") @@ -34,8 +34,7 @@ impl fmt::Display for Error { UnexpectedHeaderLength(actual_size) => { write!( f, - "Unexpected `Header` length: `{}`, should be equal or more to {}", - actual_size, SV2_FRAME_HEADER_SIZE + "Unexpected `Header` length: `{actual_size}`, should be equal or more to {SV2_FRAME_HEADER_SIZE}" ) } } diff --git a/protocols/v2/handlers-sv2/Cargo.toml b/protocols/v2/handlers-sv2/Cargo.toml new file mode 100644 index 0000000000..111ea64d6f --- /dev/null +++ b/protocols/v2/handlers-sv2/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "handlers_sv2" +version = "0.2.0" +authors = ["The Stratum V2 Developers"] +edition = "2021" +readme = "README.md" +description = "Sv2 Message handlers" +documentation = "https://docs.rs/handlers_sv2" +license = "MIT OR Apache-2.0" +repository = "https://github.com/stratum-mining/stratum" +homepage = "https://stratumprotocol.org" +keywords = ["stratum", "mining", "bitcoin", "protocol"] + +[dependencies] +trait-variant = "0.1.2" +parsers_sv2 = { path = "../parsers-sv2", version = "^0.1.0"} +binary_sv2 = { path = "../binary-sv2", version = "^4.0.0" } +common_messages_sv2 = { path = "../subprotocols/common-messages", version = "^6.0.0" } +mining_sv2 = { path = "../subprotocols/mining", version = "^5.0.0" } +template_distribution_sv2 = { path = "../subprotocols/template-distribution", version = "^4.0.0" } +job_declaration_sv2 = { path = "../subprotocols/job-declaration", version = "^5.0.0" } diff --git a/protocols/v2/handlers-sv2/README.md b/protocols/v2/handlers-sv2/README.md new file mode 100644 index 0000000000..285a6baa01 --- /dev/null +++ b/protocols/v2/handlers-sv2/README.md @@ -0,0 +1,17 @@ + +# handlers_sv2 + +[![crates.io](https://img.shields.io/crates/v/handlers_sv2.svg)](https://crates.io/crates/handlers_sv2) +[![docs.rs](https://docs.rs/handlers_sv2/badge.svg)](https://docs.rs/handlers_sv2) +[![rustc+](https://img.shields.io/badge/rustc-1.75.0%2B-lightgrey.svg)](https://blog.rust-lang.org/2023/12/28/Rust-1.75.0.html) +[![license](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](https://github.com/stratum-mining/stratum/blob/main/LICENSE.md) +[![codecov](https://codecov.io/gh/stratum-mining/stratum/branch/main/graph/badge.svg?flag=handlers_sv2-coverage)](https://codecov.io/gh/stratum-mining/stratum) + +The `handlers_sv2` crate defines traits for handling Sv2 messages, with separate variants for servers and clients. Implementors can choose which message types to support—such as `Mining`, `TemplateDistribution`, `Common`, or `JobDeclaration` based on their role in the system. Both synchronous and asynchronous versions are provided, making the crate adaptable to different execution environments. + +## Usage +To include this crate in your project, run: + +```bash +cargo add handlers_sv2 +``` \ No newline at end of file diff --git a/protocols/v2/handlers-sv2/src/common.rs b/protocols/v2/handlers-sv2/src/common.rs new file mode 100644 index 0000000000..fa183e1d95 --- /dev/null +++ b/protocols/v2/handlers-sv2/src/common.rs @@ -0,0 +1,256 @@ +use common_messages_sv2::{ + ChannelEndpointChanged, Reconnect, SetupConnectionError, SetupConnectionSuccess, *, +}; +use core::convert::TryInto; +use parsers_sv2::CommonMessages; + +use crate::error::HandlerErrorType; + +/// Synchronous handler trait for processing common messages received from servers. +/// +/// The server ID identifies which server a message originated from. +/// Whether this is relevant or not depends on which object is implementing the trait, and whether +/// this contextual information is readily available or not. In cases where `server_id` is either +/// irrelevant or can be inferred without the context, this should always be `None`. +pub trait HandleCommonMessagesFromServerSync { + type Error: HandlerErrorType; + fn handle_common_message_frame_from_server( + &mut self, + server_id: Option, + message_type: u8, + payload: &mut [u8], + ) -> Result<(), Self::Error> { + let parsed: CommonMessages<'_> = (message_type, payload) + .try_into() + .map_err(Self::Error::parse_error)?; + self.handle_common_message_from_server(server_id, parsed) + } + + fn handle_common_message_from_server( + &mut self, + server_id: Option, + message: CommonMessages<'_>, + ) -> Result<(), Self::Error> { + match message { + CommonMessages::SetupConnectionSuccess(msg) => { + self.handle_setup_connection_success(server_id, msg) + } + CommonMessages::SetupConnectionError(msg) => { + self.handle_setup_connection_error(server_id, msg) + } + CommonMessages::ChannelEndpointChanged(msg) => { + self.handle_channel_endpoint_changed(server_id, msg) + } + CommonMessages::Reconnect(msg) => self.handle_reconnect(server_id, msg), + + CommonMessages::SetupConnection(_) => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_SETUP_CONNECTION, + )), + } + } + + fn handle_setup_connection_success( + &mut self, + server_id: Option, + msg: SetupConnectionSuccess, + ) -> Result<(), Self::Error>; + + fn handle_setup_connection_error( + &mut self, + server_id: Option, + msg: SetupConnectionError, + ) -> Result<(), Self::Error>; + + fn handle_channel_endpoint_changed( + &mut self, + server_id: Option, + msg: ChannelEndpointChanged, + ) -> Result<(), Self::Error>; + + fn handle_reconnect( + &mut self, + server_id: Option, + msg: Reconnect, + ) -> Result<(), Self::Error>; +} + +/// Asynchronous handler trait for processing common messages received from servers. +/// +/// The server ID identifies which server a message originated from. +/// Whether this is relevant or not depends on which object is implementing the trait, and whether +/// this contextual information is readily available or not. In cases where `server_id` is either +/// irrelevant or can be inferred without the context, this should always be `None`. +#[trait_variant::make(Send)] +pub trait HandleCommonMessagesFromServerAsync { + type Error: HandlerErrorType; + async fn handle_common_message_frame_from_server( + &mut self, + server_id: Option, + message_type: u8, + payload: &mut [u8], + ) -> Result<(), Self::Error> { + async move { + let parsed: CommonMessages<'_> = (message_type, payload) + .try_into() + .map_err(Self::Error::parse_error)?; + self.handle_common_message_from_server(server_id, parsed) + .await + } + } + + async fn handle_common_message_from_server( + &mut self, + server_id: Option, + message: CommonMessages<'_>, + ) -> Result<(), Self::Error> { + async move { + match message { + CommonMessages::SetupConnectionSuccess(msg) => { + self.handle_setup_connection_success(server_id, msg).await + } + CommonMessages::SetupConnectionError(msg) => { + self.handle_setup_connection_error(server_id, msg).await + } + CommonMessages::ChannelEndpointChanged(msg) => { + self.handle_channel_endpoint_changed(server_id, msg).await + } + CommonMessages::Reconnect(msg) => self.handle_reconnect(server_id, msg).await, + + CommonMessages::SetupConnection(_) => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_SETUP_CONNECTION, + )), + } + } + } + + async fn handle_setup_connection_success( + &mut self, + server_id: Option, + msg: SetupConnectionSuccess, + ) -> Result<(), Self::Error>; + + async fn handle_setup_connection_error( + &mut self, + server_id: Option, + msg: SetupConnectionError, + ) -> Result<(), Self::Error>; + + async fn handle_channel_endpoint_changed( + &mut self, + server_id: Option, + msg: ChannelEndpointChanged, + ) -> Result<(), Self::Error>; + + async fn handle_reconnect( + &mut self, + server_id: Option, + msg: Reconnect, + ) -> Result<(), Self::Error>; +} + +/// Synchronous handler trait for processing common messages received from clients. +/// +/// The client ID identifies which client a message originated from. +/// Whether this is relevant or not depends on which object is implementing the trait, and whether +/// this contextual information is readily available or not. In cases where `client_id` is either +/// irrelevant or can be inferred without the context, this should always be `None`. +pub trait HandleCommonMessagesFromClientSync { + type Error: HandlerErrorType; + fn handle_common_message_frame_from_client( + &mut self, + client_id: Option, + message_type: u8, + payload: &mut [u8], + ) -> Result<(), Self::Error> { + let parsed: CommonMessages<'_> = (message_type, payload) + .try_into() + .map_err(Self::Error::parse_error)?; + self.handle_common_message_from_client(client_id, parsed) + } + + fn handle_common_message_from_client( + &mut self, + client_id: Option, + message: CommonMessages<'_>, + ) -> Result<(), Self::Error> { + match message { + CommonMessages::SetupConnectionSuccess(_) => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_SETUP_CONNECTION_SUCCESS, + )), + CommonMessages::SetupConnectionError(_) => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_SETUP_CONNECTION_ERROR, + )), + CommonMessages::ChannelEndpointChanged(_) => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_CHANNEL_ENDPOINT_CHANGED, + )), + CommonMessages::Reconnect(_) => { + Err(Self::Error::unexpected_message(MESSAGE_TYPE_RECONNECT)) + } + + CommonMessages::SetupConnection(msg) => self.handle_setup_connection(client_id, msg), + } + } + + fn handle_setup_connection( + &mut self, + client_id: Option, + msg: SetupConnection, + ) -> Result<(), Self::Error>; +} + +/// Asynchronous handler trait for processing common messages received from clients. +/// +/// The client ID identifies which client a message originated from. +/// Whether this is relevant or not depends on which object is implementing the trait, and whether +/// this contextual information is readily available or not. In cases where `client_id` is either +/// irrelevant or can be inferred without the context, this should always be `None`. +#[trait_variant::make(Send)] +pub trait HandleCommonMessagesFromClientAsync { + type Error: HandlerErrorType; + async fn handle_common_message_frame_from_client( + &mut self, + client_id: Option, + message_type: u8, + payload: &mut [u8], + ) -> Result<(), Self::Error> { + async move { + let parsed: CommonMessages<'_> = (message_type, payload) + .try_into() + .map_err(Self::Error::parse_error)?; + self.handle_common_message_from_client(client_id, parsed) + .await + } + } + + async fn handle_common_message_from_client( + &mut self, + client_id: Option, + message: CommonMessages<'_>, + ) -> Result<(), Self::Error> { + async move { + match message { + CommonMessages::SetupConnectionSuccess(_) => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_SETUP_CONNECTION_SUCCESS, + )), + CommonMessages::SetupConnectionError(_) => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_SETUP_CONNECTION_ERROR, + )), + CommonMessages::ChannelEndpointChanged(_) => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_CHANNEL_ENDPOINT_CHANGED, + )), + CommonMessages::Reconnect(_) => { + Err(Self::Error::unexpected_message(MESSAGE_TYPE_RECONNECT)) + } + CommonMessages::SetupConnection(msg) => { + self.handle_setup_connection(client_id, msg).await + } + } + } + } + + async fn handle_setup_connection( + &mut self, + client_id: Option, + msg: SetupConnection, + ) -> Result<(), Self::Error>; +} diff --git a/protocols/v2/handlers-sv2/src/error.rs b/protocols/v2/handlers-sv2/src/error.rs new file mode 100644 index 0000000000..98a002383d --- /dev/null +++ b/protocols/v2/handlers-sv2/src/error.rs @@ -0,0 +1,5 @@ +use parsers_sv2::ParserError; +pub trait HandlerErrorType { + fn unexpected_message(message_type: u8) -> Self; + fn parse_error(error: ParserError) -> Self; +} diff --git a/protocols/v2/handlers-sv2/src/job_declaration.rs b/protocols/v2/handlers-sv2/src/job_declaration.rs new file mode 100644 index 0000000000..15621b6a03 --- /dev/null +++ b/protocols/v2/handlers-sv2/src/job_declaration.rs @@ -0,0 +1,339 @@ +use core::convert::TryInto; +use job_declaration_sv2::{ + MESSAGE_TYPE_ALLOCATE_MINING_JOB_TOKEN, MESSAGE_TYPE_ALLOCATE_MINING_JOB_TOKEN_SUCCESS, + MESSAGE_TYPE_DECLARE_MINING_JOB, MESSAGE_TYPE_DECLARE_MINING_JOB_ERROR, + MESSAGE_TYPE_DECLARE_MINING_JOB_SUCCESS, MESSAGE_TYPE_PROVIDE_MISSING_TRANSACTIONS, + MESSAGE_TYPE_PROVIDE_MISSING_TRANSACTIONS_SUCCESS, MESSAGE_TYPE_PUSH_SOLUTION, *, +}; +use parsers_sv2::JobDeclaration; + +use crate::error::HandlerErrorType; + +/// Synchronous handler trait for processing job declaration messages received from servers. +/// +/// The server ID identifies which server a message originated from. +/// Whether this is relevant or not depends on which object is implementing the trait, and whether +/// this contextual information is readily available or not. In cases where `server_id` is either +/// irrelevant or can be inferred without the context, this should always be `None`. +pub trait HandleJobDeclarationMessagesFromServerSync { + type Error: HandlerErrorType; + fn handle_job_declaration_message_frame_from_server( + &mut self, + server_id: Option, + message_type: u8, + payload: &mut [u8], + ) -> Result<(), Self::Error> { + let parsed: JobDeclaration<'_> = (message_type, payload) + .try_into() + .map_err(Self::Error::parse_error)?; + self.handle_job_declaration_message_from_server(server_id, parsed) + } + + fn handle_job_declaration_message_from_server( + &mut self, + server_id: Option, + message: JobDeclaration<'_>, + ) -> Result<(), Self::Error> { + match message { + JobDeclaration::AllocateMiningJobTokenSuccess(msg) => { + self.handle_allocate_mining_job_token_success(server_id, msg) + } + JobDeclaration::DeclareMiningJobSuccess(msg) => { + self.handle_declare_mining_job_success(server_id, msg) + } + JobDeclaration::DeclareMiningJobError(msg) => { + self.handle_declare_mining_job_error(server_id, msg) + } + JobDeclaration::ProvideMissingTransactions(msg) => { + self.handle_provide_missing_transactions(server_id, msg) + } + JobDeclaration::AllocateMiningJobToken(_) => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_ALLOCATE_MINING_JOB_TOKEN, + )), + JobDeclaration::DeclareMiningJob(_) => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_DECLARE_MINING_JOB, + )), + JobDeclaration::ProvideMissingTransactionsSuccess(_) => Err( + Self::Error::unexpected_message(MESSAGE_TYPE_PROVIDE_MISSING_TRANSACTIONS_SUCCESS), + ), + JobDeclaration::PushSolution(_) => { + Err(Self::Error::unexpected_message(MESSAGE_TYPE_PUSH_SOLUTION)) + } + } + } + + fn handle_allocate_mining_job_token_success( + &mut self, + server_id: Option, + msg: AllocateMiningJobTokenSuccess, + ) -> Result<(), Self::Error>; + + fn handle_declare_mining_job_success( + &mut self, + server_id: Option, + msg: DeclareMiningJobSuccess, + ) -> Result<(), Self::Error>; + + fn handle_declare_mining_job_error( + &mut self, + server_id: Option, + msg: DeclareMiningJobError, + ) -> Result<(), Self::Error>; + + fn handle_provide_missing_transactions( + &mut self, + server_id: Option, + msg: ProvideMissingTransactions, + ) -> Result<(), Self::Error>; +} + +/// Asynchronous handler trait for processing job declaration messages received from servers. +/// +/// The server ID identifies which server a message originated from. +/// Whether this is relevant or not depends on which object is implementing the trait, and whether +/// this contextual information is readily available or not. In cases where `server_id` is either +/// irrelevant or can be inferred without the context, this should always be `None`. +#[trait_variant::make(Send)] +pub trait HandleJobDeclarationMessagesFromServerAsync { + type Error: HandlerErrorType; + async fn handle_job_declaration_message_frame_from_server( + &mut self, + server_id: Option, + message_type: u8, + payload: &mut [u8], + ) -> Result<(), Self::Error> { + async move { + let parsed: JobDeclaration<'_> = (message_type, payload) + .try_into() + .map_err(Self::Error::parse_error)?; + self.handle_job_declaration_message_from_server(server_id, parsed) + .await + } + } + + async fn handle_job_declaration_message_from_server( + &mut self, + server_id: Option, + message: JobDeclaration<'_>, + ) -> Result<(), Self::Error> { + async move { + match message { + JobDeclaration::AllocateMiningJobTokenSuccess(msg) => { + self.handle_allocate_mining_job_token_success(server_id, msg) + .await + } + JobDeclaration::DeclareMiningJobSuccess(msg) => { + self.handle_declare_mining_job_success(server_id, msg).await + } + JobDeclaration::DeclareMiningJobError(msg) => { + self.handle_declare_mining_job_error(server_id, msg).await + } + JobDeclaration::ProvideMissingTransactions(msg) => { + self.handle_provide_missing_transactions(server_id, msg) + .await + } + JobDeclaration::AllocateMiningJobToken(_) => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_ALLOCATE_MINING_JOB_TOKEN, + )), + JobDeclaration::DeclareMiningJob(_) => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_DECLARE_MINING_JOB, + )), + JobDeclaration::ProvideMissingTransactionsSuccess(_) => { + Err(Self::Error::unexpected_message( + MESSAGE_TYPE_PROVIDE_MISSING_TRANSACTIONS_SUCCESS, + )) + } + JobDeclaration::PushSolution(_) => { + Err(Self::Error::unexpected_message(MESSAGE_TYPE_PUSH_SOLUTION)) + } + } + } + } + + async fn handle_allocate_mining_job_token_success( + &mut self, + server_id: Option, + msg: AllocateMiningJobTokenSuccess, + ) -> Result<(), Self::Error>; + + async fn handle_declare_mining_job_success( + &mut self, + server_id: Option, + msg: DeclareMiningJobSuccess, + ) -> Result<(), Self::Error>; + + async fn handle_declare_mining_job_error( + &mut self, + server_id: Option, + msg: DeclareMiningJobError, + ) -> Result<(), Self::Error>; + + async fn handle_provide_missing_transactions( + &mut self, + server_id: Option, + msg: ProvideMissingTransactions, + ) -> Result<(), Self::Error>; +} + +/// Synchronous handler trait for processing job declaration messages received from clients. +/// +/// The client ID identifies which client a message originated from. +/// Whether this is relevant or not depends on which object is implementing the trait, and whether +/// this contextual information is readily available or not. In cases where `client_id` is either +/// irrelevant or can be inferred without the context, this should always be `None`. +pub trait HandleJobDeclarationMessagesFromClientSync { + type Error: HandlerErrorType; + + fn handle_job_declaration_message_frame_from_client( + &mut self, + client_id: Option, + message_type: u8, + payload: &mut [u8], + ) -> Result<(), Self::Error> { + let parsed: JobDeclaration<'_> = (message_type, payload) + .try_into() + .map_err(Self::Error::parse_error)?; + self.handle_job_declaration_message_from_client(client_id, parsed) + } + + fn handle_job_declaration_message_from_client( + &mut self, + client_id: Option, + message: JobDeclaration<'_>, + ) -> Result<(), Self::Error> { + match message { + JobDeclaration::AllocateMiningJobToken(msg) => { + self.handle_allocate_mining_job_token(client_id, msg) + } + JobDeclaration::DeclareMiningJob(msg) => self.handle_declare_mining_job(client_id, msg), + JobDeclaration::ProvideMissingTransactionsSuccess(msg) => { + self.handle_provide_missing_transactions_success(client_id, msg) + } + JobDeclaration::PushSolution(msg) => self.handle_push_solution(client_id, msg), + + JobDeclaration::AllocateMiningJobTokenSuccess(_) => Err( + Self::Error::unexpected_message(MESSAGE_TYPE_ALLOCATE_MINING_JOB_TOKEN_SUCCESS), + ), + JobDeclaration::DeclareMiningJobSuccess(_) => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_DECLARE_MINING_JOB_SUCCESS, + )), + JobDeclaration::DeclareMiningJobError(_) => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_DECLARE_MINING_JOB_ERROR, + )), + JobDeclaration::ProvideMissingTransactions(_) => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_PROVIDE_MISSING_TRANSACTIONS, + )), + } + } + + fn handle_allocate_mining_job_token( + &mut self, + client_id: Option, + msg: AllocateMiningJobToken, + ) -> Result<(), Self::Error>; + + fn handle_declare_mining_job( + &mut self, + client_id: Option, + msg: DeclareMiningJob, + ) -> Result<(), Self::Error>; + + fn handle_provide_missing_transactions_success( + &mut self, + client_id: Option, + msg: ProvideMissingTransactionsSuccess, + ) -> Result<(), Self::Error>; + + fn handle_push_solution( + &mut self, + client_id: Option, + msg: PushSolution, + ) -> Result<(), Self::Error>; +} + +/// Asynchronous handler trait for processing job declaration messages received from clients. +/// +/// The client ID identifies which client a message originated from. +/// Whether this is relevant or not depends on which object is implementing the trait, and whether +/// this contextual information is readily available or not. In cases where `client_id` is either +/// irrelevant or can be inferred without the context, this should always be `None`. +#[trait_variant::make(Send)] +pub trait HandleJobDeclarationMessagesFromClientAsync { + type Error: HandlerErrorType; + + async fn handle_job_declaration_message_frame_from_client( + &mut self, + client_id: Option, + message_type: u8, + payload: &mut [u8], + ) -> Result<(), Self::Error> { + async move { + let parsed: JobDeclaration<'_> = (message_type, payload) + .try_into() + .map_err(Self::Error::parse_error)?; + self.handle_job_declaration_message_from_client(client_id, parsed) + .await + } + } + + async fn handle_job_declaration_message_from_client( + &mut self, + client_id: Option, + message: JobDeclaration<'_>, + ) -> Result<(), Self::Error> { + async move { + match message { + JobDeclaration::AllocateMiningJobToken(msg) => { + self.handle_allocate_mining_job_token(client_id, msg).await + } + JobDeclaration::DeclareMiningJob(msg) => { + self.handle_declare_mining_job(client_id, msg).await + } + JobDeclaration::ProvideMissingTransactionsSuccess(msg) => { + self.handle_provide_missing_transactions_success(client_id, msg) + .await + } + JobDeclaration::PushSolution(msg) => { + self.handle_push_solution(client_id, msg).await + } + + JobDeclaration::AllocateMiningJobTokenSuccess(_) => Err( + Self::Error::unexpected_message(MESSAGE_TYPE_ALLOCATE_MINING_JOB_TOKEN_SUCCESS), + ), + JobDeclaration::DeclareMiningJobSuccess(_) => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_DECLARE_MINING_JOB_SUCCESS, + )), + JobDeclaration::DeclareMiningJobError(_) => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_DECLARE_MINING_JOB_ERROR, + )), + JobDeclaration::ProvideMissingTransactions(_) => Err( + Self::Error::unexpected_message(MESSAGE_TYPE_PROVIDE_MISSING_TRANSACTIONS), + ), + } + } + } + + async fn handle_allocate_mining_job_token( + &mut self, + client_id: Option, + msg: AllocateMiningJobToken, + ) -> Result<(), Self::Error>; + + async fn handle_declare_mining_job( + &mut self, + client_id: Option, + msg: DeclareMiningJob, + ) -> Result<(), Self::Error>; + + async fn handle_provide_missing_transactions_success( + &mut self, + client_id: Option, + msg: ProvideMissingTransactionsSuccess, + ) -> Result<(), Self::Error>; + + async fn handle_push_solution( + &mut self, + client_id: Option, + msg: PushSolution, + ) -> Result<(), Self::Error>; +} diff --git a/protocols/v2/handlers-sv2/src/lib.rs b/protocols/v2/handlers-sv2/src/lib.rs new file mode 100644 index 0000000000..332f072724 --- /dev/null +++ b/protocols/v2/handlers-sv2/src/lib.rs @@ -0,0 +1,29 @@ +mod common; +mod error; +mod job_declaration; +mod mining; +mod template_distribution; + +pub use error::HandlerErrorType; + +pub use common::{ + HandleCommonMessagesFromClientAsync, HandleCommonMessagesFromClientSync, + HandleCommonMessagesFromServerAsync, HandleCommonMessagesFromServerSync, +}; + +pub use mining::{ + HandleMiningMessagesFromClientAsync, HandleMiningMessagesFromClientSync, + HandleMiningMessagesFromServerAsync, HandleMiningMessagesFromServerSync, SupportedChannelTypes, +}; + +pub use template_distribution::{ + HandleTemplateDistributionMessagesFromClientAsync, + HandleTemplateDistributionMessagesFromClientSync, + HandleTemplateDistributionMessagesFromServerAsync, + HandleTemplateDistributionMessagesFromServerSync, +}; + +pub use job_declaration::{ + HandleJobDeclarationMessagesFromClientAsync, HandleJobDeclarationMessagesFromClientSync, + HandleJobDeclarationMessagesFromServerAsync, HandleJobDeclarationMessagesFromServerSync, +}; diff --git a/protocols/v2/handlers-sv2/src/mining.rs b/protocols/v2/handlers-sv2/src/mining.rs new file mode 100644 index 0000000000..b905de90d1 --- /dev/null +++ b/protocols/v2/handlers-sv2/src/mining.rs @@ -0,0 +1,733 @@ +use crate::error::HandlerErrorType; +use binary_sv2::Str0255; +use mining_sv2::{ + CloseChannel, NewExtendedMiningJob, NewMiningJob, OpenExtendedMiningChannel, + OpenExtendedMiningChannelSuccess, OpenMiningChannelError, OpenStandardMiningChannel, + OpenStandardMiningChannelSuccess, SetCustomMiningJob, SetCustomMiningJobError, + SetCustomMiningJobSuccess, SetExtranoncePrefix, SetGroupChannel, SetNewPrevHash, SetTarget, + SubmitSharesError, SubmitSharesExtended, SubmitSharesStandard, SubmitSharesSuccess, + UpdateChannel, UpdateChannelError, +}; +use parsers_sv2::Mining; +use std::convert::TryInto; + +use mining_sv2::*; + +#[derive(PartialEq, Eq)] +pub enum SupportedChannelTypes { + Standard, + Extended, + Group, + GroupAndExtended, +} +/// Synchronous handler trait for processing mining messages received from servers. +/// +/// The server ID identifies which server a message originated from. +/// Whether this is relevant or not depends on which object is implementing the trait, and whether +/// this contextual information is readily available or not. In cases where `server_id` is either +/// irrelevant or can be inferred without the context, this should always be `None`. +pub trait HandleMiningMessagesFromServerSync { + type Error: HandlerErrorType; + + fn get_channel_type_for_server(&self, server_id: Option) -> SupportedChannelTypes; + fn is_work_selection_enabled_for_server(&self, server_id: Option) -> bool; + + fn handle_mining_message_frame_from_server( + &mut self, + server_id: Option, + message_type: u8, + payload: &mut [u8], + ) -> Result<(), Self::Error> { + let parsed: Mining = (message_type, payload) + .try_into() + .map_err(Self::Error::parse_error)?; + self.handle_mining_message_from_server(server_id, parsed) + } + + fn handle_mining_message_from_server( + &mut self, + server_id: Option, + message: Mining, + ) -> Result<(), Self::Error> { + let (channel_type, work_selection) = ( + self.get_channel_type_for_server(server_id), + self.is_work_selection_enabled_for_server(server_id), + ); + + use Mining::*; + match message { + OpenStandardMiningChannelSuccess(m) => match channel_type { + SupportedChannelTypes::Standard + | SupportedChannelTypes::Group + | SupportedChannelTypes::GroupAndExtended => { + self.handle_open_standard_mining_channel_success(server_id, m) + } + _ => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_OPEN_STANDARD_MINING_CHANNEL_SUCCESS, + )), + }, + + OpenExtendedMiningChannelSuccess(m) => match channel_type { + SupportedChannelTypes::Extended | SupportedChannelTypes::GroupAndExtended => { + self.handle_open_extended_mining_channel_success(server_id, m) + } + _ => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_OPEN_EXTENDED_MINING_CHANNEL_SUCCESS, + )), + }, + + OpenMiningChannelError(m) => self.handle_open_mining_channel_error(server_id, m), + UpdateChannelError(m) => self.handle_update_channel_error(server_id, m), + CloseChannel(m) => self.handle_close_channel(server_id, m), + SetExtranoncePrefix(m) => self.handle_set_extranonce_prefix(server_id, m), + SubmitSharesSuccess(m) => self.handle_submit_shares_success(server_id, m), + SubmitSharesError(m) => self.handle_submit_shares_error(server_id, m), + + NewMiningJob(m) => match channel_type { + SupportedChannelTypes::Standard => self.handle_new_mining_job(server_id, m), + _ => Err(Self::Error::unexpected_message(MESSAGE_TYPE_NEW_MINING_JOB)), + }, + + NewExtendedMiningJob(m) => match channel_type { + SupportedChannelTypes::Extended + | SupportedChannelTypes::Group + | SupportedChannelTypes::GroupAndExtended => { + self.handle_new_extended_mining_job(server_id, m) + } + _ => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_NEW_EXTENDED_MINING_JOB, + )), + }, + + SetNewPrevHash(m) => self.handle_set_new_prev_hash(server_id, m), + + SetCustomMiningJobSuccess(m) => match (channel_type, work_selection) { + (SupportedChannelTypes::Extended, true) + | (SupportedChannelTypes::GroupAndExtended, true) => { + self.handle_set_custom_mining_job_success(server_id, m) + } + _ => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_SET_CUSTOM_MINING_JOB_SUCCESS, + )), + }, + + SetCustomMiningJobError(m) => match (channel_type, work_selection) { + (SupportedChannelTypes::Extended, true) + | (SupportedChannelTypes::Group, true) + | (SupportedChannelTypes::GroupAndExtended, true) => { + self.handle_set_custom_mining_job_error(server_id, m) + } + _ => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_SET_CUSTOM_MINING_JOB_ERROR, + )), + }, + + SetTarget(m) => self.handle_set_target(server_id, m), + + SetGroupChannel(m) => match channel_type { + SupportedChannelTypes::Group | SupportedChannelTypes::GroupAndExtended => { + self.handle_set_group_channel(server_id, m) + } + _ => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_SET_GROUP_CHANNEL, + )), + }, + + _ => Err(Self::Error::unexpected_message(0)), + } + } + + fn handle_open_standard_mining_channel_success( + &mut self, + server_id: Option, + msg: OpenStandardMiningChannelSuccess, + ) -> Result<(), Self::Error>; + + fn handle_open_extended_mining_channel_success( + &mut self, + server_id: Option, + msg: OpenExtendedMiningChannelSuccess, + ) -> Result<(), Self::Error>; + + fn handle_open_mining_channel_error( + &mut self, + server_id: Option, + msg: OpenMiningChannelError, + ) -> Result<(), Self::Error>; + + fn handle_update_channel_error( + &mut self, + server_id: Option, + msg: UpdateChannelError, + ) -> Result<(), Self::Error>; + + fn handle_close_channel( + &mut self, + server_id: Option, + msg: CloseChannel, + ) -> Result<(), Self::Error>; + + fn handle_set_extranonce_prefix( + &mut self, + server_id: Option, + msg: SetExtranoncePrefix, + ) -> Result<(), Self::Error>; + + fn handle_submit_shares_success( + &mut self, + server_id: Option, + msg: SubmitSharesSuccess, + ) -> Result<(), Self::Error>; + + fn handle_submit_shares_error( + &mut self, + server_id: Option, + msg: SubmitSharesError, + ) -> Result<(), Self::Error>; + + fn handle_new_mining_job( + &mut self, + server_id: Option, + msg: NewMiningJob, + ) -> Result<(), Self::Error>; + + fn handle_new_extended_mining_job( + &mut self, + server_id: Option, + msg: NewExtendedMiningJob, + ) -> Result<(), Self::Error>; + + fn handle_set_new_prev_hash( + &mut self, + server_id: Option, + msg: SetNewPrevHash, + ) -> Result<(), Self::Error>; + + fn handle_set_custom_mining_job_success( + &mut self, + server_id: Option, + msg: SetCustomMiningJobSuccess, + ) -> Result<(), Self::Error>; + + fn handle_set_custom_mining_job_error( + &mut self, + server_id: Option, + msg: SetCustomMiningJobError, + ) -> Result<(), Self::Error>; + + fn handle_set_target( + &mut self, + server_id: Option, + msg: SetTarget, + ) -> Result<(), Self::Error>; + + fn handle_set_group_channel( + &mut self, + server_id: Option, + msg: SetGroupChannel, + ) -> Result<(), Self::Error>; +} + +/// Asynchronous handler trait for processing mining messages received from servers. +/// +/// The server ID identifies which server a message originated from. +/// Whether this is relevant or not depends on which object is implementing the trait, and whether +/// this contextual information is readily available or not. In cases where `server_id` is either +/// irrelevant or can be inferred without the context, this should always be `None`. +#[trait_variant::make(Send)] +pub trait HandleMiningMessagesFromServerAsync { + type Error: HandlerErrorType; + + fn get_channel_type_for_server(&self, server_id: Option) -> SupportedChannelTypes; + fn is_work_selection_enabled_for_server(&self, server_id: Option) -> bool; + + async fn handle_mining_message_frame_from_server( + &mut self, + server_id: Option, + message_type: u8, + payload: &mut [u8], + ) -> Result<(), Self::Error> { + async move { + let parsed: Mining = (message_type, payload) + .try_into() + .map_err(Self::Error::parse_error)?; + self.handle_mining_message_from_server(server_id, parsed) + .await + } + } + + async fn handle_mining_message_from_server( + &mut self, + server_id: Option, + message: Mining, + ) -> Result<(), Self::Error> { + let (channel_type, work_selection) = ( + self.get_channel_type_for_server(server_id), + self.is_work_selection_enabled_for_server(server_id), + ); + + async move { + use Mining::*; + match message { + OpenStandardMiningChannelSuccess(m) => match channel_type { + SupportedChannelTypes::Standard + | SupportedChannelTypes::Group + | SupportedChannelTypes::GroupAndExtended => { + self.handle_open_standard_mining_channel_success(server_id, m) + .await + } + _ => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_OPEN_STANDARD_MINING_CHANNEL_SUCCESS, + )), + }, + + OpenExtendedMiningChannelSuccess(m) => match channel_type { + SupportedChannelTypes::Extended | SupportedChannelTypes::GroupAndExtended => { + self.handle_open_extended_mining_channel_success(server_id, m) + .await + } + _ => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_OPEN_EXTENDED_MINING_CHANNEL_SUCCESS, + )), + }, + + OpenMiningChannelError(m) => { + self.handle_open_mining_channel_error(server_id, m).await + } + UpdateChannelError(m) => self.handle_update_channel_error(server_id, m).await, + CloseChannel(m) => self.handle_close_channel(server_id, m).await, + SetExtranoncePrefix(m) => self.handle_set_extranonce_prefix(server_id, m).await, + SubmitSharesSuccess(m) => self.handle_submit_shares_success(server_id, m).await, + SubmitSharesError(m) => self.handle_submit_shares_error(server_id, m).await, + + NewMiningJob(m) => match channel_type { + SupportedChannelTypes::Standard => { + self.handle_new_mining_job(server_id, m).await + } + _ => Err(Self::Error::unexpected_message(MESSAGE_TYPE_NEW_MINING_JOB)), + }, + + NewExtendedMiningJob(m) => match channel_type { + SupportedChannelTypes::Extended + | SupportedChannelTypes::Group + | SupportedChannelTypes::GroupAndExtended => { + self.handle_new_extended_mining_job(server_id, m).await + } + _ => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_NEW_EXTENDED_MINING_JOB, + )), + }, + + SetNewPrevHash(m) => self.handle_set_new_prev_hash(server_id, m).await, + + SetCustomMiningJobSuccess(m) => match (channel_type, work_selection) { + (SupportedChannelTypes::Extended, true) + | (SupportedChannelTypes::GroupAndExtended, true) => { + self.handle_set_custom_mining_job_success(server_id, m) + .await + } + _ => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_SET_CUSTOM_MINING_JOB_SUCCESS, + )), + }, + + SetCustomMiningJobError(m) => match (channel_type, work_selection) { + (SupportedChannelTypes::Extended, true) + | (SupportedChannelTypes::Group, true) + | (SupportedChannelTypes::GroupAndExtended, true) => { + self.handle_set_custom_mining_job_error(server_id, m).await + } + _ => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_SET_CUSTOM_MINING_JOB_ERROR, + )), + }, + + SetTarget(m) => self.handle_set_target(server_id, m).await, + + SetGroupChannel(m) => match channel_type { + SupportedChannelTypes::Group | SupportedChannelTypes::GroupAndExtended => { + self.handle_set_group_channel(server_id, m).await + } + _ => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_SET_GROUP_CHANNEL, + )), + }, + _ => Err(Self::Error::unexpected_message(0)), + } + } + } + + async fn handle_open_standard_mining_channel_success( + &mut self, + server_id: Option, + msg: OpenStandardMiningChannelSuccess, + ) -> Result<(), Self::Error>; + + async fn handle_open_extended_mining_channel_success( + &mut self, + server_id: Option, + msg: OpenExtendedMiningChannelSuccess, + ) -> Result<(), Self::Error>; + + async fn handle_open_mining_channel_error( + &mut self, + server_id: Option, + msg: OpenMiningChannelError, + ) -> Result<(), Self::Error>; + + async fn handle_update_channel_error( + &mut self, + server_id: Option, + msg: UpdateChannelError, + ) -> Result<(), Self::Error>; + + async fn handle_close_channel( + &mut self, + server_id: Option, + msg: CloseChannel, + ) -> Result<(), Self::Error>; + + async fn handle_set_extranonce_prefix( + &mut self, + server_id: Option, + msg: SetExtranoncePrefix, + ) -> Result<(), Self::Error>; + + async fn handle_submit_shares_success( + &mut self, + server_id: Option, + msg: SubmitSharesSuccess, + ) -> Result<(), Self::Error>; + + async fn handle_submit_shares_error( + &mut self, + server_id: Option, + msg: SubmitSharesError, + ) -> Result<(), Self::Error>; + + async fn handle_new_mining_job( + &mut self, + server_id: Option, + msg: NewMiningJob, + ) -> Result<(), Self::Error>; + + async fn handle_new_extended_mining_job( + &mut self, + server_id: Option, + msg: NewExtendedMiningJob, + ) -> Result<(), Self::Error>; + + async fn handle_set_new_prev_hash( + &mut self, + server_id: Option, + msg: SetNewPrevHash, + ) -> Result<(), Self::Error>; + + async fn handle_set_custom_mining_job_success( + &mut self, + server_id: Option, + msg: SetCustomMiningJobSuccess, + ) -> Result<(), Self::Error>; + + async fn handle_set_custom_mining_job_error( + &mut self, + server_id: Option, + msg: SetCustomMiningJobError, + ) -> Result<(), Self::Error>; + + async fn handle_set_target( + &mut self, + server_id: Option, + msg: SetTarget, + ) -> Result<(), Self::Error>; + + async fn handle_set_group_channel( + &mut self, + server_id: Option, + msg: SetGroupChannel, + ) -> Result<(), Self::Error>; +} + +/// Synchronous handler trait for processing mining messages received from clients. +/// +/// The client ID identifies which client a message originated from. +/// Whether this is relevant or not depends on which object is implementing the trait, and whether +/// this contextual information is readily available or not. In cases where `client_id` is either +/// irrelevant or can be inferred without the context, this should always be `None`. +pub trait HandleMiningMessagesFromClientSync { + type Error: HandlerErrorType; + + fn get_channel_type_for_client(&self, client_id: Option) -> SupportedChannelTypes; + fn is_work_selection_enabled_for_client(&self, client_id: Option) -> bool; + fn is_client_authorized( + &self, + client_id: Option, + user_identity: &Str0255, + ) -> Result; + + fn handle_mining_message_frame_from_client( + &mut self, + client_id: Option, + message_type: u8, + payload: &mut [u8], + ) -> Result<(), Self::Error> { + let parsed: Mining = (message_type, payload) + .try_into() + .map_err(Self::Error::parse_error)?; + self.handle_mining_message_from_client(client_id, parsed) + } + + fn handle_mining_message_from_client( + &mut self, + client_id: Option, + message: Mining, + ) -> Result<(), Self::Error> { + let (channel_type, work_selection) = ( + self.get_channel_type_for_client(client_id), + self.is_work_selection_enabled_for_client(client_id), + ); + + use Mining::*; + match message { + OpenStandardMiningChannel(m) => match channel_type { + SupportedChannelTypes::Standard + | SupportedChannelTypes::Group + | SupportedChannelTypes::GroupAndExtended => { + self.handle_open_standard_mining_channel(client_id, m) + } + SupportedChannelTypes::Extended => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_OPEN_STANDARD_MINING_CHANNEL, + )), + }, + OpenExtendedMiningChannel(m) => match channel_type { + SupportedChannelTypes::Extended | SupportedChannelTypes::GroupAndExtended => { + self.handle_open_extended_mining_channel(client_id, m) + } + _ => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_OPEN_EXTENDED_MINING_CHANNEL, + )), + }, + UpdateChannel(m) => self.handle_update_channel(client_id, m), + + SubmitSharesStandard(m) => match channel_type { + SupportedChannelTypes::Standard + | SupportedChannelTypes::Group + | SupportedChannelTypes::GroupAndExtended => { + self.handle_submit_shares_standard(client_id, m) + } + SupportedChannelTypes::Extended => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_SUBMIT_SHARES_STANDARD, + )), + }, + + SubmitSharesExtended(m) => match channel_type { + SupportedChannelTypes::Extended | SupportedChannelTypes::GroupAndExtended => { + self.handle_submit_shares_extended(client_id, m) + } + _ => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_SUBMIT_SHARES_EXTENDED, + )), + }, + + SetCustomMiningJob(m) => match (channel_type, work_selection) { + (SupportedChannelTypes::Extended, true) + | (SupportedChannelTypes::GroupAndExtended, true) => { + self.handle_set_custom_mining_job(client_id, m) + } + _ => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_SET_CUSTOM_MINING_JOB, + )), + }, + CloseChannel(m) => self.handle_close_channel(client_id, m), + + _ => Err(Self::Error::unexpected_message(0)), + } + } + + fn handle_close_channel( + &mut self, + client_id: Option, + msg: CloseChannel, + ) -> Result<(), Self::Error>; + + fn handle_open_standard_mining_channel( + &mut self, + client_id: Option, + msg: OpenStandardMiningChannel, + ) -> Result<(), Self::Error>; + + fn handle_open_extended_mining_channel( + &mut self, + client_id: Option, + msg: OpenExtendedMiningChannel, + ) -> Result<(), Self::Error>; + + fn handle_update_channel( + &mut self, + client_id: Option, + msg: UpdateChannel, + ) -> Result<(), Self::Error>; + + fn handle_submit_shares_standard( + &mut self, + client_id: Option, + msg: SubmitSharesStandard, + ) -> Result<(), Self::Error>; + + fn handle_submit_shares_extended( + &mut self, + client_id: Option, + msg: SubmitSharesExtended, + ) -> Result<(), Self::Error>; + + fn handle_set_custom_mining_job( + &mut self, + client_id: Option, + msg: SetCustomMiningJob, + ) -> Result<(), Self::Error>; +} + +/// Asynchronous handler trait for processing mining messages received from clients. +/// +/// The client ID identifies which client a message originated from. +/// Whether this is relevant or not depends on which object is implementing the trait, and whether +/// this contextual information is readily available or not. In cases where `client_id` is either +/// irrelevant or can be inferred without the context, this should always be `None`. +#[trait_variant::make(Send)] +pub trait HandleMiningMessagesFromClientAsync { + type Error: HandlerErrorType; + + fn get_channel_type_for_client(&self, client_id: Option) -> SupportedChannelTypes; + fn is_work_selection_enabled_for_client(&self, client_id: Option) -> bool; + fn is_client_authorized( + &self, + client_id: Option, + user_identity: &Str0255, + ) -> Result; + + async fn handle_mining_message_frame_from_client( + &mut self, + client_id: Option, + message_type: u8, + payload: &mut [u8], + ) -> Result<(), Self::Error> { + async move { + let parsed: Mining = (message_type, payload) + .try_into() + .map_err(Self::Error::parse_error)?; + self.handle_mining_message_from_client(client_id, parsed) + .await + } + } + + async fn handle_mining_message_from_client( + &mut self, + client_id: Option, + message: Mining, + ) -> Result<(), Self::Error> { + let (channel_type, work_selection) = ( + self.get_channel_type_for_client(client_id), + self.is_work_selection_enabled_for_client(client_id), + ); + + async move { + use Mining::*; + match message { + OpenStandardMiningChannel(m) => match channel_type { + SupportedChannelTypes::Standard + | SupportedChannelTypes::Group + | SupportedChannelTypes::GroupAndExtended => { + self.handle_open_standard_mining_channel(client_id, m).await + } + SupportedChannelTypes::Extended => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_OPEN_STANDARD_MINING_CHANNEL, + )), + }, + OpenExtendedMiningChannel(m) => match channel_type { + SupportedChannelTypes::Extended | SupportedChannelTypes::GroupAndExtended => { + self.handle_open_extended_mining_channel(client_id, m).await + } + _ => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_OPEN_EXTENDED_MINING_CHANNEL, + )), + }, + UpdateChannel(m) => self.handle_update_channel(client_id, m).await, + + SubmitSharesStandard(m) => match channel_type { + SupportedChannelTypes::Standard + | SupportedChannelTypes::Group + | SupportedChannelTypes::GroupAndExtended => { + self.handle_submit_shares_standard(client_id, m).await + } + SupportedChannelTypes::Extended => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_SUBMIT_SHARES_STANDARD, + )), + }, + + SubmitSharesExtended(m) => match channel_type { + SupportedChannelTypes::Extended | SupportedChannelTypes::GroupAndExtended => { + self.handle_submit_shares_extended(client_id, m).await + } + _ => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_SUBMIT_SHARES_EXTENDED, + )), + }, + + SetCustomMiningJob(m) => match (channel_type, work_selection) { + (SupportedChannelTypes::Extended, true) + | (SupportedChannelTypes::GroupAndExtended, true) => { + self.handle_set_custom_mining_job(client_id, m).await + } + _ => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_SET_CUSTOM_MINING_JOB, + )), + }, + CloseChannel(m) => self.handle_close_channel(client_id, m).await, + + _ => Err(Self::Error::unexpected_message(0)), + } + } + } + + async fn handle_close_channel( + &mut self, + client_id: Option, + msg: CloseChannel, + ) -> Result<(), Self::Error>; + + async fn handle_open_standard_mining_channel( + &mut self, + client_id: Option, + msg: OpenStandardMiningChannel, + ) -> Result<(), Self::Error>; + + async fn handle_open_extended_mining_channel( + &mut self, + client_id: Option, + msg: OpenExtendedMiningChannel, + ) -> Result<(), Self::Error>; + + async fn handle_update_channel( + &mut self, + client_id: Option, + msg: UpdateChannel, + ) -> Result<(), Self::Error>; + + async fn handle_submit_shares_standard( + &mut self, + client_id: Option, + msg: SubmitSharesStandard, + ) -> Result<(), Self::Error>; + + async fn handle_submit_shares_extended( + &mut self, + client_id: Option, + msg: SubmitSharesExtended, + ) -> Result<(), Self::Error>; + + async fn handle_set_custom_mining_job( + &mut self, + client_id: Option, + msg: SetCustomMiningJob, + ) -> Result<(), Self::Error>; +} diff --git a/protocols/v2/handlers-sv2/src/template_distribution.rs b/protocols/v2/handlers-sv2/src/template_distribution.rs new file mode 100644 index 0000000000..a0ae20b68e --- /dev/null +++ b/protocols/v2/handlers-sv2/src/template_distribution.rs @@ -0,0 +1,308 @@ +use parsers_sv2::TemplateDistribution; +use template_distribution_sv2::{ + CoinbaseOutputConstraints, NewTemplate, RequestTransactionData, RequestTransactionDataError, + RequestTransactionDataSuccess, SetNewPrevHash, SubmitSolution, +}; + +use core::convert::TryInto; +use template_distribution_sv2::*; + +use crate::error::HandlerErrorType; + +/// Synchronous handler trait for processing template distribution messages received from servers. +/// +/// The server ID identifies which server a message originated from. +/// Whether this is relevant or not depends on which object is implementing the trait, and whether +/// this contextual information is readily available or not. In cases where `server_id` is either +/// irrelevant or can be inferred without the context, this should always be `None`. +pub trait HandleTemplateDistributionMessagesFromServerSync { + type Error: HandlerErrorType; + + fn handle_template_distribution_message_frame_from_server( + &mut self, + server_id: Option, + message_type: u8, + payload: &mut [u8], + ) -> Result<(), Self::Error> { + let parsed: TemplateDistribution<'_> = (message_type, payload) + .try_into() + .map_err(Self::Error::parse_error)?; + self.handle_template_distribution_message_from_server(server_id, parsed) + } + + fn handle_template_distribution_message_from_server( + &mut self, + server_id: Option, + message: TemplateDistribution<'_>, + ) -> Result<(), Self::Error> { + match message { + TemplateDistribution::NewTemplate(m) => self.handle_new_template(server_id, m), + TemplateDistribution::SetNewPrevHash(m) => self.handle_set_new_prev_hash(server_id, m), + TemplateDistribution::RequestTransactionDataSuccess(m) => { + self.handle_request_tx_data_success(server_id, m) + } + TemplateDistribution::RequestTransactionDataError(m) => { + self.handle_request_tx_data_error(server_id, m) + } + + TemplateDistribution::CoinbaseOutputConstraints(_) => Err( + Self::Error::unexpected_message(MESSAGE_TYPE_COINBASE_OUTPUT_CONSTRAINTS), + ), + TemplateDistribution::RequestTransactionData(_) => Err( + Self::Error::unexpected_message(MESSAGE_TYPE_REQUEST_TRANSACTION_DATA), + ), + TemplateDistribution::SubmitSolution(_) => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_SUBMIT_SOLUTION, + )), + } + } + fn handle_new_template( + &mut self, + server_id: Option, + msg: NewTemplate, + ) -> Result<(), Self::Error>; + + fn handle_set_new_prev_hash( + &mut self, + server_id: Option, + msg: SetNewPrevHash, + ) -> Result<(), Self::Error>; + + fn handle_request_tx_data_success( + &mut self, + server_id: Option, + msg: RequestTransactionDataSuccess, + ) -> Result<(), Self::Error>; + + fn handle_request_tx_data_error( + &mut self, + server_id: Option, + msg: RequestTransactionDataError, + ) -> Result<(), Self::Error>; +} + +/// Asynchronous handler trait for processing template distribution messages received from servers. +/// +/// The server ID identifies which server a message originated from. +/// Whether this is relevant or not depends on which object is implementing the trait, and whether +/// this contextual information is readily available or not. In cases where `server_id` is either +/// irrelevant or can be inferred without the context, this should always be `None`. +#[trait_variant::make(Send)] +pub trait HandleTemplateDistributionMessagesFromServerAsync { + type Error: HandlerErrorType; + + async fn handle_template_distribution_message_frame_from_server( + &mut self, + server_id: Option, + message_type: u8, + payload: &mut [u8], + ) -> Result<(), Self::Error> { + async move { + let parsed: TemplateDistribution<'_> = (message_type, payload) + .try_into() + .map_err(Self::Error::parse_error)?; + self.handle_template_distribution_message_from_server(server_id, parsed) + .await + } + } + + async fn handle_template_distribution_message_from_server( + &mut self, + server_id: Option, + message: TemplateDistribution<'_>, + ) -> Result<(), Self::Error> { + async move { + match message { + TemplateDistribution::NewTemplate(m) => { + self.handle_new_template(server_id, m).await + } + TemplateDistribution::SetNewPrevHash(m) => { + self.handle_set_new_prev_hash(server_id, m).await + } + TemplateDistribution::RequestTransactionDataSuccess(m) => { + self.handle_request_tx_data_success(server_id, m).await + } + TemplateDistribution::RequestTransactionDataError(m) => { + self.handle_request_tx_data_error(server_id, m).await + } + + TemplateDistribution::CoinbaseOutputConstraints(_) => Err( + Self::Error::unexpected_message(MESSAGE_TYPE_COINBASE_OUTPUT_CONSTRAINTS), + ), + TemplateDistribution::RequestTransactionData(_) => Err( + Self::Error::unexpected_message(MESSAGE_TYPE_REQUEST_TRANSACTION_DATA), + ), + TemplateDistribution::SubmitSolution(_) => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_SUBMIT_SOLUTION, + )), + } + } + } + async fn handle_new_template( + &mut self, + server_id: Option, + msg: NewTemplate, + ) -> Result<(), Self::Error>; + + async fn handle_set_new_prev_hash( + &mut self, + server_id: Option, + msg: SetNewPrevHash, + ) -> Result<(), Self::Error>; + + async fn handle_request_tx_data_success( + &mut self, + server_id: Option, + msg: RequestTransactionDataSuccess, + ) -> Result<(), Self::Error>; + + async fn handle_request_tx_data_error( + &mut self, + server_id: Option, + msg: RequestTransactionDataError, + ) -> Result<(), Self::Error>; +} + +/// Synchronous handler trait for processing template distribution messages received from clients. +/// +/// The client ID identifies which client a message originated from. +/// Whether this is relevant or not depends on which object is implementing the trait, and whether +/// this contextual information is readily available or not. In cases where `client_id` is either +/// irrelevant or can be inferred without the context, this should always be `None`. +pub trait HandleTemplateDistributionMessagesFromClientSync { + type Error: HandlerErrorType; + + fn handle_template_distribution_message_frame_from_client( + &mut self, + client_id: Option, + message_type: u8, + payload: &mut [u8], + ) -> Result<(), Self::Error> { + let parsed: TemplateDistribution<'_> = (message_type, payload) + .try_into() + .map_err(Self::Error::parse_error)?; + self.handle_template_distribution_message_from_client(client_id, parsed) + } + + fn handle_template_distribution_message_from_client( + &mut self, + client_id: Option, + message: TemplateDistribution<'_>, + ) -> Result<(), Self::Error> { + match message { + TemplateDistribution::CoinbaseOutputConstraints(m) => { + self.handle_coinbase_output_constraints(client_id, m) + } + TemplateDistribution::RequestTransactionData(m) => { + self.handle_request_tx_data(client_id, m) + } + TemplateDistribution::SubmitSolution(m) => self.handle_submit_solution(client_id, m), + + TemplateDistribution::NewTemplate(_) => { + Err(Self::Error::unexpected_message(MESSAGE_TYPE_NEW_TEMPLATE)) + } + TemplateDistribution::SetNewPrevHash(_) => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_SET_NEW_PREV_HASH, + )), + TemplateDistribution::RequestTransactionDataSuccess(_) => Err( + Self::Error::unexpected_message(MESSAGE_TYPE_REQUEST_TRANSACTION_DATA_SUCCESS), + ), + TemplateDistribution::RequestTransactionDataError(_) => Err( + Self::Error::unexpected_message(MESSAGE_TYPE_REQUEST_TRANSACTION_DATA_ERROR), + ), + } + } + + fn handle_coinbase_output_constraints( + &mut self, + client_id: Option, + msg: CoinbaseOutputConstraints, + ) -> Result<(), Self::Error>; + + fn handle_request_tx_data( + &mut self, + client_id: Option, + msg: RequestTransactionData, + ) -> Result<(), Self::Error>; + fn handle_submit_solution( + &mut self, + client_id: Option, + msg: SubmitSolution, + ) -> Result<(), Self::Error>; +} + +/// Asynchronous handler trait for processing template distribution messages received from clients. +/// +/// The client ID identifies which client a message originated from. +/// Whether this is relevant or not depends on which object is implementing the trait, and whether +/// this contextual information is readily available or not. In cases where `client_id` is either +/// irrelevant or can be inferred without the context, this should always be `None`. +#[trait_variant::make(Send)] +pub trait HandleTemplateDistributionMessagesFromClientAsync { + type Error: HandlerErrorType; + + async fn handle_template_distribution_message_frame_from_client( + &mut self, + client_id: Option, + message_type: u8, + payload: &mut [u8], + ) -> Result<(), Self::Error> { + async move { + let parsed: TemplateDistribution<'_> = (message_type, payload) + .try_into() + .map_err(Self::Error::parse_error)?; + self.handle_template_distribution_message_from_client(client_id, parsed) + .await + } + } + + async fn handle_template_distribution_message_from_client( + &mut self, + client_id: Option, + message: TemplateDistribution<'_>, + ) -> Result<(), Self::Error> { + async move { + match message { + TemplateDistribution::CoinbaseOutputConstraints(m) => { + self.handle_coinbase_output_constraints(client_id, m).await + } + TemplateDistribution::RequestTransactionData(m) => { + self.handle_request_tx_data(client_id, m).await + } + TemplateDistribution::SubmitSolution(m) => { + self.handle_submit_solution(client_id, m).await + } + + TemplateDistribution::NewTemplate(_) => { + Err(Self::Error::unexpected_message(MESSAGE_TYPE_NEW_TEMPLATE)) + } + TemplateDistribution::SetNewPrevHash(_) => Err(Self::Error::unexpected_message( + MESSAGE_TYPE_SET_NEW_PREV_HASH, + )), + TemplateDistribution::RequestTransactionDataSuccess(_) => Err( + Self::Error::unexpected_message(MESSAGE_TYPE_REQUEST_TRANSACTION_DATA_SUCCESS), + ), + TemplateDistribution::RequestTransactionDataError(_) => Err( + Self::Error::unexpected_message(MESSAGE_TYPE_REQUEST_TRANSACTION_DATA_ERROR), + ), + } + } + } + + async fn handle_coinbase_output_constraints( + &mut self, + client_id: Option, + msg: CoinbaseOutputConstraints, + ) -> Result<(), Self::Error>; + + async fn handle_request_tx_data( + &mut self, + client_id: Option, + msg: RequestTransactionData, + ) -> Result<(), Self::Error>; + async fn handle_submit_solution( + &mut self, + client_id: Option, + msg: SubmitSolution, + ) -> Result<(), Self::Error>; +} diff --git a/protocols/v2/noise-sv2/Cargo.toml b/protocols/v2/noise-sv2/Cargo.toml index eef8a96115..0af2db650e 100644 --- a/protocols/v2/noise-sv2/Cargo.toml +++ b/protocols/v2/noise-sv2/Cargo.toml @@ -2,7 +2,7 @@ name = "noise_sv2" version = "1.4.0" authors = ["The Stratum V2 Developers"] -edition = "2018" +edition = "2021" readme = "README.md" description = "Sv2 noise" documentation = "https://docs.rs/noise_sv2" @@ -17,7 +17,7 @@ rand = {version = "0.8.5", default-features = false } aes-gcm = { version = "0.10.2", features = ["alloc", "aes"], default-features = false } chacha20poly1305 = { version = "0.10.1", default-features = false, features = ["alloc"]} rand_chacha = { version = "0.3.1", default-features = false } -stratum-common = { path = "../../../common", version = "^2.0.0"} +generic-array = "=0.14.7" [features] default = ["std"] diff --git a/protocols/v2/noise-sv2/src/cipher_state.rs b/protocols/v2/noise-sv2/src/cipher_state.rs index 09d75f39c2..be20884eb5 100644 --- a/protocols/v2/noise-sv2/src/cipher_state.rs +++ b/protocols/v2/noise-sv2/src/cipher_state.rs @@ -175,6 +175,7 @@ where // `GenericCipher` enables easy switching between ciphers while maintaining secure key and nonce // management. #[allow(clippy::large_enum_variant)] +#[derive(Clone)] pub enum GenericCipher { ChaCha20Poly1305(Cipher), #[allow(dead_code)] @@ -302,6 +303,7 @@ impl CipherState for GenericCipher { // It stores the optional encryption key, the nonce, and the optional cipher instance itself. The // [`CipherState`] trait is implemented to provide a consistent interface for managing cipher // state across different AEAD ciphers. +#[derive(Clone)] pub struct Cipher { // Optional 32-byte encryption key. k: Option<[u8; 32]>, diff --git a/protocols/v2/noise-sv2/src/initiator.rs b/protocols/v2/noise-sv2/src/initiator.rs index abaa2a03d7..ad198c0bd3 100644 --- a/protocols/v2/noise-sv2/src/initiator.rs +++ b/protocols/v2/noise-sv2/src/initiator.rs @@ -61,6 +61,7 @@ use secp256k1::{ /// exchanges, and maintains the handshake hash, chaining key, and nonce for message encryption. /// After the handshake, it facilitates secure communication using either [`ChaCha20Poly1305`] or /// `AES-GCM` ciphers. Sensitive data is securely erased when no longer needed. +#[derive(Clone)] pub struct Initiator { // Cipher used for encrypting and decrypting messages during the handshake. // diff --git a/protocols/v2/noise-sv2/src/lib.rs b/protocols/v2/noise-sv2/src/lib.rs index 5c9436ea43..5f32af4f50 100644 --- a/protocols/v2/noise-sv2/src/lib.rs +++ b/protocols/v2/noise-sv2/src/lib.rs @@ -105,6 +105,7 @@ const PARITY: secp256k1::Parity = secp256k1::Parity::Even; /// Manages the encryption and decryption of messages between two parties, the [`Initiator`] and /// [`Responder`], using the Noise protocol. A symmetric cipher is used for both encrypting /// outgoing messages and decrypting incoming messages. +#[derive(Clone)] pub struct NoiseCodec { // Cipher to encrypt outgoing messages. encryptor: GenericCipher, diff --git a/protocols/v2/noise-sv2/src/responder.rs b/protocols/v2/noise-sv2/src/responder.rs index 66b4519e03..53997e1cca 100644 --- a/protocols/v2/noise-sv2/src/responder.rs +++ b/protocols/v2/noise-sv2/src/responder.rs @@ -60,6 +60,7 @@ const VERSION: u16 = 0; /// a connection with the initiator. The responder manages key generation, Diffie-Hellman exchanges, /// message decryption, and state transitions, ensuring secure communication. Sensitive /// cryptographic material is securely erased when no longer needed. +#[derive(Clone)] pub struct Responder { // Cipher used for encrypting and decrypting messages during the handshake. // diff --git a/protocols/v2/parsers-sv2/Cargo.toml b/protocols/v2/parsers-sv2/Cargo.toml new file mode 100644 index 0000000000..cea4ff8031 --- /dev/null +++ b/protocols/v2/parsers-sv2/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "parsers_sv2" +version = "0.1.1" +authors = ["The Stratum V2 Developers"] +edition = "2021" +readme = "README.md" +description = "Sv2 Message Parsers" +documentation = "https://docs.rs/parsers_sv2" +license = "MIT OR Apache-2.0" +repository = "https://github.com/stratum-mining/stratum" +homepage = "https://stratumprotocol.org" +keywords = ["stratum", "mining", "bitcoin", "protocol"] + +[dependencies] +binary_sv2 = { path = "../binary-sv2", version = "^4.0.0" } +framing_sv2 = { path = "../framing-sv2", version = "^5.0.0" } +common_messages_sv2 = { path = "../subprotocols/common-messages", version = "^6.0.0" } +mining_sv2 = { path = "../subprotocols/mining", version = "^5.0.0" } +template_distribution_sv2 = { path = "../subprotocols/template-distribution", version = "^4.0.0" } +job_declaration_sv2 = { path = "../subprotocols/job-declaration", version = "^5.0.0" } + +[dev-dependencies] +codec_sv2 = { path = "../codec-sv2", features = ["noise_sv2", "with_buffer_pool"] } diff --git a/protocols/v2/parsers-sv2/README.md b/protocols/v2/parsers-sv2/README.md new file mode 100644 index 0000000000..b23206bacc --- /dev/null +++ b/protocols/v2/parsers-sv2/README.md @@ -0,0 +1,11 @@ +# `parsers_sv2` + +[![crates.io](https://img.shields.io/crates/v/parsers_sv2.svg)](https://crates.io/crates/parsers_sv2) +[![docs.rs](https://docs.rs/parsers_sv2/badge.svg)](https://docs.rs/parsers_sv2) +[![rustc+](https://img.shields.io/badge/rustc-1.75.0%2B-lightgrey.svg)](https://blog.rust-lang.org/2023/12/28/Rust-1.75.0.html) +[![license](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](https://github.com/stratum-mining/stratum/blob/main/LICENSE.md) +[![codecov](https://codecov.io/gh/stratum-mining/stratum/branch/main/graph/badge.svg?flag=parsers_sv2-coverage)](https://codecov.io/gh/stratum-mining/stratum) + +`parsers_sv2` provides logic to convert raw Stratum V2 (Sv2) message data into Rust types, as well as logic to handle conversions among Sv2 Rust types. The crate is `no_std` compatible by default. + +Most of the logic on this crate is tightly coupled with the [`binary_sv2`](https://docs.rs/binary_sv2/latest/binary_sv2/) crate. diff --git a/protocols/v2/parsers-sv2/src/error.rs b/protocols/v2/parsers-sv2/src/error.rs new file mode 100644 index 0000000000..1c6bac1531 --- /dev/null +++ b/protocols/v2/parsers-sv2/src/error.rs @@ -0,0 +1,26 @@ +#[derive(Debug)] +pub enum ParserError { + UnexpectedMessage(u8), + BadPayloadSize, + UnexpectedPoolMessage, + BinaryError(binary_sv2::Error), +} + +impl From for ParserError { + fn from(e: binary_sv2::Error) -> Self { + ParserError::BinaryError(e) + } +} + +impl core::fmt::Display for ParserError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + ParserError::UnexpectedMessage(msg_type) => { + write!(f, "Unexpected message type: {msg_type}") + } + ParserError::BadPayloadSize => write!(f, "Bad payload size"), + ParserError::UnexpectedPoolMessage => write!(f, "Unexpected pool message"), + ParserError::BinaryError(e) => write!(f, "Binary error: {e:?}"), + } + } +} diff --git a/protocols/v2/roles-logic-sv2/src/parsers.rs b/protocols/v2/parsers-sv2/src/lib.rs similarity index 82% rename from protocols/v2/roles-logic-sv2/src/parsers.rs rename to protocols/v2/parsers-sv2/src/lib.rs index a5e13a6bd9..7f7d4d890d 100644 --- a/protocols/v2/roles-logic-sv2/src/parsers.rs +++ b/protocols/v2/parsers-sv2/src/lib.rs @@ -1,7 +1,9 @@ +#![no_std] + //! # Parsing, Serializing, and Message Type Identification //! //! Provides logic to convert raw Stratum V2 (Sv2) message data into Rust types, as well as logic -//! to handle conversions among Sv2 rust types. +//! to handle conversions among Sv2 rust types. The crate is `no_std` compatible by default. //! //! Most of the logic on this module is tightly coupled with the [`binary_sv2`] crate. //! @@ -20,14 +22,21 @@ //! - **Mining Protocol**: Manages standard mining communication (e.g., job dispatch, shares //! submission). -use crate::Error; +pub mod error; +extern crate alloc; +use alloc::vec::Vec; use binary_sv2::{ + self, decodable::{DecodableField, FieldMarker}, encodable::EncodableField, from_bytes, Deserialize, GetSize, }; use common_messages_sv2::*; -use core::convert::{TryFrom, TryInto}; +use core::{ + convert::{TryFrom, TryInto}, + fmt, +}; +pub use error::ParserError; use framing_sv2::framing::Sv2Frame; use job_declaration_sv2::*; use mining_sv2::*; @@ -132,6 +141,18 @@ pub enum CommonMessages<'a> { SetupConnectionSuccess(SetupConnectionSuccess), } +impl fmt::Display for CommonMessages<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CommonMessages::ChannelEndpointChanged(m) => write!(f, "{m}"), + CommonMessages::Reconnect(m) => write!(f, "{m}"), + CommonMessages::SetupConnection(m) => write!(f, "{m}"), + CommonMessages::SetupConnectionError(m) => write!(f, "{m}"), + CommonMessages::SetupConnectionSuccess(m) => write!(f, "{m}"), + } + } +} + /// A parser of messages of Template Distribution subprotocol, to be used for parsing raw messages #[derive(Clone, Debug)] pub enum TemplateDistribution<'a> { @@ -144,6 +165,28 @@ pub enum TemplateDistribution<'a> { SubmitSolution(SubmitSolution<'a>), } +impl fmt::Display for TemplateDistribution<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TemplateDistribution::CoinbaseOutputConstraints(m) => { + write!(f, "CoinbaseOutputConstraints: {m}") + } + TemplateDistribution::NewTemplate(m) => write!(f, "{m}"), + TemplateDistribution::RequestTransactionData(m) => { + write!(f, "{m}") + } + TemplateDistribution::RequestTransactionDataError(m) => { + write!(f, "{m}") + } + TemplateDistribution::RequestTransactionDataSuccess(m) => { + write!(f, "{m}") + } + TemplateDistribution::SetNewPrevHash(m) => write!(f, "{m}"), + TemplateDistribution::SubmitSolution(m) => write!(f, "{m}"), + } + } +} + /// A parser of messages of Job Declaration subprotocol, to be used for parsing raw messages #[derive(Clone, Debug)] pub enum JobDeclaration<'a> { @@ -157,6 +200,29 @@ pub enum JobDeclaration<'a> { PushSolution(PushSolution<'a>), } +impl fmt::Display for JobDeclaration<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + JobDeclaration::AllocateMiningJobToken(m) => write!(f, "AllocateMiningJobToken: {m}"), + JobDeclaration::AllocateMiningJobTokenSuccess(m) => { + write!(f, "AllocateMiningJobTokenSuccess: {m}") + } + JobDeclaration::DeclareMiningJob(m) => write!(f, "DeclareMiningJob: {m}"), + JobDeclaration::DeclareMiningJobError(m) => write!(f, "DeclareMiningJobError: {m}"), + JobDeclaration::DeclareMiningJobSuccess(m) => { + write!(f, "DeclareMiningJobSuccess: {m}") + } + JobDeclaration::ProvideMissingTransactions(m) => { + write!(f, "ProvideMissingTransactions: {m}") + } + JobDeclaration::ProvideMissingTransactionsSuccess(m) => { + write!(f, "ProvideMissingTransactionsSuccess: {m}") + } + JobDeclaration::PushSolution(m) => write!(f, "PushSolution: {m}"), + } + } +} + /// Mining subprotocol messages: categorization, encapsulation, and parsing. /// /// Encapsulates mining-related Sv2 protocol messages, providing both a structured representation @@ -205,6 +271,34 @@ pub enum Mining<'a> { UpdateChannelError(UpdateChannelError<'a>), } +impl fmt::Display for Mining<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Mining::CloseChannel(m) => write!(f, "{m}"), + Mining::NewExtendedMiningJob(m) => write!(f, "{m}"), + Mining::NewMiningJob(m) => write!(f, "{m}"), + Mining::OpenExtendedMiningChannel(m) => write!(f, "{m}"), + Mining::OpenExtendedMiningChannelSuccess(m) => write!(f, "{m}"), + Mining::OpenMiningChannelError(m) => write!(f, "{m}"), + Mining::OpenStandardMiningChannel(m) => write!(f, "{m}"), + Mining::OpenStandardMiningChannelSuccess(m) => write!(f, "{m}"), + Mining::SetCustomMiningJob(m) => write!(f, "{m}"), + Mining::SetCustomMiningJobError(m) => write!(f, "{m}"), + Mining::SetCustomMiningJobSuccess(m) => write!(f, "{m}"), + Mining::SetExtranoncePrefix(m) => write!(f, "{m}"), + Mining::SetGroupChannel(m) => write!(f, "{m}"), + Mining::SetNewPrevHash(m) => write!(f, "{m}"), + Mining::SetTarget(m) => write!(f, "{m}"), + Mining::SubmitSharesError(m) => write!(f, "{m}"), + Mining::SubmitSharesExtended(m) => write!(f, "{m}"), + Mining::SubmitSharesStandard(m) => write!(f, "{m}"), + Mining::SubmitSharesSuccess(m) => write!(f, "{m}"), + Mining::UpdateChannel(m) => write!(f, "{m}"), + Mining::UpdateChannelError(m) => write!(f, "{m}"), + } + } +} + impl Mining<'_> { /// converter into static lifetime pub fn into_static(self) -> Mining<'static> { @@ -244,6 +338,94 @@ impl Mining<'_> { } } +impl CommonMessages<'_> { + /// converter into static lifetime + pub fn into_static(self) -> CommonMessages<'static> { + match self { + CommonMessages::ChannelEndpointChanged(m) => CommonMessages::ChannelEndpointChanged(m), + CommonMessages::Reconnect(m) => CommonMessages::Reconnect(m.into_static()), + CommonMessages::SetupConnection(m) => CommonMessages::SetupConnection(m.into_static()), + CommonMessages::SetupConnectionError(m) => { + CommonMessages::SetupConnectionError(m.into_static()) + } + CommonMessages::SetupConnectionSuccess(m) => CommonMessages::SetupConnectionSuccess(m), + } + } +} + +impl TemplateDistribution<'_> { + /// converter into static lifetime + pub fn into_static(self) -> TemplateDistribution<'static> { + match self { + TemplateDistribution::CoinbaseOutputConstraints(m) => { + TemplateDistribution::CoinbaseOutputConstraints(m) + } + TemplateDistribution::NewTemplate(m) => { + TemplateDistribution::NewTemplate(m.into_static()) + } + TemplateDistribution::RequestTransactionData(m) => { + TemplateDistribution::RequestTransactionData(m) + } + TemplateDistribution::RequestTransactionDataError(m) => { + TemplateDistribution::RequestTransactionDataError(m.into_static()) + } + TemplateDistribution::RequestTransactionDataSuccess(m) => { + TemplateDistribution::RequestTransactionDataSuccess(m.into_static()) + } + TemplateDistribution::SetNewPrevHash(m) => { + TemplateDistribution::SetNewPrevHash(m.into_static()) + } + TemplateDistribution::SubmitSolution(m) => { + TemplateDistribution::SubmitSolution(m.into_static()) + } + } + } +} + +impl JobDeclaration<'_> { + /// converter into static lifetime + pub fn into_static(self) -> JobDeclaration<'static> { + match self { + JobDeclaration::AllocateMiningJobToken(m) => { + JobDeclaration::AllocateMiningJobToken(m.into_static()) + } + JobDeclaration::AllocateMiningJobTokenSuccess(m) => { + JobDeclaration::AllocateMiningJobTokenSuccess(m.into_static()) + } + JobDeclaration::DeclareMiningJob(m) => { + JobDeclaration::DeclareMiningJob(m.into_static()) + } + JobDeclaration::DeclareMiningJobError(m) => { + JobDeclaration::DeclareMiningJobError(m.into_static()) + } + JobDeclaration::DeclareMiningJobSuccess(m) => { + JobDeclaration::DeclareMiningJobSuccess(m.into_static()) + } + JobDeclaration::ProvideMissingTransactions(m) => { + JobDeclaration::ProvideMissingTransactions(m.into_static()) + } + JobDeclaration::ProvideMissingTransactionsSuccess(m) => { + JobDeclaration::ProvideMissingTransactionsSuccess(m.into_static()) + } + JobDeclaration::PushSolution(m) => JobDeclaration::PushSolution(m.into_static()), + } + } +} + +impl AnyMessage<'_> { + /// converter into static lifetime + pub fn into_static(self) -> AnyMessage<'static> { + match self { + AnyMessage::Common(m) => AnyMessage::Common(m.into_static()), + AnyMessage::Mining(m) => AnyMessage::Mining(m.into_static()), + AnyMessage::JobDeclaration(m) => AnyMessage::JobDeclaration(m.into_static()), + AnyMessage::TemplateDistribution(m) => { + AnyMessage::TemplateDistribution(m.into_static()) + } + } + } +} + /// A trait that every Sv2 message parser must implement. /// It helps parsing from Rust types to raw messages. pub trait IsSv2Message { @@ -527,64 +709,64 @@ impl GetSize for Mining<'_> { } impl<'decoder> Deserialize<'decoder> for CommonMessages<'decoder> { - fn get_structure(_v: &[u8]) -> std::result::Result, binary_sv2::Error> { + fn get_structure(_v: &[u8]) -> core::result::Result, binary_sv2::Error> { unimplemented!() } fn from_decoded_fields( _v: Vec>, - ) -> std::result::Result { + ) -> core::result::Result { unimplemented!() } } impl<'decoder> Deserialize<'decoder> for TemplateDistribution<'decoder> { - fn get_structure(_v: &[u8]) -> std::result::Result, binary_sv2::Error> { + fn get_structure(_v: &[u8]) -> core::result::Result, binary_sv2::Error> { unimplemented!() } fn from_decoded_fields( _v: Vec>, - ) -> std::result::Result { + ) -> core::result::Result { unimplemented!() } } impl<'decoder> Deserialize<'decoder> for JobDeclaration<'decoder> { - fn get_structure(_v: &[u8]) -> std::result::Result, binary_sv2::Error> { + fn get_structure(_v: &[u8]) -> core::result::Result, binary_sv2::Error> { unimplemented!() } fn from_decoded_fields( _v: Vec>, - ) -> std::result::Result { + ) -> core::result::Result { unimplemented!() } } impl<'decoder> Deserialize<'decoder> for Mining<'decoder> { - fn get_structure(_v: &[u8]) -> std::result::Result, binary_sv2::Error> { + fn get_structure(_v: &[u8]) -> core::result::Result, binary_sv2::Error> { unimplemented!() } fn from_decoded_fields( _v: Vec>, - ) -> std::result::Result { + ) -> core::result::Result { unimplemented!() } } impl<'decoder> Deserialize<'decoder> for AnyMessage<'decoder> { - fn get_structure(_v: &[u8]) -> std::result::Result, binary_sv2::Error> { + fn get_structure(_v: &[u8]) -> core::result::Result, binary_sv2::Error> { unimplemented!() } fn from_decoded_fields( _v: Vec>, - ) -> std::result::Result { + ) -> core::result::Result { unimplemented!() } } impl<'decoder> Deserialize<'decoder> for MiningDeviceMessages<'decoder> { - fn get_structure(_v: &[u8]) -> std::result::Result, binary_sv2::Error> { + fn get_structure(_v: &[u8]) -> core::result::Result, binary_sv2::Error> { unimplemented!() } fn from_decoded_fields( _v: Vec>, - ) -> std::result::Result { + ) -> core::result::Result { unimplemented!() } } @@ -602,22 +784,22 @@ pub enum CommonMessageTypes { } impl TryFrom for CommonMessageTypes { - type Error = Error; + type Error = ParserError; - fn try_from(v: u8) -> Result { + fn try_from(v: u8) -> Result { match v { MESSAGE_TYPE_SETUP_CONNECTION => Ok(CommonMessageTypes::SetupConnection), MESSAGE_TYPE_SETUP_CONNECTION_SUCCESS => Ok(CommonMessageTypes::SetupConnectionSuccess), MESSAGE_TYPE_SETUP_CONNECTION_ERROR => Ok(CommonMessageTypes::SetupConnectionError), MESSAGE_TYPE_CHANNEL_ENDPOINT_CHANGED => Ok(CommonMessageTypes::ChannelEndpointChanged), MESSAGE_TYPE_RECONNECT => Ok(CommonMessageTypes::Reconnect), - _ => Err(Error::UnexpectedMessage(v)), + _ => Err(ParserError::UnexpectedMessage(v)), } } } impl<'a> TryFrom<(u8, &'a mut [u8])> for CommonMessages<'a> { - type Error = Error; + type Error = ParserError; fn try_from(v: (u8, &'a mut [u8])) -> Result { let msg_type: CommonMessageTypes = v.0.try_into()?; @@ -661,9 +843,9 @@ pub enum TemplateDistributionTypes { } impl TryFrom for TemplateDistributionTypes { - type Error = Error; + type Error = ParserError; - fn try_from(v: u8) -> Result { + fn try_from(v: u8) -> Result { match v { MESSAGE_TYPE_COINBASE_OUTPUT_CONSTRAINTS => { Ok(TemplateDistributionTypes::CoinbaseOutputConstraints) @@ -680,13 +862,13 @@ impl TryFrom for TemplateDistributionTypes { Ok(TemplateDistributionTypes::RequestTransactionDataError) } MESSAGE_TYPE_SUBMIT_SOLUTION => Ok(TemplateDistributionTypes::SubmitSolution), - _ => Err(Error::UnexpectedMessage(v)), + _ => Err(ParserError::UnexpectedMessage(v)), } } } impl<'a> TryFrom<(u8, &'a mut [u8])> for TemplateDistribution<'a> { - type Error = Error; + type Error = ParserError; fn try_from(v: (u8, &'a mut [u8])) -> Result { let msg_type: TemplateDistributionTypes = v.0.try_into()?; @@ -739,9 +921,9 @@ pub enum JobDeclarationTypes { } impl TryFrom for JobDeclarationTypes { - type Error = Error; + type Error = ParserError; - fn try_from(v: u8) -> Result { + fn try_from(v: u8) -> Result { match v { MESSAGE_TYPE_ALLOCATE_MINING_JOB_TOKEN => { Ok(JobDeclarationTypes::AllocateMiningJobToken) @@ -761,13 +943,13 @@ impl TryFrom for JobDeclarationTypes { Ok(JobDeclarationTypes::ProvideMissingTransactionsSuccess) } MESSAGE_TYPE_PUSH_SOLUTION => Ok(JobDeclarationTypes::PushSolution), - _ => Err(Error::UnexpectedMessage(v)), + _ => Err(ParserError::UnexpectedMessage(v)), } } } impl<'a> TryFrom<(u8, &'a mut [u8])> for JobDeclaration<'a> { - type Error = Error; + type Error = ParserError; fn try_from(v: (u8, &'a mut [u8])) -> Result { let msg_type: JobDeclarationTypes = v.0.try_into()?; @@ -837,9 +1019,9 @@ pub enum MiningTypes { } impl TryFrom for MiningTypes { - type Error = Error; + type Error = ParserError; - fn try_from(v: u8) -> Result { + fn try_from(v: u8) -> Result { match v { MESSAGE_TYPE_CLOSE_CHANNEL => Ok(MiningTypes::CloseChannel), MESSAGE_TYPE_NEW_EXTENDED_MINING_JOB => Ok(MiningTypes::NewExtendedMiningJob), @@ -868,14 +1050,14 @@ impl TryFrom for MiningTypes { MESSAGE_TYPE_SUBMIT_SHARES_SUCCESS => Ok(MiningTypes::SubmitSharesSuccess), MESSAGE_TYPE_UPDATE_CHANNEL => Ok(MiningTypes::UpdateChannel), MESSAGE_TYPE_UPDATE_CHANNEL_ERROR => Ok(MiningTypes::UpdateChannelError), - MESSAGE_TYPE_SETUP_CONNECTION => Err(Error::UnexpectedMessage(v)), - _ => Err(Error::UnexpectedMessage(v)), + MESSAGE_TYPE_SETUP_CONNECTION => Err(ParserError::UnexpectedMessage(v)), + _ => Err(ParserError::UnexpectedMessage(v)), } } } impl<'a> TryFrom<(u8, &'a mut [u8])> for Mining<'a> { - type Error = Error; + type Error = ParserError; fn try_from(v: (u8, &'a mut [u8])) -> Result { let msg_type: MiningTypes = v.0.try_into()?; @@ -991,11 +1173,11 @@ impl GetSize for MiningDeviceMessages<'_> { } } impl<'a> TryFrom<(u8, &'a mut [u8])> for MiningDeviceMessages<'a> { - type Error = Error; + type Error = ParserError; fn try_from(v: (u8, &'a mut [u8])) -> Result { - let is_common: Result = v.0.try_into(); - let is_mining: Result = v.0.try_into(); + let is_common: Result = v.0.try_into(); + let is_mining: Result = v.0.try_into(); match (is_common, is_mining) { (Ok(_), Err(_)) => Ok(Self::Common(v.try_into()?)), (Err(_), Ok(_)) => Ok(Self::Mining(v.try_into()?)), @@ -1015,8 +1197,19 @@ pub enum AnyMessage<'a> { TemplateDistribution(TemplateDistribution<'a>), } +impl fmt::Display for AnyMessage<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AnyMessage::Common(m) => write!(f, "CommonMessage: {m}"), + AnyMessage::Mining(m) => write!(f, "MiningMessage: {m}"), + AnyMessage::JobDeclaration(m) => write!(f, "JobDeclarationMessage: {m}"), + AnyMessage::TemplateDistribution(m) => write!(f, "TemplateDistributionMessage: {m}"), + } + } +} + impl<'a> TryFrom> for AnyMessage<'a> { - type Error = Error; + type Error = ParserError; fn try_from(value: MiningDeviceMessages<'a>) -> Result { match value { @@ -1084,13 +1277,14 @@ impl IsSv2Message for MiningDeviceMessages<'_> { } impl<'a> TryFrom<(u8, &'a mut [u8])> for AnyMessage<'a> { - type Error = Error; + type Error = ParserError; fn try_from(v: (u8, &'a mut [u8])) -> Result { - let is_common: Result = v.0.try_into(); - let is_mining: Result = v.0.try_into(); - let is_job_declaration: Result = v.0.try_into(); - let is_template_distribution: Result = v.0.try_into(); + let is_common: Result = v.0.try_into(); + let is_mining: Result = v.0.try_into(); + let is_job_declaration: Result = v.0.try_into(); + let is_template_distribution: Result = + v.0.try_into(); match ( is_common, is_mining, @@ -1157,67 +1351,66 @@ impl<'a, T: Into>> From for MiningDeviceMessages<'a> { impl<'decoder, B: AsMut<[u8]> + AsRef<[u8]>> TryFrom> for Sv2Frame, B> { - type Error = Error; + type Error = ParserError; - fn try_from(v: AnyMessage<'decoder>) -> Result { + fn try_from(v: AnyMessage<'decoder>) -> Result { let extension_type = 0; let channel_bit = v.channel_bit(); let message_type = v.message_type(); Sv2Frame::from_message(v, message_type, extension_type, channel_bit) - .ok_or(Error::BadPayloadSize) + .ok_or(ParserError::BadPayloadSize) } } impl<'decoder, B: AsMut<[u8]> + AsRef<[u8]>> TryFrom> for Sv2Frame, B> { - type Error = Error; + type Error = ParserError; - fn try_from(v: MiningDeviceMessages<'decoder>) -> Result { + fn try_from(v: MiningDeviceMessages<'decoder>) -> Result { let extension_type = 0; let channel_bit = v.channel_bit(); let message_type = v.message_type(); Sv2Frame::from_message(v, message_type, extension_type, channel_bit) - .ok_or(Error::BadPayloadSize) + .ok_or(ParserError::BadPayloadSize) } } impl<'decoder, B: AsMut<[u8]> + AsRef<[u8]>> TryFrom> for Sv2Frame, B> { - type Error = Error; + type Error = ParserError; - fn try_from(v: TemplateDistribution<'decoder>) -> Result { + fn try_from(v: TemplateDistribution<'decoder>) -> Result { let extension_type = 0; let channel_bit = v.channel_bit(); let message_type = v.message_type(); Sv2Frame::from_message(v, message_type, extension_type, channel_bit) - .ok_or(Error::BadPayloadSize) + .ok_or(ParserError::BadPayloadSize) } } impl<'a> TryFrom> for MiningDeviceMessages<'a> { - type Error = Error; + type Error = ParserError; - fn try_from(value: AnyMessage<'a>) -> Result { + fn try_from(value: AnyMessage<'a>) -> Result { match value { AnyMessage::Common(message) => Ok(Self::Common(message)), AnyMessage::Mining(message) => Ok(Self::Mining(message)), - AnyMessage::JobDeclaration(_) => Err(Error::UnexpectedPoolMessage), - AnyMessage::TemplateDistribution(_) => Err(Error::UnexpectedPoolMessage), + AnyMessage::JobDeclaration(_) => Err(ParserError::UnexpectedPoolMessage), + AnyMessage::TemplateDistribution(_) => Err(ParserError::UnexpectedPoolMessage), } } } #[cfg(test)] mod test { - use crate::{ - mining_sv2::NewMiningJob, - parsers::{AnyMessage, Mining}, - }; + use crate::{AnyMessage, Mining}; + use alloc::vec::Vec; use binary_sv2::{Sv2Option, U256}; use codec_sv2::StandardSv2Frame; - use std::convert::{TryFrom, TryInto}; + use core::convert::{TryFrom, TryInto}; + use mining_sv2::NewMiningJob; pub type Message = AnyMessage<'static>; pub type StdFrame = StandardSv2Frame; @@ -1307,8 +1500,7 @@ mod test { let payload_length = extract_payload(serialized_frame).len(); assert_eq!( message_length, payload_length, - "Header declared length [{} bytes] differs from the actual payload length [{} bytes]", - message_length, payload_length, + "Header declared length [{message_length} bytes] differs from the actual payload length [{payload_length} bytes]", ); } } diff --git a/protocols/v2/roles-logic-sv2/Cargo.toml b/protocols/v2/roles-logic-sv2/Cargo.toml index 450e1a1616..04f66aa850 100644 --- a/protocols/v2/roles-logic-sv2/Cargo.toml +++ b/protocols/v2/roles-logic-sv2/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "roles_logic_sv2" -version = "3.0.0" +version = "5.0.0" authors = ["The Stratum V2 Developers"] -edition = "2018" +edition = "2021" description = "Common handlers for use within SV2 roles" documentation = "https://docs.rs/roles_logic_sv2" license = "MIT OR Apache-2.0" @@ -13,21 +13,22 @@ keywords = ["stratum", "mining", "bitcoin", "protocol"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -stratum-common = { path = "../../../common", features=["bitcoin"], version = "^2.0.0"} -binary_sv2 = { path = "../../../protocols/v2/binary-sv2", version = "^3.0.0" } -common_messages_sv2 = { path = "../../../protocols/v2/subprotocols/common-messages", version = "^5.0.0" } -mining_sv2 = { path = "../../../protocols/v2/subprotocols/mining", version = "^4.0.0" } -template_distribution_sv2 = { path = "../../../protocols/v2/subprotocols/template-distribution", version = "^3.0.0" } -job_declaration_sv2 = { path = "../../../protocols/v2/subprotocols/job-declaration", version = "^4.0.0" } -framing_sv2 = { path = "../../../protocols/v2/framing-sv2", version = "^5.0.0" } +bitcoin = { version = "0.32.5" } +channels_sv2 = { path = "../channels-sv2", version = "^2.0.0" } +parsers_sv2 = { path = "../parsers-sv2", version = "^0.1.0" } +handlers_sv2 = { path = "../handlers-sv2", version = "^0.2.0" } +common_messages_sv2 = { path = "../../../protocols/v2/subprotocols/common-messages", version = "^6.0.0" } +mining_sv2 = { path = "../../../protocols/v2/subprotocols/mining", version = "^5.0.0" } +template_distribution_sv2 = { path = "../../../protocols/v2/subprotocols/template-distribution", version = "^4.0.0" } +job_declaration_sv2 = { path = "../../../protocols/v2/subprotocols/job-declaration", version = "^5.0.0" } tracing = { version = "0.1"} chacha20poly1305 = { version = "0.10.1"} nohash-hasher = "0.2.0" primitive-types = "0.13.1" hex = {package = "hex-conservative", version = "0.3.0"} +codec_sv2 = { path = "../../../protocols/v2/codec-sv2", version = "^3.0.0", features = ["noise_sv2", "with_buffer_pool"] } [dev-dependencies] -codec_sv2 = { path = "../../../protocols/v2/codec-sv2", version = "^2.0.0" } quickcheck = "1.0.3" quickcheck_macros = "1" rand = "0.8.5" diff --git a/protocols/v2/roles-logic-sv2/src/channel_logic/channel_factory.rs b/protocols/v2/roles-logic-sv2/src/channel_logic/channel_factory.rs deleted file mode 100644 index c99aef3757..0000000000 --- a/protocols/v2/roles-logic-sv2/src/channel_logic/channel_factory.rs +++ /dev/null @@ -1,2034 +0,0 @@ -//! # Channel Factory -//! -//! This module contains logic for creating and managing channels. - -use super::extended_to_standard_job; -use crate::{ - common_properties::StandardChannel, - job_creator::{self, JobsCreators}, - parsers::Mining, - utils::{GroupId, Id, Mutex}, - Error, -}; - -use mining_sv2::{ - ExtendedExtranonce, NewExtendedMiningJob, NewMiningJob, OpenExtendedMiningChannelSuccess, - OpenMiningChannelError, OpenStandardMiningChannelSuccess, SetCustomMiningJob, - SetCustomMiningJobSuccess, SetNewPrevHash, SubmitSharesError, SubmitSharesExtended, - SubmitSharesStandard, Target, -}; - -use hex::DisplayHex; -use nohash_hasher::BuildNoHashHasher; -use std::{collections::HashMap, convert::TryInto, sync::Arc}; -use template_distribution_sv2::{NewTemplate, SetNewPrevHash as SetNewPrevHashFromTp}; - -use tracing::{debug, error, info, trace, warn}; - -use stratum_common::bitcoin::{ - block::{Header, Version}, - hash_types, - hashes::sha256d::Hash, - CompactTarget, TxOut, -}; - -/// A stripped type of `SetCustomMiningJob` without the (`channel_id, `request_id` and `token`) -/// fields -#[derive(Debug)] -pub struct PartialSetCustomMiningJob { - pub version: u32, - pub prev_hash: binary_sv2::U256<'static>, - pub min_ntime: u32, - pub nbits: u32, - pub coinbase_tx_version: u32, - pub coinbase_prefix: binary_sv2::B0255<'static>, - pub coinbase_tx_input_n_sequence: u32, - pub coinbase_tx_value_remaining: u64, - pub coinbase_tx_outputs: binary_sv2::B064K<'static>, - pub coinbase_tx_locktime: u32, - pub merkle_path: binary_sv2::Seq0255<'static, binary_sv2::U256<'static>>, - pub future_job: bool, -} - -/// Represents the action that needs to be done when a new share is received. -#[derive(Debug, Clone)] -pub enum OnNewShare { - /// Used when the received is malformed, is for an inexistent channel or do not meet downstream - /// target. - SendErrorDownstream(SubmitSharesError<'static>), - /// Used when an extended channel in a proxy receive a share, and the share meet upstream - /// target, in this case a new share must be sent upstream. Also an optional template id is - /// returned, when a job declarator want to send a valid share upstream could use the - /// template for get the up job id. - SendSubmitShareUpstream((Share, Option)), - /// Used when a group channel in a proxy receive a share that is not malformed and is for a - /// valid channel in that case we relay the same exact share upstream with a new request id. - RelaySubmitShareUpstream, - /// Indicate that the share meet bitcoin target, when there is an upstream the we should send - /// the share upstream, whenever possible we should also notify the TP about it. - /// When a pool negotiate a job with downstream we do not have the template_id so we set it to - /// None - /// (share, template id, coinbase,complete extranonce) - ShareMeetBitcoinTarget((Share, Option, Vec, Vec)), - /// Indicate that the share meet downstream target, in the case we could send a success - /// response downstream. - ShareMeetDownstreamTarget, -} - -impl OnNewShare { - /// Converts standard share into extended share - pub fn into_extended(&mut self, extranonce: Vec, up_id: u32) { - match self { - OnNewShare::SendErrorDownstream(_) => (), - OnNewShare::SendSubmitShareUpstream((share, template_id)) => match share { - Share::Extended(_) => (), - Share::Standard((share, _)) => { - let share = SubmitSharesExtended { - channel_id: up_id, - sequence_number: share.sequence_number, - job_id: share.job_id, - nonce: share.nonce, - ntime: share.ntime, - version: share.version, - extranonce: extranonce.try_into().unwrap(), - }; - *self = Self::SendSubmitShareUpstream((Share::Extended(share), *template_id)); - } - }, - OnNewShare::RelaySubmitShareUpstream => (), - OnNewShare::ShareMeetBitcoinTarget((share, t_id, coinbase, ext)) => match share { - Share::Extended(_) => (), - Share::Standard((share, _)) => { - let share = SubmitSharesExtended { - channel_id: up_id, - sequence_number: share.sequence_number, - job_id: share.job_id, - nonce: share.nonce, - ntime: share.ntime, - version: share.version, - extranonce: extranonce.try_into().unwrap(), - }; - *self = Self::ShareMeetBitcoinTarget(( - Share::Extended(share), - *t_id, - coinbase.clone(), - ext.to_vec(), - )); - } - }, - OnNewShare::ShareMeetDownstreamTarget => todo!(), - } - } -} - -/// A share can be either extended or standard -#[derive(Clone, Debug)] -pub enum Share { - Extended(SubmitSharesExtended<'static>), - // share, group id - Standard((SubmitSharesStandard, u32)), -} - -/// Helper type used before a `SetNewPrevHash` has a channel_id -#[derive(Clone, Debug)] -pub struct StagedPhash { - job_id: u32, - prev_hash: binary_sv2::U256<'static>, - min_ntime: u32, - nbits: u32, -} - -impl StagedPhash { - /// Converts a Staged PrevHash into a SetNewPrevHash message - pub fn into_set_p_hash( - &self, - channel_id: u32, - new_job_id: Option, - ) -> SetNewPrevHash<'static> { - SetNewPrevHash { - channel_id, - job_id: new_job_id.unwrap_or(self.job_id), - prev_hash: self.prev_hash.clone(), - min_ntime: self.min_ntime, - nbits: self.nbits, - } - } -} - -impl Share { - /// Get share sequence number - pub fn get_sequence_number(&self) -> u32 { - match self { - Share::Extended(s) => s.sequence_number, - Share::Standard(s) => s.0.sequence_number, - } - } - - /// Get share channel id - pub fn get_channel_id(&self) -> u32 { - match self { - Share::Extended(s) => s.channel_id, - Share::Standard(s) => s.0.channel_id, - } - } - - /// Get share timestamp - pub fn get_n_time(&self) -> u32 { - match self { - Share::Extended(s) => s.ntime, - Share::Standard(s) => s.0.ntime, - } - } - - /// Get share nonce - pub fn get_nonce(&self) -> u32 { - match self { - Share::Extended(s) => s.nonce, - Share::Standard(s) => s.0.nonce, - } - } - - /// Get share job id - pub fn get_job_id(&self) -> u32 { - match self { - Share::Extended(s) => s.job_id, - Share::Standard(s) => s.0.job_id, - } - } - - /// Get share version - pub fn get_version(&self) -> u32 { - match self { - Share::Extended(s) => s.version, - Share::Standard(s) => s.0.version, - } - } -} - -#[derive(Debug)] -/// Basic logic shared between all the channel factories -struct ChannelFactory { - ids: Arc>, - standard_channels_for_non_hom_downstreams: - HashMap>, - standard_channels_for_hom_downstreams: HashMap>, - extended_channels: - HashMap, BuildNoHashHasher>, - extranonces: ExtendedExtranonce, - share_per_min: f32, - // (NewExtendedMiningJob,group ids that already received the future job) - future_jobs: Vec<(NewExtendedMiningJob<'static>, Vec)>, - // (SetNewPrevHash,group ids that already received the set prev_hash) - last_prev_hash: Option<(StagedPhash, Vec)>, - last_prev_hash_: Option, - // (NewExtendedMiningJob,group ids that already received the job) - last_valid_job: Option<(NewExtendedMiningJob<'static>, Vec)>, - kind: ExtendedChannelKind, - job_ids: Id, - channel_to_group_id: HashMap>, - future_templates: HashMap, BuildNoHashHasher>, -} - -impl ChannelFactory { - pub fn add_standard_channel( - &mut self, - request_id: u32, - downstream_hash_rate: f32, - is_header_only: bool, - id: u32, - ) -> Result, Error> { - match is_header_only { - true => { - self.new_standard_channel_for_hom_downstream(request_id, downstream_hash_rate, id) - } - false => self.new_standard_channel_for_non_hom_downstream( - request_id, - downstream_hash_rate, - id, - ), - } - } - - /// Called when a `OpenExtendedMiningChannel` message is received. - /// Here we save the downstream's target (based on hashrate) and the - /// channel's extranonce details before returning the relevant SV2 mining messages - /// to be sent downstream. For the mining messages, we will first return an - /// `OpenExtendedMiningChannelSuccess` if the channel is successfully opened. Then we add - /// the `NewExtendedMiningJob` and `SetNewPrevHash` messages if the relevant data is - /// available. If the channel opening fails, we return `OpenExtendedMiningChannelError`. - pub fn new_extended_channel( - &mut self, - request_id: u32, - hash_rate: f32, - min_extranonce_size: u16, - ) -> Result>, Error> { - let extended_channels_group = 0; - let max_extranonce_size = self.extranonces.get_range2_len() as u16; - if min_extranonce_size <= max_extranonce_size { - // SECURITY is very unlikely to finish the ids btw this unwrap could be used by an - // attacker that want to disrupt the service maybe we should have a method - // to reuse ids that are no longer connected? - let channel_id = self - .ids - .safe_lock(|ids| ids.new_channel_id(extended_channels_group)) - .unwrap(); - self.channel_to_group_id.insert(channel_id, 0); - let target = match crate::utils::hash_rate_to_target( - hash_rate.into(), - self.share_per_min.into(), - ) { - Ok(target) => target, - Err(e) => { - error!( - "Impossible to get target: {:?}. Request id: {:?}", - e, request_id - ); - return Err(e); - } - }; - let extranonce_prefix = self - .extranonces - .next_prefix_extended(max_extranonce_size as usize) - .unwrap() - .into_b032(); - let success = OpenExtendedMiningChannelSuccess { - request_id, - channel_id, - target, - extranonce_size: max_extranonce_size, - extranonce_prefix, - }; - self.extended_channels.insert(channel_id, success.clone()); - let mut result = vec![Mining::OpenExtendedMiningChannelSuccess(success)]; - if let Some((job, _)) = &self.last_valid_job { - let mut job = job.clone(); - job.set_future(); - let j_id = job.job_id; - result.push(Mining::NewExtendedMiningJob(job)); - if let Some((new_prev_hash, _)) = &self.last_prev_hash { - let mut new_prev_hash = new_prev_hash.into_set_p_hash(channel_id, None); - new_prev_hash.job_id = j_id; - result.push(Mining::SetNewPrevHash(new_prev_hash.clone())) - }; - } else if let Some((new_prev_hash, _)) = &self.last_prev_hash { - let new_prev_hash = new_prev_hash.into_set_p_hash(channel_id, None); - result.push(Mining::SetNewPrevHash(new_prev_hash.clone())) - }; - for (job, _) in &self.future_jobs { - result.push(Mining::NewExtendedMiningJob(job.clone())) - } - Ok(result) - } else { - Ok(vec![Mining::OpenMiningChannelError( - OpenMiningChannelError::unsupported_extranonce_size(request_id), - )]) - } - } - - /// Called when we want to replicate a channel already opened by another actor. - /// It is used only in the jd client from the template provider module to mock a pool. - /// Anything else should open channel with the new_extended_channel function - pub fn replicate_upstream_extended_channel_only_jd( - &mut self, - target: binary_sv2::U256<'static>, - extranonce: mining_sv2::Extranonce, - channel_id: u32, - extranonce_size: u16, - ) -> Option<()> { - self.channel_to_group_id.insert(channel_id, 0); - let extranonce_prefix = extranonce.into(); - let success = OpenExtendedMiningChannelSuccess { - request_id: 0, - channel_id, - target, - extranonce_size, - extranonce_prefix, - }; - self.extended_channels.insert(channel_id, success.clone()); - Some(()) - } - /// Called when an `OpenStandardChannel` message is received for a header only mining channel. - /// Here we save the downstream's target (based on hashrate) and and the - /// channel's extranonce details before returning the relevant SV2 mining messages - /// to be sent downstream. - fn new_standard_channel_for_hom_downstream( - &mut self, - request_id: u32, - downstream_hash_rate: f32, - id: u32, - ) -> Result, Error> { - let hom_group_id = 0; - let mut result = vec![]; - let channel_id = id; - let target = match crate::utils::hash_rate_to_target( - downstream_hash_rate.into(), - self.share_per_min.into(), - ) { - Ok(target) => target, - Err(e) => { - error!( - "Impossible to get target: {:?}. Request id: {:?}", - e, request_id - ); - return Err(e); - } - }; - let extranonce = self - .extranonces - .next_prefix_standard() - .map_err(|_| Error::ExtranonceSpaceEnded)?; - let standard_channel = StandardChannel { - channel_id, - group_id: hom_group_id, - target: target.clone().into(), - extranonce: extranonce.clone(), - }; - self.standard_channels_for_hom_downstreams - .insert(channel_id, standard_channel); - - // First message to be sent is OpenStandardMiningChannelSuccess - result.push(Mining::OpenStandardMiningChannelSuccess( - OpenStandardMiningChannelSuccess { - request_id: request_id.into(), - channel_id, - target, - extranonce_prefix: extranonce.into(), - group_channel_id: hom_group_id, - }, - )); - self.prepare_standard_jobs_and_p_hash(&mut result, channel_id)?; - self.channel_to_group_id.insert(channel_id, hom_group_id); - Ok(result) - } - - /// This function is called when downstream have a group channel - /// should not all standard channel's be non HOM?? - fn new_standard_channel_for_non_hom_downstream( - &mut self, - request_id: u32, - downstream_hash_rate: f32, - group_id: u32, - ) -> Result, Error> { - let mut result = vec![]; - let channel_id = self - .ids - .safe_lock(|ids| ids.new_channel_id(group_id)) - .unwrap(); - let complete_id = GroupId::into_complete_id(group_id, channel_id); - let target = match crate::utils::hash_rate_to_target( - downstream_hash_rate.into(), - self.share_per_min.into(), - ) { - Ok(target_) => target_, - Err(e) => { - info!( - "Impossible to get target: {:?}. Request id: {:?}", - e, request_id - ); - return Err(e); - } - }; - let extranonce = self - .extranonces - .next_prefix_standard() - .map_err(|_| Error::ExtranonceSpaceEnded)?; - let standard_channel = StandardChannel { - channel_id, - group_id, - target: target.clone().into(), - extranonce: extranonce.clone(), - }; - self.standard_channels_for_non_hom_downstreams - .insert(complete_id, standard_channel); - - // First message to be sent is OpenStandardMiningChannelSuccess - result.push(Mining::OpenStandardMiningChannelSuccess( - OpenStandardMiningChannelSuccess { - request_id: request_id.into(), - channel_id, - target, - extranonce_prefix: extranonce.into(), - group_channel_id: group_id, - }, - )); - self.prepare_jobs_and_p_hash(&mut result, complete_id); - self.channel_to_group_id.insert(channel_id, group_id); - Ok(result) - } - - // When a hom downstream opens a channel, we use this function to prepare all the standard jobs - // (future and not) that we need to be sent downstream - fn prepare_standard_jobs_and_p_hash( - &mut self, - result: &mut Vec, - channel_id: u32, - ) -> Result<(), Error> { - // Safe cause the function is private and we always add the channel before calling this - // funtion - let standard_channel = self - .standard_channels_for_hom_downstreams - .get(&channel_id) - .unwrap(); - // OPTIMIZATION this could be memoized somewhere cause is very likely that we will receive a - // lot od OpenStandardMiningChannel requests consecutively - let job_id = self.job_ids.next(); - let future_jobs: Option>> = self - .future_jobs - .iter() - .map(|j| { - extended_to_standard_job( - &j.0, - &standard_channel.extranonce.clone().to_vec()[..], - standard_channel.channel_id, - Some(job_id), - ) - }) - .collect(); - - // OPTIMIZATION the extranonce is cloned so many time but maybe is avoidable? - let last_valid_job = match &self.last_valid_job { - Some((j, _)) => Some( - extended_to_standard_job( - j, - &standard_channel.extranonce.clone().to_vec(), - standard_channel.channel_id, - Some(self.job_ids.next()), - ) - .ok_or(Error::ImpossibleToCalculateMerkleRoot)?, - ), - None => None, - }; - - // This is the same thing of just check if there is a prev hash add it to result. If there - // is last_job add it to result and add each future job to result. - // But using the pattern match is more clear how each option is handled - match ( - &self.last_prev_hash, - last_valid_job, - self.future_jobs.is_empty(), - ) { - // If we do not have anything just do nothing - (None, None, true) => Ok(()), - // If we have only future jobs we need to send them all after the - // SetupConnectionSuccess message - (None, None, false) => { - // Safe unwrap cause we check that self.future_jobs is not empty - let mut future_jobs = future_jobs.unwrap(); - while let Some(job) = future_jobs.pop() { - result.push(Mining::NewMiningJob(job)); - } - Ok(()) - } - // If we have just a prev hash we need to send it after the SetupConnectionSuccess - // message - (Some((prev_h, _)), None, true) => { - let prev_h = prev_h.into_set_p_hash(channel_id, None); - result.push(Mining::SetNewPrevHash(prev_h.clone())); - Ok(()) - } - // If we have a prev hash and a last valid job we need to send new mining job before the - // prev hash - (Some((prev_h, _)), Some(mut job), true) => { - let prev_h = prev_h.into_set_p_hash(channel_id, Some(job.job_id)); - - // set future_job to true - job.set_future(); - - result.push(Mining::NewMiningJob(job)); - result.push(Mining::SetNewPrevHash(prev_h.clone())); - Ok(()) - } - // If we have everything we need, send the future jobs and the the prev hash - (Some((prev_h, _)), Some(mut job), false) => { - let prev_h = prev_h.into_set_p_hash(channel_id, Some(job.job_id)); - - job.set_future(); - - result.push(Mining::NewMiningJob(job)); - result.push(Mining::SetNewPrevHash(prev_h.clone())); - - // Safe unwrap cause we check that self.future_jobs is not empty - let mut future_jobs = future_jobs.unwrap(); - - while let Some(job) = future_jobs.pop() { - result.push(Mining::NewMiningJob(job)); - } - Ok(()) - } - // This can not happen because we can not have a valid job without a prev hash - (None, Some(_), true) => unreachable!(), - // This can not happen because we can not have a valid job without a prev hash - (None, Some(_), false) => unreachable!(), - // This can not happen because as soon as a prev hash is received we flush the future - // jobs - (Some(_), None, false) => unreachable!(), - } - } - - // When a new non HOM downstream opens a channel, we use this function to prepare all the - // extended jobs (future and non) and the prev hash that we need to send downstream - fn prepare_jobs_and_p_hash(&mut self, result: &mut Vec, complete_id: u64) { - // If group is 0 it means that we are preparing jobs and p hash for a non HOM downstream - // that want to open a new extended channel in that case we want to use the channel id - // TODO verify that this is true also for the case where the channel factory is in a proxy - // and not in a pool. - let group_id = match GroupId::into_group_id(complete_id) { - 0 => GroupId::into_channel_id(complete_id), - a => a, - }; - // This is the same thing of just check if there is a prev hash add it to result if there - // is last_job add it to result and add each future job to result. - // But using the pattern match is more clear how each option is handled - match ( - self.last_prev_hash.as_mut(), - self.last_valid_job.as_mut(), - self.future_jobs.is_empty(), - ) { - // If we do not have anything just do nothing - (None, None, true) => (), - // If we have only future jobs we need to send them all after the - // SetupConnectionSuccess message - (None, None, false) => { - for (job, group_id_job_sent) in &mut self.future_jobs { - if !group_id_job_sent.contains(&group_id) { - let mut job = job.clone(); - job.channel_id = group_id; - group_id_job_sent.push(group_id); - result.push(Mining::NewExtendedMiningJob(job)); - } - } - } - // If we have just a prev hash we need to send it after the SetupConnectionSuccess - // message - (Some((prev_h, group_id_p_hash_sent)), None, true) => { - if !group_id_p_hash_sent.contains(&group_id) { - let prev_h = prev_h.into_set_p_hash(group_id, None); - group_id_p_hash_sent.push(group_id); - result.push(Mining::SetNewPrevHash(prev_h.clone())); - } - } - // If we have a prev hash and a last valid job we need to send before the prev hash and - // the the valid job - (Some((prev_h, group_id_p_hash_sent)), Some((job, group_id_job_sent)), true) => { - if !group_id_p_hash_sent.contains(&group_id) { - let prev_h = prev_h.into_set_p_hash(group_id, Some(job.job_id)); - group_id_p_hash_sent.push(group_id); - result.push(Mining::SetNewPrevHash(prev_h)); - } - if !group_id_job_sent.contains(&group_id) { - let mut job = job.clone(); - job.channel_id = group_id; - group_id_job_sent.push(group_id); - result.push(Mining::NewExtendedMiningJob(job)); - } - } - // If we have everything we need, send before the prev hash and then all the jobs - (Some((prev_h, group_id_p_hash_sent)), Some((job, group_id_job_sent)), false) => { - if !group_id_p_hash_sent.contains(&group_id) { - let prev_h = prev_h.into_set_p_hash(group_id, Some(job.job_id)); - group_id_p_hash_sent.push(group_id); - result.push(Mining::SetNewPrevHash(prev_h)); - } - - if !group_id_job_sent.contains(&group_id) { - let mut job = job.clone(); - job.channel_id = group_id; - group_id_job_sent.push(group_id); - result.push(Mining::NewExtendedMiningJob(job)); - } - - for (job, group_id_future_j_sent) in &mut self.future_jobs { - if !group_id_future_j_sent.contains(&group_id) { - let mut job = job.clone(); - job.channel_id = group_id; - group_id_future_j_sent.push(group_id); - result.push(Mining::NewExtendedMiningJob(job)); - } - } - } - // This can not happen because we can not have a valid job without a prev hash - (None, Some(_), true) => unreachable!(), - // This can not happen because we can not have a valid job without a prev hash - (None, Some(_), false) => unreachable!(), - // This can not happen because as soon as a prev hash is received we flush the future - // jobs - (Some(_), None, false) => unreachable!(), - } - } - - /// Called when a new prev hash is received. If the respective job is available in the future - /// job queue, we move the future job into the valid job slot and store the prev hash as the - /// current prev hash to be referenced. - fn on_new_prev_hash(&mut self, m: StagedPhash) -> Result<(), Error> { - while let Some(mut job) = self.future_jobs.pop() { - if job.0.job_id == m.job_id { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() as u32; - job.0.set_no_future(now); - self.last_valid_job = Some(job); - break; - } - self.last_valid_job = None; - } - self.future_jobs = vec![]; - self.last_prev_hash_ = Some(crate::utils::u256_to_block_hash(m.prev_hash.clone())); - let mut ids = vec![]; - for complete_id in self.standard_channels_for_non_hom_downstreams.keys() { - let group_id = GroupId::into_group_id(*complete_id); - if !ids.contains(&group_id) { - ids.push(group_id) - } - } - self.last_prev_hash = Some((m, ids)); - Ok(()) - } - - /// Called when a `NewExtendedMiningJob` arrives. If the job is future, we add it to the future - /// queue. If the job is not future, we pair it with a the most recent prev hash - fn on_new_extended_mining_job( - &mut self, - m: NewExtendedMiningJob<'static>, - ) -> Result, BuildNoHashHasher>, Error> { - match (m.is_future(), &self.last_prev_hash) { - (true, _) => { - let mut result = HashMap::with_hasher(BuildNoHashHasher::default()); - self.prepare_jobs_for_downstream_on_new_extended(&mut result, &m)?; - let mut ids = vec![]; - for complete_id in self.standard_channels_for_non_hom_downstreams.keys() { - let group_id = GroupId::into_group_id(*complete_id); - if !ids.contains(&group_id) { - ids.push(group_id) - } - } - self.future_jobs.push((m, ids)); - Ok(result) - } - (false, Some(_)) => { - let mut result = HashMap::with_hasher(BuildNoHashHasher::default()); - self.prepare_jobs_for_downstream_on_new_extended(&mut result, &m)?; - // If job is not future it must always be paired with the last received prev hash - let mut ids = vec![]; - for complete_id in self.standard_channels_for_non_hom_downstreams.keys() { - let group_id = GroupId::into_group_id(*complete_id); - if !ids.contains(&group_id) { - ids.push(group_id) - } - } - self.last_valid_job = Some((m, ids)); - if let Some((_p_hash, _)) = &self.last_prev_hash { - Ok(result) - } else { - Err(Error::JobIsNotFutureButPrevHashNotPresent) - } - } - // This should not happen when a non future job is received we always need to have a - // prev hash - (false, None) => Err(Error::JobIsNotFutureButPrevHashNotPresent), - } - } - - // When a new extended job is received we use this function to prepare the jobs to be sent - // downstream (standard for hom and this job for non hom) - fn prepare_jobs_for_downstream_on_new_extended( - &mut self, - result: &mut HashMap>, - m: &NewExtendedMiningJob<'static>, - ) -> Result<(), Error> { - for (id, channel) in &self.standard_channels_for_hom_downstreams { - let job_id = self.job_ids.next(); - let mut standard_job = extended_to_standard_job( - m, - &channel.extranonce.clone().to_vec()[..], - *id, - Some(job_id), - ) - .unwrap(); - standard_job.channel_id = *id; - let standard_job = Mining::NewMiningJob(standard_job); - result.insert(*id, standard_job); - } - for id in self.standard_channels_for_non_hom_downstreams.keys() { - let group_id = GroupId::into_group_id(*id); - let mut extended = m.clone(); - extended.channel_id = group_id; - let extended_job = Mining::NewExtendedMiningJob(extended); - result.insert(group_id, extended_job); - } - for id in self.extended_channels.keys() { - let mut extended = m.clone(); - extended.channel_id = *id; - let extended_job = Mining::NewExtendedMiningJob(extended); - result.insert(*id, extended_job); - } - Ok(()) - } - - // If there is job creator, bitcoin_target is retrieved from there. If not, it is set to 0. - // If there is a job creator we pass the correct template id. If not, we pass `None` - // allow comparison chain because clippy wants to make job management assertion into a match - // clause - #[allow(clippy::comparison_chain)] - #[allow(clippy::too_many_arguments)] - fn check_target>( - &mut self, - mut m: Share, - bitcoin_target: Target, - template_id: Option, - up_id: u32, - merkle_path: Vec, - coinbase_tx_prefix: &[u8], - coinbase_tx_suffix: &[u8], - prev_blockhash: hash_types::BlockHash, - bits: u32, - ) -> Result { - debug!("Checking target for share {:?}", m); - let upstream_target = match &self.kind { - ExtendedChannelKind::Pool => Target::new(0, 0), - ExtendedChannelKind::Proxy { - upstream_target, .. - } - | ExtendedChannelKind::ProxyJd { - upstream_target, .. - } => upstream_target.clone(), - }; - - let (downstream_target, extranonce) = self - .get_channel_specific_mining_info(&m) - .ok_or(Error::ShareDoNotMatchAnyChannel)?; - let extranonce_1_len = self.extranonces.get_range0_len(); - let extranonce_2 = extranonce[extranonce_1_len..].to_vec(); - match &mut m { - Share::Extended(extended_share) => { - extended_share.extranonce = extranonce_2.try_into()?; - } - Share::Standard(_) => (), - }; - trace!( - "On checking target coinbase prefix is: {:?}", - coinbase_tx_prefix - ); - trace!( - "On checking target coinbase suffix is: {:?}", - coinbase_tx_suffix - ); - // Safe unwrap a sha256 can always be converted into [u8;32] - let merkle_root: [u8; 32] = crate::utils::merkle_root_from_path( - coinbase_tx_prefix, - coinbase_tx_suffix, - &extranonce[..], - &merkle_path[..], - ) - .ok_or(Error::InvalidCoinbase)? - .try_into() - .unwrap(); - let version = match &m { - Share::Extended(share) => share.version as i32, - Share::Standard(share) => share.0.version as i32, - }; - - let header = Header { - version: Version::from_consensus(version), - prev_blockhash, - merkle_root: (*Hash::from_bytes_ref(&merkle_root)).into(), - time: m.get_n_time(), - bits: CompactTarget::from_consensus(bits), - nonce: m.get_nonce(), - }; - - trace!("On checking target header is: {:?}", header); - let hash_ = header.block_hash(); - let hash: [u8; 32] = *hash_.to_raw_hash().as_ref(); - - if tracing::level_enabled!(tracing::Level::DEBUG) - || tracing::level_enabled!(tracing::Level::TRACE) - { - let bitcoin_target_log: binary_sv2::U256 = bitcoin_target.clone().into(); - let mut bitcoin_target_log = bitcoin_target_log.to_vec(); - bitcoin_target_log.reverse(); - debug!("Bitcoin target : {:?}", bitcoin_target_log.as_hex()); - let upstream_target: binary_sv2::U256 = upstream_target.clone().into(); - let mut upstream_target = upstream_target.to_vec(); - upstream_target.reverse(); - debug!("Upstream target: {:?}", upstream_target.to_vec().as_hex()); - let mut hash = hash; - hash.reverse(); - debug!("Hash : {:?}", hash.to_vec().as_hex()); - } - let hash: Target = hash.into(); - - if hash <= bitcoin_target { - let mut print_hash: [u8; 32] = *hash_.to_raw_hash().as_ref(); - print_hash.reverse(); - - info!( - "Share hash meet bitcoin target: {:?}", - print_hash.to_vec().as_hex() - ); - - let coinbase = [coinbase_tx_prefix, &extranonce[..], coinbase_tx_suffix] - .concat() - .to_vec(); - match self.kind { - ExtendedChannelKind::Proxy { .. } | ExtendedChannelKind::ProxyJd { .. } => { - let upstream_extranonce_space = self.extranonces.get_range0_len(); - let extranonce_ = extranonce[upstream_extranonce_space..].to_vec(); - let mut res = OnNewShare::ShareMeetBitcoinTarget(( - m, - template_id, - coinbase, - extranonce.to_vec(), - )); - res.into_extended(extranonce_, up_id); - Ok(res) - } - ExtendedChannelKind::Pool => Ok(OnNewShare::ShareMeetBitcoinTarget(( - m, - template_id, - coinbase, - extranonce.to_vec(), - ))), - } - } else if hash <= upstream_target { - match self.kind { - ExtendedChannelKind::Proxy { .. } | ExtendedChannelKind::ProxyJd { .. } => { - let upstream_extranonce_space = self.extranonces.get_range0_len(); - let extranonce = extranonce[upstream_extranonce_space..].to_vec(); - let mut res = OnNewShare::SendSubmitShareUpstream((m, template_id)); - res.into_extended(extranonce, up_id); - Ok(res) - } - ExtendedChannelKind::Pool => { - Ok(OnNewShare::SendSubmitShareUpstream((m, template_id))) - } - } - } else if hash <= downstream_target { - Ok(OnNewShare::ShareMeetDownstreamTarget) - } else { - error!("Share does not meet any target: {:?}", m); - let error = SubmitSharesError { - channel_id: m.get_channel_id(), - sequence_number: m.get_sequence_number(), - // Infallible unwrap we already know the len of the error code (is a - // static string) - error_code: SubmitSharesError::difficulty_too_low_error_code() - .to_string() - .try_into() - .unwrap(), - }; - Ok(OnNewShare::SendErrorDownstream(error)) - } - } - - /// Returns the downstream target and extranonce for the channel - fn get_channel_specific_mining_info(&self, m: &Share) -> Option<(mining_sv2::Target, Vec)> { - match m { - Share::Extended(share) => { - let channel = self.extended_channels.get(&m.get_channel_id())?; - let extranonce_prefix = channel.extranonce_prefix.to_vec(); - let dowstream_target = channel.target.clone().into(); - let extranonce = [&extranonce_prefix[..], &share.extranonce.to_vec()[..]] - .concat() - .to_vec(); - if extranonce.len() != self.extranonces.get_len() { - error!( - "Extranonce is not of the right len expected {} actual {}", - self.extranonces.get_len(), - extranonce.len() - ); - } - Some((dowstream_target, extranonce)) - } - Share::Standard((share, group_id)) => match &self.kind { - ExtendedChannelKind::Pool => { - let complete_id = GroupId::into_complete_id(*group_id, share.channel_id); - let mut channel = self - .standard_channels_for_non_hom_downstreams - .get(&complete_id); - if channel.is_none() { - channel = self - .standard_channels_for_hom_downstreams - .get(&share.channel_id); - }; - Some(( - channel?.target.clone(), - channel?.extranonce.clone().to_vec(), - )) - } - ExtendedChannelKind::Proxy { .. } | ExtendedChannelKind::ProxyJd { .. } => { - let complete_id = GroupId::into_complete_id(*group_id, share.channel_id); - let mut channel = self - .standard_channels_for_non_hom_downstreams - .get(&complete_id); - if channel.is_none() { - channel = self - .standard_channels_for_hom_downstreams - .get(&share.channel_id); - }; - Some(( - channel?.target.clone(), - channel?.extranonce.clone().to_vec(), - )) - } - }, - } - } - /// Updates the downstream target for the given channel_id - fn update_target_for_channel(&mut self, channel_id: u32, new_target: Target) -> Option { - let channel = self.extended_channels.get_mut(&channel_id)?; - channel.target = new_target.into(); - Some(true) - } -} - -/// Used by a pool to in order to manage all downstream channel. It adds job creation capabilities -/// to ChannelFactory. -#[derive(Debug)] -pub struct PoolChannelFactory { - inner: ChannelFactory, - job_creator: JobsCreators, - pool_coinbase_outputs: Vec, - // extended_channel_id -> SetCustomMiningJob - negotiated_jobs: HashMap, BuildNoHashHasher>, -} - -impl PoolChannelFactory { - /// constructor - pub fn new( - ids: Arc>, - extranonces: ExtendedExtranonce, - job_creator: JobsCreators, - share_per_min: f32, - kind: ExtendedChannelKind, - pool_coinbase_outputs: Vec, - ) -> Self { - let inner = ChannelFactory { - ids, - standard_channels_for_non_hom_downstreams: HashMap::with_hasher( - BuildNoHashHasher::default(), - ), - standard_channels_for_hom_downstreams: HashMap::with_hasher( - BuildNoHashHasher::default(), - ), - extended_channels: HashMap::with_hasher(BuildNoHashHasher::default()), - extranonces, - share_per_min, - future_jobs: Vec::new(), - last_prev_hash: None, - last_prev_hash_: None, - last_valid_job: None, - kind, - job_ids: Id::new(), - channel_to_group_id: HashMap::with_hasher(BuildNoHashHasher::default()), - future_templates: HashMap::with_hasher(BuildNoHashHasher::default()), - }; - - Self { - inner, - job_creator, - pool_coinbase_outputs, - negotiated_jobs: HashMap::with_hasher(BuildNoHashHasher::default()), - } - } - - /// Calls [`ChannelFactory::add_standard_channel`] - pub fn add_standard_channel( - &mut self, - request_id: u32, - downstream_hash_rate: f32, - is_header_only: bool, - id: u32, - ) -> Result, Error> { - self.inner - .add_standard_channel(request_id, downstream_hash_rate, is_header_only, id) - } - - /// Calls [`ChannelFactory::new_extended_channel`] - pub fn new_extended_channel( - &mut self, - request_id: u32, - hash_rate: f32, - min_extranonce_size: u16, - ) -> Result>, Error> { - self.inner - .new_extended_channel(request_id, hash_rate, min_extranonce_size) - } - - /// Called when we want to replicate a channel already opened by another actor. - /// is used only in the jd client from the template provider module to mock a pool. - /// Anything else should open channel with the new_extended_channel function - pub fn replicate_upstream_extended_channel_only_jd( - &mut self, - target: binary_sv2::U256<'static>, - extranonce: mining_sv2::Extranonce, - channel_id: u32, - extranonce_size: u16, - ) -> Option<()> { - self.inner.replicate_upstream_extended_channel_only_jd( - target, - extranonce, - channel_id, - extranonce_size, - ) - } - - /// Called only when a new prev hash is received by a Template Provider. It matches the - /// message with a `job_id` and calls [`ChannelFactory::on_new_prev_hash`] - /// it return the job_id - pub fn on_new_prev_hash_from_tp( - &mut self, - m: &SetNewPrevHashFromTp<'static>, - ) -> Result { - let job_id = self.job_creator.on_new_prev_hash(m).unwrap_or(0); - let new_prev_hash = StagedPhash { - job_id, - prev_hash: m.prev_hash.clone(), - min_ntime: m.header_timestamp, - nbits: m.n_bits, - }; - self.inner.on_new_prev_hash(new_prev_hash)?; - Ok(job_id) - } - - /// Called only when a new template is received by a Template Provider - pub fn on_new_template( - &mut self, - m: &mut NewTemplate<'static>, - ) -> Result, BuildNoHashHasher>, Error> { - let new_job = - self.job_creator - .on_new_template(m, true, self.pool_coinbase_outputs.clone())?; - self.inner.on_new_extended_mining_job(new_job) - } - - /// Called when a `SubmitSharesStandard` message is received from the downstream. We check the - /// shares against the channel's respective target and return `OnNewShare` to let us know if - /// and where the shares should be relayed - pub fn on_submit_shares_standard( - &mut self, - m: SubmitSharesStandard, - ) -> Result { - match self.inner.channel_to_group_id.get(&m.channel_id) { - Some(g_id) => { - let referenced_job = self - .inner - .last_valid_job - .clone() - .ok_or(Error::ShareDoNotMatchAnyJob)? - .0; - let merkle_path = referenced_job.merkle_path.to_vec(); - let template_id = self - .job_creator - .get_template_id_from_job(referenced_job.job_id) - .ok_or(Error::NoTemplateForId)?; - let target = self.job_creator.last_target(); - let prev_blockhash = self - .inner - .last_prev_hash_ - .ok_or(Error::ShareDoNotMatchAnyJob)?; - let bits = self - .inner - .last_prev_hash - .as_ref() - .ok_or(Error::ShareDoNotMatchAnyJob)? - .0 - .nbits; - self.inner.check_target( - Share::Standard((m, *g_id)), - target, - Some(template_id), - 0, - merkle_path, - referenced_job.coinbase_tx_prefix.as_ref(), - referenced_job.coinbase_tx_suffix.as_ref(), - prev_blockhash, - bits, - ) - } - None => { - let err = SubmitSharesError { - channel_id: m.channel_id, - sequence_number: m.sequence_number, - error_code: SubmitSharesError::invalid_channel_error_code() - .to_string() - .try_into() - .unwrap(), - }; - Ok(OnNewShare::SendErrorDownstream(err)) - } - } - } - - /// Called when a `SubmitSharesExtended` message is received from the downstream. We check the - /// shares against the channel's respective target and return `OnNewShare` to let us know if - /// and where the shares should be relayed - pub fn on_submit_shares_extended( - &mut self, - m: SubmitSharesExtended, - ) -> Result { - let target = self.job_creator.last_target(); - // When downstream set a custom mining job we add the job to the negotiated job - // hashmap, with the extended channel id as a key. Whenever the pool receive a share must - // first check if the channel have a negotiated job if so we can not retrieve the template - // via the job creator but we create a new one from the set custom job. - if self.negotiated_jobs.contains_key(&m.channel_id) { - let referenced_job = self.negotiated_jobs.get(&m.channel_id).unwrap(); - let merkle_path = referenced_job.merkle_path.to_vec(); - let extended_job = job_creator::extended_job_from_custom_job( - referenced_job, - self.inner.extranonces.get_len() as u8, - ) - .unwrap(); - let prev_blockhash = crate::utils::u256_to_block_hash(referenced_job.prev_hash.clone()); - let bits = referenced_job.nbits; - self.inner.check_target( - Share::Extended(m.into_static()), - target, - None, - 0, - merkle_path, - extended_job.coinbase_tx_prefix.as_ref(), - extended_job.coinbase_tx_suffix.as_ref(), - prev_blockhash, - bits, - ) - } else { - let referenced_job = self - .inner - .last_valid_job - .clone() - .ok_or(Error::ShareDoNotMatchAnyJob)? - .0; - let merkle_path = referenced_job.merkle_path.to_vec(); - let template_id = self - .job_creator - .get_template_id_from_job(referenced_job.job_id) - .ok_or(Error::NoTemplateForId)?; - let prev_blockhash = self - .inner - .last_prev_hash_ - .ok_or(Error::ShareDoNotMatchAnyJob)?; - let bits = self - .inner - .last_prev_hash - .as_ref() - .ok_or(Error::ShareDoNotMatchAnyJob)? - .0 - .nbits; - self.inner.check_target( - Share::Extended(m.into_static()), - target, - Some(template_id), - 0, - merkle_path, - referenced_job.coinbase_tx_prefix.as_ref(), - referenced_job.coinbase_tx_suffix.as_ref(), - prev_blockhash, - bits, - ) - } - } - - /// Utility function to return a new group id - pub fn new_group_id(&mut self) -> u32 { - let new_id = self.inner.ids.safe_lock(|ids| ids.new_group_id()).unwrap(); - new_id - } - - /// Utility function to return a new standard channel id - pub fn new_standard_id_for_hom(&mut self) -> u32 { - let hom_group_id = 0; - let new_id = self - .inner - .ids - .safe_lock(|ids| ids.new_channel_id(hom_group_id)) - .unwrap(); - new_id - } - - /// Returns the full extranonce, extranonce1 (static for channel) + extranonce2 (miner nonce - /// space) - pub fn extranonce_from_downstream_extranonce( - &self, - ext: mining_sv2::Extranonce, - ) -> Option { - self.inner - .extranonces - .extranonce_from_downstream_extranonce(ext) - .ok() - } - - /// Called when a new custom mining job arrives - pub fn on_new_set_custom_mining_job( - &mut self, - set_custom_mining_job: SetCustomMiningJob<'static>, - ) -> SetCustomMiningJobSuccess { - if self.check_set_custom_mining_job(&set_custom_mining_job) { - self.negotiated_jobs.insert( - set_custom_mining_job.channel_id, - set_custom_mining_job.clone(), - ); - SetCustomMiningJobSuccess { - channel_id: set_custom_mining_job.channel_id, - request_id: set_custom_mining_job.request_id, - job_id: self.inner.job_ids.next(), - } - } else { - todo!() - } - } - - fn check_set_custom_mining_job( - &self, - _set_custom_mining_job: &SetCustomMiningJob<'static>, - ) -> bool { - true - } - - /// Get extended channel ids - pub fn get_extended_channels_ids(&self) -> Vec { - self.inner.extended_channels.keys().copied().collect() - } - - pub fn get_shares_per_minute(&self) -> f32 { - self.inner.share_per_min - } - - /// Update coinbase outputs - pub fn update_pool_outputs(&mut self, outs: Vec) { - self.pool_coinbase_outputs = outs; - } - - /// Calls [`ChannelFactory::update_target_for_channel`] - /// Set a particular downstream channel target. - pub fn update_target_for_channel( - &mut self, - channel_id: u32, - new_target: Target, - ) -> Option { - self.inner.update_target_for_channel(channel_id, new_target) - } - - /// Set the target for this channel. This is the upstream target. - pub fn set_target(&mut self, new_target: &mut Target) { - self.inner.kind.set_target(new_target); - } -} - -/// Used by proxies that want to open extended channels with upstream. If the proxy has job -/// declaration capabilities, we set the job creator and the coinbase outs. -#[derive(Debug)] -pub struct ProxyExtendedChannelFactory { - inner: ChannelFactory, - job_creator: Option, - pool_coinbase_outputs: Option>, - // Id assigned to the extended channel by upstream - extended_channel_id: u32, -} - -impl ProxyExtendedChannelFactory { - /// Constructor - #[allow(clippy::too_many_arguments)] - pub fn new( - ids: Arc>, - extranonces: ExtendedExtranonce, - job_creator: Option, - share_per_min: f32, - kind: ExtendedChannelKind, - pool_coinbase_outputs: Option>, - extended_channel_id: u32, - ) -> Self { - match &kind { - ExtendedChannelKind::Proxy { .. } => { - if job_creator.is_some() { - panic!("Channel factory of kind Proxy can not be initialized with a JobCreators"); - }; - }, - ExtendedChannelKind::ProxyJd { .. } => { - if job_creator.is_none() { - panic!("Channel factory of kind ProxyJd must be initialized with a JobCreators"); - }; - } - ExtendedChannelKind::Pool => panic!("Try to construct an ProxyExtendedChannelFactory with pool kind, kind must be Proxy or ProxyJd"), - }; - let inner = ChannelFactory { - ids, - standard_channels_for_non_hom_downstreams: HashMap::with_hasher( - BuildNoHashHasher::default(), - ), - standard_channels_for_hom_downstreams: HashMap::with_hasher( - BuildNoHashHasher::default(), - ), - extended_channels: HashMap::with_hasher(BuildNoHashHasher::default()), - extranonces, - share_per_min, - future_jobs: Vec::new(), - last_prev_hash: None, - last_prev_hash_: None, - last_valid_job: None, - kind, - job_ids: Id::new(), - channel_to_group_id: HashMap::with_hasher(BuildNoHashHasher::default()), - future_templates: HashMap::with_hasher(BuildNoHashHasher::default()), - }; - ProxyExtendedChannelFactory { - inner, - job_creator, - pool_coinbase_outputs, - extended_channel_id, - } - } - - /// Calls [`ChannelFactory::add_standard_channel`] - pub fn add_standard_channel( - &mut self, - request_id: u32, - downstream_hash_rate: f32, - id_header_only: bool, - id: u32, - ) -> Result, Error> { - self.inner - .add_standard_channel(request_id, downstream_hash_rate, id_header_only, id) - } - - /// Calls [`ChannelFactory::new_extended_channel`] - pub fn new_extended_channel( - &mut self, - request_id: u32, - hash_rate: f32, - min_extranonce_size: u16, - ) -> Result, Error> { - self.inner - .new_extended_channel(request_id, hash_rate, min_extranonce_size) - } - - /// Called only when a new prev hash is received by a Template Provider when job declaration is - /// used. It matches the message with a `job_id`, creates a new custom job, and calls - /// [`ChannelFactory::on_new_prev_hash`] - pub fn on_new_prev_hash_from_tp( - &mut self, - m: &SetNewPrevHashFromTp<'static>, - ) -> Result, Error> { - if let Some(job_creator) = self.job_creator.as_mut() { - let job_id = job_creator.on_new_prev_hash(m).unwrap_or(0); - let new_prev_hash = StagedPhash { - job_id, - prev_hash: m.prev_hash.clone(), - min_ntime: m.header_timestamp, - nbits: m.n_bits, - }; - let mut custom_job = None; - if let Some(template) = self.inner.future_templates.get(&job_id) { - custom_job = Some(( - PartialSetCustomMiningJob { - version: template.version, - prev_hash: new_prev_hash.prev_hash.clone(), - min_ntime: new_prev_hash.min_ntime, - nbits: new_prev_hash.nbits, - coinbase_tx_version: template.coinbase_tx_version, - coinbase_prefix: template.coinbase_prefix.clone(), - coinbase_tx_input_n_sequence: template.coinbase_tx_input_sequence, - coinbase_tx_value_remaining: template.coinbase_tx_value_remaining, - coinbase_tx_outputs: template.coinbase_tx_outputs.clone(), - coinbase_tx_locktime: template.coinbase_tx_locktime, - merkle_path: template.merkle_path.clone(), - future_job: template.future_template, - }, - job_id, - )); - } - self.inner.future_templates = HashMap::with_hasher(BuildNoHashHasher::default()); - self.inner.on_new_prev_hash(new_prev_hash)?; - Ok(custom_job) - } else { - panic!("A channel factory without job creator do not have declaration capabilities") - } - } - - /// Called only when a new template is received by a Template Provider when job declaration is - /// used. It creates a new custom job and calls - /// [`ChannelFactory::on_new_extended_mining_job`] - #[allow(clippy::type_complexity)] - pub fn on_new_template( - &mut self, - m: &mut NewTemplate<'static>, - ) -> Result< - ( - // downstream job_id -> downstream message (newextjob or newjob) - HashMap, BuildNoHashHasher>, - // PartialSetCustomMiningJob to send to the pool - Option, - // job_id registered in the channel, the one that SetNewPrevHash refer to (upstsream - // job id) - u32, - ), - Error, - > { - if let (Some(job_creator), Some(pool_coinbase_outputs)) = ( - self.job_creator.as_mut(), - self.pool_coinbase_outputs.as_mut(), - ) { - let new_job = job_creator.on_new_template(m, true, pool_coinbase_outputs.clone())?; - let id = new_job.job_id; - if !new_job.is_future() && self.inner.last_prev_hash.is_some() { - let prev_hash = self.last_prev_hash().unwrap(); - let min_ntime = self.last_min_ntime().unwrap(); - let nbits = self.last_nbits().unwrap(); - let custom_mining_job = PartialSetCustomMiningJob { - version: m.version, - prev_hash, - min_ntime, - nbits, - coinbase_tx_version: m.coinbase_tx_version, - coinbase_prefix: m.coinbase_prefix.clone(), - coinbase_tx_input_n_sequence: m.coinbase_tx_input_sequence, - coinbase_tx_value_remaining: m.coinbase_tx_value_remaining, - coinbase_tx_outputs: m.coinbase_tx_outputs.clone(), - coinbase_tx_locktime: m.coinbase_tx_locktime, - merkle_path: m.merkle_path.clone(), - future_job: m.future_template, - }; - return Ok(( - self.inner.on_new_extended_mining_job(new_job)?, - Some(custom_mining_job), - id, - )); - } else if new_job.is_future() { - self.inner - .future_templates - .insert(new_job.job_id, m.clone()); - } - Ok((self.inner.on_new_extended_mining_job(new_job)?, None, id)) - } else { - panic!("Either channel factory has no job creator or pool_coinbase_outputs are not yet set") - } - } - - /// Called when a `SubmitSharesStandard` message is received from the downstream. We check the - /// shares against the channel's respective target and return `OnNewShare` to let us know if - /// and where the shares should be relayed - pub fn on_submit_shares_extended( - &mut self, - m: SubmitSharesExtended<'static>, - ) -> Result { - let merkle_path = self - .inner - .last_valid_job - .as_ref() - .ok_or(Error::ShareDoNotMatchAnyJob)? - .0 - .merkle_path - .to_vec(); - - let referenced_job = self - .inner - .last_valid_job - .clone() - .ok_or(Error::ShareDoNotMatchAnyJob)? - .0; - - if referenced_job.job_id != m.job_id { - let error = SubmitSharesError { - channel_id: m.channel_id, - sequence_number: m.sequence_number, - // Infallible unwrap we already know the len of the error code (is a - // static string) - error_code: SubmitSharesError::invalid_job_id_error_code() - .to_string() - .try_into() - .unwrap(), - }; - return Ok(OnNewShare::SendErrorDownstream(error)); - } - - if let Some(job_creator) = self.job_creator.as_mut() { - let template_id = job_creator - .get_template_id_from_job(referenced_job.job_id) - .ok_or(Error::NoTemplateForId)?; - let bitcoin_target = job_creator.last_target(); - let prev_blockhash = self - .inner - .last_prev_hash_ - .ok_or(Error::ShareDoNotMatchAnyJob)?; - let bits = self - .inner - .last_prev_hash - .as_ref() - .ok_or(Error::ShareDoNotMatchAnyJob)? - .0 - .nbits; - self.inner.check_target( - Share::Extended(m), - bitcoin_target, - Some(template_id), - self.extended_channel_id, - merkle_path, - referenced_job.coinbase_tx_prefix.as_ref(), - referenced_job.coinbase_tx_suffix.as_ref(), - prev_blockhash, - bits, - ) - } else { - let bitcoin_target = [0; 32]; - // if there is not job_creator is not proxy duty to check if target is below or above - // bitcoin target so we set bitcoin_target = 0. - let prev_blockhash = self - .inner - .last_prev_hash_ - .ok_or(Error::ShareDoNotMatchAnyJob)?; - let bits = self - .inner - .last_prev_hash - .as_ref() - .ok_or(Error::ShareDoNotMatchAnyJob)? - .0 - .nbits; - self.inner.check_target( - Share::Extended(m), - bitcoin_target.into(), - None, - self.extended_channel_id, - merkle_path, - referenced_job.coinbase_tx_prefix.as_ref(), - referenced_job.coinbase_tx_suffix.as_ref(), - prev_blockhash, - bits, - ) - } - } - - /// Called when a `SubmitSharesStandard` message is received from the Downstream. We check the - /// shares against the channel's respective target and return `OnNewShare` to let us know if - /// and where the shares should be relayed - pub fn on_submit_shares_standard( - &mut self, - m: SubmitSharesStandard, - ) -> Result { - let merkle_path = self - .inner - .last_valid_job - .as_ref() - .ok_or(Error::ShareDoNotMatchAnyJob)? - .0 - .merkle_path - .to_vec(); - let referenced_job = self - .inner - .last_valid_job - .clone() - .ok_or(Error::ShareDoNotMatchAnyJob)? - .0; - match self.inner.channel_to_group_id.get(&m.channel_id) { - Some(g_id) => { - if let Some(job_creator) = self.job_creator.as_mut() { - let template_id = job_creator - .get_template_id_from_job( - self.inner.last_valid_job.as_ref().unwrap().0.job_id, - ) - .ok_or(Error::NoTemplateForId)?; - let bitcoin_target = job_creator.last_target(); - let prev_blockhash = self - .inner - .last_prev_hash_ - .ok_or(Error::ShareDoNotMatchAnyJob)?; - let bits = self - .inner - .last_prev_hash - .as_ref() - .ok_or(Error::ShareDoNotMatchAnyJob)? - .0 - .nbits; - self.inner.check_target( - Share::Standard((m, *g_id)), - bitcoin_target, - Some(template_id), - self.extended_channel_id, - merkle_path, - referenced_job.coinbase_tx_prefix.as_ref(), - referenced_job.coinbase_tx_suffix.as_ref(), - prev_blockhash, - bits, - ) - } else { - let bitcoin_target = [0; 32]; - let prev_blockhash = self - .inner - .last_prev_hash_ - .ok_or(Error::ShareDoNotMatchAnyJob)?; - let bits = self - .inner - .last_prev_hash - .as_ref() - .ok_or(Error::ShareDoNotMatchAnyJob)? - .0 - .nbits; - // if there is not job_creator is not proxy duty to check if target is below or - // above bitcoin target so we set bitcoin_target = 0. - self.inner.check_target( - Share::Standard((m, *g_id)), - bitcoin_target.into(), - None, - self.extended_channel_id, - merkle_path, - referenced_job.coinbase_tx_prefix.as_ref(), - referenced_job.coinbase_tx_suffix.as_ref(), - prev_blockhash, - bits, - ) - } - } - None => { - let err = SubmitSharesError { - channel_id: m.channel_id, - sequence_number: m.sequence_number, - error_code: SubmitSharesError::invalid_channel_error_code() - .to_string() - .try_into() - .unwrap(), - }; - Ok(OnNewShare::SendErrorDownstream(err)) - } - } - } - - /// Calls [`ChannelFactory::on_new_prev_hash`] - pub fn on_new_prev_hash(&mut self, m: SetNewPrevHash<'static>) -> Result<(), Error> { - self.inner.on_new_prev_hash(StagedPhash { - job_id: m.job_id, - prev_hash: m.prev_hash.clone().into_static(), - min_ntime: m.min_ntime, - nbits: m.nbits, - }) - } - - /// Calls [`ChannelFactory::on_new_extended_mining_job`] - pub fn on_new_extended_mining_job( - &mut self, - m: NewExtendedMiningJob<'static>, - ) -> Result, BuildNoHashHasher>, Error> { - self.inner.on_new_extended_mining_job(m) - } - - /// Set new target - pub fn set_target(&mut self, new_target: &mut Target) { - self.inner.kind.set_target(new_target); - } - - /// Get last valid job version - pub fn last_valid_job_version(&self) -> Option { - self.inner.last_valid_job.as_ref().map(|j| j.0.version) - } - - /// Returns the full extranonce, extranonce1 (static for channel) + extranonce2 (miner nonce - /// space) - pub fn extranonce_from_downstream_extranonce( - &self, - ext: mining_sv2::Extranonce, - ) -> Option { - self.inner - .extranonces - .extranonce_from_downstream_extranonce(ext) - .ok() - } - - /// Returns the most recent prev hash - pub fn last_prev_hash(&self) -> Option> { - self.inner - .last_prev_hash - .as_ref() - .map(|f| f.0.prev_hash.clone()) - } - - /// Get last min ntime - pub fn last_min_ntime(&self) -> Option { - self.inner.last_prev_hash.as_ref().map(|f| f.0.min_ntime) - } - - /// Get last nbits - pub fn last_nbits(&self) -> Option { - self.inner.last_prev_hash.as_ref().map(|f| f.0.nbits) - } - - /// Get extranonce_size - pub fn extranonce_size(&self) -> usize { - self.inner.extranonces.get_len() - } - - /// Get extranonce_2 size - pub fn channel_extranonce2_size(&self) -> usize { - self.inner.extranonces.get_len() - self.inner.extranonces.get_range0_len() - } - - // Only used when the proxy is using Job Declaration - /// Updates pool outputs - pub fn update_pool_outputs(&mut self, outs: Vec) { - self.pool_coinbase_outputs = Some(outs); - } - - /// Get this channel id - pub fn get_this_channel_id(&self) -> u32 { - self.extended_channel_id - } - - /// Returns the extranonce1 len of the upstream. For a proxy, this would - /// be the extranonce_prefix len - pub fn get_upstream_extranonce1_len(&self) -> usize { - self.inner.extranonces.get_range0_len() - } - - /// Calls [`ChannelFactory::update_target_for_channel`] - pub fn update_target_for_channel( - &mut self, - channel_id: u32, - new_target: Target, - ) -> Option { - self.inner.update_target_for_channel(channel_id, new_target) - } -} - -/// Used by proxies for tracking upstream targets. -#[derive(Debug, Clone)] -pub enum ExtendedChannelKind { - Proxy { upstream_target: Target }, - ProxyJd { upstream_target: Target }, - Pool, -} -impl ExtendedChannelKind { - /// Set target - pub fn set_target(&mut self, new_target: &mut Target) { - match self { - ExtendedChannelKind::Proxy { upstream_target } - | ExtendedChannelKind::ProxyJd { upstream_target } => { - std::mem::swap(upstream_target, new_target) - } - ExtendedChannelKind::Pool => warn!("Try to set upstream target for a pool"), - } - } -} -#[cfg(test)] -mod test { - use super::*; - use binary_sv2::{Seq0255, B064K, U256}; - use mining_sv2::OpenStandardMiningChannel; - use stratum_common::bitcoin::{Amount, PublicKey, Target, TxOut, WPubkeyHash}; - - const BLOCK_REWARD: u64 = 2_000_000_000; - - // Block 1296 data - // 01000000 - // c1397d4a33adeeb3383803e9ac3db4b2c2c9d6737cbabc13a534d24600000000 - // 89687b66140ac9874656270e066ed7ef81d5133ada2d0133f09322a87b161738 - // 4eb87749 - // ffff001d - // 07cacb0e - const _PUB_K: &str = "04c6d0969c2d98a5c19ba7c36c7937c5edbd60ff2a01397c4afe54f16cd641667ea0049ba6f9e1796ba3c8e49e1b504c532ebbaaa1010c3f7d9b83a8ea7fd800e2"; - const _BLOCK_HASH: &str = "000000009a4aed3e8ba7a978c6b50fea886fb496d66e696090a91d527200b002"; - const VERSION: u32 = 1; - // version 01000000 - // inputs 01 - // prev out 0000000000000000000000000000000000000000000000000000000000000000ffffffff - // script len 07 - // script 04ffff001d0177 - // sequence ffffffff - // n inputs 01 - // amunt 00f2052a01000000 - // out lne 43 - // push 41 - // pub k 04c6d0969c2d98a5c19ba7c36c7937c5edbd60ff2a01397c4afe54f16cd641667ea0049ba6f9e1796ba3c8e49e1b504c532ebbaaa1010c3f7d9b83a8ea7fd800e2 - // checksig ac - // locktime 00000000 - const COINBASE: &str = "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0177ffffffff0100f2052a01000000434104c6d0969c2d98a5c19ba7c36c7937c5edbd60ff2a01397c4afe54f16cd641667ea0049ba6f9e1796ba3c8e49e1b504c532ebbaaa1010c3f7d9b83a8ea7fd800e2ac00000000"; - const COINBASE_OUTPUT: &str = "4104c6d0969c2d98a5c19ba7c36c7937c5edbd60ff2a01397c4afe54f16cd641667ea0049ba6f9e1796ba3c8e49e1b504c532ebbaaa1010c3f7d9b83a8ea7fd800e2ac"; - const MERKLE_PATH: &str = "59bf8acbc9d60dfae841abecc3882b4181f2bdd8ac6c1d94001165ab3aef50b0"; - const NONCE: &str = "07cacb0e"; - const NTIME: &str = "4eb87749"; - - // Prev block data (1295) - //01000000 - //cf578a234f330c287354e24234ff6b86d6ab9e4ddd3e5ba71a6bcbf600000000 - //72d12b99bdb63762bedc5db30bcffbd7903721bc736dd683de37b1a3632f9000 - //time: 2e8c7749 -> 49778c2e -> 1232571438 - //nbits: ffff001d -> 4294901789 - //29444816 - const PREV_HASH: &str = "0000000046d234a513bcba7c73d6c9c2b2b43dace9033838b3eead334a7d39c1"; - const PREV_HEADER_TIMESTAMP: u32 = 1232571438; - const PREV_HEADER_NBITS: u32 = 486604799; - - fn _get_pub_key_hash() -> WPubkeyHash { - let into_bin = decode_hex(_PUB_K).unwrap(); - let pk = PublicKey::from_slice(&into_bin[..]); - let hash = pk.unwrap().pubkey_hash(); - WPubkeyHash::from_raw_hash(hash.to_raw_hash()) - } - - fn get_coinbase() -> (Vec, Vec, Vec) { - let parsed = decode_hex(COINBASE).unwrap(); - // Coinbase prefix in Sv2 is the bip34 block height in this tx there is no prefix - let prefix = parsed[42..42].to_vec(); - let extranonce = parsed[42..49].to_vec(); - let suffix = parsed[49..].to_vec(); - (prefix, extranonce, suffix) - } - - fn get_coinbase_outputs() -> B064K<'static> { - decode_hex(COINBASE_OUTPUT).unwrap().try_into().unwrap() - } - - fn get_merkle_path() -> Seq0255<'static, U256<'static>> { - let mut m_path = decode_hex(MERKLE_PATH).unwrap(); - m_path.reverse(); - let path: U256 = m_path.try_into().unwrap(); - vec![path].try_into().unwrap() - } - - fn nbit_to_target(nbit: u32) -> U256<'static> { - let mut target = Target::from_compact(CompactTarget::from_consensus(nbit)) - .to_be_bytes() - .to_vec(); - target.reverse(); - target.try_into().unwrap() - } - - fn decode_hex(s: &str) -> Result, std::num::ParseIntError> { - (0..s.len()) - .step_by(2) - .map(|i| u8::from_str_radix(&s[i..i + 2], 16)) - .collect() - } - - #[test] - fn test_complete_mining_round() { - let (prefix, coinbase_extranonce, _) = get_coinbase(); - - // Initialize a Channel of type Pool - let out = TxOut {value: Amount::from_sat(BLOCK_REWARD), script_pubkey: decode_hex("4104c6d0969c2d98a5c19ba7c36c7937c5edbd60ff2a01397c4afe54f16cd641667ea0049ba6f9e1796ba3c8e49e1b504c532ebbaaa1010c3f7d9b83a8ea7fd800e2ac").unwrap().into()}; - let creator = JobsCreators::new(7); - let share_per_min = 1.0; - // Create an ExtendedExtranonce of len 7: - // upstream part is 0 bytes cause we are a pool so no more upstreams - // self part is 7 bytes - // downstream part is 0 cause in the test the downstream is HOM so we do not need to - // reserve space for downstream - let mut inner = coinbase_extranonce.clone(); - inner[6] = 0; - let extranonces = ExtendedExtranonce::new_with_inner_only_test(0..0, 0..0, 0..7, inner) - .expect("Failed to create ExtendedExtranonce with valid ranges"); - - let ids = Arc::new(Mutex::new(GroupId::new())); - let channel_kind = ExtendedChannelKind::Pool; - let mut channel = PoolChannelFactory::new( - ids, - extranonces, - creator, - share_per_min, - channel_kind, - vec![out], - ); - - // Build a NewTemplate - let new_template = NewTemplate { - template_id: 10, - future_template: true, - version: VERSION, - coinbase_tx_version: 1, - coinbase_prefix: prefix.try_into().unwrap(), - coinbase_tx_input_sequence: u32::MAX, - coinbase_tx_value_remaining: 5_000_000_000, - coinbase_tx_outputs_count: 0, - coinbase_tx_outputs: get_coinbase_outputs(), - coinbase_tx_locktime: 0, - merkle_path: get_merkle_path(), - }; - - // "Send" the NewTemplate to the channel - let _ = channel.on_new_template(&mut (new_template.clone())); - - // Build a PrevHash - let mut p_hash = decode_hex(PREV_HASH).unwrap(); - p_hash.reverse(); - let prev_hash = SetNewPrevHashFromTp { - template_id: 10, - prev_hash: p_hash.try_into().unwrap(), - header_timestamp: PREV_HEADER_TIMESTAMP, - n_bits: PREV_HEADER_NBITS, - target: nbit_to_target(PREV_HEADER_NBITS), - }; - - // "Send" the SetNewPrevHash to channel - let _ = channel.on_new_prev_hash_from_tp(&prev_hash); - - // Build open standard channel - let open_standard_channel = OpenStandardMiningChannel { - request_id: 100.into(), - user_identity: "Gigi".to_string().try_into().unwrap(), - nominal_hash_rate: 100_000_000_000_000.0, - max_target: [255; 32].into(), - }; - - // "Send" the OpenStandardMiningChannel to channel - let result = loop { - let id = channel.new_standard_id_for_hom(); - let result = channel - .add_standard_channel( - open_standard_channel.get_request_id_as_u32(), - open_standard_channel.nominal_hash_rate, - true, - id, - ) - .unwrap(); - let downsteram_extranonce = match &result[0] { - Mining::OpenStandardMiningChannelSuccess(msg) => { - msg.extranonce_prefix.clone().to_vec() - } - _ => panic!(), - }; - if downsteram_extranonce == coinbase_extranonce { - break result; - } - }; - let mut result = result.iter(); - - // Get the expected job id and channel_id - let mut channel_id = u32::MAX; - let job_id = loop { - match result.next().unwrap() { - Mining::OpenStandardMiningChannelSuccess(success) => { - channel_id = success.channel_id - } - Mining::SetNewPrevHash(_) => (), - Mining::NewMiningJob(job) => break job.job_id, - _ => panic!(), - } - }; - // make sure job management in channel factory is updated - (0..job_id - 1).for_each(|_| { - channel.job_creator.reset_new_templates(None); - let _ = channel.on_new_template(&mut (new_template.clone())); - let _ = channel.on_new_prev_hash_from_tp(&prev_hash); - }); - - // Build the success share - let share = SubmitSharesStandard { - channel_id, - sequence_number: 2, - job_id, - nonce: u32::from_le_bytes(decode_hex(NONCE).unwrap().try_into().unwrap()), - ntime: u32::from_le_bytes(decode_hex(NTIME).unwrap().try_into().unwrap()), - version: 1, - }; - - // "Send" the Share to channel - match channel.on_submit_shares_standard(share).unwrap() { - OnNewShare::SendErrorDownstream(e) => panic!( - "{:?} \n {}", - e, - std::str::from_utf8(&e.error_code.to_vec()[..]).unwrap() - ), - OnNewShare::SendSubmitShareUpstream(_) => panic!(), - OnNewShare::RelaySubmitShareUpstream => panic!(), - OnNewShare::ShareMeetBitcoinTarget(_) => assert!(true), - OnNewShare::ShareMeetDownstreamTarget => panic!(), - }; - } -} diff --git a/protocols/v2/roles-logic-sv2/src/channel_logic/mod.rs b/protocols/v2/roles-logic-sv2/src/channel_logic/mod.rs deleted file mode 100644 index 18653124a4..0000000000 --- a/protocols/v2/roles-logic-sv2/src/channel_logic/mod.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! # Channel Logic -//! -//! A module for managing channels on applications. -//! -//! Divided in two submodules: -//! - [`channel_factory`] -//! - [`proxy_group_channel`] - -pub mod channel_factory; -pub mod proxy_group_channel; - -use mining_sv2::{NewExtendedMiningJob, NewMiningJob}; -use std::convert::TryInto; - -/// Convert extended to standard job by calculating the merkle root -pub fn extended_to_standard_job<'a>( - extended: &NewExtendedMiningJob, - coinbase_script: &[u8], - channel_id: u32, - job_id: Option, -) -> Option> { - let merkle_root = crate::utils::merkle_root_from_path( - extended.coinbase_tx_prefix.inner_as_ref(), - extended.coinbase_tx_suffix.inner_as_ref(), - coinbase_script, - &extended.merkle_path.inner_as_ref(), - ); - - Some(NewMiningJob { - channel_id, - job_id: job_id.unwrap_or(extended.job_id), - min_ntime: extended.min_ntime.clone().into_static(), - version: extended.version, - merkle_root: merkle_root?.try_into().ok()?, - }) -} diff --git a/protocols/v2/roles-logic-sv2/src/channel_logic/proxy_group_channel.rs b/protocols/v2/roles-logic-sv2/src/channel_logic/proxy_group_channel.rs deleted file mode 100644 index 50a59d175b..0000000000 --- a/protocols/v2/roles-logic-sv2/src/channel_logic/proxy_group_channel.rs +++ /dev/null @@ -1,252 +0,0 @@ -//! # Proxy Group Channel -//! -//! This module contains logic for managing Standard Channels via Group Channels. - -use crate::{common_properties::StandardChannel, parsers::Mining, Error}; - -use mining_sv2::{ - NewExtendedMiningJob, NewMiningJob, OpenStandardMiningChannelSuccess, SetNewPrevHash, -}; - -use super::extended_to_standard_job; -use nohash_hasher::BuildNoHashHasher; -use std::collections::HashMap; - -/// Wrapper around `GroupChannel` for managing multiple group channels -#[derive(Debug, Clone, Default)] -pub struct GroupChannels { - channels: HashMap, -} -impl GroupChannels { - /// Constructor - pub fn new() -> Self { - Self { - channels: HashMap::new(), - } - } - /// Called when when a group channel created. We add the channel in its - /// respective group and call [`GroupChannel::on_channel_success_for_hom_downtream`] - pub fn on_channel_success_for_hom_downtream( - &mut self, - m: &OpenStandardMiningChannelSuccess, - ) -> Result>, Error> { - let group_id = m.group_channel_id; - - self.channels - .entry(group_id) - .or_insert_with(GroupChannel::new); - match self.channels.get_mut(&group_id) { - Some(group) => group.on_channel_success_for_hom_downtream(m.clone()), - None => unreachable!(), - } - } - /// Called when a new prev hash arrives. We loop through all group channels to update state - /// within each group - pub fn update_new_prev_hash(&mut self, m: &SetNewPrevHash) { - for group in self.channels.values_mut() { - group.update_new_prev_hash(m); - } - } - /// Called when a new extended job arrives. We loop through all group channels to update state - /// within group - pub fn on_new_extended_mining_job(&mut self, m: &NewExtendedMiningJob) { - for group in &mut self.channels.values_mut() { - let cloned = NewExtendedMiningJob { - channel_id: m.channel_id, - job_id: m.job_id, - min_ntime: m.min_ntime.clone().into_static(), - version: m.version, - version_rolling_allowed: m.version_rolling_allowed, - merkle_path: m.merkle_path.clone().into_static(), - coinbase_tx_prefix: m.coinbase_tx_prefix.clone().into_static(), - coinbase_tx_suffix: m.coinbase_tx_suffix.clone().into_static(), - }; - group.on_new_extended_mining_job(cloned); - } - } - /// Returns last valid job as a `NewMiningJob` - pub fn last_received_job_to_standard_job( - &mut self, - channel_id: u32, - group_id: u32, - ) -> Result, Error> { - match self.channels.get_mut(&group_id) { - Some(group) => group.last_received_job_to_standard_job(channel_id), - None => Err(Error::GroupIdNotFound), - } - } - - /// Get group channel ids - pub fn ids(&self) -> Vec { - self.channels.keys().copied().collect() - } -} - -#[derive(Debug, Clone)] -struct GroupChannel { - hom_downstreams: HashMap>, - future_jobs: Vec>, - last_prev_hash: Option>, - last_valid_job: Option>, - last_received_job: Option>, -} - -impl GroupChannel { - fn new() -> Self { - Self { - hom_downstreams: HashMap::with_hasher(BuildNoHashHasher::default()), - future_jobs: vec![], - last_prev_hash: None, - last_valid_job: None, - last_received_job: None, - } - } - // Called when a channel is successfully opened for header only mining(HOM) on standard - // channels. Here, we store the new channel, and update state for jobs and return relevant - // SV2 messages (NewMiningJob and SNPH) - fn on_channel_success_for_hom_downtream( - &mut self, - m: OpenStandardMiningChannelSuccess, - ) -> Result>, Error> { - let channel = StandardChannel { - channel_id: m.channel_id, - group_id: m.group_channel_id, - target: m.target.clone().into(), - extranonce: m.extranonce_prefix.clone().into(), - }; - let channel_id = m.channel_id; - let mut res = vec![]; - for extended_job in &self.future_jobs { - let standard_job = extended_to_standard_job( - extended_job, - &channel.extranonce.clone().to_vec(), - channel.channel_id, - None, - ) - .ok_or(Error::ImpossibleToCalculateMerkleRoot)?; - res.push(Mining::NewMiningJob(standard_job)); - } - - if let Some(last_valid_job) = &self.last_valid_job { - let mut standard_job = extended_to_standard_job( - last_valid_job, - &channel.extranonce.clone().to_vec(), - channel.channel_id, - None, - ) - .ok_or(Error::ImpossibleToCalculateMerkleRoot)?; - - if let Some(new_prev_hash) = &self.last_prev_hash { - let mut new_prev_hash = new_prev_hash.clone(); - standard_job.set_future(); - new_prev_hash.job_id = standard_job.job_id; - res.push(Mining::NewMiningJob(standard_job)); - res.push(Mining::SetNewPrevHash(new_prev_hash)) - } else { - res.push(Mining::NewMiningJob(standard_job)); - } - } else if let Some(new_prev_hash) = &self.last_prev_hash { - res.push(Mining::SetNewPrevHash(new_prev_hash.clone())) - } - - self.hom_downstreams.insert(channel_id, channel); - - Ok(res) - } - - // If a matching job is already in the future job queue, - // we set a new valid job, otherwise we clear the future jobs - // queue and stage a prev hash to be used when the job arrives - fn update_new_prev_hash(&mut self, m: &SetNewPrevHash) { - while let Some(job) = self.future_jobs.pop() { - if job.job_id == m.job_id { - self.last_valid_job = Some(job); - break; - } - } - self.future_jobs = vec![]; - let cloned = SetNewPrevHash { - channel_id: m.channel_id, - job_id: m.job_id, - prev_hash: m.prev_hash.clone().into_static(), - min_ntime: m.min_ntime, - nbits: m.nbits, - }; - self.last_prev_hash = Some(cloned.clone()); - } - - // Pushes new job to future_job queue if it is future, - // otherwise we set it as the valid job - fn on_new_extended_mining_job(&mut self, m: NewExtendedMiningJob<'static>) { - self.last_received_job = Some(m.clone()); - if m.is_future() { - self.future_jobs.push(m) - } else { - self.last_valid_job = Some(m) - } - } - - // Returns most recent job - fn last_received_job_to_standard_job( - &mut self, - channel_id: u32, - ) -> Result, Error> { - match &self.last_received_job { - Some(m) => { - let downstream = self - .hom_downstreams - .get(&channel_id) - .ok_or(Error::NotFoundChannelId)?; - extended_to_standard_job( - m, - &downstream.extranonce.clone().to_vec(), - downstream.channel_id, - None, - ) - .ok_or(Error::ImpossibleToCalculateMerkleRoot) - } - None => Err(Error::NoValidJob), - } - } -} - -#[cfg(test)] -mod test { - use super::*; - use binary_sv2::B064K; - use std::convert::TryFrom; - - #[test] - fn group_channel_new_prev_hash_ordering_test() { - let mut group_channel = GroupChannel::new(); - let mut new_extended_mining_job = NewExtendedMiningJob { - channel_id: 1, - job_id: 0, - min_ntime: binary_sv2::Sv2Option::new(None), - version: 0, - version_rolling_allowed: false, - merkle_path: vec![].into(), - coinbase_tx_prefix: B064K::try_from(Vec::new()).unwrap(), - coinbase_tx_suffix: B064K::try_from(Vec::new()).unwrap(), - }; - - group_channel.on_new_extended_mining_job(new_extended_mining_job.clone()); - new_extended_mining_job.version = 1; - group_channel.on_new_extended_mining_job(new_extended_mining_job); - - // Make sure this returns the last job - the one where we updated the version. - group_channel.update_new_prev_hash(&SetNewPrevHash { - channel_id: 1, - job_id: 0, - prev_hash: [ - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, - ] - .into(), - min_ntime: 989898, - nbits: 9, - }); - - assert_eq!(group_channel.last_valid_job.unwrap().version, 1); - } -} diff --git a/protocols/v2/roles-logic-sv2/src/channels/chain_tip.rs b/protocols/v2/roles-logic-sv2/src/channels/chain_tip.rs deleted file mode 100644 index b8beca1ecd..0000000000 --- a/protocols/v2/roles-logic-sv2/src/channels/chain_tip.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! # Chain Tip -use binary_sv2::U256; - -/// An abstraction over the chain tip, carrying information from `SetNewPrevHash` messages. -/// -/// Used while creating non-future jobs. -#[derive(Debug, Clone)] -pub struct ChainTip { - prev_hash: U256<'static>, - nbits: u32, - min_ntime: u32, -} - -impl ChainTip { - pub fn new(prev_hash: U256<'static>, nbits: u32, min_ntime: u32) -> Self { - Self { - prev_hash, - nbits, - min_ntime, - } - } - - pub fn prev_hash(&self) -> U256<'static> { - self.prev_hash.clone() - } - - pub fn nbits(&self) -> u32 { - self.nbits - } - - pub fn min_ntime(&self) -> u32 { - self.min_ntime - } -} diff --git a/protocols/v2/roles-logic-sv2/src/channels/client/error.rs b/protocols/v2/roles-logic-sv2/src/channels/client/error.rs deleted file mode 100644 index e7b880057b..0000000000 --- a/protocols/v2/roles-logic-sv2/src/channels/client/error.rs +++ /dev/null @@ -1,16 +0,0 @@ -#[derive(Debug)] -pub enum ExtendedChannelError { - NewExtranoncePrefixTooLarge, - JobIdNotFound, -} - -#[derive(Debug)] -pub enum StandardChannelError { - JobIdNotFound, - NewExtranoncePrefixTooLarge, -} - -#[derive(Debug)] -pub enum GroupChannelError { - JobIdNotFound, -} diff --git a/protocols/v2/roles-logic-sv2/src/channels/client/mod.rs b/protocols/v2/roles-logic-sv2/src/channels/client/mod.rs deleted file mode 100644 index 117fb3f1e1..0000000000 --- a/protocols/v2/roles-logic-sv2/src/channels/client/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Abstractions for channels to be used by mining clients. - -pub mod error; -pub mod extended; -pub mod group; -pub mod share_accounting; -pub mod standard; diff --git a/protocols/v2/roles-logic-sv2/src/channels/client/share_accounting.rs b/protocols/v2/roles-logic-sv2/src/channels/client/share_accounting.rs deleted file mode 100644 index 2a842a81db..0000000000 --- a/protocols/v2/roles-logic-sv2/src/channels/client/share_accounting.rs +++ /dev/null @@ -1,103 +0,0 @@ -//! Abstractions for share validation for a Mining Client - -use std::collections::HashSet; -use stratum_common::bitcoin::hashes::sha256d::Hash; - -/// The outcome of share validation, from the perspective of a Mining Client. -#[derive(Debug)] -pub enum ShareValidationResult { - Valid, - BlockFound, -} - -/// The error variants that can occur during share validation -#[derive(Debug)] -pub enum ShareValidationError { - Invalid, - Stale, - InvalidJobId, - DoesNotMeetTarget, - VersionRollingNotAllowed, - DuplicateShare, - NoChainTip, -} - -/// The state of share validation on the context of some specific channel (either Extended or -/// Standard) -/// -/// Only meant for usage on Mining Clients. -#[derive(Clone, Debug)] -pub struct ShareAccounting { - last_share_sequence_number: u32, - shares_accepted: u32, - share_work_sum: u64, - seen_shares: HashSet, - best_diff: f64, -} - -impl Default for ShareAccounting { - fn default() -> Self { - Self::new() - } -} - -impl ShareAccounting { - pub fn new() -> Self { - Self { - last_share_sequence_number: 0, - shares_accepted: 0, - share_work_sum: 0, - seen_shares: HashSet::new(), - best_diff: 0.0, - } - } - - pub fn update_share_accounting( - &mut self, - share_work: u64, - share_sequence_number: u32, - share_hash: Hash, - ) { - self.last_share_sequence_number = share_sequence_number; - self.shares_accepted += 1; - self.share_work_sum += share_work; - self.seen_shares.insert(share_hash); - } - - /// clears the hashset of seen shares - /// - /// should be called on every chain tip update - /// to avoid unbounded growth of memory - pub fn flush_seen_shares(&mut self) { - self.seen_shares.clear(); - } - - pub fn get_last_share_sequence_number(&self) -> u32 { - self.last_share_sequence_number - } - - pub fn get_shares_accepted(&self) -> u32 { - self.shares_accepted - } - - pub fn get_share_work_sum(&self) -> u64 { - self.share_work_sum - } - - /// Checks if the share has been seen. - /// Useful to avoid duplicate shares. - pub fn is_share_seen(&self, share_hash: Hash) -> bool { - self.seen_shares.contains(&share_hash) - } - - pub fn get_best_diff(&self) -> f64 { - self.best_diff - } - - /// Updates the best diff if the new diff is higher. - pub fn update_best_diff(&mut self, diff: f64) { - if diff > self.best_diff { - self.best_diff = diff; - } - } -} diff --git a/protocols/v2/roles-logic-sv2/src/channels/mod.rs b/protocols/v2/roles-logic-sv2/src/channels/mod.rs deleted file mode 100644 index a74782d433..0000000000 --- a/protocols/v2/roles-logic-sv2/src/channels/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod chain_tip; -pub mod client; -pub mod server; diff --git a/protocols/v2/roles-logic-sv2/src/channels/server/jobs/error.rs b/protocols/v2/roles-logic-sv2/src/channels/server/jobs/error.rs deleted file mode 100644 index f99adfaf21..0000000000 --- a/protocols/v2/roles-logic-sv2/src/channels/server/jobs/error.rs +++ /dev/null @@ -1,16 +0,0 @@ -pub enum ExtendedJobError { - CoinbaseOutputsSumOverflow, - InvalidCoinbaseOutputsSum, -} - -pub enum StandardJobError {} - -#[derive(Debug)] -pub enum JobFactoryError { - InvalidTemplate(String), - CoinbaseTxPrefixError, - CoinbaseTxSuffixError, - CoinbaseOutputsSumOverflow, - InvalidCoinbaseOutputsSum, - ChainTipRequired, -} diff --git a/protocols/v2/roles-logic-sv2/src/channels/server/jobs/extended.rs b/protocols/v2/roles-logic-sv2/src/channels/server/jobs/extended.rs deleted file mode 100644 index 8f7c1552b9..0000000000 --- a/protocols/v2/roles-logic-sv2/src/channels/server/jobs/extended.rs +++ /dev/null @@ -1,122 +0,0 @@ -use crate::{ - channels::server::jobs::JobOrigin, template_distribution_sv2::NewTemplate, - utils::deserialize_outputs, -}; -use binary_sv2::{Seq0255, Sv2Option, B064K, U256}; -use mining_sv2::{NewExtendedMiningJob, SetCustomMiningJob}; -use stratum_common::bitcoin::transaction::TxOut; - -/// Abstraction of an extended mining job with: -/// - the `NewTemplate` OR `SetCustomMiningJob` message that originated it -/// - the extranonce prefix associated with the channel at the time of job creation -/// - all coinbase outputs (spendable + unspendable) associated with the job -/// - the `NewExtendedMiningJob` message to be sent across the wire -#[derive(Debug, Clone)] -pub struct ExtendedJob<'a> { - origin: JobOrigin<'a>, - extranonce_prefix: Vec, - coinbase_outputs: Vec, - job_message: NewExtendedMiningJob<'a>, -} - -impl<'a> ExtendedJob<'a> { - /// Creates a new job from a template. - /// - /// `additional_coinbase_outputs` are added to the coinbase outputs coming from the template. - pub fn from_template( - template: NewTemplate<'a>, - extranonce_prefix: Vec, - additional_coinbase_outputs: Vec, - job_message: NewExtendedMiningJob<'a>, - ) -> Self { - let mut coinbase_outputs = vec![]; - coinbase_outputs.extend(additional_coinbase_outputs); - coinbase_outputs.extend(deserialize_outputs( - template.coinbase_tx_outputs.inner_as_ref().to_vec(), - )); - - Self { - origin: JobOrigin::NewTemplate(template), - extranonce_prefix, - coinbase_outputs, - job_message, - } - } - - pub fn from_custom_job( - custom_job: SetCustomMiningJob<'a>, - extranonce_prefix: Vec, - coinbase_outputs: Vec, - job_message: NewExtendedMiningJob<'a>, - ) -> Self { - Self { - origin: JobOrigin::SetCustomMiningJob(custom_job), - extranonce_prefix, - coinbase_outputs, - job_message, - } - } - - /// Converts the `ExtendedJob` into a `SetCustomMiningJob` message. - /// - /// To be used by a Sv2 Job Declaration Client after: - /// - creating a non-future extended job from a non-future template - /// - activating a future extended job (that was created from a future template) - pub fn into_custom_job(self) -> SetCustomMiningJob<'a> { - // we need to wait for the outcome of the discussions around - // https://github.com/stratum-mining/sv2-spec/issues/133 - // before implementing this - todo!() - } - - pub fn get_job_id(&self) -> u32 { - self.job_message.job_id - } - - pub fn get_origin(&self) -> &JobOrigin<'a> { - &self.origin - } - - pub fn get_coinbase_tx_prefix(&self) -> &B064K<'a> { - &self.job_message.coinbase_tx_prefix - } - - pub fn get_coinbase_tx_suffix(&self) -> &B064K<'a> { - &self.job_message.coinbase_tx_suffix - } - - pub fn get_extranonce_prefix(&self) -> &Vec { - &self.extranonce_prefix - } - - pub fn get_coinbase_outputs(&self) -> &Vec { - &self.coinbase_outputs - } - - pub fn get_job_message(&self) -> &NewExtendedMiningJob<'a> { - &self.job_message - } - - pub fn get_merkle_path(&self) -> &Seq0255<'a, U256<'a>> { - &self.job_message.merkle_path - } - - pub fn get_min_ntime(&self) -> Sv2Option<'a, u32> { - self.job_message.min_ntime.clone() - } - - pub fn get_version(&self) -> u32 { - self.job_message.version - } - - pub fn version_rolling_allowed(&self) -> bool { - self.job_message.version_rolling_allowed - } - - /// Activates the job, setting the `min_ntime` field of the `NewExtendedMiningJob` message. - /// - /// To be used while activating future jobs upon updating channel `ChainTip` state. - pub fn activate(&mut self, min_ntime: u32) { - self.job_message.min_ntime = Sv2Option::new(Some(min_ntime)); - } -} diff --git a/protocols/v2/roles-logic-sv2/src/channels/server/jobs/factory.rs b/protocols/v2/roles-logic-sv2/src/channels/server/jobs/factory.rs deleted file mode 100644 index 8df9bc7959..0000000000 --- a/protocols/v2/roles-logic-sv2/src/channels/server/jobs/factory.rs +++ /dev/null @@ -1,526 +0,0 @@ -//! Abstraction of a factory for creating Sv2 Extended or Standard Jobs. -use crate::{ - channels::{ - chain_tip::ChainTip, - server::jobs::{error::*, extended::ExtendedJob, standard::StandardJob}, - }, - template_distribution_sv2::NewTemplate, - utils::{deserialize_outputs, merkle_root_from_path, Id as JobIdFactory}, -}; -use binary_sv2::{Sv2Option, B064K}; -use mining_sv2::{NewExtendedMiningJob, NewMiningJob, SetCustomMiningJob, MAX_EXTRANONCE_LEN}; -use std::convert::TryInto; -use stratum_common::bitcoin::{ - absolute::LockTime, - blockdata::witness::Witness, - consensus::{serialize, Decodable}, - transaction::{OutPoint, Transaction, TxIn, TxOut, Version}, - Amount, Sequence, -}; - -/// A Factory for creating Extended or Standard Jobs. -/// -/// Ensures unique job ids. -/// -/// Enables creation of new Extended Jobs from NewTemplate and SetCustomMiningJob messages. -/// -/// Enables creation of new Standard Jobs from NewTemplate messages. -#[derive(Debug, Clone)] -pub struct JobFactory { - job_id_factory: JobIdFactory, - version_rolling_allowed: bool, -} - -impl JobFactory { - pub fn new(version_rolling_allowed: bool) -> Self { - Self { - job_id_factory: JobIdFactory::new(), - version_rolling_allowed, - } - } - - /// Creates a new job from a template. - /// - /// This job (and related shares) is fully committed to: - /// - The template - /// - The additional coinbase outputs (added to the outputs coming from the template) - /// - The extranonce prefix of the channel at the time of job creation - /// - /// The optional `ChainTip` defines whether the job will be future or not. - /// - /// Note: version rolling is always allowed for standard jobs, so the `version_rolling_allowed` - /// parameter is ignored. - pub fn new_standard_job<'a>( - &mut self, - channel_id: u32, - chain_tip: Option, - extranonce_prefix: Vec, - template: NewTemplate<'a>, - additional_coinbase_outputs: Vec, - ) -> Result, JobFactoryError> { - let job_id = self.job_id_factory.next(); - - let version = template.version; - - let coinbase_tx_prefix = - self.coinbase_tx_prefix(template.clone(), additional_coinbase_outputs.clone())?; - let coinbase_tx_suffix = - self.coinbase_tx_suffix(template.clone(), additional_coinbase_outputs.clone())?; - let merkle_path = template.merkle_path.clone(); - let merkle_root = merkle_root_from_path( - coinbase_tx_prefix.inner_as_ref(), - coinbase_tx_suffix.inner_as_ref(), - &extranonce_prefix, - &merkle_path.inner_as_ref(), - ) - .expect("merkle root must be valid") - .try_into() - .expect("merkle root must be 32 bytes"); - - let job_message = match template.future_template { - true => NewMiningJob { - channel_id, - job_id, - min_ntime: Sv2Option::new(None), - version, - merkle_root, - }, - false => { - let min_ntime = match chain_tip { - Some(chain_tip) => Some(chain_tip.min_ntime()), - None => return Err(JobFactoryError::ChainTipRequired), - }; - - NewMiningJob { - channel_id, - job_id, - min_ntime: Sv2Option::new(min_ntime), - version, - merkle_root, - } - } - }; - - let job = StandardJob::from_template( - template, - extranonce_prefix, - additional_coinbase_outputs, - job_message, - ); - - Ok(job) - } - - /// Creates a new job from a template. - /// - /// This job (and related shares) is fully committed to: - /// - The template - /// - The additional coinbase outputs (added to the outputs coming from the template) - /// - The extranonce prefix of the channel at the time of job creation - /// - /// The optional `ChainTip` defines whether the job will be future or not. - pub fn new_extended_job<'a>( - &mut self, - channel_id: u32, - chain_tip: Option, - extranonce_prefix: Vec, - template: NewTemplate<'a>, - additional_coinbase_outputs: Vec, - ) -> Result, JobFactoryError> { - let job_id = self.job_id_factory.next(); - - let version = template.version; - - let coinbase_tx_prefix = - self.coinbase_tx_prefix(template.clone(), additional_coinbase_outputs.clone())?; - let coinbase_tx_suffix = - self.coinbase_tx_suffix(template.clone(), additional_coinbase_outputs.clone())?; - let merkle_path = template.merkle_path.clone(); - - let job_message = match template.future_template { - true => NewExtendedMiningJob { - channel_id, - job_id, - min_ntime: Sv2Option::new(None), - version, - version_rolling_allowed: self.version_rolling_allowed, - merkle_path, - coinbase_tx_prefix, - coinbase_tx_suffix, - }, - false => { - let min_ntime = match chain_tip { - Some(chain_tip) => Some(chain_tip.min_ntime()), - None => return Err(JobFactoryError::ChainTipRequired), - }; - NewExtendedMiningJob { - channel_id, - job_id, - min_ntime: Sv2Option::new(min_ntime), - version, - version_rolling_allowed: self.version_rolling_allowed, - merkle_path, - coinbase_tx_prefix, - coinbase_tx_suffix, - } - } - }; - - let job = ExtendedJob::from_template( - template, - extranonce_prefix, - additional_coinbase_outputs, - job_message, - ); - - Ok(job) - } - - /// Creates a new job from a SetCustomMiningJob message. - /// - /// Assumes that the SetCustomMiningJob message has already been validated. - pub fn new_custom_job<'a>( - &mut self, - set_custom_mining_job: SetCustomMiningJob<'a>, - extranonce_prefix: Vec, - ) -> Result, JobFactoryError> { - let serialized_outputs = set_custom_mining_job - .coinbase_tx_outputs - .inner_as_ref() - .to_vec(); - - let coinbase_outputs = deserialize_outputs(serialized_outputs); - - let job_id = self.job_id_factory.next(); - - let version = set_custom_mining_job.version; - - let coinbase_tx_prefix = self.custom_coinbase_tx_prefix(set_custom_mining_job.clone())?; - let coinbase_tx_suffix = self.custom_coinbase_tx_suffix(set_custom_mining_job.clone())?; - - let merkle_path = set_custom_mining_job.merkle_path.clone().into_static(); - - let job_message = NewExtendedMiningJob { - channel_id: set_custom_mining_job.channel_id, - job_id, - min_ntime: Sv2Option::new(Some(set_custom_mining_job.min_ntime)), - version, - version_rolling_allowed: self.version_rolling_allowed, - coinbase_tx_prefix, - coinbase_tx_suffix, - merkle_path, - }; - - let job = ExtendedJob::from_custom_job( - set_custom_mining_job, - extranonce_prefix, - coinbase_outputs, - job_message, - ); - - Ok(job) - } -} - -// impl block with private methods -impl JobFactory { - // build a coinbase transaction from a SetCustomMiningJob - // this is only used to extract coinbase_tx_prefix and coinbase_tx_suffix from the custom - // coinbase - fn custom_coinbase(&self, m: SetCustomMiningJob<'_>) -> Result { - let deserialized_outputs = - deserialize_outputs(m.coinbase_tx_outputs.inner_as_ref().to_vec()); - - // note: this is assuming there's only one output - // where all the sats are added to - // hopefully we will clean this once we get a clear outcome from - // https://github.com/stratum-mining/sv2-spec/issues/133 - let mut outputs_with_value_remaining = vec![]; - for output in deserialized_outputs.iter() { - if output.script_pubkey.is_p2pk() - || output.script_pubkey.is_p2pkh() - || output.script_pubkey.is_p2tr() - || output.script_pubkey.is_p2wpkh() - || output.script_pubkey.is_p2wsh() - { - let mut output_with_value_remaining = output.clone(); - output_with_value_remaining.value = Amount::from_sat(m.coinbase_tx_value_remaining); - outputs_with_value_remaining.push(output_with_value_remaining); - } else { - outputs_with_value_remaining.push(output.clone()); - } - } - - let mut script_sig = vec![]; - script_sig.extend_from_slice(m.coinbase_prefix.inner_as_ref()); - script_sig.extend_from_slice(&[0; MAX_EXTRANONCE_LEN]); - - // Create transaction input - let tx_in = TxIn { - previous_output: OutPoint::null(), - script_sig: script_sig.into(), - sequence: Sequence(m.coinbase_tx_input_n_sequence), - witness: Witness::from(vec![vec![0; 32]]), - }; - - Ok(Transaction { - version: Version::non_standard(m.coinbase_tx_version as i32), - lock_time: LockTime::from_consensus(m.coinbase_tx_locktime), - input: vec![tx_in], - output: outputs_with_value_remaining, - }) - } - - fn custom_coinbase_tx_prefix( - &self, - m: SetCustomMiningJob<'_>, - ) -> Result, JobFactoryError> { - let coinbase = self.custom_coinbase(m.clone())?; - let serialized_coinbase = serialize(&coinbase); - - let index = 4 // tx version - + 2 // segwit - + 1 // number of inputs - + 32 // prev OutPoint - + 4 // index - + 1 // bytes in script - + m.coinbase_prefix.inner_as_ref().len(); // script_sig_prefix - - let r = serialized_coinbase[0..index].to_vec(); - - r.try_into() - .map_err(|_| JobFactoryError::CoinbaseTxPrefixError) - } - - fn custom_coinbase_tx_suffix( - &self, - m: SetCustomMiningJob<'_>, - ) -> Result, JobFactoryError> { - let coinbase = self.custom_coinbase(m.clone())?; - let serialized_coinbase = serialize(&coinbase); - - // Calculate full extranonce size - let full_extranonce_size = MAX_EXTRANONCE_LEN; - - let index = 4 // tx version - + 2 // segwit - + 1 // number of inputs - + 32 // prev OutPoint - + 4 // index - + 1 // bytes in script - + m.coinbase_prefix.inner_as_ref().len() // script_sig_prefix - + full_extranonce_size; - - let r = serialized_coinbase[index..].to_vec(); - - r.try_into() - .map_err(|_| JobFactoryError::CoinbaseTxSuffixError) - } - - // build a coinbase transaction from some template in the JobFactory - fn coinbase( - &self, - template: NewTemplate<'_>, - coinbase_reward_outputs: Vec, - ) -> Result { - // check that the sum of the additional coinbase outputs is equal to the value remaining in - // the active template - let mut coinbase_reward_outputs_sum = Amount::from_sat(0); - for output in coinbase_reward_outputs.iter() { - coinbase_reward_outputs_sum = coinbase_reward_outputs_sum - .checked_add(output.value) - .ok_or(JobFactoryError::CoinbaseOutputsSumOverflow)?; - } - - if template.coinbase_tx_value_remaining < coinbase_reward_outputs_sum.to_sat() { - return Err(JobFactoryError::InvalidCoinbaseOutputsSum); - } - - let mut outputs = vec![]; - - for output in coinbase_reward_outputs.iter() { - outputs.push(output.clone()); - } - - let serialized_template_outputs = template.coinbase_tx_outputs.to_vec(); - let mut cursor = 0; - let mut txouts = &serialized_template_outputs[cursor..]; - while let Ok(out) = TxOut::consensus_decode(&mut txouts) { - let len = match out.script_pubkey.len() { - a @ 0..=252 => 8 + 1 + a, - a @ 253..=10000 => 8 + 3 + a, - _ => break, - }; - cursor += len; - outputs.push(out); - } - - let mut script_sig = vec![]; - script_sig.extend_from_slice(&template.coinbase_prefix.to_vec()); - script_sig.extend_from_slice(&[0; MAX_EXTRANONCE_LEN]); - - let tx_in = TxIn { - previous_output: OutPoint::null(), - script_sig: script_sig.into(), - sequence: Sequence(template.coinbase_tx_input_sequence), - witness: Witness::from(vec![vec![0; 32]]), - }; - - Ok(Transaction { - version: Version::non_standard(template.coinbase_tx_version as i32), - lock_time: LockTime::from_consensus(template.coinbase_tx_locktime), - input: vec![tx_in], - output: outputs, - }) - } - - fn coinbase_tx_prefix( - &self, - template: NewTemplate<'_>, - coinbase_reward_outputs: Vec, - ) -> Result, JobFactoryError> { - let coinbase = self.coinbase(template.clone(), coinbase_reward_outputs)?; - let serialized_coinbase = serialize(&coinbase); - - let index = 4 // tx version - + 2 // segwit bytes - + 1 // number of inputs - + 32 // prev OutPoint - + 4 // index - + 1 // bytes in script - + template.coinbase_prefix.len(); // script_sig_prefix - - let r = serialized_coinbase[0..index].to_vec(); - - r.try_into() - .map_err(|_| JobFactoryError::CoinbaseTxPrefixError) - } - - fn coinbase_tx_suffix( - &self, - template: NewTemplate<'_>, - coinbase_reward_outputs: Vec, - ) -> Result, JobFactoryError> { - let coinbase = self.coinbase(template.clone(), coinbase_reward_outputs)?; - let serialized_coinbase = serialize(&coinbase); - - let full_extranonce_size = MAX_EXTRANONCE_LEN; - - let r = serialized_coinbase[4 // tx version - + 2 // segwit bytes - + 1 // number of inputs - + 32 // prev OutPoint - + 4 // index - + 1 // bytes in script - + template.coinbase_prefix.len() // script_sig_prefix - + full_extranonce_size..] - .to_vec(); - - r.try_into() - .map_err(|_| JobFactoryError::CoinbaseTxSuffixError) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use stratum_common::bitcoin::ScriptBuf; - use template_distribution_sv2::NewTemplate; - - #[test] - fn test_new_job() { - let mut job_factory = JobFactory::new(true); - - // note: - // the messages on this test were collected from a sane message flow - // we use them as test vectors to assert correct behavior of job creation - - let template = NewTemplate { - template_id: 1, - future_template: true, - version: 536870912, - coinbase_tx_version: 2, - coinbase_prefix: vec![82, 0].try_into().unwrap(), - coinbase_tx_input_sequence: 4294967295, - coinbase_tx_value_remaining: 5000000000, - coinbase_tx_outputs_count: 1, - coinbase_tx_outputs: vec![ - 0, 0, 0, 0, 0, 0, 0, 0, 38, 106, 36, 170, 33, 169, 237, 226, 246, 28, 63, 113, 209, - 222, 253, 63, 169, 153, 223, 163, 105, 83, 117, 92, 105, 6, 137, 121, 153, 98, 180, - 139, 235, 216, 54, 151, 78, 140, 249, - ] - .try_into() - .unwrap(), - coinbase_tx_locktime: 0, - merkle_path: vec![].try_into().unwrap(), - }; - - // match the original script format used to generate the coinbase_reward_outputs for the - // expected job - let pubkey_hash = [ - 235, 225, 183, 220, 194, 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194, - 8, 252, - ]; - let mut script_bytes = vec![0]; // SegWit version 0 - script_bytes.push(20); // Push 20 bytes (length of pubkey hash) - script_bytes.extend_from_slice(&pubkey_hash); - let script = ScriptBuf::from(script_bytes); - let coinbase_reward_outputs = vec![TxOut { - value: Amount::from_sat(5000000000), - script_pubkey: script, - }]; - - // match the original extranonce_prefix used to generate the expected job - let extranonce_prefix = [ - 83, 116, 114, 97, 116, 117, 109, 32, 86, 50, 32, 83, 82, 73, 32, 80, 111, 111, 108, 0, - 0, 0, 0, 0, 0, 0, 1, - ] - .to_vec(); - - let job = job_factory - .new_extended_job( - 1, - None, - extranonce_prefix, - template, - coinbase_reward_outputs, - ) - .unwrap(); - - // we know that the provided template should generate this job - let expected_job = NewExtendedMiningJob { - channel_id: 1, - job_id: 1, - min_ntime: Sv2Option::new(None), - version: 536870912, - version_rolling_allowed: true, - coinbase_tx_prefix: vec![ - 2, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 34, 82, 0, - ] - .try_into() - .unwrap(), - coinbase_tx_suffix: vec![ - 255, 255, 255, 255, 2, 0, 242, 5, 42, 1, 0, 0, 0, 22, 0, 20, 235, 225, 183, 220, - 194, 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194, 8, 252, 0, 0, 0, - 0, 0, 0, 0, 0, 38, 106, 36, 170, 33, 169, 237, 226, 246, 28, 63, 113, 209, 222, - 253, 63, 169, 153, 223, 163, 105, 83, 117, 92, 105, 6, 137, 121, 153, 98, 180, 139, - 235, 216, 54, 151, 78, 140, 249, 1, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ] - .try_into() - .unwrap(), - merkle_path: vec![].try_into().unwrap(), - }; - - assert_eq!(job.get_job_message(), &expected_job); - } - - #[test] - fn test_new_custom_job() { - // todo: assert that a SetCustomMiningJob leads to - // the correct NewExtendedMiningJob message - // we should wait until the following spec cleanup is finished - // https://github.com/stratum-mining/sv2-spec/issues/133 - } -} diff --git a/protocols/v2/roles-logic-sv2/src/channels/server/jobs/mod.rs b/protocols/v2/roles-logic-sv2/src/channels/server/jobs/mod.rs deleted file mode 100644 index e980dbcdb6..0000000000 --- a/protocols/v2/roles-logic-sv2/src/channels/server/jobs/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub mod error; -pub mod extended; -pub mod factory; -pub mod standard; - -use mining_sv2::SetCustomMiningJob; -use template_distribution_sv2::NewTemplate; - -#[derive(Clone, Debug)] -pub enum JobOrigin<'a> { - NewTemplate(NewTemplate<'a>), - SetCustomMiningJob(SetCustomMiningJob<'a>), -} diff --git a/protocols/v2/roles-logic-sv2/src/channels/server/jobs/standard.rs b/protocols/v2/roles-logic-sv2/src/channels/server/jobs/standard.rs deleted file mode 100644 index 3aa6f90561..0000000000 --- a/protocols/v2/roles-logic-sv2/src/channels/server/jobs/standard.rs +++ /dev/null @@ -1,71 +0,0 @@ -use crate::utils::deserialize_outputs; -use binary_sv2::{Sv2Option, U256}; -use mining_sv2::NewMiningJob; -use stratum_common::bitcoin::transaction::TxOut; -use template_distribution_sv2::NewTemplate; - -/// Abstraction of a standard mining job with: -/// - the `NewTemplate` message that originated it -/// - the extranonce prefix associated with the channel at the time of job creation -/// - all coinbase outputs (spendable + unspendable) associated with the job -/// - the `NewMiningJob` message to be sent across the wire -#[derive(Debug, Clone)] -pub struct StandardJob<'a> { - template: NewTemplate<'a>, - extranonce_prefix: Vec, - coinbase_outputs: Vec, - job_message: NewMiningJob<'a>, -} - -impl<'a> StandardJob<'a> { - pub fn from_template( - template: NewTemplate<'a>, - extranonce_prefix: Vec, - additional_coinbase_outputs: Vec, - job_message: NewMiningJob<'a>, - ) -> Self { - let mut coinbase_outputs = vec![]; - coinbase_outputs.extend(additional_coinbase_outputs); - coinbase_outputs.extend(deserialize_outputs( - template.coinbase_tx_outputs.inner_as_ref().to_vec(), - )); - Self { - template, - extranonce_prefix, - coinbase_outputs, - job_message, - } - } - - pub fn get_job_id(&self) -> u32 { - self.job_message.job_id - } - - pub fn get_coinbase_outputs(&self) -> &Vec { - &self.coinbase_outputs - } - - pub fn get_extranonce_prefix(&self) -> &Vec { - &self.extranonce_prefix - } - - pub fn get_job_message(&self) -> &NewMiningJob<'a> { - &self.job_message - } - - pub fn get_template(&self) -> &NewTemplate<'a> { - &self.template - } - - pub fn get_merkle_root(&self) -> &U256<'a> { - &self.job_message.merkle_root - } - - pub fn is_future(&self) -> bool { - self.job_message.min_ntime.clone().into_inner().is_none() - } - - pub fn activate(&mut self, min_ntime: u32) { - self.job_message.min_ntime = Sv2Option::new(Some(min_ntime)); - } -} diff --git a/protocols/v2/roles-logic-sv2/src/channels/server/share_accounting.rs b/protocols/v2/roles-logic-sv2/src/channels/server/share_accounting.rs deleted file mode 100644 index 42ea0bd913..0000000000 --- a/protocols/v2/roles-logic-sv2/src/channels/server/share_accounting.rs +++ /dev/null @@ -1,125 +0,0 @@ -//! Abstractions for share validation for a Mining Server - -use std::collections::HashSet; -use stratum_common::bitcoin::hashes::sha256d::Hash; - -/// The outcome of share validation, from the perspective of a Mining Server. -/// -/// The [`ShareValidationResult::ValidWithAcknowledgement`] variant carries: -/// - `last_sequence_number` (as `u32`) -/// - `new_submits_accepted_count` (as `u32`) -/// - `new_shares_sum` (as `u64`) -/// -/// which are used to craft `SubmitShares.Success` Sv2 messages. -/// -/// The [`ShareValidationResult::BlockFound`] variant carries: -/// - `template_id` (as `Option`) -/// - `coinbase` (as `Vec`) -/// -/// where `template_id` is `None` if the share is for a custom job. -#[derive(Debug)] -pub enum ShareValidationResult { - Valid, - // last_sequence_number, new_submits_accepted_count, new_shares_sum - ValidWithAcknowledgement(u32, u32, u64), - // template_id, coinbase - // template_id is None if custom job - BlockFound(Option, Vec), -} - -/// The error variants that can occur during share validation -#[derive(Debug)] -pub enum ShareValidationError { - Invalid, - Stale, - InvalidJobId, - DoesNotMeetTarget, - VersionRollingNotAllowed, - DuplicateShare, - InvalidCoinbase, - NoChainTip, -} - -/// The state of share validation on the context of some specific channel (either Extended or -/// Standard) -/// -/// Only meant for usage on Mining Servers. -#[derive(Clone, Debug)] -pub struct ShareAccounting { - last_share_sequence_number: u32, - shares_accepted: u32, - share_work_sum: u64, - share_batch_size: usize, - seen_shares: HashSet, - best_diff: f64, -} - -impl ShareAccounting { - pub fn new(share_batch_size: usize) -> Self { - Self { - last_share_sequence_number: 0, - shares_accepted: 0, - share_work_sum: 0, - share_batch_size, - seen_shares: HashSet::new(), - best_diff: 0.0, - } - } - - pub fn update_share_accounting( - &mut self, - share_work: u64, - share_sequence_number: u32, - share_hash: Hash, - ) { - self.last_share_sequence_number = share_sequence_number; - self.shares_accepted += 1; - self.share_work_sum += share_work; - self.seen_shares.insert(share_hash); - } - - /// clears the hashset of seen shares - /// - /// should be called on every chain tip update - /// to avoid unbounded growth of memory - pub fn flush_seen_shares(&mut self) { - self.seen_shares.clear(); - } - - pub fn get_last_share_sequence_number(&self) -> u32 { - self.last_share_sequence_number - } - - pub fn get_shares_accepted(&self) -> u32 { - self.shares_accepted - } - - pub fn get_share_work_sum(&self) -> u64 { - self.share_work_sum - } - - pub fn get_share_batch_size(&self) -> usize { - self.share_batch_size - } - - pub fn should_acknowledge(&self) -> bool { - self.shares_accepted % self.share_batch_size as u32 == 0 - } - - /// Checks if the share has been seen. - /// Useful to avoid duplicate shares. - pub fn is_share_seen(&self, share_hash: Hash) -> bool { - self.seen_shares.contains(&share_hash) - } - - pub fn get_best_diff(&self) -> f64 { - self.best_diff - } - - /// Updates the best diff if the new diff is higher. - pub fn update_best_diff(&mut self, diff: f64) { - if diff > self.best_diff { - self.best_diff = diff; - } - } -} diff --git a/protocols/v2/roles-logic-sv2/src/common_properties.rs b/protocols/v2/roles-logic-sv2/src/common_properties.rs deleted file mode 100644 index 535a7fd292..0000000000 --- a/protocols/v2/roles-logic-sv2/src/common_properties.rs +++ /dev/null @@ -1,344 +0,0 @@ -//! # Common Properties for Stratum V2 (Sv2) Roles -//! -//! Defines common properties, traits, and utilities for implementing upstream and downstream -//! nodes. It provides abstractions for features like connection pairing, mining job distribution, -//! and channel management. These definitions form the foundation for consistent communication and -//! behavior across Sv2 roles/applications. - -use common_messages_sv2::{has_requires_std_job, Protocol, SetupConnection}; -use mining_sv2::{Extranonce, Target}; -use nohash_hasher::BuildNoHashHasher; -use std::collections::HashMap; - -/// Defines a mining downstream node at the most basic level. -#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] -pub struct CommonDownstreamData { - /// Header-only mining flag. - /// - /// Enables the processing of standard jobs only, leaving merkle root manipulation to the - /// upstream node. - /// - /// - `true`: The downstream node only processes standard jobs, relying on the upstream for - /// merkle root manipulation. - /// - `false`: The downstream node handles extended jobs and merkle root manipulation. - pub header_only: bool, - - /// Work selection flag. - /// - /// Enables the selection of which transactions or templates the node will work on. - /// - /// - `true`: The downstream node works on a custom block template, using the Job Declaration - /// Protocol. - /// - `false`: The downstream node strictly follows the work provided by the upstream, based on - /// pre-constructed templates from the upstream (e.g. Pool). - pub work_selection: bool, - - /// Version rolling flag. - /// - /// Enables rolling the block header version bits which allows for more unique hash generation - /// on the same mining job by expanding the nonce-space. Used when other fields (e.g. - /// `nonce` or `extranonce`) are fully exhausted. - /// - /// - `true`: The downstream supports version rolling and can modify specific bits in the - /// version field. This is useful for increasing hash rate efficiency by exploring a larger - /// solution space. - /// - `false`: The downstream does not support version rolling and relies on the upstream to - /// provide jobs with adjusted version fields. - pub version_rolling: bool, -} - -/// Encapsulates settings for pairing upstream and downstream nodes. -/// -/// Simplifies the [`SetupConnection`] configuration process by bundling the protocol, version -/// range, and flag settings. -#[derive(Debug, Copy, Clone)] -pub struct PairSettings { - /// Protocol the settings apply to. - pub protocol: Protocol, - - /// Minimum protocol version the node supports. - pub min_v: u16, - - /// Minimum protocol version the node supports. - pub max_v: u16, - - /// Flags indicating optional protocol features the node supports (e.g. header-only mining, - /// work selection, version-rolling, etc.). Each protocol field as its own - /// flags. - pub flags: u32, -} - -/// Properties defining behaviors common to all Sv2 upstream nodes. -pub trait IsUpstream { - /// Returns the protocol version used by the upstream node. - fn get_version(&self) -> u16; - - /// Returns the flags indicating the upstream node's protocol capabilities. - fn get_flags(&self) -> u32; - - /// Lists the protocols supported by the upstream node. - /// - /// Used to check if the upstream supports the protocol that the downstream wants to use. - fn get_supported_protocols(&self) -> Vec; - - /// Verifies if the upstream can pair with the given downstream settings. - fn is_pairable(&self, pair_settings: &PairSettings) -> bool { - let protocol = pair_settings.protocol; - let min_v = pair_settings.min_v; - let max_v = pair_settings.max_v; - let flags = pair_settings.flags; - - let check_version = self.get_version() >= min_v && self.get_version() <= max_v; - let check_flags = SetupConnection::check_flags(protocol, self.get_flags(), flags); - check_version && check_flags - } - - /// Returns the channel ID managed by the upstream node. - fn get_id(&self) -> u32; - - /// Provides a request ID mapper for viewing and managing upstream-downstream communication. - fn get_mapper(&mut self) -> Option<&mut RequestIdMapper>; -} - -/// The types of channels that can be opened with upstream nodes. -#[derive(Debug, Clone, Copy)] -pub enum UpstreamChannel { - /// A standard channel with a nominal hash rate. - /// - /// Typically used for mining devices with a direct connection to an upstream node (e.g. a pool - /// or proxy). The hashrate is specified as a `f32` value, representing the expected - /// computational capacity of the miner. - Standard(f32), - - /// A grouped channel for aggregated mining. - /// - /// Aggregates mining work for multiple standard channels under a single group channel, - /// enabling the upstream to manage work distribution and result aggregation for an entire - /// group of channels. - /// - /// Typically used by a mining proxy managing multiple downstream miners. - Group, - - /// An extended channel for additional features. - /// - /// Provides additional features or capabilities beyond standard and group channels, - /// accommodating features like custom job templates or experimental protocol extensions. - Extended, -} - -/// Properties of a standard mining channel. -/// -/// Standard channels are intended to be used by end mining devices with a nominal hashrate, where -/// each device operates on an independent channel to its upstream. -#[derive(Debug, Clone)] -pub struct StandardChannel { - /// Identifies a specific channel, unique to each mining connection. - /// - /// Dynamically assigned when a mining connection is established (as part of the negotiation - /// process) to avoid conflicts with other connections (e.g. other mining devices) managed by - /// the same upstream node. The identifier remains stable for the whole lifetime of the - /// connection. - /// - /// Used for broadcasting new jobs by [`mining_sv2::NewMiningJob`]. - pub channel_id: u32, - - /// Identifies a specific group in which the standard channel belongs. - pub group_id: u32, - - /// Initial difficulty target assigned to the mining. - pub target: Target, - - /// Additional nonce value used to differentiate shares within the same channel. - /// - /// Helps to avoid nonce collisions when multiple mining devices are working on the same job. - /// - /// The extranonce bytes are added to the coinbase to form a fully valid submission: - /// `full coinbase = coinbase_tx_prefix + extranonce_prefix + extranonce + coinbase_tx_suffix` - pub extranonce: Extranonce, -} - -/// Properties of a Sv2-compatible mining upstream node. -/// -/// This trait extends [`IsUpstream`] with additional functionality specific to mining, such as -/// hashrate management and channel updates. -pub trait IsMiningUpstream: IsUpstream { - /// Returns the total hashrate managed by the upstream node. - fn total_hash_rate(&self) -> u64; - - /// Adds hash rate to the upstream node. - fn add_hash_rate(&mut self, to_add: u64); - - /// Returns the list of open channels on the upstream node. - fn get_opened_channels(&mut self) -> &mut Vec; - - /// Updates the list of channels managed by the upstream node. - fn update_channels(&mut self, c: UpstreamChannel); - - /// Checks if the upstream node supports header-only mining. - fn is_header_only(&self) -> bool { - has_requires_std_job(self.get_flags()) - } -} - -/// Properties defining behaviors common to all Sv2 downstream nodes. -pub trait IsDownstream { - /// Returns the common properties of the downstream node (e.g. support for header-only mining, - /// work selection, and version rolling). - fn get_downstream_mining_data(&self) -> CommonDownstreamData; -} - -/// Properties of a SV2-compatible mining downstream node. -/// -/// This trait extends [`IsDownstream`] with additional functionality specific to mining, such as -/// header-only mining checks. -pub trait IsMiningDownstream: IsDownstream { - /// Checks if the downstream node supports header-only mining. - fn is_header_only(&self) -> bool { - self.get_downstream_mining_data().header_only - } -} - -// Implemented for the `NullDownstreamMiningSelector`. -impl IsUpstream for () { - fn get_version(&self) -> u16 { - unreachable!("Null upstream do not have a version"); - } - - fn get_flags(&self) -> u32 { - unreachable!("Null upstream do not have flags"); - } - - fn get_supported_protocols(&self) -> Vec { - unreachable!("Null upstream do not support any protocol"); - } - fn get_id(&self) -> u32 { - unreachable!("Null upstream do not have an ID"); - } - - fn get_mapper(&mut self) -> Option<&mut RequestIdMapper> { - unreachable!("Null upstream do not have a mapper") - } -} - -// Implemented for the `NullDownstreamMiningSelector`. -impl IsDownstream for () { - fn get_downstream_mining_data(&self) -> CommonDownstreamData { - unreachable!("Null downstream do not have mining data"); - } -} - -impl IsMiningUpstream for () { - fn total_hash_rate(&self) -> u64 { - unreachable!("Null selector do not have hash rate"); - } - - fn add_hash_rate(&mut self, _to_add: u64) { - unreachable!("Null selector can not add hash rate"); - } - fn get_opened_channels(&mut self) -> &mut Vec { - unreachable!("Null selector can not open channels"); - } - - fn update_channels(&mut self, _: UpstreamChannel) { - unreachable!("Null selector can not update channels"); - } -} - -impl IsMiningDownstream for () {} - -/// Maps request IDs between upstream and downstream nodes. -/// -/// Most commonly used by proxies, this struct facilitates communication between upstream and -/// downstream nodes by mapping request IDs. This ensures responses are routed correctly back from -/// the upstream to the originating downstream requester, even when request IDs are modified in -/// transit. -/// -/// ### Workflow -/// 1. **Request Mapping**: When a downstream node sends a request, `on_open_channel` assigns a new -/// unique upstream request ID and stores the mapping. -/// 2. **Response Mapping**: When the upstream responds, the proxy uses the map to translate the -/// upstream ID back to the original downstream ID. -/// 3. **Cleanup**: Once the responses are processed, the mapping is removed. -#[derive(Debug, Default, PartialEq, Eq)] -pub struct RequestIdMapper { - // A mapping between upstream-assigned request IDs and the original downstream IDs. - // - // In the hashmap, the key is the upstream request ID and the value is the corresponding - // downstream request ID. `BuildNoHashHasher` is an optimization to bypass the hashing step for - // integer keys. - request_ids_map: HashMap>, - - // A counter for assigning unique request IDs to upstream nodes, incrementing after every - // assignment. - next_id: u32, -} - -impl RequestIdMapper { - /// Creates a new [`RequestIdMapper`] instance. - pub fn new() -> Self { - Self { - request_ids_map: HashMap::with_hasher(BuildNoHashHasher::default()), - next_id: 0, - } - } - - /// Assigns a new upstream request ID for a request sent by a downstream node. - /// - /// Ensures every request forwarded to the upstream node has a unique ID while retaining - /// traceability to the original downstream request. - pub fn on_open_channel(&mut self, id: u32) -> u32 { - let new_id = self.next_id; // Assign new upstream ID - self.next_id += 1; // Increment next_id for future requests - - self.request_ids_map.insert(new_id, id); // Map new upstream ID to downstream ID - new_id - } - - /// Removes the mapping for a request ID once the response has been processed. - pub fn remove(&mut self, upstream_id: u32) -> Option { - self.request_ids_map.remove(&upstream_id) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn builds_request_id_mapper() { - let expect = RequestIdMapper { - request_ids_map: HashMap::with_hasher(BuildNoHashHasher::default()), - next_id: 0, - }; - let actual = RequestIdMapper::new(); - - assert_eq!(expect, actual); - } - - #[test] - fn updates_request_id_mapper_on_open_channel() { - let id = 0; - let mut expect = RequestIdMapper { - request_ids_map: HashMap::with_hasher(BuildNoHashHasher::default()), - next_id: id, - }; - let new_id = expect.next_id; - expect.next_id += 1; - expect.request_ids_map.insert(new_id, id); - - let mut actual = RequestIdMapper::new(); - actual.on_open_channel(0); - - assert_eq!(expect, actual); - } - - #[test] - fn removes_id_from_request_id_mapper() { - let mut request_id_mapper = RequestIdMapper::new(); - request_id_mapper.on_open_channel(0); - assert!(!request_id_mapper.request_ids_map.is_empty()); - - request_id_mapper.remove(0); - assert!(request_id_mapper.request_ids_map.is_empty()); - } -} diff --git a/protocols/v2/roles-logic-sv2/src/errors.rs b/protocols/v2/roles-logic-sv2/src/errors.rs index 917e93aac3..2100c375f5 100644 --- a/protocols/v2/roles-logic-sv2/src/errors.rs +++ b/protocols/v2/roles-logic-sv2/src/errors.rs @@ -3,20 +3,19 @@ //! This module defines error types and utilities for handling errors in the `roles_logic_sv2` //! module. It includes the [`Error`] enum for representing various errors. -use crate::{ - channels::server::error::{ExtendedChannelError, GroupChannelError, StandardChannelError}, - common_properties::CommonDownstreamData, - parsers::AnyMessage as AllMessages, - utils::InputError, +use bitcoin::hashes::FromSliceError; +use channels_sv2::{ + server::error::{ExtendedChannelError, GroupChannelError, StandardChannelError}, + target::InputError, vardiff::error::VardiffError, }; -use binary_sv2::Error as BinarySv2Error; +use codec_sv2::binary_sv2::Error as BinarySv2Error; use mining_sv2::ExtendedExtranonceError; +use parsers_sv2::AnyMessage as AllMessages; use std::{ fmt::{self, Display, Formatter}, sync::{MutexGuard, PoisonError}, }; -use stratum_common::bitcoin::hashes::FromSliceError; /// Error enum #[derive(Debug)] @@ -37,8 +36,6 @@ pub enum Error { NoGroupIdOnExtendedChannel, /// No pairable upstream. Parameters are: (`min_v`, `max_v`, all flags supported) NoPairableUpstream((u16, u16, u32)), - /// No compatible upstream - NoCompatibleUpstream(CommonDownstreamData), /// Error if the hashmap `future_jobs` field in the `GroupChannelJobDispatcher` is empty. NoFutureJobs, /// No Downstream's connected @@ -131,9 +128,13 @@ pub enum Error { FailedToProcessNewTemplateStandardChannel(StandardChannelError), FailedToProcessSetNewPrevHashExtendedChannel(ExtendedChannelError), FailedToProcessSetNewPrevHashStandardChannel(StandardChannelError), + FailedToProcessGroupChannelJob(StandardChannelError), NoActiveJob, FailedToSendSolution, FailedToSetCustomMiningJob(ExtendedChannelError), + FailedToDeserializeCoinbaseOutputs, + /// Error from parsers_sv2 + ParserError(parsers_sv2::ParserError), } impl From for Error { @@ -160,6 +161,12 @@ impl From for Error { } } +impl From for Error { + fn from(v: parsers_sv2::ParserError) -> Error { + Error::ParserError(v) + } +} + impl Display for Error { fn fmt(&self, f: &mut Formatter) -> fmt::Result { use Error::*; @@ -167,8 +174,7 @@ impl Display for Error { BadPayloadSize => write!(f, "Payload is too big to fit into the frame"), BinarySv2Error(v) => write!( f, - "BinarySv2Error: error in serializing/deserializing binary format {:?}", - v + "BinarySv2Error: error in serializing/deserializing binary format {v:?}" ), DownstreamDown => { write!( @@ -176,25 +182,22 @@ impl Display for Error { "Downstream is not connected anymore" ) } - ExpectedLen32(l) => write!(f, "Expected length of 32, but received length of {}", l), + ExpectedLen32(l) => write!(f, "Expected length of 32, but received length of {l}"), NoGroupsFound => write!( f, "A channel was attempted to be added to an Upstream, but no groups are specified" ), - UnexpectedMessage(type_) => write!(f, "Error: Unexpected message received. Recv m type: {:x}", type_), + UnexpectedMessage(type_) => write!(f, "Error: Unexpected message received. Recv m type: {type_:x}"), NoGroupIdOnExtendedChannel => write!(f, "Extended channels do not have group IDs"), NoPairableUpstream(a) => { - write!(f, "No pairable upstream node: {:?}", a) - } - NoCompatibleUpstream(a) => { - write!(f, "No compatible upstream node: {:?}", a) + write!(f, "No pairable upstream node: {a:?}") } NoFutureJobs => write!(f, "GroupChannelJobDispatcher does not have any future jobs"), NoDownstreamsConnected => write!(f, "NoDownstreamsConnected"), PrevHashRequireNonExistentJobId(id) => { - write!(f, "PrevHashRequireNonExistentJobId {}", id) + write!(f, "PrevHashRequireNonExistentJobId {id}") } - RequestIdNotMapped(id) => write!(f, "RequestIdNotMapped {}", id), + RequestIdNotMapped(id) => write!(f, "RequestIdNotMapped {id}"), NoUpstreamsConnected => write!(f, "There are no upstream connected"), UnexpectedPoolMessage => write!(f, "Unexpected `PoolMessage` type"), UnimplementedProtocol => write!( @@ -203,16 +206,14 @@ impl Display for Error { ), UnknownRequestId(id) => write!( f, - "Upstream is answering with a wrong request ID {} or + "Upstream is answering with a wrong request ID {id} or DownstreamMiningSelector::on_open_standard_channel_request has not been called - before relaying open channel request to upstream", - id + before relaying open channel request to upstream" ), InvalidExtranonceSize(required_min, requested) => { write!( f, - "Invalid extranonce size: required min {}, requested {}", - required_min, requested + "Invalid extranonce size: required min {required_min}, requested {requested}" ) }, NoMoreExtranonces => write!(f, "No more extranonces"), @@ -228,37 +229,40 @@ impl Display for Error { VersionTooBig => write!(f, "We are trying to construct a block header with version bigger than i32::MAX"), TxVersionTooBig => write!(f, "Tx version can not be greater than i32::MAX"), TxVersionTooLow => write!(f, "Tx version can not be lower than 1"), - TxDecodingError(e) => write!(f, "Impossible to decode tx: {:?}", e), + TxDecodingError(e) => write!(f, "Impossible to decode tx: {e:?}"), NotFoundChannelId => write!(f, "No downstream has been registered for this channel id"), NoValidJob => write!(f, "Impossible to create a standard job for channelA cause no valid job has been received from upstream yet"), NoValidTranslatorJob => write!(f, "Impossible to create a extended job for channel cause no valid job has been received from upstream yet"), NoTemplateForId => write!(f, "Impossible to retrieve a template for the required job id"), - NoValidTemplate(e) => write!(f, "Impossible to retrieve a template for the required template id: {}", e), - PoisonLock(e) => write!(f, "Poison lock: {}", e), - JobNotUpdated(ds_job_id, us_job_id) => write!(f, "Channel Factory did not update job: Downstream job id = {}, Upstream job id = {}", ds_job_id, us_job_id), - TargetError(e) => write!(f, "Impossible to get Target: {:?}", e), - HashrateError(e) => write!(f, "Impossible to get Hashrate: {:?}", e), - LogicErrorMessage(e) => write!(f, "Message is well formatted but can not be handled: {:?}", e), + NoValidTemplate(e) => write!(f, "Impossible to retrieve a template for the required template id: {e}"), + PoisonLock(e) => write!(f, "Poison lock: {e}"), + JobNotUpdated(ds_job_id, us_job_id) => write!(f, "Channel Factory did not update job: Downstream job id = {ds_job_id}, Upstream job id = {us_job_id}"), + TargetError(e) => write!(f, "Impossible to get Target: {e:?}"), + HashrateError(e) => write!(f, "Impossible to get Hashrate: {e:?}"), + LogicErrorMessage(e) => write!(f, "Message is well formatted but can not be handled: {e:?}"), JDSMissingTransactions => write!(f, "JD server cannot propagate the block: missing transactions"), - IoError(e) => write!(f, "IO error: {:?}", e), - ExtendedExtranonceCreationFailed(e) => write!(f, "Failed to create ExtendedExtranonce: {}", e), - FromSliceError(e) => write!(f, "Failed to hash from slice: {}", e), - InvalidUserIdentity(e) => write!(f, "Invalid user identity: {}", e), - ExtranoncePrefixFactoryError(e) => write!(f, "Failed to create ExtranoncePrefixFactory: {:?}", e), - Vardiff(e) => write!(f, "Failed to adjust diff in vardiff module: {:?}", e), - FailedToCreateStandardChannel(e) => write!(f, "Failed to create StandardChannel: {:?}", e), - FailedToCreateExtendedChannel(e) => write!(f, "Failed to create ExtendedChannel: {:?}", e), - FailedToProcessNewTemplateGroupChannel(e) => write!(f, "Failed to process NewTemplate: {:?}", e), - FailedToProcessSetNewPrevHashGroupChannel(e) => write!(f, "Failed to process SetNewPrevHash: {:?}", e), + IoError(e) => write!(f, "IO error: {e:?}"), + ExtendedExtranonceCreationFailed(e) => write!(f, "Failed to create ExtendedExtranonce: {e}"), + FromSliceError(e) => write!(f, "Failed to hash from slice: {e}"), + InvalidUserIdentity(e) => write!(f, "Invalid user identity: {e}"), + ExtranoncePrefixFactoryError(e) => write!(f, "Failed to create ExtranoncePrefixFactory: {e:?}"), + Vardiff(e) => write!(f, "Failed to adjust diff in vardiff module: {e:?}"), + FailedToCreateStandardChannel(e) => write!(f, "Failed to create StandardChannel: {e:?}"), + FailedToCreateExtendedChannel(e) => write!(f, "Failed to create ExtendedChannel: {e:?}"), + FailedToProcessNewTemplateGroupChannel(e) => write!(f, "Failed to process NewTemplate: {e:?}"), + FailedToProcessSetNewPrevHashGroupChannel(e) => write!(f, "Failed to process SetNewPrevHash: {e:?}"), NoActiveJob => write!(f, "No active job"), - FailedToUpdateStandardChannel(e) => write!(f, "Failed to update StandardChannel: {:?}", e), - FailedToUpdateExtendedChannel(e) => write!(f, "Failed to update ExtendedChannel: {:?}", e), + FailedToUpdateStandardChannel(e) => write!(f, "Failed to update StandardChannel: {e:?}"), + FailedToUpdateExtendedChannel(e) => write!(f, "Failed to update ExtendedChannel: {e:?}"), FailedToSendSolution => write!(f, "Failed to send solution"), - FailedToSetCustomMiningJob(e) => write!(f, "Failed to set custom mining job: {:?}", e), - FailedToProcessNewTemplateExtendedChannel(e) => write!(f, "Failed to process NewTemplate: {:?}", e), - FailedToProcessNewTemplateStandardChannel(e) => write!(f, "Failed to process NewTemplate: {:?}", e), - FailedToProcessSetNewPrevHashExtendedChannel(e) => write!(f, "Failed to process SetNewPrevHash: {:?}", e), - FailedToProcessSetNewPrevHashStandardChannel(e) => write!(f, "Failed to process SetNewPrevHash: {:?}", e), + FailedToSetCustomMiningJob(e) => write!(f, "Failed to set custom mining job: {e:?}"), + FailedToProcessNewTemplateExtendedChannel(e) => write!(f, "Failed to process NewTemplate: {e:?}"), + FailedToProcessNewTemplateStandardChannel(e) => write!(f, "Failed to process NewTemplate: {e:?}"), + FailedToProcessSetNewPrevHashExtendedChannel(e) => write!(f, "Failed to process SetNewPrevHash: {e:?}"), + FailedToProcessSetNewPrevHashStandardChannel(e) => write!(f, "Failed to process SetNewPrevHash: {e:?}"), + FailedToDeserializeCoinbaseOutputs => write!(f, "Failed to deserialize coinbase outputs"), + FailedToProcessGroupChannelJob(e) => write!(f, "Failed to process group channel job: {e:?}"), + ParserError(v) => write!(f, "Parser error: {v}"), } } } diff --git a/protocols/v2/roles-logic-sv2/src/handlers/common.rs b/protocols/v2/roles-logic-sv2/src/handlers/common.rs index a183ab8e58..f1496853f2 100644 --- a/protocols/v2/roles-logic-sv2/src/handlers/common.rs +++ b/protocols/v2/roles-logic-sv2/src/handlers/common.rs @@ -25,7 +25,9 @@ //! Stratum V2 networks. use super::SendTo_; -use crate::{errors::Error, parsers::CommonMessages, utils::Mutex}; +use crate::{errors::Error, utils::Mutex}; +use parsers_sv2::CommonMessages; + use common_messages_sv2::{ ChannelEndpointChanged, Reconnect, SetupConnection, SetupConnectionError, SetupConnectionSuccess, *, @@ -49,7 +51,10 @@ where message_type: u8, payload: &mut [u8], ) -> Result { - Self::handle_message_common_deserialized(self_, (message_type, payload).try_into()) + Self::handle_message_common_deserialized( + self_, + (message_type, payload).try_into().map_err(Into::into), + ) } /// Takes a message and it calls the appropriate handler function @@ -117,7 +122,10 @@ where message_type: u8, payload: &mut [u8], ) -> Result { - Self::handle_message_common_deserialized(self_, (message_type, payload).try_into()) + Self::handle_message_common_deserialized( + self_, + (message_type, payload).try_into().map_err(Into::into), + ) } /// It takes a message do setup connection message, it calls diff --git a/protocols/v2/roles-logic-sv2/src/handlers/job_declaration.rs b/protocols/v2/roles-logic-sv2/src/handlers/job_declaration.rs index 19fd7ffc59..f6830158ca 100644 --- a/protocols/v2/roles-logic-sv2/src/handlers/job_declaration.rs +++ b/protocols/v2/roles-logic-sv2/src/handlers/job_declaration.rs @@ -28,7 +28,8 @@ //! - Error handling mechanisms to address unexpected messages and ensure safe processing, //! particularly in the context of shared state. -use crate::{parsers::JobDeclaration, utils::Mutex}; +use crate::utils::Mutex; +use parsers_sv2::JobDeclaration; use std::sync::Arc; /// see [`SendTo_`] @@ -58,7 +59,10 @@ where message_type: u8, payload: &mut [u8], ) -> Result { - Self::handle_message_job_declaration_deserialized(self_, (message_type, payload).try_into()) + Self::handle_message_job_declaration_deserialized( + self_, + (message_type, payload).try_into().map_err(Into::into), + ) } /// Routes a deserialized job declaration message to the appropriate handler function. @@ -141,7 +145,10 @@ where message_type: u8, payload: &mut [u8], ) -> Result { - Self::handle_message_job_declaration_deserialized(self_, (message_type, payload).try_into()) + Self::handle_message_job_declaration_deserialized( + self_, + (message_type, payload).try_into().map_err(Into::into), + ) } /// Routes a deserialized job declaration message to the appropriate handler function. diff --git a/protocols/v2/roles-logic-sv2/src/handlers/mining.rs b/protocols/v2/roles-logic-sv2/src/handlers/mining.rs index 1447194711..0e17a541ac 100644 --- a/protocols/v2/roles-logic-sv2/src/handlers/mining.rs +++ b/protocols/v2/roles-logic-sv2/src/handlers/mining.rs @@ -27,7 +27,8 @@ //! - Support for managing mining channels, extranonce prefixes, and share submissions, while //! handling edge cases and ensuring the correctness of the mining process. -use crate::{errors::Error, parsers::Mining}; +use crate::errors::Error; +use codec_sv2::binary_sv2; use core::convert::TryInto; use mining_sv2::{ CloseChannel, NewExtendedMiningJob, NewMiningJob, OpenExtendedMiningChannel, @@ -37,8 +38,7 @@ use mining_sv2::{ SubmitSharesError, SubmitSharesExtended, SubmitSharesStandard, SubmitSharesSuccess, UpdateChannel, UpdateChannelError, }; - -use crate::common_properties::{IsMiningDownstream, IsMiningUpstream}; +use parsers_sv2::Mining; use super::SendTo_; @@ -63,7 +63,7 @@ pub enum SupportedChannelTypes { /// /// This trait defines methods for parsing and routing downstream messages /// related to mining operations. -pub trait ParseMiningMessagesFromDownstream +pub trait ParseMiningMessagesFromDownstream where Self: Sized + D, { @@ -77,11 +77,11 @@ where payload: &mut [u8], ) -> Result, Error> where - Self: IsMiningDownstream + Sized, + Self: Sized, { match Self::handle_message_mining_deserialized( self_mutex, - (message_type, payload).try_into(), + (message_type, payload).try_into().map_err(Into::into), ) { Err(Error::UnexpectedMessage(0)) => Err(Error::UnexpectedMessage(message_type)), result => result, @@ -94,7 +94,7 @@ where message: Result, Error>, ) -> Result, Error> where - Self: IsMiningDownstream + Sized, + Self: Sized, { let (channel_type, is_work_selection_enabled) = self_mutex .safe_lock(|self_| (self_.get_channel_type(), self_.is_work_selection_enabled()))?; @@ -178,6 +178,7 @@ where } _ => Err(Error::UnexpectedMessage(MESSAGE_TYPE_SET_CUSTOM_MINING_JOB)), }, + Ok(Mining::CloseChannel(m)) => self_mutex.safe_lock(|x| x.handle_close_channel(m))?, Ok(_) => Err(Error::UnexpectedMessage(0)), Err(e) => Err(e), } @@ -230,13 +231,16 @@ where /// This method processes a `SetCustomMiningJob` message and applies the custom mining job /// settings. fn handle_set_custom_mining_job(&mut self, m: SetCustomMiningJob) -> Result, Error>; + + /// Handles a request to close a mining channel. + fn handle_close_channel(&mut self, m: CloseChannel) -> Result, Error>; } /// A trait defining the parser for upstream mining messages used by a downstream. /// /// This trait provides the functionality to handle and route various types of mining messages /// from the upstream based on the message type and payload. -pub trait ParseMiningMessagesFromUpstream +pub trait ParseMiningMessagesFromUpstream where Self: Sized + D, { @@ -254,7 +258,7 @@ where ) -> Result, Error> { match Self::handle_message_mining_deserialized( self_mutex, - (message_type, payload).try_into(), + (message_type, payload).try_into().map_err(Into::into), ) { Err(Error::UnexpectedMessage(0)) => Err(Error::UnexpectedMessage(message_type)), result => result, diff --git a/protocols/v2/roles-logic-sv2/src/handlers/template_distribution.rs b/protocols/v2/roles-logic-sv2/src/handlers/template_distribution.rs index 0fa218cb1e..9054a23874 100644 --- a/protocols/v2/roles-logic-sv2/src/handlers/template_distribution.rs +++ b/protocols/v2/roles-logic-sv2/src/handlers/template_distribution.rs @@ -26,7 +26,8 @@ //! especially in the context of shared state. use super::SendTo_; -use crate::{errors::Error, parsers::TemplateDistribution, utils::Mutex}; +use crate::{errors::Error, utils::Mutex}; +use parsers_sv2::TemplateDistribution; use template_distribution_sv2::{ CoinbaseOutputConstraints, NewTemplate, RequestTransactionData, RequestTransactionDataError, RequestTransactionDataSuccess, SetNewPrevHash, SubmitSolution, @@ -57,7 +58,7 @@ where ) -> Result { Self::handle_message_template_distribution_deserialized( self_, - (message_type, payload).try_into(), + (message_type, payload).try_into().map_err(Into::into), ) } @@ -144,7 +145,7 @@ where ) -> Result { Self::handle_message_template_distribution_deserialized( self_, - (message_type, payload).try_into(), + (message_type, payload).try_into().map_err(Into::into), ) } diff --git a/protocols/v2/roles-logic-sv2/src/job_creator.rs b/protocols/v2/roles-logic-sv2/src/job_creator.rs deleted file mode 100644 index 414cb91984..0000000000 --- a/protocols/v2/roles-logic-sv2/src/job_creator.rs +++ /dev/null @@ -1,714 +0,0 @@ -//! # Job Creator -//! -//! This module provides logic to create extended mining jobs given a template from -//! a template provider as well as logic to clean up old templates when new blocks are mined. -use crate::{errors, utils::Id, Error}; -use binary_sv2::B064K; -use mining_sv2::NewExtendedMiningJob; -use nohash_hasher::BuildNoHashHasher; -use std::{collections::HashMap, convert::TryInto}; -use stratum_common::{ - bitcoin, - bitcoin::{ - absolute::LockTime, - blockdata::{ - transaction::{OutPoint, Transaction, TxIn, TxOut, Version}, - witness::Witness, - }, - consensus, - consensus::Decodable, - Amount, - }, -}; -use template_distribution_sv2::{NewTemplate, SetNewPrevHash}; -use tracing::debug; - -#[derive(Debug)] -pub struct JobsCreators { - lasts_new_template: Vec>, - job_to_template_id: HashMap>, - templte_to_job_id: HashMap>, - ids: Id, - last_target: mining_sv2::Target, - last_ntime: Option, - extranonce_len: u8, -} - -/// Transforms the byte array `coinbase_outputs` in a vector of TxOut -/// It assumes the data to be valid data and does not do any kind of check -pub fn tx_outputs_to_costum_scripts(tx_outputs: &[u8]) -> Vec { - let mut txs = vec![]; - let mut cursor = 0; - let mut txouts = &tx_outputs[cursor..]; - while let Ok(out) = TxOut::consensus_decode(&mut txouts) { - let len = match out.script_pubkey.len() { - a @ 0..=252 => 8 + 1 + a, - a @ 253..=10000 => 8 + 3 + a, - _ => break, - }; - cursor += len; - txs.push(out) - } - txs -} - -impl JobsCreators { - /// Constructor - pub fn new(extranonce_len: u8) -> Self { - Self { - lasts_new_template: Vec::new(), - job_to_template_id: HashMap::with_hasher(BuildNoHashHasher::default()), - templte_to_job_id: HashMap::with_hasher(BuildNoHashHasher::default()), - ids: Id::new(), - last_target: mining_sv2::Target::new(0, 0), - last_ntime: None, - extranonce_len, - } - } - - /// Get template id from job - pub fn get_template_id_from_job(&self, job_id: u32) -> Option { - self.job_to_template_id.get(&job_id).map(|x| x - 1) - } - - /// Used to create new jobs when a new template arrives - pub fn on_new_template( - &mut self, - template: &mut NewTemplate, - version_rolling_allowed: bool, - mut pool_coinbase_outputs: Vec, - ) -> Result, Error> { - let server_tx_outputs = template.coinbase_tx_outputs.to_vec(); - let mut outputs = tx_outputs_to_costum_scripts(&server_tx_outputs); - pool_coinbase_outputs.append(&mut outputs); - - // This is to make sure that 0 is never used, so we can use 0 for - // set_new_prev_hashes that do not refer to any future job/template if needed - // Then we will do the inverse (-1) where needed - let template_id = template.template_id + 1; - self.lasts_new_template.push(template.as_static()); - let next_job_id = self.ids.next(); - self.job_to_template_id.insert(next_job_id, template_id); - self.templte_to_job_id.insert(template_id, next_job_id); - new_extended_job( - template, - &mut pool_coinbase_outputs, - next_job_id, - version_rolling_allowed, - self.extranonce_len, - self.last_ntime, - ) - } - - pub(crate) fn reset_new_templates(&mut self, template: Option>) { - match template { - Some(t) => self.lasts_new_template = vec![t], - None => self.lasts_new_template = vec![], - } - } - - /// When we get a new `SetNewPrevHash` we need to clear all the other templates and only - /// keep the one that matches the template_id of the new prev hash. If none match then - /// we clear all the saved templates. - pub fn on_new_prev_hash(&mut self, prev_hash: &SetNewPrevHash<'static>) -> Option { - self.last_target = prev_hash.target.clone().into(); - self.last_ntime = prev_hash.header_timestamp.into(); // set correct ntime - let template: Vec> = self - .lasts_new_template - .clone() - .into_iter() - .filter(|a| a.template_id == prev_hash.template_id) - .collect(); - match template.len() { - 0 => { - self.reset_new_templates(None); - None - } - 1 => { - self.reset_new_templates(Some(template[0].clone())); - - self.templte_to_job_id - .get(&(prev_hash.template_id + 1)) - .copied() - } - // TODO how many templates can we have at max - _ => todo!("{:#?}", template.len()), - } - } - - /// Returns the latest mining target - pub fn last_target(&self) -> mining_sv2::Target { - self.last_target.clone() - } -} - -/// Converts custom job into extended job -pub fn extended_job_from_custom_job( - referenced_job: &mining_sv2::SetCustomMiningJob, - extranonce_len: u8, -) -> Result, Error> { - let mut outputs = - tx_outputs_to_costum_scripts(referenced_job.coinbase_tx_outputs.clone().as_ref()); - let mut template = NewTemplate { - template_id: 0, - future_template: false, - version: referenced_job.version, - coinbase_tx_version: referenced_job.coinbase_tx_version, - coinbase_prefix: referenced_job.coinbase_prefix.clone(), - coinbase_tx_input_sequence: referenced_job.coinbase_tx_input_n_sequence, - coinbase_tx_value_remaining: referenced_job.coinbase_tx_value_remaining, - coinbase_tx_outputs_count: outputs.len() as u32, - coinbase_tx_outputs: referenced_job.coinbase_tx_outputs.clone(), - coinbase_tx_locktime: referenced_job.coinbase_tx_locktime, - merkle_path: referenced_job.merkle_path.clone(), - }; - new_extended_job( - &mut template, - &mut outputs, - 0, - true, - extranonce_len, - Some(referenced_job.min_ntime), - ) -} - -// Returns an extended job given the provided template from the Template Provider and other -// Pool role related fields. -// -// Pool related arguments: -// -// * `coinbase_outputs`: coinbase output transactions specified by the pool. -// * `job_id`: incremented job identifier specified by the pool. -// * `version_rolling_allowed`: boolean specified by the channel. -// * `extranonce_len`: extranonce length specified by the channel. -fn new_extended_job( - new_template: &mut NewTemplate, - coinbase_outputs: &mut [TxOut], - job_id: u32, - version_rolling_allowed: bool, - extranonce_len: u8, - ntime: Option, -) -> Result, Error> { - coinbase_outputs[0].value = match new_template.coinbase_tx_value_remaining.checked_mul(1) { - //check that value_remaining is updated by TP - Some(result) => Amount::from_sat(result), - None => return Err(Error::ValueRemainingNotUpdated), - }; - let tx_version = new_template - .coinbase_tx_version - .try_into() - .map_err(|_| Error::TxVersionTooBig)?; - - let script_sig_prefix = new_template.coinbase_prefix.to_vec(); - let script_sig_prefix_len = script_sig_prefix.len(); - - let coinbase = coinbase( - script_sig_prefix, - tx_version, - new_template.coinbase_tx_locktime, - new_template.coinbase_tx_input_sequence, - coinbase_outputs, - extranonce_len, - )?; - - let min_ntime = binary_sv2::Sv2Option::new(if new_template.future_template { - None - } else { - ntime - }); - - let new_extended_mining_job: NewExtendedMiningJob<'static> = NewExtendedMiningJob { - channel_id: 0, - job_id, - min_ntime, - version: new_template.version, - version_rolling_allowed, - merkle_path: new_template.merkle_path.clone().into_static(), - coinbase_tx_prefix: coinbase_tx_prefix(&coinbase, script_sig_prefix_len)?, - coinbase_tx_suffix: coinbase_tx_suffix(&coinbase, extranonce_len, script_sig_prefix_len)?, - }; - - debug!( - "New extended mining job created: {:?}", - new_extended_mining_job - ); - Ok(new_extended_mining_job) -} - -// Used to extract the coinbase transaction prefix for extended jobs -// so the extranonce search space can be introduced -fn coinbase_tx_prefix( - coinbase: &Transaction, - script_sig_prefix_len: usize, -) -> Result, Error> { - let encoded = consensus::serialize(coinbase); - // If script_prefix_len is not 0 we are not in a test environment and the coinbase will have the - // 0 witness - let segwit_bytes = match script_sig_prefix_len { - 0 => 0, - _ => 2, - }; - let index = 4 // tx version - + segwit_bytes - + 1 // number of inputs TODO can be also 3 - + 32 // prev OutPoint - + 4 // index - + 1 // bytes in script TODO can be also 3 - + script_sig_prefix_len; // script_sig_prefix - let r = encoded[0..index].to_vec(); - r.try_into().map_err(Error::BinarySv2Error) -} - -// Used to extract the coinbase transaction suffix for extended jobs -// so the extranonce search space can be introduced -fn coinbase_tx_suffix( - coinbase: &Transaction, - extranonce_len: u8, - script_sig_prefix_len: usize, -) -> Result, Error> { - let encoded = consensus::serialize(coinbase); - // If script_sig_prefix_len is not 0 we are not in a test environment and the coinbase have the - // 0 witness - let segwit_bytes = match script_sig_prefix_len { - 0 => 0, - _ => 2, - }; - let r = encoded[4 // tx version - + segwit_bytes - + 1 // number of inputs TODO can be also 3 - + 32 // prev OutPoint - + 4 // index - + 1 // bytes in script TODO can be also 3 - + script_sig_prefix_len // script_sig_prefix - + (extranonce_len as usize)..] - .to_vec(); - r.try_into().map_err(Error::BinarySv2Error) -} - -// try to build a Transaction coinbase -fn coinbase( - script_sig_prefix: Vec, - version: i32, - lock_time: u32, - sequence: u32, - coinbase_outputs: &[TxOut], - extranonce_len: u8, -) -> Result { - // If script_sig_prefix_len is not 0 we are not in a test environment and the coinbase have the - // 0 witness - let witness = match script_sig_prefix.len() { - 0 => Witness::from(vec![] as Vec>), - _ => Witness::from(vec![vec![0; 32]]), - }; - let mut script_sig = script_sig_prefix; - script_sig.extend_from_slice(&vec![0; extranonce_len as usize]); - let tx_in = TxIn { - previous_output: OutPoint::null(), - script_sig: script_sig.into(), - sequence: bitcoin::Sequence(sequence), - witness, - }; - Ok(Transaction { - version: Version::non_standard(version), - lock_time: LockTime::from_consensus(lock_time), - input: vec![tx_in], - output: coinbase_outputs.to_vec(), - }) -} - -/// Helper type to strip a segwit data from the coinbase_tx_prefix and coinbase_tx_suffix -/// to ensure miners are hashing with the correct coinbase -pub fn extended_job_to_non_segwit( - job: NewExtendedMiningJob<'static>, - full_extranonce_len: usize, -) -> Result, Error> { - let mut encoded = job.coinbase_tx_prefix.to_vec(); - // just add empty extranonce space so it can be deserialized. The real extranonce - // should be inserted based on the miner's shares - let extranonce = vec![0_u8; full_extranonce_len]; - encoded.extend_from_slice(&extranonce[..]); - encoded.extend_from_slice(job.coinbase_tx_suffix.inner_as_ref()); - let coinbase = consensus::deserialize(&encoded).map_err(|_| Error::InvalidCoinbase)?; - let stripped_tx = StrippedCoinbaseTx::from_coinbase(coinbase, full_extranonce_len)?; - - Ok(NewExtendedMiningJob { - channel_id: job.channel_id, - job_id: job.job_id, - min_ntime: job.min_ntime, - version: job.version, - version_rolling_allowed: job.version_rolling_allowed, - merkle_path: job.merkle_path, - coinbase_tx_prefix: stripped_tx.into_coinbase_tx_prefix()?, - coinbase_tx_suffix: stripped_tx.into_coinbase_tx_suffix()?, - }) -} -// Helper type to strip a segwit data from the coinbase_tx_prefix and coinbase_tx_suffix -// to ensure miners are hashing with the correct coinbase -struct StrippedCoinbaseTx { - version: u32, - inputs: Vec>, - outputs: Vec>, - lock_time: u32, - // helper field - bip141_bytes_len: usize, -} - -impl StrippedCoinbaseTx { - // create - fn from_coinbase(tx: Transaction, full_extranonce_len: usize) -> Result { - let bip141_bytes_len = tx - .input - .last() - .ok_or(Error::BadPayloadSize)? - .script_sig - .len() - - full_extranonce_len; - Ok(Self { - version: tx.version.0 as u32, - inputs: tx - .input - .iter() - .map(|txin| { - let mut ser: Vec = vec![]; - ser.extend_from_slice(txin.previous_output.txid.as_ref()); - ser.extend_from_slice(&txin.previous_output.vout.to_le_bytes()); - ser.push(txin.script_sig.len() as u8); - ser.extend_from_slice(txin.script_sig.as_bytes()); - ser.extend_from_slice(&txin.sequence.0.to_le_bytes()); - ser - }) - .collect(), - outputs: tx.output.iter().map(consensus::serialize).collect(), - lock_time: tx.lock_time.to_consensus_u32(), - bip141_bytes_len, - }) - } - - // The coinbase tx prefix is the LE bytes concatenation of the tx version and all - // of the tx inputs minus the 32 bytes after the script_sig_prefix bytes - // and the last input's sequence (used as the first entry in the coinbase tx suffix). - // The last 32 bytes after the bip34 bytes in the script will be used to allow extranonce - // space for the miner. We remove the bip141 marker and flag since it is only used for - // computing the `wtxid` and the legacy `txid` is what is used for computing the merkle root - // clippy allow because we don't want to consume self - #[allow(clippy::wrong_self_convention)] - fn into_coinbase_tx_prefix(&self) -> Result, errors::Error> { - let mut inputs = self.inputs.clone(); - let last_input = inputs.last_mut().ok_or(Error::BadPayloadSize)?; - let new_last_input_len = - 32 // outpoint - + 4 // vout - + 1 // script length byte -> TODO can be also 3 (based on TODO in `coinbase_tx_prefix()`) - + self.bip141_bytes_len // space for bip34 bytes - ; - last_input.truncate(new_last_input_len); - let mut prefix: Vec = vec![]; - prefix.extend_from_slice(&self.version.to_le_bytes()); - prefix.push(self.inputs.len() as u8); - prefix.extend_from_slice(&inputs.concat()); - prefix.try_into().map_err(Error::BinarySv2Error) - } - - // This coinbase tx suffix is the sequence of the last tx input plus - // the serialized tx outputs and the lock time. Note we do not use the witnesses - // (placed between txouts and lock time) since it is only used for - // computing the `wtxid` and the legacy `txid` is what is used for computing the merkle root - // clippy allow because we don't want to consume self - #[allow(clippy::wrong_self_convention)] - fn into_coinbase_tx_suffix(&self) -> Result, errors::Error> { - let mut suffix: Vec = vec![]; - let last_input = self.inputs.last().ok_or(Error::BadPayloadSize)?; - // only take the last intput's sequence u32 (bytes after the extranonce space) - let last_input_sequence = &last_input[last_input.len() - 4..]; - suffix.extend_from_slice(last_input_sequence); - suffix.push(self.outputs.len() as u8); - suffix.extend_from_slice(&self.outputs.concat()); - suffix.extend_from_slice(&self.lock_time.to_le_bytes()); - suffix.try_into().map_err(Error::BinarySv2Error) - } -} - -// Test -#[cfg(test)] - -pub mod tests { - use super::*; - use crate::utils::merkle_root_from_path; - #[cfg(feature = "prop_test")] - use binary_sv2::u256_from_int; - use quickcheck::{Arbitrary, Gen}; - use std::{cmp, vec}; - - #[cfg(feature = "prop_test")] - use std::borrow::BorrowMut; - - use stratum_common::bitcoin::{ - consensus::Encodable, secp256k1::Secp256k1, Network, PrivateKey, PublicKey, - }; - - pub fn template_from_gen(g: &mut Gen) -> NewTemplate<'static> { - let mut coinbase_prefix_gen = Gen::new(255); - let mut coinbase_prefix: vec::Vec = vec::Vec::new(); - - let max_num_for_script_sig_prefix = 253; - let prefix_len = cmp::min(u8::arbitrary(&mut coinbase_prefix_gen), 6); - coinbase_prefix.push(prefix_len); - coinbase_prefix.resize_with(prefix_len as usize + 2, || { - cmp::min( - u8::arbitrary(&mut coinbase_prefix_gen), - max_num_for_script_sig_prefix, - ) - }); - let coinbase_prefix: binary_sv2::B0255 = coinbase_prefix.try_into().unwrap(); - - let mut coinbase_tx_outputs_gen = Gen::new(32); - let mut coinbase_tx_outputs_inner: vec::Vec = vec::Vec::new(); - coinbase_tx_outputs_inner.resize_with(32, || u8::arbitrary(&mut coinbase_tx_outputs_gen)); - let coinbase_tx_outputs: binary_sv2::B064K = coinbase_tx_outputs_inner.try_into().unwrap(); - - let mut merkle_path_inner_gen = Gen::new(32); - let mut merkle_path_inner: vec::Vec = vec::Vec::new(); - merkle_path_inner.resize_with(32, || u8::arbitrary(&mut merkle_path_inner_gen)); - let merkle_path_inner: binary_sv2::U256 = merkle_path_inner.try_into().unwrap(); - let merkle_path: binary_sv2::Seq0255 = vec![merkle_path_inner].into(); - - NewTemplate { - template_id: u64::arbitrary(g), - future_template: bool::arbitrary(g), - version: u32::arbitrary(g), - coinbase_tx_version: 2, - coinbase_prefix, - coinbase_tx_input_sequence: u32::arbitrary(g), - coinbase_tx_value_remaining: u64::arbitrary(g), - coinbase_tx_outputs_count: 0, - coinbase_tx_outputs, - coinbase_tx_locktime: u32::arbitrary(g), - merkle_path, - } - } - - const PRIVATE_KEY_BTC: [u8; 32] = [34; 32]; - const NETWORK: Network = Network::Testnet; - - #[cfg(feature = "prop_test")] - const BLOCK_REWARD: u64 = 625_000_000_000; - - pub fn new_pub_key() -> PublicKey { - let priv_k = PrivateKey::from_slice(&PRIVATE_KEY_BTC, NETWORK).unwrap(); - let secp = Secp256k1::default(); - - PublicKey::from_private_key(&secp, &priv_k) - } - - #[cfg(feature = "prop_test")] - use stratum_common::bitcoin::ScriptBuf; - - // Test job_id_from_template - #[cfg(feature = "prop_test")] - #[quickcheck_macros::quickcheck] - fn test_job_id_from_template(mut template: NewTemplate<'static>) { - let mut prefix = template.coinbase_prefix.to_vec(); - if prefix.len() > 0 { - let len = u8::min(prefix[0], 6); - prefix[0] = len; - prefix.resize(len as usize + 2, 0); - template.coinbase_prefix = prefix.try_into().unwrap(); - }; - let out = TxOut { - value: Amount::from_sat(BLOCK_REWARD), - script_pubkey: ScriptBuf::new_p2pk(&new_pub_key()), - }; - let mut jobs_creators = JobsCreators::new(32); - - let job = jobs_creators - .on_new_template(template.borrow_mut(), false, vec![out]) - .unwrap(); - - assert_eq!( - jobs_creators.get_template_id_from_job(job.job_id), - Some(template.template_id) - ); - - // Assert returns non if no match - assert_eq!(jobs_creators.get_template_id_from_job(70), None); - } - - // Test reset new template - #[cfg(feature = "prop_test")] - #[quickcheck_macros::quickcheck] - fn test_reset_new_template(mut template: NewTemplate<'static>) { - let out = TxOut { - value: Amount::from_sat(BLOCK_REWARD), - script_pubkey: ScriptBuf::new_p2pk(&new_pub_key()), - }; - let mut jobs_creators = JobsCreators::new(32); - - assert_eq!(jobs_creators.lasts_new_template.len(), 0); - - let _ = jobs_creators.on_new_template(template.borrow_mut(), false, vec![out]); - - assert_eq!(jobs_creators.lasts_new_template.len(), 1); - assert_eq!(jobs_creators.lasts_new_template[0], template); - - //Create a 2nd template - let mut template2 = template_from_gen(&mut Gen::new(255)); - template2.template_id = template.template_id.checked_sub(1).unwrap_or(0); - - // Reset new template - jobs_creators.reset_new_templates(Some(template2.clone())); - - // Should be pointing at new template - assert_eq!(jobs_creators.lasts_new_template.len(), 1); - assert_eq!(jobs_creators.lasts_new_template[0], template2); - - // Reset new template - jobs_creators.reset_new_templates(None); - - // Should be pointing at new template - assert_eq!(jobs_creators.lasts_new_template.len(), 0); - } - - // Test on_new_prev_hash - #[cfg(feature = "prop_test")] - #[quickcheck_macros::quickcheck] - fn test_on_new_prev_hash(mut template: NewTemplate<'static>) { - let out = TxOut { - value: Amount::from_sat(BLOCK_REWARD), - script_pubkey: ScriptBuf::new_p2pk(&new_pub_key()), - }; - let mut jobs_creators = JobsCreators::new(32); - - //Create a template - let _ = jobs_creators.on_new_template(template.borrow_mut(), false, vec![out]); - let test_id = template.template_id; - - // Create a SetNewPrevHash with matching template_id - let prev_hash = SetNewPrevHash { - template_id: test_id, - prev_hash: u256_from_int(45_u32), - header_timestamp: 0, - n_bits: 0, - target: ([0_u8; 32]).try_into().unwrap(), - }; - - jobs_creators.on_new_prev_hash(&prev_hash); - - //Validate that we still have the same template loaded as there were matching templateIds - assert_eq!(jobs_creators.lasts_new_template.len(), 1); - assert_eq!(jobs_creators.lasts_new_template[0], template); - - // Create a SetNewPrevHash with matching template_id - let test_id_2 = test_id.wrapping_add(1); - let prev_hash2 = SetNewPrevHash { - template_id: test_id_2, - prev_hash: u256_from_int(45_u32), - header_timestamp: 0, - n_bits: 0, - target: ([0_u8; 32]).try_into().unwrap(), - }; - - jobs_creators.on_new_prev_hash(&prev_hash2); - - //Validate that templates were cleared as we got a new templateId in setNewPrevHash - assert_eq!(jobs_creators.lasts_new_template.len(), 0); - } - - #[quickcheck_macros::quickcheck] - fn it_parse_valid_tx_outs( - mut hash1: Vec, - mut hash2: Vec, - value1: u64, - value2: u64, - size1: u8, - size2: u8, - ) { - hash1.resize(size1 as usize + 2, 0); - hash2.resize(size2 as usize + 2, 0); - let tx1 = TxOut { - value: Amount::from_sat(value1), - script_pubkey: hash1.into(), - }; - let tx2 = TxOut { - value: Amount::from_sat(value2), - script_pubkey: hash2.into(), - }; - let mut encoded1 = vec![]; - let mut encoded2 = vec![]; - tx1.consensus_encode(&mut encoded1).unwrap(); - tx2.consensus_encode(&mut encoded2).unwrap(); - let mut encoded = vec![]; - encoded.append(&mut encoded1.clone()); - encoded.append(&mut encoded2.clone()); - let outs = tx_outputs_to_costum_scripts(&encoded[..]); - assert!(outs[0] == tx1); - assert!(outs[1] == tx2); - } - - // test that witness stripped tx id matches that of the txid of the coinbase - #[test] - fn stripped_tx_id() { - let encoded: &[u8] = &[ - 2, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 36, 2, 107, 22, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, - 255, 255, 2, 0, 0, 0, 0, 0, 0, 0, 0, 67, 65, 4, 70, 109, 127, 202, 229, 99, 229, 203, - 9, 160, 209, 135, 11, 181, 128, 52, 72, 4, 97, 120, 121, 161, 73, 73, 207, 34, 40, 95, - 27, 174, 63, 39, 103, 40, 23, 108, 60, 100, 49, 248, 238, 218, 69, 56, 220, 55, 200, - 101, 226, 120, 79, 58, 158, 119, 208, 68, 243, 62, 64, 119, 151, 225, 39, 138, 172, 0, - 0, 0, 0, 0, 0, 0, 0, 38, 106, 36, 170, 33, 169, 237, 226, 246, 28, 63, 113, 209, 222, - 253, 63, 169, 153, 223, 163, 105, 83, 117, 92, 105, 6, 137, 121, 153, 98, 180, 139, - 235, 216, 54, 151, 78, 140, 249, 1, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ]; - let coinbase: Transaction = consensus::deserialize(encoded).unwrap(); - let stripped = StrippedCoinbaseTx::from_coinbase(coinbase.clone(), 32).unwrap(); - let prefix = stripped.into_coinbase_tx_prefix().unwrap().to_vec(); - let suffix = stripped.into_coinbase_tx_suffix().unwrap().to_vec(); - let extranonce = &[0_u8; 32]; - let path: &[binary_sv2::U256] = &[]; - let stripped_merkle_root = - merkle_root_from_path(&prefix[..], &suffix[..], extranonce, path).unwrap(); - let txid = coinbase.compute_txid(); - let txid_bytes: &[u8; 32] = txid.as_ref(); - let og_merkle_root = txid_bytes.to_vec(); - assert!( - stripped_merkle_root == og_merkle_root, - "stripped tx hash is not the same as bitcoin crate" - ); - } - #[test] - fn stripped_tx_id_braiins_example() { - let mut encoded = vec![]; - let coinbase_prefix = &[ - 1_u8, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 75, 3, 176, 235, 11, 250, 190, 109, 109, - 50, 247, 22, 140, 225, 176, 1, 231, 78, 225, 50, 226, 181, 165, 55, 145, 137, 154, 46, - 9, 44, 65, 72, 231, 173, 111, 131, 26, 81, 223, 179, 225, 1, 0, 0, 0, 0, 0, 0, 0, - ]; - let coinbase_suffix = &[ - 245_u8, 192, 42, 69, 19, 47, 115, 108, 117, 115, 104, 47, 0, 0, 0, 0, 3, 78, 213, 148, - 39, 0, 0, 0, 0, 25, 118, 169, 20, 124, 21, 78, 209, 220, 89, 96, 158, 61, 38, 171, 178, - 223, 46, 163, 213, 135, 205, 140, 65, 136, 172, 0, 0, 0, 0, 0, 0, 0, 0, 44, 106, 76, - 41, 82, 83, 75, 66, 76, 79, 67, 75, 58, 214, 9, 239, 96, 221, 25, 108, 87, 155, 50, 55, - 47, 91, 115, 172, 168, 0, 12, 86, 195, 26, 241, 10, 22, 190, 151, 254, 24, 0, 78, 106, - 26, 0, 0, 0, 0, 0, 0, 0, 0, 38, 106, 36, 170, 33, 169, 237, 103, 66, 68, 105, 2, 55, - 65, 241, 216, 46, 82, 223, 150, 0, 97, 103, 2, 82, 186, 233, 145, 90, 210, 231, 35, - 100, 107, 52, 171, 233, 50, 200, 0, 0, 0, 0, - ]; - let extranonce = [0_u8; 15]; // braiins pool requires 15 bytes for extranonce - encoded.extend_from_slice(coinbase_prefix); - let mut encoded_clone = encoded.clone(); - encoded_clone.extend_from_slice(&extranonce); - encoded_clone.extend_from_slice(coinbase_suffix); - // let mut i = 1; - // while let Err(_) = Transaction::deserialize(&encoded_clone) { - // encoded_clone = encoded.clone(); - // extranonce.push(0); - // encoded_clone.extend_from_slice(&extranonce[..]); - // encoded_clone.extend_from_slice(coinbase_suffix); - // i+=1; - // } - // println!("SIZE: {:?}", i); - let _tx: Transaction = consensus::deserialize(&encoded_clone).unwrap(); - } -} diff --git a/protocols/v2/roles-logic-sv2/src/job_dispatcher.rs b/protocols/v2/roles-logic-sv2/src/job_dispatcher.rs deleted file mode 100644 index e95e08450a..0000000000 --- a/protocols/v2/roles-logic-sv2/src/job_dispatcher.rs +++ /dev/null @@ -1,610 +0,0 @@ -//! Job Dispatcher -//! -//! This module contains relevant logic to maintain group channels in proxy roles such as: -//! - converting extended jobs to standard jobs -//! - handling updates to jobs when new templates and prev hashes arrive, as well as cleaning up old -//! jobs -//! - determining if submitted shares correlate to valid jobs - -use crate::{ - common_properties::StandardChannel, - utils::{merkle_root_from_path, Id, Mutex}, - Error, -}; -use mining_sv2::{ - NewExtendedMiningJob, NewMiningJob, SetNewPrevHash, SubmitSharesError, SubmitSharesStandard, - Target, -}; -use nohash_hasher::BuildNoHashHasher; -use std::{collections::HashMap, convert::TryInto, sync::Arc}; - -use stratum_common::bitcoin::hashes::{sha256d, Hash, HashEngine}; - -/// Used to convert an extended mining job to a standard mining job. The `extranonce` field must -/// be exactly 32 bytes. -pub fn extended_to_standard_job_for_group_channel<'a>( - extended: &NewExtendedMiningJob, - extranonce: &[u8], - channel_id: u32, - job_id: u32, -) -> Option> { - let merkle_root = merkle_root_from_path( - extended.coinbase_tx_prefix.inner_as_ref(), - extended.coinbase_tx_suffix.inner_as_ref(), - extranonce, - &extended.merkle_path.inner_as_ref(), - ); - - Some(NewMiningJob { - channel_id, - job_id, - min_ntime: extended.min_ntime.clone().into_static(), - version: extended.version, - merkle_root: merkle_root?.try_into().ok()?, - }) -} - -// helper struct to easily calculate block hashes from headers -#[allow(dead_code)] -struct Header<'a> { - version: u32, - prev_hash: &'a [u8], - merkle_root: &'a [u8], - timestamp: u32, - nbits: u32, - nonce: u32, -} - -impl Header<'_> { - // calculates the sha256 blockhash of the header - #[allow(dead_code)] - pub fn hash(&self) -> Target { - let mut engine = sha256d::Hash::engine(); - engine.input(&self.version.to_le_bytes()); - engine.input(self.prev_hash); - engine.input(self.merkle_root); - engine.input(&self.timestamp.to_be_bytes()); - engine.input(&self.nbits.to_be_bytes()); - engine.input(&self.nonce.to_be_bytes()); - let hashed: [u8; 32] = *sha256d::Hash::from_engine(engine).as_ref(); - hashed.into() - } -} - -// helper struct to identify Standard Jobs being managed for downstream -#[derive(Debug)] -struct DownstreamJob { - #[allow(dead_code)] - merkle_root: Vec, - extended_job_id: u32, -} - -/// Used by proxies to keep track of standard jobs in the group channel -/// created with the sv2 server -#[derive(Debug)] -pub struct GroupChannelJobDispatcher { - //channels: Vec, - #[allow(dead_code)] - target: Target, - prev_hash: Vec, - // extended_job_id -> standard_job_id -> standard_job - future_jobs: - HashMap>, BuildNoHashHasher>, - // standard_job_id -> standard_job - jobs: HashMap>, - ids: Arc>, - // extended_id -> channel_id -> standard_id - extended_id_to_job_id: - HashMap>, BuildNoHashHasher>, - nbits: u32, -} - -/// Used to signal if submitted shares correlate to valid jobs -pub enum SendSharesResponse { - /// ValidAndMeetUpstreamTarget((SubmitSharesStandard,SubmitSharesSuccess)), - Valid(SubmitSharesStandard), - Invalid(SubmitSharesError<'static>), -} - -impl GroupChannelJobDispatcher { - /// constructor - pub fn new(ids: Arc>) -> Self { - Self { - target: [0_u8; 32].into(), - prev_hash: Vec::new(), - future_jobs: HashMap::with_hasher(BuildNoHashHasher::default()), - jobs: HashMap::with_hasher(BuildNoHashHasher::default()), - ids, - nbits: 0, - extended_id_to_job_id: HashMap::with_hasher(BuildNoHashHasher::default()), - } - } - - /// When a downstream opens a connection with a proxy, the proxy uses this function to create a - /// new mining job from the last valid new extended mining job. - /// - /// When a proxy receives a new extended mining job from upstream it uses this function to - /// create the corresponding new mining job for each connected downstream. - #[allow(clippy::option_map_unit_fn)] - pub fn on_new_extended_mining_job( - &mut self, - extended: &NewExtendedMiningJob, - channel: &StandardChannel, - // should be changed to return a Result> - ) -> Option> { - if extended.is_future() { - self.future_jobs - .entry(extended.job_id) - .or_insert_with(|| HashMap::with_hasher(BuildNoHashHasher::default())); - self.extended_id_to_job_id - .entry(extended.job_id) - .or_insert_with(|| HashMap::with_hasher(BuildNoHashHasher::default())); - } - - // Is fine to unwrap a safe_lock result - let standard_job_id = self.ids.safe_lock(|ids| ids.next()).unwrap(); - - let extranonce: Vec = channel.extranonce.clone().into(); - let new_mining_job_message = extended_to_standard_job_for_group_channel( - extended, - &extranonce, - channel.channel_id, - standard_job_id, - )?; - let job = DownstreamJob { - merkle_root: new_mining_job_message.merkle_root.to_vec(), - extended_job_id: extended.job_id, - }; - if extended.is_future() { - self.future_jobs - .get_mut(&extended.job_id) - .map(|future_jobs| { - future_jobs.insert(standard_job_id, job); - }); - - let channel_id_to_standard_id = self - .extended_id_to_job_id - .get_mut(&extended.job_id) - // The key is always in the map cause we insert it above if not present - .unwrap(); - channel_id_to_standard_id.insert(channel.channel_id, standard_job_id); - } else { - self.jobs.insert(new_mining_job_message.job_id, job); - }; - Some(new_mining_job_message) - } - - /// Called when a SetNewPrevHash message is received. - /// This function will move all future jobs to current jobs, clear old jobs, - /// and update `self` to reference the latest prev_hash and nbits - /// associated with the latest job. - pub fn on_new_prev_hash( - &mut self, - message: &SetNewPrevHash, - ) -> Result>, Error> { - let jobs = self - .future_jobs - .get_mut(&message.job_id) - .ok_or(Error::PrevHashRequireNonExistentJobId(message.job_id))?; - std::mem::swap(&mut self.jobs, jobs); - self.prev_hash = message.prev_hash.to_vec(); - self.nbits = message.nbits; - self.future_jobs.clear(); - match self.extended_id_to_job_id.remove(&message.job_id) { - Some(map) => { - self.extended_id_to_job_id.clear(); - Ok(map) - } - None => { - self.extended_id_to_job_id.clear(); - Ok(HashMap::with_hasher(BuildNoHashHasher::default())) - } - } - } - - /// takes shares submitted by a group channel miner and determines if the shares correspond to a - /// valid job. - pub fn on_submit_shares(&self, shares: SubmitSharesStandard) -> SendSharesResponse { - let id = shares.job_id; - if let Some(job) = self.jobs.get(&id) { - let success = SubmitSharesStandard { - channel_id: shares.channel_id, - sequence_number: shares.sequence_number, - job_id: job.extended_job_id, - nonce: shares.nonce, - ntime: shares.ntime, - version: shares.version, - }; - SendSharesResponse::Valid(success) - } else { - let error = SubmitSharesError { - channel_id: shares.channel_id, - sequence_number: shares.sequence_number, - // Below unwrap never panic because an empty string will always fit - // in a `Inner` type - error_code: "".to_string().into_bytes().try_into().unwrap(), - }; - SendSharesResponse::Invalid(error) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - errors::Error, - job_creator::{ - tests::{new_pub_key, template_from_gen}, - JobsCreators, - }, - }; - use binary_sv2::{u256_from_int, U256}; - use mining_sv2::Extranonce; - use quickcheck::{Arbitrary, Gen}; - use std::convert::TryFrom; - - use stratum_common::bitcoin::{Amount, ScriptBuf, TxOut}; - - const BLOCK_REWARD: u64 = 625_000_000_000; - - #[test] - fn test_block_hash() { - let le_version = "0x32950000".strip_prefix("0x").unwrap(); - let be_prev_hash = "0x00000000000000000004e962c1a0fc6a201d937bf08ffe4b1221e956615c7cd9"; - let be_merkle_root = "0x897dff6755a7c255455f1b2a2c8ad44ad1b6c23ef00fbf501d0dde7e42cd8c71"; - let le_timestamp = "0x637B9A4C".strip_prefix("0x").unwrap(); - let le_nbits = "0x17079e15".strip_prefix("0x").unwrap(); - let le_nonce = "0x102aa10".strip_prefix("0x").unwrap(); - - let le_version = u32::from_str_radix(le_version, 16).expect("Failed converting hex to u32"); - let mut be_prev_hash = - utils::decode_hex(be_prev_hash).expect("Failed converting hex to bytes"); - let mut be_merkle_root = - utils::decode_hex(be_merkle_root).expect("Failed converting hex to bytes"); - let le_timestamp: u32 = - u32::from_str_radix(le_timestamp, 16).expect("Failed converting hex to u32"); - let le_nbits = u32::from_str_radix(le_nbits, 16).expect("Failed converting hex to u32"); - let le_nonce = u32::from_str_radix(le_nonce, 16).expect("Failed converting hex to u32"); - be_prev_hash.reverse(); - be_merkle_root.reverse(); - let le_prev_hash = be_prev_hash.as_slice(); - let le_merkle_root = be_merkle_root.as_slice(); - - let block_header: Header = Header { - version: le_version, - prev_hash: le_prev_hash, - merkle_root: le_merkle_root, - timestamp: le_timestamp.to_be(), - nbits: le_nbits.to_be(), - nonce: le_nonce.to_be(), - }; - - let target = U256::from(block_header.hash()); - let mut actual_block_hash = - utils::decode_hex("00000000000000000000199349a95526c4f83959f0ef06697048a297f25e7fac") - .expect("Failed converting hex to bytes"); - actual_block_hash.reverse(); - assert_eq!( - target.to_vec(), - actual_block_hash, - "Computed block hash does not equal the actaul block hash" - ); - } - - #[test] - fn test_group_channel_job_dispatcher() { - let out = TxOut { - value: Amount::from_sat(BLOCK_REWARD), - script_pubkey: ScriptBuf::new_p2pk(&new_pub_key()), - }; - let mut jobs_creators = JobsCreators::new(32); - let group_channel_id = 1; - //Create a template - let mut template = template_from_gen(&mut Gen::new(255)); - template.template_id %= u64::MAX; - template.future_template = true; - let extended_mining_job = jobs_creators - .on_new_template(&mut template, false, vec![out]) - .expect("Failed to create new job"); - - // create GroupChannelJobDispatcher - let ids = Arc::new(Mutex::new(Id::new())); - let mut group_channel_dispatcher = GroupChannelJobDispatcher::new(ids); - // create standard channel - let target = Target::from(U256::try_from(utils::extranonce_gen()).unwrap()); - let standard_channel_id = 2; - let extranonce = Extranonce::try_from(utils::extranonce_gen()) - .expect("Failed to convert bytes to extranonce"); - let standard_channel = StandardChannel { - channel_id: standard_channel_id, - group_id: group_channel_id, - target, - extranonce: extranonce.clone(), - }; - // call target function (on_new_extended_mining_job) - let new_mining_job = group_channel_dispatcher - .on_new_extended_mining_job(&extended_mining_job, &standard_channel) - .unwrap(); - - // on_new_extended_mining_job assertions - let (future_job_id, test_merkle_root) = assert_on_new_extended_mining_job( - &group_channel_dispatcher, - &new_mining_job, - &extended_mining_job, - extranonce.clone(), - standard_channel_id, - ); - // on_new_prev_hash assertions - if extended_mining_job.is_future() { - assert_on_new_prev_hash( - &mut group_channel_dispatcher, - standard_channel_id, - future_job_id, - test_merkle_root, - ) - } - assert_on_submit_shares( - &group_channel_dispatcher, - standard_channel_id, - future_job_id, - ); - } - - fn assert_on_new_extended_mining_job( - group_channel_job_dispatcher: &GroupChannelJobDispatcher, - new_mining_job: &NewMiningJob, - extended_mining_job: &NewExtendedMiningJob, - extranonce: Extranonce, - standard_channel_id: u32, - ) -> (u32, Vec) { - // compute test merkle path - let new_root = merkle_root_from_path( - extended_mining_job.coinbase_tx_prefix.inner_as_ref(), - extended_mining_job.coinbase_tx_suffix.inner_as_ref(), - extranonce.to_vec().as_slice(), - &extended_mining_job.merkle_path.inner_as_ref(), - ) - .unwrap(); - // Assertions - assert_eq!( - new_mining_job.channel_id, standard_channel_id, - "channel_id did not convert correctly" - ); - assert_eq!( - new_mining_job.job_id, extended_mining_job.job_id, - "job_id did not convert correctly" - ); - assert_eq!( - new_mining_job.version, extended_mining_job.version, - "version did not convert correctly" - ); - assert_eq!( - new_mining_job.min_ntime, extended_mining_job.min_ntime, - "future_job did not convert correctly" - ); - assert_eq!( - new_mining_job.merkle_root.to_vec(), - new_root, - "merkle_root did not convert correctly" - ); - let mut future_job_id: u32 = 0; - if new_mining_job.is_future() { - // assert job_id counter - let job_ids = group_channel_job_dispatcher - .extended_id_to_job_id - .get(&extended_mining_job.job_id) - .unwrap(); - let standard_job_id = job_ids.get(&standard_channel_id).unwrap(); - let standard_job_id_counter: u32 = group_channel_job_dispatcher - .ids - .safe_lock(|id| id.next()) - .unwrap(); - let prev_value = standard_job_id_counter - 1; - assert_eq!( - standard_job_id, &prev_value, - "Job Id counter does not match" - ); - // assert job was stored - let future_jobs = group_channel_job_dispatcher - .future_jobs - .get(&extended_mining_job.job_id) - .unwrap(); - let job = future_jobs.get(&prev_value).unwrap(); - assert_eq!( - job.extended_job_id, extended_mining_job.job_id, - "job_id not stored correctly in future_jobs" - ); - assert_eq!( - job.merkle_root, - new_mining_job.merkle_root.to_vec(), - "job merkle root not stored correctly in future jobs" - ); - future_job_id = prev_value; - } - (future_job_id, new_root) - } - - fn assert_on_new_prev_hash( - group_channel_job_dispatcher: &mut GroupChannelJobDispatcher, - standard_channel_id: u32, - future_job_id: u32, - test_merkle_root: Vec, - ) { - let mut prev_hash = Vec::new(); - prev_hash.resize_with(32, || u8::arbitrary(&mut Gen::new(1))); - let min_ntime: u32 = 1; - let nbits: u32 = 1; - let new_message = SetNewPrevHash { - channel_id: standard_channel_id, - job_id: future_job_id, - prev_hash: U256::try_from(prev_hash).unwrap(), - min_ntime, - nbits, - }; - - group_channel_job_dispatcher - .on_new_prev_hash(&new_message) - .expect("on_new_prev_hash failed to execute"); - - // assert future job was moved to current jobs - let new_current_job = group_channel_job_dispatcher - .jobs - .get(&future_job_id) - .unwrap(); - assert_eq!( - new_current_job.merkle_root, test_merkle_root, - "Future job not moved to current job correctly (merkle root)" - ); - assert_eq!( - new_current_job.extended_job_id, future_job_id, - "Future job not moved to current job correctly (job_id)" - ); - assert_eq!( - group_channel_job_dispatcher.nbits, new_message.nbits, - "nbits not updated for SetNewPrevHash" - ); - assert_eq!( - group_channel_job_dispatcher.prev_hash, - new_message.prev_hash.to_vec(), - "prev_hash not updated for SetNewPrevHash" - ); - assert!( - group_channel_job_dispatcher.future_jobs.is_empty(), - "Future jobs did not get cleared" - ) - } - fn assert_on_submit_shares( - group_channel_job_dispatcher: &GroupChannelJobDispatcher, - standard_channel_id: u32, - job_id: u32, - ) { - let shares = SubmitSharesStandard { - // Channel identification. - channel_id: standard_channel_id, - // Unique sequential identifier of the submit within the channel. - sequence_number: 0, - // Identifier of the job as provided by *NewMiningJob* or - // *NewExtendedMiningJob* message. - job_id, - // Nonce leading to the hash being submitted. - nonce: 1, - // The nTime field in the block header. This MUST be greater than or equal - // to the header_timestamp field in the latest SetNewPrevHash message - // and lower than or equal to that value plus the number of seconds since - // the receipt of that message. - ntime: 1, - // Full nVersion field. - version: 1, - }; - let mut faulty_shares = shares.clone(); - faulty_shares.job_id += 1; - - for (index, shares) in [shares, faulty_shares].iter().enumerate() { - match group_channel_job_dispatcher.on_submit_shares(shares.clone()) { - SendSharesResponse::Valid(resp) => { - assert_eq!( - index, 0, - "Only the first item in iterator should be a valid response" - ); - assert_eq!(resp.channel_id, standard_channel_id); - assert_eq!(resp.job_id, job_id); - assert_eq!(resp.sequence_number, shares.sequence_number); - assert_eq!(resp.nonce, shares.nonce); - assert_eq!(resp.ntime, shares.ntime); - assert_eq!(resp.version, shares.version); - } - SendSharesResponse::Invalid(err) => { - assert_eq!( - index, 1, - "Only the second item in iterator should be an invalid response" - ); - assert_eq!(err.channel_id, standard_channel_id); - assert_eq!(err.sequence_number, shares.sequence_number); - assert_eq!( - err.error_code, - "".to_string().into_bytes().try_into().unwrap() - ); - } - }; - } - } - - #[test] - fn builds_group_channel_job_dispatcher() { - let expect = GroupChannelJobDispatcher { - target: [0_u8; 32].into(), - prev_hash: Vec::new(), - future_jobs: HashMap::with_hasher(BuildNoHashHasher::default()), - jobs: HashMap::with_hasher(BuildNoHashHasher::default()), - ids: Arc::new(Mutex::new(Id::new())), - nbits: 0, - extended_id_to_job_id: HashMap::with_hasher(BuildNoHashHasher::default()), - }; - - let ids = Arc::new(Mutex::new(Id::new())); - let actual = GroupChannelJobDispatcher::new(ids); - - assert_eq!(expect.target, actual.target); - assert_eq!(expect.prev_hash, actual.prev_hash); - assert_eq!(expect.nbits, actual.nbits); - assert!(actual.future_jobs.is_empty()); - assert!(actual.jobs.is_empty()); - // check actual.ids, but idk how to properly test arc - // assert_eq!(expect.ids, actual.ids); - } - - #[ignore] - #[test] - fn updates_group_channel_job_dispatcher_on_new_prev_hash() -> Result<(), Error> { - let message = SetNewPrevHash { - channel_id: 0, - job_id: 0, - prev_hash: u256_from_int(45_u32), - min_ntime: 0, - nbits: 0, - }; - let ids = Arc::new(Mutex::new(Id::new())); - let mut dispatcher = GroupChannelJobDispatcher::new(ids); - - // fails on self.future_jobs unwrap in the first line of the on_new_prev_hash fn - let _actual = dispatcher.on_new_prev_hash(&message); - // let actual_prev_hash: U256<'static> = u256_from_int(tt); - let expect_prev_hash: Vec = dispatcher.prev_hash.to_vec(); - // assert_eq!(expect_prev_hash, dispatcher.prev_hash); - assert_eq!(expect_prev_hash, dispatcher.prev_hash); - - Ok(()) - } - - pub mod utils { - use super::*; - use std::fmt::Write; - - pub fn extranonce_gen() -> Vec { - let mut u8_gen = Gen::new(1); - let mut extranonce: Vec = Vec::new(); - extranonce.resize_with(32, || u8::arbitrary(&mut u8_gen)); - extranonce - } - - pub fn decode_hex(s: &str) -> Result, core::num::ParseIntError> { - let s = match s.strip_prefix("0x") { - Some(s) => s, - None => s, - }; - (0..s.len()) - .step_by(2) - .map(|i| u8::from_str_radix(&s[i..i + 2], 16)) - .collect() - } - - pub fn _encode_hex(bytes: &[u8]) -> String { - let mut s = String::with_capacity(bytes.len() * 2); - for &b in bytes { - write!(&mut s, "{:02x}", b).unwrap(); - } - s - } - } -} diff --git a/protocols/v2/roles-logic-sv2/src/lib.rs b/protocols/v2/roles-logic-sv2/src/lib.rs index c94be788ce..05a3d68639 100644 --- a/protocols/v2/roles-logic-sv2/src/lib.rs +++ b/protocols/v2/roles-logic-sv2/src/lib.rs @@ -17,19 +17,16 @@ //! This crate can be built with the following features: //! //! - `prop_test`: Enables support for property testing in [`template_distribution_sv2`] crate. -pub mod channel_logic; -pub mod channels; -pub mod common_properties; pub mod errors; pub mod handlers; -pub mod job_creator; -pub mod job_dispatcher; -pub mod parsers; pub mod utils; -pub mod vardiff; +pub use bitcoin; +pub use channels_sv2; +pub use codec_sv2; pub use common_messages_sv2; pub use errors::Error; +pub use handlers_sv2; pub use job_declaration_sv2; pub use mining_sv2; +pub use parsers_sv2; pub use template_distribution_sv2; -pub use vardiff::{classic::VardiffState, Vardiff}; diff --git a/protocols/v2/roles-logic-sv2/src/utils.rs b/protocols/v2/roles-logic-sv2/src/utils.rs index 89ad188972..8b23742da9 100644 --- a/protocols/v2/roles-logic-sv2/src/utils.rs +++ b/protocols/v2/roles-logic-sv2/src/utils.rs @@ -5,61 +5,7 @@ //! management, mutex management, difficulty target calculations, merkle root calculations, and //! more. -use binary_sv2::U256; -use bitcoin::Block; -use job_declaration_sv2::{DeclareMiningJob, PushSolution}; -use mining_sv2::Target; -use primitive_types::U256 as U256Primitive; -use std::{ - cmp::max, - convert::TryInto, - fmt::Write, - ops::Div, - sync::{Mutex as Mutex_, MutexGuard, PoisonError}, -}; -use stratum_common::{ - bitcoin, - bitcoin::{ - blockdata::block::{Header, Version}, - consensus, - consensus::Decodable, - hash_types::{BlockHash, TxMerkleNode}, - hashes::{sha256d::Hash as DHash, Hash}, - transaction::TxOut, - CompactTarget, Transaction, - }, -}; -use tracing::error; - -use crate::errors::Error; - -/// Generator of unique IDs for channels and groups. -/// -/// It keeps an internal counter, which is incremented every time a new unique id is requested. -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct Id { - state: u32, -} - -impl Id { - /// Creates a new [`Id`] instance initialized to `0`. - pub fn new() -> Self { - Self { state: 0 } - } - - /// Increments then returns the internal state on a new ID. - #[allow(clippy::should_implement_trait)] - pub fn next(&mut self) -> u32 { - self.state += 1; - self.state - } -} - -impl Default for Id { - fn default() -> Self { - Self::new() - } -} +use std::sync::{Mutex as Mutex_, MutexGuard, PoisonError}; /// Custom synchronization primitive for managing shared mutable state. /// @@ -156,995 +102,9 @@ impl Mutex { } } -/// Computes the Merkle root from coinbase transaction components and a path of transaction hashes. -/// -/// Validates and deserializes a coinbase transaction before building the 32-byte Merkle root. -/// Returns [`None`] is the arguments are invalid. -/// -/// ## Components -/// * `coinbase_tx_prefix`: First part of the coinbase transaction (the part before the extranonce). -/// Should be converted from [`binary_sv2::B064K`]. -/// * `coinbase_tx_suffix`: Coinbase transaction suffix (the part after the extranonce). Should be -/// converted from [`binary_sv2::B064K`]. -/// * `extranonce`: Extra nonce space. Should be converted from [`binary_sv2::B032`] and padded with -/// zeros if not `32` bytes long. -/// * `path`: List of transaction hashes. Should be converted from [`binary_sv2::U256`]. -pub fn merkle_root_from_path>( - coinbase_tx_prefix: &[u8], - coinbase_tx_suffix: &[u8], - extranonce: &[u8], - path: &[T], -) -> Option> { - let mut coinbase = - Vec::with_capacity(coinbase_tx_prefix.len() + coinbase_tx_suffix.len() + extranonce.len()); - coinbase.extend_from_slice(coinbase_tx_prefix); - coinbase.extend_from_slice(extranonce); - coinbase.extend_from_slice(coinbase_tx_suffix); - let coinbase: Transaction = match consensus::deserialize(&coinbase[..]) { - Ok(trans) => trans, - Err(e) => { - error!("ERROR: {}", e); - dbg!(e); - return None; - } - }; - - let coinbase_id: [u8; 32] = *coinbase.compute_txid().as_ref(); - - Some(merkle_root_from_path_(coinbase_id, path).to_vec()) -} - -/// Computes the Merkle root from a validated coinbase transaction and a path of transaction -/// hashes. -/// -/// If the `path` is empty, the coinbase transaction hash (`coinbase_id`) is returned as the root. -/// -/// ## Components -/// * `coinbase_id`: Coinbase transaction hash. -/// * `path`: List of transaction hashes. Should be converted from [`binary_sv2::U256`]. -pub fn merkle_root_from_path_>(coinbase_id: [u8; 32], path: &[T]) -> [u8; 32] { - match path.len() { - 0 => coinbase_id, - _ => reduce_path(coinbase_id, path), - } -} - -// Helper function to format bytes as hex string -// useful for visualizing targets -pub fn bytes_to_hex(bytes: &[u8]) -> String { - let mut s = String::with_capacity(bytes.len() * 2); - for &b in bytes { - write!(&mut s, "{:02x}", b) - .expect("Writing hex bytes to pre-allocated string should never fail"); - } - s -} - -// Computes the Merkle root by iteratively combining the coinbase transaction hash with each -// transaction hash in the `path`. -// -// Handles the core logic of combining hashes using the Bitcoin double-SHA256 hashing algorithm. -fn reduce_path>(coinbase_id: [u8; 32], path: &[T]) -> [u8; 32] { - let mut root = coinbase_id; - for node in path { - let to_hash = [&root[..], node.as_ref()].concat(); - let hash = DHash::hash(&to_hash); - root = *hash.as_ref(); - } - root -} - -/// Deserializes a list of outputs from a serialized format. -pub fn deserialize_outputs(serialized_outputs: Vec) -> Vec { - let mut deserialized_outputs: Vec = vec![]; - - // The serialized outputs are in Bitcoin consensus format - // We need to parse them one by one, keeping track of cursor position - let mut cursor = 0; - let mut txouts = &serialized_outputs[cursor..]; - - // Iteratively decode each TxOut until we can't decode any more - while let Ok(out) = TxOut::consensus_decode(&mut txouts) { - // Calculate the size of this TxOut based on its script_pubkey length - // 8 bytes for value + variable bytes for script_pubkey length - // For small scripts (0-252 bytes): 1 byte length prefix - // For medium scripts (253-1000000 bytes): 3 byte length prefix (1 marker + 2 byte - // length) - let len = match out.script_pubkey.len() { - a @ 0..=252 => 8 + 1 + a, // 8 (value) + 1 (compact size) + script_len - a @ 253..=1000000 => 8 + 3 + a, // 8 (value) + 3 (compact size) + script_len - _ => break, // Unreasonably large script, likely an error - }; - - // Move the cursor forward by the size of this TxOut - cursor += len; - deserialized_outputs.push(out); - } - - deserialized_outputs -} - -/// A list of potential errors during conversion between hashrate and target -#[derive(Debug)] -pub enum InputError { - NegativeInput, - DivisionByZero, - ArithmeticOverflow, -} - -/// Calculates the mining target threshold for a mining device based on its hashrate (H/s) and -/// desired share frequency (shares/min). -/// -/// Determines the maximum hash value (target), in big endian, that a mining device can produce to -/// find a valid share. The target is derived from the miner's hashrate and the expected number of -/// shares per minute, aligning the miner's workload with the upstream's (e.g. pool's) share -/// frequency requirements. -/// -/// Typically used during connection setup to assign a starting target based on the mining device's -/// reported hashrate and to recalculate during runtime when a mining device's hashrate changes, -/// ensuring they submit shares at the desired rate. -/// -/// ## Formula -/// ```text -/// t = (2^256 - sh) / (sh + 1) -/// ``` -/// -/// Where: -/// - `h`: Mining device hashrate (H/s). -/// - `s`: Shares per second `60 / shares/min` (s). -/// - `sh`: `h * s`, the mining device's work over `s` seconds. -/// -/// According to \[1] and \[2], it is possible to model the probability of finding a block with -/// a random variable X whose distribution is negative hypergeometric \[3]. Such a variable is -/// characterized as follows: -/// -/// Say that there are `n` (`2^256`) elements (possible hash values), of which `t` (values <= -/// target) are defined as success and the remaining as failures. The variable `X` has co-domain -/// the positive integers, and `X=k` is the event where element are drawn one after the other, -/// without replacement, and only the `k`th element is successful. The expected value of this -/// variable is `(n-t)/(t+1)`. So, on average, a miner has to perform `(2^256-t)/(t+1)` hashes -/// before finding hash whose value is below the target `t`. -/// -/// If the pool wants, on average, a share every `s` seconds, then, on average, the miner has to -/// perform `h*s` hashes before finding one that is smaller than the target, where `h` is the -/// miner's hashrate. Therefore, `s*h= (2^256-t)/(t+1)`. If we consider `h` the global Bitcoin's -/// hashrate, `s = 600` seconds and `t` the Bitcoin global target, then, for all the blocks we -/// tried, the two members of the equations have the same order of magnitude and, most of the -/// cases, they coincide with the first two digits. -/// -/// We take this as evidence of the correctness of our calculations. Thus, if the pool wants on -/// average a share every `s` seconds from a miner with hashrate `h`, then the target `t` for the -/// miner is `t = (2^256-sh)/(sh+1)`. -/// -/// \[1] [https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3399742](https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3399742) -/// -/// \[2] [https://www.zora.uzh.ch/id/eprint/173483/1/SSRN-id3399742-2.pdf](https://www.zora.uzh.ch/id/eprint/173483/1/SSRN-id3399742-2.pdf) -/// -/// \[3] [https://en.wikipedia.org/wiki/Negative_hypergeometric_distribution](https://en.wikipedia.org/wiki/Negative_hypergeometric_distribution) -pub fn hash_rate_to_target( - hashrate: f64, - share_per_min: f64, -) -> Result, crate::Error> { - // checks that we are not dividing by zero - if share_per_min == 0.0 { - return Err(Error::TargetError(InputError::DivisionByZero)); - } - if share_per_min.is_sign_negative() { - return Err(Error::TargetError(InputError::NegativeInput)); - }; - if hashrate.is_sign_negative() { - return Err(Error::TargetError(InputError::NegativeInput)); - }; - - // if we want 5 shares per minute, this means that s=60/5=12 seconds interval between shares - // this quantity will be at the numerator, so we multiply the result by 100 again later - let shares_occurrency_frequence = 60_f64 / share_per_min; - - let h_times_s = hashrate * shares_occurrency_frequence; - let h_times_s = h_times_s as u128; - - // We calculate the denominator: h*s+1 - // the denominator is h*s+1, where h*s is an u128, so always positive. - // this means that the denominator can never be zero - // we add 100 in place of 1 because h*s is actually h*s*100, we in order to simplify later we - // must calculate (h*s+1)*100 - let h_times_s_plus_one = max(h_times_s, h_times_s + 1); - - let h_times_s_plus_one = from_u128_to_u256(h_times_s_plus_one); - let denominator = h_times_s_plus_one; - - // We calculate the numerator: 2^256-sh - let two_to_256_minus_one = [255_u8; 32]; - let two_to_256_minus_one = U256Primitive::from_big_endian(two_to_256_minus_one.as_ref()); - - let mut h_times_s_array = [0u8; 32]; - h_times_s_array[16..].copy_from_slice(&h_times_s.to_be_bytes()); - let numerator = two_to_256_minus_one - U256Primitive::from_big_endian(h_times_s_array.as_ref()); - - let mut target = numerator.div(denominator).to_big_endian(); - target.reverse(); - Ok(U256::<'static>::from(target)) -} - -/// Calculates the hashrate (H/s) required to produce a specific number of shares per minute for a -/// given mining target (big endian). -/// -/// It is the inverse of [`hash_rate_to_target`], enabling backward calculations to estimate a -/// mining device's performance from its submitted shares. -/// -/// Typically used to calculate the mining device's effective hashrate during runtime based on the -/// submitted shares and the assigned target, also helps detect changes in miner performance and -/// recalibrate the target (using [`hash_rate_to_target`]) if necessary. -/// -/// ## Formula -/// ```text -/// h = (2^256 - t) / (s * (t + 1)) -/// ``` -/// -/// Where: -/// - `h`: Mining device hashrate (H/s). -/// - `t`: Target threshold. -/// - `s`: Shares per minute. -pub fn hash_rate_from_target(target: U256<'static>, share_per_min: f64) -> Result { - // checks that we are not dividing by zero - if share_per_min == 0.0 { - return Err(Error::HashrateError(InputError::DivisionByZero)); - } - if share_per_min.is_sign_negative() { - return Err(Error::HashrateError(InputError::NegativeInput)); - } - let mut target_arr: [u8; 32] = [0; 32]; - let slice: &mut [u8] = &mut target_arr; - slice.copy_from_slice(target.inner_as_ref()); - target_arr.reverse(); - let target = U256Primitive::from_big_endian(target_arr.as_ref()); - // we calculate the numerator 2^256-t - // note that [255_u8,;32] actually is 2^256 -1, but 2^256 -t = (2^256-1) - (t-1) - let max_target = [255_u8; 32]; - let max_target = U256Primitive::from_big_endian(max_target.as_ref()); - let numerator = max_target - (target - U256Primitive::one()); - // now we calculate the denominator s(t+1) - // *100 here to move the fractional bit up so we can make this an int later - let shares_occurrency_frequence = 60_f64 / (share_per_min) * 100.0; - // note that t+1 cannot be zero because t unsigned. Therefore the denominator is zero if and - // only if s is zero. - let shares_occurrency_frequence = shares_occurrency_frequence as u128; - if shares_occurrency_frequence == 0_u128 { - return Err(Error::HashrateError(InputError::DivisionByZero)); - } - let shares_occurrency_frequence = from_u128_to_u256(shares_occurrency_frequence); - let target_plus_one = - U256Primitive::from_big_endian(target_arr.as_ref()) + U256Primitive::one(); - let denominator = target_plus_one - .checked_mul(shares_occurrency_frequence) - .and_then(|e| e.checked_div(U256Primitive::from(100))) - .ok_or(Error::HashrateError(InputError::ArithmeticOverflow))?; - let result = numerator.div(denominator).low_u128(); - // we multiply back by 100 so that it cancels with the same factor at the denominator - Ok(result as f64) -} - -/// Converts a `Target` to a `f64` difficulty. -pub fn target_to_difficulty(target: Target) -> f64 { - // Genesis block target: 0x00000000ffff0000000000000000000000000000000000000000000000000000 - // (in little endian) - let max_target_bytes = [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, - 0x00, 0x00, - ]; - let max_target = U256Primitive::from_little_endian(&max_target_bytes); - - // Convert input target to U256Primitive - let target_u256: U256<'static> = target.into(); - let mut target_bytes = [0u8; 32]; - target_bytes.copy_from_slice(target_u256.inner_as_ref()); - let target = U256Primitive::from_little_endian(&target_bytes); - - // Calculate difficulty = max_target / target - // We need to handle the full 256-bit values properly - // Convert to f64 by taking the ratio of the most significant bits - let max_target_high = (max_target >> 128).low_u128() as f64; - let max_target_low = max_target.low_u128() as f64; - let target_high = (target >> 128).low_u128() as f64; - let target_low = target.low_u128() as f64; - - // Combine high and low parts with appropriate scaling - let max_target_f64 = max_target_high * (2.0f64.powi(128)) + max_target_low; - let target_f64 = target_high * (2.0f64.powi(128)) + target_low; - - max_target_f64 / target_f64 -} - -/// Converts a `u128` to a [`U256`]. -pub fn from_u128_to_u256(input: u128) -> U256Primitive { - let input: [u8; 16] = input.to_be_bytes(); - let mut be_bytes = [0_u8; 32]; - for (i, b) in input.iter().enumerate() { - be_bytes[16 + i] = *b; - } - U256Primitive::from_big_endian(be_bytes.as_ref()) -} - -/// Generates and manages unique IDs for groups and channels. -/// -/// [`GroupId`] allows combining the group and channel [`Id`]s into a single 64-bit value, enabling -/// efficient tracking and referencing of group-channel relationships. -/// -/// This is specifically used for packaging multiple channels into a single group, such that -/// multiple mining or communication channels can be managed as a cohesive unit. This is -/// particularly useful in scenarios where multiple downstreams share common properties or need to -/// be treated collectively for routing or load balancing. -/// -/// A group acts as a container for multiple channels. Each channel represents a distinct -/// communication pathway between a downstream (e.g. a mining device) and an upstream (e.g. a proxy -/// or pool). Channels within a group might share common configurations, such as difficulty -/// settings or work templates. Operations like broadcasting job updates or handling difficulty -/// adjustments can be efficiently applied to all channels in a group. By treating a group as a -/// single entity, the protocol reduces overhead of managing individual channels, especially in -/// large mining farms. -#[derive(Debug, Default)] -pub struct GroupId { - group_ids: Id, - channel_ids: Id, -} - -impl GroupId { - /// Creates a new [`GroupId`] instance. - /// - /// New GroupId it starts with groups 0, since 0 is reserved for hom downstream's. - pub fn new() -> Self { - Self { - group_ids: Id::new(), - channel_ids: Id::new(), - } - } - - /// Generates a new unique group ID. - /// - /// Increments the internal group ID counter and returns the next available group ID. - pub fn new_group_id(&mut self) -> u32 { - self.group_ids.next() - } - - /// Generates a new unique channel ID for a given group. - /// - /// Increments the internal channel ID counter and returns the next available channel ID. - /// - /// **Note**: The `_group_id` parameter is reserved for future use to create a hierarchical - /// structure of IDs without breaking compatibility with older versions. - pub fn new_channel_id(&mut self, _group_id: u32) -> u32 { - self.channel_ids.next() - } - - /// Combines a group ID and channel ID into a single 64-bit unique ID. - /// - /// Concatenates the group ID and channel ID, storing the group ID in the higher 32 bits and - /// the channel ID in the lower 32 bits. This combined identifier is useful for efficiently - /// tracking and referencing unique group-channel pairs. - pub fn into_complete_id(group_id: u32, channel_id: u32) -> u64 { - let part_1 = channel_id.to_le_bytes(); - let part_2 = group_id.to_le_bytes(); - u64::from_be_bytes([ - part_2[3], part_2[2], part_2[1], part_2[0], part_1[3], part_1[2], part_1[1], part_1[0], - ]) - } - - /// Extracts the group ID from a complete group-channel 64-bit unique ID. - /// - /// The group ID is the higher 32 bits. - pub fn into_group_id(complete_id: u64) -> u32 { - let complete = complete_id.to_le_bytes(); - u32::from_le_bytes([complete[4], complete[5], complete[6], complete[7]]) - } - - /// Extracts the channel ID from a complete group-channel 64-bit unique ID. - /// - /// The channel ID is the lower 32 bits. - pub fn into_channel_id(complete_id: u64) -> u32 { - let complete = complete_id.to_le_bytes(); - u32::from_le_bytes([complete[0], complete[1], complete[2], complete[3]]) - } -} - -#[test] -fn test_group_id_new_group_id() { - let mut group_ids = GroupId::new(); - let _ = group_ids.new_group_id(); - let id = group_ids.new_group_id(); - assert!(id == 2); -} -#[test] -fn test_group_id_new_channel_id() { - let mut group_ids = GroupId::new(); - let _ = group_ids.new_group_id(); - let id = group_ids.new_group_id(); - let channel_id = group_ids.new_channel_id(id); - assert!(channel_id == 1); -} -#[test] -fn test_group_id_new_into_complete_id() { - let group_id = u32::from_le_bytes([0, 1, 2, 3]); - let channel_id = u32::from_le_bytes([10, 11, 12, 13]); - let complete_id = GroupId::into_complete_id(group_id, channel_id); - assert!([10, 11, 12, 13, 0, 1, 2, 3] == complete_id.to_le_bytes()); -} - -#[test] -fn test_group_id_new_into_group_id() { - let group_id = u32::from_le_bytes([0, 1, 2, 3]); - let channel_id = u32::from_le_bytes([10, 11, 12, 13]); - let complete_id = GroupId::into_complete_id(group_id, channel_id); - let channel_from_complete = GroupId::into_channel_id(complete_id); - assert!(channel_id == channel_from_complete); -} - -#[test] -fn test_merkle_root_independent_vector() { - const REFERENCE_MERKLE_ROOT: [u8; 32] = [ - 28, 204, 213, 73, 250, 160, 146, 15, 5, 127, 9, 214, 204, 20, 164, 199, 20, 181, 26, 190, - 236, 91, 40, 225, 128, 239, 213, 148, 232, 77, 4, 36, - ]; - const BRANCH: &[[u8; 32]] = &[ - [ - 224, 195, 140, 86, 17, 172, 9, 61, 54, 73, 215, 202, 109, 83, 124, 163, 215, 78, 143, - 204, 44, 242, 242, 122, 37, 106, 55, 81, 58, 234, 27, 210, - ], - [ - 35, 10, 232, 246, 235, 117, 56, 190, 87, 77, 81, 11, 159, 79, 90, 62, 91, 52, 41, 49, - 57, 245, 219, 122, 115, 223, 199, 229, 238, 60, 47, 144, - ], - [ - 95, 18, 132, 87, 213, 76, 188, 74, 245, 106, 18, 149, 106, 32, 209, 158, 239, 3, 17, - 26, 207, 230, 118, 149, 120, 48, 96, 66, 214, 150, 137, 220, - ], - [ - 205, 167, 106, 179, 82, 50, 157, 76, 91, 36, 54, 226, 34, 183, 162, 179, 109, 64, 185, - 207, 103, 192, 63, 31, 141, 126, 34, 30, 68, 69, 154, 176, - ], - [ - 251, 236, 76, 1, 218, 98, 98, 236, 144, 52, 151, 246, 95, 13, 109, 240, 240, 195, 64, - 157, 7, 142, 28, 242, 29, 123, 51, 93, 51, 36, 143, 148, - ], - [ - 35, 146, 105, 130, 188, 39, 97, 252, 75, 229, 185, 148, 242, 106, 164, 112, 123, 66, - 34, 95, 218, 203, 50, 203, 129, 208, 109, 220, 112, 228, 121, 160, - ], - [ - 44, 55, 125, 47, 249, 213, 175, 143, 140, 50, 219, 72, 111, 71, 125, 54, 85, 70, 4, 85, - 60, 92, 208, 35, 113, 245, 128, 139, 228, 4, 230, 177, - ], - [ - 169, 119, 48, 178, 205, 188, 19, 220, 85, 29, 174, 45, 158, 172, 222, 238, 170, 144, - 79, 140, 56, 90, 105, 187, 204, 145, 241, 96, 75, 88, 6, 133, - ], - [ - 72, 202, 11, 90, 167, 140, 253, 12, 58, 85, 223, 17, 82, 112, 24, 129, 186, 39, 224, - 171, 227, 192, 14, 167, 154, 248, 150, 55, 114, 169, 43, 17, - ], - ]; - const CB_PREFIX: &[u8] = &[ - 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 75, 3, 139, 133, 11, 250, 190, 109, 109, 43, 220, - 215, 96, 154, 211, 18, 14, 53, 53, 0, 95, 132, 159, 127, 54, 197, 70, 135, 74, 17, 149, 12, - 104, 133, 16, 182, 152, 109, 207, 13, 9, 1, 0, 0, 0, 0, 0, 0, 0, - ]; - const CB_SUFFIX: &[u8] = &[ - 89, 236, 29, 54, 20, 47, 115, 108, 117, 115, 104, 47, 0, 0, 0, 0, 3, 236, 42, 86, 37, 0, 0, - 0, 0, 25, 118, 169, 20, 124, 21, 78, 209, 220, 89, 96, 158, 61, 38, 171, 178, 223, 46, 163, - 213, 135, 205, 140, 65, 136, 172, 0, 0, 0, 0, 0, 0, 0, 0, 44, 106, 76, 41, 82, 83, 75, 66, - 76, 79, 67, 75, 58, 155, 83, 3, 23, 69, 4, 30, 18, 212, 34, 33, 76, 167, 101, 132, 91, 1, - 127, 124, 85, 238, 57, 118, 135, 107, 35, 25, 33, 0, 71, 6, 88, 0, 0, 0, 0, 0, 0, 0, 0, 38, - 106, 36, 170, 33, 169, 237, 123, 170, 130, 253, 191, 130, 150, 16, 0, 18, 157, 2, 231, 33, - 177, 230, 137, 182, 134, 51, 32, 216, 181, 6, 73, 60, 103, 211, 194, 61, 77, 64, 0, 0, 0, - 0, - ]; - const EXTRANONCE_PREFIX: &[u8] = &[41, 101, 8, 3, 39, 21, 251]; - const EXTRANONCE: &[u8] = &[165, 6, 238, 7, 139, 252, 22, 7]; - - let full_extranonce = { - let mut xn = EXTRANONCE_PREFIX.to_vec(); - xn.extend_from_slice(EXTRANONCE); - xn - }; - - let calculated_merkle_root = - merkle_root_from_path(CB_PREFIX, CB_SUFFIX, &full_extranonce, BRANCH) - .expect("Ultimate failure. Merkle root calculator returned None"); - assert_eq!( - calculated_merkle_root, REFERENCE_MERKLE_ROOT, - "Merkle root does not match reference" - ) -} - -#[test] -fn test_merkle_root_from_path() { - let coinbase_bytes = vec![ - 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 75, 3, 63, 146, 11, 250, 190, 109, 109, 86, 6, - 110, 64, 228, 218, 247, 203, 127, 75, 141, 53, 51, 197, 180, 38, 117, 115, 221, 103, 2, 11, - 85, 213, 65, 221, 74, 90, 97, 128, 91, 182, 1, 0, 0, 0, 0, 0, 0, 0, 49, 101, 7, 7, 139, - 168, 76, 0, 1, 0, 0, 0, 0, 0, 0, 70, 84, 183, 110, 24, 47, 115, 108, 117, 115, 104, 47, 0, - 0, 0, 0, 3, 120, 55, 179, 37, 0, 0, 0, 0, 25, 118, 169, 20, 124, 21, 78, 209, 220, 89, 96, - 158, 61, 38, 171, 178, 223, 46, 163, 213, 135, 205, 140, 65, 136, 172, 0, 0, 0, 0, 0, 0, 0, - 0, 44, 106, 76, 41, 82, 83, 75, 66, 76, 79, 67, 75, 58, 216, 82, 49, 182, 148, 133, 228, - 178, 20, 248, 55, 219, 145, 83, 227, 86, 32, 97, 240, 182, 3, 175, 116, 196, 69, 114, 83, - 46, 0, 71, 230, 205, 0, 0, 0, 0, 0, 0, 0, 0, 38, 106, 36, 170, 33, 169, 237, 179, 75, 32, - 206, 223, 111, 113, 150, 112, 248, 21, 36, 163, 123, 107, 168, 153, 76, 233, 86, 77, 218, - 162, 59, 48, 26, 180, 38, 62, 34, 3, 185, 0, 0, 0, 0, - ]; - let a = [ - 122, 97, 64, 124, 164, 158, 164, 14, 87, 119, 226, 169, 34, 196, 251, 51, 31, 131, 109, - 250, 13, 54, 94, 6, 177, 27, 156, 154, 101, 30, 123, 159, - ]; - let b = [ - 180, 113, 121, 253, 215, 85, 129, 38, 108, 2, 86, 66, 46, 12, 131, 139, 130, 87, 29, 92, - 59, 164, 247, 114, 251, 140, 129, 88, 127, 196, 125, 116, - ]; - let c = [ - 171, 77, 225, 148, 80, 32, 41, 157, 246, 77, 161, 49, 87, 139, 214, 236, 149, 164, 192, - 128, 195, 9, 5, 168, 131, 27, 250, 9, 60, 179, 206, 94, - ]; - let d = [ - 6, 187, 202, 75, 155, 220, 255, 166, 199, 35, 182, 220, 20, 96, 123, 41, 109, 40, 186, 142, - 13, 139, 230, 164, 116, 177, 217, 23, 16, 123, 135, 202, - ]; - let e = [ - 109, 45, 171, 89, 223, 39, 132, 14, 150, 128, 241, 113, 136, 227, 105, 123, 224, 48, 66, - 240, 189, 186, 222, 49, 173, 143, 80, 90, 110, 219, 192, 235, - ]; - let f = [ - 196, 7, 21, 180, 228, 161, 182, 132, 28, 153, 242, 12, 210, 127, 157, 86, 62, 123, 181, 33, - 84, 3, 105, 129, 148, 162, 5, 152, 64, 7, 196, 156, - ]; - let g = [ - 22, 16, 18, 180, 109, 237, 68, 167, 197, 10, 195, 134, 11, 119, 219, 184, 49, 140, 239, 45, - 27, 210, 212, 120, 186, 60, 155, 105, 106, 219, 218, 32, - ]; - let h = [ - 83, 228, 21, 241, 42, 240, 8, 254, 109, 156, 59, 171, 167, 46, 183, 60, 27, 63, 241, 211, - 235, 179, 147, 99, 46, 3, 22, 166, 159, 169, 183, 159, - ]; - let i = [ - 230, 81, 3, 190, 66, 73, 200, 55, 94, 135, 209, 50, 92, 193, 114, 202, 141, 170, 124, 142, - 206, 29, 88, 9, 22, 110, 203, 145, 238, 66, 166, 35, - ]; - let l = [ - 43, 106, 86, 239, 237, 74, 208, 202, 247, 133, 88, 42, 15, 77, 163, 186, 85, 26, 89, 151, - 5, 19, 30, 122, 108, 220, 215, 104, 152, 226, 113, 55, - ]; - let m = [ - 148, 76, 200, 221, 206, 54, 56, 45, 252, 60, 123, 202, 195, 73, 144, 65, 168, 184, 59, 130, - 145, 229, 250, 44, 213, 70, 175, 128, 34, 31, 102, 80, - ]; - let n = [ - 203, 112, 102, 31, 49, 147, 24, 25, 245, 61, 179, 146, 205, 127, 126, 100, 78, 204, 228, - 146, 209, 154, 89, 194, 209, 81, 57, 167, 88, 251, 44, 76, - ]; - let mut path = vec![a, b, c, d, e, f, g, h, i, l, m, n]; - let expected_root = vec![ - 73, 100, 41, 247, 106, 44, 1, 242, 3, 64, 100, 1, 98, 155, 40, 91, 170, 255, 170, 29, 193, - 255, 244, 71, 236, 29, 134, 218, 94, 45, 78, 77, - ]; - let root = merkle_root_from_path( - &coinbase_bytes[..20], - &coinbase_bytes[30..], - &coinbase_bytes[20..30], - &path, - ) - .unwrap(); - assert_eq!(expected_root, root); - - //Target coinbase_id return path - path.clear(); - let coinbase_id = vec![ - 10, 66, 217, 241, 152, 86, 5, 234, 225, 85, 251, 215, 105, 1, 21, 126, 222, 69, 40, 157, - 23, 177, 157, 106, 234, 164, 243, 206, 23, 241, 250, 166, - ]; - - let root = merkle_root_from_path( - &coinbase_bytes[..20], - &coinbase_bytes[30..], - &coinbase_bytes[20..30], - &path, - ) - .unwrap(); - assert_eq!(coinbase_id, root); - - //Target None return path on serialization - assert_eq!( - merkle_root_from_path(&coinbase_bytes, &coinbase_bytes, &coinbase_bytes, &path), - None - ); -} - -/// Converts a `u256` to a [`BlockHash`] type. -pub fn u256_to_block_hash(v: U256<'static>) -> BlockHash { - let hash: [u8; 32] = v.to_vec().try_into().unwrap(); - let hash = Hash::from_slice(&hash).unwrap(); - BlockHash::from_raw_hash(hash) -} - -// Returns a new `Header`. -// -// Expected endianness inputs: -// `version` LE -// `prev_hash` BE -// `merkle_root` BE -// `time` BE -// `bits` BE -// `nonce` BE -#[allow(dead_code)] -pub(crate) fn new_header( - version: i32, - prev_hash: &[u8], - merkle_root: &[u8], - time: u32, - bits: u32, - nonce: u32, -) -> Result { - if prev_hash.len() != 32 { - return Err(Error::ExpectedLen32(prev_hash.len())); - } - if merkle_root.len() != 32 { - return Err(Error::ExpectedLen32(merkle_root.len())); - } - let mut prev_hash_arr = [0u8; 32]; - prev_hash_arr.copy_from_slice(prev_hash); - let prev_hash = DHash::from_bytes_ref(&prev_hash_arr); - - let mut merkle_root_arr = [0u8; 32]; - merkle_root_arr.copy_from_slice(merkle_root); - let merkle_root = DHash::from_bytes_ref(&merkle_root_arr); - - Ok(Header { - version: Version::from_consensus(version), - prev_blockhash: BlockHash::from_raw_hash(*prev_hash), - merkle_root: TxMerkleNode::from_raw_hash(*merkle_root), - time, - bits: CompactTarget::from_consensus(bits), - nonce, - }) -} - -/// Creates a block from a solution submission. -/// -/// Facilitates the creation of valid Bitcoin blocks by combining a declared mining job, a list of -/// transactions, and a solution message from the mining device. It encapsulates the necessary data -/// (the coinbase, a list of transactions, and a miner-provided solution) to assemble a complete -/// and valid block that can be submitted to the Bitcoin network. -/// -/// It is used in the Job Declarator server to handle the final step in processing the mining job -/// solutions. -pub struct BlockCreator<'a> { - last_declare: DeclareMiningJob<'a>, - tx_list: Vec, - message: PushSolution<'a>, -} - -impl<'a> BlockCreator<'a> { - /// Creates a new [`BlockCreator`] instance. - pub fn new( - last_declare: DeclareMiningJob<'a>, - tx_list: Vec, - message: PushSolution<'a>, - ) -> BlockCreator<'a> { - BlockCreator { - last_declare, - tx_list, - message, - } - } -} - -// TODO write a test for this function that takes an already mined block, and test if the new -// block created with the hash of the new block created with the block creator coincides with the -// hash of the mined block -impl<'a> From> for bitcoin::Block { - fn from(block_creator: BlockCreator<'a>) -> bitcoin::Block { - let last_declare = block_creator.last_declare; - let mut tx_list = block_creator.tx_list; - let message = block_creator.message; - - let coinbase_pre = last_declare.coinbase_prefix.to_vec(); - let extranonce = message.extranonce.to_vec(); - let coinbase_suf = last_declare.coinbase_suffix.to_vec(); - let mut path: Vec> = vec![]; - for tx in &tx_list { - let id = tx.compute_txid(); - let id_bytes: &[u8; 32] = id.as_ref(); - path.push(id_bytes.to_vec()); - } - let merkle_root = - merkle_root_from_path(&coinbase_pre[..], &coinbase_suf[..], &extranonce[..], &path) - .expect("Invalid coinbase"); - let merkle_root = Hash::from_slice(merkle_root.as_slice()).unwrap(); - - let prev_blockhash = u256_to_block_hash(message.prev_hash.into_static()); - let header = Header { - version: Version::from_consensus(message.version as i32), - prev_blockhash, - merkle_root, - time: message.ntime, - bits: CompactTarget::from_consensus(message.nbits), - nonce: message.nonce, - }; - - let coinbase = [coinbase_pre, extranonce, coinbase_suf].concat(); - let coinbase = consensus::deserialize(&coinbase[..]).unwrap(); - tx_list.insert(0, coinbase); - - let mut block = Block { - header, - txdata: tx_list.clone(), - }; - - block.header.merkle_root = block.compute_merkle_root().unwrap(); - block - } -} - #[cfg(test)] mod tests { - use super::{hash_rate_from_target, hash_rate_to_target, *}; - use binary_sv2::{Seq0255, B064K, U256}; - use rand::Rng; - use serde::Deserialize; - use std::{convert::TryInto, num::ParseIntError}; - - fn decode_hex(s: &str) -> Result, ParseIntError> { - (0..s.len()) - .step_by(2) - .map(|i| u8::from_str_radix(&s[i..i + 2], 16)) - .collect() - } - - #[derive(Debug, Deserialize)] - struct TestBlockToml { - block_hash: String, - version: u32, - prev_hash: String, - time: u32, - merkle_root: String, - nbits: u32, - nonce: u32, - coinbase_tx_prefix: String, - coinbase_script: String, - coinbase_tx_suffix: String, - path: Vec, - } - - #[derive(Debug)] - struct TestBlock<'decoder> { - #[allow(dead_code)] - block_hash: U256<'decoder>, - version: u32, - prev_hash: Vec, - time: u32, - merkle_root: Vec, - nbits: u32, - nonce: u32, - coinbase_tx_prefix: B064K<'decoder>, - coinbase_script: Vec, - coinbase_tx_suffix: B064K<'decoder>, - path: Seq0255<'decoder, U256<'decoder>>, - } - - fn get_test_block<'decoder>() -> TestBlock<'decoder> { - let test_file = std::fs::read_to_string("reg-test-block.toml") - .expect("Could not read file from string"); - let block: TestBlockToml = - toml::from_str(&test_file).expect("Could not parse toml file as `TestBlockToml`"); - - // Get block hash - let block_hash_vec = - decode_hex(&block.block_hash).expect("Could not decode hex string to `Vec`"); - let mut block_hash_vec: [u8; 32] = block_hash_vec - .try_into() - .expect("Slice is incorrect length"); - block_hash_vec.reverse(); - let block_hash: U256 = block_hash_vec.into(); - - // Get prev hash - let mut prev_hash: Vec = - decode_hex(&block.prev_hash).expect("Could not convert `String` to `&[u8]`"); - prev_hash.reverse(); - - // Get Merkle root - let mut merkle_root = - decode_hex(&block.merkle_root).expect("Could not decode hex string to `Vec`"); - // Swap endianness to LE - merkle_root.reverse(); - - // Get Merkle path - let mut path_vec = Vec::::new(); - for p in block.path { - let p_vec = decode_hex(&p).expect("Could not decode hex string to `Vec`"); - let p_arr: [u8; 32] = p_vec.try_into().expect("Slice is incorrect length"); - let p_u256: U256 = (p_arr).into(); - path_vec.push(p_u256); - } - - let path = Seq0255::new(path_vec).expect("Could not convert `Vec` to `Seq0255`"); - - // Pass in coinbase as three pieces: - // coinbase_tx_prefix + coinbase script + coinbase_tx_suffix - let coinbase_tx_prefix_vec = decode_hex(&block.coinbase_tx_prefix) - .expect("Could not decode hex string to `Vec`"); - let coinbase_tx_prefix: B064K = coinbase_tx_prefix_vec - .try_into() - .expect("Could not convert `Vec` into `B064K`"); - - let coinbase_script = - decode_hex(&block.coinbase_script).expect("Could not decode hex `String` to `Vec`"); - - let coinbase_tx_suffix_vec = decode_hex(&block.coinbase_tx_suffix) - .expect("Could not decode hex `String` to `Vec`"); - let coinbase_tx_suffix: B064K = coinbase_tx_suffix_vec - .try_into() - .expect("Could not convert `Vec` to `B064K`"); - - TestBlock { - block_hash, - version: block.version, - prev_hash, - time: block.time, - merkle_root, - nbits: block.nbits, - nonce: block.nonce, - coinbase_tx_prefix, - coinbase_script, - coinbase_tx_suffix, - path, - } - } - - #[test] - fn gets_merkle_root_from_path() { - let block = get_test_block(); - let expect: Vec = block.merkle_root; - - let actual = merkle_root_from_path( - block.coinbase_tx_prefix.inner_as_ref(), - block.coinbase_tx_suffix.inner_as_ref(), - &block.coinbase_script, - &block.path.inner_as_ref(), - ) - .unwrap(); - - assert_eq!(expect, actual); - } - - #[test] - - fn gets_new_header() -> Result<(), Error> { - let block = get_test_block(); - - if !block.prev_hash.len() == 32 { - return Err(Error::ExpectedLen32(block.prev_hash.len())); - } - if !block.merkle_root.len() == 32 { - return Err(Error::ExpectedLen32(block.merkle_root.len())); - } - let mut prev_hash_arr = [0u8; 32]; - prev_hash_arr.copy_from_slice(&block.prev_hash); - let prev_hash = DHash::from_bytes_ref(&prev_hash_arr); - - let mut merkle_root_arr = [0u8; 32]; - merkle_root_arr.copy_from_slice(&block.merkle_root); - let merkle_root = DHash::from_bytes_ref(&merkle_root_arr); - - let expect = Header { - version: Version::from_consensus(block.version as i32), - prev_blockhash: BlockHash::from_raw_hash(*prev_hash), - merkle_root: TxMerkleNode::from_raw_hash(*merkle_root), - time: block.time, - bits: CompactTarget::from_consensus(block.nbits), - nonce: block.nonce, - }; - - let actual_block = get_test_block(); - let actual = new_header( - block.version as i32, - &actual_block.prev_hash, - &actual_block.merkle_root, - block.time, - block.nbits, - block.nonce, - )?; - assert_eq!(actual, expect); - Ok(()) - } - - #[test] - fn test_hash_rate_to_target() { - let mut rng = rand::thread_rng(); - let mut successes = 0; - - let hr = 10.0; // 10 h/s - let hrs = hr * 60.0; // number of hashes in 1 minute - let mut target = hash_rate_to_target(hr, 1.0).unwrap().to_vec(); - target.reverse(); - let target = U256Primitive::from_big_endian(&target[..]); - - let mut i: i64 = 0; - let mut results = vec![]; - let attempts = 1000; - while successes < attempts { - let a: u128 = rng.gen(); - let b: u128 = rng.gen(); - let a = a.to_be_bytes(); - let b = b.to_be_bytes(); - let concat = [&a[..], &b[..]].concat().to_vec(); - i += 1; - if U256Primitive::from_big_endian(&concat[..]) <= target { - results.push(i); - i = 0; - successes += 1; - } - } - - let mut average: f64 = 0.0; - for i in &results { - average += (*i as f64) / attempts as f64; - } - let delta = (hrs - average) as i64; - assert!(delta.abs() < 100); - } - - #[test] - fn test_hash_rate_from_target() { - let hr = 202470.828; - let expected_share_per_min = 1.0; - let target = hash_rate_to_target(hr, expected_share_per_min).unwrap(); - let realized_share_per_min = expected_share_per_min * 10.0; // increase SPM by 10x - let hash_rate = hash_rate_from_target(target.clone(), realized_share_per_min).unwrap(); - let new_hr = (hr * 10.0).trunc(); - - assert!( - hash_rate == new_hr, - "hash_rate_from_target equation was not properly transformed" - ) - } - - #[test] - fn test_hash_rate_from_target_zero_share_per_min() { - // Test division by zero error handling when share_per_min is 0. - let hr = 202470.828; - let expected_share_per_min = 1.0; - let target = hash_rate_to_target(hr, expected_share_per_min).unwrap(); - let share_per_min = 0.0; - let result = hash_rate_from_target(target, share_per_min); - - assert!( - matches!( - result, - Err(Error::HashrateError(InputError::DivisionByZero)) - ), - "Should return division by zero error" - ); - } - - #[test] - fn test_hash_rate_from_target_arithmetic_overflow() { - // Test arithmetic overflow error handling with extremely low share_per_min and maximal - // target. - let mut target_bytes = [0xff; 32]; - target_bytes[0] = 0x7f; // Reduce the magnitude to avoid direct overflow - let target_sv2 = U256::from(target_bytes); - let share_per_min = 0.001; - let result = hash_rate_from_target(target_sv2, share_per_min); - - assert!( - matches!( - result, - Err(Error::HashrateError(InputError::ArithmeticOverflow)) - ), - "Should return arithmetic overflow error" - ); - } - #[test] fn test_super_safe_lock() { let m = super::Mutex::new(1u32); @@ -1152,45 +112,4 @@ mod tests { // m.super_safe_lock(|i| *i = (*i).checked_add(1).unwrap()); // will not compile m.super_safe_lock(|i| *i = (*i).checked_add(1).unwrap_or_default()); // compiles } - - #[test] - fn test_target_to_difficulty() { - // Test target: 0x000000000004864c000000000000000000000000000000000000000000000000 - let target_bytes = [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4c, 0x86, 0x04, 0x00, - 0x00, 0x00, 0x00, 0x00, - ]; - let target = Target::from(target_bytes); - let difficulty = target_to_difficulty(target); - - // Expected difficulty: 14484.162361 - let expected_difficulty = 14484.162361; - let epsilon = 0.000001; // Small value for floating point comparison - - assert!( - (difficulty - expected_difficulty).abs() < epsilon, - "Expected difficulty {}, got {}", - expected_difficulty, - difficulty - ); - - let max_target_bytes = [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, - ]; - let max_target = Target::from(max_target_bytes); - let max_difficulty = target_to_difficulty(max_target); - - let expected_max_difficulty = 1.0; - let epsilon = 0.000001; // Small value for floating point comparison - - assert!( - (max_difficulty - expected_max_difficulty).abs() < epsilon, - "Expected difficulty {}, got {}", - expected_max_difficulty, - max_difficulty - ); - } } diff --git a/protocols/v2/subprotocols/common-messages/Cargo.toml b/protocols/v2/subprotocols/common-messages/Cargo.toml index e7054b38ca..d1a242d39a 100644 --- a/protocols/v2/subprotocols/common-messages/Cargo.toml +++ b/protocols/v2/subprotocols/common-messages/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "common_messages_sv2" -version = "5.1.0" +version = "6.0.1" authors = ["The Stratum V2 Developers"] -edition = "2018" +edition = "2021" readme = "README.md" description = "Sv2 subprotocol common messages" documentation = "https://docs.rs/common_messages_sv2" @@ -14,8 +14,7 @@ keywords = ["stratum", "mining", "bitcoin", "protocol"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -binary_sv2 = { path = "../../binary-sv2", version = "^3.0.0" } -stratum-common = { version = "^2.0.0", path = "../../../../common" } +binary_sv2 = { path = "../../binary-sv2", version = "^4.0.0" } quickcheck = { version = "1.0.3", optional = true } quickcheck_macros = { version = "1", optional = true } diff --git a/protocols/v2/subprotocols/common-messages/src/channel_endpoint_changed.rs b/protocols/v2/subprotocols/common-messages/src/channel_endpoint_changed.rs index f3988a7400..965368bd96 100644 --- a/protocols/v2/subprotocols/common-messages/src/channel_endpoint_changed.rs +++ b/protocols/v2/subprotocols/common-messages/src/channel_endpoint_changed.rs @@ -1,4 +1,4 @@ -use alloc::vec::Vec; +use alloc::{fmt, vec::Vec}; use binary_sv2::{binary_codec_sv2, Deserialize, Serialize}; use core::convert::TryInto; @@ -10,9 +10,15 @@ use core::convert::TryInto; /// /// When a downstream receives such a message, any extension state (including version and extension /// support) must be reset and renegotiated. -#[repr(C)] + #[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq)] pub struct ChannelEndpointChanged { /// Unique identifier of the channel that has changed its endpoint. pub channel_id: u32, } + +impl fmt::Display for ChannelEndpointChanged { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "ChannelEndpointChanged(channel_id: {})", self.channel_id) + } +} diff --git a/protocols/v2/subprotocols/common-messages/src/lib.rs b/protocols/v2/subprotocols/common-messages/src/lib.rs index bf4eb29c9d..14cf309501 100644 --- a/protocols/v2/subprotocols/common-messages/src/lib.rs +++ b/protocols/v2/subprotocols/common-messages/src/lib.rs @@ -32,8 +32,6 @@ pub use setup_connection::{ SetupConnectionError, SetupConnectionSuccess, }; -pub use setup_connection::{CSetupConnection, CSetupConnectionError}; - // Discriminants for Stratum V2 (sub)protocols // // Discriminants are unique identifiers used to distinguish between different @@ -61,14 +59,6 @@ pub const CHANNEL_BIT_SETUP_CONNECTION_SUCCESS: bool = false; pub const CHANNEL_BIT_SETUP_CONNECTION_ERROR: bool = false; pub const CHANNEL_BIT_CHANNEL_ENDPOINT_CHANGED: bool = true; -#[no_mangle] -/// A C-compatible function that exports the [`ChannelEndpointChanged`] struct. -pub extern "C" fn _c_export_channel_endpoint_changed(_a: ChannelEndpointChanged) {} - -#[no_mangle] -/// A C-compatible function that exports the `SetupConnection` struct. -pub extern "C" fn _c_export_setup_conn_succ(_a: SetupConnectionSuccess) {} - #[cfg(feature = "prop_test")] impl ChannelEndpointChanged { pub fn from_gen(g: &mut Gen) -> Self { diff --git a/protocols/v2/subprotocols/common-messages/src/reconnect.rs b/protocols/v2/subprotocols/common-messages/src/reconnect.rs index 33ab80ac81..9e33a769a3 100644 --- a/protocols/v2/subprotocols/common-messages/src/reconnect.rs +++ b/protocols/v2/subprotocols/common-messages/src/reconnect.rs @@ -1,4 +1,4 @@ -use alloc::vec::Vec; +use alloc::{fmt, vec::Vec}; use binary_sv2::{binary_codec_sv2, Deserialize, Serialize, Str0255}; use core::convert::TryInto; @@ -20,6 +20,17 @@ pub struct Reconnect<'decoder> { pub new_port: u16, } +impl fmt::Display for Reconnect<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Reconnect(new_host: {}, new_port: {})", + self.new_host.as_utf8_or_hex(), + self.new_port + ) + } +} + impl PartialEq for Reconnect<'_> { fn eq(&self, other: &Self) -> bool { self.new_host.as_ref() == other.new_host.as_ref() && self.new_port == other.new_port diff --git a/protocols/v2/subprotocols/common-messages/src/setup_connection.rs b/protocols/v2/subprotocols/common-messages/src/setup_connection.rs index e0ff207d65..eb12189b1d 100644 --- a/protocols/v2/subprotocols/common-messages/src/setup_connection.rs +++ b/protocols/v2/subprotocols/common-messages/src/setup_connection.rs @@ -2,12 +2,11 @@ use crate::{ SV2_JOB_DECLARATION_PROTOCOL_DISCRIMINANT, SV2_MINING_PROTOCOL_DISCRIMINANT, SV2_TEMPLATE_DISTRIBUTION_PROTOCOL_DISCRIMINANT, }; -use alloc::vec::Vec; +use alloc::{fmt, vec::Vec}; use binary_sv2::{ binary_codec_sv2, - binary_codec_sv2::CVec, decodable::{DecodableField, FieldMarker}, - free_vec, Deserialize, Error, GetSize, Serialize, Str0255, + Deserialize, GetSize, Serialize, Str0255, }; use core::convert::{TryFrom, TryInto}; @@ -54,6 +53,25 @@ pub struct SetupConnection<'decoder> { pub device_id: Str0255<'decoder>, } +impl fmt::Display for SetupConnection<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "SetupConnection(protocol: {}, min_version: {}, max_version: {}, flags: 0x{:08x}, endpoint_host: {}, endpoint_port: {}, vendor: {}, hardware_version: {}, firmware: {}, device_id: {})", + self.protocol as u8, + self.min_version, + self.max_version, + self.flags, + self.endpoint_host.as_utf8_or_hex(), + self.endpoint_port, + self.vendor.as_utf8_or_hex(), + self.hardware_version.as_utf8_or_hex(), + self.firmware.as_utf8_or_hex(), + self.device_id.as_utf8_or_hex() + ) + } +} + impl SetupConnection<'_> { /// Set the flag to indicate that the downstream requires a standard job pub fn set_requires_standard_job(&mut self) { @@ -166,7 +184,7 @@ pub fn has_requires_std_job(flags: u32) -> bool { /// Helper function to check if `REQUIRES_VERSION_ROLLING` bit flag present. pub fn has_version_rolling(flags: u32) -> bool { let flags = flags.reverse_bits(); - let flags = flags << 1; + let flags = flags << 2; let flag = flags >> 31; flag != 0 } @@ -174,105 +192,16 @@ pub fn has_version_rolling(flags: u32) -> bool { /// Helper function to check if `REQUIRES_WORK_SELECTION` bit flag present. pub fn has_work_selection(flags: u32) -> bool { let flags = flags.reverse_bits(); - let flags = flags << 2; + let flags = flags << 1; let flag = flags >> 31; flag != 0 } -/// C representation of [`SetupConnection`] -#[repr(C)] -#[derive(Debug, Clone)] -pub struct CSetupConnection { - /// Protocol to be used for the connection. - pub protocol: Protocol, - /// The minimum protocol version supported. - /// - /// Currently must be set to 2. - pub min_version: u16, - /// The maximum protocol version supported. - /// - /// Currently must be set to 2. - pub max_version: u16, - /// Flags indicating optional protocol features supported by the downstream. - /// - /// Each [`SetupConnection::protocol`] value has it's own flags. - pub flags: u32, - /// ASCII representation of the connection hostname or IP address. - pub endpoint_host: CVec, - /// Connection port value. - pub endpoint_port: u16, - /// Device vendor name. - pub vendor: CVec, - /// Device hardware version. - pub hardware_version: CVec, - /// Device firmware version. - pub firmware: CVec, - /// Device identifier. - pub device_id: CVec, -} - -impl<'a> CSetupConnection { - #[allow(clippy::wrong_self_convention)] - /// Convert C representation to Rust representation - pub fn to_rust_rep_mut(&'a mut self) -> Result, Error> { - let endpoint_host: Str0255 = self.endpoint_host.as_mut_slice().try_into()?; - let vendor: Str0255 = self.vendor.as_mut_slice().try_into()?; - let hardware_version: Str0255 = self.hardware_version.as_mut_slice().try_into()?; - let firmware: Str0255 = self.firmware.as_mut_slice().try_into()?; - let device_id: Str0255 = self.device_id.as_mut_slice().try_into()?; - - Ok(SetupConnection { - protocol: self.protocol, - min_version: self.min_version, - max_version: self.max_version, - flags: self.flags, - endpoint_host, - endpoint_port: self.endpoint_port, - vendor, - hardware_version, - firmware, - device_id, - }) - } -} - -#[no_mangle] -pub extern "C" fn free_setup_connection(s: CSetupConnection) { - drop(s) -} - -impl Drop for CSetupConnection { - fn drop(&mut self) { - free_vec(&mut self.endpoint_host); - free_vec(&mut self.vendor); - free_vec(&mut self.hardware_version); - free_vec(&mut self.firmware); - free_vec(&mut self.device_id); - } -} - -impl From> for CSetupConnection { - fn from(v: SetupConnection) -> Self { - Self { - protocol: v.protocol, - min_version: v.min_version, - max_version: v.max_version, - flags: v.flags, - endpoint_host: v.endpoint_host.into(), - endpoint_port: v.endpoint_port, - vendor: v.vendor.into(), - hardware_version: v.hardware_version.into(), - firmware: v.firmware.into(), - device_id: v.device_id.into(), - } - } -} - /// Message used by an upstream role to accept a connection setup request from a downstream role. /// /// This message is sent in response to a [`SetupConnection`] message. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Copy)] -#[repr(C)] + pub struct SetupConnectionSuccess { /// Selected version based on the [`SetupConnection::min_version`] and /// [`SetupConnection::max_version`] sent by the downstream role. @@ -287,6 +216,16 @@ pub struct SetupConnectionSuccess { pub flags: u32, } +impl fmt::Display for SetupConnectionSuccess { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "SetupConnectionSuccess(used_version: {}, flags: 0x{:08x})", + self.used_version, self.flags + ) + } +} + /// Message used by an upstream role to reject a connection setup request from a downstream role. /// /// This message is sent in response to a [`SetupConnection`] message. @@ -316,44 +255,14 @@ pub struct SetupConnectionError<'decoder> { pub error_code: Str0255<'decoder>, } -#[repr(C)] -#[derive(Debug, Clone)] -/// C representation of [`SetupConnectionError`] -pub struct CSetupConnectionError { - flags: u32, - error_code: CVec, -} - -impl<'a> CSetupConnectionError { - #[allow(clippy::wrong_self_convention)] - /// Convert C representation to Rust representation - pub fn to_rust_rep_mut(&'a mut self) -> Result, Error> { - let error_code: Str0255 = self.error_code.as_mut_slice().try_into()?; - - Ok(SetupConnectionError { - flags: self.flags, - error_code, - }) - } -} - -#[no_mangle] -pub extern "C" fn free_setup_connection_error(s: CSetupConnectionError) { - drop(s) -} - -impl Drop for CSetupConnectionError { - fn drop(&mut self) { - free_vec(&mut self.error_code); - } -} - -impl<'a> From> for CSetupConnectionError { - fn from(v: SetupConnectionError<'a>) -> Self { - Self { - flags: v.flags, - error_code: v.error_code.into(), - } +impl fmt::Display for SetupConnectionError<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "SetupConnectionError(flags: 0x{:08x}, error_code: {})", + self.flags, + self.error_code.as_utf8_or_hex() + ) } } @@ -453,7 +362,7 @@ mod test { #[test] fn test_has_version_rolling() { - let flags = 0b_0000_0000_0000_0000_0000_0000_0000_0010; + let flags = 0b_0000_0000_0000_0000_0000_0000_0000_0100; assert!(has_version_rolling(flags)); let flags = 0b_0000_0000_0000_0000_0000_0000_0000_0001; assert!(!has_version_rolling(flags)); @@ -461,7 +370,7 @@ mod test { #[test] fn test_has_work_selection() { - let flags = 0b_0000_0000_0000_0000_0000_0000_0000_0100; + let flags = 0b_0000_0000_0000_0000_0000_0000_0000_0010; assert!(has_work_selection(flags)); let flags = 0b_0000_0000_0000_0000_0000_0000_0000_0001; assert!(!has_work_selection(flags)); diff --git a/protocols/v2/subprotocols/job-declaration/Cargo.toml b/protocols/v2/subprotocols/job-declaration/Cargo.toml index 06ec244551..1a211b9216 100644 --- a/protocols/v2/subprotocols/job-declaration/Cargo.toml +++ b/protocols/v2/subprotocols/job-declaration/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "job_declaration_sv2" -version = "4.0.0" +version = "5.0.1" authors = ["The Stratum V2 Developers"] -edition = "2018" +edition = "2021" readme = "README.md" description = "SV2 job declaration protocol types" documentation = "https://docs.rs/job_declaration_sv2" @@ -13,5 +13,4 @@ keywords = ["stratum", "mining", "bitcoin", "protocol"] [dependencies] -binary_sv2 = { path = "../../binary-sv2", version = "^3.0.0" } -stratum-common = { version = "^2.0.0", path = "../../../../common" } +binary_sv2 = { path = "../../binary-sv2", version = "^4.0.0" } diff --git a/protocols/v2/subprotocols/job-declaration/src/allocate_mining_job_token.rs b/protocols/v2/subprotocols/job-declaration/src/allocate_mining_job_token.rs index f8e2ecf1ca..15baced478 100644 --- a/protocols/v2/subprotocols/job-declaration/src/allocate_mining_job_token.rs +++ b/protocols/v2/subprotocols/job-declaration/src/allocate_mining_job_token.rs @@ -1,4 +1,4 @@ -use alloc::vec::Vec; +use alloc::{fmt, vec::Vec}; use binary_sv2::{binary_codec_sv2, Deserialize, Serialize, Str0255, B0255, B064K}; use core::convert::TryInto; @@ -14,6 +14,17 @@ pub struct AllocateMiningJobToken<'decoder> { pub request_id: u32, } +impl fmt::Display for AllocateMiningJobToken<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AllocateMiningJobToken(user_identifier: {}, request_id: {})", + self.user_identifier.as_utf8_or_hex(), + self.request_id + ) + } +} + /// Message used by JDS to accept [`AllocateMiningJobToken`] message. #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[repr(C)] @@ -25,12 +36,18 @@ pub struct AllocateMiningJobTokenSuccess<'decoder> { /// A token that makes the JDC eligible for committing a mining job for approval/transactions /// declaration or for identifying custom mining job on mining connection. pub mining_job_token: B0255<'decoder>, - /// The maximum additional serialized bytes which the JDS will add in coinbase transaction - /// outputs. - pub coinbase_output_max_additional_size: u32, - /// The maximum additional sigops which the JDS will add in coinbase transaction - /// outputs. - pub coinbase_output_max_additional_sigops: u16, /// Bitcoin transaction outputs added by JDS. - pub coinbase_output: B064K<'decoder>, + pub coinbase_outputs: B064K<'decoder>, +} + +impl fmt::Display for AllocateMiningJobTokenSuccess<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "AllocateMiningJobTokenSuccess(request_id: {}, mining_job_token: {}, coinbase_outputs: {})", + self.request_id, + self.mining_job_token.as_hex(), + self.coinbase_outputs + ) + } } diff --git a/protocols/v2/subprotocols/job-declaration/src/declare_mining_job.rs b/protocols/v2/subprotocols/job-declaration/src/declare_mining_job.rs index 8cc9e3ba7a..da4c16134e 100644 --- a/protocols/v2/subprotocols/job-declaration/src/declare_mining_job.rs +++ b/protocols/v2/subprotocols/job-declaration/src/declare_mining_job.rs @@ -1,4 +1,4 @@ -use alloc::vec::Vec; +use alloc::{fmt, vec::Vec}; use binary_sv2::{binary_codec_sv2, Deserialize, Seq064K, Serialize, Str0255, B0255, B064K, U256}; use core::convert::TryInto; @@ -19,11 +19,11 @@ pub struct DeclareMiningJob<'decoder> { pub mining_job_token: B0255<'decoder>, /// Header version field. pub version: u32, - /// The coinbase transaction nVersion field - pub coinbase_prefix: B064K<'decoder>, - /// Up to 8 bytes (not including the length byte) which are to be placed at the beginning of - /// the coinbase field in the coinbase transaction. - pub coinbase_suffix: B064K<'decoder>, + /// Serialized bytes representing the initial part of the coinbase transaction (not including + /// extranonce) + pub coinbase_tx_prefix: B064K<'decoder>, + /// Serialized bytes representing the final part of the coinbase transaction (after extranonce) + pub coinbase_tx_suffix: B064K<'decoder>, /// List of the transaction ids contained in the template. JDS checks the list against its /// mempool and requests missing txs via [`crate::ProvideMissingTransactions`]. /// @@ -34,6 +34,22 @@ pub struct DeclareMiningJob<'decoder> { pub excess_data: B064K<'decoder>, } +impl fmt::Display for DeclareMiningJob<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "DeclareMiningJob(request_id: {}, mining_job_token: {}, version: 0x{:08x}, coinbase_tx_prefix: {}, coinbase_tx_suffix: {}, tx_ids_list: {}, excess_data: {})", + self.request_id, + self.mining_job_token.as_hex(), + self.version, + self.coinbase_tx_prefix, + self.coinbase_tx_suffix, + self.tx_ids_list, + self.excess_data + ) + } +} + /// Messaged used by JDS to accept [`DeclareMiningJob`] message. /// /// If [`Full Template`] mode is used, JDS MAY request txdata via `ProvideMissingTransactions` @@ -54,6 +70,17 @@ pub struct DeclareMiningJobSuccess<'decoder> { pub new_mining_job_token: B0255<'decoder>, } +impl fmt::Display for DeclareMiningJobSuccess<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "DeclareMiningJobSuccess(request_id: {}, new_mining_job_token: {})", + self.request_id, + self.new_mining_job_token.as_hex() + ) + } +} + /// Messaged used by JDS to reject [`DeclareMiningJob`] message. /// /// Downstream should consider this as a trigger to fallback into some other Pool/JDS or solo @@ -73,3 +100,15 @@ pub struct DeclareMiningJobError<'decoder> { /// Optional details about the error. pub error_details: B064K<'decoder>, } + +impl fmt::Display for DeclareMiningJobError<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "DeclareMiningJobError(request_id: {}, error_code: {}, error_details: {})", + self.request_id, + self.error_code.as_utf8_or_hex(), + self.error_details + ) + } +} diff --git a/protocols/v2/subprotocols/job-declaration/src/provide_missing_transactions.rs b/protocols/v2/subprotocols/job-declaration/src/provide_missing_transactions.rs index e6e4782866..6618745fc6 100644 --- a/protocols/v2/subprotocols/job-declaration/src/provide_missing_transactions.rs +++ b/protocols/v2/subprotocols/job-declaration/src/provide_missing_transactions.rs @@ -1,4 +1,4 @@ -use alloc::vec::Vec; +use alloc::{fmt, vec::Vec}; use binary_sv2::{binary_codec_sv2, Deserialize, Seq064K, Serialize, B016M}; use core::convert::TryInto; @@ -28,6 +28,16 @@ pub struct ProvideMissingTransactions<'decoder> { pub unknown_tx_position_list: Seq064K<'decoder, u16>, } +impl fmt::Display for ProvideMissingTransactions<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "ProvideMissingTransactions(request_id: {}, unknown_tx_position_list: {})", + self.request_id, self.unknown_tx_position_list + ) + } +} + /// Message used by JDC to accept [`ProvideMissingTransactions`] message and provide the full /// list of transactions in the order they were requested by [`ProvideMissingTransactions`]. #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] @@ -40,3 +50,12 @@ pub struct ProvideMissingTransactionsSuccess<'decoder> { /// List of full transactions as requested by [`ProvideMissingTransactions`]. pub transaction_list: Seq064K<'decoder, B016M<'decoder>>, } +impl fmt::Display for ProvideMissingTransactionsSuccess<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "ProvideMissingTransactionsSuccess(request_id: {}, transaction_list: {})", + self.request_id, self.transaction_list + ) + } +} diff --git a/protocols/v2/subprotocols/job-declaration/src/push_solution.rs b/protocols/v2/subprotocols/job-declaration/src/push_solution.rs index d4aa2a207b..7bdfa5a3cf 100644 --- a/protocols/v2/subprotocols/job-declaration/src/push_solution.rs +++ b/protocols/v2/subprotocols/job-declaration/src/push_solution.rs @@ -1,6 +1,6 @@ use alloc::vec::Vec; use binary_sv2::{binary_codec_sv2, Deserialize, Serialize, B032, U256}; -use core::convert::TryInto; +use core::{convert::TryInto, fmt}; /// Message used by JDC to push a solution to JDS as soon as it finds a new valid block. /// @@ -35,3 +35,18 @@ pub struct PushSolution<'decoder> { /// [`BIP320`]: https://en.bitcoin.it/wiki/BIP_0320 pub version: u32, } + +impl fmt::Display for PushSolution<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "PushSolution(extranonce: {}, prev_hash: {}, ntime: {}, nonce: 0x{:08x}, nbits: 0x{:08x}, version: 0x{:08x})", + self.extranonce, + self.prev_hash, + self.ntime, + self.nonce, + self.nbits, + self.version + ) + } +} diff --git a/protocols/v2/subprotocols/mining/Cargo.toml b/protocols/v2/subprotocols/mining/Cargo.toml index c3d4eacead..50bb4217f8 100644 --- a/protocols/v2/subprotocols/mining/Cargo.toml +++ b/protocols/v2/subprotocols/mining/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "mining_sv2" -version = "4.0.0" +version = "5.0.1" authors = ["The Stratum V2 Developers"] -edition = "2018" +edition = "2021" readme = "README.md" description = "SV2 mining protocol types" documentation = "https://docs.rs/mining_sv2" @@ -15,8 +15,7 @@ keywords = ["stratum", "mining", "bitcoin", "protocol"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -binary_sv2 = { path = "../../binary-sv2", version = "^3.0.0" } -stratum-common = { version = "^2.0.0", path = "../../../../common" } +binary_sv2 = { path = "../../binary-sv2", version = "^4.0.0" } [dev-dependencies] quickcheck = "1.0.3" diff --git a/protocols/v2/subprotocols/mining/src/close_channel.rs b/protocols/v2/subprotocols/mining/src/close_channel.rs index 83e7be3f96..a9f32c8280 100644 --- a/protocols/v2/subprotocols/mining/src/close_channel.rs +++ b/protocols/v2/subprotocols/mining/src/close_channel.rs @@ -1,4 +1,4 @@ -use alloc::vec::Vec; +use alloc::{fmt, vec::Vec}; use binary_sv2::{binary_codec_sv2, Deserialize, Serialize, Str0255}; use core::convert::TryInto; @@ -15,3 +15,14 @@ pub struct CloseChannel<'decoder> { /// Reason for closing the channel. pub reason_code: Str0255<'decoder>, } + +impl fmt::Display for CloseChannel<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "CloseChannel(channel_id: {}, reason_code: {})", + self.channel_id, + self.reason_code.as_utf8_or_hex() + ) + } +} diff --git a/protocols/v2/subprotocols/mining/src/lib.rs b/protocols/v2/subprotocols/mining/src/lib.rs index 09dde9e8c4..8620967b03 100644 --- a/protocols/v2/subprotocols/mining/src/lib.rs +++ b/protocols/v2/subprotocols/mining/src/lib.rs @@ -266,7 +266,7 @@ impl Extranonce { // B032 type is more used, this is why the output signature is not ExtendedExtranoncee the B032 // type is more used, this is why the output signature is not ExtendedExtranoncee #[allow(clippy::should_implement_trait)] - pub fn next(&mut self) -> Option { + pub fn next(&mut self) -> Option> { increment_bytes_be(&mut self.extranonce).ok()?; // below unwraps never panics Some(self.extranonce.clone().try_into().unwrap()) @@ -311,9 +311,9 @@ impl From<&mut ExtendedExtranonce> for Extranonce { /// the upstream. /// - `range_1` → `0..16`: The pool P increments these bytes to ensure each downstream gets a unique /// extended extranonce search space. The pool could optionally choose to set some fixed bytes as -/// `additional_coinbase_script_data` (smaller than 16 bytes), which are set on the beginning of -/// this range and will not be incremented. Usually, these bytes are used to add an identifier for -/// the pool. +/// `static_prefix` (no bigger than 2 bytes), which are set on the beginning of this range and +/// will not be incremented. These bytes are used to allow unique allocation for the pool's mining +/// server (if there are more than one). /// - `range_2` → `16..32`: These bytes are not changed by the pool but are changed by the pool's /// downstream. /// @@ -330,7 +330,7 @@ impl From<&mut ExtendedExtranonce> for Extranonce { /// /// # Examples /// -/// Basic usage without additional coinbase script data: +/// Basic usage without static prefix: /// /// ``` /// use mining_sv2::*; @@ -392,29 +392,29 @@ impl From<&mut ExtendedExtranonce> for Extranonce { /// assert_eq!(extranonce_to_send.to_vec(), expected_extranonce_to_send); /// ``` /// -/// Using additional coinbase script data: +/// Using static prefix: /// /// ``` /// use mining_sv2::*; /// use core::convert::TryInto; /// -/// // Create an extended extranonce with additional coinbase script data -/// let additional_data = vec![0x42, 0x43]; // Example additional coinbase script data +/// // Create an extended extranonce with a static prefix +/// let static_prefix = vec![0x42, 0x43]; // Example static prefix /// let mut pool_extended_extranonce = ExtendedExtranonce::new( /// 0..0, /// 0..7, /// 7..32, -/// Some(additional_data.clone()) +/// Some(static_prefix.clone()) /// ).unwrap(); /// -/// // When using additional coinbase script data, only bytes after the data are incremented +/// // When using static prefix, only bytes after the static prefix are incremented /// let new_extended_channel_extranonce = pool_extended_extranonce.next_prefix_extended(3).unwrap(); /// let expected_extranonce = vec![0x42, 0x43, 0, 0, 0, 0, 1]; /// assert_eq!(new_extended_channel_extranonce.clone().to_vec(), expected_extranonce); /// -/// // For standard channels, only range_2 is incremented while range_1 (including additional data) is preserved +/// // For standard channels, only range_2 is incremented while range_1 (including static prefix) is preserved /// let new_standard_channel_extranonce = pool_extended_extranonce.next_prefix_standard().unwrap(); -/// // Note that the additional data (0x42, 0x43) and the incremented bytes in range_1 are preserved +/// // Note that the static prefix (0x42, 0x43) and the incremented bytes in range_1 are preserved /// let expected_standard_extranonce = vec![0x42, 0x43, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]; /// assert_eq!(new_standard_channel_extranonce.to_vec(), expected_standard_extranonce); /// @@ -453,7 +453,7 @@ pub struct ExtendedExtranonce { range_0: core::ops::Range, range_1: core::ops::Range, range_2: core::ops::Range, - additional_coinbase_script_data: Option>, + static_prefix: Option>, } /// Error type for ExtendedExtranonce operations @@ -467,8 +467,8 @@ pub enum ExtendedExtranonceError { InvalidDownstreamLength, /// The extranonce bytes in range_1 are at maximum value and can't be incremented MaxValueReached, - /// The additional coinbase script data length is invalid - InvalidAdditionalCoinbaseScriptDataLength, + /// The static prefix length is invalid + InvalidStaticPrefixLength, } /// the trait PartialEq is implemented in such a way that only the relevant bytes are compared. @@ -501,7 +501,7 @@ impl ExtendedExtranonce { range_0: Range, range_1: Range, range_2: Range, - additional_coinbase_script_data: Option>, + static_prefix: Option>, ) -> Result { // Validate ranges if range_0.start != 0 @@ -513,9 +513,9 @@ impl ExtendedExtranonce { return Err(ExtendedExtranonceError::InvalidRanges); } - if let Some(additional_coinbase_script_data) = additional_coinbase_script_data.clone() { - if additional_coinbase_script_data.len() > range_1.end - range_1.start { - return Err(ExtendedExtranonceError::InvalidAdditionalCoinbaseScriptDataLength); + if let Some(static_prefix) = static_prefix.clone() { + if static_prefix.len() > core::cmp::min(2, range_1.end - range_1.start) { + return Err(ExtendedExtranonceError::InvalidStaticPrefixLength); } } @@ -525,9 +525,9 @@ impl ExtendedExtranonce { } let mut inner = vec![0; range_2.end]; - if let Some(additional_coinbase_script_data) = additional_coinbase_script_data.clone() { - inner[range_1.start..range_1.start + additional_coinbase_script_data.len()] - .copy_from_slice(&additional_coinbase_script_data); + if let Some(static_prefix) = static_prefix.clone() { + inner[range_1.start..range_1.start + static_prefix.len()] + .copy_from_slice(&static_prefix); } Ok(Self { @@ -535,7 +535,7 @@ impl ExtendedExtranonce { range_0, range_1, range_2, - additional_coinbase_script_data, + static_prefix, }) } @@ -566,7 +566,7 @@ impl ExtendedExtranonce { range_0, range_1, range_2, - additional_coinbase_script_data: None, + static_prefix: None, }) } @@ -622,7 +622,7 @@ impl ExtendedExtranonce { range_0, range_1, range_2, - additional_coinbase_script_data: None, + static_prefix: None, }) } @@ -662,15 +662,11 @@ impl ExtendedExtranonce { return Err(ExtendedExtranonceError::InvalidDownstreamLength); }; - // Determine the start position for extended_part based on additional_coinbase_script_data - // If additional_coinbase_script_data is Some, some bytes are meant to be fixed and not + // Determine the start position for extended_part based on static_prefix + // If static_prefix is Some, some bytes are meant to be fixed and not // incremented let extended_part_start = - if let Some(additional_data) = &self.additional_coinbase_script_data { - self.range_1.start + additional_data.len() - } else { - self.range_1.start - }; + self.range_1.start + self.static_prefix.as_ref().map_or(0, |p| p.len()); let extended_part = &mut self.inner[extended_part_start..self.range_1.end]; match increment_bytes_be(extended_part) { @@ -1236,84 +1232,78 @@ pub mod tests { } #[test] - fn test_extended_extranonce_with_additional_coinbase_script_data() { + fn test_extended_extranonce_with_static_prefix() { let range_0 = 0..0; let range_1 = 0..4; let range_2 = 4..8; - let additional_data = vec![0x42, 0x43, 0x44]; // Some fixed data + let static_prefix = vec![0x42, 0x43]; // Some fixed data - // Create an ExtendedExtranonce with additional_coinbase_script_data + // Create an ExtendedExtranonce with static prefix let extended = ExtendedExtranonce::new( range_0.clone(), range_1.clone(), range_2.clone(), - Some(additional_data.clone()), + Some(static_prefix.clone()), ) .unwrap(); - // Verify the additional data was stored - assert_eq!( - extended.additional_coinbase_script_data, - Some(additional_data.clone()) - ); + // Verify the static prefix was stored + assert_eq!(extended.static_prefix, Some(static_prefix.clone())); - // Verify the inner data contains the additional data + // Verify the inner data contains the static prefix assert_eq!( - extended.inner[range_1.start..range_1.start + additional_data.len()], - additional_data[..] + extended.inner[range_1.start..range_1.start + static_prefix.len()], + static_prefix[..] ); } #[test] - fn test_extended_extranonce_invalid_additional_coinbase_script_data_length() { + fn test_extended_extranonce_invalid_static_prefix_length() { let range_0 = 0..0; - let range_1 = 0..2; // Range length is 2 - let range_2 = 2..4; - let additional_data = vec![0x42, 0x43, 0x44]; // Length 3 > range_1 length + let range_1 = 0..4; + let range_2 = 4..8; + let static_prefix = vec![0x42, 0x43, 0x44]; // Length > 2 not allowed - // Create an ExtendedExtranonce with additional_coinbase_script_data that's too long - let result = ExtendedExtranonce::new(range_0, range_1, range_2, Some(additional_data)); + // Create an ExtendedExtranonce with static prefix that's too long + let result = ExtendedExtranonce::new(range_0, range_1, range_2, Some(static_prefix)); // Verify the correct error is returned assert!(result.is_err()); assert_eq!( result.unwrap_err(), - ExtendedExtranonceError::InvalidAdditionalCoinbaseScriptDataLength + ExtendedExtranonceError::InvalidStaticPrefixLength ); } #[test] - fn test_next_extended_with_additional_coinbase_script_data() { + fn test_next_extended_with_static_prefix() { let range_0 = 0..0; let range_1 = 0..4; let range_2 = 4..8; - let additional_data = vec![0x42, 0x43]; // Fixed data of length 2 + let static_prefix = vec![0x42, 0x43]; // Fixed data of length 2 - // Create an ExtendedExtranonce with additional_coinbase_script_data + // Create an ExtendedExtranonce with static prefix let mut extended = ExtendedExtranonce::new( range_0, range_1.clone(), range_2, - Some(additional_data.clone()), + Some(static_prefix.clone()), ) .unwrap(); // Call next_extended let result = extended.next_prefix_extended(3).unwrap(); - // Verify the result contains the additional data - assert_eq!( - result.extranonce[0..additional_data.len()], - additional_data[..] - ); + // Verify the result contains the static prefix + assert_eq!(result.extranonce[0..static_prefix.len()], static_prefix[..]); // Call next_extended again let result2 = extended.next_prefix_extended(3).unwrap(); // Verify the fixed part remains unchanged assert_eq!( - result2.extranonce[0..additional_data.len()], - additional_data[..] + result2.extranonce[0..static_prefix.len()], + static_prefix[..] ); // Verify the incremented part has changed @@ -1321,18 +1311,18 @@ pub mod tests { } #[test] - fn test_multiple_next_extended_with_additional_coinbase_script_data() { + fn test_multiple_next_extended_with_static_prefix() { let range_0 = 0..0; let range_1 = 0..4; let range_2 = 4..8; - let additional_data = vec![0x42, 0x43]; // Fixed data of length 2 + let static_prefix = vec![0x42, 0x43]; // Fixed data of length 2 - // Create an ExtendedExtranonce with additional_coinbase_script_data + // Create an ExtendedExtranonce with static prefix let mut extended = ExtendedExtranonce::new( range_0, range_1.clone(), range_2, - Some(additional_data.clone()), + Some(static_prefix.clone()), ) .unwrap(); @@ -1345,10 +1335,7 @@ pub mod tests { // Verify all results have the same fixed part for result in &results { - assert_eq!( - result.extranonce[0..additional_data.len()], - additional_data[..] - ); + assert_eq!(result.extranonce[0..static_prefix.len()], static_prefix[..]); } // Verify all results have different incremented parts diff --git a/protocols/v2/subprotocols/mining/src/new_mining_job.rs b/protocols/v2/subprotocols/mining/src/new_mining_job.rs index cfb39f5612..666c013726 100644 --- a/protocols/v2/subprotocols/mining/src/new_mining_job.rs +++ b/protocols/v2/subprotocols/mining/src/new_mining_job.rs @@ -1,6 +1,6 @@ use alloc::vec::Vec; use binary_sv2::{binary_codec_sv2, Deserialize, Seq0255, Serialize, Sv2Option, B064K, U256}; -use core::convert::TryInto; +use core::{convert::TryInto, fmt}; /// Message used by an upstream to provide an updated mining job to downstream. /// @@ -46,6 +46,16 @@ pub struct NewMiningJob<'decoder> { pub merkle_root: U256<'decoder>, } +impl fmt::Display for NewMiningJob<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "NewMiningJob(channel_id: {}, job_id: {}, min_ntime: {}, version: 0x{:08x}, merkle_root: {})", + self.channel_id, self.job_id, self.min_ntime, self.version, self.merkle_root + ) + } +} + impl NewMiningJob<'_> { pub fn is_future(&self) -> bool { self.min_ntime.clone().into_inner().is_none() @@ -111,6 +121,23 @@ pub struct NewExtendedMiningJob<'decoder> { pub coinbase_tx_suffix: B064K<'decoder>, } +impl fmt::Display for NewExtendedMiningJob<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "NewExtendedMiningJob(channel_id: {}, job_id: {}, min_ntime: {}, version: 0x{:08x}, version_rolling_allowed: {}, merkle_path: {}, coinbase_tx_prefix: {}, coinbase_tx_suffix: {})", + self.channel_id, + self.job_id, + self.min_ntime, + self.version, + self.version_rolling_allowed, + self.merkle_path, + self.coinbase_tx_prefix, + self.coinbase_tx_suffix + ) + } +} + impl NewExtendedMiningJob<'_> { pub fn is_future(&self) -> bool { self.min_ntime.clone().into_inner().is_none() diff --git a/protocols/v2/subprotocols/mining/src/open_channel.rs b/protocols/v2/subprotocols/mining/src/open_channel.rs index e5a34cd3c7..0af64e43f8 100644 --- a/protocols/v2/subprotocols/mining/src/open_channel.rs +++ b/protocols/v2/subprotocols/mining/src/open_channel.rs @@ -1,6 +1,6 @@ use alloc::{string::ToString, vec::Vec}; use binary_sv2::{binary_codec_sv2, Deserialize, Serialize, Str0255, U32AsRef, B032, U256}; -use core::convert::TryInto; +use core::{convert::TryInto, fmt}; /// Message used by a downstream to request opening a Standard Channel. /// /// Upon receiving `SetupConnectionSuccess` message, the downstream should open channel(s) on the @@ -36,6 +36,19 @@ pub struct OpenStandardMiningChannel<'decoder> { pub max_target: U256<'decoder>, } +impl fmt::Display for OpenStandardMiningChannel<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "OpenStandardMiningChannel(request_id: {}, user_identity: {}, nominal_hash_rate: {}, max_target: {})", + self.request_id, + self.user_identity.as_utf8_or_hex(), + self.nominal_hash_rate, + self.max_target + ) + } +} + impl OpenStandardMiningChannel<'_> { pub fn get_request_id_as_u32(&self) -> u32 { (&self.request_id).into() @@ -73,6 +86,20 @@ pub struct OpenStandardMiningChannelSuccess<'decoder> { pub group_channel_id: u32, } +impl fmt::Display for OpenStandardMiningChannelSuccess<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "OpenStandardMiningChannelSuccess(request_id: {}, channel_id: {}, target: {}, extranonce_prefix: {}, group_channel_id: {})", + self.request_id, + self.channel_id, + self.target, + self.extranonce_prefix, + self.group_channel_id + ) + } +} + impl OpenStandardMiningChannelSuccess<'_> { pub fn get_request_id_as_u32(&self) -> u32 { (&self.request_id).into() @@ -128,6 +155,20 @@ pub struct OpenExtendedMiningChannel<'decoder> { pub min_extranonce_size: u16, } +impl fmt::Display for OpenExtendedMiningChannel<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "OpenExtendedMiningChannel(request_id: {}, user_identity: {}, nominal_hash_rate: {}, max_target: {}, min_extranonce_size: {})", + self.request_id, + self.user_identity.as_utf8_or_hex(), + self.nominal_hash_rate, + self.max_target, + self.min_extranonce_size + ) + } +} + impl OpenExtendedMiningChannel<'_> { pub fn get_request_id_as_u32(&self) -> u32 { self.request_id @@ -154,6 +195,20 @@ pub struct OpenExtendedMiningChannelSuccess<'decoder> { pub extranonce_prefix: B032<'decoder>, } +impl fmt::Display for OpenExtendedMiningChannelSuccess<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "OpenExtendedMiningChannelSuccess(request_id: {}, channel_id: {}, target: {}, extranonce_size: {}, extranonce_prefix: {})", + self.request_id, + self.channel_id, + self.target, + self.extranonce_size, + self.extranonce_prefix + ) + } +} + /// Message used by upstream to reject [`OpenExtendedMiningChannel`] or /// [`OpenStandardMiningchannel`] request from downstream. #[derive(Serialize, Deserialize, Debug, Clone)] @@ -172,6 +227,17 @@ pub struct OpenMiningChannelError<'decoder> { pub error_code: Str0255<'decoder>, } +impl fmt::Display for OpenMiningChannelError<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "OpenMiningChannelError(request_id: {}, error_code: {})", + self.request_id, + self.error_code.as_utf8_or_hex() + ) + } +} + impl OpenMiningChannelError<'_> { pub fn new_max_target_out_of_range(request_id: u32) -> Self { Self { diff --git a/protocols/v2/subprotocols/mining/src/set_custom_mining_job.rs b/protocols/v2/subprotocols/mining/src/set_custom_mining_job.rs index 5b03b577cf..f098dc1284 100644 --- a/protocols/v2/subprotocols/mining/src/set_custom_mining_job.rs +++ b/protocols/v2/subprotocols/mining/src/set_custom_mining_job.rs @@ -1,4 +1,4 @@ -use alloc::vec::Vec; +use alloc::{fmt, vec::Vec}; use binary_sv2::{binary_codec_sv2, Deserialize, Seq0255, Serialize, Str0255, B0255, B064K, U256}; use core::convert::TryInto; @@ -44,9 +44,6 @@ pub struct SetCustomMiningJob<'decoder> { pub coinbase_prefix: B0255<'decoder>, /// The coinbase transaction input’s nSequence field. pub coinbase_tx_input_n_sequence: u32, - /// The value, in satoshis, available for spending in coinbase outputs added by the client. - /// Includes both transaction fees and block subsidy. - pub coinbase_tx_value_remaining: u64, /// All the outputs that will be included in the coinbase txs pub coinbase_tx_outputs: B064K<'decoder>, /// The `locktime` field in the coinbase transaction. @@ -55,6 +52,26 @@ pub struct SetCustomMiningJob<'decoder> { pub merkle_path: Seq0255<'decoder, U256<'decoder>>, } +impl fmt::Display for SetCustomMiningJob<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "SetCustomMiningJob(channel_id={}, request_id={}, token={}, version=0x{:08x}, prev_hash={}, min_ntime={}, nbits=0x{:08x}, coinbase_tx_version=0x{:08x}, coinbase_prefix={}, coinbase_tx_input_n_sequence=0x{:08x}, coinbase_tx_outputs={}, coinbase_tx_locktime={}, merkle_path={})", + self.channel_id, + self.request_id, + self.token.as_hex(), + self.version, + self.prev_hash, + self.min_ntime, + self.nbits, + self.coinbase_tx_version, + self.coinbase_prefix.as_hex(), + self.coinbase_tx_input_n_sequence, + self.coinbase_tx_outputs, + self.coinbase_tx_locktime, + self.merkle_path + ) + } +} + /// Message used by upstream to accept [`SetCustomMiningJob`] request. /// /// Upon receiving this message, downstream can start submitting shares for this job immediately (by @@ -69,6 +86,16 @@ pub struct SetCustomMiningJobSuccess { pub job_id: u32, } +impl fmt::Display for SetCustomMiningJobSuccess { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "SetCustomMiningJobSuccess(channel_id={}, request_id={}, job_id={})", + self.channel_id, self.request_id, self.job_id + ) + } +} + /// Message used by upstream to reject [`SetCustomMiningJob`] request. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct SetCustomMiningJobError<'decoder> { @@ -84,3 +111,15 @@ pub struct SetCustomMiningJobError<'decoder> { /// - invalid-job-param-value-{field_name} pub error_code: Str0255<'decoder>, } + +impl fmt::Display for SetCustomMiningJobError<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "SetCustomMiningJobError(channel_id={}, request_id={}, error_code={})", + self.channel_id, + self.request_id, + self.error_code.as_utf8_or_hex() + ) + } +} diff --git a/protocols/v2/subprotocols/mining/src/set_extranonce_prefix.rs b/protocols/v2/subprotocols/mining/src/set_extranonce_prefix.rs index 8a78793056..e10f617468 100644 --- a/protocols/v2/subprotocols/mining/src/set_extranonce_prefix.rs +++ b/protocols/v2/subprotocols/mining/src/set_extranonce_prefix.rs @@ -1,4 +1,4 @@ -use alloc::vec::Vec; +use alloc::{fmt, vec::Vec}; use binary_sv2::{binary_codec_sv2, Deserialize, Serialize, B032}; @@ -18,3 +18,13 @@ pub struct SetExtranoncePrefix<'decoder> { /// New extranonce prefix. pub extranonce_prefix: B032<'decoder>, } + +impl fmt::Display for SetExtranoncePrefix<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "SetExtranoncePrefix(channel_id={}, extranonce_prefix={})", + self.channel_id, self.extranonce_prefix + ) + } +} diff --git a/protocols/v2/subprotocols/mining/src/set_group_channel.rs b/protocols/v2/subprotocols/mining/src/set_group_channel.rs index 6b5340a6d2..2662150cd5 100644 --- a/protocols/v2/subprotocols/mining/src/set_group_channel.rs +++ b/protocols/v2/subprotocols/mining/src/set_group_channel.rs @@ -1,4 +1,4 @@ -use alloc::vec::Vec; +use alloc::{fmt, vec::Vec}; use binary_sv2::{binary_codec_sv2, Deserialize, Seq064K, Serialize}; use core::convert::TryInto; @@ -24,3 +24,13 @@ pub struct SetGroupChannel<'decoder> { /// A sequence of opened standard channel IDs, for which the group channel is being redefined. pub channel_ids: Seq064K<'decoder, u32>, } + +impl fmt::Display for SetGroupChannel<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "SetGroupChannel(group_channel_id={}, channel_ids={})", + self.group_channel_id, self.channel_ids + ) + } +} diff --git a/protocols/v2/subprotocols/mining/src/set_new_prev_hash.rs b/protocols/v2/subprotocols/mining/src/set_new_prev_hash.rs index e09f20b331..200ea4c9fb 100644 --- a/protocols/v2/subprotocols/mining/src/set_new_prev_hash.rs +++ b/protocols/v2/subprotocols/mining/src/set_new_prev_hash.rs @@ -1,6 +1,6 @@ use alloc::vec::Vec; use binary_sv2::{binary_codec_sv2, Deserialize, Serialize, U256}; -use core::convert::TryInto; +use core::{convert::TryInto, fmt}; /// Message used by upstream to share or distribute the latest block hash. /// @@ -27,3 +27,13 @@ pub struct SetNewPrevHash<'decoder> { /// Block header field. pub nbits: u32, } + +impl fmt::Display for SetNewPrevHash<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "SetNewPrevHash(channel_id={}, job_id={}, prev_hash={}, min_ntime={}, nbits=0x{:08x})", + self.channel_id, self.job_id, self.prev_hash, self.min_ntime, self.nbits + ) + } +} diff --git a/protocols/v2/subprotocols/mining/src/set_target.rs b/protocols/v2/subprotocols/mining/src/set_target.rs index 87e5db952b..62e5b07ed7 100644 --- a/protocols/v2/subprotocols/mining/src/set_target.rs +++ b/protocols/v2/subprotocols/mining/src/set_target.rs @@ -1,4 +1,4 @@ -use alloc::vec::Vec; +use alloc::{fmt, vec::Vec}; use binary_sv2::{binary_codec_sv2, Deserialize, Serialize, U256}; use core::convert::TryInto; @@ -21,3 +21,13 @@ pub struct SetTarget<'decoder> { /// Maximum value of produced hash that will be accepted by a upstream to accept shares. pub maximum_target: U256<'decoder>, } + +impl fmt::Display for SetTarget<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "SetTarget(channel_id={}, maximum_target={})", + self.channel_id, self.maximum_target + ) + } +} diff --git a/protocols/v2/subprotocols/mining/src/submit_shares.rs b/protocols/v2/subprotocols/mining/src/submit_shares.rs index 5ed2354f8e..d5bd4f1f02 100644 --- a/protocols/v2/subprotocols/mining/src/submit_shares.rs +++ b/protocols/v2/subprotocols/mining/src/submit_shares.rs @@ -1,4 +1,4 @@ -use alloc::vec::Vec; +use alloc::{fmt, vec::Vec}; use binary_sv2::{binary_codec_sv2, Deserialize, Serialize, Str0255, B032}; use core::convert::TryInto; @@ -26,6 +26,16 @@ pub struct SubmitSharesStandard { pub version: u32, } +impl fmt::Display for SubmitSharesStandard { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "SubmitSharesStandard(channel_id={}, sequence_number={}, job_id={}, nonce=0x{:08x}, ntime={}, version=0x{:08x})", + self.channel_id, self.sequence_number, self.job_id, self.nonce, self.ntime, self.version + ) + } +} + /// Message used by downstream to send result of its hashing work to an upstream. /// /// The message is the same as [`SubmitShares`], but with an additional field, @@ -62,6 +72,16 @@ pub struct SubmitSharesExtended<'decoder> { pub extranonce: B032<'decoder>, } +impl fmt::Display for SubmitSharesExtended<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "SubmitSharesExtended(channel_id={}, sequence_number={}, job_id={}, nonce=0x{:08x}, ntime={}, version=0x{:08x}, extranonce={})", + self.channel_id, self.sequence_number, self.job_id, self.nonce, self.ntime, self.version, self.extranonce + ) + } +} + /// Message used by upstream to accept [`SubmitSharesStandard`] or [`SubmitSharesExtended`]. /// /// Because it is a common case that shares submission is successful, this response can be provided @@ -82,6 +102,16 @@ pub struct SubmitSharesSuccess { pub new_shares_sum: u64, } +impl fmt::Display for SubmitSharesSuccess { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "SubmitSharesSuccess(channel_id={}, last_sequence_number={}, new_submits_accepted_count={}, new_shares_sum={})", + self.channel_id, self.last_sequence_number, self.new_submits_accepted_count, self.new_shares_sum + ) + } +} + /// Message used by upstream to reject [`SubmitSharesStandard`] or [`SubmitSharesExtended`]. /// /// In case the upstream is not able to immediately validate the submission, the error is sent as @@ -104,6 +134,18 @@ pub struct SubmitSharesError<'decoder> { pub error_code: Str0255<'decoder>, } +impl fmt::Display for SubmitSharesError<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "SubmitSharesError(channel_id={}, sequence_number={}, error_code={})", + self.channel_id, + self.sequence_number, + self.error_code.as_utf8_or_hex() + ) + } +} + impl SubmitSharesError<'_> { pub fn invalid_channel_error_code() -> &'static str { "invalid-channel-id" diff --git a/protocols/v2/subprotocols/mining/src/update_channel.rs b/protocols/v2/subprotocols/mining/src/update_channel.rs index 4ca06831b6..6ba0d1d237 100644 --- a/protocols/v2/subprotocols/mining/src/update_channel.rs +++ b/protocols/v2/subprotocols/mining/src/update_channel.rs @@ -1,4 +1,4 @@ -use alloc::vec::Vec; +use alloc::{fmt, vec::Vec}; use binary_sv2::{binary_codec_sv2, Deserialize, Serialize, Str0255, U256}; use core::convert::TryInto; @@ -34,6 +34,16 @@ pub struct UpdateChannel<'decoder> { pub maximum_target: U256<'decoder>, } +impl fmt::Display for UpdateChannel<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "â›ï¸ UpdateChannel(channel_id={}, nominal_hash_rate={}, maximum_target={})", + self.channel_id, self.nominal_hash_rate, self.maximum_target + ) + } +} + /// Message used by upstream to notify downstream about an error in the [`UpdateChannel`] message. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct UpdateChannelError<'decoder> { @@ -46,3 +56,14 @@ pub struct UpdateChannelError<'decoder> { /// - invalid-channel-id pub error_code: Str0255<'decoder>, } + +impl fmt::Display for UpdateChannelError<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "UpdateChannelError(channel_id={}, error_code={})", + self.channel_id, + self.error_code.as_utf8_or_hex() + ) + } +} diff --git a/protocols/v2/subprotocols/template-distribution/Cargo.toml b/protocols/v2/subprotocols/template-distribution/Cargo.toml index ef8db761f9..9fc1b057b3 100644 --- a/protocols/v2/subprotocols/template-distribution/Cargo.toml +++ b/protocols/v2/subprotocols/template-distribution/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "template_distribution_sv2" -version = "3.1.0" +version = "4.0.1" authors = ["The Stratum V2 Developers"] -edition = "2018" +edition = "2021" readme = "README.md" description = "Sv2 template distribution subprotocol" documentation = "https://docs.rs/template_distribution_sv2" @@ -14,8 +14,7 @@ keywords = ["stratum", "mining", "bitcoin", "protocol"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -binary_sv2 = { path = "../../binary-sv2", version = "^3.0.0" } -stratum-common = { version = "^2.0.0", path = "../../../../common" } +binary_sv2 = { path = "../../binary-sv2", version = "^4.0.0" } quickcheck = { version = "1.0.3", optional=true } quickcheck_macros = { version = "1", optional=true } diff --git a/protocols/v2/subprotocols/template-distribution/src/coinbase_output_constraints.rs b/protocols/v2/subprotocols/template-distribution/src/coinbase_output_constraints.rs index 3824bcb0b0..39de20d7ce 100644 --- a/protocols/v2/subprotocols/template-distribution/src/coinbase_output_constraints.rs +++ b/protocols/v2/subprotocols/template-distribution/src/coinbase_output_constraints.rs @@ -1,4 +1,4 @@ -use alloc::vec::Vec; +use alloc::{fmt, vec::Vec}; use binary_sv2::{binary_codec_sv2, Deserialize, Serialize}; use core::convert::TryInto; @@ -23,10 +23,21 @@ use core::convert::TryInto; /// /// [`NewTemplate`]: crate::NewTemplate #[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq)] -#[repr(C)] + pub struct CoinbaseOutputConstraints { /// Additional serialized bytes needed in coinbase transaction outputs. pub coinbase_output_max_additional_size: u32, /// Additional sigops needed in coinbase transaction outputs. pub coinbase_output_max_additional_sigops: u16, } + +impl fmt::Display for CoinbaseOutputConstraints { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "CoinbaseOutputConstraints(coinbase_output_max_additional_size: {}, coinbase_output_max_additional_sigops: {})", + self.coinbase_output_max_additional_size, + self.coinbase_output_max_additional_sigops + ) + } +} diff --git a/protocols/v2/subprotocols/template-distribution/src/lib.rs b/protocols/v2/subprotocols/template-distribution/src/lib.rs index c43bd9cf85..e9a37e1e60 100644 --- a/protocols/v2/subprotocols/template-distribution/src/lib.rs +++ b/protocols/v2/subprotocols/template-distribution/src/lib.rs @@ -31,13 +31,12 @@ mod set_new_prev_hash; mod submit_solution; pub use coinbase_output_constraints::CoinbaseOutputConstraints; -pub use new_template::{CNewTemplate, NewTemplate}; +pub use new_template::NewTemplate; pub use request_transaction_data::{ - CRequestTransactionDataError, CRequestTransactionDataSuccess, RequestTransactionData, - RequestTransactionDataError, RequestTransactionDataSuccess, + RequestTransactionData, RequestTransactionDataError, RequestTransactionDataSuccess, }; -pub use set_new_prev_hash::{CSetNewPrevHash, SetNewPrevHash}; -pub use submit_solution::{CSubmitSolution, SubmitSolution}; +pub use set_new_prev_hash::SetNewPrevHash; +pub use submit_solution::SubmitSolution; // Template Distribution Protocol message types. pub const MESSAGE_TYPE_COINBASE_OUTPUT_CONSTRAINTS: u8 = 0x70; @@ -57,14 +56,6 @@ pub const CHANNEL_BIT_REQUEST_TRANSACTION_DATA_SUCCESS: bool = false; pub const CHANNEL_BIT_REQUEST_TRANSACTION_DATA_ERROR: bool = false; pub const CHANNEL_BIT_SUBMIT_SOLUTION: bool = false; -/// Exports the [`CoinbaseOutputConstraints`] struct to C. -#[no_mangle] -pub extern "C" fn _c_export_coinbase_out(_a: CoinbaseOutputConstraints) {} - -/// Exports the [`RequestTransactionData`] struct to C. -#[no_mangle] -pub extern "C" fn _c_export_req_tx_data(_a: RequestTransactionData) {} - #[cfg(feature = "prop_test")] impl NewTemplate<'static> { pub fn from_gen(g: &mut Gen) -> Self { diff --git a/protocols/v2/subprotocols/template-distribution/src/new_template.rs b/protocols/v2/subprotocols/template-distribution/src/new_template.rs index 650d5e9569..b016ded7cd 100644 --- a/protocols/v2/subprotocols/template-distribution/src/new_template.rs +++ b/protocols/v2/subprotocols/template-distribution/src/new_template.rs @@ -1,8 +1,5 @@ -use alloc::vec::Vec; -use binary_sv2::{ - binary_codec_sv2::{self, free_vec, free_vec_2, CVec, CVec2}, - Deserialize, Error, Seq0255, Serialize, B0255, B064K, U256, -}; +use alloc::{fmt, vec::Vec}; +use binary_sv2::{binary_codec_sv2, Deserialize, Seq0255, Serialize, B0255, B064K, U256}; use core::convert::TryInto; /// Message used by an upstream(Template Provider) to provide a new template for downstream to mine @@ -52,82 +49,26 @@ pub struct NewTemplate<'decoder> { pub merkle_path: Seq0255<'decoder, U256<'decoder>>, } -/// C representation of [`NewTemplate`]. -#[repr(C)] -pub struct CNewTemplate { - template_id: u64, - future_template: bool, - version: u32, - coinbase_tx_version: u32, - coinbase_prefix: CVec, - coinbase_tx_input_sequence: u32, - coinbase_tx_value_remaining: u64, - coinbase_tx_outputs_count: u32, - coinbase_tx_outputs: CVec, - coinbase_tx_locktime: u32, - merkle_path: CVec2, -} - -/// Drops the [`CNewTemplate`] object. -#[no_mangle] -pub extern "C" fn free_new_template(s: CNewTemplate) { - drop(s) -} - -impl Drop for CNewTemplate { - fn drop(&mut self) { - free_vec(&mut self.coinbase_prefix); - free_vec(&mut self.coinbase_tx_outputs); - free_vec_2(&mut self.merkle_path); - } -} - -impl<'a> From> for CNewTemplate { - fn from(v: NewTemplate<'a>) -> Self { - Self { - template_id: v.template_id, - future_template: v.future_template, - version: v.version, - coinbase_tx_version: v.coinbase_tx_version, - coinbase_prefix: v.coinbase_prefix.into(), - coinbase_tx_input_sequence: v.coinbase_tx_input_sequence, - coinbase_tx_value_remaining: v.coinbase_tx_value_remaining, - coinbase_tx_outputs_count: v.coinbase_tx_outputs_count, - coinbase_tx_outputs: v.coinbase_tx_outputs.into(), - coinbase_tx_locktime: v.coinbase_tx_locktime, - merkle_path: v.merkle_path.into(), - } - } -} - -impl<'a> CNewTemplate { - /// Converts from C to Rust representation. - #[allow(clippy::wrong_self_convention)] - pub fn to_rust_rep_mut(&'a mut self) -> Result, Error> { - let coinbase_prefix: B0255 = self.coinbase_prefix.as_mut_slice().try_into()?; - - let merkle_path_ = self.merkle_path.as_mut_slice(); - let mut merkle_path: Vec = Vec::new(); - for cvec in merkle_path_ { - merkle_path.push(cvec.as_mut_slice().try_into()?); - } - let merkle_path = Seq0255::new(merkle_path)?; - - let coinbase_tx_outputs = self.coinbase_tx_outputs.as_mut_slice().try_into()?; - - Ok(NewTemplate { - template_id: self.template_id, - future_template: self.future_template, - version: self.version, - coinbase_tx_version: self.coinbase_tx_version, - coinbase_prefix, - coinbase_tx_input_sequence: self.coinbase_tx_input_sequence, - coinbase_tx_value_remaining: self.coinbase_tx_value_remaining, - coinbase_tx_outputs_count: self.coinbase_tx_outputs_count, - coinbase_tx_outputs, - coinbase_tx_locktime: self.coinbase_tx_locktime, - merkle_path, - }) +impl fmt::Display for NewTemplate<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "NewTemplate(template_id: {}, future_template: {}, version: 0x{:08x}, coinbase_tx_version: 0x{:08x}, \ + coinbase_prefix: {}, coinbase_tx_input_sequence: 0x{:08x}, coinbase_tx_value_remaining: {}, \ + coinbase_tx_outputs_count: {}, coinbase_tx_outputs: {}, coinbase_tx_locktime: {}, \ + merkle_path: {})", + self.template_id, + self.future_template, + self.version, + self.coinbase_tx_version, + self.coinbase_prefix.as_hex(), + self.coinbase_tx_input_sequence, + self.coinbase_tx_value_remaining, + self.coinbase_tx_outputs_count, + self.coinbase_tx_outputs, + self.coinbase_tx_locktime, + self.merkle_path + ) } } diff --git a/protocols/v2/subprotocols/template-distribution/src/request_transaction_data.rs b/protocols/v2/subprotocols/template-distribution/src/request_transaction_data.rs index ffcaf9b325..e775800e68 100644 --- a/protocols/v2/subprotocols/template-distribution/src/request_transaction_data.rs +++ b/protocols/v2/subprotocols/template-distribution/src/request_transaction_data.rs @@ -1,8 +1,5 @@ -use alloc::vec::Vec; -use binary_sv2::{ - binary_codec_sv2::{self, free_vec, free_vec_2, CVec, CVec2}, - Deserialize, Error, Seq064K, Serialize, Str0255, B016M, B064K, -}; +use alloc::{fmt, vec::Vec}; +use binary_sv2::{binary_codec_sv2, Deserialize, Seq064K, Serialize, Str0255, B016M, B064K}; use core::convert::TryInto; /// Message used by a downstream to request data about all transactions in a block template. @@ -11,7 +8,7 @@ use core::convert::TryInto; /// /// Note that the coinbase transaction is excluded from this data. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Copy)] -#[repr(C)] + pub struct RequestTransactionData { /// Identifier of the template that the downstream node is requesting transaction data for. /// @@ -19,6 +16,16 @@ pub struct RequestTransactionData { pub template_id: u64, } +impl fmt::Display for RequestTransactionData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "RequestTransactionData(template_id: {})", + self.template_id + ) + } +} + /// Message used by an upstream(Template Provider) to respond successfully to a /// [`RequestTransactionData`] message. /// @@ -62,53 +69,13 @@ pub struct RequestTransactionDataSuccess<'decoder> { pub transaction_list: Seq064K<'decoder, B016M<'decoder>>, } -/// C representation of [`RequestTransactionDataSuccess`]. -#[repr(C)] -pub struct CRequestTransactionDataSuccess { - template_id: u64, - excess_data: CVec, - transaction_list: CVec2, -} - -impl<'a> CRequestTransactionDataSuccess { - /// Converts C struct to Rust struct. - #[allow(clippy::wrong_self_convention)] - pub fn to_rust_rep_mut(&'a mut self) -> Result, Error> { - let excess_data: B064K = self.excess_data.as_mut_slice().try_into()?; - let transaction_list_ = self.transaction_list.as_mut_slice(); - let mut transaction_list: Vec = Vec::new(); - for cvec in transaction_list_ { - transaction_list.push(cvec.as_mut_slice().try_into()?); - } - let transaction_list = Seq064K::new(transaction_list)?; - Ok(RequestTransactionDataSuccess { - template_id: self.template_id, - excess_data, - transaction_list, - }) - } -} - -/// Drops the CRequestTransactionDataSuccess object. -#[no_mangle] -pub extern "C" fn free_request_tx_data_success(s: CRequestTransactionDataSuccess) { - drop(s) -} - -impl Drop for CRequestTransactionDataSuccess { - fn drop(&mut self) { - free_vec(&mut self.excess_data); - free_vec_2(&mut self.transaction_list); - } -} - -impl<'a> From> for CRequestTransactionDataSuccess { - fn from(v: RequestTransactionDataSuccess<'a>) -> Self { - Self { - template_id: v.template_id, - excess_data: v.excess_data.into(), - transaction_list: v.transaction_list.into(), - } +impl fmt::Display for RequestTransactionDataSuccess<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "RequestTransactionDataSuccess(template_id: {}, excess_data: {}, transaction_list: {})", + self.template_id, self.excess_data, self.transaction_list + ) } } @@ -125,42 +92,13 @@ pub struct RequestTransactionDataError<'decoder> { pub error_code: Str0255<'decoder>, } -/// C representation of [`RequestTransactionDataError`]. -#[repr(C)] -pub struct CRequestTransactionDataError { - template_id: u64, - error_code: CVec, -} - -impl<'a> CRequestTransactionDataError { - /// Converts C struct to Rust struct. - #[allow(clippy::wrong_self_convention)] - pub fn to_rust_rep_mut(&'a mut self) -> Result, Error> { - let error_code: Str0255 = self.error_code.as_mut_slice().try_into()?; - Ok(RequestTransactionDataError { - template_id: self.template_id, - error_code, - }) - } -} - -/// Drops the CRequestTransactionDataError object. -#[no_mangle] -pub extern "C" fn free_request_tx_data_error(s: CRequestTransactionDataError) { - drop(s) -} - -impl Drop for CRequestTransactionDataError { - fn drop(&mut self) { - free_vec(&mut self.error_code); - } -} - -impl<'a> From> for CRequestTransactionDataError { - fn from(v: RequestTransactionDataError<'a>) -> Self { - Self { - template_id: v.template_id, - error_code: v.error_code.into(), - } +impl fmt::Display for RequestTransactionDataError<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "RequestTransactionDataError(template_id: {}, error_code: {})", + self.template_id, + self.error_code.as_utf8_or_hex() + ) } } diff --git a/protocols/v2/subprotocols/template-distribution/src/set_new_prev_hash.rs b/protocols/v2/subprotocols/template-distribution/src/set_new_prev_hash.rs index b3ee50f433..48fc811096 100644 --- a/protocols/v2/subprotocols/template-distribution/src/set_new_prev_hash.rs +++ b/protocols/v2/subprotocols/template-distribution/src/set_new_prev_hash.rs @@ -1,9 +1,6 @@ use alloc::vec::Vec; -use binary_sv2::{ - binary_codec_sv2::{self, free_vec, CVec}, - Deserialize, Error, Serialize, U256, -}; -use core::convert::TryInto; +use binary_sv2::{binary_codec_sv2, Deserialize, Serialize, U256}; +use core::{convert::TryInto, fmt}; /// Message used by an upstream(Template Provider) to indicate the latest block header hash /// to mine on. @@ -33,53 +30,16 @@ pub struct SetNewPrevHash<'decoder> { pub target: U256<'decoder>, } -/// C representation of [`SetNewPrevHash`]. -#[repr(C)] -pub struct CSetNewPrevHash { - template_id: u64, - prev_hash: CVec, - header_timestamp: u32, - n_bits: u32, - target: CVec, -} - -impl<'a> CSetNewPrevHash { - /// Converts CSetNewPrevHash(C representation) to SetNewPrevHash(Rust representation). - #[allow(clippy::wrong_self_convention)] - pub fn to_rust_rep_mut(&'a mut self) -> Result, Error> { - let prev_hash: U256 = self.prev_hash.as_mut_slice().try_into()?; - let target: U256 = self.target.as_mut_slice().try_into()?; - - Ok(SetNewPrevHash { - template_id: self.template_id, - prev_hash, - header_timestamp: self.header_timestamp, - n_bits: self.n_bits, - target, - }) - } -} - -/// Drops the CSetNewPrevHash object. -#[no_mangle] -pub extern "C" fn free_set_new_prev_hash(s: CSetNewPrevHash) { - drop(s) -} - -impl Drop for CSetNewPrevHash { - fn drop(&mut self) { - free_vec(&mut self.target); - } -} - -impl<'a> From> for CSetNewPrevHash { - fn from(v: SetNewPrevHash<'a>) -> Self { - Self { - template_id: v.template_id, - prev_hash: v.prev_hash.into(), - header_timestamp: v.header_timestamp, - n_bits: v.n_bits, - target: v.target.into(), - } +impl fmt::Display for SetNewPrevHash<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "SetNewPrevHash {{ template_id: {}, prev_hash: {}, header_timestamp: {}, n_bits: 0x{:08x}, target: {} }}", + self.template_id, + self.prev_hash, + self.header_timestamp, + self.n_bits, + self.target + ) } } diff --git a/protocols/v2/subprotocols/template-distribution/src/submit_solution.rs b/protocols/v2/subprotocols/template-distribution/src/submit_solution.rs index fe6ad63884..255807708d 100644 --- a/protocols/v2/subprotocols/template-distribution/src/submit_solution.rs +++ b/protocols/v2/subprotocols/template-distribution/src/submit_solution.rs @@ -1,8 +1,5 @@ -use alloc::vec::Vec; -use binary_sv2::{ - binary_codec_sv2::{self, free_vec, CVec}, - Deserialize, Error, Serialize, B064K, -}; +use alloc::{fmt, vec::Vec}; +use binary_sv2::{binary_codec_sv2, Deserialize, Serialize, B064K}; use core::convert::TryInto; /// Message used by a downstream to submit a successful solution to a previously provided template. @@ -39,52 +36,16 @@ pub struct SubmitSolution<'decoder> { pub coinbase_tx: B064K<'decoder>, } -/// C representation of [`SubmitSolution`]. -#[repr(C)] -pub struct CSubmitSolution { - template_id: u64, - version: u32, - header_timestamp: u32, - header_nonce: u32, - coinbase_tx: CVec, -} - -impl<'a> CSubmitSolution { - /// Converts CSubmitSolution(C representation) to SubmitSolution(Rust representation). - #[allow(clippy::wrong_self_convention)] - pub fn to_rust_rep_mut(&'a mut self) -> Result, Error> { - let coinbase_tx: B064K = self.coinbase_tx.as_mut_slice().try_into()?; - - Ok(SubmitSolution { - template_id: self.template_id, - version: self.version, - header_timestamp: self.header_timestamp, - header_nonce: self.header_nonce, - coinbase_tx, - }) - } -} - -/// Drops the CSubmitSolution object. -#[no_mangle] -pub extern "C" fn free_submit_solution(s: CSubmitSolution) { - drop(s) -} - -impl Drop for CSubmitSolution { - fn drop(&mut self) { - free_vec(&mut self.coinbase_tx); - } -} - -impl<'a> From> for CSubmitSolution { - fn from(v: SubmitSolution<'a>) -> Self { - Self { - template_id: v.template_id, - version: v.version, - header_timestamp: v.header_timestamp, - header_nonce: v.header_nonce, - coinbase_tx: v.coinbase_tx.into(), - } +impl fmt::Display for SubmitSolution<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "SubmitSolution {{ template_id: {}, version: 0x{:08x}, header_timestamp: {}, header_nonce: 0x{:08x}, coinbase_tx: {} }}", + self.template_id, + self.version, + self.header_timestamp, + self.header_nonce, + self.coinbase_tx + ) } } diff --git a/protocols/v2/sv2-ffi/Cargo.toml b/protocols/v2/sv2-ffi/Cargo.toml deleted file mode 100644 index eae8e9bc49..0000000000 --- a/protocols/v2/sv2-ffi/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "sv2_ffi" -version = "2.1.0" -authors = ["The Stratum V2 Developers"] -edition = "2018" -description = "SV2 FFI" -documentation = "https://github.com/stratum-mining/stratum" -license = "MIT OR Apache-2.0" -repository = "https://github.com/stratum-mining/stratum" -homepage = "https://stratumprotocol.org" -keywords = ["stratum", "mining", "bitcoin", "protocol"] - -[lib] -crate-type = ["staticlib"] - -[dependencies] -codec_sv2 = { path = "../codec-sv2", version = "^2.0.0" } -stratum-common = { version = "^2.0.0", path = "../../../common" } -binary_sv2 = { path = "../binary-sv2", version = "^3.0.0" } -common_messages_sv2 = { path = "../subprotocols/common-messages", version = "^5.0.0" } -template_distribution_sv2 = { path = "../subprotocols/template-distribution", version = "^3.0.0" } - -[dev-dependencies] -quickcheck = "1.0.3" -quickcheck_macros = "1" - -[features] -prop_test = ["binary_sv2/prop_test", "common_messages_sv2/prop_test", "template_distribution_sv2/prop_test"] diff --git a/protocols/v2/sv2-ffi/src/lib.rs b/protocols/v2/sv2-ffi/src/lib.rs deleted file mode 100644 index 271a587ba4..0000000000 --- a/protocols/v2/sv2-ffi/src/lib.rs +++ /dev/null @@ -1,1232 +0,0 @@ -use std::{ - fmt, - fmt::{Display, Formatter}, -}; - -use codec_sv2::{Encoder, StandardDecoder, StandardSv2Frame}; -use common_messages_sv2::*; -use template_distribution_sv2::*; - -use binary_sv2::{ - binary_codec_sv2::CVec, - decodable::{DecodableField, FieldMarker}, - encodable::EncodableField, - from_bytes, Deserialize, Error, -}; - -use core::convert::{TryFrom, TryInto}; - -#[derive(Clone, Debug)] -pub enum Sv2Message<'a> { - CoinbaseOutputConstraints(CoinbaseOutputConstraints), - NewTemplate(NewTemplate<'a>), - RequestTransactionData(RequestTransactionData), - RequestTransactionDataError(RequestTransactionDataError<'a>), - RequestTransactionDataSuccess(RequestTransactionDataSuccess<'a>), - SetNewPrevHash(template_distribution_sv2::SetNewPrevHash<'a>), - SubmitSolution(SubmitSolution<'a>), - ChannelEndpointChanged(ChannelEndpointChanged), - SetupConnection(SetupConnection<'a>), - SetupConnectionError(SetupConnectionError<'a>), - SetupConnectionSuccess(SetupConnectionSuccess), -} - -impl Sv2Message<'_> { - pub fn message_type(&self) -> u8 { - match self { - Sv2Message::CoinbaseOutputConstraints(_) => MESSAGE_TYPE_COINBASE_OUTPUT_CONSTRAINTS, - Sv2Message::NewTemplate(_) => MESSAGE_TYPE_NEW_TEMPLATE, - Sv2Message::RequestTransactionData(_) => MESSAGE_TYPE_REQUEST_TRANSACTION_DATA, - Sv2Message::RequestTransactionDataError(_) => { - MESSAGE_TYPE_REQUEST_TRANSACTION_DATA_ERROR - } - Sv2Message::RequestTransactionDataSuccess(_) => { - MESSAGE_TYPE_REQUEST_TRANSACTION_DATA_SUCCESS - } - Sv2Message::SetNewPrevHash(_) => MESSAGE_TYPE_SET_NEW_PREV_HASH, - Sv2Message::SubmitSolution(_) => MESSAGE_TYPE_SUBMIT_SOLUTION, - Sv2Message::ChannelEndpointChanged(_) => MESSAGE_TYPE_CHANNEL_ENDPOINT_CHANGED, - Sv2Message::SetupConnection(_) => MESSAGE_TYPE_SETUP_CONNECTION, - Sv2Message::SetupConnectionError(_) => MESSAGE_TYPE_SETUP_CONNECTION_ERROR, - Sv2Message::SetupConnectionSuccess(_) => MESSAGE_TYPE_SETUP_CONNECTION_SUCCESS, - } - } - - pub fn channel_bit(&self) -> bool { - match self { - Sv2Message::CoinbaseOutputConstraints(_) => CHANNEL_BIT_COINBASE_OUTPUT_CONSTRAINTS, - Sv2Message::NewTemplate(_) => CHANNEL_BIT_NEW_TEMPLATE, - Sv2Message::RequestTransactionData(_) => CHANNEL_BIT_REQUEST_TRANSACTION_DATA, - Sv2Message::RequestTransactionDataError(_) => { - CHANNEL_BIT_REQUEST_TRANSACTION_DATA_ERROR - } - Sv2Message::RequestTransactionDataSuccess(_) => { - CHANNEL_BIT_REQUEST_TRANSACTION_DATA_SUCCESS - } - Sv2Message::SetNewPrevHash(_) => CHANNEL_BIT_SET_NEW_PREV_HASH, - Sv2Message::SubmitSolution(_) => CHANNEL_BIT_SUBMIT_SOLUTION, - Sv2Message::ChannelEndpointChanged(_) => CHANNEL_BIT_CHANNEL_ENDPOINT_CHANGED, - Sv2Message::SetupConnection(_) => CHANNEL_BIT_SETUP_CONNECTION, - Sv2Message::SetupConnectionError(_) => CHANNEL_BIT_SETUP_CONNECTION_ERROR, - Sv2Message::SetupConnectionSuccess(_) => CHANNEL_BIT_SETUP_CONNECTION_SUCCESS, - } - } -} - -impl Display for Sv2Message<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", *self) - } -} - -#[repr(C)] -pub enum CSv2Message { - CoinbaseOutputConstraints(CoinbaseOutputConstraints), - NewTemplate(CNewTemplate), - RequestTransactionData(RequestTransactionData), - RequestTransactionDataError(CRequestTransactionDataError), - RequestTransactionDataSuccess(CRequestTransactionDataSuccess), - SetNewPrevHash(CSetNewPrevHash), - SubmitSolution(CSubmitSolution), - ChannelEndpointChanged(ChannelEndpointChanged), - SetupConnection(CSetupConnection), - SetupConnectionError(CSetupConnectionError), - SetupConnectionSuccess(SetupConnectionSuccess), -} - -#[no_mangle] -pub extern "C" fn drop_sv2_message(s: CSv2Message) { - match s { - CSv2Message::CoinbaseOutputConstraints(_) => (), - CSv2Message::NewTemplate(a) => drop(a), - CSv2Message::RequestTransactionData(_) => (), - CSv2Message::RequestTransactionDataError(a) => drop(a), - CSv2Message::RequestTransactionDataSuccess(a) => drop(a), - CSv2Message::SetNewPrevHash(a) => drop(a), - CSv2Message::SubmitSolution(a) => drop(a), - CSv2Message::ChannelEndpointChanged(_) => (), - CSv2Message::SetupConnection(a) => drop(a), - CSv2Message::SetupConnectionError(a) => drop(a), - CSv2Message::SetupConnectionSuccess(_) => (), - } -} - -/// This function does nothing unless there is some heap allocated data owned by the C side that -/// needs to be dropped (specifically a `CVec`). In this case, `free_vec` is used in order to drop -/// that memory. -#[no_mangle] -pub extern "C" fn drop_sv2_error(s: Sv2Error) { - match s { - Sv2Error::BinaryError(a) => drop(a), - Sv2Error::CodecError(_) => (), - Sv2Error::EncoderBusy => (), - Sv2Error::InvalidSv2Frame => (), - Sv2Error::MissingBytes => (), - Sv2Error::PayloadTooBig(_) => (), - Sv2Error::Unknown => (), - } -} - -impl<'a> From> for CSv2Message { - fn from(v: Sv2Message<'a>) -> Self { - match v { - Sv2Message::CoinbaseOutputConstraints(a) => Self::CoinbaseOutputConstraints(a), - Sv2Message::NewTemplate(a) => Self::NewTemplate(a.into()), - Sv2Message::RequestTransactionData(a) => Self::RequestTransactionData(a), - Sv2Message::RequestTransactionDataError(a) => { - Self::RequestTransactionDataError(a.into()) - } - Sv2Message::RequestTransactionDataSuccess(a) => { - Self::RequestTransactionDataSuccess(a.into()) - } - Sv2Message::SetNewPrevHash(a) => Self::SetNewPrevHash(a.into()), - Sv2Message::SubmitSolution(a) => Self::SubmitSolution(a.into()), - Sv2Message::ChannelEndpointChanged(a) => Self::ChannelEndpointChanged(a), - Sv2Message::SetupConnection(a) => Self::SetupConnection(a.into()), - Sv2Message::SetupConnectionError(a) => Self::SetupConnectionError(a.into()), - Sv2Message::SetupConnectionSuccess(a) => Self::SetupConnectionSuccess(a), - } - } -} - -impl<'a> CSv2Message { - #[allow(clippy::wrong_self_convention)] - pub fn to_rust_rep_mut(&'a mut self) -> Result, Error> { - match self { - CSv2Message::NewTemplate(v) => Ok(Sv2Message::NewTemplate(v.to_rust_rep_mut()?)), - CSv2Message::SetNewPrevHash(v) => Ok(Sv2Message::SetNewPrevHash(v.to_rust_rep_mut()?)), - CSv2Message::SubmitSolution(v) => Ok(Sv2Message::SubmitSolution(v.to_rust_rep_mut()?)), - CSv2Message::SetupConnectionError(v) => { - Ok(Sv2Message::SetupConnectionError(v.to_rust_rep_mut()?)) - } - CSv2Message::SetupConnectionSuccess(v) => Ok(Sv2Message::SetupConnectionSuccess(*v)), - CSv2Message::CoinbaseOutputConstraints(v) => { - Ok(Sv2Message::CoinbaseOutputConstraints(*v)) - } - CSv2Message::RequestTransactionData(v) => Ok(Sv2Message::RequestTransactionData(*v)), - CSv2Message::RequestTransactionDataError(v) => Ok( - Sv2Message::RequestTransactionDataError(v.to_rust_rep_mut()?), - ), - CSv2Message::RequestTransactionDataSuccess(v) => Ok( - Sv2Message::RequestTransactionDataSuccess(v.to_rust_rep_mut()?), - ), - CSv2Message::ChannelEndpointChanged(v) => Ok(Sv2Message::ChannelEndpointChanged(*v)), - CSv2Message::SetupConnection(v) => { - Ok(Sv2Message::SetupConnection(v.to_rust_rep_mut()?)) - } - } - } -} - -impl<'decoder> From> for EncodableField<'decoder> { - fn from(m: Sv2Message<'decoder>) -> Self { - match m { - Sv2Message::CoinbaseOutputConstraints(a) => a.into(), - Sv2Message::NewTemplate(a) => a.into(), - Sv2Message::RequestTransactionData(a) => a.into(), - Sv2Message::RequestTransactionDataError(a) => a.into(), - Sv2Message::RequestTransactionDataSuccess(a) => a.into(), - Sv2Message::SetNewPrevHash(a) => a.into(), - Sv2Message::SubmitSolution(a) => a.into(), - Sv2Message::ChannelEndpointChanged(a) => a.into(), - Sv2Message::SetupConnection(a) => a.into(), - Sv2Message::SetupConnectionError(a) => a.into(), - Sv2Message::SetupConnectionSuccess(a) => a.into(), - } - } -} - -impl binary_sv2::GetSize for Sv2Message<'_> { - fn get_size(&self) -> usize { - match self { - Sv2Message::CoinbaseOutputConstraints(a) => a.get_size(), - Sv2Message::NewTemplate(a) => a.get_size(), - Sv2Message::RequestTransactionData(a) => a.get_size(), - Sv2Message::RequestTransactionDataError(a) => a.get_size(), - Sv2Message::RequestTransactionDataSuccess(a) => a.get_size(), - Sv2Message::SetNewPrevHash(a) => a.get_size(), - Sv2Message::SubmitSolution(a) => a.get_size(), - Sv2Message::ChannelEndpointChanged(a) => a.get_size(), - Sv2Message::SetupConnection(a) => a.get_size(), - Sv2Message::SetupConnectionError(a) => a.get_size(), - Sv2Message::SetupConnectionSuccess(a) => a.get_size(), - } - } -} - -impl<'decoder> Deserialize<'decoder> for Sv2Message<'decoder> { - fn get_structure(_v: &[u8]) -> std::result::Result, binary_sv2::Error> { - unimplemented!() - } - fn from_decoded_fields( - _v: Vec>, - ) -> std::result::Result { - unimplemented!() - } -} - -impl<'a> TryFrom<(u8, &'a mut [u8])> for Sv2Message<'a> { - type Error = Error; - - fn try_from(v: (u8, &'a mut [u8])) -> Result { - let msg_type = v.0; - match msg_type { - MESSAGE_TYPE_SETUP_CONNECTION => { - let message: SetupConnection<'a> = from_bytes(v.1)?; - Ok(Sv2Message::SetupConnection(message)) - } - MESSAGE_TYPE_SETUP_CONNECTION_SUCCESS => { - let message: SetupConnectionSuccess = from_bytes(v.1)?; - Ok(Sv2Message::SetupConnectionSuccess(message)) - } - MESSAGE_TYPE_SETUP_CONNECTION_ERROR => { - let message: SetupConnectionError<'a> = from_bytes(v.1)?; - Ok(Sv2Message::SetupConnectionError(message)) - } - MESSAGE_TYPE_CHANNEL_ENDPOINT_CHANGED => { - let message: ChannelEndpointChanged = from_bytes(v.1)?; - Ok(Sv2Message::ChannelEndpointChanged(message)) - } - MESSAGE_TYPE_COINBASE_OUTPUT_CONSTRAINTS => { - let message: CoinbaseOutputConstraints = from_bytes(v.1)?; - Ok(Sv2Message::CoinbaseOutputConstraints(message)) - } - MESSAGE_TYPE_NEW_TEMPLATE => { - let message: NewTemplate<'a> = from_bytes(v.1)?; - Ok(Sv2Message::NewTemplate(message)) - } - MESSAGE_TYPE_SET_NEW_PREV_HASH => { - let message: template_distribution_sv2::SetNewPrevHash<'a> = from_bytes(v.1)?; - Ok(Sv2Message::SetNewPrevHash(message)) - } - MESSAGE_TYPE_REQUEST_TRANSACTION_DATA => { - let message: RequestTransactionData = from_bytes(v.1)?; - Ok(Sv2Message::RequestTransactionData(message)) - } - MESSAGE_TYPE_REQUEST_TRANSACTION_DATA_SUCCESS => { - let message: RequestTransactionDataSuccess = from_bytes(v.1)?; - Ok(Sv2Message::RequestTransactionDataSuccess(message)) - } - MESSAGE_TYPE_REQUEST_TRANSACTION_DATA_ERROR => { - let message: RequestTransactionDataError = from_bytes(v.1)?; - Ok(Sv2Message::RequestTransactionDataError(message)) - } - MESSAGE_TYPE_SUBMIT_SOLUTION => { - let message: SubmitSolution = from_bytes(v.1)?; - Ok(Sv2Message::SubmitSolution(message)) - } - _ => Err(Error::UnknownMessageType(msg_type)), - } - } -} - -#[repr(C)] -pub enum CResult { - Ok(T), - Err(E), -} - -#[repr(C)] -pub enum Sv2Error { - BinaryError(binary_sv2::CError), - CodecError(codec_sv2::CError), - EncoderBusy, - InvalidSv2Frame, - MissingBytes, - PayloadTooBig(CVec), - Unknown, -} - -impl fmt::Display for Sv2Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use Sv2Error::*; - match self { - BinaryError(ref e) => write!(f, "{:?}", e), - CodecError(ref e) => write!(f, "{:?}", e), - PayloadTooBig(ref e) => write!(f, "Payload is too big: {:?}", e), - InvalidSv2Frame => write!(f, "Invalid Sv2 frame"), - MissingBytes => write!(f, "Missing expected bytes"), - EncoderBusy => write!(f, "Encoder is busy"), - Unknown => write!(f, "Unknown error occurred"), - } - } -} - -impl From for Sv2Error { - fn from(e: binary_sv2::CError) -> Sv2Error { - Sv2Error::BinaryError(e) - } -} - -impl From for Sv2Error { - fn from(e: binary_sv2::Error) -> Sv2Error { - Sv2Error::BinaryError(e.into()) - } -} - -impl From for Sv2Error { - fn from(e: codec_sv2::CError) -> Sv2Error { - Sv2Error::CodecError(e) - } -} - -#[no_mangle] -pub extern "C" fn is_ok(cresult: &CResult) -> bool { - match cresult { - CResult::Ok(_) => true, - CResult::Err(_) => false, - } -} - -impl From> for CResult { - fn from(v: Result) -> Self { - match v { - Ok(v) => Self::Ok(v), - Err(e) => Self::Err(e), - } - } -} - -#[derive(Debug)] -pub struct EncoderWrapper { - encoder: Encoder>, - free: bool, -} - -#[no_mangle] -pub extern "C" fn new_encoder() -> *mut EncoderWrapper { - let encoder: Encoder> = Encoder::new(); - let s = Box::new(EncoderWrapper { - encoder, - free: true, - }); - Box::into_raw(s) -} - -#[no_mangle] -#[allow(clippy::not_unsafe_ptr_arg_deref)] -pub extern "C" fn flush_encoder(encoder: *mut EncoderWrapper) { - let mut encoder = unsafe { Box::from_raw(encoder) }; - encoder.free = true; - let _ = Box::into_raw(encoder); -} - -fn encode_( - message: &'static mut CSv2Message, - encoder: &mut EncoderWrapper, -) -> Result { - let message: Sv2Message = message.to_rust_rep_mut()?; - let m_type = message.message_type(); - let c_bit = message.channel_bit(); - let frame = - StandardSv2Frame::>::from_message(message.clone(), m_type, 0, c_bit) - .ok_or(Sv2Error::PayloadTooBig( - format!("{}", message).as_bytes().into(), - ))?; - encoder - .encoder - .encode(frame) - .map_err(|e| Sv2Error::CodecError(e.into())) - .map(|x| x.into()) -} - -#[no_mangle] -#[allow(clippy::not_unsafe_ptr_arg_deref)] -pub extern "C" fn free_decoder(decoder: *mut DecoderWrapper) { - let decoder = unsafe { Box::from_raw(decoder) }; - drop(decoder); -} - -/// # Safety -#[no_mangle] -pub unsafe extern "C" fn encode( - message: &'static mut CSv2Message, - encoder: *mut EncoderWrapper, -) -> CResult { - let mut encoder = Box::from_raw(encoder); - if encoder.free { - let result = encode_(message, &mut encoder); - encoder.free = false; - let _ = Box::into_raw(encoder); - result.into() - } else { - CResult::Err(Sv2Error::EncoderBusy) - } -} - -#[derive(Debug)] -pub struct DecoderWrapper(StandardDecoder>); - -#[no_mangle] -pub extern "C" fn new_decoder() -> *mut DecoderWrapper { - let s = Box::new(DecoderWrapper(StandardDecoder::new())); - Box::into_raw(s) -} - -#[no_mangle] -#[allow(clippy::not_unsafe_ptr_arg_deref)] -pub extern "C" fn get_writable(decoder: *mut DecoderWrapper) -> CVec { - let mut decoder = unsafe { Box::from_raw(decoder) }; - let writable = decoder.0.writable(); - let res = CVec::as_shared_buffer(writable); - let _ = Box::into_raw(decoder); - res -} - -#[no_mangle] -#[allow(clippy::not_unsafe_ptr_arg_deref)] -pub extern "C" fn next_frame(decoder: *mut DecoderWrapper) -> CResult { - let mut decoder = unsafe { Box::from_raw(decoder) }; - - match decoder.0.next_frame() { - Ok(mut f) => { - let msg_type = match f.get_header() { - Some(header) => header.msg_type(), - None => return CResult::Err(Sv2Error::InvalidSv2Frame), - }; - let payload = f.payload(); - let len = payload.len(); - let ptr = payload.as_mut_ptr(); - let payload = unsafe { std::slice::from_raw_parts_mut(ptr, len) }; - let _ = Box::into_raw(decoder); - (msg_type, payload) - .try_into() - .map(|x: Sv2Message| x.into()) - .map_err(|_| Sv2Error::Unknown) - .into() - } - Err(_) => { - let _ = Box::into_raw(decoder); - CResult::Err(Sv2Error::MissingBytes) - } - } -} -#[cfg(test)] -#[cfg(feature = "prop_test")] -mod tests { - use super::*; - use core::convert::TryInto; - use quickcheck::{Arbitrary, Gen}; - use quickcheck_macros; - use template_distribution_sv2::CoinbaseOutputConstraints; - - #[derive(Clone, Debug)] - pub struct RandomCoinbaseOutputConstraints(pub CoinbaseOutputConstraints); - - impl Arbitrary for RandomCoinbaseOutputConstraints { - fn arbitrary(g: &mut Gen) -> Self { - RandomCoinbaseOutputConstraints(CoinbaseOutputConstraints::from_gen(g)) - } - } - - fn get_setup_connection() -> SetupConnection<'static> { - get_setup_connection_w_params( - common_messages_sv2::Protocol::TemplateDistributionProtocol, - 2, - 2, - 0, - "0.0.0.0".to_string(), - 8081, - "Bitmain".to_string(), - "901".to_string(), - "abcX".to_string(), - "89567".to_string(), - ) - } - - fn get_setup_connection_w_params( - protocol: common_messages_sv2::Protocol, - min_version: u16, - max_version: u16, - flags: u32, - endpoint_host: String, - endpoint_port: u16, - vendor: String, - hardware_version: String, - firmware: String, - device_id: String, - ) -> SetupConnection<'static> { - SetupConnection { - protocol, - min_version, - max_version, - flags, - endpoint_host: endpoint_host.into_bytes().try_into().unwrap(), - endpoint_port, - vendor: vendor.into_bytes().try_into().unwrap(), - hardware_version: hardware_version.into_bytes().try_into().unwrap(), - firmware: firmware.into_bytes().try_into().unwrap(), - device_id: device_id.into_bytes().try_into().unwrap(), - } - } - - #[test] - fn test_message_type_cb_output_data_size() { - let expect = MESSAGE_TYPE_COINBASE_OUTPUT_CONSTRAINTS; - let cb_output_data_size = CoinbaseOutputConstraints { - coinbase_output_max_additional_size: 0, - coinbase_output_max_additional_sigops: 0, - }; - let sv2_message = Sv2Message::CoinbaseOutputConstraints(cb_output_data_size); - let actual = sv2_message.message_type(); - - assert_eq!(expect, actual); - } - - #[test] - fn test_message_type_new_template() { - let expect = MESSAGE_TYPE_NEW_TEMPLATE; - let new_template = NewTemplate { - template_id: 0, - future_template: false, - version: 0x01000000, - coinbase_tx_version: 0x01000000, - coinbase_prefix: "0".to_string().into_bytes().try_into().unwrap(), - coinbase_tx_input_sequence: 0xffffffff, - coinbase_tx_value_remaining: 0x00f2052a, - coinbase_tx_outputs_count: 1, - coinbase_tx_outputs: vec![].try_into().unwrap(), - coinbase_tx_locktime: 0x00000000, - merkle_path: binary_sv2::Seq0255::new(Vec::::new()).unwrap(), - }; - let sv2_message = Sv2Message::NewTemplate(new_template); - let actual = sv2_message.message_type(); - - assert_eq!(expect, actual); - } - - #[test] - fn test_message_type_request_transaction_data() { - let expect = MESSAGE_TYPE_REQUEST_TRANSACTION_DATA; - let request_tx_data = RequestTransactionData { template_id: 0 }; - let sv2_message = Sv2Message::RequestTransactionData(request_tx_data); - let actual = sv2_message.message_type(); - - assert_eq!(expect, actual); - } - - #[test] - fn test_message_type_request_transaction_data_error() { - let expect = MESSAGE_TYPE_REQUEST_TRANSACTION_DATA_ERROR; - let request_tx_data_err = RequestTransactionDataError { - template_id: 0, - error_code: "an error code".to_string().into_bytes().try_into().unwrap(), - }; - let sv2_message = Sv2Message::RequestTransactionDataError(request_tx_data_err); - let actual = sv2_message.message_type(); - - assert_eq!(expect, actual); - } - - #[test] - fn test_message_type_request_transaction_data_success() { - let expect = MESSAGE_TYPE_REQUEST_TRANSACTION_DATA_SUCCESS; - - let request_tx_data_success = RequestTransactionDataSuccess { - template_id: 0, - excess_data: "some_excess_data" - .to_string() - .into_bytes() - .try_into() - .unwrap(), - transaction_list: binary_sv2::Seq064K::new(Vec::new()).unwrap(), - }; - let sv2_message = Sv2Message::RequestTransactionDataSuccess(request_tx_data_success); - let actual = sv2_message.message_type(); - - assert_eq!(expect, actual); - } - - #[test] - fn test_message_type_set_new_prev_hash() { - let expect = MESSAGE_TYPE_SET_NEW_PREV_HASH; - - let mut u256 = [0_u8; 32]; - let u256_prev_hash: binary_sv2::U256 = (&mut u256[..]).try_into().unwrap(); - - let mut u256 = [0_u8; 32]; - let u256_target: binary_sv2::U256 = (&mut u256[..]).try_into().unwrap(); - - let set_new_prev_hash = SetNewPrevHash { - template_id: 0, - prev_hash: u256_prev_hash, - header_timestamp: 0x29ab5f49, - n_bits: 0xffff001d, - target: u256_target, - }; - let sv2_message = Sv2Message::SetNewPrevHash(set_new_prev_hash); - let actual = sv2_message.message_type(); - - assert_eq!(expect, actual); - } - - #[test] - fn test_message_type_submit_solution() { - let expect = MESSAGE_TYPE_SUBMIT_SOLUTION; - - let submit_solution = SubmitSolution { - template_id: 0, - version: 0x01000000, - header_timestamp: 0x29ab5f49, - header_nonce: 0x1dac2b7c, - coinbase_tx: "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000" - .to_string() - .into_bytes() - .try_into() - .unwrap(), - }; - - let sv2_message = Sv2Message::SubmitSolution(submit_solution); - let actual = sv2_message.message_type(); - - assert_eq!(expect, actual); - } - - #[test] - fn test_message_type_channel_endpoint_changed() { - let expect = MESSAGE_TYPE_CHANNEL_ENDPOINT_CHANGED; - - let channel_endpoint_changed = ChannelEndpointChanged { channel_id: 0 }; - - let sv2_message = Sv2Message::ChannelEndpointChanged(channel_endpoint_changed); - let actual = sv2_message.message_type(); - - assert_eq!(expect, actual); - } - - #[test] - fn test_message_type_setup_connection() { - let expect = MESSAGE_TYPE_SETUP_CONNECTION; - - let setup_connection = get_setup_connection(); - - let sv2_message = Sv2Message::SetupConnection(setup_connection); - let actual = sv2_message.message_type(); - - assert_eq!(expect, actual); - } - - #[test] - fn test_message_type_setup_connection_error() { - let expect = MESSAGE_TYPE_SETUP_CONNECTION_ERROR; - - let setup_connection_err = SetupConnectionError { - flags: 0, - error_code: "an error code".to_string().into_bytes().try_into().unwrap(), - }; - - let sv2_message = Sv2Message::SetupConnectionError(setup_connection_err); - let actual = sv2_message.message_type(); - - assert_eq!(expect, actual); - } - - #[test] - fn test_message_type_setup_connection_success() { - let expect = MESSAGE_TYPE_SETUP_CONNECTION_SUCCESS; - - let setup_connection_success = SetupConnectionSuccess { - used_version: 1, - flags: 0, - }; - - let sv2_message = Sv2Message::SetupConnectionSuccess(setup_connection_success); - let actual = sv2_message.message_type(); - - assert_eq!(expect, actual); - } - - #[test] - #[ignore] - fn test_next_frame() { - let decoder = StandardDecoder::>::new(); - println!("DECODER: {:?}", &decoder); - println!("DECODER 2: {:?}", &decoder); - let mut decoder_wrapper = DecoderWrapper(decoder); - let _res = next_frame(&mut decoder_wrapper); - } - - // RR - - #[quickcheck_macros::quickcheck] - fn encode_with_c_coinbase_output_data_size(message: RandomCoinbaseOutputConstraints) -> bool { - let expected = message.clone().0; - - let mut encoder = Encoder::::new(); - let mut decoder = StandardDecoder::>::new(); - - let frame = StandardSv2Frame::from_message( - message.0, - MESSAGE_TYPE_COINBASE_OUTPUT_CONSTRAINTS, - 0, - false, - ) - .unwrap(); - let encoded_frame = encoder.encode(frame).unwrap(); - - let buffer = decoder.writable(); - for i in 0..buffer.len() { - buffer[i] = encoded_frame[i] - } - let _ = decoder.next_frame(); - - let buffer = decoder.writable(); - for i in 0..buffer.len() { - buffer[i] = encoded_frame[i + 6] - } - - let mut decoded = decoder.next_frame().unwrap(); - - let msg_type = decoded.get_header().unwrap().msg_type(); - let payload = decoded.payload(); - let decoded_message: Sv2Message = (msg_type, payload).try_into().unwrap(); - let decoded_message = match decoded_message { - Sv2Message::CoinbaseOutputConstraints(m) => m, - _ => panic!(), - }; - - decoded_message == expected - } - - #[derive(Clone, Debug)] - pub struct RandomNewTemplate(pub NewTemplate<'static>); - impl Arbitrary for RandomNewTemplate { - fn arbitrary(g: &mut Gen) -> Self { - RandomNewTemplate(NewTemplate::from_gen(g)) - } - } - - #[quickcheck_macros::quickcheck] - fn encode_with_c_new_template_id(message: RandomNewTemplate) -> bool { - let expected = message.clone().0; - - let mut encoder = Encoder::::new(); - let mut decoder = StandardDecoder::>::new(); - - // Create frame - let frame = - StandardSv2Frame::from_message(message.0, MESSAGE_TYPE_NEW_TEMPLATE, 0, false).unwrap(); - // Encode frame - let encoded_frame = encoder.encode(frame).unwrap(); - - // Decode encoded frame - let buffer = decoder.writable(); - for i in 0..buffer.len() { - buffer[i] = encoded_frame[i] - } - // Puts decoder in the next state (next 6 bytes). If frame is incomplete, returns an error - // prompting to add more bytes to decode the frame - // Required between two writes because of how this is intended to use the decoder in a loop - // read from a stream. - let _ = decoder.next_frame(); - - let buffer = decoder.writable(); - for i in 0..buffer.len() { - buffer[i] = encoded_frame[i + 6] - } - - // Decoded frame, complete frame is filled - let mut decoded = decoder.next_frame().unwrap(); - - // Extract payload of the frame which is the NewTemplate message - let msg_type = decoded.get_header().unwrap().msg_type(); - let payload = decoded.payload(); - let decoded_message: Sv2Message = (msg_type, payload).try_into().unwrap(); - let decoded_message = match decoded_message { - Sv2Message::NewTemplate(m) => m, - _ => panic!(), - }; - - decoded_message == expected - } - - #[derive(Clone, Debug)] - pub struct RandomRequestTransactionData(pub RequestTransactionData); - impl Arbitrary for RandomRequestTransactionData { - fn arbitrary(g: &mut Gen) -> Self { - RandomRequestTransactionData(RequestTransactionData::from_gen(g)) - } - } - - #[quickcheck_macros::quickcheck] - fn encode_with_c_request_transaction_data(message: RandomRequestTransactionData) -> bool { - let expected = message.clone().0; - - let mut encoder = Encoder::::new(); - let mut decoder = StandardDecoder::>::new(); - - let frame = StandardSv2Frame::from_message( - message.0, - MESSAGE_TYPE_REQUEST_TRANSACTION_DATA, - 0, - false, - ) - .unwrap(); - let encoded_frame = encoder.encode(frame).unwrap(); - - let buffer = decoder.writable(); - for i in 0..buffer.len() { - buffer[i] = encoded_frame[i] - } - let _ = decoder.next_frame(); - - let buffer = decoder.writable(); - for i in 0..buffer.len() { - buffer[i] = encoded_frame[i + 6] - } - - let mut decoded = decoder.next_frame().unwrap(); - - let msg_type = decoded.get_header().unwrap().msg_type(); - let payload = decoded.payload(); - let decoded_message: Sv2Message = (msg_type, payload).try_into().unwrap(); - let decoded_message = match decoded_message { - Sv2Message::RequestTransactionData(m) => m, - _ => panic!(), - }; - - decoded_message == expected - } - - #[derive(Clone, Debug)] - pub struct RandomRequestTransactionDataError(pub RequestTransactionDataError<'static>); - - impl Arbitrary for RandomRequestTransactionDataError { - fn arbitrary(g: &mut Gen) -> Self { - RandomRequestTransactionDataError(RequestTransactionDataError::from_gen(g)) - } - } - #[quickcheck_macros::quickcheck] - fn encode_with_c_request_transaction_data_error( - message: RandomRequestTransactionDataError, - ) -> bool { - let expected = message.clone().0; - - let mut encoder = Encoder::::new(); - let mut decoder = StandardDecoder::>::new(); - - let frame = StandardSv2Frame::from_message( - message.0, - MESSAGE_TYPE_REQUEST_TRANSACTION_DATA_ERROR, - 0, - false, - ) - .unwrap(); - let encoded_frame = encoder.encode(frame).unwrap(); - - let buffer = decoder.writable(); - for i in 0..buffer.len() { - buffer[i] = encoded_frame[i] - } - let _ = decoder.next_frame(); - - let buffer = decoder.writable(); - for i in 0..buffer.len() { - buffer[i] = encoded_frame[i + 6] - } - - let mut decoded = decoder.next_frame().unwrap(); - - let msg_type = decoded.get_header().unwrap().msg_type(); - let payload = decoded.payload(); - let decoded_message: Sv2Message = (msg_type, payload).try_into().unwrap(); - let decoded_message = match decoded_message { - Sv2Message::RequestTransactionDataError(m) => m, - _ => panic!(), - }; - - decoded_message == expected - } - - #[derive(Clone, Debug)] - pub struct RandomRequestTransactionDataSuccess(pub RequestTransactionDataSuccess<'static>); - - impl Arbitrary for RandomRequestTransactionDataSuccess { - fn arbitrary(g: &mut Gen) -> Self { - RandomRequestTransactionDataSuccess(RequestTransactionDataSuccess::from_gen(g)) - } - } - #[quickcheck_macros::quickcheck] - fn encode_with_c_request_transaction_data_success( - message: RandomRequestTransactionDataSuccess, - ) -> bool { - let expected = message.clone().0; - - let mut encoder = Encoder::::new(); - let mut decoder = StandardDecoder::>::new(); - - let frame = StandardSv2Frame::from_message( - message.0, - MESSAGE_TYPE_REQUEST_TRANSACTION_DATA_SUCCESS, - 0, - false, - ) - .unwrap(); - let encoded_frame = encoder.encode(frame).unwrap(); - - let buffer = decoder.writable(); - for i in 0..buffer.len() { - buffer[i] = encoded_frame[i] - } - let _ = decoder.next_frame(); - - let buffer = decoder.writable(); - for i in 0..buffer.len() { - buffer[i] = encoded_frame[i + 6] - } - - let mut decoded = decoder.next_frame().unwrap(); - - let msg_type = decoded.get_header().unwrap().msg_type(); - let payload = decoded.payload(); - let decoded_message: Sv2Message = (msg_type, payload).try_into().unwrap(); - let decoded_message = match decoded_message { - Sv2Message::RequestTransactionDataSuccess(m) => m, - _ => panic!(), - }; - - decoded_message == expected - } - - #[derive(Clone, Debug)] - pub struct RandomSetNewPrevHash(pub SetNewPrevHash<'static>); - - impl Arbitrary for RandomSetNewPrevHash { - fn arbitrary(g: &mut Gen) -> Self { - RandomSetNewPrevHash(SetNewPrevHash::from_gen(g)) - } - } - - #[quickcheck_macros::quickcheck] - fn encode_with_c_set_new_prev_hash(message: RandomSetNewPrevHash) -> bool { - let expected = message.clone().0; - - let mut encoder = Encoder::::new(); - let mut decoder = StandardDecoder::>::new(); - - let frame = - StandardSv2Frame::from_message(message.0, MESSAGE_TYPE_SET_NEW_PREV_HASH, 0, false) - .unwrap(); - let encoded_frame = encoder.encode(frame).unwrap(); - - let buffer = decoder.writable(); - for i in 0..buffer.len() { - buffer[i] = encoded_frame[i] - } - let _ = decoder.next_frame(); - - let buffer = decoder.writable(); - for i in 0..buffer.len() { - buffer[i] = encoded_frame[i + 6] - } - - let mut decoded = decoder.next_frame().unwrap(); - - let msg_type = decoded.get_header().unwrap().msg_type(); - let payload = decoded.payload(); - let decoded_message: Sv2Message = (msg_type, payload).try_into().unwrap(); - let decoded_message = match decoded_message { - Sv2Message::SetNewPrevHash(m) => m, - _ => panic!(), - }; - - decoded_message == expected - } - - #[derive(Clone, Debug)] - pub struct RandomSubmitSolution(pub SubmitSolution<'static>); - - impl Arbitrary for RandomSubmitSolution { - fn arbitrary(g: &mut Gen) -> Self { - RandomSubmitSolution(SubmitSolution::from_gen(g)) - } - } - - #[quickcheck_macros::quickcheck] - fn encode_with_c_submit_solution(message: RandomSubmitSolution) -> bool { - let expected = message.clone().0; - - let mut encoder = Encoder::::new(); - let mut decoder = StandardDecoder::>::new(); - - let frame = - StandardSv2Frame::from_message(message.0, MESSAGE_TYPE_SUBMIT_SOLUTION, 0, false) - .unwrap(); - let encoded_frame = encoder.encode(frame).unwrap(); - - let buffer = decoder.writable(); - for i in 0..buffer.len() { - buffer[i] = encoded_frame[i] - } - let _ = decoder.next_frame(); - - let buffer = decoder.writable(); - for i in 0..buffer.len() { - buffer[i] = encoded_frame[i + 6] - } - - let mut decoded = decoder.next_frame().unwrap(); - - let msg_type = decoded.get_header().unwrap().msg_type(); - let payload = decoded.payload(); - let decoded_message: Sv2Message = (msg_type, payload).try_into().unwrap(); - let decoded_message = match decoded_message { - Sv2Message::SubmitSolution(m) => m, - _ => panic!(), - }; - - decoded_message == expected - } - - #[derive(Clone, Debug)] - pub struct RandomSetupConnection(pub SetupConnection<'static>); - - impl Arbitrary for RandomSetupConnection { - fn arbitrary(g: &mut Gen) -> Self { - RandomSetupConnection(SetupConnection::from_gen(g)) - } - } - - #[derive(Clone, Debug)] - pub struct RandomChannelEndpointChanged(pub ChannelEndpointChanged); - - impl Arbitrary for RandomChannelEndpointChanged { - fn arbitrary(g: &mut Gen) -> Self { - RandomChannelEndpointChanged(ChannelEndpointChanged::from_gen(g)) - } - } - - #[quickcheck_macros::quickcheck] - fn encode_with_c_channel_endpoint_changed(message: RandomChannelEndpointChanged) -> bool { - let expected = message.clone().0; - - let mut encoder = Encoder::::new(); - let mut decoder = StandardDecoder::>::new(); - - let frame = StandardSv2Frame::from_message( - message.0, - MESSAGE_TYPE_CHANNEL_ENDPOINT_CHANGED, - 0, - false, - ) - .unwrap(); - let encoded_frame = encoder.encode(frame).unwrap(); - - let buffer = decoder.writable(); - for i in 0..buffer.len() { - buffer[i] = encoded_frame[i] - } - let _ = decoder.next_frame(); - - let buffer = decoder.writable(); - for i in 0..buffer.len() { - buffer[i] = encoded_frame[i + 6] - } - - let mut decoded = decoder.next_frame().unwrap(); - - let msg_type = decoded.get_header().unwrap().msg_type(); - let payload = decoded.payload(); - let decoded_message: Sv2Message = (msg_type, payload).try_into().unwrap(); - let decoded_message = match decoded_message { - Sv2Message::ChannelEndpointChanged(m) => m, - _ => panic!(), - }; - - decoded_message == expected - } - - #[quickcheck_macros::quickcheck] - fn encode_with_c_setup_connection(message: RandomSetupConnection) -> bool { - let expected = message.clone().0; - - let mut encoder = Encoder::::new(); - let mut decoder = StandardDecoder::>::new(); - - let frame = - StandardSv2Frame::from_message(message.0, MESSAGE_TYPE_SETUP_CONNECTION, 0, false) - .unwrap(); - let encoded_frame = encoder.encode(frame).unwrap(); - - let buffer = decoder.writable(); - for i in 0..buffer.len() { - buffer[i] = encoded_frame[i] - } - let _ = decoder.next_frame(); - - let buffer = decoder.writable(); - for i in 0..buffer.len() { - buffer[i] = encoded_frame[i + 6] - } - - let mut decoded = decoder.next_frame().unwrap(); - - let msg_type = decoded.get_header().unwrap().msg_type(); - let payload = decoded.payload(); - let decoded_message: Sv2Message = (msg_type, payload).try_into().unwrap(); - let decoded_message = match decoded_message { - Sv2Message::SetupConnection(m) => m, - _ => panic!(), - }; - - decoded_message == expected - } - - #[derive(Clone, Debug)] - pub struct RandomSetupConnectionError(pub SetupConnectionError<'static>); - - impl Arbitrary for RandomSetupConnectionError { - fn arbitrary(g: &mut Gen) -> Self { - RandomSetupConnectionError(SetupConnectionError::from_gen(g)) - } - } - - #[quickcheck_macros::quickcheck] - fn encode_with_c_setup_connection_error(message: RandomSetupConnectionError) -> bool { - let expected = message.clone().0; - - let mut encoder = Encoder::::new(); - let mut decoder = StandardDecoder::>::new(); - - let frame = StandardSv2Frame::from_message( - message.0, - MESSAGE_TYPE_SETUP_CONNECTION_ERROR, - 0, - false, - ) - .unwrap(); - let encoded_frame = encoder.encode(frame).unwrap(); - - let buffer = decoder.writable(); - for i in 0..buffer.len() { - buffer[i] = encoded_frame[i] - } - let _ = decoder.next_frame(); - - let buffer = decoder.writable(); - for i in 0..buffer.len() { - buffer[i] = encoded_frame[i + 6] - } - - let mut decoded = decoder.next_frame().unwrap(); - - let msg_type = decoded.get_header().unwrap().msg_type(); - let payload = decoded.payload(); - let decoded_message: Sv2Message = (msg_type, payload).try_into().unwrap(); - let decoded_message = match decoded_message { - Sv2Message::SetupConnectionError(m) => m, - _ => panic!(), - }; - - decoded_message.flags == expected.flags - } - - #[derive(Clone, Debug)] - pub struct RandomSetupConnectionSuccess(pub SetupConnectionSuccess); - - #[cfg(feature = "prop_test")] - impl Arbitrary for RandomSetupConnectionSuccess { - fn arbitrary(g: &mut Gen) -> Self { - RandomSetupConnectionSuccess(SetupConnectionSuccess::from_gen(g)) - } - } - #[quickcheck_macros::quickcheck] - fn encode_with_c_setup_connection_success(message: RandomSetupConnectionSuccess) -> bool { - let expected = message.clone().0; - - let mut encoder = Encoder::::new(); - let mut decoder = StandardDecoder::>::new(); - - let frame = StandardSv2Frame::from_message( - message.0, - MESSAGE_TYPE_SETUP_CONNECTION_SUCCESS, - 0, - false, - ) - .unwrap(); - let encoded_frame = encoder.encode(frame).unwrap(); - - let buffer = decoder.writable(); - for i in 0..buffer.len() { - buffer[i] = encoded_frame[i] - } - let _ = decoder.next_frame(); - - let buffer = decoder.writable(); - for i in 0..buffer.len() { - buffer[i] = encoded_frame[i + 6] - } - - let mut decoded = decoder.next_frame().unwrap(); - - let msg_type = decoded.get_header().unwrap().msg_type(); - let payload = decoded.payload(); - let decoded_message: Sv2Message = (msg_type, payload).try_into().unwrap(); - let decoded_message = match decoded_message { - Sv2Message::SetupConnectionSuccess(m) => m, - _ => panic!(), - }; - - decoded_message.flags == expected.flags - } -} diff --git a/protocols/v2/sv2-ffi/sv2.h b/protocols/v2/sv2-ffi/sv2.h deleted file mode 100644 index 45f1c17cbe..0000000000 --- a/protocols/v2/sv2-ffi/sv2.h +++ /dev/null @@ -1,712 +0,0 @@ -#include -#include -#include -#include -#include - -/// Identifier for the extension_type field in the SV2 frame, indicating no -/// extensions. -static const uint16_t EXTENSION_TYPE_NO_EXTENSION = 0; - -/// Size of the SV2 frame header in bytes. -static const uintptr_t SV2_FRAME_HEADER_SIZE = 6; - -/// Maximum size of an SV2 frame chunk in bytes. -static const uintptr_t SV2_FRAME_CHUNK_SIZE = 65535; - -/// Size of the MAC for supported AEAD encryption algorithm (ChaChaPoly). -static const uintptr_t AEAD_MAC_LEN = 16; - -/// Size of the encrypted SV2 frame header, including the MAC. -static const uintptr_t ENCRYPTED_SV2_FRAME_HEADER_SIZE = (SV2_FRAME_HEADER_SIZE + AEAD_MAC_LEN); - -/// Size of the Noise protocol frame header in bytes. -static const uintptr_t NOISE_FRAME_HEADER_SIZE = 2; - -static const uintptr_t NOISE_FRAME_HEADER_LEN_OFFSET = 0; - -/// Size in bytes of the encoded elliptic curve point using ElligatorSwift -/// encoding. This encoding produces a 64-byte representation of the -/// X-coordinate of a secp256k1 curve point. -static const uintptr_t ELLSWIFT_ENCODING_SIZE = 64; - -static const uintptr_t MAC = 16; - -/// Size in bytes of the encrypted ElligatorSwift encoded data, which includes -/// the original ElligatorSwift encoded data and a MAC for integrity -/// verification. -static const uintptr_t ENCRYPTED_ELLSWIFT_ENCODING_SIZE = (ELLSWIFT_ENCODING_SIZE + MAC); - -/// Size in bytes of the SIGNATURE_NOISE_MESSAGE, which contains information and -/// a signature for the handshake initiator, formatted according to the Noise -/// Protocol specifications. -static const uintptr_t SIGNATURE_NOISE_MESSAGE_SIZE = 74; - -/// Size in bytes of the encrypted signature noise message, which includes the -/// SIGNATURE_NOISE_MESSAGE and a MAC for integrity verification. -static const uintptr_t ENCRYPTED_SIGNATURE_NOISE_MESSAGE_SIZE = (SIGNATURE_NOISE_MESSAGE_SIZE + MAC); - -/// Size in bytes of the handshake message expected by the initiator, -/// encompassing: -/// - ElligatorSwift encoded public key -/// - Encrypted ElligatorSwift encoding -/// - Encrypted SIGNATURE_NOISE_MESSAGE -static const uintptr_t INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_SIZE = ((ELLSWIFT_ENCODING_SIZE + ENCRYPTED_ELLSWIFT_ENCODING_SIZE) + ENCRYPTED_SIGNATURE_NOISE_MESSAGE_SIZE); - -static const uint8_t SV2_MINING_PROTOCOL_DISCRIMINANT = 0; - -static const uint8_t SV2_JOB_DECLARATION_PROTOCOL_DISCRIMINANT = 1; - -static const uint8_t SV2_TEMPLATE_DISTR_PROTOCOL_DISCRIMINANT = 2; - -static const uint8_t MESSAGE_TYPE_SETUP_CONNECTION = 0; - -static const uint8_t MESSAGE_TYPE_SETUP_CONNECTION_SUCCESS = 1; - -static const uint8_t MESSAGE_TYPE_SETUP_CONNECTION_ERROR = 2; - -static const uint8_t MESSAGE_TYPE_CHANNEL_ENDPOINT_CHANGED = 3; - -static const uint8_t MESSAGE_TYPE_OPEN_STANDARD_MINING_CHANNEL = 16; - -static const uint8_t MESSAGE_TYPE_OPEN_STANDARD_MINING_CHANNEL_SUCCESS = 17; - -static const uint8_t MESSAGE_TYPE_OPEN_MINING_CHANNEL_ERROR = 18; - -static const uint8_t MESSAGE_TYPE_OPEN_EXTENDED_MINING_CHANNEL = 19; - -static const uint8_t MESSAGE_TYPE_OPEN_EXTENDED_MINING_CHANNEL_SUCCES = 20; - -static const uint8_t MESSAGE_TYPE_NEW_MINING_JOB = 21; - -static const uint8_t MESSAGE_TYPE_UPDATE_CHANNEL = 22; - -static const uint8_t MESSAGE_TYPE_UPDATE_CHANNEL_ERROR = 23; - -static const uint8_t MESSAGE_TYPE_CLOSE_CHANNEL = 24; - -static const uint8_t MESSAGE_TYPE_SET_EXTRANONCE_PREFIX = 25; - -static const uint8_t MESSAGE_TYPE_SUBMIT_SHARES_STANDARD = 26; - -static const uint8_t MESSAGE_TYPE_SUBMIT_SHARES_EXTENDED = 27; - -static const uint8_t MESSAGE_TYPE_SUBMIT_SHARES_SUCCESS = 28; - -static const uint8_t MESSAGE_TYPE_SUBMIT_SHARES_ERROR = 29; - -static const uint8_t MESSAGE_TYPE_NEW_EXTENDED_MINING_JOB = 31; - -static const uint8_t MESSAGE_TYPE_MINING_SET_NEW_PREV_HASH = 32; - -static const uint8_t MESSAGE_TYPE_SET_TARGET = 33; - -static const uint8_t MESSAGE_TYPE_SET_CUSTOM_MINING_JOB = 34; - -static const uint8_t MESSAGE_TYPE_SET_CUSTOM_MINING_JOB_SUCCESS = 35; - -static const uint8_t MESSAGE_TYPE_SET_CUSTOM_MINING_JOB_ERROR = 36; - -static const uint8_t MESSAGE_TYPE_RECONNECT = 37; - -static const uint8_t MESSAGE_TYPE_SET_GROUP_CHANNEL = 38; - -static const uint8_t MESSAGE_TYPE_ALLOCATE_MINING_JOB_TOKEN = 80; - -static const uint8_t MESSAGE_TYPE_ALLOCATE_MINING_JOB_TOKEN_SUCCESS = 81; - -static const uint8_t MESSAGE_TYPE_PROVIDE_MISSING_TRANSACTIONS = 85; - -static const uint8_t MESSAGE_TYPE_PROVIDE_MISSING_TRANSACTIONS_SUCCESS = 86; - -static const uint8_t MESSAGE_TYPE_DECLARE_MINING_JOB = 87; - -static const uint8_t MESSAGE_TYPE_DECLARE_MINING_JOB_SUCCESS = 88; - -static const uint8_t MESSAGE_TYPE_DECLARE_MINING_JOB_ERROR = 89; - -static const uint8_t MESSAGE_TYPE_PUSH_SOLUTION = 96; - -static const uint8_t MESSAGE_TYPE_COINBASE_OUTPUT_CONSTRAINTS = 112; - -static const uint8_t MESSAGE_TYPE_NEW_TEMPLATE = 113; - -static const uint8_t MESSAGE_TYPE_SET_NEW_PREV_HASH = 114; - -static const uint8_t MESSAGE_TYPE_REQUEST_TRANSACTION_DATA = 115; - -static const uint8_t MESSAGE_TYPE_REQUEST_TRANSACTION_DATA_SUCCESS = 116; - -static const uint8_t MESSAGE_TYPE_REQUEST_TRANSACTION_DATA_ERROR = 117; - -static const uint8_t MESSAGE_TYPE_SUBMIT_SOLUTION = 118; - -static const bool CHANNEL_BIT_SETUP_CONNECTION = false; - -static const bool CHANNEL_BIT_SETUP_CONNECTION_SUCCESS = false; - -static const bool CHANNEL_BIT_SETUP_CONNECTION_ERROR = false; - -static const bool CHANNEL_BIT_CHANNEL_ENDPOINT_CHANGED = true; - -static const bool CHANNEL_BIT_COINBASE_OUTPUT_CONSTRAINTS = false; - -static const bool CHANNEL_BIT_NEW_TEMPLATE = false; - -static const bool CHANNEL_BIT_SET_NEW_PREV_HASH = false; - -static const bool CHANNEL_BIT_REQUEST_TRANSACTION_DATA = false; - -static const bool CHANNEL_BIT_REQUEST_TRANSACTION_DATA_SUCCESS = false; - -static const bool CHANNEL_BIT_REQUEST_TRANSACTION_DATA_ERROR = false; - -static const bool CHANNEL_BIT_SUBMIT_SOLUTION = false; - -static const bool CHANNEL_BIT_ALLOCATE_MINING_JOB_TOKEN = false; - -static const bool CHANNEL_BIT_ALLOCATE_MINING_JOB_TOKEN_SUCCESS = false; - -static const bool CHANNEL_BIT_DECLARE_MINING_JOB = false; - -static const bool CHANNEL_BIT_DECLARE_MINING_JOB_SUCCESS = false; - -static const bool CHANNEL_BIT_DECLARE_MINING_JOB_ERROR = false; - -static const bool CHANNEL_BIT_PROVIDE_MISSING_TRANSACTIONS = false; - -static const bool CHANNEL_BIT_PROVIDE_MISSING_TRANSACTIONS_SUCCESS = false; - -static const bool CHANNEL_BIT_SUBMIT_SOLUTION_JD = true; - -static const bool CHANNEL_BIT_CLOSE_CHANNEL = true; - -static const bool CHANNEL_BIT_NEW_EXTENDED_MINING_JOB = true; - -static const bool CHANNEL_BIT_NEW_MINING_JOB = true; - -static const bool CHANNEL_BIT_OPEN_EXTENDED_MINING_CHANNEL = false; - -static const bool CHANNEL_BIT_OPEN_EXTENDED_MINING_CHANNEL_SUCCES = false; - -static const bool CHANNEL_BIT_OPEN_MINING_CHANNEL_ERROR = false; - -static const bool CHANNEL_BIT_OPEN_STANDARD_MINING_CHANNEL = false; - -static const bool CHANNEL_BIT_OPEN_STANDARD_MINING_CHANNEL_SUCCESS = false; - -static const bool CHANNEL_BIT_RECONNECT = false; - -static const bool CHANNEL_BIT_SET_CUSTOM_MINING_JOB = false; - -static const bool CHANNEL_BIT_SET_CUSTOM_MINING_JOB_ERROR = false; - -static const bool CHANNEL_BIT_SET_CUSTOM_MINING_JOB_SUCCESS = false; - -static const bool CHANNEL_BIT_SET_EXTRANONCE_PREFIX = true; - -static const bool CHANNEL_BIT_SET_GROUP_CHANNEL = false; - -static const bool CHANNEL_BIT_MINING_SET_NEW_PREV_HASH = true; - -static const bool CHANNEL_BIT_SET_TARGET = true; - -static const bool CHANNEL_BIT_SUBMIT_SHARES_ERROR = true; - -static const bool CHANNEL_BIT_SUBMIT_SHARES_EXTENDED = true; - -static const bool CHANNEL_BIT_SUBMIT_SHARES_STANDARD = true; - -static const bool CHANNEL_BIT_SUBMIT_SHARES_SUCCESS = true; - -static const bool CHANNEL_BIT_UPDATE_CHANNEL = true; - -static const bool CHANNEL_BIT_UPDATE_CHANNEL_ERROR = true; -#include -#include -#include -#include -#include - -/// A struct to facilitate transferring a `Vec` across FFI boundaries. -struct CVec { - uint8_t *data; - uintptr_t len; - uintptr_t capacity; -}; - -/// A struct to manage a collection of `CVec` objects across FFI boundaries. -struct CVec2 { - CVec *data; - uintptr_t len; - uintptr_t capacity; -}; - -/// Represents a 24-bit unsigned integer (`U24`), supporting SV2 serialization and deserialization. -/// Only first 3 bytes of a u32 is considered to get the SV2 value, and rest are ignored (in little -/// endian). -struct U24 { - uint32_t _0; -}; - -extern "C" { - -/// Creates a `CVec` from a buffer that was allocated in C. -/// -/// # Safety -/// The caller must ensure that the buffer is valid and that -/// the data length does not exceed the allocated size. -CVec cvec_from_buffer(const uint8_t *data, uintptr_t len); - -/// Initializes an empty `CVec2`. -/// -/// # Safety -/// The caller is responsible for freeing the `CVec2` when it is no longer needed. -CVec2 init_cvec2(); - -/// Adds a `CVec` to a `CVec2`. -/// -/// # Safety -/// The caller must ensure no duplicate `CVec`s are added, as duplicates may -/// lead to double-free errors when the message is dropped. -void cvec2_push(CVec2 *cvec2, CVec cvec); - -/// Exported FFI functions for interoperability with C code for u24 -void _c_export_u24(U24 _a); - -/// Exported FFI functions for interoperability with C code for CVec -void _c_export_cvec(CVec _a); - -/// Exported FFI functions for interoperability with C code for CVec2 -void _c_export_cvec2(CVec2 _a); - -} // extern "C" -#include -#include -#include -#include -#include - -/// This enum has a list of the different Stratum V2 subprotocols. -enum class Protocol : uint8_t { - /// Mining protocol. - MiningProtocol = SV2_MINING_PROTOCOL_DISCRIMINANT, - /// Job declaration protocol. - JobDeclarationProtocol = SV2_JOB_DECLARATION_PROTOCOL_DISCRIMINANT, - /// Template distribution protocol. - TemplateDistributionProtocol = SV2_TEMPLATE_DISTR_PROTOCOL_DISCRIMINANT, -}; - -/// Message used by an upstream role for announcing a mining channel endpoint change. -/// -/// This message should be sent when a mining channel’s upstream or downstream endpoint changes and -/// that channel had previously exchanged message(s) with `channel_msg` bitset of unknown -/// `extension_type`. -/// -/// When a downstream receives such a message, any extension state (including version and extension -/// support) must be reset and renegotiated. -struct ChannelEndpointChanged { - /// Unique identifier of the channel that has changed its endpoint. - uint32_t channel_id; -}; - -/// Message used by an upstream role to accept a connection setup request from a downstream role. -/// -/// This message is sent in response to a [`SetupConnection`] message. -struct SetupConnectionSuccess { - /// Selected version based on the [`SetupConnection::min_version`] and - /// [`SetupConnection::max_version`] sent by the downstream role. - /// - /// This version will be used on the connection for the rest of its life. - uint16_t used_version; - /// Flags indicating optional protocol features supported by the upstream. - /// - /// The downstream is required to verify this set of flags and act accordingly. - /// - /// Each [`SetupConnection::protocol`] field has its own values/flags. - uint32_t flags; -}; - -/// C representation of [`SetupConnection`] -struct CSetupConnection { - /// Protocol to be used for the connection. - Protocol protocol; - /// The minimum protocol version supported. - /// - /// Currently must be set to 2. - uint16_t min_version; - /// The maximum protocol version supported. - /// - /// Currently must be set to 2. - uint16_t max_version; - /// Flags indicating optional protocol features supported by the downstream. - /// - /// Each [`SetupConnection::protocol`] value has it's own flags. - uint32_t flags; - /// ASCII representation of the connection hostname or IP address. - CVec endpoint_host; - /// Connection port value. - uint16_t endpoint_port; - /// Device vendor name. - CVec vendor; - /// Device hardware version. - CVec hardware_version; - /// Device firmware version. - CVec firmware; - /// Device identifier. - CVec device_id; -}; - -/// C representation of [`SetupConnectionError`] -struct CSetupConnectionError { - uint32_t flags; - CVec error_code; -}; - -extern "C" { - -/// A C-compatible function that exports the [`ChannelEndpointChanged`] struct. -void _c_export_channel_endpoint_changed(ChannelEndpointChanged _a); - -/// A C-compatible function that exports the `SetupConnection` struct. -void _c_export_setup_conn_succ(SetupConnectionSuccess _a); - -void free_setup_connection(CSetupConnection s); - -void free_setup_connection_error(CSetupConnectionError s); - -} // extern "C" -#include -#include -#include -#include -#include - -/// Message used by a downstream to indicate the size of the additional bytes they will need in -/// coinbase transaction outputs. -/// -/// As the pool is responsible for adding coinbase transaction outputs for payouts and other uses, -/// the Template Provider will need to consider this reserved space when selecting transactions for -/// inclusion in a block(to avoid an invalid, oversized block). Thus, this message indicates that -/// additional space in the block/coinbase transaction must be reserved for, assuming they will use -/// the entirety of this space. -/// -/// The Job Declarator **must** discover the maximum serialized size of the additional outputs which -/// will be added by the pools it intends to use this work. It then **must** communicate the sum of -/// such size to the Template Provider via this message. -/// -/// The Template Provider **must not** provide [`NewTemplate`] messages which would represent -/// consensus-invalid blocks once this additional size — along with a maximally-sized (100 byte) -/// coinbase field — is added. Further, the Template Provider **must** consider the maximum -/// additional bytes required in the output count variable-length integer in the coinbase -/// transaction when complying with the size limits. -/// -/// [`NewTemplate`]: crate::NewTemplate -struct CoinbaseOutputConstraints { - /// Additional serialized bytes needed in coinbase transaction outputs. - uint32_t coinbase_output_max_additional_size; - /// Additional sigops needed in coinbase transaction outputs. - uint16_t coinbase_output_max_additional_sigops; -}; - -/// Message used by a downstream to request data about all transactions in a block template. -/// -/// Data includes the full transaction data and any additional data required to block validation. -/// -/// Note that the coinbase transaction is excluded from this data. -struct RequestTransactionData { - /// Identifier of the template that the downstream node is requesting transaction data for. - /// - /// This must be identical to previously exchanged [`crate::NewTemplate::template_id`]. - uint64_t template_id; -}; - -/// C representation of [`NewTemplate`]. -struct CNewTemplate { - uint64_t template_id; - bool future_template; - uint32_t version; - uint32_t coinbase_tx_version; - CVec coinbase_prefix; - uint32_t coinbase_tx_input_sequence; - uint64_t coinbase_tx_value_remaining; - uint32_t coinbase_tx_outputs_count; - CVec coinbase_tx_outputs; - uint32_t coinbase_tx_locktime; - CVec2 merkle_path; -}; - -/// C representation of [`RequestTransactionDataSuccess`]. -struct CRequestTransactionDataSuccess { - uint64_t template_id; - CVec excess_data; - CVec2 transaction_list; -}; - -/// C representation of [`RequestTransactionDataError`]. -struct CRequestTransactionDataError { - uint64_t template_id; - CVec error_code; -}; - -/// C representation of [`SetNewPrevHash`]. -struct CSetNewPrevHash { - uint64_t template_id; - CVec prev_hash; - uint32_t header_timestamp; - uint32_t n_bits; - CVec target; -}; - -/// C representation of [`SubmitSolution`]. -struct CSubmitSolution { - uint64_t template_id; - uint32_t version; - uint32_t header_timestamp; - uint32_t header_nonce; - CVec coinbase_tx; -}; - -extern "C" { - -/// Exports the [`CoinbaseOutputConstraints`] struct to C. -void _c_export_coinbase_out(CoinbaseOutputConstraints _a); - -/// Exports the [`RequestTransactionData`] struct to C. -void _c_export_req_tx_data(RequestTransactionData _a); - -/// Drops the [`CNewTemplate`] object. -void free_new_template(CNewTemplate s); - -/// Drops the CRequestTransactionDataSuccess object. -void free_request_tx_data_success(CRequestTransactionDataSuccess s); - -/// Drops the CRequestTransactionDataError object. -void free_request_tx_data_error(CRequestTransactionDataError s); - -/// Drops the CSetNewPrevHash object. -void free_set_new_prev_hash(CSetNewPrevHash s); - -/// Drops the CSubmitSolution object. -void free_submit_solution(CSubmitSolution s); - -} // extern "C" -#include -#include -#include -#include -#include - -/// C-compatible enumeration of possible errors in the `codec_sv2` module. -/// -/// This enum mirrors the [`Error`] enum but is designed to be used in C code through FFI. It -/// represents the same set of errors as [`Error`], making them accessible to C programs. -struct CError { - enum class Tag { - /// AEAD (`snow`) error in the Noise protocol. - AeadError, - /// Binary Sv2 data format error. - BinarySv2Error, - /// Framing Sv2 error. - FramingError, - /// Framing Sv2 error. - FramingSv2Error, - /// Invalid step for initiator in the Noise protocol. - InvalidStepForInitiator, - /// Invalid step for responder in the Noise protocol. - InvalidStepForResponder, - /// Missing bytes in the Noise protocol. - MissingBytes, - /// Sv2 Noise protocol error. - NoiseSv2Error, - /// Noise protocol is not in the expected handshake state. - NotInHandShakeState, - /// Unexpected state in the Noise protocol. - UnexpectedNoiseState, - }; - - struct MissingBytes_Body { - uintptr_t _0; - }; - - Tag tag; - union { - MissingBytes_Body missing_bytes; - }; -}; - -extern "C" { - -/// Force `cbindgen` to create a header for [`CError`]. -/// -/// It ensures that [`CError`] is included in the generated C header file. This function is not -/// meant to be called and will panic if called. Its only purpose is to make [`CError`] visible to -/// `cbindgen`. -CError export_cerror(); - -} // extern "C" -#include -#include -#include -#include -#include - -struct DecoderWrapper; - -struct EncoderWrapper; - -struct CSv2Message { - enum class Tag { - CoinbaseOutputConstraints, - NewTemplate, - RequestTransactionData, - RequestTransactionDataError, - RequestTransactionDataSuccess, - SetNewPrevHash, - SubmitSolution, - ChannelEndpointChanged, - SetupConnection, - SetupConnectionError, - SetupConnectionSuccess, - }; - - struct CoinbaseOutputConstraints_Body { - CoinbaseOutputConstraints _0; - }; - - struct NewTemplate_Body { - CNewTemplate _0; - }; - - struct RequestTransactionData_Body { - RequestTransactionData _0; - }; - - struct RequestTransactionDataError_Body { - CRequestTransactionDataError _0; - }; - - struct RequestTransactionDataSuccess_Body { - CRequestTransactionDataSuccess _0; - }; - - struct SetNewPrevHash_Body { - CSetNewPrevHash _0; - }; - - struct SubmitSolution_Body { - CSubmitSolution _0; - }; - - struct ChannelEndpointChanged_Body { - ChannelEndpointChanged _0; - }; - - struct SetupConnection_Body { - CSetupConnection _0; - }; - - struct SetupConnectionError_Body { - CSetupConnectionError _0; - }; - - struct SetupConnectionSuccess_Body { - SetupConnectionSuccess _0; - }; - - Tag tag; - union { - CoinbaseOutputConstraints_Body coinbase_output_constraints; - NewTemplate_Body new_template; - RequestTransactionData_Body request_transaction_data; - RequestTransactionDataError_Body request_transaction_data_error; - RequestTransactionDataSuccess_Body request_transaction_data_success; - SetNewPrevHash_Body set_new_prev_hash; - SubmitSolution_Body submit_solution; - ChannelEndpointChanged_Body channel_endpoint_changed; - SetupConnection_Body setup_connection; - SetupConnectionError_Body setup_connection_error; - SetupConnectionSuccess_Body setup_connection_success; - }; -}; - -struct Sv2Error { - enum class Tag { - BinaryError, - CodecError, - EncoderBusy, - InvalidSv2Frame, - MissingBytes, - PayloadTooBig, - Unknown, - }; - - struct BinaryError_Body { - CError _0; - }; - - struct CodecError_Body { - CError _0; - }; - - struct PayloadTooBig_Body { - CVec _0; - }; - - Tag tag; - union { - BinaryError_Body binary_error; - CodecError_Body codec_error; - PayloadTooBig_Body payload_too_big; - }; -}; - -template -struct CResult { - enum class Tag { - Ok, - Err, - }; - - struct Ok_Body { - T _0; - }; - - struct Err_Body { - E _0; - }; - - Tag tag; - union { - Ok_Body ok; - Err_Body err; - }; -}; - -extern "C" { - -void drop_sv2_message(CSv2Message s); - -/// This function does nothing unless there is some heap allocated data owned by the C side that -/// needs to be dropped (specifically a `CVec`). In this case, `free_vec` is used in order to drop -/// that memory. -void drop_sv2_error(Sv2Error s); - -bool is_ok(const CResult *cresult); - -EncoderWrapper *new_encoder(); - -void flush_encoder(EncoderWrapper *encoder); - -void free_decoder(DecoderWrapper *decoder); - -/// # Safety -CResult encode(CSv2Message *message, EncoderWrapper *encoder); - -DecoderWrapper *new_decoder(); - -CVec get_writable(DecoderWrapper *decoder); - -CResult next_frame(DecoderWrapper *decoder); - -} // extern "C" diff --git a/roles/Cargo.lock b/roles/Cargo.lock index 525f4c69b8..dc9070d428 100644 --- a/roles/Cargo.lock +++ b/roles/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aead" @@ -58,21 +58,30 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "once_cell", "version_check", ] [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", "once_cell", "version_check", - "zerocopy 0.7.35", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", ] [[package]] @@ -81,11 +90,17 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", @@ -98,44 +113,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", - "once_cell", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arraydeque" @@ -162,9 +177,9 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", @@ -174,62 +189,131 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.1" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" dependencies = [ "async-task", "concurrent-queue", - "fastrand", - "futures-lite", + "fastrand 2.3.0", + "futures-lite 2.6.1", + "pin-project-lite", "slab", ] +[[package]] +name = "async-fs" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "blocking", + "futures-lite 1.13.0", +] + [[package]] name = "async-global-executor" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ - "async-channel 2.3.1", + "async-channel 2.5.0", "async-executor", - "async-io", - "async-lock", + "async-io 2.6.0", + "async-lock 3.4.1", "blocking", - "futures-lite", + "futures-lite 2.6.1", "once_cell", ] [[package]] name = "async-io" -version = "2.4.0" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite 1.13.0", + "log", + "parking", + "polling 2.8.0", + "rustix 0.37.28", + "slab", + "socket2 0.4.10", + "waker-fn", +] + +[[package]] +name = "async-io" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" dependencies = [ - "async-lock", + "autocfg", "cfg-if", "concurrent-queue", "futures-io", - "futures-lite", + "futures-lite 2.6.1", "parking", - "polling", - "rustix 0.38.44", + "polling 3.11.0", + "rustix 1.1.2", "slab", - "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] name = "async-lock" -version = "3.4.0" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" dependencies = [ - "event-listener 5.4.0", + "event-listener 5.4.1", "event-listener-strategy", "pin-project-lite", ] +[[package]] +name = "async-net" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0434b1ed18ce1cf5769b8ac540e33f01fa9471058b5e89da9e06f3c882a8c12f" +dependencies = [ + "async-io 1.13.0", + "blocking", + "futures-lite 1.13.0", +] + +[[package]] +name = "async-process" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" +dependencies = [ + "async-io 1.13.0", + "async-lock 2.8.0", + "async-signal", + "blocking", + "cfg-if", + "event-listener 3.1.0", + "futures-lite 1.13.0", + "rustix 0.38.44", + "windows-sys 0.48.0", +] + [[package]] name = "async-recursion" version = "0.3.2" @@ -249,24 +333,42 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io 2.6.0", + "async-lock 3.4.1", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.2", + "signal-hook-registry", + "slab", + "windows-sys 0.61.0", ] [[package]] name = "async-std" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730294c1c08c2e0f85759590518f6333f0d5a0a766a27d519c1b244c3dfd8a24" +checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" dependencies = [ "async-channel 1.9.0", "async-global-executor", - "async-io", - "async-lock", + "async-io 2.6.0", + "async-lock 3.4.1", "crossbeam-utils", "futures-channel", "futures-core", "futures-io", - "futures-lite", + "futures-lite 2.6.1", "gloo-timers", "kv-log-macro", "log", @@ -286,13 +388,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -303,15 +405,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -319,7 +421,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -344,6 +446,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bech32" version = "0.11.0" @@ -352,14 +460,14 @@ checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" [[package]] name = "binary_codec_sv2" -version = "2.0.0" +version = "3.0.0" dependencies = [ "buffer_sv2", ] [[package]] name = "binary_sv2" -version = "3.0.0" +version = "4.0.0" dependencies = [ "binary_codec_sv2", "derive_codec_sv2", @@ -367,9 +475,9 @@ dependencies = [ [[package]] name = "bitcoin" -version = "0.32.5" +version = "0.32.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026" +checksum = "0fda569d741b895131a88ee5589a467e73e9c4718e958ac9308e4f7dc44b6945" dependencies = [ "base58ck", "base64 0.21.7", @@ -447,9 +555,15 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.9.0" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" dependencies = [ "serde", ] @@ -486,14 +600,14 @@ dependencies = [ [[package]] name = "blocking" -version = "1.6.1" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ - "async-channel 2.3.1", + "async-channel 2.5.0", "async-task", "futures-io", - "futures-lite", + "futures-lite 2.6.1", "piper", ] @@ -511,13 +625,14 @@ name = "buffer_sv2" version = "2.0.0" dependencies = [ "aes-gcm", + "generic-array", ] [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "byte-slice-cast" @@ -537,20 +652,27 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" -version = "1.2.17" +version = "1.2.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" +checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9" dependencies = [ + "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "chacha20" @@ -576,6 +698,47 @@ dependencies = [ "zeroize", ] +[[package]] +name = "channels_sv2" +version = "2.0.0" +dependencies = [ + "binary_sv2", + "bitcoin", + "common_messages_sv2", + "job_declaration_sv2", + "mining_sv2", + "primitive-types", + "template_distribution_sv2", + "tracing", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "cipher" version = "0.4.4" @@ -589,9 +752,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.39" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" dependencies = [ "clap_builder", "clap_derive", @@ -599,9 +762,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.39" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" +checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" dependencies = [ "anstream", "anstyle", @@ -611,47 +774,45 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "codec_sv2" -version = "2.1.0" +version = "3.0.1" dependencies = [ "binary_sv2", "buffer_sv2", "framing_sv2", "noise_sv2", "rand 0.8.5", - "stratum-common", "tracing", ] [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "common_messages_sv2" -version = "5.1.0" +version = "6.0.1" dependencies = [ "binary_sv2", - "stratum-common", ] [[package]] @@ -683,12 +844,13 @@ dependencies = [ ] [[package]] -name = "config-helpers" +name = "config_helpers_sv2" version = "0.1.0" dependencies = [ "miniscript", - "roles_logic_sv2", "serde", + "tracing", + "tracing-subscriber", ] [[package]] @@ -706,7 +868,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "once_cell", "tiny-keccak", ] @@ -740,6 +902,22 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "corepc-client" version = "0.7.0" @@ -756,9 +934,9 @@ dependencies = [ [[package]] name = "corepc-node" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cb0b5b9e99b8290eeac6cdccfa4f86821fb49011480d86111d85e26287d128" +checksum = "b2bcc6e09458f052024ec36e4728bd5619e248643da6175876eb3b10ca6d4d86" dependencies = [ "anyhow", "corepc-client", @@ -790,13 +968,52 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "async-std", + "cast", + "ciborium", + "clap", + "criterion-plot", + "csv", + "futures", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "regex", + "serde", + "serde_derive", + "serde_json", + "smol", + "tinytemplate", + "tokio", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -805,9 +1022,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" @@ -820,6 +1037,27 @@ dependencies = [ "typenum", ] +[[package]] +name = "csv" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" +dependencies = [ + "memchr", +] + [[package]] name = "ctr" version = "0.9.2" @@ -864,6 +1102,12 @@ dependencies = [ "const-random", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -881,12 +1125,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -901,9 +1145,20 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "5.4.0" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", @@ -916,10 +1171,19 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "event-listener 5.4.0", + "event-listener 5.4.1", "pin-project-lite", ] +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -928,16 +1192,22 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "filetime" -version = "0.2.25" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" + [[package]] name = "fixed-hash" version = "0.8.0" @@ -952,9 +1222,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", "miniz_oxide", @@ -968,12 +1238,11 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "framing_sv2" -version = "5.1.0" +version = "5.0.1" dependencies = [ "binary_sv2", "buffer_sv2", "noise_sv2", - "stratum-common", ] [[package]] @@ -1032,11 +1301,26 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.6.0" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ - "fastrand", + "fastrand 2.3.0", "futures-core", "futures-io", "parking", @@ -1051,7 +1335,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -1096,25 +1380,25 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] name = "getrandom" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi 0.14.7+wasi-0.2.4", ] [[package]] @@ -1147,9 +1431,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.8" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -1164,6 +1448,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc52e53916c08643f1b56ec082790d1e86a32e58dc5268f897f313fbae7b4872" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "handlers_sv2" +version = "0.2.0" +dependencies = [ + "binary_sv2", + "common_messages_sv2", + "job_declaration_sv2", + "mining_sv2", + "parsers_sv2", + "template_distribution_sv2", + "trait-variant", +] + [[package]] name = "hashbrown" version = "0.11.2" @@ -1180,15 +1487,15 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash 0.8.11", + "ahash 0.8.12", "allocator-api2", ] [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" [[package]] name = "hashlink" @@ -1207,14 +1514,20 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.4.0" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] @@ -1295,13 +1608,14 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http", "http-body", @@ -1309,6 +1623,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -1316,21 +1631,28 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", + "futures-core", "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.0", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -1350,17 +1672,17 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] name = "indexmap" -version = "2.8.0" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.16.0", ] [[package]] @@ -1372,42 +1694,94 @@ dependencies = [ "generic-array", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "integration_tests_sv2" version = "0.1.0" dependencies = [ "async-channel 1.9.0", - "binary_sv2", - "codec_sv2", - "config-helpers", + "config_helpers_sv2", "corepc-node", "flate2", - "jd_client", + "jd_client_sv2", "jd_server", "key-utils", "mining_device", - "mining_device_sv1", - "mining_proxy_sv2", "minreq", - "network_helpers_sv2", "once_cell", "pool_sv2", - "rand 0.9.0", - "roles_logic_sv2", + "rand 0.9.2", "stratum-common", "tar", "tokio", + "tokio-util", "tracing", "tracing-subscriber", "translator_sv2", ] +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags 2.9.4", + "cfg-if", + "libc", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi 0.5.2", + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1415,30 +1789,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] -name = "jd_client" -version = "0.1.4" +name = "jd_client_sv2" +version = "0.1.0" dependencies = [ "async-channel 1.9.0", - "async-recursion 0.3.2", - "binary_sv2", "buffer_sv2", "clap", - "codec_sv2", "config", - "config-helpers", - "error_handling", - "framing_sv2", - "futures", + "config_helpers_sv2", "key-utils", - "network_helpers_sv2", - "nohash-hasher", - "primitive-types", - "roles_logic_sv2", "serde", "stratum-common", "tokio", "tracing", - "tracing-subscriber", ] [[package]] @@ -1446,43 +1809,36 @@ name = "jd_server" version = "0.1.3" dependencies = [ "async-channel 1.9.0", - "binary_sv2", "buffer_sv2", "clap", - "codec_sv2", "config", - "config-helpers", + "config_helpers_sv2", "error_handling", "hashbrown 0.11.2", "hex", "key-utils", - "network_helpers_sv2", "nohash-hasher", - "noise_sv2", "rand 0.8.5", - "roles_logic_sv2", "rpc_sv2", "serde", "serde_json", "stratum-common", "tokio", "tracing", - "tracing-subscriber", ] [[package]] name = "job_declaration_sv2" -version = "4.0.0" +version = "5.0.1" dependencies = [ "binary_sv2", - "stratum-common", ] [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" dependencies = [ "once_cell", "wasm-bindgen", @@ -1516,6 +1872,7 @@ name = "key-utils" version = "1.2.0" dependencies = [ "bs58", + "generic-array", "rand 0.8.5", "rustversion", "secp256k1 0.28.2", @@ -1539,21 +1896,27 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.171" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags", + "bitflags 2.9.4", "libc", "redox_syscall", ] +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1562,15 +1925,15 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.3" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -1578,18 +1941,27 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" dependencies = [ "value-bag", ] +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "minimal-lexical" @@ -1603,59 +1975,17 @@ version = "0.1.3" dependencies = [ "async-channel 1.9.0", "async-recursion 0.3.2", - "binary_sv2", "buffer_sv2", "clap", - "codec_sv2", + "criterion", "futures", + "half", "key-utils", - "network_helpers_sv2", + "num-format", + "num_cpus", "primitive-types", "rand 0.8.5", - "roles_logic_sv2", - "sha2 0.10.8", - "stratum-common", - "tokio", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "mining_device_sv1" -version = "0.1.0" -dependencies = [ - "async-channel 1.9.0", - "num-bigint", - "num-traits", - "primitive-types", - "roles_logic_sv2", - "serde", - "serde_json", - "stratum-common", - "sv1_api", - "tokio", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "mining_proxy_sv2" -version = "0.1.3" -dependencies = [ - "async-channel 1.9.0", - "async-recursion 0.3.2", - "binary_sv2", - "buffer_sv2", - "clap", - "codec_sv2", - "config", - "futures", - "key-utils", - "network_helpers_sv2", - "nohash-hasher", - "once_cell", - "roles_logic_sv2", - "serde", + "sha2 0.10.9", "stratum-common", "tokio", "tracing", @@ -1664,17 +1994,16 @@ dependencies = [ [[package]] name = "mining_sv2" -version = "4.0.0" +version = "5.0.1" dependencies = [ "binary_sv2", - "stratum-common", ] [[package]] name = "miniscript" -version = "12.3.2" +version = "12.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0760e92feaf4ee26bd2e616f557de64712bf1e75f3b1b218dfb475c0a84c7943" +checksum = "487906208f38448e186e3deb02f2b8ef046a9078b0de00bdb28bf4fb9b76951c" dependencies = [ "bech32", "bitcoin", @@ -1682,18 +2011,18 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.8.5" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "minreq" -version = "2.13.2" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0c420feb01b9fb5061f8c8f452534361dd783756dcf38ec45191ce55e7a161" +checksum = "36a8e50e917e18a37d500d27d40b7bc7d127e71c0c94fb2d83f43b4afd308390" dependencies = [ "log", "once_cell", @@ -1706,26 +2035,24 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", ] [[package]] name = "network_helpers_sv2" -version = "3.1.0" +version = "4.0.1" dependencies = [ "async-channel 1.9.0", "async-std", - "binary_sv2", "codec_sv2", "futures", "serde_json", - "stratum-common", "sv1_api", "tokio", "tokio-util", @@ -1744,10 +2071,10 @@ version = "1.4.0" dependencies = [ "aes-gcm", "chacha20poly1305", + "generic-array", "rand 0.8.5", "rand_chacha 0.3.1", "secp256k1 0.28.2", - "stratum-common", ] [[package]] @@ -1762,40 +2089,40 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "overload", - "winapi", + "windows-sys 0.52.0", ] [[package]] -name = "num-bigint" -version = "0.4.6" +name = "num-format" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" dependencies = [ - "num-integer", - "num-traits", + "arrayvec", + "itoa", ] [[package]] -name = "num-integer" -version = "0.1.46" +name = "num-traits" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ - "num-traits", + "autocfg", ] [[package]] -name = "num-traits" -version = "0.2.19" +name = "num_cpus" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "autocfg", + "hermit-abi 0.5.2", + "libc", ] [[package]] @@ -1813,6 +2140,18 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -1829,17 +2168,11 @@ dependencies = [ "hashbrown 0.14.5", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "parity-scale-codec" -version = "3.7.4" +version = "3.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9fde3d0718baf5bc92f577d652001da0f8d54cd03a7974e118d04fc888dc23d" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" dependencies = [ "arrayvec", "bitvec", @@ -1853,14 +2186,14 @@ dependencies = [ [[package]] name = "parity-scale-codec-derive" -version = "3.7.4" +version = "3.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581c837bb6b9541ce7faa9377c20616e4fb7650f6b0f68bc93c827ee504fb7b3" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -1871,9 +2204,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -1881,15 +2214,27 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "parsers_sv2" +version = "0.1.1" +dependencies = [ + "binary_sv2", + "common_messages_sv2", + "framing_sv2", + "job_declaration_sv2", + "mining_sv2", + "template_distribution_sv2", ] [[package]] @@ -1898,11 +2243,17 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pest" -version = "2.8.0" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" +checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" dependencies = [ "memchr", "thiserror", @@ -1911,9 +2262,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.0" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" +checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" dependencies = [ "pest", "pest_generator", @@ -1921,26 +2272,26 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.0" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" +checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] name = "pest_meta" -version = "2.8.0" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" +checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" dependencies = [ "once_cell", "pest", - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] @@ -1962,23 +2313,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" dependencies = [ "atomic-waker", - "fastrand", + "fastrand 2.3.0", "futures-io", ] [[package]] name = "polling" -version = "3.7.4" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" dependencies = [ + "autocfg", + "bitflags 1.3.2", "cfg-if", "concurrent-queue", - "hermit-abi", + "libc", + "log", "pin-project-lite", - "rustix 0.38.44", - "tracing", - "windows-sys 0.59.0", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.5.2", + "pin-project-lite", + "rustix 1.1.2", + "windows-sys 0.61.0", ] [[package]] @@ -2010,26 +2376,21 @@ version = "0.1.3" dependencies = [ "async-channel 1.9.0", "async-recursion 1.1.1", - "binary_sv2", "buffer_sv2", "clap", - "codec_sv2", "config", - "config-helpers", + "config_helpers_sv2", "error_handling", "hex", "integration_tests_sv2", "key-utils", - "network_helpers_sv2", "nohash-hasher", - "noise_sv2", "rand 0.8.5", - "roles_logic_sv2", + "secp256k1 0.28.2", "serde", "stratum-common", "tokio", "tracing", - "tracing-subscriber", ] [[package]] @@ -2038,7 +2399,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.8.24", + "zerocopy", ] [[package]] @@ -2054,18 +2415,18 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit", + "toml_edit 0.23.6", ] [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -2081,9 +2442,9 @@ dependencies = [ [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "radium" @@ -2104,13 +2465,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", - "zerocopy 0.8.24", ] [[package]] @@ -2139,7 +2499,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] @@ -2148,18 +2508,47 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.3", ] [[package]] name = "redox_syscall" -version = "0.5.10" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags 2.9.4", +] + +[[package]] +name = "regex" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ - "bitflags", + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", ] +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + [[package]] name = "ring" version = "0.17.14" @@ -2168,7 +2557,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", @@ -2176,18 +2565,20 @@ dependencies = [ [[package]] name = "roles_logic_sv2" -version = "3.0.0" +version = "5.0.0" dependencies = [ - "binary_sv2", + "bitcoin", "chacha20poly1305", + "channels_sv2", + "codec_sv2", "common_messages_sv2", - "framing_sv2", + "handlers_sv2", "hex-conservative 0.3.0", "job_declaration_sv2", "mining_sv2", "nohash-hasher", + "parsers_sv2", "primitive-types", - "stratum-common", "template_distribution_sv2", "tracing", ] @@ -2199,14 +2590,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64 0.21.7", - "bitflags", + "bitflags 2.9.4", "serde", "serde_derive", ] [[package]] name = "rpc_sv2" -version = "1.0.0" +version = "1.1.1" dependencies = [ "base64 0.21.7", "hex", @@ -2230,9 +2621,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hex" @@ -2240,13 +2631,27 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" +[[package]] +name = "rustix" +version = "0.37.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "519165d378b97752ca44bbe15047d5d3409e875f39327546b42ac81d7e18c1b6" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + [[package]] name = "rustix" version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags", + "bitflags 2.9.4", "errno", "libc", "linux-raw-sys 0.4.15", @@ -2255,15 +2660,15 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.3" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags", + "bitflags 2.9.4", "errno", "libc", - "linux-raw-sys 0.9.3", - "windows-sys 0.59.0", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.0", ] [[package]] @@ -2290,9 +2695,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -2301,8 +2706,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] -name = "scopeguard" -version = "1.2.0" +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" @@ -2358,41 +2772,52 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.226" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] @@ -2412,13 +2837,23 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", "digest 0.10.7", + "sha2-asm", +] + +[[package]] +name = "sha2-asm" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b845214d6175804686b2bd482bcffe96651bb2d1200742b712003504a2dac1ab" +dependencies = [ + "cc", ] [[package]] @@ -2438,36 +2873,60 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] [[package]] name = "slab" -version = "0.4.9" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smol" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +checksum = "13f2b548cd8447f8de0fdf1c592929f70f4fc7039a05e47404b0d096ec6987a1" dependencies = [ - "autocfg", + "async-channel 1.9.0", + "async-executor", + "async-fs", + "async-io 1.13.0", + "async-lock 2.8.0", + "async-net", + "async-process", + "blocking", + "futures-lite 1.13.0", ] [[package]] -name = "smallvec" -version = "1.14.0" +name = "socket2" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] [[package]] name = "socket2" -version = "0.5.8" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2478,10 +2937,21 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "stratum-common" -version = "2.0.0" +version = "4.0.1" dependencies = [ - "bitcoin", - "secp256k1 0.28.2", + "network_helpers_sv2", + "roles_logic_sv2", +] + +[[package]] +name = "stratum_translation" +version = "0.1.0" +dependencies = [ + "binary_sv2", + "channels_sv2", + "mining_sv2", + "sv1_api", + "tracing", ] [[package]] @@ -2498,7 +2968,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sv1_api" -version = "1.0.1" +version = "2.1.1" dependencies = [ "binary_sv2", "bitcoin_hashes 0.3.2", @@ -2522,15 +2992,36 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.100" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.4", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tap" version = "1.0.1" @@ -2549,52 +3040,50 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.19.1" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ - "fastrand", + "fastrand 2.3.0", "once_cell", - "rustix 1.0.3", - "windows-sys 0.59.0", + "rustix 1.1.2", + "windows-sys 0.61.0", ] [[package]] name = "template_distribution_sv2" -version = "3.1.0" +version = "4.0.1" dependencies = [ "binary_sv2", - "stratum-common", ] [[package]] name = "thiserror" -version = "2.0.12" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] @@ -2606,23 +3095,35 @@ dependencies = [ "crunchy", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tokio" -version = "1.44.1" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "slab", + "socket2 0.6.0", "tokio-macros", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2633,14 +3134,14 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] name = "tokio-util" -version = "0.7.14" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", @@ -2651,38 +3152,75 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.20" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.11", + "toml_write", "winnow", ] +[[package]] +name = "toml_edit" +version = "0.23.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +dependencies = [ + "indexmap", + "toml_datetime 0.7.2", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tower-service" version = "0.3.3" @@ -2702,20 +3240,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", @@ -2734,47 +3272,52 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex-automata", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] +[[package]] +name = "trait-variant" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "translator_sv2" -version = "1.0.0" +version = "2.0.0" dependencies = [ "async-channel 1.9.0", - "async-recursion 0.3.2", - "binary_sv2", "buffer_sv2", "clap", - "codec_sv2", "config", - "error_handling", - "framing_sv2", - "futures", + "config_helpers_sv2", "key-utils", "network_helpers_sv2", - "once_cell", - "primitive-types", - "rand 0.8.5", - "roles_logic_sv2", "serde", "serde_json", - "sha2 0.10.8", + "sha2 0.10.9", "stratum-common", + "stratum_translation", "sv1_api", "tokio", - "tokio-util", "tracing", - "tracing-subscriber", ] [[package]] @@ -2809,9 +3352,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-segmentation" @@ -2865,6 +3408,22 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -2876,50 +3435,60 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.7+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" dependencies = [ - "wit-bindgen-rt", + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "a0b221ff421256839509adbb55998214a70d829d3a28c69b4a6672e9d2a42f67" dependencies = [ "cfg-if", "js-sys", @@ -2930,9 +3499,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2940,31 +3509,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc" dependencies = [ "js-sys", "wasm-bindgen", @@ -3001,19 +3570,78 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -3022,7 +3650,40 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-sys" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +dependencies = [ + "windows-link 0.2.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -3031,81 +3692,185 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link 0.1.3", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" -version = "0.7.4" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags", -] +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "wyz" @@ -3129,42 +3894,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "zerocopy-derive 0.7.35", -] - -[[package]] -name = "zerocopy" -version = "0.8.24" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ - "zerocopy-derive 0.8.24", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", + "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.24" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] diff --git a/roles/Cargo.toml b/roles/Cargo.toml index fe9d5fa0aa..58d57c2b1a 100644 --- a/roles/Cargo.toml +++ b/roles/Cargo.toml @@ -2,13 +2,13 @@ resolver="2" members = [ - "mining-proxy", "pool", "test-utils/mining-device", - "test-utils/mining-device-sv1", "translator", "jd-client", - "jd-server" + "jd-server", + "roles-utils/network-helpers", + "roles-utils/stratum-translation" ] [profile.dev] diff --git a/roles/jd-client/Cargo.toml b/roles/jd-client/Cargo.toml index ed69a0f4f6..75577b6209 100644 --- a/roles/jd-client/Cargo.toml +++ b/roles/jd-client/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "jd_client" -version = "0.1.4" +name = "jd_client_sv2" +version = "0.1.0" authors = ["The Stratum V2 Developers"] edition = "2021" description = "Job Declarator Client (JDC) role" @@ -12,28 +12,17 @@ license = "MIT OR Apache-2.0" keywords = ["stratum", "mining", "bitcoin", "protocol"] [lib] -name = "jd_client" +name = "jd_client_sv2" path = "src/lib/mod.rs" [dependencies] -stratum-common = { path = "../../common", features=["bitcoin"]} async-channel = "1.5.1" -async-recursion = "0.3.2" -binary_sv2 = { path = "../../protocols/v2/binary-sv2" } buffer_sv2 = { path = "../../utils/buffer" } -codec_sv2 = { path = "../../protocols/v2/codec-sv2", features = ["noise_sv2", "with_buffer_pool"] } -framing_sv2 = { path = "../../protocols/v2/framing-sv2" } -network_helpers_sv2 = { path = "../roles-utils/network-helpers", features=["with_buffer_pool"] } -roles_logic_sv2 = { path = "../../protocols/v2/roles-logic-sv2" } +stratum-common = { path = "../../common", features = ["with_network_helpers"] } serde = { version = "1.0.89", default-features = false, features = ["derive", "alloc"] } -futures = "0.3.25" tokio = { version = "1.44.1", features = ["full"] } ext-config = { version = "0.14.0", features = ["toml"], package = "config" } tracing = { version = "0.1" } -tracing-subscriber = { version = "0.3" } -error_handling = { path = "../../utils/error-handling" } -nohash-hasher = "0.2.0" key-utils = { path = "../../utils/key-utils" } -primitive-types = "0.13.1" -config-helpers = { path = "../roles-utils/config-helpers" } -clap = { version = "4.5.39", features = ["derive"] } +config_helpers_sv2 = { path = "../roles-utils/config-helpers" } +clap = { version = "4.5.39", features = ["derive"] } \ No newline at end of file diff --git a/roles/jd-client/README.md b/roles/jd-client/README.md index 18ab95197f..e08a767fae 100644 --- a/roles/jd-client/README.md +++ b/roles/jd-client/README.md @@ -1,10 +1,50 @@ -# JD Client -* connect to the jd-server -* connect to the template-provider -The JD Client receives custom block templates from a Template Provider and declares use of the template with the pool using the Job Declaration Protocol. Further distributes the jobs to Mining Proxy (or Proxies) using the Job Distribution Protocol. ``` -* transparently relay the `OpenExtendedChannel` to upstream +# Job Declarator Client +The **Job Declarator Client (JDC)** is responsible for: + +* Connecting to the **Pool** and **JD Server**. +* Connecting to the **Template Provider**. +* Receiving custom block templates from the Template Provider and declaring them to the pool via the **Job Declaration Protocol**. +* Sending jobs to downstream clients. +* Forwarding shares to the pool. + +## Architecture Overview + +The JDC sits between **SV2 downstream clients** (e.g., SV2 mining devices or Translator Proxies) and **SV2 upstream servers** (the Pool and JD Server). + +* It obtains templates from the Bitcoin node. +* It creates and broadcasts jobs to downstream clients. +* It declares and sets custom jobs to the pool side. +* It also supports solo mining mode in case no upstream is available or the upstream is fraudulent + +Note: while JDC can cater for multiple downstream clients, with either one or multiple channels per client, it only opens one single extended channel with the upstream Pool server. + +``` +<--- Most Downstream ------------------------------------------------------------------------------------------------ Most Upstream ---> + ++----------------------------------------------------------------------------------------------------+ +------------------------------+ +| Mining Farm | | Remote Pool | +| | | | +| +-------------------+ +------------------+ | | +-----------------+ | +| | SV1 Mining Device | <-> | Translator Proxy |-------| |-------------------------------> | SV2 Pool Server | | +| +-------------------+ +------------------+ | | | | +-----------------+ | +| | | | | | +| | | | | | +| +-----------------------+| | | | +| | Job Declarator Client | | | | +| +-----------------------+| | | +-----------------------+ | +| | |--------------------------------> | Job Declarator Server | | +| +-------------------+ | | | +-----------------------+ | +| | SV2 Mining Device |-----------------------------| | | | +| +-------------------+ | | | +| | | | +| | | | +| | | | ++----------------------------------------------------------------------------------------------------+ +------------------------------+ + + +``` ## Setup ### Configuration File @@ -12,23 +52,137 @@ The JD Client receives custom block templates from a Template Provider and decla The configuration file contains the following information: 1. The downstream socket information, which includes the listening IP address (`downstream_address`) and port (`downstream_port`). -2. The maximum and minimum SRI versions (`max_supported_version` and `min_supported_version`) with size as (`min_extranonce2_size`) -3. The authentication keys for the downstream connection (`authority_public_key`, `authority_secret_key`) -4. A `retry` parameter which tells JDC the number of times to reinitialize itself after a failure. -6. The Template Provider address (`tp_address`). -7. Optionally, you may want to verify that your TP connection is authentic. You may get `tp_authority_public_key` from the logs of your TP, for example: +2. The maximum and minimum protocol versions (`max_supported_version` and `min_supported_version`) with size as (`min_extranonce2_size`) +3. The authentication keys used for the downstream connections (`authority_public_key`, `authority_secret_key`) +4. The Template Provider address (`tp_address`). + +## Configuration + +The JDC is configured via a `.toml` file. +See [`config-examples/jdc-config-local-example.toml`](./config-examples/jdc-config-local-example.toml) for a full example. + +### Example Configuration -# 2024-02-13T14:59:24Z Template Provider authority key: EguTM8URcZDQVeEBsM4B5vg9weqEUnufA8pm85fG4bZd +```toml +# Listening address for downstream clients +listening_address = "127.0.0.1:34265" -### Run +# Version support +max_supported_version = 2 +min_supported_version = 2 -Run the Job Declarator Client (JDC): -There are two files when you cd into roles/jd-client/config-examples/ +# Authentication keys for encrypted downstream connections +authority_public_key = "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72" +authority_secret_key = "mkDLTBBRxdBv998612qipDYoTK3YUrqLe8uWw7gu3iXbSrn2n" +cert_validity_sec = 3600 -1. `jdc-config-hosted-example.toml` connects to the community-hosted roles. -2. `jdc-config-local-example.toml` connects to self-hosted Job Declarator Client (JDC) and Translator Proxy +user_identity = "your_username_here" -``` bash -cd roles/jd-client/config-examples/ -cargo run -- -c jdc-config-hosted-example.toml +# Target shares per minute & batching +shares_per_minute = 1.0 +share_batch_size = 1 +min_extranonce_size = 4 + +# Template Provider +tp_address = "127.0.0.1:8442" +jdc_signature = "Sv2MinerSignature" + +# Coinbase output for solo mining fallback +coinbase_reward_script = "addr(tb1qa0sm0hxzj0x25rh8gw5xlzwlsfvvyz8u96w3p8)" + +[[upstreams]] +authority_pubkey = "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72" +pool_address = "127.0.0.1" +pool_port = 34254 +jd_address = "127.0.0.1" +jd_port = 34264 ``` + +For a complete, annotated config, see the [full example](./config-examples/jdc-config-hosted-example.toml). + + +## Usage + +### Installation & Build + +```bash +# Clone the repository +git clone https://github.com/stratum-mining/stratum.git +cd stratum + +# Build JDC +cargo build --release -p jd_client +``` + +### Running JDC + +#### With Local Pool and Job Declarator Server + +```bash +cd roles/jd_client +cargo run -- -c config-examples/jdc-config-local-example.toml +``` + +#### With Hosted Pool and Job Declarator Server + +```bash +cd roles/jd_client +cargo run -- -c config-examples/jdc-config-hosted-example.toml +``` + +### Command Line Options + +```bash +# Use specific config file +jd_client -c /path/to/config.toml +jd_client --config /path/to/config.toml + +# Show help +jd_client -h +jd_client --help +``` + +## Architecture Details + +### **Component Overview** + +1. **Channel Manager**: Orchestrates message routing among sub-systems in JDC +2. **Task Manager**: Manages async task lifecycle and coordination +3. **Status System**: Provides real-time monitoring and health reporting + +## Internal Architecture + +JDC is built from several modules that divide responsibility for handling different roles and protocols: + +### **Modules** + +1. **Upstream** + + * Connects to the **pool**. + * Handles messages coming from the Pool (the ones defined in the Common Protocol are directly handled, others are forwarded to the Channel Manager). + +2. **Downstream** + + * Accepts connections from Sv2 Mining Devices or Translator Proxies. + * Includes a **ChannelState**, which provisions new channels when `OpenStandard/ExtendedChannel` messages arrive from the downstreams. + +3. **Template Receiver** + + * Connects to the **Template Provider**. + * Handles messages received by the TP (the ones defined in the Common Protocol are directly handled, while the others are forwarded to the Channel Manager). + +4. **Job Declarator** + + * Connects to the **Job Declarator Server (JDS)**. + * Handles messages received by the JDS (the ones defined in the Common Protocol are directly handled, while the others are forwarded to the Channel Manager). + +5. **Channel Manager (Orchestrator)** + + * Central coordination point. + * Responsibilities: + + * Handles **non-common messages** forwarded from all modules. + * Maintains **upstream channel state**. + * Maintains most of the **Job Declarator state**. + * Orchestrates job lifecycle and state synchronization across upstream and downstream roles. + diff --git a/roles/jd-client/config-examples/jdc-config-hosted-example.toml b/roles/jd-client/config-examples/jdc-config-hosted-example.toml index bf80c2c236..3b9d9887f8 100644 --- a/roles/jd-client/config-examples/jdc-config-hosted-example.toml +++ b/roles/jd-client/config-examples/jdc-config-hosted-example.toml @@ -5,14 +5,26 @@ listening_address = "127.0.0.1:34265" max_supported_version = 2 min_supported_version = 2 -# Withhold -withhold = false - # Auth keys for open encrypted connection downstream authority_public_key = "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72" authority_secret_key = "mkDLTBBRxdBv998612qipDYoTK3YUrqLe8uWw7gu3iXbSrn2n" cert_validity_sec = 3600 + +# User identity/username for pool connection +user_identity = "your_username_here" + +# target number of shares per minute applied to every downstream channel +shares_per_minute = 6.0 + +# Share batch size +share_batch_size = 10 + +# JDC supports two modes: +# "FULLTEMPLATE" - full template mining +# "COINBASEONLY" - coinbase-only mining +mode = "FULLTEMPLATE" + # Template Provider config # Local TP (this is pointing to localhost so you must run a TP locally for this configuration to work) # tp_address = "127.0.0.1:8442" @@ -20,37 +32,35 @@ cert_validity_sec = 3600 tp_address = "75.119.150.111:8442" tp_authority_public_key = "9bwHCYnjhbHm4AS3pWg9MtAH83mzWohoJJJDELYBqZhDNqszDLc" -# string to be added into `extranonce_prefix` -# note: these bytes are fixed and they effectively reduce the search space available for the extranonce -# the bigger this field, the smaller the search space available for downstream -jdc_signature = "JDC" +# string to be added into the Coinbase scriptSig +jdc_signature = "Sv2MinerSignature" # Solo Mining config -# List of coinbase outputs used to build the coinbase tx in case of Solo Mining (as last-resort solution of the pools fallback system) -# ! Put your Extended Public Key or Script as output_script_value ! -# ! Right now only one output is supported, so comment all the ones you don't need ! -# For P2PK, P2PKH, P2WPKH, P2TR a public key is needed. For P2SH and P2WSH, a redeem script is needed. -coinbase_outputs = [ - #{ output_script_type = "P2PK", output_script_value = "0372c47307e5b75ce365daf835f226d246c5a7a92fe24395018d5552123354f086" }, - #{ output_script_type = "P2PKH", output_script_value = "0372c47307e5b75ce365daf835f226d246c5a7a92fe24395018d5552123354f086" }, - #{ output_script_type = "P2SH", output_script_value = "00142ef89234bc95136eb9e6fee9d32722ebd8c1f0ab" }, - #{ output_script_type = "P2WSH", output_script_value = "00142ef89234bc95136eb9e6fee9d32722ebd8c1f0ab" }, - { output_script_type = "P2WPKH", output_script_value = "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075" }, - #{ output_script_type = "P2TR", output_script_value = "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075" }, -] - -[timeout] -unit = "secs" -value = 1 +# Coinbase output used to build the coinbase tx in case of Solo Mining (as last-resort solution of the pools fallback system) +# +# Coinbase outputs are specified as descriptors. A full list of descriptors is available at +# https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki#appendix-b-index-of-script-expressions +# Although the `musig` descriptor is not yet supported and the legacy `combo` descriptor never +# will be. If you have an address, embed it in a descriptor like `addr(
)`. +coinbase_reward_script = "addr(tb1qa0sm0hxzj0x25rh8gw5xlzwlsfvvyz8u96w3p8)" + +# Enable this option to set a predefined log file path. +# When enabled, logs will always be written to this file. +# The CLI option --log-file (or -f) will override this setting if provided. +# log_file = "./jd-client.log" # List of upstreams (JDS) used as backup endpoints # In case of shares refused by the JDS, the fallback system will propose the same job to the next upstream in this list [[upstreams]] authority_pubkey = "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72" -pool_address = "75.119.150.111:34254" -jd_address = "75.119.150.111:34264" +pool_address = "75.119.150.111" +pool_port = "34254" +jds_address = "75.119.150.111" +jds_port = "34264" # [[upstreams]] # authority_pubkey = "2di19GHYQnAZJmEpoUeP7C3Eg9TCcksHr23rZCC83dvUiZgiDL" # pool_address = "127.0.0.1:34254" -# jd_address = "127.0.0.1:34264" \ No newline at end of file +# pool_port = "34254" +# jds_address = "127.0.0.1:34264" +# jds_port = "34264" diff --git a/roles/jd-client/config-examples/jdc-config-local-example.toml b/roles/jd-client/config-examples/jdc-config-local-example.toml index c8c77ad014..f550a5fd6d 100644 --- a/roles/jd-client/config-examples/jdc-config-local-example.toml +++ b/roles/jd-client/config-examples/jdc-config-local-example.toml @@ -5,51 +5,62 @@ listening_address = "127.0.0.1:34265" max_supported_version = 2 min_supported_version = 2 -# Withhold -withhold = false - # Auth keys for open encrypted connection downstream authority_public_key = "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72" authority_secret_key = "mkDLTBBRxdBv998612qipDYoTK3YUrqLe8uWw7gu3iXbSrn2n" cert_validity_sec = 3600 + +# User identity/username for pool connection +user_identity = "your_username_here" + +# target number of shares per minute applied to every downstream channel +shares_per_minute = 6.0 + +# Share batch size +share_batch_size = 10 + +# JDC supports two modes: +# "FULLTEMPLATE" - full template mining +# "COINBASEONLY" - coinbase-only mining +mode = "FULLTEMPLATE" + # Template Provider config # Local TP (this is pointing to localhost so you must run a TP locally for this configuration to work) tp_address = "127.0.0.1:8442" # Hosted testnet TP # tp_address = "75.119.150.111:8442" -# string to be added into `extranonce_prefix` -# note: these bytes are fixed and they effectively reduce the search space available for the extranonce -# the bigger this field, the smaller the search space available for downstream -jdc_signature = "JDC" +# string to be added into the Coinbase scriptSig +jdc_signature = "Sv2MinerSignature" # Solo Mining config -# List of coinbase outputs used to build the coinbase tx in case of Solo Mining (as last-resort solution of the pools fallback system) -# ! Put your Extended Public Key or Script as output_script_value ! -# ! Right now only one output is supported, so comment all the ones you don't need ! -# For P2PK, P2PKH, P2WPKH, P2TR a public key is needed. For P2SH and P2WSH, a redeem script is needed. -coinbase_outputs = [ - #{ output_script_type = "P2PK", output_script_value = "0372c47307e5b75ce365daf835f226d246c5a7a92fe24395018d5552123354f086" }, - #{ output_script_type = "P2PKH", output_script_value = "0372c47307e5b75ce365daf835f226d246c5a7a92fe24395018d5552123354f086" }, - #{ output_script_type = "P2SH", output_script_value = "00142ef89234bc95136eb9e6fee9d32722ebd8c1f0ab" }, - #{ output_script_type = "P2WSH", output_script_value = "00142ef89234bc95136eb9e6fee9d32722ebd8c1f0ab" }, - { output_script_type = "P2WPKH", output_script_value = "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075" }, - #{ output_script_type = "P2TR", output_script_value = "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075" }, -] - -[timeout] -unit = "secs" -value = 1 +# Coinbase output used to build the coinbase tx in case of Solo Mining (as last-resort solution of the pools fallback system) +# +# Coinbase outputs are specified as descriptors. A full list of descriptors is available at +# https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki#appendix-b-index-of-script-expressions +# Although the `musig` descriptor is not yet supported and the legacy `combo` descriptor never +# will be. If you have an address, embed it in a descriptor like `addr(
)`. +coinbase_reward_script = "addr(tb1qa0sm0hxzj0x25rh8gw5xlzwlsfvvyz8u96w3p8)" + +# Enable this option to set a predefined log file path. +# When enabled, logs will always be written to this file. +# The CLI option --log-file (or -f) will override this setting if provided. +# log_file = "./jd-client.log" + # List of upstreams (JDS) used as backup endpoints # In case of shares refused by the JDS, the fallback system will propose the same job to the next upstream in this list [[upstreams]] authority_pubkey = "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72" -pool_address = "127.0.0.1:34254" -jd_address = "127.0.0.1:34264" - -# [[upstreams]] -# authority_pubkey = "2di19GHYQnAZJmEpoUeP7C3Eg9TCcksHr23rZCC83dvUiZgiDL" -# pool_address = "127.0.0.1:34254" -# jd_address = "127.0.0.1:34264" +pool_address = "127.0.0.1" +pool_port = 34254 +jds_address = "127.0.0.1" +jds_port = 34264 + +[[upstreams]] +authority_pubkey = "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72" +pool_address = "75.119.150.111" +pool_port = "34254" +jds_address = "75.119.150.111" +jds_port = "34264" diff --git a/roles/jd-client/src/args.rs b/roles/jd-client/src/args.rs index e59a29cf0e..1836b2d1c1 100644 --- a/roles/jd-client/src/args.rs +++ b/roles/jd-client/src/args.rs @@ -1,14 +1,6 @@ -//! ## CLI Arguments Parsing Module -//! -//! This module is responsible for parsing the command-line arguments provided -//! to the application. - use clap::Parser; use ext_config::{Config, File, FileFormat}; -use jd_client::{ - config::JobDeclaratorClientConfig, - error::{Error, ProxyResult}, -}; +use jd_client_sv2::{config::JobDeclaratorClientConfig, error::JDCError}; use std::path::PathBuf; use tracing::error; @@ -22,25 +14,30 @@ pub struct Args { default_value = "jdc-config.toml" )] pub config_path: PathBuf, + #[arg( + short = 'f', + long = "log-file", + help = "Path to the log file. If not set, logs will only be written to stdout." + )] + pub log_file: Option, } -/// Process CLI args and load configuration. #[allow(clippy::result_large_err)] -pub fn process_cli_args<'a>() -> ProxyResult<'a, JobDeclaratorClientConfig> { - // Parse CLI arguments +pub fn process_cli_args() -> Result { let args = Args::parse(); - // Build configuration from the provided file path let config_path = args.config_path.to_str().ok_or_else(|| { error!("Invalid configuration path."); - Error::BadCliArgs + JDCError::BadCliArgs })?; let settings = Config::builder() .add_source(File::new(config_path, FileFormat::Toml)) .build()?; - // Deserialize settings into JobDeclaratorClientConfig - let config = settings.try_deserialize::()?; + let mut config = settings.try_deserialize::()?; + + config.set_log_file(args.log_file); + Ok(config) } diff --git a/roles/jd-client/src/lib/channel_manager/downstream_message_handler.rs b/roles/jd-client/src/lib/channel_manager/downstream_message_handler.rs new file mode 100644 index 0000000000..8babfa1387 --- /dev/null +++ b/roles/jd-client/src/lib/channel_manager/downstream_message_handler.rs @@ -0,0 +1,1288 @@ +use std::sync::atomic::Ordering; + +use stratum_common::roles_logic_sv2::{ + self, + bitcoin::Amount, + channels_sv2::{ + client, + outputs::deserialize_outputs, + server::{ + error::{ExtendedChannelError, StandardChannelError}, + extended::ExtendedChannel, + group::GroupChannel, + jobs::job_store::DefaultJobStore, + share_accounting::{ShareValidationError, ShareValidationResult}, + standard::StandardChannel, + }, + Vardiff, VardiffState, + }, + codec_sv2::binary_sv2::Str0255, + handlers_sv2::{HandleMiningMessagesFromClientAsync, SupportedChannelTypes}, + job_declaration_sv2::PushSolution, + mining_sv2::*, + parsers_sv2::{AnyMessage, JobDeclaration, Mining, TemplateDistribution}, + template_distribution_sv2::SubmitSolution, +}; +use tracing::{debug, error, info, warn}; + +use crate::{ + channel_manager::{ChannelManager, ChannelManagerChannel}, + error::JDCError, + jd_mode::{get_jd_mode, JdMode}, + utils::StdFrame, +}; + +/// `RouteMessageTo` is an abstraction used to route protocol messages +/// to the appropriate subsystem connected to the JDC. +/// +/// Instead of manually handling routing logic for each message type, +/// this enum provides a unified interface. Each variant represents +/// a possible destination: +/// +/// - [`RouteMessageTo::Upstream`] → For messages intended for the upstream. +/// - [`RouteMessageTo::JobDeclarator`] → For job declaration messages sent to the JDS. +/// - [`RouteMessageTo::TemplateProvider`] → For template distribution messages sent to the template +/// provider. +/// - [`RouteMessageTo::Downstream`] → For messages destined to a specific downstream client, +/// identified by its `u32` downstream ID. +#[derive(Clone)] +pub enum RouteMessageTo<'a> { + /// Route to the upstream (mining) channel. + Upstream(Mining<'a>), + /// Route to the job declarator subsystem. + JobDeclarator(JobDeclaration<'a>), + /// Route to the template provider subsystem. + TemplateProvider(TemplateDistribution<'a>), + /// Route to a specific downstream client by ID, along with its mining message. + Downstream((u32, Mining<'a>)), +} + +impl<'a> From> for RouteMessageTo<'a> { + fn from(value: Mining<'a>) -> Self { + Self::Upstream(value) + } +} + +impl<'a> From> for RouteMessageTo<'a> { + fn from(value: JobDeclaration<'a>) -> Self { + Self::JobDeclarator(value) + } +} + +impl<'a> From> for RouteMessageTo<'a> { + fn from(value: TemplateDistribution<'a>) -> Self { + Self::TemplateProvider(value) + } +} + +impl<'a> From<(u32, Mining<'a>)> for RouteMessageTo<'a> { + fn from(value: (u32, Mining<'a>)) -> Self { + Self::Downstream(value) + } +} + +impl RouteMessageTo<'_> { + /// Forwards the message to its corresponding destination channel. + /// + /// The routing is handled as follows: + /// - [`RouteMessageTo::Downstream`] → Sends the mining message to the specified downstream + /// client. + /// - [`RouteMessageTo::Upstream`] → Sends the mining message upstream, unless in + /// [`JdMode::SoloMining`]. + /// - [`RouteMessageTo::JobDeclarator`] → Sends the job declaration message to the JDS. + /// - [`RouteMessageTo::TemplateProvider`] → Sends the template distribution message to the + /// template provider. + /// + /// Messages are automatically converted into the appropriate + /// [`AnyMessage`] variant and wrapped into a [`StdFrame`]. + pub async fn forward(self, channel_manager_channel: &ChannelManagerChannel) { + match self { + RouteMessageTo::Downstream((downstream_id, message)) => { + _ = channel_manager_channel + .downstream_sender + .send((downstream_id, AnyMessage::Mining(message).into_static())); + } + RouteMessageTo::Upstream(message) => { + if get_jd_mode() != JdMode::SoloMining { + let message = AnyMessage::Mining(message).into_static(); + let frame: StdFrame = message.try_into().unwrap(); + _ = channel_manager_channel.upstream_sender.send(frame).await; + } + } + RouteMessageTo::JobDeclarator(message) => { + let message = AnyMessage::JobDeclaration(message).into_static(); + let frame: StdFrame = message.try_into().unwrap(); + _ = channel_manager_channel.jd_sender.send(frame).await; + } + RouteMessageTo::TemplateProvider(message) => { + let message = AnyMessage::TemplateDistribution(message).into_static(); + let frame: StdFrame = message.try_into().unwrap(); + _ = channel_manager_channel.tp_sender.send(frame).await; + } + } + } +} + +impl HandleMiningMessagesFromClientAsync for ChannelManager { + type Error = JDCError; + + fn get_channel_type_for_client(&self, _client_id: Option) -> SupportedChannelTypes { + SupportedChannelTypes::GroupAndExtended + } + fn is_work_selection_enabled_for_client(&self, _client_id: Option) -> bool { + false + } + fn is_client_authorized( + &self, + _client_id: Option, + _user_identity: &Str0255, + ) -> Result { + Ok(true) + } + + // Handles a `CloseChannel` message: + // - Look up the downstream associated with the given `channel_id`. + // - If found, remove the channel from its `extended_channels` and `standard_channels`. + // - If not found, return an appropriate error. + async fn handle_close_channel( + &mut self, + _client_id: Option, + msg: CloseChannel<'_>, + ) -> Result<(), Self::Error> { + info!("Received: {}", msg); + self.channel_manager_data + .super_safe_lock(|channel_manager_data| { + let Some(downstream_id) = channel_manager_data + .channel_id_to_downstream_id + .get(&msg.channel_id) + else { + error!( + "No downstream_id related to channel_id: {:?}, found", + msg.channel_id + ); + return Err(JDCError::DownstreamNotFoundWithChannelId(msg.channel_id)); + }; + let Some(downstream) = channel_manager_data.downstream.get(downstream_id) else { + error!( + "No downstream with channel_id: {:?} and downstream_id: {:?}, found", + msg.channel_id, downstream_id + ); + return Err(JDCError::DownstreamNotFound(*downstream_id)); + }; + downstream.downstream_data.super_safe_lock(|data| { + data.extended_channels.remove(&msg.channel_id); + data.standard_channels.remove(&msg.channel_id); + }); + Ok(()) + }) + } + + // Handles an `OpenStandardMiningChannel` message from a downstream. + // + // Steps: + // 1. Parse the `downstream_id` from the `user_identity`. + // 2. Create a new `StandardChannel` for the downstream. + // 3. Ensure a valid `GroupChannel` exists (create one if needed). + // 4. Apply the latest future template and prevhash to both group and standard channels. + // 5. Send the following messages back to the downstream: + // - `OpenStandardMiningChannelSuccess` + // - `NewMiningJob` + // - `SetNewPrevHash` + // 6. Update the downstream state, including: + // - Channel manager mappings + // - Standard and group channel registrations + // - Vardiff state + // + // Returns an error if any step fails, such as missing templates, invalid identity, + // or failure to apply updates to channels. + async fn handle_open_standard_mining_channel( + &mut self, + _client_id: Option, + msg: OpenStandardMiningChannel<'_>, + ) -> Result<(), Self::Error> { + let request_id = msg.get_request_id_as_u32(); + let user_string = msg.user_identity.as_utf8_or_hex(); + + let coinbase_outputs = self + .channel_manager_data + .super_safe_lock(|data| data.coinbase_outputs.clone()); + + let mut coinbase_outputs = deserialize_outputs(coinbase_outputs) + .map_err(|_| JDCError::ChannelManagerHasBadCoinbaseOutputs)?; + + let (user_identity, downstream_id) = match user_string.rsplit_once('#') { + Some((user_identity, id)) => match id.parse::() { + Ok(id) => (user_identity, id), + Err(e) => { + warn!( + ?e, + user_string, "Failed to parse downstream_id from user_identity" + ); + return Err(JDCError::ParseInt(e)); + } + }, + None => { + warn!(user_string, "User identity missing downstream_id"); + return Err(JDCError::DownstreamIdNotFound); + } + }; + + info!(downstream_id, "Received: {}", msg); + + let build_error = |code: &str| { + Mining::OpenMiningChannelError(OpenMiningChannelError { + request_id, + error_code: code.to_string().try_into().expect("valid error code"), + }) + }; + + let messages: Vec = + self.channel_manager_data + .super_safe_lock(|channel_manager_data| { + let Some(last_future_template) = + channel_manager_data.last_future_template.clone() + else { + error!("Missing last_future_template, cannot open channel"); + return Err(JDCError::FutureTemplateNotPresent); + }; + + let Some(last_new_prev_hash) = channel_manager_data.last_new_prev_hash.clone() + else { + error!("Missing last_new_prev_hash, cannot open channel"); + return Err(JDCError::LastNewPrevhashNotFound); + }; + + let Some(downstream) = channel_manager_data.downstream.get(&downstream_id) + else { + error!(downstream_id, "Downstream not registered"); + return Err(JDCError::DownstreamNotFound(downstream_id)); + }; + + coinbase_outputs[0].value = + Amount::from_sat(last_future_template.coinbase_tx_value_remaining); + + downstream.downstream_data.super_safe_lock(|data| { + let mut messages: Vec = vec![]; + + if !data.require_std_job && data.group_channels.is_none() { + let group_channel_id = channel_manager_data.channel_id_factory.fetch_add(1, Ordering::Relaxed); + let job_store = DefaultJobStore::new(); + let mut group_channel = GroupChannel::new_for_job_declaration_client( + group_channel_id, + job_store, + channel_manager_data.pool_tag_string.clone(), + self.miner_tag_string.clone(), + ); + + if let Err(e) = group_channel.on_new_template( + last_future_template.clone(), + coinbase_outputs.clone(), + ) { + error!(?e, "Failed to apply template to group channel"); + return Err(JDCError::RolesSv2Logic(roles_logic_sv2::Error::FailedToProcessNewTemplateGroupChannel(e))); + } + + if let Err(e) = + group_channel.on_set_new_prev_hash(last_new_prev_hash.clone()) + { + error!(?e, "Failed to apply prevhash to group channel"); + return Err(JDCError::RolesSv2Logic(roles_logic_sv2::Error::FailedToProcessSetNewPrevHashGroupChannel(e))); + }; + + data.group_channels = Some(group_channel); + } + + let nominal_hash_rate = msg.nominal_hash_rate; + let requested_max_target = msg.max_target.into_static(); + + let group_channel_id = data + .group_channels + .as_ref() + .map(|gc| gc.get_group_channel_id()) + .unwrap_or(0); + let standard_channel_id = channel_manager_data.channel_id_factory.fetch_add(1, Ordering::Relaxed); + + let extranonce_prefix = match channel_manager_data + .extranonce_prefix_factory_standard + .next_prefix_standard() + { + Ok(p) => p, + Err(e) => { + error!(?e, "Failed to get extranonce prefix"); + return Err(JDCError::RolesSv2Logic(roles_logic_sv2::Error::ExtranoncePrefixFactoryError(e))); + } + }; + + let job_store = DefaultJobStore::new(); + let mut standard_channel = + match StandardChannel::new_for_job_declaration_client( + standard_channel_id, + user_identity.to_string(), + extranonce_prefix.to_vec(), + requested_max_target.into(), + nominal_hash_rate, + self.share_batch_size, + self.shares_per_minute, + job_store, + channel_manager_data.pool_tag_string.clone(), + self.miner_tag_string.clone(), + ) { + Ok(channel) => channel, + Err(e) => { + error!(?e, "Failed to create standard channel"); + return match e { + StandardChannelError::InvalidNominalHashrate => { + Ok(vec![(downstream_id, build_error("invalid-nominal-hashrate")).into()]) + } + StandardChannelError::RequestedMaxTargetOutOfRange => { + Ok(vec![(downstream_id, build_error("max-target-out-of-range")).into()]) + } + other => Err( + JDCError::RolesSv2Logic( + roles_logic_sv2::Error::FailedToCreateStandardChannel(other) + ) + ), + } + } + }; + + let open_standard_mining_channel_success = + OpenStandardMiningChannelSuccess { + request_id: msg.request_id.clone(), + channel_id: standard_channel_id, + target: standard_channel.get_target().clone().into(), + extranonce_prefix: standard_channel + .get_extranonce_prefix() + .clone() + .try_into() + .expect("extranonce_prefix must be valid"), + group_channel_id, + } + .into_static(); + + messages.push( + ( + downstream_id, + Mining::OpenStandardMiningChannelSuccess( + open_standard_mining_channel_success, + ), + ) + .into(), + ); + + if let Err(e) = standard_channel + .on_new_template(last_future_template.clone(), coinbase_outputs.clone()) + { + error!(?e, "Failed to apply template to standard channel"); + return Err(JDCError::RolesSv2Logic(roles_logic_sv2::Error::FailedToProcessNewTemplateStandardChannel(e))); + } + + let future_standard_job_id = standard_channel + .get_future_template_to_job_id() + .get(&last_future_template.template_id) + .cloned() + .expect("future job id must exist"); + + let future_standard_job = standard_channel + .get_future_jobs() + .get(&future_standard_job_id) + .expect("future job must exist"); + + let future_standard_job_message = + future_standard_job.get_job_message().clone().into_static(); + + messages.push( + ( + downstream_id, + Mining::NewMiningJob(future_standard_job_message), + ) + .into(), + ); + + let prev_hash = last_new_prev_hash.prev_hash.clone(); + let header_timestamp = last_new_prev_hash.header_timestamp; + let n_bits = last_new_prev_hash.n_bits; + let set_new_prev_hash_mining = SetNewPrevHash { + channel_id: standard_channel_id, + job_id: future_standard_job_id, + prev_hash, + min_ntime: header_timestamp, + nbits: n_bits, + }; + + if let Err(e) = + standard_channel.on_set_new_prev_hash(last_new_prev_hash.clone()) + { + error!(?e, "Failed to apply prevhash to standard channel"); + return Err(JDCError::RolesSv2Logic(roles_logic_sv2::Error::FailedToProcessSetNewPrevHashStandardChannel(e))); + } + messages.push( + ( + downstream_id, + Mining::SetNewPrevHash(set_new_prev_hash_mining), + ) + .into(), + ); + + let vardiff = VardiffState::new().expect("Vardiff state should instantiate."); + + channel_manager_data.vardiff.insert((standard_channel_id, downstream_id),vardiff); + data.standard_channels.insert(standard_channel_id, standard_channel); + channel_manager_data + .channel_id_to_downstream_id + .insert(standard_channel_id, downstream_id); + + channel_manager_data.downstream_channel_id_and_job_id_to_template_id.insert((standard_channel_id, future_standard_job_id), last_future_template.template_id); + if let Some(group_channel) = data.group_channels.as_mut() { + group_channel.add_standard_channel_id(standard_channel_id); + } + + Ok(messages) + }) + })?; + + for messages in messages { + messages.forward(&self.channel_manager_channel).await; + } + Ok(()) + } + + // Handles an `OpenExtendedMiningChannel` request from a downstream. + // + // Workflow: + // 1. Extract the `downstream_id` from `user_identity`. + // 2. Create a new `ExtendedChannel` with the requested parameters. + // 3. Send back to the downstream: + // - `OpenExtendedMiningChannelSuccess` + // - `NewExtendedMiningJob` (based on the latest future template) + // - `SetNewPrevHash` (to immediately activate the job) + // 4. Update internal state, including: + // - Extended channel registry + // - Downstream/channel mappings + // - Vardiff state + // + // Returns an error if the downstream is missing, template/prevhash are unavailable, + // or if extended channel creation fails. + async fn handle_open_extended_mining_channel( + &mut self, + _client_id: Option, + msg: OpenExtendedMiningChannel<'_>, + ) -> Result<(), Self::Error> { + let user_string = msg.user_identity.as_utf8_or_hex(); + let (user_identity, downstream_id) = match user_string.rsplit_once('#') { + Some((user_identity, id)) => match id.parse::() { + Ok(v) => (user_identity, v), + Err(e) => { + warn!(?e, user_string, "Invalid downstream_id in user_identity"); + return Err(JDCError::ParseInt(e)); + } + }, + None => { + warn!(user_string, "Missing downstream_id in user_identity"); + return Err(JDCError::DownstreamIdNotFound); + } + }; + + info!(downstream_id, "Received: {}", msg); + let request_id = msg.get_request_id_as_u32(); + + let nominal_hash_rate = msg.nominal_hash_rate; + let requested_max_target = msg.max_target.into_static(); + let requested_min_rollable_extranonce_size = msg.min_extranonce_size; + + let build_error = |code: &str| { + Mining::OpenMiningChannelError(OpenMiningChannelError { + request_id, + error_code: code.to_string().try_into().expect("valid error code"), + }) + }; + + let messages = + self.channel_manager_data + .super_safe_lock(|channel_manager_data| { + + let Some(downstream) = channel_manager_data.downstream.get_mut(&downstream_id) else { + error!(downstream_id, "Downstream not found"); + return Err(JDCError::DownstreamNotFound(downstream_id)); + }; + + downstream.downstream_data.super_safe_lock(|data| { + + let mut messages: Vec = vec![]; + let extended_channel_id = channel_manager_data.channel_id_factory.fetch_add(1, Ordering::Relaxed); + + let extranonce_prefix = match channel_manager_data.extranonce_prefix_factory_extended + .next_prefix_extended(requested_min_rollable_extranonce_size.into()) + { + Ok(p) => p, + Err(e) => { + error!(?e, "Extranonce prefix error"); + return Err(JDCError::RolesSv2Logic(roles_logic_sv2::Error::ExtranoncePrefixFactoryError(e))); + } + }; + + let Some(last_future_template) = channel_manager_data.last_future_template.clone() else { + error!("No template to share"); + return Err(JDCError::FutureTemplateNotPresent); + }; + + let Some(last_new_prev_hash) = channel_manager_data.last_new_prev_hash.clone() else { + error!("No prevhash in system"); + return Err(JDCError::LastNewPrevhashNotFound); + }; + + let job_store = DefaultJobStore::new(); + + let mut extended_channel = match ExtendedChannel::new_for_job_declaration_client( + extended_channel_id, + user_identity.to_string(), + extranonce_prefix.into(), + requested_max_target.into(), + nominal_hash_rate, + true, + requested_min_rollable_extranonce_size, + self.share_batch_size, + self.shares_per_minute, + job_store, + channel_manager_data.pool_tag_string.clone(), + self.miner_tag_string.clone(), + ) { + Ok(c) => c, + Err(e) => { + error!(?e, "Failed to create ExtendedChannel"); + return match e { + ExtendedChannelError::InvalidNominalHashrate => { + Ok(vec![(downstream_id, build_error("invalid-nominal-hashrate")).into()]) + } + ExtendedChannelError::RequestedMaxTargetOutOfRange => { + Ok(vec![(downstream_id, build_error("max-target-out-of-range")).into()]) + } + ExtendedChannelError::RequestedMinExtranonceSizeTooLarge => { + Ok(vec![(downstream_id, build_error("min-extranonce-size-too-large")).into()]) + } + other => Err( + JDCError::RolesSv2Logic( + roles_logic_sv2::Error::FailedToCreateExtendedChannel(other) + ) + ), + } + } + }; + + let open_extended_mining_channel_success = + OpenExtendedMiningChannelSuccess { + request_id, + channel_id: extended_channel_id, + target: extended_channel.get_target().clone().into(), + extranonce_prefix: extended_channel + .get_extranonce_prefix() + .clone() + .try_into() + .expect("valid extranonce prefix"), + extranonce_size: extended_channel.get_rollable_extranonce_size(), + } + .into_static(); + + messages.push(( + downstream_id, + Mining::OpenExtendedMiningChannelSuccess( + open_extended_mining_channel_success, + ), + ).into()); + + let mut coinbase_outputs = match deserialize_outputs(channel_manager_data.coinbase_outputs.clone()) { + Ok(outputs) => outputs, + Err(_) => return Err(JDCError::ChannelManagerHasBadCoinbaseOutputs), + }; + coinbase_outputs[0].value = + Amount::from_sat(last_future_template.coinbase_tx_value_remaining); + + + // create a future extended job based on the last future template + if let Err(e) = + extended_channel.on_new_template(last_future_template.clone(), coinbase_outputs) + { + error!(?e, "Failed to apply template to extended channel"); + return Err(JDCError::RolesSv2Logic(roles_logic_sv2::Error::FailedToProcessNewTemplateExtendedChannel(e))); + } + + let future_extended_job_id = extended_channel + .get_future_template_to_job_id() + .get(&last_future_template.template_id) + .cloned() + .expect("future job id must exist"); + let future_extended_job = extended_channel + .get_future_jobs() + .get(&future_extended_job_id) + .expect("future job must exist"); + + let future_extended_job_message = + future_extended_job.get_job_message().clone().into_static(); + + // send this future job as new job message + // to be immediately activated with the subsequent SetNewPrevHash message + messages.push(( + downstream_id, + Mining::NewExtendedMiningJob( + future_extended_job_message, + ), + ).into()); + + // SetNewPrevHash message activates the future job + let prev_hash = last_new_prev_hash.prev_hash.clone(); + let header_timestamp = last_new_prev_hash.header_timestamp; + let n_bits = last_new_prev_hash.n_bits; + let set_new_prev_hash_mining = SetNewPrevHash { + channel_id: extended_channel_id, + job_id: future_extended_job_id, + prev_hash, + min_ntime: header_timestamp, + nbits: n_bits, + }; + if let Err(e) = extended_channel.on_set_new_prev_hash(last_new_prev_hash) { + error!(?e, "Failed to set prevhash on extended channel"); + return Err(JDCError::RolesSv2Logic(roles_logic_sv2::Error::FailedToProcessSetNewPrevHashExtendedChannel(e))); + } + messages.push(( + downstream_id, + Mining::SetNewPrevHash(set_new_prev_hash_mining), + ).into()); + + let vardiff = VardiffState::new().expect("Vardiff should instantiate."); + data.extended_channels.insert(extended_channel_id, extended_channel); + + channel_manager_data.downstream_channel_id_and_job_id_to_template_id.insert((extended_channel_id, future_extended_job_id), last_future_template.template_id); + channel_manager_data + .channel_id_to_downstream_id + .insert(extended_channel_id, downstream_id); + channel_manager_data.vardiff.insert((extended_channel_id, downstream_id), vardiff); + + Ok(messages) + }) + })?; + + for messages in messages { + messages.forward(&self.channel_manager_channel).await; + } + + Ok(()) + } + + // Handles an `UpdateChannel` message from a downstream. + // + // Workflow: + // 1. Update the target for the corresponding downstream channel (standard or extended). + // - On success, reply with a `SetTarget`. + // - On failure, return an `UpdateChannelError`. + // 2. Recompute aggregate downstream state: + // - Sum all downstream nominal hashrates. + // - Determine the minimum target across all downstream channels. + // 3. Propagate the update upstream by sending an `UpdateChannel` with the aggregated hashrate + // and minimum target. + // + // Returns an error if the downstream channel is missing or update + // validation fails. + async fn handle_update_channel( + &mut self, + _client_id: Option, + msg: UpdateChannel<'_>, + ) -> Result<(), Self::Error> { + info!("Received: {}", msg); + let channel_id = msg.channel_id; + let new_nominal_hash_rate = msg.nominal_hash_rate; + let requested_maximum_target = msg.maximum_target.into_static(); + + let messages = self + .channel_manager_data + .super_safe_lock(|channel_manager_data| { + let mut messages: Vec = vec![]; + + let downstream_id = match channel_manager_data + .channel_id_to_downstream_id + .get(&channel_id) + { + Some(id) => *id, + None => { + error!( + channel_id, + "UpdateChannelError: invalid-channel-id (no downstream_id mapping)" + ); + return Err(JDCError::DownstreamNotFoundWithChannelId(channel_id)); + } + }; + + if let Some(downstream) = channel_manager_data.downstream.get_mut(&downstream_id) { + messages.extend_from_slice(&downstream.downstream_data.super_safe_lock( + |data| { + let mut messages: Vec = vec![]; + + let build_error = |code: &str| { + error!(channel_id, error_code = code, "UpdateChannelError"); + Mining::UpdateChannelError(UpdateChannelError { + channel_id, + error_code: code + .to_string() + .try_into() + .expect("valid error code"), + }) + }; + + if let Some(standard_channel) = + data.standard_channels.get_mut(&channel_id) + { + let update_channel = standard_channel.update_channel( + new_nominal_hash_rate, + Some(requested_maximum_target.into()), + ); + let new_target = standard_channel.get_target().clone(); + + if let Err(e) = update_channel { + error!(channel_id, ?e, "StandardChannel update failed"); + + let err_code = match e { + StandardChannelError::InvalidNominalHashrate => { + "invalid-nominal-hashrate" + } + StandardChannelError::RequestedMaxTargetOutOfRange => { + "requested-max-target-out-of-range" + } + _ => "internal-error", + }; + if err_code == "internal-error" { + warn!("Failed to update extended channel {channel_id}"); + } else { + return vec![(downstream_id, build_error(err_code)).into()]; + } + } + + messages.push( + ( + downstream_id, + Mining::SetTarget(SetTarget { + channel_id, + maximum_target: new_target.into(), + }), + ) + .into(), + ); + } else if let Some(extended_channel) = + data.extended_channels.get_mut(&channel_id) + { + let update_channel = extended_channel.update_channel( + new_nominal_hash_rate, + Some(requested_maximum_target.into()), + ); + let new_target = extended_channel.get_target().clone(); + + if let Err(e) = update_channel { + error!(channel_id, ?e, "StandardChannel update failed"); + let err_code = match e { + ExtendedChannelError::InvalidNominalHashrate => { + "invalid-nominal-hashrate" + } + ExtendedChannelError::RequestedMaxTargetOutOfRange => { + "requested-max-target-out-of-range" + } + _ => "internal-error", + }; + if err_code == "internal-error" { + warn!("Failed to update extended channel {channel_id}"); + } else { + return vec![(downstream_id, build_error(err_code)).into()]; + } + } + + messages.push( + ( + downstream_id, + Mining::SetTarget(SetTarget { + channel_id, + maximum_target: new_target.into(), + }), + ) + .into(), + ); + } else { + error!("UpdateChannelError: invalid-channel-id"); + return vec![ + (downstream_id, build_error("invalid-channel-id")).into() + ]; + } + + messages + }, + )); + } + + let mut downstream_hashrate = 0.0; + let mut min_target: Target = [0xff; 32].into(); + + for (_, downstream) in channel_manager_data.downstream.iter() { + downstream.downstream_data.super_safe_lock(|data| { + let mut update_from_channel = |hashrate: f32, target: &Target| { + downstream_hashrate += hashrate; + min_target = std::cmp::min(target.clone(), min_target.clone()); + }; + + for (_, channel) in data.standard_channels.iter() { + update_from_channel( + channel.get_nominal_hashrate(), + channel.get_target(), + ); + } + + for (_, channel) in data.extended_channels.iter() { + update_from_channel( + channel.get_nominal_hashrate(), + channel.get_target(), + ); + } + }); + } + + if let Some(ref upstream_channel) = channel_manager_data.upstream_channel { + debug!( + "Checking upstream channel {} with hashrate {} and target {:?}", + upstream_channel.get_channel_id(), + upstream_channel.get_nominal_hashrate(), + upstream_channel.get_target() + ); + + info!("Sending update channel message upstream"); + messages.push( + Mining::UpdateChannel(UpdateChannel { + channel_id: upstream_channel.get_channel_id(), + nominal_hash_rate: downstream_hashrate, + maximum_target: min_target.into(), + }) + .into(), + ) + } + + Ok(messages) + })?; + + for messages in messages { + messages.forward(&self.channel_manager_channel).await; + } + + Ok(()) + } + + // Handles a `SubmitSharesStandard` message from a downstream. + // + // Steps: + // 1. Validate the share against the downstream channel. + // - On error, respond with `SubmitSharesError`. + // - On success, acknowledge with `SubmitSharesSuccess` (and optionally a block found). + // + // 2. If the share is valid, attempt to forward it upstream: + // - Translate the share into an upstream `SubmitSharesExtended`. + // - Validate with the upstream channel. + // - Forward valid shares (or block solutions) upstream. + async fn handle_submit_shares_standard( + &mut self, + _client_id: Option, + msg: SubmitSharesStandard, + ) -> Result<(), Self::Error> { + info!("Received SubmitSharesStandard"); + let channel_id = msg.channel_id; + let job_id = msg.job_id; + + let build_error = |code: &str| { + Mining::SubmitSharesError(SubmitSharesError { + channel_id, + sequence_number: msg.sequence_number, + error_code: code.to_string().try_into().expect("valid error code"), + }) + }; + + let messages = self.channel_manager_data.super_safe_lock(|channel_manager_data| { + let Some(downstream_id) = channel_manager_data.channel_id_to_downstream_id.get(&channel_id) else { + warn!("No downstream_id found for channel_id={channel_id}"); + return Err(JDCError::DownstreamNotFoundWithChannelId(channel_id)) + }; + let Some(downstream) = channel_manager_data.downstream.get_mut(downstream_id) else { + warn!("No downstream found for downstream_id={downstream_id}"); + return Err(JDCError::DownstreamNotFound(*downstream_id)); + }; + let Some(prev_hash) = channel_manager_data.last_new_prev_hash.as_ref() else { + warn!("No prev_hash available yet, ignoring share"); + return Err(JDCError::LastNewPrevhashNotFound); + }; + + downstream.downstream_data.super_safe_lock(|data| { + let mut messages: Vec = vec![]; + + let Some(standard_channel) = data.standard_channels.get_mut(&channel_id) else { + error!("SubmitSharesError: channel_id: {channel_id}, sequence_number: {}, error_code: invalid-channel-id", msg.sequence_number); + return Ok(vec![(*downstream_id, build_error("invalid-channel-id")).into()]); + }; + + let Some(vardiff) = channel_manager_data.vardiff.get_mut(&(channel_id, *downstream_id)) else { + return Err(JDCError::VardiffNotFound(channel_id)); + }; + vardiff.increment_shares_since_last_update(); + let res = standard_channel.validate_share(msg.clone()); + let mut is_downstream_share_valid = false; + match res { + Ok(ShareValidationResult::Valid) => { + info!( + "SubmitSharesStandard on downstream channel: valid share | channel_id: {}, sequence_number: {} ☑ï¸", + channel_id, msg.sequence_number + ); + is_downstream_share_valid = true; + } + Ok(ShareValidationResult::ValidWithAcknowledgement( + last_sequence_number, + new_submits_accepted_count, + new_shares_sum, + )) => { + let success = SubmitSharesSuccess { + channel_id, + last_sequence_number, + new_submits_accepted_count, + new_shares_sum, + }; + is_downstream_share_valid = true; + info!("SubmitSharesStandard on downstream channel: {} ✅", success); + messages.push( + (downstream.downstream_id, + Mining::SubmitSharesSuccess(success)).into(), + ); + } + Ok(ShareValidationResult::BlockFound(template_id, coinbase)) => { + info!("SubmitSharesStandard on downstream channel: 💰 Block Found!!! 💰"); + is_downstream_share_valid = true; + if let Some(template_id) = template_id { + info!("SubmitSharesStandard: Propagating solution to the Template Provider."); + let solution = SubmitSolution { + template_id, + version: msg.version, + header_timestamp: msg.ntime, + header_nonce: msg.nonce, + coinbase_tx: coinbase.try_into()?, + }; + + messages.push(TemplateDistribution::SubmitSolution(solution.clone()).into()); + } + let share_accounting = standard_channel.get_share_accounting().clone(); + let success = SubmitSharesSuccess { + channel_id, + last_sequence_number: share_accounting.get_last_share_sequence_number(), + new_submits_accepted_count: share_accounting.get_shares_accepted(), + new_shares_sum: share_accounting.get_share_work_sum(), + }; + messages.push(( + downstream.downstream_id, + Mining::SubmitSharesSuccess(success), + ).into()); + } + Err(err) => { + let code = match err { + ShareValidationError::Invalid => "invalid-share", + ShareValidationError::Stale => "stale-share", + ShareValidationError::InvalidJobId => "invalid-job-id", + ShareValidationError::DoesNotMeetTarget => "difficulty-too-low", + ShareValidationError::DuplicateShare => "duplicate-share", + _ => unreachable!(), + }; + error!("⌠SubmitSharesError: ch={}, seq={}, error={code}", channel_id, msg.sequence_number); + messages.push((*downstream_id, build_error(code)).into()); + } + } + + if !is_downstream_share_valid { + return Ok(messages); + } + + if let Some(upstream_channel) = channel_manager_data.upstream_channel.as_mut() { + let prefix = standard_channel.get_extranonce_prefix().clone(); + let mut extranonce_parts = Vec::new(); + let up_prefix = upstream_channel.get_extranonce_prefix(); + extranonce_parts.extend_from_slice(&prefix[up_prefix.len()..]); + + let upstream_message = channel_manager_data + .downstream_channel_id_and_job_id_to_template_id + .get(&(channel_id, job_id)) + .and_then(|tid| channel_manager_data.template_id_to_upstream_job_id.get(tid)) + .map(|&upstream_job_id| { + SubmitSharesExtended { + channel_id: upstream_channel.get_channel_id(), + job_id: upstream_job_id as u32, + extranonce: extranonce_parts.try_into().unwrap(), + nonce: msg.nonce, + ntime: msg.ntime, + // We assign sequence number later, when we validate the share + // and send it to upstream. + sequence_number: 0, + version: msg.version, + } + }); + + if let Some(mut upstream_message) = upstream_message { + let res = upstream_channel.validate_share(upstream_message.clone()); + match res { + Ok(client::share_accounting::ShareValidationResult::Valid) => { + upstream_message.sequence_number = channel_manager_data.sequence_number_factory.fetch_add(1, Ordering::Relaxed); + info!( + "SubmitSharesStandard, forwarding it to upstream: valid share | channel_id: {}, sequence_number: {} ✅", + channel_id, upstream_message.sequence_number + ); + messages.push(Mining::SubmitSharesExtended(upstream_message).into()); + } + Ok(client::share_accounting::ShareValidationResult::BlockFound) => { + upstream_message.sequence_number = channel_manager_data.sequence_number_factory.fetch_add(1, Ordering::Relaxed); + info!("SubmitSharesStandard forwarding it to upstream: 💰 Block Found!!! 💰"); + let push_solution = PushSolution { + extranonce: standard_channel.get_extranonce_prefix().to_vec().try_into()?, + ntime: upstream_message.ntime, + nonce: upstream_message.nonce, + version: upstream_message.version, + nbits: prev_hash.n_bits, + prev_hash: prev_hash.prev_hash.clone(), + }; + messages.push(JobDeclaration::PushSolution(push_solution).into()); + messages.push(Mining::SubmitSharesExtended(upstream_message).into()); + } + Err(err) => { + let code = match err { + client::share_accounting::ShareValidationError::Invalid => "invalid-share", + client::share_accounting::ShareValidationError::Stale => "stale-share", + client::share_accounting::ShareValidationError::InvalidJobId => "invalid-job-id", + client::share_accounting::ShareValidationError::DoesNotMeetTarget => "difficulty-too-low", + client::share_accounting::ShareValidationError::DuplicateShare => "duplicate-share", + _ => unreachable!(), + }; + debug!("⌠SubmitSharesError not forwarding it to upstream: ch={}, seq={}, error={code}", channel_id, upstream_message.sequence_number); + } + } + } + } + + Ok(messages) + }) + })?; + + for messages in messages { + messages.forward(&self.channel_manager_channel).await; + } + + Ok(()) + } + + // Handles a `SubmitSharesExtended` message from a downstream. + // + // Steps: + // 1. Validate the share against the downstream channel. + // - On error, respond with `SubmitSharesError`. + // - On success, acknowledge with `SubmitSharesSuccess` (and optionally a block found). + // + // 2. If the share is valid, attempt to forward it upstream: + // - Translate the share into an upstream `SubmitSharesExtended`. + // - Validate with the upstream channel. + // - Forward valid shares (or block solutions) upstream. + async fn handle_submit_shares_extended( + &mut self, + _client_id: Option, + msg: SubmitSharesExtended<'_>, + ) -> Result<(), Self::Error> { + info!("Received SubmitSharesExtended"); + let channel_id = msg.channel_id; + let job_id = msg.job_id; + + let build_error = |code: &str| { + Mining::SubmitSharesError(SubmitSharesError { + channel_id, + sequence_number: msg.sequence_number, + error_code: code.to_string().try_into().expect("valid error code"), + }) + }; + + let messages = self.channel_manager_data.super_safe_lock(|channel_manager_data| { + let Some(downstream_id) = channel_manager_data.channel_id_to_downstream_id.get(&channel_id) else { + warn!("No downstream_id found for channel_id={channel_id}"); + return Err(JDCError::DownstreamNotFoundWithChannelId(channel_id)); + }; + let Some(downstream) = channel_manager_data.downstream.get_mut(downstream_id) else { + warn!("No downstream found for downstream_id={downstream_id}"); + return Err(JDCError::DownstreamNotFound(*downstream_id)); + }; + let Some(prev_hash) = channel_manager_data.last_new_prev_hash.as_ref() else { + warn!("No prev_hash available yet, ignoring share"); + return Err(JDCError::LastNewPrevhashNotFound); + }; + downstream.downstream_data.super_safe_lock(|data| { + let mut messages: Vec = vec![]; + + let Some(extended_channel) = data.extended_channels.get_mut(&channel_id) else { + error!("SubmitSharesError: channel_id: {channel_id}, sequence_number: {}, error_code: invalid-channel-id", msg.sequence_number); + return Ok(vec![(*downstream_id, build_error("invalid-channel-id")).into()]); + }; + + let Some(vardiff) = channel_manager_data.vardiff.get_mut(&(channel_id, *downstream_id)) else { + return Err(JDCError::VardiffNotFound(channel_id)); + }; + vardiff.increment_shares_since_last_update(); + let res = extended_channel.validate_share(msg.clone()); + let mut is_downstream_share_valid = false; + match res { + Ok(ShareValidationResult::Valid) => { + info!( + "SubmitSharesExtended on downstream channel: valid share | channel_id: {}, sequence_number: {} ☑ï¸", + channel_id, msg.sequence_number + ); + is_downstream_share_valid = true; + } + Ok(ShareValidationResult::ValidWithAcknowledgement( + last_sequence_number, + new_submits_accepted_count, + new_shares_sum, + )) => { + let success = SubmitSharesSuccess { + channel_id, + last_sequence_number, + new_submits_accepted_count, + new_shares_sum, + }; + info!("SubmitSharesExtended on downstream channel: {} ✅", success); + is_downstream_share_valid = true; + messages.push(( + downstream.downstream_id, + Mining::SubmitSharesSuccess(success), + ).into()); + } + Ok(ShareValidationResult::BlockFound(template_id, coinbase)) => { + info!("SubmitSharesExtended on downstream channel: 💰 Block Found!!! 💰"); + if let Some(template_id) = template_id { + info!("SubmitSharesExtended: Propagating solution to the Template Provider."); + let solution = SubmitSolution { + template_id, + version: msg.version, + header_timestamp: msg.ntime, + header_nonce: msg.nonce, + coinbase_tx: coinbase.try_into()?, + }; + messages.push(TemplateDistribution::SubmitSolution(solution.clone()).into()); + } + let share_accounting = extended_channel.get_share_accounting().clone(); + let success = SubmitSharesSuccess { + channel_id, + last_sequence_number: share_accounting.get_last_share_sequence_number(), + new_submits_accepted_count: share_accounting.get_shares_accepted(), + new_shares_sum: share_accounting.get_share_work_sum(), + }; + is_downstream_share_valid = true; + messages.push(( + downstream.downstream_id, + Mining::SubmitSharesSuccess(success), + ).into()); + } + Err(err) => { + let code = match err { + ShareValidationError::Invalid => "invalid-share", + ShareValidationError::Stale => "stale-share", + ShareValidationError::InvalidJobId => "invalid-job-id", + ShareValidationError::DoesNotMeetTarget => "difficulty-too-low", + ShareValidationError::DuplicateShare => "duplicate-share", + _ => unreachable!(), + }; + error!("⌠SubmitSharesError on downstream channel: ch={}, seq={}, error={code}", channel_id, msg.sequence_number); + messages.push((*downstream_id, build_error(code)).into()); + } + } + + if !is_downstream_share_valid{ + return Ok(messages); + } + + if let Some(upstream_channel) = channel_manager_data.upstream_channel.as_mut() { + let prefix = extended_channel.get_extranonce_prefix().clone(); + let mut extranonce_parts = Vec::new(); + let up_prefix = upstream_channel.get_extranonce_prefix(); + extranonce_parts.extend_from_slice(&prefix[up_prefix.len()..]); + + let upstream_message = channel_manager_data + .downstream_channel_id_and_job_id_to_template_id + .get(&(channel_id, job_id)) + .and_then(|tid| channel_manager_data.template_id_to_upstream_job_id.get(tid)) + .map(|&upstream_job_id| { + let mut new_msg = msg.clone(); + new_msg.channel_id = upstream_channel.get_channel_id(); + new_msg.job_id = upstream_job_id as u32; + // We assign sequence number later, when we validate the share + // and send it to upstream. + new_msg.sequence_number = 0; + + extranonce_parts.extend_from_slice(&msg.extranonce.to_vec()); + new_msg.extranonce = extranonce_parts.try_into().unwrap(); + + new_msg + }); + if let Some(mut upstream_message) = upstream_message{ + let res = upstream_channel.validate_share(upstream_message.clone()); + match res { + Ok(client::share_accounting::ShareValidationResult::Valid) => { + upstream_message.sequence_number = channel_manager_data.sequence_number_factory.fetch_add(1, Ordering::Relaxed); + info!( + "SubmitSharesExtended forwarding it to upstream: valid share | channel_id: {}, sequence_number: {} ✅", + channel_id, upstream_message.sequence_number + ); + messages.push( + Mining::SubmitSharesExtended(upstream_message.into_static()).into(), + ); + } + Ok(client::share_accounting::ShareValidationResult::BlockFound) => { + upstream_message.sequence_number = channel_manager_data.sequence_number_factory.fetch_add(1, Ordering::Relaxed); + info!("SubmitSharesExtended forwarding it to upstream: 💰 Block Found!!! 💰"); + let mut channel_extranonce = upstream_channel.get_extranonce_prefix().to_vec(); + channel_extranonce.extend_from_slice(&upstream_message.extranonce.to_vec()); + let push_solution = PushSolution { + extranonce: channel_extranonce.try_into()?, + ntime: upstream_message.ntime, + nonce: upstream_message.nonce, + version: upstream_message.version, + nbits: prev_hash.n_bits, + prev_hash: prev_hash.prev_hash.clone(), + }; + messages.push(JobDeclaration::PushSolution(push_solution.clone()).into()); + messages.push(Mining::SubmitSharesExtended(upstream_message.into_static()).into()); + } + Err(err) => { + let code = match err { + client::share_accounting::ShareValidationError::Invalid=>"invalid-share", + client::share_accounting::ShareValidationError::Stale=>"stale-share", + client::share_accounting::ShareValidationError::InvalidJobId=>"invalid-job-id", + client::share_accounting::ShareValidationError::DoesNotMeetTarget=>"difficulty-too-low", + client::share_accounting::ShareValidationError::DuplicateShare=>"duplicate-share", + _ => unreachable!(), + }; + debug!("⌠SubmitSharesError not forwarding it to upstream: ch={}, seq={}, error={code}", channel_id, upstream_message.sequence_number); + } + } + } + } + + Ok(messages) + }) + })?; + + for messages in messages { + messages.forward(&self.channel_manager_channel).await; + } + + Ok(()) + } + + // Handles an incoming `SetCustomMiningJob` message from a downstream. + async fn handle_set_custom_mining_job( + &mut self, + _client_id: Option, + msg: SetCustomMiningJob<'_>, + ) -> Result<(), Self::Error> { + warn!("Received: {}", msg); + Err(Self::Error::UnexpectedMessage( + MESSAGE_TYPE_SET_CUSTOM_MINING_JOB, + )) + } +} diff --git a/roles/jd-client/src/lib/channel_manager/jd_message_handler.rs b/roles/jd-client/src/lib/channel_manager/jd_message_handler.rs new file mode 100644 index 0000000000..c0ff49b702 --- /dev/null +++ b/roles/jd-client/src/lib/channel_manager/jd_message_handler.rs @@ -0,0 +1,292 @@ +use stratum_common::roles_logic_sv2::{ + bitcoin::{ + self, absolute::LockTime, transaction::Version, OutPoint, ScriptBuf, Sequence, Transaction, + TxIn, TxOut, Witness, + }, + channels_sv2::outputs::deserialize_outputs, + codec_sv2::binary_sv2::{self, Sv2DataType, B016M}, + handlers_sv2::HandleJobDeclarationMessagesFromServerAsync, + job_declaration_sv2::{ + AllocateMiningJobTokenSuccess, DeclareMiningJobError, DeclareMiningJobSuccess, + ProvideMissingTransactions, ProvideMissingTransactionsSuccess, + }, + parsers_sv2::{AnyMessage, JobDeclaration, Mining, TemplateDistribution}, + template_distribution_sv2::CoinbaseOutputConstraints, +}; +use tracing::{debug, error, info, warn}; + +use crate::{ + channel_manager::ChannelManager, + error::JDCError, + status::{State, Status}, + utils::StdFrame, +}; + +impl HandleJobDeclarationMessagesFromServerAsync for ChannelManager { + type Error = JDCError; + + // Handles a successful `AllocateMiningJobToken` response from the JDS. + // + // When the JDS confirms job token allocation: + // - Updates the channel manager state with the newly issued token. + // - Checks whether the JDS has provided updated coinbase outputs. + // - If outputs have changed, recalculates the corresponding size and sigops constraints. + // - Sends an updated `CoinbaseOutputConstraints` message to the Template Provider to ensure + // the new coinbase rules are enforced. + // - If outputs are unchanged, skips recomputation and continues as normal. + async fn handle_allocate_mining_job_token_success( + &mut self, + _server_id: Option, + msg: AllocateMiningJobTokenSuccess<'_>, + ) -> Result<(), Self::Error> { + info!("Received: {}", msg); + + let coinbase_changed = self.channel_manager_data.super_safe_lock(|data| { + let changed = data.coinbase_outputs != msg.coinbase_outputs.to_vec(); + data.coinbase_outputs = msg.coinbase_outputs.to_vec(); + data.allocate_tokens = Some(msg.clone().into_static()); + changed + }); + + if coinbase_changed { + info!("Coinbase outputs from JDS changed, recalculating constraints"); + let deserialized_jds_coinbase_outputs: Vec = + bitcoin::consensus::deserialize(&msg.coinbase_outputs.to_vec()) + .map_err(JDCError::BitcoinEncodeError)?; + + let max_additional_size: usize = deserialized_jds_coinbase_outputs + .iter() + .map(|o| o.size()) + .sum(); + + // create a dummy coinbase transaction with the empty output + // this is used to calculate the sigops of the coinbase output + let dummy_coinbase = Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint::null(), + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::from(vec![vec![0; 32]]), + }], + output: deserialized_jds_coinbase_outputs, + }; + + let max_additional_sigops = dummy_coinbase.total_sigop_cost(|_| None) as u16; + + debug!( + max_additional_size, + max_additional_sigops, "Computed coinbase output constraints" + ); + + let coinbase_output_contraints_message = AnyMessage::TemplateDistribution( + TemplateDistribution::CoinbaseOutputConstraints(CoinbaseOutputConstraints { + coinbase_output_max_additional_size: max_additional_size as u32, + coinbase_output_max_additional_sigops: max_additional_sigops, + }), + ); + + let frame: StdFrame = coinbase_output_contraints_message.try_into()?; + + self.channel_manager_channel + .tp_sender + .send(frame) + .await + .map_err(|_e| JDCError::ChannelErrorSender)?; + + info!("Sent updated CoinbaseOutputConstraints to TP channel"); + } else { + debug!("Coinbase outputs unchanged, skipping constraints update"); + } + + Ok(()) + } + + // Handles a `DeclareMiningJobError` response from the JDS. + // + // Receiving this error is treated as a malicious or invalid upstream behavior, + // since it indicates the JDS has rejected a declared mining job request. + // + // Upon receiving it: + // - Triggers the fallback mechanism by signaling a shutdown through the status channel, causing + // the Job Declarator Client to enter `JobDeclaratorShutdownFallback`. + // + // This ensures that the system does not continue relying on a potentially + // untrustworthy or misbehaving JDS, and instead fails over to a safer state. + async fn handle_declare_mining_job_error( + &mut self, + _server_id: Option, + msg: DeclareMiningJobError<'_>, + ) -> Result<(), Self::Error> { + warn!("Received: {}", msg); + warn!("âš ï¸ JDS refused the declared job with a DeclareMiningJobError âŒ. Starting fallback mechanism."); + self.channel_manager_channel + .status_sender + .send(Status { + state: State::JobDeclaratorShutdownFallback(JDCError::Shutdown), + }) + .await + .map_err(|_e| JDCError::ChannelErrorSender)?; + + Ok(()) + } + + // Handles a `DeclareMiningJobSuccess` message from the JDS. + // + // Receiving this message means the JDS has accepted the declared mining job, + // giving us the green light to propagate it upstream. + // + // The steps are: + // 1. Look up the last declared job using the `request_id`. + // 2. Validate that a `prevhash` exists and retrieve job details. + // 3. Use the job factory to create a new `SetCustomMiningJob` request, embedding the token + // provided by the JDS. + // 4. Update the channel manager state with the newly created custom job. + // 5. Send the `SetCustomMiningJob` message to the upstream, ensuring the job is now distributed + // across the mining network. + // + // If any required data (like `prevhash` or the last declared job) is missing, + // this handler returns an error to prevent propagation of an incomplete job. + async fn handle_declare_mining_job_success( + &mut self, + _server_id: Option, + msg: DeclareMiningJobSuccess<'_>, + ) -> Result<(), Self::Error> { + info!("Received: {}", msg); + + let Some(last_declare_job) = self + .channel_manager_data + .super_safe_lock(|data| data.last_declare_job_store.get(&msg.request_id).cloned()) + else { + error!( + "No last_declare_job found for request_id={}", + msg.request_id + ); + return Err(JDCError::LastDeclareJobNotFound(msg.request_id)); + }; + + let Some(prevhash) = last_declare_job.prev_hash else { + error!("Prevhash not found for request_id = {}", msg.request_id); + return Err(JDCError::LastNewPrevhashNotFound); + }; + + let outputs = match deserialize_outputs(last_declare_job.coinbase_output.clone()) { + Ok(outputs) => outputs, + Err(_) => return Err(JDCError::ChannelManagerHasBadCoinbaseOutputs), + }; + + let Some(custom_job) = self + .channel_manager_data + .super_safe_lock(|channel_manager_data| { + let job_factory = channel_manager_data.job_factory.as_mut()?; + let upstream_channel = channel_manager_data.upstream_channel.as_ref()?; + let custom_job = job_factory.new_custom_job( + upstream_channel.get_channel_id(), + msg.request_id, + msg.new_mining_job_token, + prevhash.into(), + last_declare_job.template, + outputs, + ); + Some(custom_job) + }) + else { + return Err(JDCError::FailedToCreateCustomJob); + }; + + let custom_job = custom_job.map_err(|_e| JDCError::FailedToCreateCustomJob)?; + + self.channel_manager_data.super_safe_lock(|data| { + if let Some(value) = data.last_declare_job_store.get_mut(&msg.request_id) { + value.set_custom_mining_job = Some(custom_job.clone().into_static()); + } + }); + + let channel_id = custom_job.channel_id; + + debug!("Sending SetCustomMiningJob to the upstream with channel_id: {channel_id}"); + let message = AnyMessage::Mining(Mining::SetCustomMiningJob(custom_job)).into_static(); + let frame: StdFrame = message.try_into()?; + + self.channel_manager_channel + .upstream_sender + .send(frame) + .await + .map_err(|_e| JDCError::ChannelErrorSender)?; + + info!("Successfully sent SetCustomMiningJob to the upstream with channel_id: {channel_id}"); + Ok(()) + } + + // Handles a `ProvideMissingTransactions` request from the JDS. + // + // The JDS provides a list of transaction positions it could not resolve. + // We then: + // - Retrieve the full transaction list for the given `request_id`. + // - Identify which transactions are missing based on the provided positions. + // - Collect and package those transactions into a `ProvideMissingTransactionsSuccess`. + // - Send the response back to the JDS. + async fn handle_provide_missing_transactions( + &mut self, + _server_id: Option, + msg: ProvideMissingTransactions<'_>, + ) -> Result<(), Self::Error> { + let request_id = msg.request_id; + + info!("Received: {}", msg); + + let tx_store_entry = self + .channel_manager_data + .super_safe_lock(|data| data.last_declare_job_store.get(&request_id).cloned()); + + let Some(entry) = tx_store_entry else { + warn!( + "No transaction list found for request_id={}", + msg.request_id + ); + return Err(JDCError::LastDeclareJobNotFound(msg.request_id)); + }; + + let full_tx_list: Vec = entry + .tx_list + .iter() + .map(|raw| B016M::from_vec_unchecked(raw.clone())) + .collect(); + + let unknown_positions: Vec = msg.unknown_tx_position_list.into_inner(); + debug!( + total_known = full_tx_list.len(), + unknown_positions = unknown_positions.len(), + "Resolving missing transactions" + ); + + let missing_txns: Vec = unknown_positions + .iter() + .filter_map(|&pos| full_tx_list.get(pos as usize).cloned()) + .collect(); + + if missing_txns.is_empty() { + warn!("No matching transactions found for request_id={request_id}"); + } + + let response = ProvideMissingTransactionsSuccess { + request_id: msg.request_id, + transaction_list: binary_sv2::Seq064K::new(missing_txns) + .map_err(JDCError::BinarySv2)?, + }; + let frame: StdFrame = + AnyMessage::JobDeclaration(JobDeclaration::ProvideMissingTransactionsSuccess(response)) + .try_into()?; + + self.channel_manager_channel + .jd_sender + .send(frame) + .await + .map_err(|_e| JDCError::ChannelErrorSender)?; + + info!("Successfully sent ProvideMissingTransactionsSuccess to the JDS with request_id: {request_id}"); + + Ok(()) + } +} diff --git a/roles/jd-client/src/lib/channel_manager/mod.rs b/roles/jd-client/src/lib/channel_manager/mod.rs new file mode 100644 index 0000000000..cef324467d --- /dev/null +++ b/roles/jd-client/src/lib/channel_manager/mod.rs @@ -0,0 +1,1118 @@ +use std::{ + collections::{HashMap, VecDeque}, + net::SocketAddr, + sync::{ + atomic::{AtomicU32, Ordering}, + Arc, + }, +}; + +use async_channel::{Receiver, Sender}; +use key_utils::{Secp256k1PublicKey, Secp256k1SecretKey}; +use stratum_common::{ + network_helpers_sv2::noise_stream::NoiseTcpStream, + roles_logic_sv2::{ + self, + channels_sv2::{ + client::extended::ExtendedChannel, + server::{ + jobs::{ + extended::ExtendedJob, factory::JobFactory, job_store::DefaultJobStore, + standard::StandardJob, + }, + standard::StandardChannel, + }, + Vardiff, VardiffState, + }, + codec_sv2::{Responder, Sv2Frame}, + handlers_sv2::{ + HandleJobDeclarationMessagesFromServerAsync, HandleMiningMessagesFromClientAsync, + HandleMiningMessagesFromServerAsync, HandleTemplateDistributionMessagesFromServerAsync, + }, + job_declaration_sv2::{ + AllocateMiningJobToken, AllocateMiningJobTokenSuccess, DeclareMiningJob, + }, + mining_sv2::{ + ExtendedExtranonce, OpenExtendedMiningChannel, SetCustomMiningJob, SetTarget, Target, + UpdateChannel, MAX_EXTRANONCE_LEN, MESSAGE_TYPE_OPEN_EXTENDED_MINING_CHANNEL, + MESSAGE_TYPE_OPEN_STANDARD_MINING_CHANNEL, + }, + parsers_sv2::{AnyMessage, JobDeclaration, Mining}, + template_distribution_sv2::{NewTemplate, SetNewPrevHash as SetNewPrevHashTdp}, + utils::Mutex, + }, +}; +use tokio::{net::TcpListener, select, sync::broadcast}; +use tracing::{debug, error, info, warn}; + +use crate::{ + channel_manager::downstream_message_handler::RouteMessageTo, + config::JobDeclaratorClientConfig, + downstream::Downstream, + error::JDCError, + status::{handle_error, Status, StatusSender}, + task_manager::TaskManager, + utils::{ + AtomicUpstreamState, Message, PendingChannelRequest, SV2Frame, ShutdownMessage, StdFrame, + UpstreamState, + }, +}; +mod downstream_message_handler; +mod jd_message_handler; +mod template_message_handler; +mod upstream_message_handler; + +pub const JDC_SEARCH_SPACE_BYTES: usize = 4; + +/// A `DeclaredJob` encapsulates all the relevant data associated with a single +/// job declaration, including its template, optional messages, coinbase output, +/// and transaction list. +#[derive(Clone, Debug)] +pub struct DeclaredJob { + // The original `DeclareMiningJob` message associated with this job, + // if one was sent. + declare_mining_job: Option>, + // The template associated with the declared job. + template: NewTemplate<'static>, + // The `SetNewPrevHashTdp` message associated with this job, if available. + prev_hash: Option>, + // The `SetCustomMiningJob` message associated with this job, + // if a custom job was created. + set_custom_mining_job: Option>, + // The coinbase output for this job. + coinbase_output: Vec, + // The list of transactions included in the job’s template. + tx_list: Vec>, +} + +/// Central state container for the **Channel Manager**. +/// +/// `ChannelManagerData` holds all runtime state that the JDC +/// needs to manage downstream clients, upstream connections, extranonce allocation, +/// job tracking, and various ID factories. +pub struct ChannelManagerData { + // Mapping of `downstream_id` → `Downstream` object, + // used by the channel manager to locate and interact with downstream clients. + downstream: HashMap, + // Extranonce prefix factory for **extended downstream channels**. + // Each new extended downstream receives a unique extranonce prefix. + extranonce_prefix_factory_extended: ExtendedExtranonce, + // Extranonce prefix factory for **standard downstream channels**. + // Each new standard downstream receives a unique extranonce prefix. + extranonce_prefix_factory_standard: ExtendedExtranonce, + // Factory that generates **monotonically increasing request IDs** + // for messages sent from the JDC. + request_id_factory: AtomicU32, + // Factory that assigns a unique ID to each new **downstream connection**. + downstream_id_factory: AtomicU32, + // Factory that assigns a unique **channel ID** to each channel. + // + // âš ï¸ Note: In this version of the JDC, channel IDs are unique + // across *all downstreams*, not scoped per downstream. + channel_id_factory: AtomicU32, + // Factory that assigns a unique **sequence number** to each share + // submitted from the JDC to the upstream. + sequence_number_factory: AtomicU32, + // The last **future template** received from the upstream. + last_future_template: Option>, + // The last **new prevhash** received from the upstream. + last_new_prev_hash: Option>, + // The most recent set of **allocation tokens** received from the JDS. + allocate_tokens: Option>, + // Stores new templates as they arrive, mapped by their **template ID**. + template_store: HashMap>, + // Stores the last declared job, keyed by the `request_id` used when + // declaring the job to the JDS. + // This is later used to send a `SetCustomMiningJob`. + last_declare_job_store: HashMap, + // Maps a template ID → corresponding upstream job ID. + template_id_to_upstream_job_id: HashMap, + // Maps a downstream channel ID + job ID → corresponding template ID. + downstream_channel_id_and_job_id_to_template_id: HashMap<(u32, u32), u64>, + // The coinbase outputs currently in use. + coinbase_outputs: Vec, + // Maps channel ID → downstream ID. + channel_id_to_downstream_id: HashMap, + // The active upstream extended channel (client-side instance), if any. + upstream_channel: Option>, + // Optional "pool tag" string, identifying the pool. + pool_tag_string: Option, + // List of pending downstream connection requests, + // persisted while the JDC is opening a channel with the upstream. + pending_downstream_requests: VecDeque, + // Factory for creating **custom mining jobs**, if available. + job_factory: Option, + // Mapping of `(downstream_id, channel_id)` → vardiff controller. + // Each entry manages variable difficulty for a specific downstream channel. + vardiff: HashMap<(u32, u32), VardiffState>, +} + +impl ChannelManagerData { + /// Resets the internal state of the Channel Manager. + /// + /// This method is primarily used during **fallback scenarios** to clear and + /// reinitialize all internal data structures. It ensures that the Channel Manager + /// returns to a clean state, ready to handle fresh upstream or downstream connections. + pub fn reset(&mut self, coinbase_outputs: Vec) { + self.downstream.clear(); + self.template_store.clear(); + self.last_declare_job_store.clear(); + self.template_id_to_upstream_job_id.clear(); + self.downstream_channel_id_and_job_id_to_template_id.clear(); + self.channel_id_to_downstream_id.clear(); + self.pending_downstream_requests.clear(); + + self.downstream_id_factory = AtomicU32::new(0); + self.request_id_factory = AtomicU32::new(0); + self.channel_id_factory = AtomicU32::new(0); + + let (range_0, range_1, range_2) = { + let range_1 = 0..JDC_SEARCH_SPACE_BYTES; + ( + 0..range_1.start, + range_1.clone(), + range_1.end..MAX_EXTRANONCE_LEN, + ) + }; + self.extranonce_prefix_factory_extended = + ExtendedExtranonce::new(range_0.clone(), range_1.clone(), range_2.clone(), None) + .expect("valid ranges"); + self.extranonce_prefix_factory_standard = + ExtendedExtranonce::new(range_0, range_1, range_2, None).expect("valid ranges"); + + self.allocate_tokens = None; + self.upstream_channel = None; + self.pool_tag_string = None; + + self.coinbase_outputs = coinbase_outputs; + } +} + +/// Represents all communication channels managed by the Channel Manager. +/// +/// The `ChannelManagerChannel` holds all the asynchronous communication primitives +/// required for message exchange between the **Channel Manager** and other subsystems. +/// It ensures decoupled, structured communication between upstreams, downstreams, +/// the Job Dispatcher Service (JDS), and the Template Provider (TP). +/// +/// # Channels +/// 1. **Upstream**: +/// - `(upstream_sender, upstream_receiver)` Used to send and receive messages from the upstream +/// subsystem. +/// +/// 2. **JDS**: +/// - `(jd_sender, jd_receiver)` Handles communication with JDS. +/// +/// 3. **Template Provider**: +/// - `(tp_sender, tp_receiver)` Manages communication with the Template Provider. +/// +/// 4. **Downstream**: +/// - `(downstream_sender, downstream_receiver)` Broadcasts messages to all downstream clients +/// and receives messages from them. +/// +/// 5. **Status**: +/// - `status_sender` Allows the Channel Manager to notify the main status loop of critical state +/// changes. + +#[derive(Clone)] +pub struct ChannelManagerChannel { + upstream_sender: Sender, + upstream_receiver: Receiver, + jd_sender: Sender, + jd_receiver: Receiver, + tp_sender: Sender, + tp_receiver: Receiver, + downstream_sender: broadcast::Sender<(u32, Message)>, + downstream_receiver: Receiver<(u32, SV2Frame)>, + status_sender: Sender, +} + +/// Contains all the state of mutable and immutable data required +/// by channel manager to process its task along with channels +/// to perform message traversal. +#[derive(Clone)] +pub struct ChannelManager { + channel_manager_data: Arc>, + channel_manager_channel: ChannelManagerChannel, + miner_tag_string: String, + share_batch_size: usize, + shares_per_minute: f32, + user_identity: String, + /// This represent the current state of Upstream channel + /// 1. NoChannel: No active upstream connection. + /// 2. Pending: A channel request has been sent, awaiting response. + /// 3. Connected: An upstream channel is successfully established. + /// 4. SoloMining: No upstream is available; the JDC operates in solo mining mode. case. + pub upstream_state: AtomicUpstreamState, +} + +impl ChannelManager { + /// Constructor method used to instantiate the Channel Manager + #[allow(clippy::too_many_arguments)] + pub async fn new( + config: JobDeclaratorClientConfig, + upstream_sender: Sender, + upstream_receiver: Receiver, + jd_sender: Sender, + jd_receiver: Receiver, + tp_sender: Sender, + tp_receiver: Receiver, + downstream_sender: broadcast::Sender<(u32, Message)>, + downstream_receiver: Receiver<(u32, SV2Frame)>, + status_sender: Sender, + coinbase_outputs: Vec, + ) -> Result { + let (range_0, range_1, range_2) = { + let range_1 = 0..JDC_SEARCH_SPACE_BYTES; + ( + 0..range_1.start, + range_1.clone(), + range_1.end..MAX_EXTRANONCE_LEN, + ) + }; + + let make_extranonce_factory = || { + ExtendedExtranonce::new(range_0.clone(), range_1.clone(), range_2.clone(), None) + .expect("Failed to create ExtendedExtranonce with valid ranges") + }; + + let extranonce_prefix_factory_extended = make_extranonce_factory(); + let extranonce_prefix_factory_standard = make_extranonce_factory(); + + let channel_manager_data = Arc::new(Mutex::new(ChannelManagerData { + downstream: HashMap::new(), + extranonce_prefix_factory_extended, + extranonce_prefix_factory_standard, + downstream_id_factory: AtomicU32::new(0), + request_id_factory: AtomicU32::new(0), + channel_id_factory: AtomicU32::new(0), + sequence_number_factory: AtomicU32::new(0), + last_future_template: None, + last_new_prev_hash: None, + allocate_tokens: None, + template_store: HashMap::new(), + last_declare_job_store: HashMap::new(), + template_id_to_upstream_job_id: HashMap::new(), + downstream_channel_id_and_job_id_to_template_id: HashMap::new(), + coinbase_outputs, + channel_id_to_downstream_id: HashMap::new(), + upstream_channel: None, + pool_tag_string: None, + pending_downstream_requests: VecDeque::new(), + job_factory: None, + vardiff: HashMap::new(), + })); + + let channel_manager_channel = ChannelManagerChannel { + upstream_sender, + upstream_receiver, + jd_sender, + jd_receiver, + tp_sender, + tp_receiver, + downstream_sender, + downstream_receiver, + status_sender, + }; + + let channel_manager = ChannelManager { + channel_manager_data, + channel_manager_channel, + share_batch_size: config.share_batch_size() as usize, + shares_per_minute: config.shares_per_minute() as f32, + miner_tag_string: config.jdc_signature().to_string(), + user_identity: config.user_identity().to_string(), + upstream_state: AtomicUpstreamState::new(UpstreamState::SoloMining), + }; + + Ok(channel_manager) + } + + /// Starts the downstream server, and accepts new connection request. + #[allow(clippy::too_many_arguments)] + pub async fn start_downstream_server( + self, + authority_public_key: Secp256k1PublicKey, + authority_secret_key: Secp256k1SecretKey, + cert_validity_sec: u64, + listening_address: SocketAddr, + task_manager: Arc, + notify_shutdown: broadcast::Sender, + status_sender: Sender, + channel_manager_sender: Sender<(u32, SV2Frame)>, + channel_manager_receiver: broadcast::Sender<(u32, Message)>, + ) -> Result<(), JDCError> { + info!("Starting downstream server at {listening_address}"); + let server = TcpListener::bind(listening_address).await.map_err(|e| { + error!(error = ?e, "Failed to bind downstream server at {listening_address}"); + e + })?; + + let mut shutdown_rx = notify_shutdown.subscribe(); + + let task_manager_clone = task_manager.clone(); + task_manager.spawn(async move { + + loop { + select! { + message = shutdown_rx.recv() => { + match message { + Ok(ShutdownMessage::ShutdownAll) => { + info!("Channel Manager: received shutdown signal"); + break; + } + Ok(ShutdownMessage::JobDeclaratorShutdownFallback(_)) => { + info!("Downstream Server: received job declarator shutdown signal"); + break; + } + Ok(ShutdownMessage::UpstreamShutdownFallback(_)) => { + info!("Downstream Server: received upstream shutdown signal"); + break; + } + Err(e) => { + warn!(error = ?e, "shutdown channel closed unexpectedly"); + break; + } + _ => {} + } + } + res = server.accept() => { + match res { + Ok((stream, socket_address)) => { + info!(%socket_address, "New downstream connection"); + let responder = match Responder::from_authority_kp( + &authority_public_key.into_bytes(), + &authority_secret_key.into_bytes(), + std::time::Duration::from_secs(cert_validity_sec), + ) { + Ok(r) => r, + Err(e) => { + error!(error = ?e, "Failed to create responder"); + continue; + } + }; + let noise_stream = match NoiseTcpStream::::new( + stream, + stratum_common::roles_logic_sv2::codec_sv2::HandshakeRole::Responder(responder), + ) + .await + { + Ok(ns) => ns, + Err(e) => { + error!(error = ?e, "Noise handshake failed"); + continue; + } + }; + + let downstream_id = self + .channel_manager_data + .super_safe_lock(|data| data.downstream_id_factory.fetch_add(1, Ordering::Relaxed)); + + let downstream = Downstream::new( + downstream_id, + channel_manager_sender.clone(), + channel_manager_receiver.clone(), + noise_stream, + notify_shutdown.clone(), + task_manager_clone.clone(), + status_sender.clone(), + ); + + self.channel_manager_data.super_safe_lock(|data| { + data.downstream.insert(downstream_id, downstream.clone()); + }); + + downstream + .start( + notify_shutdown.clone(), + status_sender.clone(), + task_manager_clone.clone(), + ) + .await; + } + + Err(e) => { + error!(error = ?e, "Failed to accept new downstream connection"); + } + } + } + } + } + info!("Downstream server: Unified loop break"); + }); + Ok(()) + } + + /// The central orchestrator of the Channel Manager. + /// + /// Responsible for receiving messages from all subsystems, processing them, + /// and either forwarding them to the appropriate subsystem or updating + /// the internal state of the Channel Manager as needed. + pub async fn start( + mut self, + notify_shutdown: broadcast::Sender, + status_sender: Sender, + task_manager: Arc, + ) { + let status_sender = StatusSender::ChannelManager(status_sender); + let mut shutdown_rx = notify_shutdown.subscribe(); + + task_manager.spawn(async move { + let cm = self.clone(); + let vd = self.clone(); + let vardiff_future = vd.run_vardiff_loop(); + tokio::pin!(vardiff_future); + loop { + let mut cm_jds = cm.clone(); + let mut cm_pool = cm.clone(); + let mut cm_template = cm.clone(); + let mut cm_downstreams = cm.clone(); + tokio::select! { + message = shutdown_rx.recv() => { + match message { + Ok(ShutdownMessage::ShutdownAll) => { + info!("Channel Manager: received shutdown signal"); + break; + } + Ok(ShutdownMessage::DownstreamShutdown(downstream_id)) => { + info!(%downstream_id, "Channel Manager: removing downstream after shutdown"); + if let Err(e) = self.remove_downstream(downstream_id) { + tracing::error!(%downstream_id, error = ?e, "Failed to remove downstream"); + } + } + Ok(ShutdownMessage::JobDeclaratorShutdownFallback((coinbase_outputs,tx))) => { + info!("Channel Manager: Job declarator shutdown signal"); + self.upstream_state.set(UpstreamState::SoloMining); + self.channel_manager_data.super_safe_lock(|data| data.reset(coinbase_outputs)); + drop(tx); + } + Ok(ShutdownMessage::UpstreamShutdownFallback((coinbase_outputs,tx))) => { + info!("Channel Manager: Upstream shutdown signal"); + self.upstream_state.set(UpstreamState::SoloMining); + self.channel_manager_data.super_safe_lock(|data| data.reset(coinbase_outputs)); + drop(tx); + } + Err(e) => { + warn!(error = ?e, "shutdown channel closed unexpectedly"); + break; + } + _ => {} + } + } + res = &mut vardiff_future => { + info!("Vardiff loop completed with: {res:?}"); + } + res = cm_jds.handle_jds_message() => { + if let Err(e) = res { + if !e.is_critical() { + continue; + } + error!(error = ?e, "Error handling JDS message"); + handle_error(&status_sender, e).await; + break; + } + } + res = cm_pool.handle_pool_message() => { + if let Err(e) = res { + if !e.is_critical() { + continue; + } + error!(error = ?e, "Error handling Pool message"); + handle_error(&status_sender, e).await; + break; + } + } + res = cm_template.handle_template_provider_message() => { + if let Err(e) = res { + if !e.is_critical() { + continue; + } + error!(error = ?e, "Error handling Template Receiver message"); + handle_error(&status_sender, e).await; + break; + } + } + res = cm_downstreams.handle_downstream_message() => { + if let Err(e) = res { + if !e.is_critical() { + continue; + } + error!(error = ?e, "Error handling Downstreams message"); + handle_error(&status_sender, e).await; + break; + } + } + } + } + }); + } + + // Removes a downstream entry from the Channel Manager’s state. + // + // Given a `downstream_id`, this method: + // 1. Removes the corresponding downstream from the `downstream` map. + // 2. Cleans up all associated channel mappings (both standard and extended) by removing their + // entries from `channel_id_to_downstream_id`. + fn remove_downstream(&mut self, downstream_id: u32) -> Result<(), JDCError> { + self.channel_manager_data.super_safe_lock(|cm_data| { + if let Some(downstream) = cm_data.downstream.remove(&downstream_id) { + downstream.downstream_data.super_safe_lock(|ds_data| { + for k in ds_data + .standard_channels + .keys() + .chain(ds_data.extended_channels.keys()) + { + cm_data.channel_id_to_downstream_id.remove(k); + } + }); + } + }); + Ok(()) + } + + /// Handles messages received from the JDS subsystem. + /// + /// This method listens for incoming frames on the `jd_receiver` channel. + /// - If the frame contains a JobDeclaration message, it forwards it to the job declaration + /// message handler. + /// - If the frame contains any unsupported message type, an error is returned. + async fn handle_jds_message(&mut self) -> Result<(), JDCError> { + if let Ok(mut sv2_frame) = self.channel_manager_channel.jd_receiver.recv().await { + let Some(message_type) = sv2_frame.get_header().map(|m| m.msg_type()) else { + return Ok(()); + }; + + self.handle_job_declaration_message_frame_from_server( + None, + message_type, + sv2_frame.payload(), + ) + .await?; + } + Ok(()) + } + + /// Handles messages received from the Upstream subsystem. + /// + /// This method listens for incoming frames on the `upstream_receiver` channel. + /// - If the frame contains a **Mining** message, it forwards it to the mining message + /// handler. + /// - If the frame contains any unsupported message type, an error is returned. + async fn handle_pool_message(&mut self) -> Result<(), JDCError> { + if let Ok(mut sv2_frame) = self.channel_manager_channel.upstream_receiver.recv().await { + let Some(message_type) = sv2_frame.get_header().map(|m| m.msg_type()) else { + return Ok(()); + }; + + self.handle_mining_message_frame_from_server(None, message_type, sv2_frame.payload()) + .await?; + } + Ok(()) + } + + // Handles messages received from the TP subsystem. + // + // This method listens for incoming frames on the `tp_receiver` channel. + // - If the frame contains a TemplateDistribution message, it forwards it to the template + // distribution message handler. + // - If the frame contains any unsupported message type, an error is returned. + async fn handle_template_provider_message(&mut self) -> Result<(), JDCError> { + if let Ok(mut sv2_frame) = self.channel_manager_channel.tp_receiver.recv().await { + let Some(message_type) = sv2_frame.get_header().map(|m| m.msg_type()) else { + return Ok(()); + }; + self.handle_template_distribution_message_frame_from_server( + None, + message_type, + sv2_frame.payload(), + ) + .await?; + } + Ok(()) + } + + // Handles messages received from downstream clients and routes them appropriately. + // + // # Overview + // This method is similar to the upstream JDS message handler, but introduces additional + // logic for handling OpenChannel requests (both standard and extended). + // + // # Message Flow + // - For most mining messages: The message is forwarded directly to + // `handle_mining_message_from_client`, and the `channel_id_to_downstream_id` map is used to + // determine the origin downstream. + // + // - For OpenChannel messages: At the time of request, the `channel_id` is not yet assigned, so + // we cannot map the message back to the downstream. To solve this: + // 1. The `downstream_id` is appended to the `user_identity` (e.g., + // `"identity#downstream_id"`). + // 2. Later, the appended downstream ID is stripped and used by the message handler to + // correctly attribute the request. + // + // # Channel Establishment Logic + // - NoChannel → Pending: + // - The first downstream OpenChannel request is stored in `pending_downstream_requests`. + // - The upstream state transitions from `NoChannel` to `Pending`. + // - A single channel request is then sent to the upstream (JDC → upstream). + // + // - Pending: + // - Additional downstream OpenChannel requests are stored in `pending_downstream_requests` + // until the upstream connection is established. + // + // - Connected / SoloMining: + // - Downstream OpenChannel requests are immediately forwarded to the mining handler. + // + // # Notes + // - Only one upstream channel is created per JDC instance. + // - After the upstream channel is established, all new downstream requests bypass the pending + // mechanism and are sent directly to the mining handler. + async fn handle_downstream_message(&mut self) -> Result<(), JDCError> { + if let Ok((downstream_id, mut sv2_frame)) = self + .channel_manager_channel + .downstream_receiver + .recv() + .await + { + let Some(message_type) = sv2_frame.get_header().map(|m| m.msg_type()) else { + return Err(JDCError::UnexpectedMessage(0)); + }; + + match message_type { + MESSAGE_TYPE_OPEN_EXTENDED_MINING_CHANNEL => { + let message: Mining = (message_type, sv2_frame.payload()).try_into()?; + let Mining::OpenExtendedMiningChannel(mut downstream_channel_request) = message + else { + return Err(JDCError::UnexpectedMessage(message_type)); + }; + let user_identity = format!( + "{}#{}", + downstream_channel_request.user_identity.as_utf8_or_hex(), + downstream_id + ); + downstream_channel_request.user_identity = user_identity.try_into()?; + + let downstream_msg = downstream_channel_request.clone().into_static(); + + match self.upstream_state.get() { + UpstreamState::NoChannel => { + self.channel_manager_data.super_safe_lock(|data| { + data.pending_downstream_requests + .push_front(downstream_msg.into()); + }); + + if self + .upstream_state + .compare_and_set(UpstreamState::NoChannel, UpstreamState::Pending) + .is_ok() + { + let mut upstream_message = downstream_channel_request; + upstream_message.user_identity = + self.user_identity.clone().try_into()?; + upstream_message.request_id = 1; + upstream_message.min_extranonce_size += + JDC_SEARCH_SPACE_BYTES as u16; + let upstream_message = AnyMessage::Mining( + Mining::OpenExtendedMiningChannel(upstream_message) + .into_static(), + ); + let frame: StdFrame = upstream_message.try_into()?; + + self.channel_manager_channel + .upstream_sender + .send(frame) + .await + .map_err(|_| JDCError::ChannelErrorSender)?; + } + } + UpstreamState::Pending => { + self.channel_manager_data.super_safe_lock(|data| { + data.pending_downstream_requests + .push_back(downstream_msg.into()); + }); + } + UpstreamState::Connected => { + self.send_open_channel_request_to_mining_handler( + Mining::OpenExtendedMiningChannel(downstream_msg), + message_type, + ) + .await?; + } + UpstreamState::SoloMining => { + self.send_open_channel_request_to_mining_handler( + Mining::OpenExtendedMiningChannel(downstream_msg), + message_type, + ) + .await?; + } + } + } + MESSAGE_TYPE_OPEN_STANDARD_MINING_CHANNEL => { + let message: Mining = (message_type, sv2_frame.payload()).try_into()?; + let Mining::OpenStandardMiningChannel(mut downstream_channel_request) = message + else { + return Err(JDCError::UnexpectedMessage(message_type)); + }; + + let user_identity = format!( + "{:?}#{}", + downstream_channel_request.user_identity, downstream_id + ); + downstream_channel_request.user_identity = user_identity.try_into()?; + + let downstream_msg = downstream_channel_request.clone().into_static(); + + match self.upstream_state.get() { + UpstreamState::NoChannel => { + self.channel_manager_data.super_safe_lock(|data| { + data.pending_downstream_requests + .push_front(downstream_msg.into()) + }); + + if self + .upstream_state + .compare_and_set(UpstreamState::NoChannel, UpstreamState::Pending) + .is_ok() + { + let upstream_open = OpenExtendedMiningChannel { + user_identity: self.user_identity.clone().try_into().unwrap(), + request_id: 1, + nominal_hash_rate: downstream_channel_request.nominal_hash_rate, + max_target: downstream_channel_request.max_target, + min_extranonce_size: JDC_SEARCH_SPACE_BYTES as u16, + }; + + let frame: StdFrame = AnyMessage::Mining( + Mining::OpenExtendedMiningChannel(upstream_open).into_static(), + ) + .try_into()?; + self.channel_manager_channel + .upstream_sender + .send(frame) + .await + .map_err(|_| JDCError::ChannelErrorSender)?; + } + } + UpstreamState::Pending => { + self.channel_manager_data.super_safe_lock(|data| { + data.pending_downstream_requests + .push_back(downstream_msg.into()) + }); + } + UpstreamState::Connected => { + self.send_open_channel_request_to_mining_handler( + Mining::OpenStandardMiningChannel(downstream_msg), + message_type, + ) + .await?; + } + UpstreamState::SoloMining => { + self.send_open_channel_request_to_mining_handler( + Mining::OpenStandardMiningChannel(downstream_msg), + message_type, + ) + .await?; + } + } + } + _ => { + self.handle_mining_message_frame_from_client( + None, + message_type, + sv2_frame.payload(), + ) + .await?; + } + } + } + + Ok(()) + } + + // Utility method to send open channel request from downstream to message handler. + async fn send_open_channel_request_to_mining_handler( + &mut self, + mining_msg: Mining<'static>, + message_type: u8, + ) -> Result<(), JDCError> { + let sv2_frame: Sv2Frame, Vec> = match Sv2Frame::from_message( + mining_msg, + message_type, + 0, + false, + ) { + Some(f) => f, + None => { + warn!(%message_type, "Failed to build Sv2Frame from mining message; dropping request"); + return Err(JDCError::FrameConversionError); + } + }; + + let mut serialized = vec![0u8; sv2_frame.encoded_length()]; + if let Err(e) = sv2_frame.serialize(&mut serialized) { + warn!(?e, %message_type, len = serialized.len(), "Failed to serialize Sv2Frame; dropping request"); + return Err(JDCError::FramingSv2(e)); + } + + let mut deserialized_frame = + match Sv2Frame::, Vec>::from_bytes(serialized) { + Ok(f) => f, + Err(e) => { + warn!(?e, %message_type, "Failed to deserialize Sv2Frame; dropping request"); + return Err(JDCError::FrameConversionError); + } + }; + + let payload = deserialized_frame.payload(); + self.handle_mining_message_frame_from_client(None, message_type, payload) + .await?; + Ok(()) + } + + /// Utility method to request for more token to JDS. + pub async fn allocate_tokens(&self, token_to_allocate: u32) -> Result<(), JDCError> { + debug!("Allocating {} job tokens", token_to_allocate); + + for i in 0..token_to_allocate { + let request_id = self + .channel_manager_data + .super_safe_lock(|data| data.request_id_factory.fetch_add(1, Ordering::Relaxed)); + + debug!( + request_id, + "Allocating token {}/{}", + i + 1, + token_to_allocate + ); + + let message = JobDeclaration::AllocateMiningJobToken(AllocateMiningJobToken { + user_identifier: self + .user_identity + .to_string() + .try_into() + .expect("Static string should always convert"), + request_id, + }); + + let frame: StdFrame = AnyMessage::JobDeclaration(message) + .try_into() + .map_err(|e| { + info!(error = ?e, "Failed to convert AllocateMiningJobToken to frame"); + e + })?; + + self.channel_manager_channel + .jd_sender + .send(frame) + .await + .map_err(|e| { + info!(error = ?e, "Failed to send AllocateMiningJobToken frame"); + JDCError::ChannelErrorSender + })?; + } + + info!("Requested allocation of {token_to_allocate} mining job tokens to JDS"); + Ok(()) + } + + // Runs the vardiff on extended channel. + fn run_vardiff_on_extended_channel( + downstream_id: u32, + channel_id: u32, + channel_state: &mut roles_logic_sv2::channels_sv2::server::extended::ExtendedChannel< + 'static, + DefaultJobStore>, + >, + vardiff_state: &mut VardiffState, + updates: &mut Vec, + ) { + let (hashrate, target, shares_per_minute) = ( + channel_state.get_nominal_hashrate(), + channel_state.get_target(), + channel_state.get_shares_per_minute(), + ); + + let Ok(new_hashrate_opt) = vardiff_state.try_vardiff(hashrate, target, shares_per_minute) + else { + debug!("Vardiff computation failed for extended channel {channel_id}"); + return; + }; + + let Some(new_hashrate) = new_hashrate_opt else { + return; + }; + + match channel_state.update_channel(new_hashrate, None) { + Ok(()) => { + let updated_target = channel_state.get_target(); + updates.push( + ( + downstream_id, + Mining::SetTarget(SetTarget { + channel_id, + maximum_target: updated_target.clone().into(), + }), + ) + .into(), + ); + debug!("Updated target for extended channel_id={channel_id} to {updated_target:?}",); + } + Err(e) => warn!( + "Failed to update extended channel channel_id={channel_id} during vardiff {e:?}" + ), + } + } + + // Runs the vardiff on the standard channel. + fn run_vardiff_on_standard_channel( + downstream_id: u32, + channel_id: u32, + channel: &mut StandardChannel<'static, DefaultJobStore>>, + vardiff_state: &mut VardiffState, + updates: &mut Vec, + ) { + let hashrate = channel.get_nominal_hashrate(); + let target = channel.get_target(); + let shares_per_minute = channel.get_shares_per_minute(); + + let Ok(new_hashrate_opt) = vardiff_state.try_vardiff(hashrate, target, shares_per_minute) + else { + debug!("Vardiff computation failed for standard channel {channel_id}"); + return; + }; + + if let Some(new_hashrate) = new_hashrate_opt { + match channel.update_channel(new_hashrate, None) { + Ok(()) => { + let updated_target = channel.get_target(); + updates.push( + ( + downstream_id, + Mining::SetTarget(SetTarget { + channel_id, + maximum_target: updated_target.clone().into(), + }), + ) + .into(), + ); + debug!("Updated target for standard channel channel_id={channel_id} to {updated_target:?}"); + } + Err(e) => warn!( + "Failed to update standard channel channel_id={channel_id} during vardiff {e:?}" + ), + } + } + } + + // Periodic vardiff task loop. + // + // # Purpose + // - Executes the vardiff cycle every 60 seconds for all downstreams. + // - Delegates to [`Self::run_vardiff`] on each tick. + async fn run_vardiff_loop(&self) -> Result<(), JDCError> { + let mut ticker = tokio::time::interval(std::time::Duration::from_secs(60)); + loop { + ticker.tick().await; + info!("Starting vardiff loop for downstreams"); + + if let Err(e) = self.run_vardiff().await { + error!(error = ?e, "Vardiff iteration failed"); + } + } + } + + // Runs vardiff across **all channels** and generates updates. + // + // # Purpose + // - Iterates through all downstream channels (both standard and extended). + // - Runs vardiff for each channel and collects the resulting updates. + // - Propagates difficulty changes to downstreams and also sends an `UpdateChannel` message + // upstream if applicable. + async fn run_vardiff(&self) -> Result<(), JDCError> { + let mut messages: Vec = vec![]; + self.channel_manager_data + .super_safe_lock(|channel_manager_data| { + for ((channel_id, downstream_id), vardiff_state) in + channel_manager_data.vardiff.iter_mut() + { + let Some(downstream) = channel_manager_data.downstream.get_mut(downstream_id) + else { + continue; + }; + downstream.downstream_data.super_safe_lock(|data| { + if let Some(standard_channel) = data.standard_channels.get_mut(channel_id) { + Self::run_vardiff_on_standard_channel( + *downstream_id, + *channel_id, + standard_channel, + vardiff_state, + &mut messages, + ); + } + if let Some(extended_channel) = data.extended_channels.get_mut(channel_id) { + Self::run_vardiff_on_extended_channel( + *downstream_id, + *channel_id, + extended_channel, + vardiff_state, + &mut messages, + ); + } + }); + } + + if !messages.is_empty() { + let mut downstream_hashrate = 0.0; + let mut min_target: Target = [0xff; 32].into(); + + for (_, downstream) in channel_manager_data.downstream.iter() { + downstream.downstream_data.super_safe_lock(|data| { + let mut update_from_channel = |hashrate: f32, target: &Target| { + downstream_hashrate += hashrate; + min_target = std::cmp::min(target.clone(), min_target.clone()); + }; + + for (_, channel) in data.standard_channels.iter() { + update_from_channel( + channel.get_nominal_hashrate(), + channel.get_target(), + ); + } + + for (_, channel) in data.extended_channels.iter() { + update_from_channel( + channel.get_nominal_hashrate(), + channel.get_target(), + ); + } + }); + } + + if let Some(ref upstream_channel) = channel_manager_data.upstream_channel { + debug!( + "Checking upstream channel {} with hashrate {} and target {:?}", + upstream_channel.get_channel_id(), + upstream_channel.get_nominal_hashrate(), + upstream_channel.get_target() + ); + + info!("Sending update channel message upstream"); + messages.push( + Mining::UpdateChannel(UpdateChannel { + channel_id: upstream_channel.get_channel_id(), + nominal_hash_rate: downstream_hashrate, + maximum_target: min_target.into(), + }) + .into(), + ) + } + } + }); + + for message in messages { + message.forward(&self.channel_manager_channel).await; + } + + info!("Vardiff update cycle complete"); + Ok(()) + } +} diff --git a/roles/jd-client/src/lib/channel_manager/template_message_handler.rs b/roles/jd-client/src/lib/channel_manager/template_message_handler.rs new file mode 100644 index 0000000000..af00d62c40 --- /dev/null +++ b/roles/jd-client/src/lib/channel_manager/template_message_handler.rs @@ -0,0 +1,600 @@ +use std::sync::atomic::Ordering; + +use stratum_common::roles_logic_sv2::{ + bitcoin::{consensus, hashes::Hash, Amount, Transaction}, + channels_sv2::{chain_tip::ChainTip, outputs::deserialize_outputs}, + codec_sv2::binary_sv2::{Seq064K, U256}, + handlers_sv2::HandleTemplateDistributionMessagesFromServerAsync, + job_declaration_sv2::DeclareMiningJob, + mining_sv2::SetNewPrevHash as SetNewPrevHashMp, + parsers_sv2::{AnyMessage, JobDeclaration, Mining, TemplateDistribution}, + template_distribution_sv2::*, +}; +use tracing::{error, info, warn}; + +use crate::{ + channel_manager::{downstream_message_handler::RouteMessageTo, ChannelManager, DeclaredJob}, + error::JDCError, + jd_mode::{get_jd_mode, JdMode}, + utils::StdFrame, +}; + +impl HandleTemplateDistributionMessagesFromServerAsync for ChannelManager { + type Error = JDCError; + + // Handles a `NewTemplate` message from the Template Provider. + // + // Behavior depends on the JD mode: + // - FullTemplate: sends a `RequestTransactionData` to start the declare-mining-job flow. + // - CoinbaseOnly: sends a `SetCustomMiningJob` and continues with that flow. + // + // In both modes, the new template is stored and propagated to all + // downstream channels, updating their state and dispatching the + // appropriate mining job messages (standard, group, or extended). + // + // Also updates future/active template state and triggers token + // allocation if needed. + async fn handle_new_template( + &mut self, + _server_id: Option, + msg: NewTemplate<'_>, + ) -> Result<(), Self::Error> { + info!("Received: {}", msg); + + let coinbase_outputs = self.channel_manager_data.super_safe_lock(|data| { + data.template_store + .insert(msg.template_id, msg.clone().into_static()); + if msg.future_template { + data.last_future_template = Some(msg.clone().into_static()); + } + data.coinbase_outputs.clone() + }); + + let mut coinbase_outputs = deserialize_outputs(coinbase_outputs) + .map_err(|_| JDCError::ChannelManagerHasBadCoinbaseOutputs)?; + + if get_jd_mode() == JdMode::FullTemplate { + let tx_data_request = AnyMessage::TemplateDistribution( + TemplateDistribution::RequestTransactionData(RequestTransactionData { + template_id: msg.template_id, + }), + ); + let frame: StdFrame = tx_data_request.try_into()?; + self.channel_manager_channel + .tp_sender + .send(frame) + .await + .map_err(|_e| JDCError::ChannelErrorSender)?; + } + + let messages = self.channel_manager_data.super_safe_lock(|channel_manager_data| { + let mut messages: Vec = Vec::new(); + coinbase_outputs[0].value = Amount::from_sat(msg.coinbase_tx_value_remaining); + + for (downstream_id, downstream) in channel_manager_data.downstream.iter_mut() { + + let messages_ = downstream.downstream_data.super_safe_lock(|data| { + + let mut messages: Vec = vec![]; + + let group_channel_job = if let Some(ref mut group_channel) = data.group_channels { + if group_channel.on_new_template(msg.clone().into_static(), coinbase_outputs.clone()).is_ok() { + match msg.future_template { + true => { + let future_job_id = group_channel + .get_future_template_to_job_id() + .get(&msg.template_id) + .expect("job_id must exist"); + Some(group_channel + .get_future_jobs() + .get(future_job_id) + .expect("future job must exist")).cloned() + }, + false => { + Some(group_channel + .get_active_job() + .expect("active job must exist")).cloned() + } + } + } else { + tracing::error!("Some issue with downstream: {downstream_id}, group channel"); + None + } + } else { + None + }; + + if let Some(upstream_channel) = channel_manager_data.upstream_channel.as_mut() { + if !msg.future_template && get_jd_mode() == JdMode::CoinbaseOnly { + if let (Some(token), Some(prevhash)) = ( + channel_manager_data.allocate_tokens.clone(), + channel_manager_data.last_new_prev_hash.clone(), + ) { + let request_id = channel_manager_data.request_id_factory.fetch_add(1, Ordering::Relaxed); + let job_factory = channel_manager_data.job_factory.as_mut().unwrap(); + let custom_job = job_factory.new_custom_job(upstream_channel.get_channel_id(), request_id, token.clone().mining_job_token, prevhash.clone().into(), msg.clone(), coinbase_outputs.clone()); + + if let Ok(custom_job) = custom_job{ + let last_declare = DeclaredJob { + declare_mining_job: None, + template: msg.clone().into_static(), + prev_hash: Some(prevhash), + set_custom_mining_job: Some(custom_job.clone().into_static()), + coinbase_output: channel_manager_data.coinbase_outputs.clone(), + tx_list: Vec::new(), + }; + channel_manager_data + .last_declare_job_store + .insert(request_id, last_declare); + messages.push( + Mining::SetCustomMiningJob(custom_job).into() + ); + } + } + } + } + match msg.future_template { + true => { + for (channel_id, standard_channel) in data.standard_channels.iter_mut() { + if data.group_channels.is_none() { + if let Err(e) = standard_channel.on_new_template(msg.clone().into_static(), coinbase_outputs.clone()) { + tracing::error!("Error while adding template to standard channel: {channel_id:?} {e:?}"); + continue; + } + let standard_job_id = standard_channel.get_future_template_to_job_id().get(&msg.template_id).expect("job_id must exist"); + let standard_job = standard_channel.get_future_jobs().get(standard_job_id).expect("standard job must exist"); + channel_manager_data.downstream_channel_id_and_job_id_to_template_id.insert((*channel_id, *standard_job_id), msg.template_id); + let standard_job_message = standard_job.get_job_message(); + messages.push((*downstream_id, Mining::NewMiningJob(standard_job_message.clone())).into()); + } + if let Some(ref group_channel_job) = group_channel_job { + if let Err(e) = standard_channel.on_new_template(msg.clone().into_static(), coinbase_outputs.clone()) { + tracing::error!("Error while adding template to standard channel: {channel_id:?} {e:?}"); + continue; + } + _ = standard_channel + .on_group_channel_job(group_channel_job.clone()); + } + } + if let Some(group_channel_job) = group_channel_job { + let job_message = group_channel_job.get_job_message(); + messages.push((*downstream_id, Mining::NewExtendedMiningJob(job_message.clone())).into()); + } + + for (channel_id, extended_channel) in data.extended_channels.iter_mut() { + if let Err(e) = extended_channel.on_new_template(msg.clone().into_static(), coinbase_outputs.clone()) { + tracing::error!("Error while adding template to standard channel: {channel_id:?} {e:?}"); + continue; + } + let extended_job_id = extended_channel + .get_future_template_to_job_id() + .get(&msg.template_id) + .expect("job_id must exist"); + + let extended_job = extended_channel + .get_future_jobs() + .get(extended_job_id) + .expect("extended job must exist"); + + channel_manager_data.downstream_channel_id_and_job_id_to_template_id.insert((*channel_id, *extended_job_id), msg.template_id); + let extended_job_message = extended_job.get_job_message(); + + messages.push((*downstream_id,Mining::NewExtendedMiningJob(extended_job_message.clone())).into()); + } + } + false => { + for (channel_id, standard_channel) in data.standard_channels.iter_mut() { + if data.group_channels.is_none() { + if let Err(e) = standard_channel.on_new_template(msg.clone().into_static(), coinbase_outputs.clone()) { + tracing::error!("Error while adding template to standard channel: {channel_id:?} {e:?}"); + continue; + } + let standard_job = standard_channel.get_active_job().expect("standard job must exist"); + channel_manager_data.downstream_channel_id_and_job_id_to_template_id.insert((*channel_id, standard_job.get_job_id()), msg.template_id); + let standard_job_message = standard_job.get_job_message(); + messages.push((*downstream_id, Mining::NewMiningJob(standard_job_message.clone())).into()); + } + if let Some(ref group_channel_job) = group_channel_job { + if let Err(e) = standard_channel.on_new_template(msg.clone().into_static(), coinbase_outputs.clone()) { + tracing::error!("Error while adding template to standard channel: {channel_id:?} {e:?}"); + continue; + } + _ = standard_channel + .on_group_channel_job(group_channel_job.clone()); + } + } + if let Some(group_channel_job) = group_channel_job { + let job_message = group_channel_job.get_job_message(); + messages.push((*downstream_id, Mining::NewExtendedMiningJob(job_message.clone())).into()); + } + + for (channel_id, extended_channel) in data.extended_channels.iter_mut() { + if let Err(e) = extended_channel.on_new_template(msg.clone().into_static(), coinbase_outputs.clone()) { + tracing::error!("Error while adding template to standard channel: {channel_id:?} {e:?}"); + continue; + } + let extended_job = extended_channel + .get_active_job() + .expect("extended job must exist"); + + channel_manager_data.downstream_channel_id_and_job_id_to_template_id.insert((*channel_id, extended_job.get_job_id()), msg.template_id); + let extended_job_message = extended_job.get_job_message(); + + messages.push((*downstream_id,Mining::NewExtendedMiningJob(extended_job_message.clone())).into()); + } + } + } + + messages + + }); + messages.extend(messages_); + } + messages + }); + + if get_jd_mode() == JdMode::CoinbaseOnly && !msg.future_template { + _ = self.allocate_tokens(1).await; + } + + for message in messages { + message.forward(&self.channel_manager_channel).await; + } + + Ok(()) + } + + // Handles a `RequestTransactionDataError` message from the Template Provider. + async fn handle_request_tx_data_error( + &mut self, + _server_id: Option, + msg: RequestTransactionDataError<'_>, + ) -> Result<(), Self::Error> { + warn!("Received: {}", msg); + let error_code = msg.error_code.as_utf8_or_hex(); + + if matches!( + error_code.as_str(), + "template-id-not-found" | "stale-template-id" + ) { + return Ok(()); + } + Err(JDCError::TxDataError) + } + + // Handles a `RequestTransactionDataSuccess` message from the Template Provider. + // + // Flow: + // - If the template is not a future template, immediately declare a mining job to JDS. + // - If the template is a future template: + // - Check if the current `prevhash` activates this template. + // - If activated → proceed with the normal declare job flow. + // - If not activated → cache it as a declare job for later propagation. + async fn handle_request_tx_data_success( + &mut self, + _server_id: Option, + msg: RequestTransactionDataSuccess<'_>, + ) -> Result<(), Self::Error> { + info!("Received: {}", msg); + + let transactions_data = msg.transaction_list; + let excess_data = msg.excess_data; + + let coinbase_outputs = self + .channel_manager_data + .super_safe_lock(|data| data.coinbase_outputs.clone()); + + let mut deserialized_outputs = deserialize_outputs(coinbase_outputs) + .map_err(|_| JDCError::ChannelManagerHasBadCoinbaseOutputs)?; + + let (token, template_message, request_id, prevhash) = + self.channel_manager_data.super_safe_lock(|data| { + ( + data.allocate_tokens.clone(), + data.template_store.remove(&msg.template_id), + data.request_id_factory.fetch_add(1, Ordering::Relaxed), + data.last_new_prev_hash.clone(), + ) + }); + + _ = self.allocate_tokens(1).await; + let Some(token) = token else { + error!("Token not found, template id: {}", msg.template_id); + return Err(JDCError::TokenNotFound); + }; + + let Some(template_message) = template_message else { + error!("Template not found, template id: {}", msg.template_id); + return Err(JDCError::TemplateNotFound(msg.template_id)); + }; + + let mining_token = token.mining_job_token.clone(); + deserialized_outputs[0].value = + Amount::from_sat(template_message.coinbase_tx_value_remaining); + let reserialized_outputs = consensus::serialize(&deserialized_outputs); + + let tx_list: Vec = transactions_data + .to_vec() + .iter() + .map(|raw_tx| consensus::deserialize(raw_tx).expect("invalid tx")) + .collect(); + + let txids_as_u256: Vec> = tx_list + .iter() + .map(|tx| { + let txid = tx.compute_txid(); + let byte_array: [u8; 32] = *txid.as_byte_array(); + U256::Owned(byte_array.to_vec()) + }) + .collect(); + + let tx_ids = Seq064K::new(txids_as_u256).map_err(JDCError::BinarySv2)?; + let is_activated_future_template = template_message.future_template + && prevhash + .map(|prev_hash| prev_hash.template_id != template_message.template_id) + .unwrap_or(true); + + let declare_job = self.channel_manager_data.super_safe_lock(|data| { + let job_factory = data.job_factory.as_mut()?; + + if let Ok((coinbase_tx_prefix, coinbase_tx_suffix)) = job_factory + .new_coinbase_tx_prefix_and_suffix( + template_message.clone(), + deserialized_outputs.clone(), + ) + { + let version = template_message.version; + + let declare_job = DeclareMiningJob { + request_id, + mining_job_token: mining_token.to_vec().try_into().unwrap(), + version, + coinbase_tx_prefix: coinbase_tx_prefix.try_into().unwrap(), + coinbase_tx_suffix: coinbase_tx_suffix.try_into().unwrap(), + tx_ids_list: tx_ids, + excess_data: excess_data.to_vec().try_into().unwrap(), + }; + + let last_declare = DeclaredJob { + declare_mining_job: Some(declare_job.clone()), + template: template_message, + prev_hash: data.last_new_prev_hash.clone(), + set_custom_mining_job: None, + coinbase_output: reserialized_outputs, + tx_list: transactions_data.to_vec(), + }; + + data.last_declare_job_store.insert(request_id, last_declare); + + return Some(declare_job); + } + None + }); + + if is_activated_future_template { + return Ok(()); + } + + if let Some(declare_job) = declare_job { + let frame: StdFrame = + AnyMessage::JobDeclaration(JobDeclaration::DeclareMiningJob(declare_job)) + .try_into()?; + + _ = self.channel_manager_channel.jd_sender.send(frame).await; + } + + Ok(()) + } + + // Handles a `SetNewPrevHash` message: + // + // - Check `declare_job_cache` to see if the `prevhash` activates a future template. + // - In FullTemplate mode → send a `DeclareMiningJob`. + // - In CoinbaseOnly mode → send a `CustomMiningJob` for the activated future template. + // - Update the upstream channel state. + // - Update all downstream channels and propagate the new `prevhash` via `SetNewPrevHash`. + async fn handle_set_new_prev_hash( + &mut self, + _server_id: Option, + msg: SetNewPrevHash<'_>, + ) -> Result<(), Self::Error> { + info!("Received: {}", msg); + + let coinbase_outputs = self + .channel_manager_data + .super_safe_lock(|data| data.coinbase_outputs.clone()); + + let outputs = deserialize_outputs(coinbase_outputs) + .map_err(|_| JDCError::ChannelManagerHasBadCoinbaseOutputs)?; + + let (future_template, declare_job) = self.channel_manager_data.super_safe_lock(|data| { + if let Some(upstream_channel) = data.upstream_channel.as_mut() { + if let Err(e) = upstream_channel.on_chain_tip_update(msg.clone().into()) { + error!( + "Couldn't update chaintip of the upstream channel: {msg}, error: {e:#?}" + ); + } + } + + let declare_job = data + .last_declare_job_store + .values() + .find(|declared_job| { + Some(declared_job.template.template_id) + == data.last_future_template.as_ref().map(|t| t.template_id) + }) + .map(|declared_job| declared_job.declare_mining_job.clone()); + + (data.last_future_template.clone(), declare_job) + }); + + if get_jd_mode() == JdMode::FullTemplate { + if let Some(Some(job)) = declare_job { + let frame: StdFrame = + AnyMessage::JobDeclaration(JobDeclaration::DeclareMiningJob(job)).try_into()?; + + self.channel_manager_channel + .jd_sender + .send(frame) + .await + .map_err(|_e| JDCError::ChannelErrorSender)?; + } + } + + let messages = self.channel_manager_data.super_safe_lock(|data| { + data.last_new_prev_hash = Some(msg.clone().into_static()); + data.last_declare_job_store.iter_mut().for_each(|(_k, v)| { + if v.template.future_template && v.template.template_id == msg.template_id { + v.prev_hash = Some(msg.clone().into_static()); + v.template.future_template = false; + } + }); + + let mut messages: Vec = vec![]; + + if let Some(ref mut upstream_channel) = data.upstream_channel { + _ = upstream_channel.on_chain_tip_update(msg.clone().into()); + + if get_jd_mode() == JdMode::CoinbaseOnly { + if let (Some(job_factory), Some(token), Some(template)) = ( + data.job_factory.as_mut(), + data.allocate_tokens.clone(), + future_template.clone(), + ) { + let request_id = data.request_id_factory.fetch_add(1, Ordering::Relaxed); + let chain_tip = ChainTip::new( + msg.prev_hash.clone().into_static(), + msg.n_bits, + msg.header_timestamp, + ); + + if let Ok(custom_job) = job_factory.new_custom_job( + upstream_channel.get_channel_id(), + request_id, + token.clone().mining_job_token, + chain_tip, + template.clone(), + outputs, + ) { + let last_declare = DeclaredJob { + declare_mining_job: None, + template: template.into_static(), + prev_hash: Some(msg.clone().into_static()), + set_custom_mining_job: Some(custom_job.clone().into_static()), + coinbase_output: data.coinbase_outputs.clone(), + tx_list: vec![], + }; + + data.last_declare_job_store.insert(request_id, last_declare); + messages.push(Mining::SetCustomMiningJob(custom_job).into()); + } + } + } + } + + for (downstream_id, downstream) in data.downstream.iter_mut() { + let downstream_messages = downstream.downstream_data.super_safe_lock(|data| { + let mut messages: Vec = vec![]; + if let Some(ref mut group_channel) = data.group_channels { + _ = group_channel.on_set_new_prev_hash(msg.clone().into_static()); + let group_channel_id = group_channel.get_group_channel_id(); + let activated_group_job_id = group_channel + .get_active_job() + .expect("active job must exist") + .get_job_id(); + + let set_new_prev_hash_message = SetNewPrevHashMp { + channel_id: group_channel_id, + job_id: activated_group_job_id, + prev_hash: msg.prev_hash.clone(), + min_ntime: msg.header_timestamp, + nbits: msg.n_bits, + }; + messages.push( + ( + *downstream_id, + Mining::SetNewPrevHash(set_new_prev_hash_message), + ) + .into(), + ); + } + + for (channel_id, standard_channel) in data.standard_channels.iter_mut() { + if let Err(_e) = + standard_channel.on_set_new_prev_hash(msg.clone().into_static()) + { + continue; + }; + + // did SetupConnection have the REQUIRES_STANDARD_JOBS flag set? + // if yes, there's no group channel, so we need to send the SetNewPrevHashMp + // to each standard channel + if data.group_channels.is_none() { + let activated_standard_job_id = standard_channel + .get_active_job() + .expect("active job must exist") + .get_job_id(); + let set_new_prev_hash_message = SetNewPrevHashMp { + channel_id: *channel_id, + job_id: activated_standard_job_id, + prev_hash: msg.prev_hash.clone(), + min_ntime: msg.header_timestamp, + nbits: msg.n_bits, + }; + messages.push( + ( + *downstream_id, + Mining::SetNewPrevHash(set_new_prev_hash_message), + ) + .into(), + ); + } + } + + for (channel_id, extended_channel) in data.extended_channels.iter_mut() { + if let Err(_e) = + extended_channel.on_set_new_prev_hash(msg.clone().into_static()) + { + continue; + }; + + let activated_extended_job_id = extended_channel + .get_active_job() + .expect("active job must exist") + .get_job_id(); + let set_new_prev_hash_message = SetNewPrevHashMp { + channel_id: *channel_id, + job_id: activated_extended_job_id, + prev_hash: msg.prev_hash.clone(), + min_ntime: msg.header_timestamp, + nbits: msg.n_bits, + }; + messages.push( + ( + *downstream_id, + Mining::SetNewPrevHash(set_new_prev_hash_message), + ) + .into(), + ); + } + + messages + }); + + messages.extend(downstream_messages); + } + + messages + }); + + if get_jd_mode() == JdMode::CoinbaseOnly { + _ = self.allocate_tokens(1).await; + } + + for message in messages { + message.forward(&self.channel_manager_channel).await; + } + + Ok(()) + } +} diff --git a/roles/jd-client/src/lib/channel_manager/upstream_message_handler.rs b/roles/jd-client/src/lib/channel_manager/upstream_message_handler.rs new file mode 100644 index 0000000000..4576dfacf3 --- /dev/null +++ b/roles/jd-client/src/lib/channel_manager/upstream_message_handler.rs @@ -0,0 +1,597 @@ +use std::sync::atomic::Ordering; + +use stratum_common::roles_logic_sv2::{ + self, + channels_sv2::{ + client::extended::ExtendedChannel, outputs::deserialize_outputs, + server::jobs::factory::JobFactory, + }, + handlers_sv2::{HandleMiningMessagesFromServerAsync, SupportedChannelTypes}, + mining_sv2::*, + parsers_sv2::{AnyMessage, Mining, TemplateDistribution}, + template_distribution_sv2::RequestTransactionData, +}; +use tracing::{debug, error, info, warn}; + +use crate::{ + channel_manager::{ + downstream_message_handler::RouteMessageTo, ChannelManager, DeclaredJob, + JDC_SEARCH_SPACE_BYTES, + }, + error::JDCError, + jd_mode::{get_jd_mode, JdMode}, + status::{State, Status}, + utils::{create_close_channel_msg, PendingChannelRequest, StdFrame, UpstreamState}, +}; + +impl HandleMiningMessagesFromServerAsync for ChannelManager { + type Error = JDCError; + + fn get_channel_type_for_server(&self, _server_id: Option) -> SupportedChannelTypes { + SupportedChannelTypes::Extended + } + fn is_work_selection_enabled_for_server(&self, _server_id: Option) -> bool { + true + } + + // Handles an unexpected `OpenStandardMiningChannelSuccess` message from the upstream. + // + // The Job Declarator Client (JDC) only supports extended channel when + // communicating with upstream peer. Receiving a standard channel success + // indicates either misbehavior or a protocol violation by the upstream. + // + // In such cases, the event is treated as malicious, and a fallback + // (`UpstreamShutdownFallback`) is immediately triggered to protect the system. + async fn handle_open_standard_mining_channel_success( + &mut self, + _server_id: Option, + msg: OpenStandardMiningChannelSuccess<'_>, + ) -> Result<(), Self::Error> { + info!("Received: {}", msg); + info!( + "âš ï¸ JDC can only open extended channels with the upstream server, preparing fallback." + ); + _ = self + .channel_manager_channel + .status_sender + .send(Status { + state: State::UpstreamShutdownFallback(JDCError::Shutdown), + }) + .await; + Ok(()) + } + + // Handles `OpenExtendedMiningChannelSuccess` messages from upstream. + // + // On success, this establishes a client-side extended channel: + // - If initialization fails at any step, the upstream state is reverted from `Pending` to + // `NoChannel`. + // - If initialization succeeds, we configure the extranonce factory, create a new + // `ExtendedChannel` and `JobFactory`, and update the upstream state from `Pending` to + // `Connected`. + // + // Once the upstream state transitions to `Connected`, all pending downstream requests are + // processed, and downstream channels are opened accordingly. + async fn handle_open_extended_mining_channel_success( + &mut self, + _server_id: Option, + msg: OpenExtendedMiningChannelSuccess<'_>, + ) -> Result<(), Self::Error> { + info!("Received: {}", msg); + + let coinbase_outputs = self + .channel_manager_data + .super_safe_lock(|data| data.coinbase_outputs.clone()); + + let outputs = deserialize_outputs(coinbase_outputs) + .map_err(|_| JDCError::DeclaredJobHasBadCoinbaseOutputs)?; + + let (channel_state, template, custom_job, close_channel) = + self.channel_manager_data.super_safe_lock(|data| { + let Some(pending_request) = data.pending_downstream_requests.front() else { + self.upstream_state.set(UpstreamState::NoChannel); + let close_channel = + create_close_channel_msg(msg.channel_id, "downstream not available"); + return (self.upstream_state.get(), None, None, Some(close_channel)); + }; + + let hashrate = match pending_request { + PendingChannelRequest::ExtendedChannel(m) => m.nominal_hash_rate, + PendingChannelRequest::StandardChannel(m) => m.nominal_hash_rate, + }; + + let prefix_len = msg.extranonce_prefix.len(); + + let total_len = prefix_len + msg.extranonce_size as usize; + let range_0 = 0..prefix_len; + let range_1 = prefix_len..prefix_len + JDC_SEARCH_SPACE_BYTES; + let range_2 = prefix_len + JDC_SEARCH_SPACE_BYTES..total_len; + + debug!( + prefix_len, + extranonce_size = msg.extranonce_size, + total_len, + "Calculated extranonce ranges" + ); + + let extranonces = match ExtendedExtranonce::from_upstream_extranonce( + msg.extranonce_prefix.clone().into(), + range_0, + range_1, + range_2, + ) { + Ok(e) => e, + Err(e) => { + warn!("Failed to build extranonce factory: {e:?}"); + self.upstream_state.set(UpstreamState::NoChannel); + let close_channel = + create_close_channel_msg(msg.channel_id, "downstream not available"); + return (self.upstream_state.get(), None, None, Some(close_channel)); + } + }; + + let job_factory = JobFactory::new( + true, + data.pool_tag_string.clone(), + Some(self.miner_tag_string.clone()), + ); + + let mut extended_channel = ExtendedChannel::new( + msg.channel_id, + self.user_identity.clone(), + msg.extranonce_prefix.to_vec(), + msg.target.into(), + hashrate, + true, + msg.extranonce_size, + ); + + if let Some(ref mut prevhash) = data.last_new_prev_hash { + _ = extended_channel.on_chain_tip_update(prevhash.clone().into()); + debug!("Applied last_new_prev_hash to new extended channel"); + } + + let set_custom_job = if get_jd_mode() == JdMode::CoinbaseOnly { + if let (Some(job_factory), Some(token), Some(template), Some(prevhash)) = ( + data.job_factory.as_mut(), + data.allocate_tokens.clone(), + data.last_future_template.clone(), + data.last_new_prev_hash.clone(), + ) { + let request_id = data.request_id_factory.fetch_add(1, Ordering::Relaxed); + + if let Ok(custom_job) = job_factory.new_custom_job( + extended_channel.get_channel_id(), + request_id, + token.clone().mining_job_token, + prevhash.clone().into(), + template.clone(), + outputs.clone(), + ) { + let last_declare = DeclaredJob { + declare_mining_job: None, + template: template.into_static(), + prev_hash: Some(prevhash.into_static()), + set_custom_mining_job: Some(custom_job.clone().into_static()), + coinbase_output: data.coinbase_outputs.clone(), + tx_list: vec![], + }; + + data.last_declare_job_store.insert(request_id, last_declare); + Some(custom_job) + } else { + None + } + } else { + None + } + } else { + None + }; + + data.extranonce_prefix_factory_extended = extranonces.clone(); + data.extranonce_prefix_factory_standard = extranonces; + data.upstream_channel = Some(extended_channel); + data.job_factory = Some(job_factory); + self.upstream_state.set(UpstreamState::Connected); + + info!("Extended mining channel successfully initialized"); + ( + self.upstream_state.get(), + data.last_future_template.clone(), + set_custom_job, + None, + ) + }); + + if channel_state == UpstreamState::Connected { + if get_jd_mode() == JdMode::FullTemplate { + if let Some(template) = template { + let tx_data_request = AnyMessage::TemplateDistribution( + TemplateDistribution::RequestTransactionData(RequestTransactionData { + template_id: template.template_id, + }), + ); + let frame: StdFrame = tx_data_request.try_into()?; + self.channel_manager_channel + .tp_sender + .send(frame) + .await + .map_err(|_e| JDCError::ChannelErrorSender)?; + } + } + + if get_jd_mode() == JdMode::CoinbaseOnly { + if let Some(custom_job) = custom_job { + let set_custom_job = AnyMessage::Mining(Mining::SetCustomMiningJob(custom_job)); + let frame: StdFrame = set_custom_job.try_into()?; + self.channel_manager_channel + .jd_sender + .send(frame) + .await + .map_err(|_e| JDCError::ChannelErrorSender)?; + _ = self.allocate_tokens(1).await; + } + } + + let pending_downstreams = self + .channel_manager_data + .super_safe_lock(|data| std::mem::take(&mut data.pending_downstream_requests)); + + for pending_downstream in pending_downstreams { + let message_type = pending_downstream.message_type(); + self.send_open_channel_request_to_mining_handler( + pending_downstream.into(), + message_type, + ) + .await?; + } + } + + // In case of failure, close the channel with upstream. + if let Some(close_channel) = close_channel { + let close_channel = AnyMessage::Mining(Mining::CloseChannel(close_channel)); + let frame: StdFrame = close_channel.try_into()?; + self.channel_manager_channel + .upstream_sender + .send(frame) + .await + .map_err(|_e| JDCError::ChannelErrorSender)?; + _ = self.allocate_tokens(1).await; + } + + Ok(()) + } + + // Handles `OpenMiningChannelError` messages received from upstream. + // + // Receiving this message is treated as malicious behavior, since JDC only supports + // extended channels. When encountered, we immediately trigger the fallback mechanism + // by transitioning the upstream state into a shutdown-fallback mode. + async fn handle_open_mining_channel_error( + &mut self, + _server_id: Option, + msg: OpenMiningChannelError<'_>, + ) -> Result<(), Self::Error> { + warn!("Received: {}", msg); + warn!("âš ï¸ Cannot open extended channel with the upstream server, preparing fallback."); + + _ = self + .channel_manager_channel + .status_sender + .send(Status { + state: State::UpstreamShutdownFallback(JDCError::Shutdown), + }) + .await; + Ok(()) + } + + // Handles `UpdateChannelError` messages from upstream. + async fn handle_update_channel_error( + &mut self, + _server_id: Option, + msg: UpdateChannelError<'_>, + ) -> Result<(), Self::Error> { + warn!("Received: {}", msg); + Ok(()) + } + + // Handles `CloseChannel` messages from upstream. + // + // Upon receiving this message, the upstream channel is immediately closed and + // the system transitions into the upstream shutdown fallback state. + async fn handle_close_channel( + &mut self, + _server_id: Option, + msg: CloseChannel<'_>, + ) -> Result<(), Self::Error> { + info!("Received: {}", msg); + + self.channel_manager_data.super_safe_lock(|data| { + data.upstream_channel = None; + }); + _ = self + .channel_manager_channel + .status_sender + .send(Status { + state: State::UpstreamShutdownFallback(JDCError::Shutdown), + }) + .await; + Ok(()) + } + + // Handles `SetExtranoncePrefix` messages from upstream. + // + // When received, this updates the current extranonce prefix and rebuilds both the + // standard and extended extranonce factories. Each active downstream channel is then + // assigned a new extranonce prefix, and a corresponding `SetExtranoncePrefix` message + // is sent downstream to synchronize state. + async fn handle_set_extranonce_prefix( + &mut self, + _server_id: Option, + msg: SetExtranoncePrefix<'_>, + ) -> Result<(), Self::Error> { + info!("Received: {}", msg); + let messages_results = self + .channel_manager_data + .super_safe_lock(|channel_manager_data| { + let mut messages_results: Vec> = vec![]; + if let Some(upstream_channel) = channel_manager_data.upstream_channel.as_mut() { + if let Err(_e) = + upstream_channel.set_extranonce_prefix(msg.extranonce_prefix.to_vec()) + { + // Correct these errors, we need Extended Channel Error but on client side. + return Err(JDCError::RolesSv2Logic( + roles_logic_sv2::Error::BadPayloadSize), + ); + } + + let prefix_len = msg.extranonce_prefix.len(); + let extranonce_size = MAX_EXTRANONCE_LEN - prefix_len; + + let total_len = prefix_len + extranonce_size; + let range_0 = 0..prefix_len; + let range_1 = prefix_len..prefix_len + JDC_SEARCH_SPACE_BYTES; + let range_2 = prefix_len + JDC_SEARCH_SPACE_BYTES..total_len; + + debug!( + prefix_len, + extranonce_size, + total_len, + "Calculated extranonce ranges" + ); + let extranonces = match ExtendedExtranonce::from_upstream_extranonce( + msg.extranonce_prefix.clone().into(), + range_0, + range_1, + range_2, + ) { + Ok(e) => e, + Err(e) => { + warn!("Failed to build extranonce factory: {e:?}"); + return Err(JDCError::RolesSv2Logic( + roles_logic_sv2::Error::ExtranoncePrefixFactoryError(e), + )); + } + }; + + channel_manager_data.extranonce_prefix_factory_extended = extranonces.clone(); + channel_manager_data.extranonce_prefix_factory_standard = extranonces; + + for (downstream_id, downstream) in channel_manager_data.downstream.iter_mut() { + downstream.downstream_data.super_safe_lock(|data| { + for (channel_id, standard_channel) in data.standard_channels.iter_mut() + { + match channel_manager_data + .extranonce_prefix_factory_standard + .next_prefix_standard() + { + Ok(prefix) => match standard_channel.set_extranonce_prefix(prefix.clone().to_vec()) { + Ok(_) => { + messages_results.push(Ok(( + *downstream_id, + Mining::SetExtranoncePrefix(SetExtranoncePrefix { + channel_id: *channel_id, + extranonce_prefix: prefix.into(), + }), + ) + .into())); + } + Err(e) => { + messages_results.push(Err(JDCError::RolesSv2Logic( + roles_logic_sv2::Error::FailedToUpdateStandardChannel(e), + ))); + } + }, + Err(e) => { + messages_results.push(Err(JDCError::RolesSv2Logic( + roles_logic_sv2::Error::ExtranoncePrefixFactoryError(e), + ))); + } + } + } + for (channel_id, extended_channel) in data.extended_channels.iter_mut() + { + match channel_manager_data + .extranonce_prefix_factory_extended + .next_prefix_extended(extended_channel.get_rollable_extranonce_size() as usize) + { + Ok(prefix) => match extended_channel.set_extranonce_prefix(prefix.clone().to_vec()) { + Ok(_) => { + messages_results.push(Ok(( + *downstream_id, + Mining::SetExtranoncePrefix(SetExtranoncePrefix { + channel_id: *channel_id, + extranonce_prefix: prefix.into(), + }), + ) + .into())); + } + Err(e) => { + messages_results.push(Err(JDCError::RolesSv2Logic( + roles_logic_sv2::Error::FailedToUpdateExtendedChannel(e), + ))); + } + }, + Err(e) => { + messages_results.push(Err(JDCError::RolesSv2Logic( + roles_logic_sv2::Error::ExtranoncePrefixFactoryError(e), + ))); + } + } + } + }); + } + } + Ok(messages_results) + })?; + + for message in messages_results.into_iter().flatten() { + message.forward(&self.channel_manager_channel).await; + } + Ok(()) + } + + // Handles `SubmitSharesSuccess` messages from upstream. + async fn handle_submit_shares_success( + &mut self, + _server_id: Option, + msg: SubmitSharesSuccess, + ) -> Result<(), Self::Error> { + info!("Received: {} ✅", msg); + Ok(()) + } + + // Handles `SubmitSharesError` messages from upstream. + async fn handle_submit_shares_error( + &mut self, + _server_id: Option, + msg: SubmitSharesError<'_>, + ) -> Result<(), Self::Error> { + warn!("Received: {} âŒ", msg); + Ok(()) + } + + // Handles `NewMiningJob` messages from upstream. JDC ignores it. + async fn handle_new_mining_job( + &mut self, + _server_id: Option, + msg: NewMiningJob<'_>, + ) -> Result<(), Self::Error> { + warn!("Received: {}", msg); + warn!("âš ï¸ JDC does not expect jobs from the upstream server — ignoring."); + Ok(()) + } + + // Handles `NewExtendedMiningJob` messages from upstream. JDC ignores it. + async fn handle_new_extended_mining_job( + &mut self, + _server_id: Option, + msg: NewExtendedMiningJob<'_>, + ) -> Result<(), Self::Error> { + warn!("Received: {}", msg); + warn!("âš ï¸ JDC does not expect jobs from the upstream server — ignoring."); + Ok(()) + } + + // Handles `SetNewPrevHash` messages from upstream. JDC ignores it. + async fn handle_set_new_prev_hash( + &mut self, + _server_id: Option, + msg: SetNewPrevHash<'_>, + ) -> Result<(), Self::Error> { + warn!("Received: {}", msg); + warn!("âš ï¸ JDC does not expect prevhash updates from the upstream server — ignoring."); + Ok(()) + } + + // Handles `SetCustomMiningJobSuccess` messages from upstream. + // + // On success: + // - Updates the `job_id_to_template_id` mapping. + // - Updates the channel state accordingly. + // - Removes the associated `last_declare_job`, completing its lifecycle. + async fn handle_set_custom_mining_job_success( + &mut self, + _server_id: Option, + msg: SetCustomMiningJobSuccess, + ) -> Result<(), Self::Error> { + info!("Received: {} ✅", msg); + self.channel_manager_data.super_safe_lock(|data| { + if let Some(last_declare_job) = data.last_declare_job_store.remove(&msg.request_id) { + let template_id = last_declare_job.template.template_id; + data.last_declare_job_store + .retain(|_, job| job.template.template_id != template_id); + + data.template_id_to_upstream_job_id + .insert(last_declare_job.template.template_id, msg.job_id as u64); + debug!(job_id = msg.job_id, "Mapped custom job into template store"); + if let (Some(upstream_channel), Some(set_custom_job)) = ( + data.upstream_channel.as_mut(), + last_declare_job.set_custom_mining_job, + ) { + if let Err(e) = + upstream_channel.on_set_custom_mining_job_success(set_custom_job, msg) + { + error!("Custom mining job success validation failed: {e:#?}"); + } + } + } else { + warn!( + request_id = msg.request_id, + "No matching declare job found for custom job success" + ); + } + }); + Ok(()) + } + + // Handles a `SetCustomMiningJobError` from upstream. + // + // Receiving this is treated as malicious behavior, so we immediately + // trigger the fallback mechanism. + async fn handle_set_custom_mining_job_error( + &mut self, + _server_id: Option, + msg: SetCustomMiningJobError<'_>, + ) -> Result<(), Self::Error> { + warn!("âš ï¸ Received: {} âŒ", msg); + warn!("âš ï¸ Starting fallback mechanism."); + _ = self + .channel_manager_channel + .status_sender + .send(Status { + state: State::UpstreamShutdownFallback(JDCError::Shutdown), + }) + .await; + Ok(()) + } + + // Handles a `SetTarget` message from upstream. + // + // Updates the corresponding upstream channel's target state. + async fn handle_set_target( + &mut self, + _server_id: Option, + msg: SetTarget<'_>, + ) -> Result<(), Self::Error> { + info!("Received: {}", msg); + self.channel_manager_data.super_safe_lock(|data| { + if let Some(ref mut upstream) = data.upstream_channel { + upstream.set_target(msg.maximum_target.clone().into()); + } + }); + Ok(()) + } + + // Handles `SetGroupChannel` messages from upstream. JDC ignores it. + async fn handle_set_group_channel( + &mut self, + _server_id: Option, + msg: SetGroupChannel<'_>, + ) -> Result<(), Self::Error> { + warn!("Received: {}", msg); + warn!("âš ï¸ JDC does not expect group channel updates from the upstream server — ignoring."); + Ok(()) + } +} diff --git a/roles/jd-client/src/lib/config.rs b/roles/jd-client/src/lib/config.rs index 22045bb9cb..b12b362d5e 100644 --- a/roles/jd-client/src/lib/config.rs +++ b/roles/jd-client/src/lib/config.rs @@ -1,27 +1,12 @@ -//! ## JDC Configuration Module -//! -//! The main configuration struct is [`JobDeclaratorClientConfig`], which is typically -//! loaded from a configuration file (e.g., TOML). Helper structs like [`PoolConfig`], -//! [`TPConfig`], [`ProtocolConfig`], and [`Upstream`] are used during the construction -//! of the main configuration. - -#![allow(dead_code)] -use config_helpers::CoinbaseOutput; +use config_helpers_sv2::CoinbaseRewardScript; use key_utils::{Secp256k1PublicKey, Secp256k1SecretKey}; use serde::Deserialize; -use std::{net::SocketAddr, time::Duration}; -use stratum_common::bitcoin::{Amount, TxOut}; - -/// Represents the configuration of a Job Declarator Client (JDC). -/// -/// This struct holds all the necessary configuration parameters for a JDC instance. -/// JDC can operate in two modes: -/// -/// 1. Downstream: Connects to a mining pool (specifically Pool and JDS) and a Template Provider -/// (TP) to receive job templates. The pool and jds connection details are specified in the -/// `upstreams` field, and the TP connection details are in `tp_address`. -/// 2. Upstream: Listens for incoming connections from other downstreams on the address specified in -/// `listening_address`. +use std::{ + net::SocketAddr, + path::{Path, PathBuf}, + str::FromStr, +}; +use stratum_common::roles_logic_sv2::bitcoin::{Amount, TxOut}; #[derive(Debug, Deserialize, Clone)] pub struct JobDeclaratorClientConfig { @@ -32,8 +17,6 @@ pub struct JobDeclaratorClientConfig { max_supported_version: u16, // The minimum supported SV2 protocol version. min_supported_version: u16, - // Needs more discussion.. - withhold: bool, // The public key used by this JDC for noise encryption. authority_public_key: Secp256k1PublicKey, /// The secret key used by this JDC for noise encryption. @@ -47,43 +30,56 @@ pub struct JobDeclaratorClientConfig { /// A list of upstream Job Declarator Servers (JDS) that this JDC can connect to. /// JDC can fallover between these upstreams. upstreams: Vec, - /// The timeout duration for network operations. - #[serde(deserialize_with = "config_helpers::duration_from_toml")] - timeout: Duration, - /// A list of coinbase outputs to be included in the block templates. /// This is only used during solo-mining. - coinbase_outputs: Vec, + pub coinbase_reward_script: CoinbaseRewardScript, /// A signature string identifying this JDC instance. jdc_signature: String, + /// The path to the log file where JDC will write logs. + log_file: Option, + /// User Identity + user_identity: String, + /// Shares per minute + shares_per_minute: f64, + /// share batch size + share_batch_size: u64, + /// JDC mode: FullTemplate or CoinbaseOnly + #[serde(deserialize_with = "deserialize_jdc_mode", default)] + pub mode: ConfigJDCMode, } impl JobDeclaratorClientConfig { - /// Creates a new instance of [`JobDeclaratorClientConfig`]. #[allow(clippy::too_many_arguments)] pub fn new( listening_address: SocketAddr, protocol_config: ProtocolConfig, - withhold: bool, + user_identity: String, + shares_per_minute: f64, + share_batch_size: u64, pool_config: PoolConfig, tp_config: TPConfig, upstreams: Vec, - timeout: Duration, jdc_signature: String, + jdc_mode: Option, ) -> Self { Self { listening_address, max_supported_version: protocol_config.max_supported_version, min_supported_version: protocol_config.min_supported_version, - withhold, authority_public_key: pool_config.authority_public_key, authority_secret_key: pool_config.authority_secret_key, cert_validity_sec: tp_config.cert_validity_sec, tp_address: tp_config.tp_address, tp_authority_public_key: tp_config.tp_authority_public_key, upstreams, - timeout, - coinbase_outputs: protocol_config.coinbase_outputs, + coinbase_reward_script: protocol_config.coinbase_reward_script, jdc_signature, + log_file: None, + user_identity, + shares_per_minute, + share_batch_size, + mode: jdc_mode + .map(|s| s.parse::().unwrap_or_default()) + .unwrap_or_default(), } } @@ -99,16 +95,6 @@ impl JobDeclaratorClientConfig { &self.upstreams } - /// Returns the timeout duration. - pub fn timeout(&self) -> Duration { - self.timeout - } - - /// Returns the withhold flag. - pub fn withhold(&self) -> bool { - self.withhold - } - /// Returns the authority public key. pub fn authority_public_key(&self) -> &Secp256k1PublicKey { &self.authority_public_key @@ -149,20 +135,59 @@ impl JobDeclaratorClientConfig { &self.jdc_signature } - pub fn get_txout(&self) -> Result, config_helpers::CoinbaseOutputError> { - let mut result = Vec::new(); - for coinbase_output_pool in &self.coinbase_outputs { - let output_script = coinbase_output_pool.clone().try_into()?; - result.push(TxOut { - value: Amount::from_sat(0), - script_pubkey: output_script, - }); + pub fn get_txout(&self) -> TxOut { + TxOut { + value: Amount::from_sat(0), + script_pubkey: self.coinbase_reward_script.script_pubkey().to_owned(), } - match result.is_empty() { - true => Err(config_helpers::CoinbaseOutputError::EmptyCoinbaseOutputs), - _ => Ok(result), + } + + pub fn log_file(&self) -> Option<&Path> { + self.log_file.as_deref() + } + pub fn set_log_file(&mut self, log_file: Option) { + if let Some(log_file) = log_file { + self.log_file = Some(log_file); } } + pub fn user_identity(&self) -> &str { + &self.user_identity + } + + pub fn shares_per_minute(&self) -> f64 { + self.shares_per_minute + } + + pub fn share_batch_size(&self) -> u64 { + self.share_batch_size + } +} + +#[derive(Debug, Deserialize, Clone, Default)] +#[serde(rename_all = "UPPERCASE")] +pub enum ConfigJDCMode { + #[default] + FullTemplate, + CoinbaseOnly, +} + +impl std::str::FromStr for ConfigJDCMode { + type Err = (); + + fn from_str(s: &str) -> Result { + match s.to_uppercase().as_str() { + "COINBASEONLY" => Ok(ConfigJDCMode::CoinbaseOnly), + _ => Ok(ConfigJDCMode::FullTemplate), + } + } +} + +fn deserialize_jdc_mode<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let s: String = String::deserialize(deserializer)?; + Ok(ConfigJDCMode::from_str(&s).unwrap_or_default()) } /// Represents pool specific encryption keys. @@ -215,8 +240,8 @@ pub struct ProtocolConfig { max_supported_version: u16, // The minimum supported SV2 protocol version. min_supported_version: u16, - // A list of coinbase outputs to be included in block templates. - coinbase_outputs: Vec, + // A coinbase output to be included in block templates. + coinbase_reward_script: CoinbaseRewardScript, } impl ProtocolConfig { @@ -224,12 +249,12 @@ impl ProtocolConfig { pub fn new( max_supported_version: u16, min_supported_version: u16, - coinbase_outputs: Vec, + coinbase_reward_script: CoinbaseRewardScript, ) -> Self { Self { max_supported_version, min_supported_version, - coinbase_outputs, + coinbase_reward_script, } } } @@ -241,8 +266,10 @@ pub struct Upstream { pub authority_pubkey: Secp256k1PublicKey, // The address of the upstream pool's main server. pub pool_address: String, + pub pool_port: u16, // The network address of the JDS. - pub jd_address: String, + pub jds_address: String, + pub jds_port: u16, } impl Upstream { @@ -250,12 +277,16 @@ impl Upstream { pub fn new( authority_pubkey: Secp256k1PublicKey, pool_address: String, - jd_address: String, + pool_port: u16, + jds_address: String, + jds_port: u16, ) -> Self { Self { authority_pubkey, pool_address, - jd_address, + pool_port, + jds_address, + jds_port, } } } diff --git a/roles/jd-client/src/lib/downstream.rs b/roles/jd-client/src/lib/downstream.rs deleted file mode 100644 index def7b61e36..0000000000 --- a/roles/jd-client/src/lib/downstream.rs +++ /dev/null @@ -1,1123 +0,0 @@ -//! ## Downstream Module -//! -//! It contains the logic and structures for the Job Declarator Client (JDC) to spawn a server and -//! provide ports for downstream proxies or mining nodes to connect to. -//! -//! It handles the lifecycle of downstream connections, including establishing secure -//! communication, processing incoming SV2 messages from the downstream, interpreting -//! these messages based on the SV2 Mining Protocol, and forwarding relevant messages -//! to the appropriate upstream components (either the main Pool or the Job Declarator Server). -//! -//! All the traits required for downstream connection handling, message parsing, -//! and message interpretation are implemented within this module for the `DownstreamMiningNode` -//! struct. -//! -//! Implemented Traits: -//! - [`IsDownstream`] — Provides access to common downstream data. -//! - [`ParseMiningMessagesFromDownstream`] — Handles all messages specific to -//! the SV2 Mining Protocol received from the downstream. -//! - [`ParseCommonMessagesFromDownstream`] — Handles common SV2 messages like `SetupConnection` -//! received during the initial handshake. -//! - [`IsMiningDownstream`] — (possibly redundant.) - -use super::{config::JobDeclaratorClientConfig, template_receiver::TemplateRx, PoolChangerTrigger}; - -use super::{ - job_declarator::JobDeclarator, - status::{self, State}, - upstream_sv2::Upstream as UpstreamMiningNode, -}; -use async_channel::{bounded, Receiver, SendError, Sender}; -use roles_logic_sv2::{ - channel_logic::channel_factory::{OnNewShare, PoolChannelFactory, Share}, - common_messages_sv2::{SetupConnection, SetupConnectionSuccess}, - common_properties::{CommonDownstreamData, IsDownstream, IsMiningDownstream}, - errors::Error, - handlers::{ - common::{ParseCommonMessagesFromDownstream, SendTo as SendToCommon}, - mining::{ParseMiningMessagesFromDownstream, SendTo, SupportedChannelTypes}, - }, - job_creator::JobsCreators, - mining_sv2::*, - parsers::{AnyMessage, Mining, MiningDeviceMessages}, - template_distribution_sv2::{NewTemplate, SubmitSolution}, - utils::Mutex, -}; -use tokio::sync::Notify; -use tracing::{debug, error, info, warn}; - -use codec_sv2::{HandshakeRole, Responder, StandardEitherFrame, StandardSv2Frame}; -use key_utils::{Secp256k1PublicKey, Secp256k1SecretKey}; - -use stratum_common::bitcoin::{consensus::Decodable, TxOut}; - -pub type Message = MiningDeviceMessages<'static>; -pub type StdFrame = StandardSv2Frame; -pub type EitherFrame = StandardEitherFrame; - -/// Represents a connection to a single downstream mining node or proxy. -/// -/// **NOTE:** The current implementation of the JDC's downstream handling is -/// limited to a one-to-one connection with a single downstream entity. This is -/// noted in the code as a conceptual mistake that needs refactoring to support -/// multiple downstream connections. -/// -/// A downstream can be either a direct mining device or another proxy. It is -/// assumed that a single downstream node connects to only one upstream pool -/// at a time via this JDC. -#[derive(Debug)] -pub struct DownstreamMiningNode { - // Receiver channel for incoming messages from the downstream node. - receiver: Receiver, - // Sender channel for sending messages to the downstream node. - sender: Sender, - /// The current status of the downstream connection, tracking its lifecycle stage. - pub status: DownstreamMiningNodeStatus, - // This field might be used in future for job tracking. or not sure. - #[allow(dead_code)] - pub prev_job_id: Option, - // Sender channel for forwarding validated miner solutions to the template receiver - // or other components that handle block solution submission. - solution_sender: Sender>, - // Needs more discussion.. - withhold: bool, - task_collector: Arc>>, - // Sender for communicating status updates (e.g., disconnection) back to the main Status loop. - tx_status: status::Sender, - // The miner's configured coinbase output(s). Used in solo mining mode. - miner_coinbase_output: Vec, - // The template ID of the last job sent to this downstream. Used to correlate - // submitted shares with the correct job ID when sending upstream. - last_template_id: u64, - /// `JobDeclarator` instance. Present when connected to a pool and using the Job Declaration - /// Protocol. Absent in solo mining mode. - pub jd: Option>>, - // The JDC's signature string, used in solo mining channel setup. - jdc_signature: String, -} - -/// Represents the different lifecycle stages of a connection with a downstream mining node or -/// proxy. -/// -/// This enum tracks the state transitions for both regular downstream connections -/// (paired with an upstream pool) and solo mining downstream connections. -#[allow(clippy::large_enum_variant)] -#[derive(Debug)] -pub enum DownstreamMiningNodeStatus { - /// The downstream node is in the initial handshake/initialization phase. - /// Holds an optional reference to the upstream if connecting to a pool. - Initializing(Option>>), - /// The downstream node has completed the initial setup and is paired with an upstream pool. - /// Holds common downstream data and a reference to the upstream. - Paired((CommonDownstreamData, Arc>)), - /// A mining channel (specifically an Extended channel in this implementation) - /// has been successfully opened for the downstream node. - /// Holds the `PoolChannelFactory` for this channel, common downstream data, - /// and a reference to the upstream. - ChannelOpened( - ( - PoolChannelFactory, - CommonDownstreamData, - Arc>, - ), - ), - /// The downstream node has completed initialization and is operating in solo mining mode. - /// Holds common downstream data. - SoloMinerPaired(CommonDownstreamData), - /// The solo miner has opened a mining channel. - /// Holds the `PoolChannelFactory` for this channel and common downstream data. - SoloMinerChannelOpend((PoolChannelFactory, CommonDownstreamData)), -} - -impl DownstreamMiningNodeStatus { - // Checks if the downstream connection is in a paired state (either with an upstream pool or in - // solo mining). - fn is_paired(&self) -> bool { - match self { - DownstreamMiningNodeStatus::Initializing(_) => false, - DownstreamMiningNodeStatus::Paired(_) => true, - DownstreamMiningNodeStatus::ChannelOpened(_) => true, - DownstreamMiningNodeStatus::SoloMinerPaired(_) => true, - DownstreamMiningNodeStatus::SoloMinerChannelOpend(_) => true, - } - } - - // Transitions the status from `Initializing` to either `Paired` (if an upstream exists) - // or `SoloMinerPaired` (if in solo mining mode). - fn pair(&mut self, data: CommonDownstreamData) { - match self { - DownstreamMiningNodeStatus::Initializing(Some(up)) => { - let self_ = Self::Paired((data, up.clone())); - let _ = std::mem::replace(self, self_); - } - DownstreamMiningNodeStatus::Initializing(None) => { - let self_ = Self::SoloMinerPaired(data); - let _ = std::mem::replace(self, self_); - } - _ => panic!("Try to pair an already paired downstream"), - } - } - - // Sets the `PoolChannelFactory` for the downstream connection and transitions the status - // to either `ChannelOpened` (if paired with upstream) or `SoloMinerChannelOpend` - // (if in solo mining mode). - fn set_channel(&mut self, channel: PoolChannelFactory) -> bool { - match self { - DownstreamMiningNodeStatus::Initializing(_) => false, - DownstreamMiningNodeStatus::Paired((data, up)) => { - let self_ = Self::ChannelOpened((channel, *data, up.clone())); - let _ = std::mem::replace(self, self_); - true - } - DownstreamMiningNodeStatus::ChannelOpened(_) => false, - DownstreamMiningNodeStatus::SoloMinerPaired(data) => { - let self_ = Self::SoloMinerChannelOpend((channel, *data)); - let _ = std::mem::replace(self, self_); - true - } - DownstreamMiningNodeStatus::SoloMinerChannelOpend(_) => false, - } - } - - /// Returns a mutable reference to the `PoolChannelFactory` if the downstream is in a - /// channel-opened state (either pooled or solo mining). - pub fn get_channel(&mut self) -> &mut PoolChannelFactory { - match self { - DownstreamMiningNodeStatus::Initializing(_) => panic!(), - DownstreamMiningNodeStatus::Paired(_) => panic!(), - DownstreamMiningNodeStatus::ChannelOpened((channel, _, _)) => channel, - DownstreamMiningNodeStatus::SoloMinerPaired(_) => panic!(), - DownstreamMiningNodeStatus::SoloMinerChannelOpend((channel, _)) => channel, - } - } - - // Checks if the downstream connection has an opened mining channel. - fn have_channel(&self) -> bool { - match self { - DownstreamMiningNodeStatus::Initializing(_) => false, - DownstreamMiningNodeStatus::Paired(_) => false, - DownstreamMiningNodeStatus::ChannelOpened(_) => true, - DownstreamMiningNodeStatus::SoloMinerPaired(_) => false, - DownstreamMiningNodeStatus::SoloMinerChannelOpend(_) => true, - } - } - - // Returns an optional Arc-wrapped Mutex reference to the upstream mining node - // if the downstream is connected to one. - fn get_upstream(&mut self) -> Option>> { - match self { - DownstreamMiningNodeStatus::Initializing(Some(up)) => Some(up.clone()), - DownstreamMiningNodeStatus::Paired((_, up)) => Some(up.clone()), - DownstreamMiningNodeStatus::ChannelOpened((_, _, up)) => Some(up.clone()), - DownstreamMiningNodeStatus::Initializing(None) => None, - DownstreamMiningNodeStatus::SoloMinerPaired(_) => None, - DownstreamMiningNodeStatus::SoloMinerChannelOpend(_) => None, - } - } - - // Checks if the downstream connection is operating in solo mining mode. - fn is_solo_miner(&mut self) -> bool { - matches!( - self, - DownstreamMiningNodeStatus::Initializing(None) - | DownstreamMiningNodeStatus::SoloMinerPaired(_) - | DownstreamMiningNodeStatus::SoloMinerChannelOpend(_) - ) - } -} - -use core::convert::TryInto; -use std::{net::IpAddr, str::FromStr, sync::Arc}; - -impl DownstreamMiningNode { - /// Constructs a new `DownstreamMiningNode` instance. - #[allow(clippy::too_many_arguments)] - pub fn new( - receiver: Receiver, - sender: Sender, - upstream: Option>>, - solution_sender: Sender>, - withhold: bool, - task_collector: Arc>>, - tx_status: status::Sender, - miner_coinbase_output: Vec, - jd: Option>>, - jdc_signature: String, - ) -> Self { - Self { - receiver, - sender, - status: DownstreamMiningNodeStatus::Initializing(upstream), - prev_job_id: None, - solution_sender, - withhold, - task_collector, - tx_status, - miner_coinbase_output, - // set it to an arbitrary value cause when we use it we always updated it. - // Is used before sending the share to upstream in the main loop when we have a share. - // Is upated in the message handler that si called earlier in the main loop. - last_template_id: 0, - jd, - jdc_signature, - } - } - - /// Starts the processing of messages from the downstream mining node. - /// - /// This method is called after the initial `SetupConnection` handshake is complete - /// and the downstream's status has been set to `Paired` or `SoloMinerPaired`. - /// It sends a `SetupConnectionSuccess` message back to the downstream and then - /// enters a loop to continuously receive and process incoming messages. - /// If the downstream connection closes, it reports a `DownstreamShutdown` status. - pub async fn start( - self_mutex: &Arc>, - setup_connection_success: SetupConnectionSuccess, - ) { - // Ensure the downstream is in a paired state before starting message processing. - if self_mutex - .safe_lock(|self_| self_.status.is_paired()) - .unwrap() - { - let setup_connection_success: MiningDeviceMessages = setup_connection_success.into(); - - { - DownstreamMiningNode::send( - self_mutex, - setup_connection_success.try_into().unwrap(), - ) - .await - .unwrap(); - } - let receiver = self_mutex - .safe_lock(|self_| self_.receiver.clone()) - .unwrap(); - Self::set_channel_factory(self_mutex.clone()); - - while let Ok(message) = receiver.recv().await { - let incoming: StdFrame = message.try_into().unwrap(); - Self::next(self_mutex, incoming).await; - } - let tx_status = self_mutex.safe_lock(|s| s.tx_status.clone()).unwrap(); - let err = Error::DownstreamDown; - let status = status::Status { - state: State::DownstreamShutdown(err.into()), - }; - tx_status.send(status).await.unwrap(); - } else { - panic!() - } - } - - // Sets up the `PoolChannelFactory` for this downstream connection. - // - // In pooled mining mode, it waits for the `OpenExtendedMiningChannelSuccess` - // message from the upstream, which provides the necessary information to - // take the initialized factory from the upstream instance. - // In solo mining mode, it creates a new `PoolChannelFactory` instance locally. - fn set_channel_factory(self_mutex: Arc>) { - // Check if the downstream is in solo mining mode. - if !self_mutex.safe_lock(|s| s.status.is_solo_miner()).unwrap() { - // Safe unwrap already checked if it contains an upstream with `is_solo_miner` - let upstream = self_mutex - .safe_lock(|s| s.status.get_upstream().unwrap()) - .unwrap(); - // Spawn a task to wait for and take the channel factory from the upstream. - let recv_factory = { - let self_mutex = self_mutex.clone(); - tokio::task::spawn(async move { - let factory = UpstreamMiningNode::take_channel_factory(upstream).await; - self_mutex - .safe_lock(|s| { - s.status.set_channel(factory); - }) - .unwrap(); - }) - }; - self_mutex - .safe_lock(|s| { - s.task_collector - .safe_lock(|c| c.push(recv_factory.abort_handle())) - .unwrap() - }) - .unwrap(); - } - } - - /// Parses the received message from the downstream and dispatches it to the - /// appropriate handler based on the `ParseMiningMessagesFromDownstream` trait. - /// - /// After processing, it calls `match_send_to` to handle the result from the handler. - pub async fn next(self_mutex: &Arc>, mut incoming: StdFrame) { - // Extract message type and payload. Panics on missing header or type. - let message_type = incoming.get_header().unwrap().msg_type(); - let payload = incoming.payload(); - - let next_message_to_send = ParseMiningMessagesFromDownstream::handle_message_mining( - self_mutex.clone(), - message_type, - payload, - ); - - // Process the result from the message handler - Self::match_send_to(self_mutex.clone(), next_message_to_send, Some(incoming)).await; - } - - /// Recursively handles the `SendTo` result from a message handler. - /// - /// This method determines how to proceed based on the instruction received - /// from the handler: - /// - `RelaySameMessageToRemote`: Relays the original incoming message to the upstream. - /// - `RelayNewMessage`: Sends a newly constructed message (e.g., a processed share) to the - /// upstream. Includes logic for retrieving the correct job ID for shares. - /// - `Multiple`: Processes a vector of `SendTo` instructions recursively. - /// - `Respond`: Sends a message as a response back to the downstream. - /// - `None`: Indicates no immediate action is required. - #[async_recursion::async_recursion] - async fn match_send_to( - self_mutex: Arc>, - next_message_to_send: Result, Error>, - incoming: Option, - ) { - match next_message_to_send { - // If the handler requests to relay the same message upstream. - Ok(SendTo::RelaySameMessageToRemote(upstream_mutex)) => { - let sv2_frame: codec_sv2::Sv2Frame = - incoming.unwrap().map(|payload| payload.try_into().unwrap()); - - // Send the message to the upstream. - UpstreamMiningNode::send(&upstream_mutex, sv2_frame) - .await - .unwrap(); - } - // If the handler requests to relay a new message, specifically SubmitSharesExtended. - Ok(SendTo::RelayNewMessage(Mining::SubmitSharesExtended(mut share))) => { - // This case is for pooled mining, where the JDC forwards a share. - let upstream_mutex = self_mutex - .safe_lock(|s| s.status.get_upstream().unwrap()) - .unwrap(); - - // When re receive SetupConnectionSuccess we link the last_template_id with the - // pool's job_id. The below return as soon as we have a pairable job id for the - // template_id associated with this share. - let last_template_id = self_mutex.safe_lock(|s| s.last_template_id).unwrap(); - let job_id_future = - UpstreamMiningNode::get_job_id(&upstream_mutex, last_template_id); - - // Wait for the job ID with a timeout. If it times out, don't send the share - let job_id = match timeout(Duration::from_secs(10), job_id_future).await { - Ok(job_id) => job_id, - Err(_) => { - return; - } - }; - - // Set the job ID in the share message. - share.job_id = job_id; - debug!( - "Sending valid block solution upstream, with job_id {}", - job_id - ); - let message = Mining::SubmitSharesExtended(share); - let message: AnyMessage = AnyMessage::Mining(message); - let sv2_frame: codec_sv2::Sv2Frame = - message.try_into().unwrap(); - - // Send the share message to the upstream. - UpstreamMiningNode::send(&upstream_mutex, sv2_frame) - .await - .unwrap(); - } - // If the handler requests to relay a *new* message. - Ok(SendTo::RelayNewMessage(message)) => { - let message: AnyMessage = AnyMessage::Mining(message); - let sv2_frame: codec_sv2::Sv2Frame = - message.try_into().unwrap(); - let upstream_mutex = self_mutex.safe_lock(|s| s.status.get_upstream().expect("We should return RelayNewMessage only if we are not in solo mining mode")).unwrap(); - UpstreamMiningNode::send(&upstream_mutex, sv2_frame) - .await - .unwrap(); - } - // If the handler requests to send multiple messages. - Ok(SendTo::Multiple(messages)) => { - // Iterate through the messages and handle each one recursively. - for message in messages { - // Note: We pass None for `incoming` as these are new messages, not the - // original. - Self::match_send_to(self_mutex.clone(), Ok(message), None).await; - } - } - // If the handler requests to send a response back to the downstream. - Ok(SendTo::Respond(message)) => { - let message = MiningDeviceMessages::Mining(message); - let sv2_frame: codec_sv2::Sv2Frame = - message.try_into().unwrap(); - - // Send the response message downstream. - Self::send(&self_mutex, sv2_frame).await.unwrap(); - } - Ok(SendTo::None(None)) => (), - Ok(m) => unreachable!("Unexpected message type: {:?}", m), - Err(_) => todo!(), - } - } - - /// Send a message downstream - pub async fn send( - self_mutex: &Arc>, - sv2_frame: StdFrame, - ) -> Result<(), SendError> { - let either_frame = sv2_frame.into(); - let sender = self_mutex.safe_lock(|self_| self_.sender.clone()).unwrap(); - match sender.send(either_frame).await { - Ok(_) => Ok(()), - Err(_) => { - todo!() - } - } - } - - /// Handles a `NewTemplate` message received from the template receiver. - /// - /// This method is called directly from the template receiver when a new block - /// template is available. It updates the internal `PoolChannelFactory` with - /// the new template and the pool's coinbase output (if a channel is open). - /// It then generates the appropriate mining messages (`NewExtendedMiningJob`) - /// using the channel factory and sends them downstream to the miner. - /// Finally, it updates the `last_template_id` and sets the `IS_NEW_TEMPLATE_HANDLED` - /// global flag to `true` (Release ordering) to signal that the downstream - /// has finished processing the new template. - pub async fn on_new_template( - self_mutex: &Arc>, - mut new_template: NewTemplate<'static>, - pool_output: &[u8], - ) -> Result<(), Error> { - // Check if a channel is open. If not, just set the flag and return. - if !self_mutex.safe_lock(|s| s.status.have_channel()).unwrap() { - super::IS_NEW_TEMPLATE_HANDLED.store(true, std::sync::atomic::Ordering::Release); - return Ok(()); - } - - // Decode the pool's coinbase output. - let mut pool_out = &pool_output[0..]; - let pool_output = - TxOut::consensus_decode(&mut pool_out).expect("Upstream sent an invalid coinbase"); - - // Update the channel factory with the new template and pool outputs and get messages to - // send downstream. - let to_send = self_mutex - .safe_lock(|s| { - let channel = s.status.get_channel(); - channel.update_pool_outputs(vec![pool_output]); - channel.on_new_template(&mut new_template) - }) - .unwrap()?; - - // to_send is a HashMap. Since this implementation - // currently only supports one downstream connection and one channel, we can - // take all messages from the map's values. - let to_send = to_send.into_values(); - - // Send each generated message downstream. - for message in to_send { - // If the message is NewExtendedMiningJob, update the JobDeclarator's coinbase prefix - // and suffix. - let message = if let Mining::NewExtendedMiningJob(job) = message { - if let Some(jd) = self_mutex.safe_lock(|s| s.jd.clone()).unwrap() { - jd.safe_lock(|jd| { - jd.coinbase_tx_prefix = job.coinbase_tx_prefix.clone(); - jd.coinbase_tx_suffix = job.coinbase_tx_suffix.clone(); - }) - .unwrap(); - } - Mining::NewExtendedMiningJob(job) - } else { - message - }; - let message = MiningDeviceMessages::Mining(message); - let frame: StdFrame = message.try_into().unwrap(); - Self::send(self_mutex, frame).await.unwrap(); - } - - // Set the global flag to true to indicate that the downstream - // has finished handling the NewTemplate message. - super::IS_NEW_TEMPLATE_HANDLED.store(true, std::sync::atomic::Ordering::Release); - Ok(()) - } - - /// Handles a `SetNewPrevHash` message received from the template receiver. - /// - /// This method is called directly from the template receiver when a new - /// previous block hash is available. It updates the internal `PoolChannelFactory` - /// with the new previous hash (if a channel is open), which is necessary for - /// the factory to generate correct job IDs. It then generates the appropriate - /// `SetNewPrevHash` message for the downstream miner and sends it. - pub async fn on_set_new_prev_hash( - self_mutex: &Arc>, - new_prev_hash: roles_logic_sv2::template_distribution_sv2::SetNewPrevHash<'static>, - ) -> Result<(), Error> { - // Check if a channel is open. If not, return immediately. - if !self_mutex.safe_lock(|s| s.status.have_channel()).unwrap() { - return Ok(()); - } - - // Update the channel factory with the new previous hash and get the corresponding job ID. - let job_id = self_mutex - .safe_lock(|s| { - let channel = s.status.get_channel(); - channel.on_new_prev_hash_from_tp(&new_prev_hash) - }) - .unwrap()?; - - // Get the extended channel IDs from the factory. Expect exactly one in this 1:1 setup. - let channel_ids = self_mutex - .safe_lock(|s| s.status.get_channel().get_extended_channels_ids()) - .unwrap(); - - // Determine the channel ID. Panics if the number of channels is not 1. - let channel_id = match channel_ids.len() { - 1 => channel_ids[0], - _ => unreachable!(), - }; - - // Construct the SetNewPrevHash message for the downstream miner. - let to_send = SetNewPrevHash { - channel_id, - job_id, - prev_hash: new_prev_hash.prev_hash, - min_ntime: new_prev_hash.header_timestamp, - nbits: new_prev_hash.n_bits, - }; - let message = MiningDeviceMessages::Mining(Mining::SetNewPrevHash(to_send)); - let frame = message.try_into().unwrap(); - Self::send(self_mutex, frame).await.unwrap(); - Ok(()) - } -} - -/// It impl UpstreamMining cause the proxy act as an upstream node for the DownstreamMiningNode -impl ParseMiningMessagesFromDownstream for DownstreamMiningNode { - // Returns the channel type supported between the downstream mining node and - // this JDC instance. Only `Extended` channels are supported. - fn get_channel_type(&self) -> SupportedChannelTypes { - SupportedChannelTypes::Extended - } - - // Indicates whether work selection is enabled for this connection. - // In this JDC implementation acting as an upstream for the downstream, work - // selection is handled upstream (by the pool or JDC logic), not by the downstream. - fn is_work_selection_enabled(&self) -> bool { - false - } - - // Checks if a downstream user identity is authorized to connect. - fn is_downstream_authorized( - _self_mutex: Arc>, - _user_identity: &Str0255, - ) -> Result { - Ok(true) - } - - // Handles an `OpenStandardMiningChannel` message received from the downstream. - // - // This method logs a warning and ignores the message because the JDC - // is configured to only support `Extended` mining channels with downstream nodes. - // - // Returns `Ok(SendTo::None(None))` indicating no action is taken. - fn handle_open_standard_mining_channel( - &mut self, - _: OpenStandardMiningChannel, - ) -> Result, Error> { - warn!("Ignoring OpenStandardMiningChannel"); - Ok(SendTo::None(None)) - } - - // Handles an `OpenExtendedMiningChannel` message received from the downstream. - // - // This is the expected message from a downstream miner to open a mining channel. - // - If the JDC is connected to a pool, it relays this message to the upstream pool to request - // an extended channel there. - // - If the JDC is in solo mining mode, it creates a local `PoolChannelFactory` for this channel - // and responds with `OpenExtendedMiningChannelSuccess`. - // - // Returns `Ok(SendTo::RelaySameMessageToRemote(upstream_mutex))` if in pooled mining mode. - // Returns `Ok(SendTo::Multiple(messages))` if in solo mining mode, containing - // the `OpenExtendedMiningChannelSuccess` and potentially other initial messages. - fn handle_open_extended_mining_channel( - &mut self, - m: OpenExtendedMiningChannel, - ) -> Result, Error> { - info!( - "Received OpenExtendedMiningChannel from: {} with id: {}", - std::str::from_utf8(m.user_identity.as_ref()).unwrap_or("Unknown identity"), - m.get_request_id_as_u32() - ); - debug!("OpenExtendedMiningChannel: {:?}", m); - - // Check if the downstream is in solo mining mode. - if !self.status.is_solo_miner() { - // If not solo mining, it's pooled mining. Relay the message upstream. - // Safe unwrap: is_solo_miner being false implies there's an upstream. - Ok(SendTo::RelaySameMessageToRemote( - self.status.get_upstream().unwrap(), - )) - } else { - // If in solo mining mode, create a local channel factory. - // The channel factory is created here to ensure it exists when a channel is opened. - // hardcoded value - let extranonce_len = 32; - let jdc_signature_len = self.jdc_signature.len(); - let range_0 = std::ops::Range { start: 0, end: 0 }; - - // JDC only allows for one single downstream, so we don't need any free bytes on range_1 - // we just allocate enough space for the JDC signature - let range_1 = std::ops::Range { - start: 0, - end: jdc_signature_len, - }; - let range_2 = std::ops::Range { - start: jdc_signature_len, - end: extranonce_len, - }; - let ids = Arc::new(Mutex::new(roles_logic_sv2::utils::GroupId::new())); - let coinbase_outputs = self.miner_coinbase_output.clone(); - - // Create the ExtendedExtranonce structure. - let extranonces = ExtendedExtranonce::new( - range_0, - range_1, - range_2, - Some(self.jdc_signature.as_bytes().to_vec()), - ) - .map_err(|_| { - roles_logic_sv2::Error::ExtendedExtranonceCreationFailed( - "Failed to create ExtendedExtranonce".into(), - ) - })?; - let creator = JobsCreators::new(extranonce_len as u8); - // hardcoded value - let share_per_min = 1.0; - let kind = roles_logic_sv2::channel_logic::channel_factory::ExtendedChannelKind::Pool; - - // Create the PoolChannelFactory instance for solo mining. - let channel_factory = PoolChannelFactory::new( - ids, - extranonces, - creator, - share_per_min, - kind, - coinbase_outputs, - ); - - // Set the created channel factory in the downstream's status. - self.status.set_channel(channel_factory); - - // Process the new extended channel request in the locally created factory. - let request_id = m.request_id; - let hash_rate = m.nominal_hash_rate; - let min_extranonce_size = m.min_extranonce_size; - let messages_res = self.status.get_channel().new_extended_channel( - request_id, - hash_rate, - min_extranonce_size, - ); - - // Based on the factory's response, generate messages to send back to the downstream. - match messages_res { - Ok(messages) => { - let messages = messages.into_iter().map(SendTo::Respond).collect(); - Ok(SendTo::Multiple(messages)) - } - Err(_) => Err(roles_logic_sv2::Error::ChannelIsNeitherExtendedNeitherInAPool), - } - } - } - - // Handles an `UpdateChannel` message received from the downstream. - // - // - If in pooled mining mode, it relays this message upstream to the pool. - // - If in solo mining mode, it updates the maximum target in the local `PoolChannelFactory` - // based on the nominal hash rate provided by the miner and responds with a `SetTarget` - // message to the downstream. - // - // Returns `Ok(SendTo::RelaySameMessageToRemote(upstream_mutex))` if in pooled mining mode. - // Returns `Ok(SendTo::Respond(Mining::SetTarget(set_target)))` if in solo mining mode. - fn handle_update_channel( - &mut self, - m: UpdateChannel, - ) -> Result, Error> { - info!("Received UpdateChannel message"); - - // Check if the downstream is in solo mining mode. - if !self.status.is_solo_miner() { - // If not solo mining, relay the message upstream. - // Safe unwrap: is_solo_miner being false implies there's an upstream - Ok(SendTo::RelaySameMessageToRemote( - self.status.get_upstream().unwrap(), - )) - } else { - // If in solo mining mode, calculate the target based on the miner's hash rate. - let maximum_target = - roles_logic_sv2::utils::hash_rate_to_target(m.nominal_hash_rate.into(), 10.0)?; - - // Update the target in the local channel factory. - self.status - .get_channel() - .update_target_for_channel(m.channel_id, maximum_target.clone().into()); - - // Construct the SetTarget message for the downstream. - let set_target = SetTarget { - channel_id: m.channel_id, - maximum_target, - }; - Ok(SendTo::Respond(Mining::SetTarget(set_target))) - } - } - - // Handles a `SubmitSharesStandard` message received from the downstream. - // - // Returns `Ok(SendTo::None(None))` indicating no action is taken. - fn handle_submit_shares_standard( - &mut self, - _: SubmitSharesStandard, - ) -> Result, Error> { - warn!("Ignoring SubmitSharesStandard"); - Ok(SendTo::None(None)) - } - - /// Handles a `SubmitSharesExtended` message received from the downstream. - /// - /// This method processes a submitted share from the miner using the internal - /// `PoolChannelFactory` to validate it against the current job's target. - /// - If the share meets the downstream target, it is processed further. - /// - If the share meets the Bitcoin target (in solo mining), it's a potential block. - /// - If the share meets the upstream pool's target (in pooled mining and withholding is off), - /// it is relayed upstream. - /// - If the share is invalid, an error response is sent downstream. - fn handle_submit_shares_extended( - &mut self, - m: SubmitSharesExtended, - ) -> Result, Error> { - info!("Received SubmitSharesExtended message"); - debug!("SubmitSharesExtended {:?}", m); - - // Process the submitted share using the channel factory. - match self - .status - .get_channel() - .on_submit_shares_extended(m.clone()) - .unwrap() - { - // If the share does not meet the downstream target. - OnNewShare::SendErrorDownstream(s) => { - error!("Share does not meet the downstream target"); - Ok(SendTo::Respond(Mining::SubmitSharesError(s))) - } - // If the share is valid and should be sent upstream (pooled mining). - OnNewShare::SendSubmitShareUpstream((m, Some(template_id))) => { - if !self.status.is_solo_miner() { - match m { - Share::Extended(share) => { - // Update the last_template_id for correlating with upstream job ID. - let for_upstream = Mining::SubmitSharesExtended(share); - self.last_template_id = template_id; - Ok(SendTo::RelayNewMessage(for_upstream)) - } - // We are in an extended channel shares are extended - Share::Standard(_) => unreachable!(), - } - } else { - // This case should not happen in solo mining, as shares meeting Bitcoin target - // are handled separately. - Ok(SendTo::None(None)) - } - } - OnNewShare::RelaySubmitShareUpstream => unreachable!(), - // If the share meets the Bitcoin target (potential block found). - OnNewShare::ShareMeetBitcoinTarget(( - share, - Some(template_id), - coinbase, - extranonce, - )) => { - match share { - Share::Extended(share) => { - // Get the solution sender channel - let solution_sender = self.solution_sender.clone(); - - // Construct the SubmitSolution message for the template receiver. - let solution = SubmitSolution { - template_id, - version: share.version, - header_timestamp: share.ntime, - header_nonce: share.nonce, - coinbase_tx: coinbase.try_into()?, - }; - - // Send the solution to the solution sender. Blocking send is used, - // expecting the channel to not be full. Panics on send failure. - solution_sender.send_blocking(solution).unwrap(); - - // If not in solo mining mode, send the solution to the Job Declarator - // to potentially push it as a block candidate to the JDS. - if !self.status.is_solo_miner() { - { - let jd = self.jd.clone(); - let mut share = share.clone(); - // Update the share's extranonce with the full calculated - // extranonce. - share.extranonce = extranonce.try_into().unwrap(); - // Spawn a task to send the solution to the Job Declarator. - tokio::task::spawn(async move { - JobDeclarator::on_solution(&jd.unwrap(), share).await - }); - } - } - - // If not withholding and not in solo mining mode, relay the share upstream. - // This is likely for block propagation to the pool. - // Safe unwrap: is_solo_miner being false implies there's an upstream. - if !self.withhold && !self.status.is_solo_miner() { - self.last_template_id = template_id; - let for_upstream = Mining::SubmitSharesExtended(share); - Ok(SendTo::RelayNewMessage(for_upstream)) - } else { - // If withholding or in solo mining, no action is needed upstream. - Ok(SendTo::None(None)) - } - } - // We are in an extended channel shares are extended - Share::Standard(_) => unreachable!(), - } - } - // When we have a ShareMeetBitcoinTarget it means that the proxy know the bitcoin - // target that means that the proxy must have JD capabilities that means that the - // second tuple elements can not be None but must be Some(template_id) - OnNewShare::ShareMeetBitcoinTarget(_) => unreachable!(), - OnNewShare::SendSubmitShareUpstream(_) => unreachable!(), - OnNewShare::ShareMeetDownstreamTarget => Ok(SendTo::None(None)), - } - } - - /// Handles a `SetCustomMiningJob` message received from the downstream. - /// - /// Returns `Ok(SendTo::None(None))` indicating no action is taken. - fn handle_set_custom_mining_job( - &mut self, - _: SetCustomMiningJob, - ) -> Result, Error> { - warn!("Ignoring SetCustomMiningJob"); - Ok(SendTo::None(None)) - } -} - -impl ParseCommonMessagesFromDownstream for DownstreamMiningNode { - /// Handles a `SetupConnection` message received from the downstream. - /// - /// Returns `Ok(SendToCommon::Respond(response.into()))` indicating that a - /// `SetupConnectionSuccess` message should be sent back to the downstream - fn handle_setup_connection( - &mut self, - m: SetupConnection, - ) -> Result { - info!( - "Received `SetupConnection`: version={}, flags={:b}", - m.min_version, m.flags - ); - let response = SetupConnectionSuccess { - used_version: 2, - // require extended channels - flags: 0b0000_0000_0000_0010, - }; - let data = CommonDownstreamData { - header_only: false, - work_selection: false, - version_rolling: true, - }; - self.status.pair(data); - Ok(SendToCommon::Respond(response.into())) - } -} - -use binary_sv2::Str0255; -use network_helpers_sv2::noise_connection::Connection; -use std::net::SocketAddr; -use tokio::{ - net::TcpListener, - task::AbortHandle, - time::{timeout, Duration}, -}; - -/// Starts listening for incoming downstream mining node connections on the specified address. -/// -/// This function sets up a TCP listener and continuously accepts incoming connections. -/// For each accepted connection, it performs a Noise handshake, -/// initializes a `DownstreamMiningNode` instance, and spawns an task to handle -/// that specific downstream connection's lifecycle and message processing. -/// -/// NOTE: The current implementation is explicitly designed to handle only one downstream -/// connection at a time. If a second connection attempt is made while one is active, -/// it will be ignored and logged. This limitation needs refactoring to properly manage -/// the state and resources of multiple downstream nodes concurrently for a production environment. -/// -/// FIX ME: There is a noted issue where the downstream connection is established -/// and fully processed (including the initial handshake and starting its main loop) -/// *before* the connection with the Template Provider is initiated. This order can lead -/// to the downstream miner receiving jobs before the JDC has received template information. -/// The connection order should either be mutually exclusive or carefully managed to ensure -/// template information is available before providing jobs to the downstream. -#[allow(clippy::too_many_arguments)] -pub async fn listen_for_downstream_mining( - address: SocketAddr, - upstream: Option>>, - withhold: bool, - authority_public_key: Secp256k1PublicKey, - authority_secret_key: Secp256k1SecretKey, - cert_validity_sec: u64, - task_collector: Arc>>, - tx_status: async_channel::Sender>, - miner_coinbase_output: Vec, - jd: Option>>, - config: JobDeclaratorClientConfig, - shutdown: Arc, - jdc_signature: String, -) { - info!("Listening for downstream mining connections on {}", address); - - // Bind to the listener address. - let listener = TcpListener::bind(address).await.unwrap(); - let mut has_downstream = false; - - // Loop indefinitely to accept incoming connections or handle shutdown. - loop { - tokio::select! { - // Handle shutdown signal. - _ = shutdown.notified() => { - info!("Shutdown signal received. Stopping downstream mining listener."); - break; - } - // Accept an incoming TCP connection. - Ok((stream, _)) = listener.accept() => { - // Check if a downstream connection is already active. - if has_downstream { - error!("A downstream connection is already active. Ignoring additional connections."); - continue; - } - has_downstream = true; - let task_collector = task_collector.clone(); - let miner_coinbase_output = miner_coinbase_output.clone(); - let jd = jd.clone(); - let upstream = upstream.clone(); - let timeout = config.timeout(); - let mut parts = config.tp_address().split(':'); - let ip_tp = parts.next().unwrap().to_string(); - let port_tp = parts.next().unwrap().parse::().unwrap(); - - // Create a channel for sending miner solutions from this downstream to the template receiver. - let (send_solution, recv_solution) = bounded(10); - - let responder = Responder::from_authority_kp( - &authority_public_key.into_bytes(), - &authority_secret_key.into_bytes(), - std::time::Duration::from_secs(cert_validity_sec), - ) - .unwrap(); - let (receiver, sender) = - Connection::new(stream, HandshakeRole::Responder(responder)) - .await - .expect("impossible to connect"); - - let tx_status_downstream = status::Sender::Downstream(tx_status.clone()); - - // Create a new DownstreamMiningNode instance for this connection. - let node = DownstreamMiningNode::new( - receiver, - sender, - upstream.clone(), - send_solution, - withhold, - task_collector.clone(), - tx_status_downstream, - miner_coinbase_output, - jd.clone(), - jdc_signature.clone(), - ); - - // The first message from the downstream should be SetupConnection. - // Receive and attempt to parse this initial message. - let mut incoming: StdFrame = node.receiver.recv().await.unwrap().try_into().unwrap(); - let message_type = incoming.get_header().unwrap().msg_type(); - let payload = incoming.payload(); - let node = Arc::new(Mutex::new(node)); - - // If connected to an upstream pool, set this downstream as the upstream's downstream. - if let Some(upstream) = upstream { - upstream - .safe_lock(|s| s.downstream = Some(node.clone())) - .unwrap(); - } - - if let Ok(SendToCommon::Respond(message)) = DownstreamMiningNode::handle_message_common( - node.clone(), - message_type, - payload, - ) { - let message = match message { - roles_logic_sv2::parsers::CommonMessages::SetupConnectionSuccess(m) => m, - _ => panic!(), - }; - - // Spawn a task to start the main message processing loop for this downstream. - let main_task = tokio::task::spawn({ - let node = node.clone(); - async move { - DownstreamMiningNode::start(&node, message).await; - } - }); - - node.safe_lock(|n| { - n.task_collector - .safe_lock(|c| { - c.push(main_task.abort_handle()); - }) - .unwrap() - }) - .unwrap(); - - - TemplateRx::connect( - SocketAddr::new(IpAddr::from_str(ip_tp.as_str()).unwrap(), port_tp), - recv_solution, - status::Sender::TemplateReceiver(tx_status.clone()), - jd, - node, - task_collector, - Arc::new(Mutex::new(PoolChangerTrigger::new(timeout))), - vec![], - config.tp_authority_public_key().cloned(), - ) - .await; - } - } - } - } - - info!("Downstream mining listener has shut down."); -} - -impl IsDownstream for DownstreamMiningNode { - // Retrieves the `CommonDownstreamData` from the current status of the downstream node. - fn get_downstream_mining_data(&self) -> CommonDownstreamData { - match self.status { - DownstreamMiningNodeStatus::Initializing(_) => panic!(), - DownstreamMiningNodeStatus::Paired((data, _)) => data, - DownstreamMiningNodeStatus::ChannelOpened((_, data, _)) => data, - DownstreamMiningNodeStatus::SoloMinerPaired(data) => data, - DownstreamMiningNodeStatus::SoloMinerChannelOpend((_, data)) => data, - } - } -} - -/// Implementation of the `IsMiningDownstream` trait for `DownstreamMiningNode`. Marker trait, -/// should we remove this? -impl IsMiningDownstream for DownstreamMiningNode {} diff --git a/roles/jd-client/src/lib/downstream/message_handler.rs b/roles/jd-client/src/lib/downstream/message_handler.rs new file mode 100644 index 0000000000..7d6b0e72cf --- /dev/null +++ b/roles/jd-client/src/lib/downstream/message_handler.rs @@ -0,0 +1,89 @@ +use crate::{downstream::Downstream, error::JDCError, utils::StdFrame}; +use std::convert::TryInto; +use stratum_common::roles_logic_sv2::{ + common_messages_sv2::{ + has_requires_std_job, has_work_selection, Protocol, SetupConnection, SetupConnectionError, + SetupConnectionSuccess, + }, + handlers_sv2::HandleCommonMessagesFromClientAsync, + parsers_sv2::AnyMessage, +}; +use tracing::info; + +impl HandleCommonMessagesFromClientAsync for Downstream { + type Error = JDCError; + // Handles the initial [`SetupConnection`] message from a downstream client. + // + // This method validates that the connection request is compatible with the + // supported mining protocol and feature set. The flow is: + // + // 1. Protocol validation + // - Only the `MiningProtocol` is supported. + // - If the client requests another protocol, the connection is rejected with a + // [`SetupConnectionError`] (`unsupported-protocol`). + // + // 2. Feature flag validation + // - Work selection (`work_selection`) is not allowed. + // - If requested, the connection is rejected with a [`SetupConnectionError`] + // (`unsupported-feature-flags`). + // + // 3. Standard job requirement + // - If the downstream sets the `requires_standard_job` flag, it is recorded in + // [`DownstreamData::require_std_job`]. + // + // 4. Successful setup + // - If all validations pass, a [`SetupConnectionSuccess`] message is + async fn handle_setup_connection( + &mut self, + _client_id: Option, + msg: SetupConnection<'_>, + ) -> Result<(), Self::Error> { + info!("Received: {}", msg); + + if msg.protocol != Protocol::MiningProtocol { + info!("Rejecting connection: SetupConnection asking for other protocols than mining protocol."); + let response = SetupConnectionError { + flags: 0, + error_code: "unsupported-protocol" + .to_string() + .try_into() + .expect("error code must be valid string"), + }; + let frame: StdFrame = AnyMessage::Common(response.into_static().into()).try_into()?; + _ = self.downstream_channel.downstream_sender.send(frame).await; + + return Err(JDCError::Shutdown); + } + + if has_work_selection(msg.flags) { + info!("Rejecting: work selection not allowed."); + let response = SetupConnectionError { + flags: 0b0000_0000_0000_0010, + error_code: "unsupported-feature-flags" + .to_string() + .try_into() + .expect("error code must be valid string"), + }; + let frame: StdFrame = AnyMessage::Common(response.into_static().into()) + .try_into() + .unwrap(); + _ = self.downstream_channel.downstream_sender.send(frame).await; + + return Err(JDCError::Shutdown); + } + + if has_requires_std_job(msg.flags) { + self.downstream_data + .super_safe_lock(|data| data.require_std_job = true); + } + let response = SetupConnectionSuccess { + used_version: 2, + flags: msg.flags, + }; + let frame: StdFrame = AnyMessage::Common(response.into_static().into()).try_into()?; + + _ = self.downstream_channel.downstream_sender.send(frame).await; + + Ok(()) + } +} diff --git a/roles/jd-client/src/lib/downstream/mod.rs b/roles/jd-client/src/lib/downstream/mod.rs new file mode 100644 index 0000000000..ffa7677387 --- /dev/null +++ b/roles/jd-client/src/lib/downstream/mod.rs @@ -0,0 +1,283 @@ +use std::{collections::HashMap, sync::Arc}; + +use async_channel::{unbounded, Receiver, Sender}; +use stratum_common::{ + network_helpers_sv2::noise_stream::NoiseTcpStream, + roles_logic_sv2::{ + channels_sv2::server::{ + extended::ExtendedChannel, + group::GroupChannel, + jobs::{extended::ExtendedJob, job_store::DefaultJobStore, standard::StandardJob}, + standard::StandardChannel, + }, + codec_sv2, + common_messages_sv2::MESSAGE_TYPE_SETUP_CONNECTION, + handlers_sv2::HandleCommonMessagesFromClientAsync, + parsers_sv2::{AnyMessage, IsSv2Message}, + utils::Mutex, + }, +}; + +use tokio::sync::broadcast; +use tracing::{debug, error, warn}; + +use crate::{ + error::JDCError, + status::{handle_error, Status, StatusSender}, + task_manager::TaskManager, + utils::{ + protocol_message_type, spawn_io_tasks, Message, MessageType, SV2Frame, ShutdownMessage, + StdFrame, + }, +}; + +mod message_handler; + +/// Holds state related to a downstream connection's mining channels. +/// +/// This includes: +/// - Whether the downstream requires a standard job (`require_std_job`). +/// - An optional [`GroupChannel`] if group channeling is used. +/// - Active [`ExtendedChannel`]s keyed by channel ID. +/// - Active [`StandardChannel`]s keyed by channel ID. +pub struct DownstreamData { + pub require_std_job: bool, + pub group_channels: Option>>>, + pub extended_channels: + HashMap>>>, + pub standard_channels: + HashMap>>>, +} + +/// Communication layer for a downstream connection. +/// +/// Provides the messaging primitives for interacting with the +/// channel manager and the downstream peer: +/// - `channel_manager_sender`: sends frames to the channel manager. +/// - `channel_manager_receiver`: receives messages from the channel manager. +/// - `downstream_sender`: sends frames to the downstream. +/// - `downstream_receiver`: receives frames from the downstream. +#[derive(Clone)] +pub struct DownstreamChannel { + channel_manager_sender: Sender<(u32, SV2Frame)>, + channel_manager_receiver: broadcast::Sender<(u32, Message)>, + downstream_sender: Sender, + downstream_receiver: Receiver, +} + +/// Represents a downstream client connected to this node. +#[derive(Clone)] +pub struct Downstream { + pub downstream_data: Arc>, + downstream_channel: DownstreamChannel, + pub downstream_id: u32, +} + +impl Downstream { + /// Creates a new [`Downstream`] instance and spawns the necessary I/O tasks. + pub fn new( + downstream_id: u32, + channel_manager_sender: Sender<(u32, SV2Frame)>, + channel_manager_receiver: broadcast::Sender<(u32, Message)>, + noise_stream: NoiseTcpStream, + notify_shutdown: broadcast::Sender, + task_manager: Arc, + status_sender: Sender, + ) -> Self { + let (noise_stream_reader, noise_stream_writer) = noise_stream.into_split(); + let status_sender = StatusSender::Downstream { + downstream_id, + tx: status_sender, + }; + let (inbound_tx, inbound_rx) = unbounded::(); + let (outbound_tx, outbound_rx) = unbounded::(); + spawn_io_tasks( + task_manager, + noise_stream_reader, + noise_stream_writer, + outbound_rx, + inbound_tx, + notify_shutdown, + status_sender, + ); + + let downstream_channel = DownstreamChannel { + channel_manager_receiver, + channel_manager_sender, + downstream_sender: outbound_tx, + downstream_receiver: inbound_rx, + }; + let downstream_data = Arc::new(Mutex::new(DownstreamData { + require_std_job: false, + extended_channels: HashMap::new(), + standard_channels: HashMap::new(), + group_channels: None, + })); + Downstream { + downstream_channel, + downstream_data, + downstream_id, + } + } + + /// Starts the downstream loop. + /// + /// Responsibilities: + /// - Performs the initial `SetupConnection` handshake with the downstream. + /// - Forwards mining-related messages to the channel manager. + /// - Forwards channel manager messages back to the downstream peer. + pub async fn start( + mut self, + notify_shutdown: broadcast::Sender, + status_sender: Sender, + task_manager: Arc, + ) { + let status_sender = StatusSender::Downstream { + downstream_id: self.downstream_id, + tx: status_sender, + }; + + let mut shutdown_rx = notify_shutdown.subscribe(); + + // Setup initial connection + if let Err(e) = self.setup_connection_with_downstream().await { + error!(?e, "Failed to set up downstream connection"); + handle_error(&status_sender, e).await; + return; + } + + let mut receiver = self.downstream_channel.channel_manager_receiver.subscribe(); + task_manager.spawn(async move { + loop { + let self_clone_1 = self.clone(); + let downstream_id = self_clone_1.downstream_id; + let self_clone_2 = self.clone(); + tokio::select! { + message = shutdown_rx.recv() => { + match message { + Ok(ShutdownMessage::ShutdownAll) => { + debug!("Downstream {downstream_id}: Received global shutdown"); + break; + } + Ok(ShutdownMessage::DownstreamShutdown(id)) if downstream_id == id => { + debug!("Downstream {downstream_id}: Received downstream {id} shutdown"); + break; + } + Ok(ShutdownMessage::JobDeclaratorShutdownFallback(_)) => { + debug!("Downstream {downstream_id}: Received job declaratorShutdown shutdown"); + break; + } + Ok(ShutdownMessage::UpstreamShutdownFallback(_)) => { + debug!("Downstream {downstream_id}: Received job Upstream shutdown"); + break; + } + _ => {} + } + } + res = self_clone_1.handle_downstream_message() => { + if let Err(e) = res { + error!(?e, "Error handling downstream message for {downstream_id}"); + handle_error(&status_sender, e).await; + break; + } + } + res = self_clone_2.handle_channel_manager_message(&mut receiver) => { + if let Err(e) = res { + error!(?e, "Error handling channel manager message for {downstream_id}"); + handle_error(&status_sender, e).await; + break; + } + } + + } + } + warn!("Downstream: unified message loop exited."); + }); + } + + // Performs the initial handshake with a downstream peer. + async fn setup_connection_with_downstream(&mut self) -> Result<(), JDCError> { + let mut frame = self.downstream_channel.downstream_receiver.recv().await?; + + let Some(message_type) = frame.get_header().map(|m| m.msg_type()) else { + return Err(JDCError::UnexpectedMessage(0)); + }; + if message_type == MESSAGE_TYPE_SETUP_CONNECTION { + self.handle_common_message_frame_from_client(None, message_type, frame.payload()) + .await?; + return Ok(()); + } + Err(JDCError::UnexpectedMessage(message_type)) + } + + // Handles messages sent from the channel manager to this downstream. + async fn handle_channel_manager_message( + self, + receiver: &mut broadcast::Receiver<(u32, AnyMessage<'static>)>, + ) -> Result<(), JDCError> { + let (downstream_id, frame) = match receiver.recv().await { + Ok(msg) => msg, + Err(e) => { + warn!(?e, "Broadcast receive failed"); + return Ok(()); + } + }; + + if downstream_id != self.downstream_id { + debug!( + ?downstream_id, + "Message ignored for non-matching downstream" + ); + return Ok(()); + } + + let message_type = frame.message_type(); + let std_frame = match StdFrame::from_message(frame, message_type, 0, true) { + Some(f) => f, + None => { + debug!("Invalid frame conversion; skipping message"); + return Ok(()); + } + }; + + self.downstream_channel + .downstream_sender + .send(std_frame) + .await + .map_err(|e| { + error!(?e, "Downstream send failed"); + JDCError::CodecNoise(codec_sv2::noise_sv2::Error::ExpectedIncomingHandshakeMessage) + })?; + + Ok(()) + } + + // Handles incoming messages from the downstream peer. + async fn handle_downstream_message(self) -> Result<(), JDCError> { + let sv2_frame = self.downstream_channel.downstream_receiver.recv().await?; + + let Some(message_type) = sv2_frame.get_header().map(|h| h.msg_type()) else { + return Ok(()); + }; + + if protocol_message_type(message_type) != MessageType::Mining { + warn!( + ?message_type, + "Received unsupported message type from downstream." + ); + return Ok(()); + } + + debug!("Received mining SV2 frame from downstream."); + self.downstream_channel + .channel_manager_sender + .send((self.downstream_id, sv2_frame)) + .await + .map_err(|e| { + error!(error=?e, "Failed to send mining message to channel manager."); + JDCError::ChannelErrorSender + })?; + + Ok(()) + } +} diff --git a/roles/jd-client/src/lib/error.rs b/roles/jd-client/src/lib/error.rs index 682c0244ba..bba6d9d212 100644 --- a/roles/jd-client/src/lib/error.rs +++ b/roles/jd-client/src/lib/error.rs @@ -12,34 +12,20 @@ //! This module ensures that all errors can be passed around consistently, including across async //! boundaries. use ext_config::ConfigError; -use roles_logic_sv2::mining_sv2::{ExtendedExtranonce, NewExtendedMiningJob, SetCustomMiningJob}; use std::fmt; - -pub type ProxyResult<'a, T> = core::result::Result>; - -#[allow(dead_code)] -#[derive(Debug)] -pub enum ChannelSendError<'a> { - SubmitSharesExtended( - async_channel::SendError>, - ), - SetNewPrevHash(async_channel::SendError>), - NewExtendedMiningJob(async_channel::SendError>), - General(String), - Extranonce(async_channel::SendError<(ExtendedExtranonce, u32)>), - SetCustomMiningJob( - async_channel::SendError>, - ), - NewTemplate( - async_channel::SendError<( - roles_logic_sv2::template_distribution_sv2::SetNewPrevHash<'a>, - Vec, - )>, - ), -} +use stratum_common::{ + network_helpers_sv2, + roles_logic_sv2::{ + self, bitcoin, + codec_sv2::{self, binary_sv2, framing_sv2}, + handlers_sv2::HandlerErrorType, + parsers_sv2::ParserError, + }, +}; +use tokio::{sync::broadcast, time::error::Elapsed}; #[derive(Debug)] -pub enum Error<'a> { +pub enum JDCError { #[allow(dead_code)] VecToSlice32(Vec), /// Errors on bad CLI argument input. @@ -63,166 +49,247 @@ pub enum Error<'a> { SubprotocolMining(String), // Locking Errors PoisonLock, - // Channel Receiver Error - ChannelErrorReceiver(async_channel::RecvError), TokioChannelErrorRecv(tokio::sync::broadcast::error::RecvError), - // Channel Sender Errors - ChannelErrorSender(ChannelSendError<'a>), Infallible(std::convert::Infallible), + Parser(ParserError), + /// Channel receiver error + ChannelErrorReceiver(async_channel::RecvError), + /// Channel sender error + ChannelErrorSender, + /// Broadcast channel receiver error + BroadcastChannelErrorReceiver(broadcast::error::RecvError), + Shutdown, + NetworkHelpersError(stratum_common::network_helpers_sv2::Error), + UnexpectedMessage(u8), + InvalidUserIdentity(String), + BitcoinEncodeError(bitcoin::consensus::encode::Error), + InvalidSocketAddress(String), + Timeout, + LastDeclareJobNotFound(u32), + ActiveJobNotFound(u32), + TokenNotFound, + TemplateNotFound(u64), + DownstreamNotFoundWithChannelId(u32), + DownstreamNotFound(u32), + DownstreamIdNotFound, + FutureTemplateNotPresent, + LastNewPrevhashNotFound, + VardiffNotFound(u32), + TxDataError, + FrameConversionError, + FailedToCreateCustomJob, + AllocateMiningJobTokenSuccessCoinbaseOutputsError, + ChannelManagerHasBadCoinbaseOutputs, + DeclaredJobHasBadCoinbaseOutputs, } -impl fmt::Display for Error<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use Error::*; +impl std::error::Error for JDCError {} + +impl fmt::Display for JDCError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use JDCError::*; match self { BadCliArgs => write!(f, "Bad CLI arg input"), - BadConfigDeserialize(ref e) => write!(f, "Bad `config` TOML deserialize: `{:?}`", e), - BinarySv2(ref e) => write!(f, "Binary SV2 error: `{:?}`", e), - CodecNoise(ref e) => write!(f, "Noise error: `{:?}", e), - FramingSv2(ref e) => write!(f, "Framing SV2 error: `{:?}`", e), - Io(ref e) => write!(f, "I/O error: `{:?}", e), - ParseInt(ref e) => write!(f, "Bad convert from `String` to `int`: `{:?}`", e), - RolesSv2Logic(ref e) => write!(f, "Roles SV2 Logic Error: `{:?}`", e), - SubprotocolMining(ref e) => write!(f, "Subprotocol Mining Error: `{:?}`", e), - UpstreamIncoming(ref e) => write!(f, "Upstream parse incoming error: `{:?}`", e), + BadConfigDeserialize(ref e) => write!(f, "Bad `config` TOML deserialize: `{e:?}`"), + BinarySv2(ref e) => write!(f, "Binary SV2 error: `{e:?}`"), + CodecNoise(ref e) => write!(f, "Noise error: `{e:?}"), + FramingSv2(ref e) => write!(f, "Framing SV2 error: `{e:?}`"), + Io(ref e) => write!(f, "I/O error: `{e:?}"), + ParseInt(ref e) => write!(f, "Bad convert from `String` to `int`: `{e:?}`"), + RolesSv2Logic(ref e) => write!(f, "Roles SV2 Logic Error: `{e:?}`"), + SubprotocolMining(ref e) => write!(f, "Subprotocol Mining Error: `{e:?}`"), + UpstreamIncoming(ref e) => write!(f, "Upstream parse incoming error: `{e:?}`"), PoisonLock => write!(f, "Poison Lock error"), - ChannelErrorReceiver(ref e) => write!(f, "Channel receive error: `{:?}`", e), - TokioChannelErrorRecv(ref e) => write!(f, "Channel receive error: `{:?}`", e), - ChannelErrorSender(ref e) => write!(f, "Channel send error: `{:?}`", e), - VecToSlice32(ref e) => write!(f, "Standard Error: `{:?}`", e), - Infallible(ref e) => write!(f, "Infallible Error:`{:?}`", e), + ChannelErrorReceiver(ref e) => write!(f, "Channel receive error: `{e:?}`"), + TokioChannelErrorRecv(ref e) => write!(f, "Channel receive error: `{e:?}`"), + VecToSlice32(ref e) => write!(f, "Standard Error: `{e:?}`"), + Infallible(ref e) => write!(f, "Infallible Error:`{e:?}`"), + Parser(ref e) => write!(f, "Parser error: `{e:?}`"), + BroadcastChannelErrorReceiver(ref e) => { + write!(f, "Broadcast channel receive error: {e:?}") + } + ChannelErrorSender => write!(f, "Sender error"), + Shutdown => write!(f, "Shutdown"), + NetworkHelpersError(ref e) => write!(f, "Network error: {e:?}"), + UnexpectedMessage(message_type) => write!(f, "Unexpected Message: {message_type}"), + InvalidUserIdentity(_) => write!(f, "User ID is invalid"), + BitcoinEncodeError(_) => write!(f, "Error generated during encoding"), + InvalidSocketAddress(ref s) => write!(f, "Invalid socket address: {s}"), + Timeout => write!(f, "Time out error"), + LastDeclareJobNotFound(request_id) => { + write!(f, "last declare job not found for request id: {request_id}") + } + ActiveJobNotFound(request_id) => { + write!(f, "Active Job not found for request_id: {request_id}") + } + TokenNotFound => { + write!(f, "Token Not found") + } + TemplateNotFound(template_id) => { + write!(f, "Template not found, template_id: {template_id}") + } + DownstreamNotFoundWithChannelId(channel_id) => { + write!(f, "Downstream not found with channel id: {channel_id}") + } + DownstreamNotFound(downstream_id) => { + write!( + f, + "Downstream not found with downstream_id: {downstream_id}" + ) + } + DownstreamIdNotFound => { + write!(f, "Downstream id not found") + } + FutureTemplateNotPresent => { + write!(f, "Future template not present") + } + LastNewPrevhashNotFound => { + write!(f, "Last new prevhash not found") + } + VardiffNotFound(channel_id) => { + write!(f, "Vardiff not found for channel id: {channel_id:?}") + } + TxDataError => { + write!(f, "Transaction data error") + } + FrameConversionError => { + write!(f, "Could not convert message to frame") + } + FailedToCreateCustomJob => { + write!(f, "failed to create custom job") + } + AllocateMiningJobTokenSuccessCoinbaseOutputsError => { + write!( + f, + "AllocateMiningJobToken.Success coinbase outputs are not deserializable" + ) + } + ChannelManagerHasBadCoinbaseOutputs => { + write!(f, "Channel Manager coinbase outputs are not deserializable") + } + DeclaredJobHasBadCoinbaseOutputs => { + write!(f, "Declared job coinbase outputs are not deserializable") + } + } + } +} + +impl JDCError { + fn is_non_critical_variant(&self) -> bool { + matches!( + self, + JDCError::LastNewPrevhashNotFound + | JDCError::FutureTemplateNotPresent + | JDCError::LastDeclareJobNotFound(_) + | JDCError::ActiveJobNotFound(_) + | JDCError::TokenNotFound + | JDCError::TemplateNotFound(_) + | JDCError::DownstreamNotFoundWithChannelId(_) + | JDCError::DownstreamNotFound(_) + | JDCError::DownstreamIdNotFound + | JDCError::VardiffNotFound(_) + | JDCError::TxDataError + | JDCError::FrameConversionError + | JDCError::FailedToCreateCustomJob + ) + } + + /// Adds basic priority to error types: + /// todo: design a better error priority system. + pub fn is_critical(&self) -> bool { + if self.is_non_critical_variant() { + tracing::error!("Non-critical error: {self}"); + return false; } + + true + } +} + +impl From for JDCError { + fn from(e: ParserError) -> Self { + JDCError::Parser(e) } } -impl From for Error<'_> { +impl From for JDCError { fn from(e: binary_sv2::Error) -> Self { - Error::BinarySv2(e) + JDCError::BinarySv2(e) } } -impl From for Error<'_> { +impl From for JDCError { fn from(e: codec_sv2::noise_sv2::Error) -> Self { - Error::CodecNoise(e) + JDCError::CodecNoise(e) } } -impl From for Error<'_> { +impl From for JDCError { fn from(e: framing_sv2::Error) -> Self { - Error::FramingSv2(e) + JDCError::FramingSv2(e) } } -impl From for Error<'_> { +impl From for JDCError { fn from(e: std::io::Error) -> Self { - Error::Io(e) + JDCError::Io(e) } } -impl From for Error<'_> { +impl From for JDCError { fn from(e: std::num::ParseIntError) -> Self { - Error::ParseInt(e) + JDCError::ParseInt(e) } } -impl From for Error<'_> { +impl From for JDCError { fn from(e: roles_logic_sv2::errors::Error) -> Self { - Error::RolesSv2Logic(e) + JDCError::RolesSv2Logic(e) } } -impl From for Error<'_> { +impl From for JDCError { fn from(e: ConfigError) -> Self { - Error::BadConfigDeserialize(e) + JDCError::BadConfigDeserialize(e) } } -impl From for Error<'_> { +impl From for JDCError { fn from(e: async_channel::RecvError) -> Self { - Error::ChannelErrorReceiver(e) + JDCError::ChannelErrorReceiver(e) } } -impl From for Error<'_> { +impl From for JDCError { fn from(e: tokio::sync::broadcast::error::RecvError) -> Self { - Error::TokioChannelErrorRecv(e) + JDCError::TokioChannelErrorRecv(e) } } -// *** LOCK ERRORS *** -// impl<'a> From>> for Error<'a> { -// fn from(e: PoisonError>) -> Self { -// Error::PoisonLock( -// LockError::Bridge(e) -// ) -// } -// } - -// impl<'a> From>> for Error<'a> { -// fn from(e: PoisonError>) -> Self { -// Error::PoisonLock( -// LockError::NextMiningNotify(e) -// ) -// } -// } - -// *** CHANNEL SENDER ERRORS *** -impl<'a> From>> - for Error<'a> -{ - fn from( - e: async_channel::SendError>, - ) -> Self { - Error::ChannelErrorSender(ChannelSendError::SubmitSharesExtended(e)) +impl From for JDCError { + fn from(value: network_helpers_sv2::Error) -> Self { + JDCError::NetworkHelpersError(value) } } -impl<'a> From>> - for Error<'a> -{ - fn from(e: async_channel::SendError>) -> Self { - Error::ChannelErrorSender(ChannelSendError::SetNewPrevHash(e)) +impl From for JDCError { + fn from(value: stratum_common::roles_logic_sv2::bitcoin::consensus::encode::Error) -> Self { + JDCError::BitcoinEncodeError(value) } } -impl From> for Error<'_> { - fn from(e: async_channel::SendError<(ExtendedExtranonce, u32)>) -> Self { - Error::ChannelErrorSender(ChannelSendError::Extranonce(e)) +impl From for JDCError { + fn from(_value: Elapsed) -> Self { + Self::Timeout } } -impl<'a> From>> for Error<'a> { - fn from(e: async_channel::SendError>) -> Self { - Error::ChannelErrorSender(ChannelSendError::NewExtendedMiningJob(e)) +impl HandlerErrorType for JDCError { + fn parse_error(error: ParserError) -> Self { + JDCError::Parser(error) } -} - -impl<'a> From>> for Error<'a> { - fn from(e: async_channel::SendError>) -> Self { - Error::ChannelErrorSender(ChannelSendError::SetCustomMiningJob(e)) - } -} - -impl<'a> - From< - async_channel::SendError<( - roles_logic_sv2::template_distribution_sv2::SetNewPrevHash<'a>, - Vec, - )>, - > for Error<'a> -{ - fn from( - e: async_channel::SendError<( - roles_logic_sv2::template_distribution_sv2::SetNewPrevHash<'a>, - Vec, - )>, - ) -> Self { - Error::ChannelErrorSender(ChannelSendError::NewTemplate(e)) - } -} -impl From for Error<'_> { - fn from(e: std::convert::Infallible) -> Self { - Error::Infallible(e) + fn unexpected_message(message_type: u8) -> Self { + JDCError::UnexpectedMessage(message_type) } } diff --git a/roles/jd-client/src/lib/jd_mode.rs b/roles/jd-client/src/lib/jd_mode.rs new file mode 100644 index 0000000000..0533afc718 --- /dev/null +++ b/roles/jd-client/src/lib/jd_mode.rs @@ -0,0 +1,61 @@ +//! Global configuration for Job Declarator (JD) operating mode. +//! +//! This module defines different operating modes for the Job Declarator +//! and provides atomic accessors for setting and retrieving the current mode. +//! +//! Modes are stored in a global [`AtomicU8`] to allow safe concurrent access +//! across threads. +use std::sync::atomic::{AtomicU8, Ordering}; + +/// Operating modes for the Job Declarator. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum JdMode { + /// Runs in Coinbase only mode. + CoinbaseOnly = 0, + /// Runs in Full template mode, + FullTemplate = 1, + /// Runs in solo mining mode, + SoloMining = 2, +} + +impl From for JdMode { + fn from(val: u8) -> Self { + match val { + 0 => JdMode::CoinbaseOnly, + 1 => JdMode::FullTemplate, + 2 => JdMode::SoloMining, + _ => JdMode::SoloMining, + } + } +} + +impl From for JdMode { + fn from(val: u32) -> Self { + match val { + 0 => JdMode::CoinbaseOnly, + 1 => JdMode::FullTemplate, + 2 => JdMode::SoloMining, + _ => JdMode::SoloMining, + } + } +} + +impl From for u8 { + fn from(mode: JdMode) -> Self { + mode as u8 + } +} + +/// Global atomic variable storing the current JD mode. +pub static JD_MODE: AtomicU8 = AtomicU8::new(JdMode::SoloMining as u8); + +/// Updates the global JD mode. +pub fn set_jd_mode(mode: JdMode) { + JD_MODE.store(mode as u8, Ordering::SeqCst); +} + +/// Returns the current global JD mode. +pub fn get_jd_mode() -> JdMode { + JD_MODE.load(Ordering::SeqCst).into() +} diff --git a/roles/jd-client/src/lib/job_declarator/message_handler.rs b/roles/jd-client/src/lib/job_declarator/message_handler.rs index 07b26938b6..c6f825bbf0 100644 --- a/roles/jd-client/src/lib/job_declarator/message_handler.rs +++ b/roles/jd-client/src/lib/job_declarator/message_handler.rs @@ -1,135 +1,65 @@ -//! Job Declarator: Message handler module -//! -//! Handles upstream Job Declaration Protocol messages by implementing the -//! `ParseJobDeclarationMessagesFromUpstream` trait. -use super::JobDeclarator; -use roles_logic_sv2::{ - handlers::{job_declaration::ParseJobDeclarationMessagesFromUpstream, SendTo_}, - job_declaration_sv2::{ - AllocateMiningJobTokenSuccess, DeclareMiningJobError, DeclareMiningJobSuccess, - ProvideMissingTransactions, ProvideMissingTransactionsSuccess, +use stratum_common::roles_logic_sv2::{ + common_messages_sv2::{ + ChannelEndpointChanged, Reconnect, SetupConnectionError, SetupConnectionSuccess, }, - parsers::JobDeclaration, + handlers_sv2::HandleCommonMessagesFromServerAsync, }; -use tracing::{debug, error, info}; -pub type SendTo = SendTo_, ()>; -use roles_logic_sv2::errors::Error; +use tracing::{info, warn}; -impl ParseJobDeclarationMessagesFromUpstream for JobDeclarator { - /// Handles an `AllocateMiningJobTokenSuccess` message received from the JDS. - /// - /// This message indicates that the JDS has successfully allocated a mining job token - /// in response to a previous `AllocateMiningJobToken` request. The allocated token - /// is added to the `JobDeclarator`'s internal pool of available tokens. - /// - /// Returns `Ok(SendTo::None(None))` indicating that no immediate response - /// is needed back to the JDS after successfully receiving and processing the token. - fn handle_allocate_mining_job_token_success( +use crate::{ + error::JDCError, + jd_mode::{set_jd_mode, JdMode}, + job_declarator::JobDeclarator, +}; + +impl HandleCommonMessagesFromServerAsync for JobDeclarator { + type Error = JDCError; + + async fn handle_setup_connection_success( &mut self, - message: AllocateMiningJobTokenSuccess, - ) -> Result { - info!( - "Received `AllocateMiningJobTokenSuccess` with id: {}", - message.request_id - ); - self.allocated_tokens.push(message.into_static()); + _server_id: Option, + msg: SetupConnectionSuccess, + ) -> Result<(), Self::Error> { + info!("Received: {}", msg); + + let jd_mode = match msg.flags { + 0 => JdMode::CoinbaseOnly, + 1 => JdMode::FullTemplate, + _ => JdMode::SoloMining, + }; + set_jd_mode(jd_mode); - Ok(SendTo::None(None)) + if jd_mode == JdMode::SoloMining { + return Err(JDCError::Shutdown); + } + + Ok(()) } - /// Handles a `DeclareMiningJobSuccess` message received from the JDS. - /// - /// Returns `Ok(SendTo::None(Some(message)))` wrapping the processed message - /// for potential forwarding or further handling within the `JobDeclarator`'s - /// message processing loop. - fn handle_declare_mining_job_success( + async fn handle_channel_endpoint_changed( &mut self, - message: DeclareMiningJobSuccess, - ) -> Result { - info!( - "Received `DeclareMiningJobSuccess` with id {}", - message.request_id - ); - debug!("`DeclareMiningJobSuccess`: {:?}", message); - let message = JobDeclaration::DeclareMiningJobSuccess(message.into_static()); - Ok(SendTo::None(Some(message))) + _server_id: Option, + msg: ChannelEndpointChanged, + ) -> Result<(), Self::Error> { + info!("Received: {}", msg); + Ok(()) } - /// Handles a `DeclareMiningJobError` message received from the JDS. - /// - /// Returns `Ok(SendTo::None(None))` indicating that no immediate response is - /// needed back to the JDS after receiving a job declaration error. The error - /// has been logged. - fn handle_declare_mining_job_error( + async fn handle_reconnect( &mut self, - message: DeclareMiningJobError, - ) -> Result { - error!( - "Received `DeclareMiningJobError`, error code: {}", - std::str::from_utf8(message.error_code.as_ref()).unwrap_or("unknown error code") - ); - debug!("`DeclareMiningJobError`: {:?}", message); - Ok(SendTo::None(None)) + _server_id: Option, + msg: Reconnect<'_>, + ) -> Result<(), Self::Error> { + info!("Received: {}", msg); + Ok(()) } - /// Handles a `ProvideMissingTransactions` message received from the JDS. - /// - /// This message is sent by the JDS to request the full transaction data for - /// specific transactions that it has identified as missing based on previous - /// communication (e.g., from an `IdentifyTransactions` exchange or a job declaration). - /// - /// The handler retrieves the full transaction list for the requested job (identified - /// by `request_id`) from its `last_declare_mining_jobs_sent` window. It then filters - /// this list to include only the transactions at the positions specified in the - /// `unknown_tx_position_list` from the incoming message and constructs a - /// `ProvideMissingTransactionsSuccess` message containing the requested transactions. - /// - /// Returns `Ok(SendTo::Respond(message_enum))` indicating that a response - /// (`ProvideMissingTransactionsSuccess`) containing the requested transactions - /// should be sent back to the JDS. - fn handle_provide_missing_transactions( + async fn handle_setup_connection_error( &mut self, - message: ProvideMissingTransactions, - ) -> Result { - info!( - "Received `ProvideMissingTransactions` with id: {}", - message.request_id - ); - debug!("`ProvideMissingTransactions`: {:?}", message); - - // Find the corresponding declared job in the window using the request ID. - // Extract the full transaction list from the found job's details. - let tx_list = self - .last_declare_mining_jobs_sent - .iter() - .find_map(|entry| { - if let Some((id, last_declare_job)) = entry { - if *id == message.request_id { - Some(last_declare_job.clone().tx_list.into_inner()) - } else { - None - } - } else { - None - } - }) - .ok_or(Error::UnknownRequestId(message.request_id))?; - - // Get the list of positions for missing transactions. - let unknown_tx_position_list: Vec = message.unknown_tx_position_list.into_inner(); - - // Filter the full transaction list to get the missing transactions based on positions. - let missing_transactions: Vec = unknown_tx_position_list - .iter() - .filter_map(|&pos| tx_list.get(pos as usize).cloned()) - .collect(); - let request_id = message.request_id; - let message_provide_missing_transactions = ProvideMissingTransactionsSuccess { - request_id, - transaction_list: binary_sv2::Seq064K::new(missing_transactions).unwrap(), - }; - let message_enum = - JobDeclaration::ProvideMissingTransactionsSuccess(message_provide_missing_transactions); - Ok(SendTo::Respond(message_enum)) + _server_id: Option, + msg: SetupConnectionError<'_>, + ) -> Result<(), Self::Error> { + warn!("Received: {}", msg); + Err(JDCError::Shutdown) } } diff --git a/roles/jd-client/src/lib/job_declarator/mod.rs b/roles/jd-client/src/lib/job_declarator/mod.rs index 0694852e8d..b486bcd32d 100644 --- a/roles/jd-client/src/lib/job_declarator/mod.rs +++ b/roles/jd-client/src/lib/job_declarator/mod.rs @@ -1,610 +1,309 @@ -//! ## Job Declarator Module -//! -//! It contains logic and constructs to connect to the Job Declarator Server (JDS) and process -//! messaging logic related to job declaration. -//! -//! It handles the lifecycle of declaring mining jobs to the JDS, including -//! allocating mining job tokens, sending job declarations based on templates, -//! processing responses from the JDS, and pushing block solutions. -//! -//! The module tightly couples with the `template_receiver` for new templates and -//! previous hash updates, and with the `upstream_sv2` module for communication -//! with the main pool instance (to send `SetCustomMiningJob(s)`). +use std::{net::SocketAddr, sync::Arc}; -pub mod message_handler; -use async_channel::{Receiver, Sender}; -use binary_sv2::{Seq0255, Seq064K, B016M, B064K, U256}; -use codec_sv2::{HandshakeRole, Initiator, StandardEitherFrame, StandardSv2Frame}; -use network_helpers_sv2::noise_connection::Connection; -use roles_logic_sv2::{ - handlers::SendTo_, - job_declaration_sv2::{AllocateMiningJobTokenSuccess, PushSolution}, - mining_sv2::SubmitSharesExtended, - parsers::{AnyMessage, JobDeclaration}, - template_distribution_sv2::SetNewPrevHash, - utils::Mutex, +use async_channel::{unbounded, Receiver, Sender}; +use key_utils::Secp256k1PublicKey; +use stratum_common::{ + network_helpers_sv2::noise_stream::NoiseTcpStream, + roles_logic_sv2::{ + codec_sv2::{self, framing_sv2, HandshakeRole, Initiator}, + handlers_sv2::HandleCommonMessagesFromServerAsync, + utils::Mutex, + }, }; -use std::{collections::HashMap, convert::TryInto}; -use stratum_common::bitcoin::{consensus, hashes::Hash, Transaction}; -use tokio::task::AbortHandle; -use tracing::{debug, error, info}; - -use async_recursion::async_recursion; -use nohash_hasher::BuildNoHashHasher; -use roles_logic_sv2::{ - handlers::job_declaration::ParseJobDeclarationMessagesFromUpstream, - job_declaration_sv2::{AllocateMiningJobToken, DeclareMiningJob}, - template_distribution_sv2::NewTemplate, - utils::Id, +use tokio::{ + net::TcpStream, + sync::{broadcast, mpsc}, }; -use std::{net::SocketAddr, sync::Arc}; +use tracing::{debug, error, info, warn}; -pub type Message = AnyMessage<'static>; -pub type SendTo = SendTo_, ()>; -pub type StdFrame = StandardSv2Frame; +use crate::{ + config::ConfigJDCMode, + error::JDCError, + status::{handle_error, Status, StatusSender}, + task_manager::TaskManager, + utils::{ + get_setup_connection_message_jds, protocol_message_type, spawn_io_tasks, Message, + MessageType, SV2Frame, ShutdownMessage, StdFrame, + }, +}; -mod setup_connection; -use setup_connection::SetupConnectionHandler; +mod message_handler; -use super::{config::JobDeclaratorClientConfig, error::Error, upstream_sv2::Upstream}; +/// Shared state for Job Declarator +pub struct JobDeclaratorData; -/// Struct describing LastDeclareJob fields. -#[derive(Debug, Clone)] -pub struct LastDeclareJob { - // The `DeclareMiningJob` message that was sent to the JDS. - declare_job: DeclareMiningJob<'static>, - // The `NewTemplate` message from which this job was derived. - template: NewTemplate<'static>, - // The `SetNewPrevHash` message associated with this job, if it's not a future template. - prev_hash: Option>, - // The pool's coinbase output(s) for this job - coinbase_pool_output: Vec, - // The list of transactions (as raw bytes) included in this job's template. - tx_list: Seq064K<'static, B016M<'static>>, +/// Holds all channels required for Job Declarator communication. +#[derive(Clone)] +pub struct JobDeclaratorChannel { + channel_manager_sender: Sender, + channel_manager_receiver: Receiver, + jds_sender: Sender, + jds_receiver: Receiver, } -/// Struct describing the JDC internal state and components. -#[derive(Debug)] +/// Manages the lifecycle and communication with a Job Declarator (JDS) +#[allow(warnings)] +#[derive(Clone)] pub struct JobDeclarator { - // Receiver channel for messages from the JDS. - receiver: Receiver>>, - // Sender channel for messages to the JDS. - sender: Sender>>, - // A pool of pre-allocated `AllocateMiningJobTokenSuccess` tokens received from the JDS. - allocated_tokens: Vec>, - // A simple ID generator for tracking request IDs. - req_ids: Id, - // An array to store information about the last two `DeclareMiningJob` messages sent. - // This is used to correlate `DeclareMiningJobSuccess` responses. - last_declare_mining_jobs_sent: [Option<(u32, LastDeclareJob)>; 2], - // The last received `SetNewPrevHash` message. - last_set_new_prev_hash: Option>, - // A counter to track how many `SetNewPrevHash` messages have been received - /// since the last time a future job was promoted. Used to determine if - /// a future job is still relevant when its `SetNewPrevHash` arrives. - set_new_prev_hash_counter: u8, - // A map storing information about future jobs (jobs derived from future templates) - // received from the Template Provider, keyed by their template ID. - // This information is kept until the corresponding `SetNewPrevHash` arrives, - // at which point the future job can be promoted and declared to the upstream pool. - #[allow(clippy::type_complexity)] - future_jobs: HashMap< - u64, - ( - DeclareMiningJob<'static>, - Seq0255<'static, U256<'static>>, - NewTemplate<'static>, - // pool's outputs - Vec, - ), - BuildNoHashHasher, - >, - // `Upstream` instance, used for communicating with the main pool instance - up: Arc>, - task_collector: Arc>>, - // The prefix of the coinbase transaction, - pub coinbase_tx_prefix: B064K<'static>, - // The suffix of the coinbase transaction, - pub coinbase_tx_suffix: B064K<'static>, + /// Internal state + job_declarator_data: Arc>, + /// Messaging channels to/from the channel manager and JD. + job_declarator_channel: JobDeclaratorChannel, + /// Socket address of the Job Declarator server. + socket_address: SocketAddr, + /// Config JDC mode + mode: ConfigJDCMode, } impl JobDeclarator { - /// Instantiates a new `JobDeclarator` client, connects to the provided JDS address, - /// performs the SV2 setup connection handshake, allocates initial mining job tokens, - /// and starts the background task for processing messages from the JDS. + /// Creates a new JobDeclarator instance by connecting and performing a Noise handshake. + /// + /// - Establishes TCP connection. + /// - Performs SV2 Noise handshake. + /// - Spawns background IO tasks for reading/writing frames. pub async fn new( - address: SocketAddr, - authority_public_key: [u8; 32], - config: JobDeclaratorClientConfig, - up: Arc>, - task_collector: Arc>>, - ) -> Result>, Error<'static>> { - let stream = tokio::net::TcpStream::connect(address).await?; - let initiator = Initiator::from_raw_k(authority_public_key)?; - let (mut receiver, mut sender) = - Connection::new(stream, HandshakeRole::Initiator(initiator)) - .await - .expect("impossible to connect"); - - info!( - "JD Client: SETUP_CONNECTION address: {:?}", - config.listening_address() - ); - - SetupConnectionHandler::setup(&mut receiver, &mut sender, *config.listening_address()) - .await - .unwrap(); - - info!("JD CONNECTED"); - - let self_ = Arc::new(Mutex::new(JobDeclarator { - receiver, - sender, - allocated_tokens: vec![], - req_ids: Id::new(), - last_declare_mining_jobs_sent: [None, None], - last_set_new_prev_hash: None, - future_jobs: HashMap::with_hasher(BuildNoHashHasher::default()), - up, - task_collector, - coinbase_tx_prefix: vec![].try_into().unwrap(), - coinbase_tx_suffix: vec![].try_into().unwrap(), - set_new_prev_hash_counter: 0, - })); - - Self::allocate_tokens(&self_, 2).await; - Self::on_upstream_message(self_.clone()); - Ok(self_) - } - - // Utility method to retrieve information about a previously sent `DeclareMiningJob` - // from the `last_declare_mining_jobs_sent` window based on its request ID. - fn get_last_declare_job_sent( - self_mutex: &Arc>, - request_id: u32, - ) -> Option { - self_mutex - .safe_lock(|s| { - for (id, job) in s.last_declare_mining_jobs_sent.iter().flatten() { - if *id == request_id { - return Some(job.to_owned()); - } - } - None - }) - .unwrap() - } - - // We maintain a window of 2 jobs. If more than 2 blocks are found, - // the ordering will depend on the request ID. Only the 2 most recent request - // IDs will be kept in memory, while the rest will be discarded. - // More information can be found here: https://github.com/stratum-mining/stratum/pull/904#discussion_r1609469048 - fn update_last_declare_job_sent( - self_mutex: &Arc>, - request_id: u32, - j: LastDeclareJob, - ) { - self_mutex - .safe_lock(|s| { - if let Some(empty_index) = s - .last_declare_mining_jobs_sent - .iter() - .position(|entry| entry.is_none()) - { - s.last_declare_mining_jobs_sent[empty_index] = Some((request_id, j)); - } else if let Some((min_index, _)) = s - .last_declare_mining_jobs_sent - .iter() - .enumerate() - .filter_map(|(i, entry)| entry.as_ref().map(|(id, _)| (i, id))) - .min_by_key(|&(_, id)| id) - { - s.last_declare_mining_jobs_sent[min_index] = Some((request_id, j)); - } - }) - .unwrap(); - } + upstreams: &(SocketAddr, SocketAddr, Secp256k1PublicKey, bool), + channel_manager_sender: Sender, + channel_manager_receiver: Receiver, + notify_shutdown: broadcast::Sender, + mode: ConfigJDCMode, + task_manager: Arc, + status_sender: Sender, + ) -> Result { + let (_, addr, pubkey, _) = upstreams; + info!("Connecting to JD Server at {addr}"); + let stream = tokio::time::timeout( + tokio::time::Duration::from_secs(5), + TcpStream::connect(addr), + ) + .await??; + info!("Connection established with JD Server at {addr} in mode: {mode:?}"); + let initiator = Initiator::from_raw_k(pubkey.into_bytes())?; + let (noise_stream_reader, noise_stream_writer) = + NoiseTcpStream::::new(stream, HandshakeRole::Initiator(initiator)) + .await? + .into_split(); - /// This method retrieves an allocated mining job token from the internal pool. - /// - /// If the pool of allocated tokens is empty or low, this method triggers the - /// allocation of more tokens from the JDS and waits until tokens are available - /// before returning one. This ensures that tokens are always available when - /// needed to declare new jobs. - #[async_recursion] - pub async fn get_last_token( - self_mutex: &Arc>, - ) -> AllocateMiningJobTokenSuccess<'static> { - // Check the current number of allocated tokens. - let mut token_len = self_mutex.safe_lock(|s| s.allocated_tokens.len()).unwrap(); - match token_len { - 0 => { - // If no tokens are available, spawn a task to allocate more (2 tokens). - { - let task = { - let self_mutex = self_mutex.clone(); - tokio::task::spawn(async move { - Self::allocate_tokens(&self_mutex, 2).await; - }) - }; - // Add the allocation task's handle to the collector. - self_mutex - .safe_lock(|s| { - s.task_collector - .safe_lock(|c| c.push(task.abort_handle())) - .unwrap() - }) - .unwrap(); - } + let status_sender = StatusSender::JobDeclarator(status_sender); + let (inbound_tx, inbound_rx) = unbounded::(); + let (outbound_tx, outbound_rx) = unbounded::(); - // Wait until at least one token becomes available to avoid infinite recursion. - while token_len == 0 { - tokio::task::yield_now().await; - token_len = self_mutex.safe_lock(|s| s.allocated_tokens.len()).unwrap(); - } - // Once tokens are available, recursively call get_last_token to retrieve one. - Self::get_last_token(self_mutex).await - } - 1 => { - // If only one token is available, spawn a task to allocate one more - // to maintain a buffer, but return the current token immediately. - { - let task = { - let self_mutex = self_mutex.clone(); - tokio::task::spawn(async move { - Self::allocate_tokens(&self_mutex, 1).await; - }) - }; - // Add the allocation task's handle to the collector. - self_mutex - .safe_lock(|s| { - s.task_collector - .safe_lock(|c| c.push(task.abort_handle())) - .unwrap() - }) - .unwrap(); - } - // There is a token, unwrap is safe - self_mutex - .safe_lock(|s| s.allocated_tokens.pop()) - .unwrap() - .unwrap() - } - // There are tokens, unwrap is safe - _ => self_mutex - .safe_lock(|s| s.allocated_tokens.pop()) - .unwrap() - .unwrap(), - } + spawn_io_tasks( + task_manager, + noise_stream_reader, + noise_stream_writer, + outbound_rx, + inbound_tx, + notify_shutdown, + status_sender, + ); + let job_declarator_data = Arc::new(Mutex::new(JobDeclaratorData)); + let job_declarator_channel = JobDeclaratorChannel { + channel_manager_receiver, + channel_manager_sender, + jds_sender: outbound_tx, + jds_receiver: inbound_rx, + }; + Ok(JobDeclarator { + job_declarator_channel, + job_declarator_data, + socket_address: *addr, + mode, + }) } - /// Handles the event of a new template being received from the Template Receiver. + /// Starts the JobDeclarator message loop. /// - /// This method constructs a `DeclareMiningJob` message based on the new template, - /// an allocated mining job token, and the miner's coinbase transaction parts. - /// It then updates the window of last sent jobs and sends the `DeclareMiningJob` - /// message to the JDS. - pub async fn on_new_template( - self_mutex: &Arc>, - template: NewTemplate<'static>, - token: Vec, - tx_list_: Seq064K<'static, B016M<'static>>, - excess_data: B064K<'static>, - coinbase_pool_output: Vec, + /// - Waits for shutdown signals. + /// - Handles incoming messages from Job Declarator and Channel Manager. + /// - Cleans up on termination. + pub async fn start( + mut self, + notify_shutdown: broadcast::Sender, + shutdown_complete_tx: mpsc::Sender<()>, + status_sender: Sender, + task_manager: Arc, ) { - // Get a new request ID and clone the sender for sending the message. - let (id, sender) = self_mutex - .safe_lock(|s| (s.req_ids.next(), s.sender.clone())) - .unwrap(); + let status_sender = StatusSender::JobDeclarator(status_sender); + let mut shutdown_rx = notify_shutdown.subscribe(); - // Deserialize the transaction list to calculate short hashes and the list hash. - let mut tx_list: Vec = Vec::new(); - let mut txids_as_u256: Vec> = Vec::new(); - for tx in tx_list_.to_vec() { - //TODO remove unwrap - let tx: Transaction = consensus::deserialize(&tx).unwrap(); - let txid = tx.compute_txid(); - let byte_array: [u8; 32] = *txid.as_byte_array(); - let owned_vec: Vec = byte_array.into(); - let txid_as_u256 = U256::Owned(owned_vec); - txids_as_u256.push(txid_as_u256); - tx_list.push(tx); + if let Err(e) = self.setup_connection().await { + handle_error(&status_sender, e).await; + return; } - let tx_ids = Seq064K::new(txids_as_u256).expect("Failed to create Seq064K"); - - // Construct the DeclareMiningJob message. - let declare_job = DeclareMiningJob { - request_id: id, - mining_job_token: token.try_into().unwrap(), - version: template.version, - coinbase_prefix: self_mutex - .safe_lock(|s| s.coinbase_tx_prefix.clone()) - .unwrap(), - coinbase_suffix: self_mutex - .safe_lock(|s| s.coinbase_tx_suffix.clone()) - .unwrap(), - tx_ids_list: tx_ids, - excess_data, // request transaction data - }; - - // Determine the associated SetNewPrevHash message. This is only relevant - // if the template is *not* a future template. - let prev_hash = self_mutex - .safe_lock(|s| s.last_set_new_prev_hash.clone()) - .unwrap() - .filter(|_| !template.future_template); - - // Store information about this declared job in the window for tracking responses. - let last_declare = LastDeclareJob { - declare_job: declare_job.clone(), - template, - prev_hash, - coinbase_pool_output, - tx_list: tx_list_.clone(), - }; - Self::update_last_declare_job_sent(self_mutex, id, last_declare); - let frame: StdFrame = - AnyMessage::JobDeclaration(JobDeclaration::DeclareMiningJob(declare_job)) - .try_into() - .unwrap(); - sender.send(frame.into()).await.unwrap(); - } - /// This method contains the core logic for processing incoming messages from the JDS. - /// - /// It runs in a background task and continuously receives messages from the JDS. - /// It dispatches these messages to the appropriate handlers defined in the - /// `ParseJobDeclarationMessagesFromUpstream` trait implementation. - /// Based on the handler's response, it may process the message further - /// (e.g., handling `DeclareMiningJobSuccess` or `DeclareMiningJobError`) or - /// send a response back to the JDS if required. - pub fn on_upstream_message(self_mutex: Arc>) { - let up = self_mutex.safe_lock(|s| s.up.clone()).unwrap(); - // Spawn the main task for receiving and processing JDS messages. - let main_task = { - let self_mutex = self_mutex.clone(); - tokio::task::spawn(async move { - let receiver = self_mutex.safe_lock(|d| d.receiver.clone()).unwrap(); + task_manager.spawn( + async move { loop { - let mut incoming: StdFrame = receiver.recv().await.unwrap().try_into().unwrap(); - let message_type = incoming.get_header().unwrap().msg_type(); - let payload = incoming.payload(); - let next_message_to_send = - ParseJobDeclarationMessagesFromUpstream::handle_message_job_declaration( - self_mutex.clone(), - message_type, - payload, - ); - // Process the result of the message handling. - match next_message_to_send { - // Handle a successful job declaration response. - Ok(SendTo::None(Some(JobDeclaration::DeclareMiningJobSuccess(m)))) => { - let new_token = m.new_mining_job_token; - // Retrieve the information about the original DeclareMiningJob using - // the request ID. - let last_declare = Self::get_last_declare_job_sent(&self_mutex, m.request_id).unwrap_or_else(|| panic!("Failed to get last declare job: job not found, Request Id: {:?}.", m.request_id)); - debug!("LastDeclareJob.prev_hash: {:?}", last_declare.prev_hash); - let mut last_declare_mining_job_sent = last_declare.declare_job; - let is_future = last_declare.template.future_template; - let id = last_declare.template.template_id; - let merkle_path = last_declare.template.merkle_path.clone(); - let template = last_declare.template; - - // TODO: Signaling mechanism needed here to inform on_set_new_prev_hash - // that the token has been updated, so it can decide whether to send - // SetCustomJobs. - if is_future { - // If it was a future job, update its mining job token and store it - // in the future_jobs map. - last_declare_mining_job_sent.mining_job_token = new_token; - self_mutex - .safe_lock(|s| { - s.future_jobs.insert( - id, - ( - last_declare_mining_job_sent, - merkle_path, - template, - last_declare.coinbase_pool_output, - ), - ); - }) - .unwrap(); - } else { - // If it was a non-future job, it should have an associated - // SetNewPrevHash. - let set_new_prev_hash = last_declare.prev_hash; - let mut template_outs = template.coinbase_tx_outputs.to_vec(); - let mut pool_outs = last_declare.coinbase_pool_output; - pool_outs.append(&mut template_outs); - match set_new_prev_hash { - // Send the SetCustomJobs message to the upstream pool. - Some(p) => Upstream::set_custom_jobs( - &up, - last_declare_mining_job_sent, - p, - merkle_path, - new_token, - template.coinbase_tx_version, - template.coinbase_prefix, - template.coinbase_tx_input_sequence, - template.coinbase_tx_value_remaining, - pool_outs, - template.coinbase_tx_locktime, - template.template_id - ).await.unwrap(), - None => panic!("Invalid state we received a NewTemplate not future, without having received a set new prev hash") + let mut self_clone_1 = self.clone(); + let self_clone_2 = self.clone(); + tokio::select! { + message = shutdown_rx.recv() => { + match message { + Ok(ShutdownMessage::ShutdownAll) => { + info!("Job Declarator: received shutdown signal."); + break; + } + Ok(ShutdownMessage::JobDeclaratorShutdownFallback(_)) => { + info!("Job Declarator: Received Job declarator shutdown."); + break; + } + Ok(ShutdownMessage::UpstreamShutdownFallback(_)) => { + info!("Job Declarator: Received Upstream shutdown."); + break; + } + Ok(ShutdownMessage::UpstreamShutdown(tx)) => { + info!("Job declarator shutdown requested"); + drop(tx); + break; } + Ok(ShutdownMessage::JobDeclaratorShutdown(tx)) => { + info!("Job declarator shutdown requested"); + drop(tx); + break; + } + Err(e) => { + warn!(error = ?e, "Job Declarator: shutdown channel closed unexpectedly"); + break; + } + _ => {} } } - Ok(SendTo::None(Some(JobDeclaration::DeclareMiningJobError(m)))) => { - error!("Job is not verified: {:?}", m); - } - Ok(SendTo::None(None)) => (), - Ok(SendTo::Respond(m)) => { - let sv2_frame: StdFrame = - AnyMessage::JobDeclaration(m).try_into().unwrap(); - let sender = - self_mutex.safe_lock(|self_| self_.sender.clone()).unwrap(); - sender.send(sv2_frame.into()).await.unwrap(); + res = self_clone_1.handle_job_declarator_message() => { + if let Err(e) = res { + error!(error = ?e, "Job Declarator message handling failed"); + handle_error(&status_sender, e).await; + break; + } } - Ok(_) => unreachable!(), - Err(_) => todo!(), + res = self_clone_2.handle_channel_manager_message() => { + if let Err(e) = res { + error!(error = ?e, "Channel Manager message handling failed"); + handle_error(&status_sender, e).await; + break; + } + }, } } - }) - }; - self_mutex - .safe_lock(|s| { - s.task_collector - .safe_lock(|c| c.push(main_task.abort_handle())) - .unwrap() - }) - .unwrap(); + drop(shutdown_complete_tx); + warn!("JobDeclarator: unified message loop exited."); + }, + ); } - /// Handles the event of a `SetNewPrevHash` being received - /// from the Template Receiver. + /// Performs SV2 setup connection handshake with Job Declarator server. /// - /// This method updates the stored last `SetNewPrevHash` if the new one is - /// more recent. It then checks if a corresponding future job is stored. - /// If a matching future job is found and is still relevant (based on the - /// `set_new_prev_hash_counter`), the future job is promoted, removed from - /// the future jobs map, and a `SetCustomJobs` message is sent to the upstream - /// pool to activate this job for mining. - pub fn on_set_new_prev_hash( - self_mutex: Arc>, - set_new_prev_hash: SetNewPrevHash<'static>, - ) { - // Spawn a task to handle the SetNewPrevHash - tokio::task::spawn(async move { - let id = set_new_prev_hash.template_id; - // Update the last_set_new_prev_hash if the new one is more recent. - let _ = self_mutex.safe_lock(|s| { - debug!("Before update - last_set_new_prev_hash: {:?}, set_new_prev_hash_counter: {}", - s.last_set_new_prev_hash, s.set_new_prev_hash_counter); - let should_update = s - .last_set_new_prev_hash - .as_ref() - .map(|prev| set_new_prev_hash.template_id > prev.template_id) - .unwrap_or(true); + /// - Sends `SetupConnection` message. + /// - Waits for and validates server response. + /// - Completes SV2 protocol handshake. + pub async fn setup_connection(&mut self) -> Result<(), JDCError> { + info!("Sending SetupConnection to JDS at {}", self.socket_address); - if should_update { - s.last_set_new_prev_hash = Some(set_new_prev_hash.clone()); - s.set_new_prev_hash_counter += 1; - debug!("After update - last_set_new_prev_hash updated to: {:?}, set_new_prev_hash_counter: {}", - s.last_set_new_prev_hash, s.set_new_prev_hash_counter); - } else { - debug!("Received outdated SetNewPrevHash: {:?} compared to current: {:?}", - set_new_prev_hash, s.last_set_new_prev_hash); - } - }); - // Loop to find and promote the corresponding future job. - let (job, up, merkle_path, template, mut pool_outs) = loop { - match self_mutex - .safe_lock(|s| { - // Check if the received SetNewPrevHash is outdated based on the counter - if s.set_new_prev_hash_counter > 1 - && s.last_set_new_prev_hash != Some(set_new_prev_hash.clone()) - { - debug!( - "Declared job {} skipped due to set_new_prev_hash_counter", - id - ); - s.set_new_prev_hash_counter -= 1; - Some(None) - } else { - // Attempt to remove and retrieve the future job matching the template - // ID. - s.future_jobs.remove(&id).map( - |(job, merkle_path, template, pool_outs)| { - s.future_jobs = - HashMap::with_hasher(BuildNoHashHasher::default()); - s.set_new_prev_hash_counter -= 1; - Some((job, s.up.clone(), merkle_path, template, pool_outs)) - }, - ) - } - }) - .unwrap() - { - Some(Some(future_job_tuple)) => break future_job_tuple, - Some(None) => return, - None => {} - }; - tokio::task::yield_now().await; - }; - // The token received from JDS for this job. - let signed_token = job.mining_job_token.clone(); - // Prepare the pool's coinbase output by appending the template's outputs. - let mut template_outs = template.coinbase_tx_outputs.to_vec(); - pool_outs.append(&mut template_outs); - // Send the SetCustomJobs message to the upstream pool to activate this job. - Upstream::set_custom_jobs( - &up, - job, - set_new_prev_hash, - merkle_path, - signed_token, - template.coinbase_tx_version, - template.coinbase_prefix, - template.coinbase_tx_input_sequence, - template.coinbase_tx_value_remaining, - pool_outs, - template.coinbase_tx_locktime, - template.template_id, - ) + let setup_connection = get_setup_connection_message_jds(&self.socket_address, &self.mode); + let sv2_frame: StdFrame = Message::Common(setup_connection.into()) + .try_into() + .map_err(|e| { + error!(error=?e, "Failed to serialize SetupConnection message."); + JDCError::CodecNoise(codec_sv2::noise_sv2::Error::ExpectedIncomingHandshakeMessage) + })?; + + if let Err(e) = self.job_declarator_channel.jds_sender.send(sv2_frame).await { + error!(error=?e, "Failed to send SetupConnection frame."); + return Err(JDCError::ChannelErrorSender); + } + debug!("SetupConnection frame sent successfully."); + + let mut incoming = self + .job_declarator_channel + .jds_receiver + .recv() .await - .unwrap(); - }); + .map_err(|e| { + error!(error=?e, "No handshake response received from Job declarator."); + JDCError::CodecNoise(codec_sv2::noise_sv2::Error::ExpectedIncomingHandshakeMessage) + })?; + + let message_type = incoming + .get_header() + .ok_or_else(|| { + error!("Handshake frame missing header."); + framing_sv2::Error::ExpectedHandshakeFrame + })? + .msg_type(); + + debug!(?message_type, "Processing handshake response."); + + self.handle_common_message_frame_from_server(None, message_type, incoming.payload()) + .await?; + + info!("Job declarator: SV2 handshake completed successfully."); + Ok(()) } - /// Sends `AllocateMiningJobToken` messages to the JDS to request new mining job tokens. - /// - /// This method is typically called when the internal pool of allocated tokens - /// is low, ensuring that tokens are available for future job declarations. - async fn allocate_tokens(self_mutex: &Arc>, token_to_allocate: u32) { - for i in 0..token_to_allocate { - let message = JobDeclaration::AllocateMiningJobToken(AllocateMiningJobToken { - user_identifier: "todo".to_string().try_into().unwrap(), - request_id: i, - }); - let sender = self_mutex.safe_lock(|s| s.sender.clone()).unwrap(); - // Safe unwrap message is build above and is valid, below can never panic - let frame: StdFrame = AnyMessage::JobDeclaration(message).try_into().unwrap(); - // TODO join re - sender.send(frame.into()).await.unwrap(); + // Handles messages coming from the Channel Manager and forwards them to the Job Declarator. + async fn handle_channel_manager_message(&self) -> Result<(), JDCError> { + match self + .job_declarator_channel + .channel_manager_receiver + .recv() + .await + { + Ok(msg) => { + debug!("Forwarding message from channel manager to JDS."); + self.job_declarator_channel + .jds_sender + .send(msg) + .await + .map_err(|e| { + error!("Failed to send message to outbound channel: {:?}", e); + JDCError::ChannelErrorSender + })?; + } + Err(e) => { + warn!("Channel manager receiver closed or errored: {:?}", e); + } } + Ok(()) } - /// Handles the event of a miner solution being received from the downstream. - /// - /// This method constructs a `PushSolution` message containing the necessary - /// solution details and sends it to the JDS. - pub async fn on_solution( - self_mutex: &Arc>, - solution: SubmitSharesExtended<'static>, - ) { - // Retrieve the last received SetNewPrevHash message. - let prev_hash = self_mutex - .safe_lock(|s| s.last_set_new_prev_hash.clone()) - .unwrap() - .expect(""); + // Handles messages received from the Job Declarator. + // + // - Forwards `JobDeclaration` messages to Channel Manager. + // - Processes `Common` messages via handler. + // - Rejects unsupported message types. + async fn handle_job_declarator_message(&mut self) -> Result<(), JDCError> { + let mut sv2_frame = self.job_declarator_channel.jds_receiver.recv().await?; - // Construct the PushSolution message using details from the solution and the last prev - // hash - let solution = PushSolution { - extranonce: solution.extranonce, - prev_hash: prev_hash.prev_hash, - ntime: solution.ntime, - nonce: solution.nonce, - nbits: prev_hash.n_bits, - version: solution.version, + debug!("Received SV2 frame from JDS."); + let Some(message_type) = sv2_frame.get_header().map(|m| m.msg_type()) else { + return Ok(()); }; - let frame: StdFrame = AnyMessage::JobDeclaration(JobDeclaration::PushSolution(solution)) - .try_into() - .unwrap(); - let sender = self_mutex.safe_lock(|s| s.sender.clone()).unwrap(); - sender.send(frame.into()).await.unwrap(); + + match protocol_message_type(message_type) { + MessageType::Common => { + info!(?message_type, "Handling common message from Upstream."); + self.handle_common_message_frame_from_server( + None, + message_type, + sv2_frame.payload(), + ) + .await?; + } + MessageType::JobDeclaration => { + self.job_declarator_channel + .channel_manager_sender + .send(sv2_frame) + .await + .map_err(|e| { + error!(error=?e, "Failed to send Job declaration message to channel manager."); + JDCError::ChannelErrorSender + })?; + } + _ => { + warn!("Received unsupported message type from Job declarator: {message_type}"); + } + } + + Ok(()) } } diff --git a/roles/jd-client/src/lib/job_declarator/setup_connection.rs b/roles/jd-client/src/lib/job_declarator/setup_connection.rs deleted file mode 100644 index a696639e67..0000000000 --- a/roles/jd-client/src/lib/job_declarator/setup_connection.rs +++ /dev/null @@ -1,119 +0,0 @@ -//! Job Declarator: Setup Connection Handler Module -//! -//! Handles the logic for setting up a connection with an upstream Job Declarator (JDS). -//! -//! This includes building and sending a `SetupConnection` message, receiving the response, -//! and handling common SV2 connection-related messages. - -use async_channel::{Receiver, Sender}; -use codec_sv2::{StandardEitherFrame, StandardSv2Frame}; -use roles_logic_sv2::{ - common_messages_sv2::{Protocol, Reconnect, SetupConnection}, - handlers::common::{ParseCommonMessagesFromUpstream, SendTo}, - parsers::AnyMessage, - utils::Mutex, - Error, -}; -use std::{convert::TryInto, net::SocketAddr, sync::Arc}; -use tracing::info; - -pub type Message = AnyMessage<'static>; -pub type StdFrame = StandardSv2Frame; -pub type EitherFrame = StandardEitherFrame; - -/// Manages the process of sending and handling the `SetupConnection` handshake -/// for establishing a connection with a JDS. -pub struct SetupConnectionHandler {} - -impl SetupConnectionHandler { - // Builds a `SetupConnection` message using the given proxy address. - fn get_setup_connection_message(proxy_address: SocketAddr) -> SetupConnection<'static> { - let endpoint_host = proxy_address - .ip() - .to_string() - .into_bytes() - .try_into() - .unwrap(); - let vendor = String::new().try_into().unwrap(); - let hardware_version = String::new().try_into().unwrap(); - let firmware = String::new().try_into().unwrap(); - let device_id = String::new().try_into().unwrap(); - let mut setup_connection = SetupConnection { - protocol: Protocol::JobDeclarationProtocol, - min_version: 2, - max_version: 2, - flags: 0b0000_0000_0000_0000_0000_0000_0000_0000, - endpoint_host, - endpoint_port: proxy_address.port(), - vendor, - hardware_version, - firmware, - device_id, - }; - setup_connection.allow_full_template_mode(); - setup_connection - } - - /// This method sets up a job declarator connection. - pub async fn setup( - receiver: &mut Receiver, - sender: &mut Sender, - proxy_address: SocketAddr, - ) -> Result<(), ()> { - let setup_connection = Self::get_setup_connection_message(proxy_address); - - let sv2_frame: StdFrame = AnyMessage::Common(setup_connection.into()) - .try_into() - .unwrap(); - let sv2_frame = sv2_frame.into(); - - sender.send(sv2_frame).await.map_err(|_| ())?; - - let mut incoming: StdFrame = receiver.recv().await.unwrap().try_into().unwrap(); - - let message_type = incoming.get_header().unwrap().msg_type(); - let payload = incoming.payload(); - ParseCommonMessagesFromUpstream::handle_message_common( - Arc::new(Mutex::new(SetupConnectionHandler {})), - message_type, - payload, - ) - .unwrap(); - Ok(()) - } -} - -impl ParseCommonMessagesFromUpstream for SetupConnectionHandler { - // Handles a `SetupConnectionSuccess` message received from the JDS. - // - // Returns `Ok(SendTo::None(None))` indicating that no immediate message needs - // to be sent back to the JDS as a direct response to `SetupConnectionSuccess`. - fn handle_setup_connection_success( - &mut self, - m: roles_logic_sv2::common_messages_sv2::SetupConnectionSuccess, - ) -> Result { - info!( - "Received `SetupConnectionSuccess` from JDS: version={}, flags={:b}", - m.used_version, m.flags - ); - Ok(SendTo::None(None)) - } - - fn handle_setup_connection_error( - &mut self, - _: roles_logic_sv2::common_messages_sv2::SetupConnectionError, - ) -> Result { - todo!() - } - - fn handle_channel_endpoint_changed( - &mut self, - _: roles_logic_sv2::common_messages_sv2::ChannelEndpointChanged, - ) -> Result { - todo!() - } - - fn handle_reconnect(&mut self, _m: Reconnect) -> Result { - todo!() - } -} diff --git a/roles/jd-client/src/lib/mod.rs b/roles/jd-client/src/lib/mod.rs index fefdf85397..d7147e25e8 100644 --- a/roles/jd-client/src/lib/mod.rs +++ b/roles/jd-client/src/lib/mod.rs @@ -1,469 +1,474 @@ -//! ## Job Declarator Client -//! -//! The `JobDeclaratorClient` is a miner-side role responsible for: -//! - Creating new mining jobs from templates received via a Template Provider. -//! - Declaring custom jobs to a remote Job Declarator Server (JDS). -//! - Handling pool fallback by switching to backup pools or entering solo mining mode if needed. +use std::{net::SocketAddr, sync::Arc, time::Duration}; + +use async_channel::{unbounded, Receiver, Sender}; +use key_utils::Secp256k1PublicKey; +use stratum_common::roles_logic_sv2::bitcoin::consensus::Encodable; +use tokio::sync::{broadcast, mpsc}; +use tracing::{debug, info, warn}; + +use crate::{ + channel_manager::ChannelManager, + config::{ConfigJDCMode, JobDeclaratorClientConfig}, + error::JDCError, + jd_mode::{set_jd_mode, JdMode}, + job_declarator::JobDeclarator, + status::{State, Status}, + task_manager::TaskManager, + template_receiver::TemplateReceiver, + upstream::Upstream, + utils::{SV2Frame, ShutdownMessage, UpstreamState}, +}; +mod channel_manager; pub mod config; -pub mod downstream; +mod downstream; pub mod error; -pub mod job_declarator; -pub mod status; -pub mod template_receiver; -pub mod upstream_sv2; - -use std::{sync::atomic::AtomicBool, time::Duration}; - -use async_channel::unbounded; -use config::JobDeclaratorClientConfig; -use futures::{select, FutureExt}; -use job_declarator::JobDeclarator; -use roles_logic_sv2::utils::Mutex; -use std::{ - net::{IpAddr, SocketAddr}, - str::FromStr, - sync::Arc, -}; -use tokio::{sync::Notify, task::AbortHandle}; - -use tracing::{error, info}; - -/// Is used by the template receiver and the downstream. When a NewTemplate is received the context -/// that is running the template receiver set this value to false and then the message is sent to -/// the context that is running the Downstream that do something and then set it back to true. -/// -/// In the meantime if the context that is running the template receiver receives a SetNewPrevHash -/// it wait until the value of this global is true before doing anything. -/// -/// Acquire and Release memory ordering is used. -/// -/// Memory Ordering Explanation: -/// We use Acquire-Release ordering instead of SeqCst or Relaxed for the following reasons: -/// 1. Acquire in template receiver context ensures we see all operations before the Release store -/// the downstream. -/// 2. Within the same execution context (template receiver), a Relaxed store followed by an Acquire -/// load is sufficient. This is because operations within the same context execute in the order -/// they appear in the code. -/// 3. The combination of Release in downstream and Acquire in template receiver contexts -/// establishes a happens-before relationship, guaranteeing that we handle the SetNewPrevHash -/// message after that downstream have finished handling the NewTemplate. -/// 3. SeqCst is overkill we only need to synchronize two contexts, a globally agreed-upon order -/// between all the contexts is not necessary. -pub static IS_NEW_TEMPLATE_HANDLED: AtomicBool = AtomicBool::new(true); - -/// Job Declarator Client (or JDC) is the role which is Miner-side, in charge of creating new -/// mining jobs from the templates received by the Template Provider to which it is connected. It -/// declares custom jobs to the JDS, in order to start working on them. -/// JDC is also responsible for putting in action the Pool-fallback mechanism, automatically -/// switching to backup Pools in case of declared custom jobs refused by JDS (which is Pool side). -/// As a solution of last-resort, it is able to switch to Solo Mining until new safe Pools appear -/// in the market. -#[derive(Debug, Clone)] +pub mod jd_mode; +mod job_declarator; +mod status; +mod task_manager; +mod template_receiver; +mod upstream; +pub mod utils; + +/// Represent Job Declarator Client +#[derive(Clone)] pub struct JobDeclaratorClient { - // Configuration of the [`JobDeclaratorClient`]. config: JobDeclaratorClientConfig, - // Used for notifying the [`JobDeclaratorClient`] to shutdown gracefully. - shutdown: Arc, + notify_shutdown: broadcast::Sender, } impl JobDeclaratorClient { - /// Instantiate a new `JobDeclaratorClient` instance. + /// Creates a new [`JobDeclaratorClient`] instance. pub fn new(config: JobDeclaratorClientConfig) -> Self { + let (notify_shutdown, _) = tokio::sync::broadcast::channel::(100); Self { config, - shutdown: Arc::new(Notify::new()), - } - } - - /// Starts the main operational loop of the Job Declarator Client. - /// - /// This involves connecting to configured upstream pools (or entering solo mining mode), - /// setting up the Job Declarator Server (JDS) connection, listening for downstream connections, - /// and managing the template receiving process. - /// - /// The method handles automatic pool fallback in case of disconnection or detected - /// rogue behavior from the current upstream pool. It also manages graceful shutdown - /// upon receiving a termination signal (e.g., CTRL+C) or encountering internal errors. - /// - /// Subsystems are spawned sequentially with dependencies: Pool → JDS → Downstream → Template - /// Receiver (implicitly handled within Downstream or other components). - pub async fn start(self) { - let mut upstream_index = 0; - - // Channel used to manage failed tasks - let (tx_status, rx_status) = unbounded(); - - let task_collector = Arc::new(Mutex::new(vec![])); - - // Spawn a task to listen for the CTRL+C signal for graceful shutdown. - tokio::spawn({ - let shutdown_signal = self.shutdown.clone(); - async move { - if tokio::signal::ctrl_c().await.is_ok() { - info!("Interrupt received"); - shutdown_signal.notify_one(); - } - } - }); - - let config = self.config; - 'outer: loop { - let task_collector = task_collector.clone(); - let tx_status = tx_status.clone(); - let config = config.clone(); - let shutdown = self.shutdown.clone(); - let root_handler; - - // Check if there is a configured upstream pool and jds at the current index. - if let Some(upstream) = config.upstreams().get(upstream_index) { - let tx_status = tx_status.clone(); - let task_collector = task_collector.clone(); - let upstream = upstream.clone(); - // Spawn the initialization process for connecting to a pool. - root_handler = tokio::spawn(async move { - Self::initialize_jd(config, tx_status, task_collector, upstream, shutdown) - .await; - }); - } else { - // If no more upstream pools are configured, enter solo mining mode. - let tx_status: async_channel::Sender> = tx_status.clone(); - let task_collector = task_collector.clone(); - root_handler = tokio::spawn(async move { - Self::initialize_jd_as_solo_miner( - config, - tx_status.clone(), - task_collector.clone(), - shutdown, - ) - .await; - }); - } - - // Inner loop to monitor the status of the root handler and spawned tasks. - loop { - select! { - task_status = rx_status.recv().fuse() => { - if let Ok(task_status) = task_status { - match task_status.state { - // Should only be sent by the downstream listener - status::State::DownstreamShutdown(err) => { - error!("SHUTDOWN from: {}", err); - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - task_collector - .safe_lock(|s| { - for handle in s { - handle.abort(); - } - }) - .unwrap(); - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - break; - } - status::State::UpstreamShutdown(err) => { - error!("SHUTDOWN from: {}", err); - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - task_collector - .safe_lock(|s| { - for handle in s { - handle.abort(); - } - }) - .unwrap(); - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - break; - } - status::State::UpstreamRogue => { - error!("Changing Pool"); - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - task_collector - .safe_lock(|s| { - for handle in s { - handle.abort(); - } - }) - .unwrap(); - upstream_index += 1; - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - break; - } - status::State::Healthy(msg) => { - info!("HEALTHY message: {}", msg); - } - } - } else { - info!("Received unknown task. Shutting down."); - task_collector - .safe_lock(|s| { - for handle in s { - handle.abort(); - } - }) - .unwrap(); - root_handler.abort(); - break 'outer; - } - }, - _ = self.shutdown.notified().fuse() => { - info!("Shutting down gracefully..."); - task_collector - .safe_lock(|s| { - for handle in s { - handle.abort(); - } - }) - .unwrap(); - root_handler.abort(); - break 'outer; - } - }; - } + notify_shutdown, } } - // Initializes the Job Declarator Client to operate in solo mining mode. - // - // This function is called when no upstream pools are configured or available. - // In solo mining mode, the JDC will generate its own mining jobs rather than - // receiving them from a pool. It primarily sets up the downstream listener - // to provide these solo mining jobs to connected miners. - async fn initialize_jd_as_solo_miner( - config: JobDeclaratorClientConfig, - tx_status: async_channel::Sender>, - task_collector: Arc>>, - shutdown: Arc, - ) { - let miner_tx_out = config.get_txout().expect("Failed to get txout"); - - // Spawn the downstream listener task. In solo mode, `upstream` and `jd` are `None`. - let downstream_handle = tokio::spawn(downstream::listen_for_downstream_mining( - *config.listening_address(), - None, - config.withhold(), - *config.authority_public_key(), - *config.authority_secret_key(), - config.cert_validity_sec(), - task_collector.clone(), - tx_status.clone(), - miner_tx_out.clone(), - None, - config.clone(), - shutdown, - config.jdc_signature().to_string(), - )); - let _ = task_collector.safe_lock(|e| { - e.push(downstream_handle.abort_handle()); - }); - } - - /// Initializes the Job Declarator Client by connecting to a configured upstream Pool - /// and setting up the associated downstream listener and Job Declarator. - /// - /// This function is called when there is an available upstream pool in the configuration. - /// It handles the connection to the SV2 upstream, sets up the Job Declarator for - /// communication with the pool's JDS, and starts the downstream listener to relay - /// jobs from the pool to the miner. - async fn initialize_jd( - config: JobDeclaratorClientConfig, - tx_status: async_channel::Sender>, - task_collector: Arc>>, - upstream_config: config::Upstream, - shutdown: Arc, - ) { - let timeout = config.timeout(); - - // Parse and format the upstream pool connection address. - let mut parts = upstream_config.pool_address.split(':'); - let address = parts - .next() - .unwrap_or_else(|| panic!("Invalid pool address {}", upstream_config.pool_address)); - let port = parts - .next() - .and_then(|p| p.parse::().ok()) - .unwrap_or_else(|| panic!("Invalid pool address {}", upstream_config.pool_address)); - let upstream_addr = SocketAddr::new( - IpAddr::from_str(address).unwrap_or_else(|_| { - panic!("Invalid pool address {}", upstream_config.pool_address) - }), - port, + /// Starts the Job Declarator Client (JDC) main loop. + pub async fn start(&self) { + info!( + "Job declarator client starting... setting up subsystems, User Identity: {}", + self.config.user_identity() ); - // Instantiate and connect to the SV2 Upstream (Pool). - let upstream = match upstream_sv2::Upstream::new( - upstream_addr, - upstream_config.authority_pubkey, - status::Sender::Upstream(tx_status.clone()), - task_collector.clone(), - Arc::new(Mutex::new(PoolChangerTrigger::new(timeout))), - config.jdc_signature().to_string(), + let miner_coinbase_outputs = vec![self.config.get_txout()]; + let mut encoded_outputs = vec![]; + + miner_coinbase_outputs + .consensus_encode(&mut encoded_outputs) + .expect("Invalid coinbase output in config"); + + let notify_shutdown = self.notify_shutdown.clone(); + let (shutdown_complete_tx, mut shutdown_complete_rx) = mpsc::channel::<()>(1); + let task_manager = Arc::new(TaskManager::new()); + + let (status_sender, status_receiver) = async_channel::unbounded::(); + + let (channel_manager_to_upstream_sender, channel_manager_to_upstream_receiver) = + unbounded::(); + let (upstream_to_channel_manager_sender, upstream_to_channel_manager_receiver) = + unbounded::(); + + let (channel_manager_to_jd_sender, channel_manager_to_jd_receiver) = + unbounded::(); + let (jd_to_channel_manager_sender, jd_to_channel_manager_receiver) = + unbounded::(); + + let (channel_manager_to_downstream_sender, _channel_manager_to_downstream_receiver) = + broadcast::channel(10); + let (downstream_to_channel_manager_sender, downstream_to_channel_manager_receiver) = + unbounded(); + + let (channel_manager_to_tp_sender, channel_manager_to_tp_receiver) = + unbounded::(); + let (tp_to_channel_manager_sender, tp_to_channel_manager_receiver) = + unbounded::(); + + debug!("Channels initialized."); + + let channel_manager = ChannelManager::new( + self.config.clone(), + channel_manager_to_upstream_sender.clone(), + upstream_to_channel_manager_receiver.clone(), + channel_manager_to_jd_sender.clone(), + jd_to_channel_manager_receiver.clone(), + channel_manager_to_tp_sender.clone(), + tp_to_channel_manager_receiver.clone(), + channel_manager_to_downstream_sender.clone(), + downstream_to_channel_manager_receiver, + status_sender.clone(), + encoded_outputs.clone(), ) .await - { - Ok(upstream) => upstream, - Err(e) => { - error!("Failed to create upstream: {}", e); - panic!() - } - }; - - // Set up the SV2 connection with the upstream pool. - match upstream_sv2::Upstream::setup_connection( - upstream.clone(), - config.min_supported_version(), - config.max_supported_version(), + .unwrap(); + + let channel_manager_clone = channel_manager.clone(); + + // Initialize the template Receiver + let tp_address = self.config.tp_address().to_string(); + let tp_pubkey = self.config.tp_authority_public_key().copied(); + + let template_receiver = TemplateReceiver::new( + tp_address.clone(), + tp_pubkey, + channel_manager_to_tp_receiver, + tp_to_channel_manager_sender, + notify_shutdown.clone(), + task_manager.clone(), + status_sender.clone(), ) .await + .unwrap(); + + info!("Template provider setup done"); + + let notify_shutdown_cl = notify_shutdown.clone(); + let status_sender_cl = status_sender.clone(); + let task_manager_cl = task_manager.clone(); + + template_receiver + .start( + tp_address, + notify_shutdown_cl, + status_sender_cl, + task_manager_cl, + encoded_outputs.clone(), + ) + .await; + + let mut upstream_addresses: Vec<_> = self + .config + .upstreams() + .iter() + .map(|u| { + let pool_addr = SocketAddr::new( + u.pool_address.parse().expect("Invalid pool address"), + u.pool_port, + ); + let jd_addr = SocketAddr::new( + u.jds_address.parse().expect("Invalid JD address"), + u.jds_port, + ); + (pool_addr, jd_addr, u.authority_pubkey, false) + }) + .collect(); + + channel_manager + .start( + notify_shutdown.clone(), + status_sender.clone(), + task_manager.clone(), + ) + .await; + + info!("Attempting to initialize upstream..."); + + match self + .initialize_jd( + &mut upstream_addresses, + channel_manager_to_upstream_receiver.clone(), + upstream_to_channel_manager_sender.clone(), + channel_manager_to_jd_receiver.clone(), + jd_to_channel_manager_sender.clone(), + notify_shutdown.clone(), + status_sender.clone(), + self.config.mode.clone(), + task_manager.clone(), + ) + .await { - Ok(_) => info!("Connected to Upstream!"), - Err(e) => { - error!("Failed to connect to Upstream EXITING! : {}", e); - panic!() - } - } + Ok((upstream, job_declarator)) => { + upstream + .start( + self.config.min_supported_version(), + self.config.max_supported_version(), + notify_shutdown.clone(), + shutdown_complete_tx.clone(), + status_sender.clone(), + task_manager.clone(), + ) + .await; - // Start the task to receive and parse incoming messages from the SV2 upstream. - if let Err(e) = upstream_sv2::Upstream::parse_incoming(upstream.clone()) { - error!("failed to create sv2 parser: {}", e); - panic!() - } + job_declarator + .start( + notify_shutdown.clone(), + shutdown_complete_tx, + status_sender.clone(), + task_manager.clone(), + ) + .await; - // Parse and format the Job Declarator Server (JDS) address for this pool. - let mut parts = upstream_config.jd_address.split(':'); - let ip_jd = parts.next().unwrap().to_string(); - let port_jd = parts.next().unwrap().parse::().unwrap(); - - // Instantiate the Job Declarator component. - let jd = match JobDeclarator::new( - SocketAddr::new(IpAddr::from_str(ip_jd.as_str()).unwrap(), port_jd), - upstream_config.authority_pubkey.into_bytes(), - config.clone(), - upstream.clone(), - task_collector.clone(), - ) - .await - { - Ok(c) => c, + channel_manager_clone + .upstream_state + .set(UpstreamState::NoChannel); + _ = channel_manager_clone.allocate_tokens(1).await; + } Err(e) => { - let _ = tx_status - .send(status::Status { - state: status::State::UpstreamShutdown(e), - }) - .await; - return; + tracing::error!("Failed to initialize upstream: {:?}", e); + set_jd_mode(jd_mode::JdMode::SoloMining); } }; - // Spawn the downstream listener task, providing the upstream and JobDeclarator instances. - let downstream_handle = tokio::spawn(downstream::listen_for_downstream_mining( - *config.listening_address(), - Some(upstream), - config.withhold(), - *config.authority_public_key(), - *config.authority_secret_key(), - config.cert_validity_sec(), - task_collector.clone(), - tx_status.clone(), - vec![], - Some(jd), - config.clone(), - shutdown, - config.jdc_signature().to_string(), - )); - let _ = task_collector.safe_lock(|e| { - e.push(downstream_handle.abort_handle()); - }); - } + _ = channel_manager_clone + .clone() + .start_downstream_server( + *self.config.authority_public_key(), + *self.config.authority_secret_key(), + self.config.cert_validity_sec(), + *self.config.listening_address(), + task_manager.clone(), + notify_shutdown.clone(), + status_sender.clone(), + downstream_to_channel_manager_sender.clone(), + channel_manager_to_downstream_sender.clone(), + ) + .await; + + info!("Spawning status listener task..."); + let notify_shutdown_clone = notify_shutdown.clone(); + + loop { + tokio::select! { + _ = tokio::signal::ctrl_c() => { + info!("Ctrl+C received — initiating graceful shutdown..."); + let _ = notify_shutdown_clone.send(ShutdownMessage::ShutdownAll); + break; + } + message = status_receiver.recv() => { + if let Ok(status) = message { + match status.state { + State::DownstreamShutdown{downstream_id,..} => { + warn!("Downstream {downstream_id:?} disconnected — Channel manager."); + let _ = notify_shutdown_clone.send(ShutdownMessage::DownstreamShutdown(downstream_id)); + } + State::TemplateReceiverShutdown(_) => { + warn!("Template Receiver shutdown requested — initiating full shutdown."); + let _ = notify_shutdown_clone.send(ShutdownMessage::ShutdownAll); + break; + } + State::ChannelManagerShutdown(_) => { + warn!("Channel Manager shutdown requested — initiating full shutdown."); + let _ = notify_shutdown_clone.send(ShutdownMessage::ShutdownAll); + break; + } + State::UpstreamShutdownFallback(_) | State::JobDeclaratorShutdownFallback(_) => { + warn!("Upstream/Job Declarator connection dropped — attempting reconnection..."); + let (tx, mut rx) = mpsc::channel::<()>(1); + let _ = notify_shutdown_clone.send(ShutdownMessage::UpstreamShutdownFallback((encoded_outputs.clone(), tx))); + set_jd_mode(JdMode::SoloMining); + shutdown_complete_rx.recv().await; + tracing::error!("Existing Upstream or JD instance taken out"); + rx.recv().await; + tracing::error!("All entities acknowledged Upstream fallback. Preparing fallback."); + + let (shutdown_complete_tx_fallback, shutdown_complete_rx_fallback) = mpsc::channel::<()>(1); + + shutdown_complete_rx = shutdown_complete_rx_fallback; + + info!("Attempting to initialize Jd and upstream..."); + + match self + .initialize_jd( + &mut upstream_addresses, + channel_manager_to_upstream_receiver.clone(), + upstream_to_channel_manager_sender.clone(), + channel_manager_to_jd_receiver.clone(), + jd_to_channel_manager_sender.clone(), + notify_shutdown.clone(), + status_sender.clone(), + self.config.mode.clone(), + task_manager.clone(), + ) + .await + { + Ok((upstream, job_declarator)) => { + upstream + .start( + self.config.min_supported_version(), + self.config.max_supported_version(), + notify_shutdown.clone(), + shutdown_complete_tx_fallback.clone(), + status_sender.clone(), + task_manager.clone(), + ) + .await; + + job_declarator + .start( + notify_shutdown.clone(), + shutdown_complete_tx_fallback, + status_sender.clone(), + task_manager.clone(), + ) + .await; + + channel_manager_clone.upstream_state.set(UpstreamState::NoChannel); + + _ = channel_manager_clone.allocate_tokens(1).await; + } + Err(e) => { + tracing::error!("Failed to initialize upstream: {:?}", e); + channel_manager_clone.upstream_state.set(UpstreamState::SoloMining); + set_jd_mode(jd_mode::JdMode::SoloMining); + info!("Fallback to solo mining mode"); + } + }; + + _ = channel_manager_clone.clone() + .start_downstream_server( + *self.config.authority_public_key(), + *self.config.authority_secret_key(), + self.config.cert_validity_sec(), + *self.config.listening_address(), + task_manager.clone(), + notify_shutdown.clone(), + status_sender.clone(), + downstream_to_channel_manager_sender.clone(), + channel_manager_to_downstream_sender.clone(), + ) + .await; + } + } + } + } + } + } - /// Closes JDC role and any open connection associated with it. - /// - /// Note that this method will result in a full exit of the running - /// jd-client and any open connection most be re-initiated upon new - /// start. - #[allow(dead_code)] - pub fn shutdown(&self) { - self.shutdown.notify_one(); - } -} + warn!("Graceful shutdown"); + task_manager.abort_all().await; -/// A trigger mechanism to detect if an upstream pool is unresponsive and initiate a pool change. -#[derive(Debug)] -pub struct PoolChangerTrigger { - // The timeout duration after which the upstream is considered rogue if no activity is - // detected. - timeout: Duration, - // The handle for the spawned task that monitors the timeout. - task: Option>, -} - -impl PoolChangerTrigger { - /// Creates a new `PoolChangerTrigger` instance. - pub fn new(timeout: Duration) -> Self { - Self { - timeout, - task: None, - } + info!("Joining remaining tasks..."); + task_manager.join_all().await; + info!("JD Client shutdown complete."); } - /// Starts the pool changer trigger. - /// - /// This spawns a task that will wait for the configured timeout. - /// If the timeout is reached before `stop` is called, it sends an `UpstreamRogue` - /// status message to the provided sender, triggering a pool change in the main JDC loop. - pub fn start(&mut self, sender: status::Sender) { - let timeout = self.timeout; - let task = tokio::task::spawn(async move { - tokio::time::sleep(timeout).await; - let _ = sender - .send(status::Status { - state: status::State::UpstreamRogue, - }) - .await; - }); - self.task = Some(task); - } + /// Initializes an upstream pool + JD connection pair. + #[allow(clippy::too_many_arguments)] + pub async fn initialize_jd( + &self, + upstreams: &mut [(SocketAddr, SocketAddr, Secp256k1PublicKey, bool)], + channel_manager_to_upstream_receiver: Receiver, + upstream_to_channel_manager_sender: Sender, + channel_manager_to_jd_receiver: Receiver, + jd_to_channel_manager_sender: Sender, + notify_shutdown: broadcast::Sender, + status_sender: Sender, + mode: ConfigJDCMode, + task_manager: Arc, + ) -> Result<(Upstream, JobDeclarator), JDCError> { + const MAX_RETRIES: usize = 3; + let upstream_len = upstreams.len(); + for (i, upstream_addr) in upstreams.iter_mut().enumerate() { + info!( + "Trying upstream {} of {}: {:?}", + i + 1, + upstream_len, + upstream_addr + ); + + tokio::time::sleep(Duration::from_secs(1)).await; + + if upstream_addr.3 { + info!( + "Upstream previously marked as malicious, skipping initial attempt warnings." + ); + continue; + } - /// Stops the pool changer trigger. - pub fn stop(&mut self) { - if let Some(task) = self.task.take() { - task.abort(); + for attempt in 1..=MAX_RETRIES { + info!("Connection attempt {}/{}...", attempt, MAX_RETRIES); + + match try_initialize_single( + upstream_addr, + upstream_to_channel_manager_sender.clone(), + channel_manager_to_upstream_receiver.clone(), + jd_to_channel_manager_sender.clone(), + channel_manager_to_jd_receiver.clone(), + notify_shutdown.clone(), + status_sender.clone(), + mode.clone(), + task_manager.clone(), + ) + .await + { + Ok(pair) => { + upstream_addr.3 = true; + return Ok(pair); + } + Err(e) => { + let (tx, mut rx) = mpsc::channel::<()>(1); + let _ = notify_shutdown.send(ShutdownMessage::JobDeclaratorShutdown(tx)); + rx.recv().await; + tracing::error!("All sparsed upstream and JDS connection is be terminated"); + tokio::time::sleep(Duration::from_secs(1)).await; + warn!( + "Attempt {}/{} failed for {:?}: {:?}", + attempt, MAX_RETRIES, upstream_addr, e + ); + if attempt == MAX_RETRIES { + warn!( + "Max retries reached for {:?}, moving to next upstream", + upstream_addr + ); + } + } + } + } + upstream_addr.3 = true; } + + tracing::error!("All upstreams failed after {} retries each", MAX_RETRIES); + Err(JDCError::Shutdown) } } -#[cfg(test)] -mod tests { - use ext_config::{Config, File, FileFormat}; - - use crate::*; +// Attempts to initialize a single upstream (pool + JDS pair). +#[allow(clippy::too_many_arguments)] +async fn try_initialize_single( + upstream_addr: &(SocketAddr, SocketAddr, Secp256k1PublicKey, bool), + upstream_to_channel_manager_sender: Sender, + channel_manager_to_upstream_receiver: Receiver, + jd_to_channel_manager_sender: Sender, + channel_manager_to_jd_receiver: Receiver, + notify_shutdown: broadcast::Sender, + status_sender: Sender, + mode: ConfigJDCMode, + task_manager: Arc, +) -> Result<(Upstream, JobDeclarator), JDCError> { + info!("Upstream connection in-progress at initialize single"); + let upstream = Upstream::new( + upstream_addr, + upstream_to_channel_manager_sender, + channel_manager_to_upstream_receiver, + notify_shutdown.clone(), + task_manager.clone(), + status_sender.clone(), + ) + .await?; + + info!("Upstream connection done at initialize single"); + + let job_declarator = JobDeclarator::new( + upstream_addr, + jd_to_channel_manager_sender, + channel_manager_to_jd_receiver, + notify_shutdown, + mode, + task_manager.clone(), + status_sender.clone(), + ) + .await?; + + Ok((upstream, job_declarator)) +} - #[tokio::test] - async fn test_shutdown() { - let config_path = "config-examples/jdc-config-hosted-example.toml"; - let config: JobDeclaratorClientConfig = match Config::builder() - .add_source(File::new(config_path, FileFormat::Toml)) - .build() - { - Ok(settings) => match settings.try_deserialize::() { - Ok(c) => c, - Err(e) => { - dbg!(&e); - return; - } - }, - Err(e) => { - dbg!(&e); - return; - } - }; - let jdc = JobDeclaratorClient::new(config.clone()); - let cloned = jdc.clone(); - tokio::spawn(async move { - cloned.start().await; - }); - jdc.shutdown(); - let ip = config.listening_address().ip(); - let port = config.listening_address().port(); - let jdc_addr = format!("{}:{}", ip, port); - assert!(std::net::TcpListener::bind(jdc_addr).is_ok()); +impl Drop for JobDeclaratorClient { + fn drop(&mut self) { + info!("JobDeclaratorClient dropped"); + let _ = self.notify_shutdown.send(ShutdownMessage::ShutdownAll); } } diff --git a/roles/jd-client/src/lib/status.rs b/roles/jd-client/src/lib/status.rs index f6e16bd577..feea1d2015 100644 --- a/roles/jd-client/src/lib/status.rs +++ b/roles/jd-client/src/lib/status.rs @@ -1,167 +1,154 @@ -//! ## Status Reporting System for JDC +//! Status reporting and error propagation Utility. //! -//! This module defines how internal components of the Job Declarator Client (JDC) report -//! health, errors, and shutdown conditions back to the main runtime loop in `lib/mod.rs`. -//! -//! At the core, tasks send a [`Status`] (wrapping a [`State`]) through a channel, -//! which is tagged with a [`Sender`] enum to indicate the origin of the message. -//! -//! This allows for centralized, consistent error handling across the application. +//! This module provides mechanisms for communicating shutdown events and +//! component state changes across the system. Each component (downstream, +//! upstream, job declarator, template receiver, channel manager) can send +//! and receive status updates via typed channels. Errors are automatically +//! converted into shutdown signals, allowing coordinated teardown of tasks. -use super::error::{self, Error}; +use tracing::{debug, error, warn}; -/// Identifies the component that originated a [`Status`] update. -/// -/// Each sender is associated with a dedicated side of the status channel. -/// This lets the central loop distinguish between errors from different parts of the system. -#[derive(Debug)] -pub enum Sender { - /// Downstream task (e.g. per-client connection handler) - Downstream(async_channel::Sender>), - /// Listener for incoming downstream connections - DownstreamListener(async_channel::Sender>), - /// Upstream task (e.g, connection to pool) - Upstream(async_channel::Sender>), - /// Template Provider - TemplateReceiver(async_channel::Sender>), +use crate::error::JDCError; + +/// Sender type for propagating status updates from different system components. +#[derive(Debug, Clone)] +pub enum StatusSender { + /// Status updates from a specific downstream connection. + Downstream { + downstream_id: u32, + tx: async_channel::Sender, + }, + /// Status updates from the template receiver. + TemplateReceiver(async_channel::Sender), + /// Status updates from the channel manager. + ChannelManager(async_channel::Sender), + /// Status updates from the upstream. + Upstream(async_channel::Sender), + /// Status updates from the job declarator. + JobDeclarator(async_channel::Sender), } -impl Sender { - /// The send method is used to send status of component to central status receiver. - pub async fn send( - &self, - status: Status<'static>, - ) -> Result<(), async_channel::SendError>> { - match self { - Self::Downstream(inner) => inner.send(status).await, - Self::DownstreamListener(inner) => inner.send(status).await, - Self::Upstream(inner) => inner.send(status).await, - Self::TemplateReceiver(inner) => inner.send(status).await, +/// High-level identifier of a component type that can send status updates. +#[derive(Debug, PartialEq, Eq)] +pub enum StatusType { + /// A downstream connection identified by its ID. + Downstream(u32), + /// The template receiver component. + TemplateReceiver, + /// The channel manager component. + ChannelManager, + /// The upstream component. + Upstream, + /// The job declarator component. + JobDeclarator, +} + +impl From<&StatusSender> for StatusType { + fn from(value: &StatusSender) -> Self { + match value { + StatusSender::ChannelManager(_) => StatusType::ChannelManager, + StatusSender::Downstream { + downstream_id, + tx: _, + } => StatusType::Downstream(*downstream_id), + StatusSender::JobDeclarator(_) => StatusType::JobDeclarator, + StatusSender::Upstream(_) => StatusType::Upstream, + StatusSender::TemplateReceiver(_) => StatusType::TemplateReceiver, } } } -impl Clone for Sender { - fn clone(&self) -> Self { +impl StatusSender { + /// Sends a status update for the associated component. + pub async fn send(&self, status: Status) -> Result<(), async_channel::SendError> { match self { - Self::Downstream(inner) => Self::Downstream(inner.clone()), - Self::DownstreamListener(inner) => Self::DownstreamListener(inner.clone()), - Self::Upstream(inner) => Self::Upstream(inner.clone()), - Self::TemplateReceiver(inner) => Self::TemplateReceiver(inner.clone()), + Self::Downstream { downstream_id, tx } => { + debug!( + "Sending status from Downstream [{}]: {:?}", + downstream_id, status.state + ); + tx.send(status).await + } + Self::TemplateReceiver(tx) => { + debug!("Sending status from TemplateReceiver: {:?}", status.state); + tx.send(status).await + } + Self::ChannelManager(tx) => { + debug!("Sending status from ChannelManager: {:?}", status.state); + tx.send(status).await + } + Self::Upstream(tx) => { + debug!("Sending status from Upstream: {:?}", status.state); + tx.send(status).await + } + Self::JobDeclarator(tx) => { + debug!("Sending status from JobDeclarator: {:?}", status.state); + tx.send(status).await + } } } } -/// The kind of event or status being reported by a task. +/// Represents the state of a component, typically triggered by an error or shutdown event. #[derive(Debug)] -pub enum State<'a> { - /// A downstream component (e.g. client) failed and should be shut down. - DownstreamShutdown(Error<'a>), - /// A upstream component failed and should be shut down. - UpstreamShutdown(Error<'a>), - /// A upstream component gone rogue. - UpstreamRogue, - /// A generic message to indicate health or non-critical errors. - Healthy(String), +pub enum State { + /// A downstream connection has shut down with a reason. + DownstreamShutdown { + downstream_id: u32, + reason: JDCError, + }, + /// Template receiver has shut down with a reason. + TemplateReceiverShutdown(JDCError), + /// Job declarator has shut down during fallback with a reason. + JobDeclaratorShutdownFallback(JDCError), + /// Channel manager has shut down with a reason. + ChannelManagerShutdown(JDCError), + /// Upstream has shut down during fallback with a reason. + UpstreamShutdownFallback(JDCError), } -/// Wraps a status update, to be passed through a status channel. +/// Wrapper around a component’s state, sent as status updates across the system. #[derive(Debug)] -pub struct Status<'a> { - /// State represent current state of the component. - pub state: State<'a>, +pub struct Status { + /// The current state being reported. + pub state: State, } -/// Sends a [`Status`] message tagged with its [`Sender`] to the central loop. -/// -/// This is the core logic used to determine which status variant should be sent -/// based on the error type and sender context. -async fn send_status( - sender: &Sender, - e: error::Error<'static>, - outcome: error_handling::ErrorBranch, -) -> error_handling::ErrorBranch { - match sender { - Sender::Downstream(tx) => { - tx.send(Status { - state: State::Healthy(e.to_string()), - }) - .await - .unwrap_or(()); +/// Sends a shutdown status for the given component, logging the error cause. +async fn send_status(sender: &StatusSender, error: JDCError) { + let state = match sender { + StatusSender::Downstream { downstream_id, .. } => { + warn!("Downstream [{downstream_id}] shutting down due to error: {error:?}"); + State::DownstreamShutdown { + downstream_id: *downstream_id, + reason: error, + } + } + StatusSender::TemplateReceiver(_) => { + warn!("Template Receiver shutting down due to error: {error:?}"); + State::TemplateReceiverShutdown(error) } - Sender::DownstreamListener(tx) => { - tx.send(Status { - state: State::DownstreamShutdown(e), - }) - .await - .unwrap_or(()); + StatusSender::ChannelManager(_) => { + warn!("ChannelManager shutting down due to error: {error:?}"); + State::ChannelManagerShutdown(error) } - Sender::Upstream(tx) => { - tx.send(Status { - state: State::UpstreamShutdown(e), - }) - .await - .unwrap_or(()); + StatusSender::Upstream(_) => { + warn!("Upstream shutting down due to error: {error:?}"); + State::UpstreamShutdownFallback(error) } - Sender::TemplateReceiver(tx) => { - tx.send(Status { - state: State::UpstreamShutdown(e), - }) - .await - .unwrap_or(()); + StatusSender::JobDeclarator(_) => { + warn!("Job declarator shutting down due to error: {error:?}"); + State::JobDeclaratorShutdownFallback(error) } + }; + + if let Err(e) = sender.send(Status { state }).await { + tracing::error!("Failed to send status update from {sender:?}: {e:?}"); } - outcome } -/// Centralized error dispatcher for the JDC. -/// -/// Used by the `handle_result!` macro across the codebase. -/// Decides whether the task should `Continue` or `Break` based on the error type and source. -pub async fn handle_error( - sender: &Sender, - e: error::Error<'static>, -) -> error_handling::ErrorBranch { - tracing::error!("Error: {:?}", &e); - match e { - Error::VecToSlice32(_) => send_status(sender, e, error_handling::ErrorBranch::Break).await, - // Errors on bad CLI argument input. - Error::BadCliArgs => send_status(sender, e, error_handling::ErrorBranch::Break).await, - // Errors on bad `config` TOML deserialize. - Error::BadConfigDeserialize(_) => { - send_status(sender, e, error_handling::ErrorBranch::Break).await - } - // Errors from `binary_sv2` crate. - Error::BinarySv2(_) => send_status(sender, e, error_handling::ErrorBranch::Break).await, - // Errors on bad noise handshake. - Error::CodecNoise(_) => send_status(sender, e, error_handling::ErrorBranch::Break).await, - // Errors from `framing_sv2` crate. - Error::FramingSv2(_) => send_status(sender, e, error_handling::ErrorBranch::Break).await, - // Errors on bad `TcpStream` connection. - Error::Io(_) => send_status(sender, e, error_handling::ErrorBranch::Break).await, - // Errors on bad `String` to `int` conversion. - Error::ParseInt(_) => send_status(sender, e, error_handling::ErrorBranch::Break).await, - // Errors from `roles_logic_sv2` crate. - Error::RolesSv2Logic(_) => send_status(sender, e, error_handling::ErrorBranch::Break).await, - Error::UpstreamIncoming(_) => { - send_status(sender, e, error_handling::ErrorBranch::Break).await - } - Error::SubprotocolMining(_) => { - send_status(sender, e, error_handling::ErrorBranch::Break).await - } - // Locking Errors - Error::PoisonLock => send_status(sender, e, error_handling::ErrorBranch::Break).await, - // Channel Receiver Error - Error::ChannelErrorReceiver(_) => { - send_status(sender, e, error_handling::ErrorBranch::Break).await - } - Error::TokioChannelErrorRecv(_) => { - send_status(sender, e, error_handling::ErrorBranch::Break).await - } - // Channel Sender Errors - Error::ChannelErrorSender(_) => { - send_status(sender, e, error_handling::ErrorBranch::Break).await - } - Error::Infallible(_) => send_status(sender, e, error_handling::ErrorBranch::Break).await, - } +/// Logs an error and propagates a corresponding shutdown status for the component. +pub async fn handle_error(sender: &StatusSender, e: JDCError) { + error!("Error in {:?}: {:?}", sender, e); + send_status(sender, e).await; } diff --git a/roles/jd-client/src/lib/task_manager.rs b/roles/jd-client/src/lib/task_manager.rs new file mode 100644 index 0000000000..95435a020c --- /dev/null +++ b/roles/jd-client/src/lib/task_manager.rs @@ -0,0 +1,72 @@ +use std::sync::Mutex as StdMutex; +use tokio::task::JoinHandle; + +/// Manages a collection of spawned tokio tasks. +/// +/// This struct provides a centralized way to spawn, track, and manage the lifecycle +/// of async tasks. It maintains a list of join handles that can +/// be used to wait for all tasks to complete or abort them during shutdown. +pub struct TaskManager { + tasks: StdMutex>>, +} + +impl Default for TaskManager { + fn default() -> Self { + Self::new() + } +} + +impl TaskManager { + /// Creates a new TaskManager instance. + /// + /// Initializes an empty task manager ready to spawn and track tasks. + pub fn new() -> Self { + Self { + tasks: StdMutex::new(Vec::new()), + } + } + + /// Spawns a new async task and adds it to the managed collection. + /// + /// The task will be tracked by this manager and can be waited for or aborted + /// using the other methods. + /// + /// # Arguments + /// * `fut` - The future to spawn as a task + pub fn spawn(&self, fut: F) + where + F: std::future::Future + Send + 'static, + { + let handle = tokio::spawn(async move { + fut.await; + }); + self.tasks.lock().unwrap().push(handle); + } + + /// Waits for all managed tasks to complete. + /// + /// This method will block until all tasks that were spawned through this + /// manager have finished executing. Tasks are joined in reverse order + /// (most recently spawned first). + pub async fn join_all(&self) { + let handles = { + let mut tasks = self.tasks.lock().unwrap(); + std::mem::take(&mut *tasks) + }; + + for handle in handles { + let _ = handle.await; + } + } + + /// Aborts all managed tasks. + /// + /// This method immediately cancels all tasks that were spawned through this + /// manager. The tasks will be terminated without waiting for them to complete. + pub async fn abort_all(&self) { + let mut tasks = self.tasks.lock().unwrap(); + for handle in tasks.drain(..) { + handle.abort(); + } + } +} diff --git a/roles/jd-client/src/lib/template_receiver/message_handler.rs b/roles/jd-client/src/lib/template_receiver/message_handler.rs index fff8e6fb94..ecf12f5595 100644 --- a/roles/jd-client/src/lib/template_receiver/message_handler.rs +++ b/roles/jd-client/src/lib/template_receiver/message_handler.rs @@ -1,104 +1,50 @@ -//! ## Template Receiver: Message Handler -//! -//! Implementation of the `ParseTemplateDistributionMessagesFromServer` trait for `TemplateRx`. -//! -//! This trait defines how the `TemplateRx` component handles various types of -//! template distribution messages received from a server, and how it responds -//! to each message type accordingly. -use super::TemplateRx; -use roles_logic_sv2::{ - errors::Error, - handlers::template_distribution::{ParseTemplateDistributionMessagesFromServer, SendTo}, - parsers::TemplateDistribution, - template_distribution_sv2::*, +use stratum_common::roles_logic_sv2::{ + common_messages_sv2::{ + ChannelEndpointChanged, Reconnect, SetupConnectionError, SetupConnectionSuccess, + }, + handlers_sv2::HandleCommonMessagesFromServerAsync, }; -use tracing::{debug, error, info}; +use tracing::{info, warn}; -impl ParseTemplateDistributionMessagesFromServer for TemplateRx { - // Handles a `NewTemplate` message received from the Template Provider. - // - // Returns `Ok(SendTo::None(Some(new_template)))` indicating that no immediate - // message needs to be sent back to the server as a direct response to `NewTemplate`, - fn handle_new_template(&mut self, m: NewTemplate) -> Result { - info!( - "Received NewTemplate with id: {}, is future: {}", - m.template_id, m.future_template - ); - debug!("NewTemplate: {:?}", m); - let new_template = m.into_static(); - let new_template = TemplateDistribution::NewTemplate(new_template); - Ok(SendTo::None(Some(new_template))) +use crate::{error::JDCError, template_receiver::TemplateReceiver}; + +impl HandleCommonMessagesFromServerAsync for TemplateReceiver { + type Error = JDCError; + + async fn handle_setup_connection_success( + &mut self, + _server_id: Option, + msg: SetupConnectionSuccess, + ) -> Result<(), Self::Error> { + info!("Received: {}", msg); + + Ok(()) } - // Handles a `SetNewPrevHash` message received from the Template Provider. - // - // Returns `Ok(SendTo::None(Some(new_prev_hash)))` indicating no immediate response - // to the server but wrapping the processed `SetNewPrevHash` for forwarding to - // other components like the `JobDeclarator` and downstream. - fn handle_set_new_prev_hash(&mut self, m: SetNewPrevHash) -> Result { - info!("Received SetNewPrevHash for template: {}", m.template_id); - debug!("SetNewPrevHash: {:?}", m); - let new_prev_hash = SetNewPrevHash { - template_id: m.template_id, - prev_hash: m.prev_hash.into_static(), - header_timestamp: m.header_timestamp, - n_bits: m.n_bits, - target: m.target.into_static(), - }; - let new_prev_hash = TemplateDistribution::SetNewPrevHash(new_prev_hash); - self.pool_chaneger_trigger.safe_lock(|t| t.stop()).unwrap(); - Ok(SendTo::None(Some(new_prev_hash))) + async fn handle_channel_endpoint_changed( + &mut self, + _server_id: Option, + msg: ChannelEndpointChanged, + ) -> Result<(), Self::Error> { + info!("Received: {}", msg); + Ok(()) } - // Handles a `RequestTransactionDataSuccess` message received from the Template Provider. - // - // Returns `Ok(SendTo::None(Some(tx_received)))` wrapping the processed message - // for forwarding to components like the `JobDeclarator` to complete the job information. - // Returns `Err(Error)` if an error occurs (currently not possible). - fn handle_request_tx_data_success( + async fn handle_reconnect( &mut self, - m: RequestTransactionDataSuccess, - ) -> Result { - info!( - "Received RequestTransactionDataSuccess for template: {}", - m.template_id - ); - debug!("RequestTransactionDataSuccess: {:?}", m); - let m = RequestTransactionDataSuccess { - transaction_list: m.transaction_list.into_static(), - excess_data: m.excess_data.into_static(), - template_id: m.template_id, - }; - let tx_received = TemplateDistribution::RequestTransactionDataSuccess(m); - Ok(SendTo::None(Some(tx_received))) + _server_id: Option, + msg: Reconnect<'_>, + ) -> Result<(), Self::Error> { + info!("Received: {}", msg); + Ok(()) } - // Handles a `RequestTransactionDataError` message received from the server. - // - // Returns `Err(Error::NoValidTemplate)` if the error code is "template-id-not-found" - // or an unrecognized error code, indicating that the requested template is invalid - // or no longer available. - fn handle_request_tx_data_error( + async fn handle_setup_connection_error( &mut self, - m: RequestTransactionDataError, - ) -> Result { - error!( - "Received RequestTransactionDataError for template: {}, error: {}", - m.template_id, - std::str::from_utf8(m.error_code.as_ref()).unwrap_or("unknown error code") - ); - let m = RequestTransactionDataError { - template_id: m.template_id, - error_code: m.error_code.into_static(), - }; - let error_code_string = - std::str::from_utf8(m.error_code.as_ref()).unwrap_or("unknown error code"); - match error_code_string { - "template-id-not-found" => Err(Error::NoValidTemplate(error_code_string.to_string())), - "stale-template-id" => Ok(SendTo::None(Some( - TemplateDistribution::RequestTransactionDataError(m), - ))), - _ => Err(Error::NoValidTemplate(error_code_string.to_string())), - } + _server_id: Option, + msg: SetupConnectionError<'_>, + ) -> Result<(), Self::Error> { + warn!("Received: {}", msg); + Err(JDCError::Shutdown) } } diff --git a/roles/jd-client/src/lib/template_receiver/mod.rs b/roles/jd-client/src/lib/template_receiver/mod.rs index 2803654e68..4ebc880c2d 100644 --- a/roles/jd-client/src/lib/template_receiver/mod.rs +++ b/roles/jd-client/src/lib/template_receiver/mod.rs @@ -1,426 +1,422 @@ -//! ## Template Receiver (JDC) -//! Contains the logic required for the Job Declarator Client (JDC) to connect to and communicate -//! with a Template Provider (TP). +//! Template Receiver module //! -//! This includes establishing a secure connection, sending and receiving SV2 Template Distribution -//! protocol messages, handling template-related events, and coordinating with the job declarator -//! and downstream subsystem. -use super::{job_declarator::JobDeclarator, status, PoolChangerTrigger}; -use async_channel::{Receiver, Sender}; -use codec_sv2::{HandshakeRole, Initiator, StandardEitherFrame, StandardSv2Frame}; -use error_handling::handle_result; +//! This module defines the [`TemplateReceiver`] struct, which manages a connection +//! to a Template Provider (TP). +//! +//! Responsibilities: +//! - Establish TCP + Noise encrypted connection to the template provider +//! - Perform `SetupConnection` handshake +//! - Forward SV2 `TemplateDistribution` messages to the channel manager +//! - Forward messages from the channel manager upstream to the template provider +//! - Send [`CoinbaseOutputConstraints`] to the template provider + +use std::{net::SocketAddr, sync::Arc}; + +use async_channel::{unbounded, Receiver, Sender}; use key_utils::Secp256k1PublicKey; -use network_helpers_sv2::noise_connection::Connection; -use roles_logic_sv2::{ - handlers::{template_distribution::ParseTemplateDistributionMessagesFromServer, SendTo_}, - job_declaration_sv2::AllocateMiningJobTokenSuccess, - parsers::{AnyMessage, TemplateDistribution}, - template_distribution_sv2::{ - CoinbaseOutputConstraints, NewTemplate, RequestTransactionData, SubmitSolution, +use stratum_common::{ + network_helpers_sv2::noise_stream::NoiseTcpStream, + roles_logic_sv2::{ + bitcoin::{ + self, absolute::LockTime, transaction::Version, OutPoint, ScriptBuf, Sequence, + Transaction, TxIn, TxOut, Witness, + }, + codec_sv2::{self, framing_sv2, HandshakeRole, Initiator}, + handlers_sv2::HandleCommonMessagesFromServerAsync, + parsers_sv2::{AnyMessage, TemplateDistribution}, + template_distribution_sv2::CoinbaseOutputConstraints, + utils::Mutex, }, - utils::Mutex, }; -use setup_connection::SetupConnectionHandler; -use std::{convert::TryInto, net::SocketAddr, sync::Arc}; -use stratum_common::bitcoin::{ - consensus::{deserialize, Encodable}, - Transaction, TxOut, +use tokio::{net::TcpStream, sync::broadcast}; +use tracing::{debug, error, info, warn}; + +use crate::{ + error::JDCError, + status::{handle_error, Status, StatusSender}, + task_manager::TaskManager, + utils::{ + get_setup_connection_message_tp, protocol_message_type, spawn_io_tasks, Message, + MessageType, SV2Frame, ShutdownMessage, StdFrame, + }, }; -use tokio::task::AbortHandle; -use tracing::{error, info, warn}; mod message_handler; -mod setup_connection; - -pub type SendTo = SendTo_, ()>; -pub type Message = AnyMessage<'static>; -pub type StdFrame = StandardSv2Frame; -pub type EitherFrame = StandardEitherFrame; - -/// Represents a template receiver client -pub struct TemplateRx { - // Receiver channel for incoming messages from the Template Provider. - receiver: Receiver, - // Sender channel for sending messages to the Template Provider. - sender: Sender, - // Sender for communicating status updates back to the main status loop - // for error handling and state management. - tx_status: status::Sender, - // Present when connected to a pool, absent in solo mining mode. - jd: Option>>, - // used for sending template and job information to the downstream. - down: Arc>, - task_collector: Arc>>, - // Stores the last received `NewTemplate` message. - new_template_message: Option>, - // Trigger mechanism to detect unresponsive upstream behavior and initiate a pool change. - pool_chaneger_trigger: Arc>, - // The encoded miner's coinbase output(s) from the configuration. - miner_coinbase_output: Vec, -} -impl TemplateRx { - // The connect method connects to the Template Provider over TCP, performs the SV2 setup - // connection handshake, and starts background tasks for handling incoming template messages - // and forwarding miner solutions. - // - // This is the entry point for establishing communication with the Template Provider. - #[allow(clippy::too_many_arguments)] - pub async fn connect( - address: SocketAddr, - solution_receiver: Receiver>, - tx_status: status::Sender, - jd: Option>>, - down: Arc>, - task_collector: Arc>>, - pool_chaneger_trigger: Arc>, - miner_coinbase_outputs: Vec, - authority_public_key: Option, - ) { - let mut encoded_outputs = vec![]; - // If in solo mining mode (jd is None), encode only the first coinbase output - // as per JDS behavior. Otherwise, encode all provided outputs. - if jd.is_none() { - miner_coinbase_outputs[0] - .consensus_encode(&mut encoded_outputs) - .expect("Invalid coinbase output in config"); - } else { - miner_coinbase_outputs - .consensus_encode(&mut encoded_outputs) - .expect("Invalid coinbase output in config"); - } - // Establish a TCP connection to the Template Provider address. - let stream = tokio::net::TcpStream::connect(address).await.unwrap(); +/// Placeholder for future template receiver–specific state. +pub struct TemplateReceiverData; - let initiator = match authority_public_key { - Some(pub_key) => Initiator::from_raw_k(pub_key.into_bytes()), - None => Initiator::without_pk(), - } - .unwrap(); - let (mut receiver, mut sender) = - Connection::new(stream, HandshakeRole::Initiator(initiator)) - .await - .unwrap(); - - info!("Template Receiver try to set up connection"); - // Perform the SV2 setup connection handshake with the Template Provider. - SetupConnectionHandler::setup(&mut receiver, &mut sender, address) - .await - .unwrap(); - info!("Template Receiver connection set up"); - - let self_mutex = Arc::new(Mutex::new(Self { - receiver: receiver.clone(), - sender: sender.clone(), - tx_status, - jd, - down, - task_collector: task_collector.clone(), - new_template_message: None, - pool_chaneger_trigger, - miner_coinbase_output: encoded_outputs, - })); - - // Spawn a task to handle incoming block solutions from the miner and forward them - // to the Template Provider - let task = tokio::task::spawn(Self::on_new_solution(self_mutex.clone(), solution_receiver)); - task_collector - .safe_lock(|c| c.push(task.abort_handle())) - .unwrap(); - - // Start the main task for receiving and processing template-related messages - // from the Template Provider. - Self::start_templates(self_mutex); - } +/// Holds communication channels between the template receiver, channel manager, +/// and upstream template provider. +/// +/// - `channel_manager_sender` → sends frames to the channel manager +/// - `channel_manager_receiver` → receives frames from the channel manager +/// - `outbound_tx` → sends frames upstream to the template provider +/// - `inbound_rx` → receives frames from the template provider +#[derive(Clone)] +pub struct TemplateReceiverChannel { + channel_manager_sender: Sender, + channel_manager_receiver: Receiver, + tp_sender: Sender, + tp_receiver: Receiver, +} - /// This method is used to send message to template provider. - pub async fn send(self_: &Arc>, sv2_frame: StdFrame) { - let either_frame = sv2_frame.into(); - let sender_to_tp = self_.safe_lock(|self_| self_.sender.clone()).unwrap(); - match sender_to_tp.send(either_frame).await { - Ok(_) => (), - Err(e) => panic!("{:?}", e), - } - } +/// Manages communication with a Stratum V2 Template Provider. +/// +/// Responsibilities: +/// - Establishes TCP + Noise connection to TP +/// - Performs handshake (`SetupConnection`) +/// - Sends [`CoinbaseOutputConstraints`] to TP +/// - Routes messages between TP and channel manager +/// - Handles shutdown/fallback notifications +#[allow(warnings)] +#[derive(Clone)] +pub struct TemplateReceiver { + /// Internal state + template_receiver_data: Arc>, + /// Messaging channels to/from the channel manager and TP. + template_receiver_channel: TemplateReceiverChannel, + /// Address of the template provider (string form) + tp_address: String, +} - /// Sends a `CoinbaseOutputConstraints` message to the Template Provider. +impl TemplateReceiver { + /// Establish a new connection to a Template Provider. /// - /// This informs the TP about the maximum size and sigops allowed in the miner's - /// additional coinbase output data. - pub async fn send_coinbase_output_constraints( - self_mutex: &Arc>, - size: u32, - sigops: u16, - ) { - let coinbase_output_data_size = AnyMessage::TemplateDistribution( - TemplateDistribution::CoinbaseOutputConstraints(CoinbaseOutputConstraints { - coinbase_output_max_additional_size: size, - coinbase_output_max_additional_sigops: sigops, - }), - ); - let frame: StdFrame = coinbase_output_data_size.try_into().unwrap(); - Self::send(self_mutex, frame).await; - } - - /// Sends a `RequestTransactionData` message to the Template Provider. + /// - Opens a TCP connection + /// - Performs Noise handshake + /// - Spawns IO tasks for inbound/outbound frames /// - /// This requests the full transaction data for a template identified by its ID. - pub async fn send_tx_data_request( - self_mutex: &Arc>, - new_template: NewTemplate<'static>, - ) { - let tx_data_request = AnyMessage::TemplateDistribution( - TemplateDistribution::RequestTransactionData(RequestTransactionData { - template_id: new_template.template_id, - }), - ); - let frame: StdFrame = tx_data_request.try_into().unwrap(); - Self::send(self_mutex, frame).await; - } + /// Retries up to 3 times before returning [`JDCError::Shutdown`]. + pub async fn new( + tp_address: String, + public_key: Option, + channel_manager_receiver: Receiver, + channel_manager_sender: Sender, + notify_shutdown: broadcast::Sender, + task_manager: Arc, + status_sender: Sender, + ) -> Result { + const MAX_RETRIES: usize = 3; - /// Retrieves the last allocated mining job token. - /// - /// If the JDC is connected to a pool, it fetches the token from the `JobDeclarator`. - /// In solo mining mode, it generates a dummy token with constraints derived from - /// the miner's configured coinbase output. - async fn get_last_token( - jd: Option>>, - miner_coinbase_output: &[u8], - ) -> AllocateMiningJobTokenSuccess<'static> { - if let Some(jd) = jd { - JobDeclarator::get_last_token(&jd).await - } else { - // This is when JDC is doing solo mining - let deserialized_miner_coinbase_output: Transaction = - deserialize(miner_coinbase_output).expect("Invalid coinbase output"); - let miner_coinbase_output_sigops = deserialized_miner_coinbase_output - .output - .iter() - .map(|output| output.script_pubkey.count_sigops() as u16) - .sum::(); - - AllocateMiningJobTokenSuccess { - request_id: 0, - mining_job_token: vec![0; 32].try_into().unwrap(), - coinbase_output_max_additional_size: 100, - coinbase_output_max_additional_sigops: miner_coinbase_output_sigops, - coinbase_output: miner_coinbase_output.to_vec().try_into().unwrap(), + for attempt in 1..=MAX_RETRIES { + info!(attempt, MAX_RETRIES, "Connecting to template provider"); + + let initiator = match public_key { + Some(pub_key) => { + debug!(attempt, "Using public key for initiator handshake"); + Initiator::from_raw_k(pub_key.into_bytes()) + } + None => { + debug!(attempt, "Using anonymous initiator (no public key)"); + Initiator::without_pk() + } + }?; + + match TcpStream::connect(tp_address.as_str()).await { + Ok(stream) => { + info!( + attempt, + "TCP connection established, starting Noise handshake" + ); + + match NoiseTcpStream::::new( + stream, + HandshakeRole::Initiator(initiator), + ) + .await + { + Ok(noise_stream) => { + info!(attempt, "Noise handshake completed successfully"); + + let (noise_stream_reader, noise_stream_writer) = + noise_stream.into_split(); + + let status_sender = StatusSender::TemplateReceiver(status_sender); + let (inbound_tx, inbound_rx) = unbounded::(); + let (outbound_tx, outbound_rx) = unbounded::(); + + info!(attempt, "Spawning IO tasks for template receiver"); + spawn_io_tasks( + task_manager.clone(), + noise_stream_reader, + noise_stream_writer, + outbound_rx, + inbound_tx, + notify_shutdown, + status_sender, + ); + + let template_receiver_data = Arc::new(Mutex::new(TemplateReceiverData)); + let template_receiver_channel = TemplateReceiverChannel { + channel_manager_receiver, + channel_manager_sender, + tp_receiver: inbound_rx, + tp_sender: outbound_tx, + }; + + info!(attempt, "TemplateReceiver initialized successfully"); + return Ok(TemplateReceiver { + template_receiver_channel, + template_receiver_data, + tp_address, + }); + } + Err(e) => { + error!(attempt, error = ?e, "Noise handshake failed"); + } + } + } + Err(e) => { + warn!(attempt, MAX_RETRIES, error = ?e, "Failed to connect to template provider"); + } + } + + if attempt < MAX_RETRIES { + debug!(attempt, "Retrying connection after backoff"); + tokio::time::sleep(std::time::Duration::from_secs(2)).await; } } + + error!("Exhausted all connection attempts, shutting down TemplateReceiver"); + Err(JDCError::Shutdown) } - /// Contains the core logic for the Template Receiver's main operational loop. - /// - /// This function is responsible for: - /// 1. Sending initial `CoinbaseOutputConstraints` to the Template Provider. - /// 2. Continuously receiving and processing messages from the Template Provider. - /// 3. Handling different Template Distribution messages (`NewTemplate`, `SetNewPrevHash`, - /// `RequestTransactionDataSuccess`, `RequestTransactionDataError`). - /// 4. Requesting transaction data for new templates. - /// 5. Coordinating the delivery of template and job information to the `JobDeclarator` (when - /// connected to a pool) and the `DownstreamMiningNode`. - /// 6. Utilizing the `IS_NEW_TEMPLATE_HANDLED` global atomic for synchronization between the - /// template receiver and downstream when processing `NewTemplate` and `SetNewPrevHash`. + /// Start unified message loop for template receiver. /// - /// FIX ME: Remove dependence from other modules in this. This gonna help in - /// removing sequential component spawning. - pub fn start_templates(self_mutex: Arc>) { - let jd = self_mutex.safe_lock(|s| s.jd.clone()).unwrap(); - let down = self_mutex.safe_lock(|s| s.down.clone()).unwrap(); - let tx_status = self_mutex.safe_lock(|s| s.tx_status.clone()).unwrap(); - let mut coinbase_output_constraints_sent = false; - let mut last_token = None; - let miner_coinbase_output = self_mutex - .safe_lock(|s| s.miner_coinbase_output.clone()) - .unwrap(); - - // Spawn the main task for handling incoming template messages. - let main_task = { - let self_mutex = self_mutex.clone(); - tokio::task::spawn(async move { - // Send CoinbaseOutputConstraints to TP - loop { - // Retrieve the last allocated mining job token if not already available. - if last_token.is_none() { - let jd = self_mutex.safe_lock(|s| s.jd.clone()).unwrap(); - last_token = - Some(Self::get_last_token(jd, &miner_coinbase_output[..]).await); - } - // Send CoinbaseOutputConstraints to the Template Provider if not already sent. - if !coinbase_output_constraints_sent { - coinbase_output_constraints_sent = true; - Self::send_coinbase_output_constraints( - &self_mutex, - last_token - .clone() - .unwrap() - .coinbase_output_max_additional_size, - last_token - .clone() - .unwrap() - .coinbase_output_max_additional_sigops, - ) - .await; - } + /// Responsibilities: + /// - Run handshake (`setup_connection`) + /// - Send [`CoinbaseOutputConstraints`] + /// - Handle: + /// - Messages from template provider + /// - Messages from channel manager + /// - Shutdown signals (upstream/job-declarator fallback) + pub async fn start( + mut self, + socket_address: String, + notify_shutdown: broadcast::Sender, + status_sender: Sender, + task_manager: Arc, + coinbase_outputs: Vec, + ) { + let status_sender = StatusSender::TemplateReceiver(status_sender); + let mut shutdown_rx = notify_shutdown.subscribe(); - // Receive Templates and SetPrevHash from TP to send to JD - let receiver = self_mutex - .clone() - .safe_lock(|s| s.receiver.clone()) - .unwrap(); - let received = handle_result!(tx_status.clone(), receiver.recv().await); - let mut frame: StdFrame = - handle_result!(tx_status.clone(), received.try_into()); - let message_type = frame.get_header().unwrap().msg_type(); - let payload = frame.payload(); - - // Process the received message using the template distribution message handler - let next_message_to_send = - ParseTemplateDistributionMessagesFromServer::handle_message_template_distribution( - self_mutex.clone(), - message_type, - payload, - ); - match next_message_to_send { - Ok(SendTo::None(m)) => { - match m { - // Send the new template along with the token to the JD so that JD - // can declare the mining job - Some(TemplateDistribution::NewTemplate(m)) => { - // Set the global flag to false (Release ordering) to signal - // that a new template is being handled by the downstream. - super::IS_NEW_TEMPLATE_HANDLED - .store(false, std::sync::atomic::Ordering::Release); - // Request transaction data for the new template. - Self::send_tx_data_request(&self_mutex, m.clone()).await; - self_mutex - .safe_lock(|t| t.new_template_message = Some(m.clone())) - .unwrap(); - // Get the pool's coinbase output from the last token. - let token = last_token.clone().unwrap(); - let pool_output = token.coinbase_output.to_vec(); - - // Notify the downstream mining node about the new template. - super::downstream::DownstreamMiningNode::on_new_template( - &down, - m.clone(), - &pool_output[..], - ) - .await - .unwrap(); - } - // Handle SetNewPrevHash messages. - Some(TemplateDistribution::SetNewPrevHash(m)) => { - info!("Received SetNewPrevHash, waiting for IS_NEW_TEMPLATE_HANDLED"); - // Wait until the IS_NEW_TEMPLATE_HANDLED flag is true, - // indicating the downstream has finished processing the - // previous NewTemplate. - while !super::IS_NEW_TEMPLATE_HANDLED - .load(std::sync::atomic::Ordering::Acquire) - { - tokio::task::yield_now().await; - } - info!("IS_NEW_TEMPLATE_HANDLED ok"); - // If connected to a pool, notify the Job Declarator about the - // new prev hash. - if let Some(jd) = jd.as_ref() { - super::job_declarator::JobDeclarator::on_set_new_prev_hash( - jd.clone(), - m.clone(), - ); - } - // Notify the downstream mining node about the new prev hash. - super::downstream::DownstreamMiningNode::on_set_new_prev_hash( - &down, m, - ) - .await - .unwrap(); - } - // Handle RequestTransactionDataSuccess messages. - Some(TemplateDistribution::RequestTransactionDataSuccess(m)) => { - // safe to unwrap because this message is received after the new - // template message - let transactions_data = m.transaction_list; - let excess_data = m.excess_data; - - // Retrieve the stored NewTemplate message (safe to unwrap as - // this message follows a NewTemplate). - let m = self_mutex - .safe_lock(|t| t.new_template_message.clone()) - .unwrap() - .unwrap(); - - // Retrieve the last token and reset the stored token. - let token = last_token.unwrap(); - last_token = None; - - // Extract mining token and pool coinbase output from the token. - let mining_token = token.mining_job_token.to_vec(); - let pool_coinbase_out = token.coinbase_output.to_vec(); - - // If connected to a pool, notify the Job Declarator with the - // complete template information (including transactions). - if let Some(jd) = jd.as_ref() { - super::job_declarator::JobDeclarator::on_new_template( - jd, - m.clone(), - mining_token, - transactions_data, - excess_data, - pool_coinbase_out, - ) - .await; - } + info!("Initialized state for starting template receiver"); + _ = self.setup_connection(socket_address).await; + + _ = self.coinbase_constraints(coinbase_outputs).await; + + info!("Setup Connection done. connection with template receiver is now done"); + task_manager.spawn( + async move { + loop { + let mut self_clone_1 = self.clone(); + let self_clone_2 = self.clone(); + tokio::select! { + message = shutdown_rx.recv() => { + match message { + Ok(ShutdownMessage::ShutdownAll) => { + info!("Template Receiver: received shutdown signal"); + break; + }, + Ok(ShutdownMessage::UpstreamShutdownFallback((coinbase_outputs,tx))) => { + info!("Template provider: Received Upstream shutdown."); + _ = self.coinbase_constraints(coinbase_outputs).await; + drop(tx); } - Some(TemplateDistribution::RequestTransactionDataError(_)) => { - warn!("The prev_hash of the template requested to Template Provider no longer points to the latest tip. Continuing work on the updated template.") + Ok(ShutdownMessage::JobDeclaratorShutdownFallback((coinbase_outputs, tx))) => { + info!("Template provider: Received Job declarator shutdown."); + _ = self.coinbase_constraints(coinbase_outputs).await; + drop(tx); } - _ => { - error!("{:?}", frame); - error!("{:?}", frame.payload()); - error!("{:?}", frame.get_header()); - std::process::exit(1); + Err(e) => { + warn!(error = ?e, "Template Receiver: shutdown channel closed unexpectedly"); + break; } + _ => {} } } - Ok(m) => { - error!("{:?}", m); - error!("{:?}", frame); - error!("{:?}", frame.payload()); - error!("{:?}", frame.get_header()); - std::process::exit(1); - } - Err(e) => { - error!("{:?}", e); - error!("{:?}", frame); - error!("{:?}", frame.payload()); - error!("{:?}", frame.get_header()); - std::process::exit(1); + res = self_clone_1.handle_template_provider_message() => { + if let Err(e) = res { + error!("TemplateReceiver template provider handler failed: {e:?}"); + handle_error(&status_sender, e).await; + break; + } } + res = self_clone_2.handle_channel_manager_message() => { + if let Err(e) = res { + error!("TemplateReceiver channel manager handler failed: {e:?}"); + handle_error(&status_sender, e).await; + break; + } + }, } } - }) - }; - self_mutex - .safe_lock(|s| { - s.task_collector - .safe_lock(|c| c.push(main_task.abort_handle())) - .unwrap() - }) - .unwrap(); + warn!("TemplateReceiver: unified message loop exited."); + }, + ); } - /// Handles incoming `SubmitSolution` messages from the miner. + /// Handle inbound messages from the template provider. /// - /// This method continuously receives solutions from the provided receiver channel - /// and forwards them as `SubmitSolution` messages to the Template Provider. - async fn on_new_solution(self_: Arc>, rx: Receiver>) { - while let Ok(solution) = rx.recv().await { - let sv2_frame: StdFrame = - AnyMessage::TemplateDistribution(TemplateDistribution::SubmitSolution(solution)) - .try_into() - .expect("Failed to convert solution to sv2 frame!"); - Self::send(&self_, sv2_frame).await + /// Routes: + /// - `Common` messages → handled locally + /// - `TemplateDistribution` messages → forwarded to channel manager + /// - Unsupported messages → logged and ignored + pub async fn handle_template_provider_message(&mut self) -> Result<(), JDCError> { + let mut sv2_frame = self.template_receiver_channel.tp_receiver.recv().await?; + + debug!("Received SV2 frame from Template provider."); + let Some(message_type) = sv2_frame.get_header().map(|m| m.msg_type()) else { + return Ok(()); + }; + match protocol_message_type(message_type) { + MessageType::Common => { + info!( + ?message_type, + "Handling common message from Template provider." + ); + self.handle_common_message_frame_from_server( + None, + message_type, + sv2_frame.payload(), + ) + .await?; + } + MessageType::TemplateDistribution => { + self.template_receiver_channel + .channel_manager_sender + .send(sv2_frame) + .await + .map_err(|e| { + error!(error=?e, "Failed to send template distribution message to channel manager."); + JDCError::ChannelErrorSender + })?; + } + _ => { + warn!("Received unsupported message type from template provider: {message_type}"); + } } + Ok(()) + } + + /// Handle messages from channel manager → template provider. + /// + /// Forwards outbound frames upstream + pub async fn handle_channel_manager_message(&self) -> Result<(), JDCError> { + let msg = self + .template_receiver_channel + .channel_manager_receiver + .recv() + .await?; + debug!("Forwarding message from channel manager to outbound_tx"); + self.template_receiver_channel + .tp_sender + .send(msg) + .await + .map_err(|_| JDCError::ChannelErrorSender)?; + + Ok(()) + } + + /// Build and send [`CoinbaseOutputConstraints`] upstream TP. + pub async fn coinbase_constraints( + &mut self, + coinbase_outputs: Vec, + ) -> Result<(), JDCError> { + debug!( + "Deserializing coinbase outputs ({} bytes)", + coinbase_outputs.len() + ); + let outputs: Vec = bitcoin::consensus::deserialize(&coinbase_outputs)?; + + let max_size: u32 = outputs.iter().map(|o| o.size() as u32).sum(); + debug!( + max_size, + outputs_count = outputs.len(), + "Calculated max coinbase output size" + ); + + let dummy_coinbase = Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint::null(), + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::from(vec![vec![0; 32]]), + }], + output: outputs, + }; + + let max_sigops = dummy_coinbase.total_sigop_cost(|_| None) as u16; + debug!(max_sigops, "Calculated max sigops for coinbase"); + + let constraints = CoinbaseOutputConstraints { + coinbase_output_max_additional_size: max_size, + coinbase_output_max_additional_sigops: max_sigops, + }; + + let msg = AnyMessage::TemplateDistribution( + TemplateDistribution::CoinbaseOutputConstraints(constraints), + ); + + let frame: StdFrame = msg.try_into()?; + info!("Sending CoinbaseOutputConstraints message upstream"); + self.template_receiver_channel + .tp_sender + .send(frame) + .await + .map_err(|_| { + error!("Failed to send CoinbaseOutputConstraints message upstream"); + JDCError::ChannelErrorSender + })?; + + Ok(()) + } + + // Performs the initial handshake with template provider. + pub async fn setup_connection(&mut self, addr: String) -> Result<(), JDCError> { + let socket: SocketAddr = addr.parse().map_err(|_| { + error!(%addr, "Invalid socket address"); + JDCError::InvalidSocketAddress(addr.clone()) + })?; + + info!(%socket, "Building setup connection message for upstream"); + let setup_msg = get_setup_connection_message_tp(socket); + let frame: StdFrame = Message::Common(setup_msg.into()).try_into()?; + + info!("Sending setup connection message to upstream"); + self.template_receiver_channel + .tp_sender + .send(frame) + .await + .map_err(|_| { + error!("Failed to send setup connection message upstream"); + JDCError::ChannelErrorSender + })?; + + info!("Waiting for upstream handshake response"); + let mut incoming: StdFrame = self + .template_receiver_channel + .tp_receiver + .recv() + .await + .map_err(|e| { + error!(?e, "Upstream connection closed during handshake"); + JDCError::CodecNoise(codec_sv2::noise_sv2::Error::ExpectedIncomingHandshakeMessage) + })?; + + let msg_type = incoming + .get_header() + .ok_or(framing_sv2::Error::ExpectedHandshakeFrame)? + .msg_type(); + debug!(?msg_type, "Received upstream handshake response"); + + self.handle_common_message_frame_from_server(None, msg_type, incoming.payload()) + .await?; + info!("Handshake with upstream completed successfully"); + Ok(()) } } diff --git a/roles/jd-client/src/lib/template_receiver/setup_connection.rs b/roles/jd-client/src/lib/template_receiver/setup_connection.rs deleted file mode 100644 index af1b6e7e77..0000000000 --- a/roles/jd-client/src/lib/template_receiver/setup_connection.rs +++ /dev/null @@ -1,113 +0,0 @@ -//! Template Receiver: Setup Connection Handler -//! -//! Handles setup connection logic using the Template Distribution Protocol in SV2. -//! -//! This includes sending a `SetupConnection` message and processing responses from the upstream. - -use async_channel::{Receiver, Sender}; -use codec_sv2::{StandardEitherFrame, StandardSv2Frame}; -use roles_logic_sv2::{ - common_messages_sv2::{Protocol, Reconnect, SetupConnection}, - handlers::common::{ParseCommonMessagesFromUpstream, SendTo}, - parsers::AnyMessage, - utils::Mutex, - Error, -}; -use std::{convert::TryInto, net::SocketAddr, sync::Arc}; -use tracing::info; - -pub type Message = AnyMessage<'static>; -pub type StdFrame = StandardSv2Frame; -pub type EitherFrame = StandardEitherFrame; - -// A handler responsible for managing the setup connection handshake. -pub struct SetupConnectionHandler {} - -impl SetupConnectionHandler { - /// Builds a `SetupConnection` message using the given socket address. - fn get_setup_connection_message(address: SocketAddr) -> SetupConnection<'static> { - let endpoint_host = address.ip().to_string().into_bytes().try_into().unwrap(); - let vendor = String::new().try_into().unwrap(); - let hardware_version = String::new().try_into().unwrap(); - let firmware = String::new().try_into().unwrap(); - let device_id = String::new().try_into().unwrap(); - SetupConnection { - protocol: Protocol::TemplateDistributionProtocol, - min_version: 2, - max_version: 2, - flags: 0b0000_0000_0000_0000_0000_0000_0000_0000, - endpoint_host, - endpoint_port: address.port(), - vendor, - hardware_version, - firmware, - device_id, - } - } - - /// processes the setup connection lifecycle. - pub async fn setup( - receiver: &mut Receiver, - sender: &mut Sender, - address: SocketAddr, - ) -> Result<(), ()> { - let setup_connection = Self::get_setup_connection_message(address); - - let sv2_frame: StdFrame = AnyMessage::Common(setup_connection.into()) - .try_into() - .unwrap(); - let sv2_frame = sv2_frame.into(); - sender.send(sv2_frame).await.map_err(|_| ())?; - - let mut incoming: StdFrame = receiver - .recv() - .await - .expect("Connection to TP closed!") - .try_into() - .expect("Failed to parse incoming SetupConnectionResponse"); - let message_type = incoming.get_header().unwrap().msg_type(); - let payload = incoming.payload(); - ParseCommonMessagesFromUpstream::handle_message_common( - Arc::new(Mutex::new(SetupConnectionHandler {})), - message_type, - payload, - ) - .unwrap(); - Ok(()) - } -} - -impl ParseCommonMessagesFromUpstream for SetupConnectionHandler { - // Handles a `SetupConnectionSuccess` message received from the upstream. - // - // Returns `Ok(SendTo::None(None))` indicating that no immediate message needs - // to be sent back to the server as a response to `SetupConnectionSuccess`. - fn handle_setup_connection_success( - &mut self, - m: roles_logic_sv2::common_messages_sv2::SetupConnectionSuccess, - ) -> Result { - info!( - "Received `SetupConnectionSuccess` from TP: version={}, flags={:b}", - m.used_version, m.flags - ); - Ok(SendTo::None(None)) - } - - fn handle_setup_connection_error( - &mut self, - _: roles_logic_sv2::common_messages_sv2::SetupConnectionError, - ) -> Result { - todo!() - } - - fn handle_channel_endpoint_changed( - &mut self, - _: roles_logic_sv2::common_messages_sv2::ChannelEndpointChanged, - ) -> Result { - todo!() - } - - fn handle_reconnect(&mut self, _m: Reconnect) -> Result { - todo!() - } -} diff --git a/roles/jd-client/src/lib/upstream/message_handler.rs b/roles/jd-client/src/lib/upstream/message_handler.rs new file mode 100644 index 0000000000..7c6a344968 --- /dev/null +++ b/roles/jd-client/src/lib/upstream/message_handler.rs @@ -0,0 +1,50 @@ +use stratum_common::roles_logic_sv2::{ + common_messages_sv2::{ + ChannelEndpointChanged, Reconnect, SetupConnectionError, SetupConnectionSuccess, + }, + handlers_sv2::HandleCommonMessagesFromServerAsync, +}; +use tracing::{info, warn}; + +use crate::{error::JDCError, upstream::Upstream}; + +impl HandleCommonMessagesFromServerAsync for Upstream { + type Error = JDCError; + + async fn handle_setup_connection_success( + &mut self, + _server_id: Option, + msg: SetupConnectionSuccess, + ) -> Result<(), Self::Error> { + info!("Received: {}", msg); + + Ok(()) + } + + async fn handle_channel_endpoint_changed( + &mut self, + _server_id: Option, + msg: ChannelEndpointChanged, + ) -> Result<(), Self::Error> { + info!("Received: {}", msg); + Ok(()) + } + + async fn handle_reconnect( + &mut self, + _server_id: Option, + msg: Reconnect<'_>, + ) -> Result<(), Self::Error> { + info!("Received: {}", msg); + Ok(()) + } + + async fn handle_setup_connection_error( + &mut self, + _server_id: Option, + msg: SetupConnectionError<'_>, + ) -> Result<(), Self::Error> { + warn!("Received: {}", msg); + Err(JDCError::Shutdown) + } +} diff --git a/roles/jd-client/src/lib/upstream/mod.rs b/roles/jd-client/src/lib/upstream/mod.rs new file mode 100644 index 0000000000..1c087b4563 --- /dev/null +++ b/roles/jd-client/src/lib/upstream/mod.rs @@ -0,0 +1,322 @@ +//! Upstream module +//! +//! This module defines the [`Upstream`] struct, which manages communication +//! with an upstream SV2 server (e.g., pool). +//! +//! Responsibilities: +//! - Establish a TCP + Noise encrypted connection to upstream +//! - Perform `SetupConnection` handshake +//! - Forward SV2 mining messages between upstream and channel manager +//! - Handle common messages from upstream + +use std::{net::SocketAddr, sync::Arc}; + +use async_channel::{unbounded, Receiver, Sender}; +use key_utils::Secp256k1PublicKey; +use stratum_common::{ + network_helpers_sv2::noise_stream::NoiseTcpStream, + roles_logic_sv2::{ + codec_sv2::{self, framing_sv2, HandshakeRole, Initiator}, + handlers_sv2::HandleCommonMessagesFromServerAsync, + utils::Mutex, + }, +}; +use tokio::{ + net::TcpStream, + sync::{broadcast, mpsc}, +}; +use tracing::{debug, error, info, warn}; + +use crate::{ + error::JDCError, + status::{handle_error, Status, StatusSender}, + task_manager::TaskManager, + utils::{ + get_setup_connection_message, protocol_message_type, spawn_io_tasks, Message, MessageType, + SV2Frame, ShutdownMessage, StdFrame, + }, +}; + +mod message_handler; + +/// Placeholder for future upstream-specific data/state. +pub struct UpstreamData; + +/// Holds channels for communication between upstream and channel manager. +/// +/// - `channel_manager_sender` → sends frames to channel manager +/// - `channel_manager_receiver` → receives frames from channel manager +/// - `outbound_tx` → sends frames outbound to upstream +/// - `inbound_rx` → receives frames inbound from upstream +#[derive(Clone)] +pub struct UpstreamChannel { + channel_manager_sender: Sender, + channel_manager_receiver: Receiver, + upstream_sender: Sender, + upstream_receiver: Receiver, +} + +/// Represents an upstream connection (e.g., a pool). +#[derive(Clone)] +pub struct Upstream { + #[allow(dead_code)] + /// Internal state + upstream_data: Arc>, + /// Messaging channels to/from the channel manager and Upstream. + upstream_channel: UpstreamChannel, +} + +impl Upstream { + /// Create a new [`Upstream`] connection to the given address. + /// + /// - Establishes TCP + Noise connection + /// - Spawns IO tasks to handle inbound/outbound traffic + pub async fn new( + upstreams: &(SocketAddr, SocketAddr, Secp256k1PublicKey, bool), + channel_manager_sender: Sender, + channel_manager_receiver: Receiver, + notify_shutdown: broadcast::Sender, + task_manager: Arc, + status_sender: Sender, + ) -> Result { + let (addr, _, pubkey, _) = upstreams; + let stream = tokio::time::timeout( + tokio::time::Duration::from_secs(5), + TcpStream::connect(addr), + ) + .await??; + info!("Connected to upstream at {}", addr); + let initiator = Initiator::from_raw_k(pubkey.into_bytes())?; + debug!("Begin with noise setup in upstream connection"); + let (noise_stream_reader, noise_stream_writer) = + NoiseTcpStream::::new(stream, HandshakeRole::Initiator(initiator)) + .await? + .into_split(); + + let status_sender = StatusSender::Upstream(status_sender); + let (inbound_tx, inbound_rx) = unbounded::(); + let (outbound_tx, outbound_rx) = unbounded::(); + + spawn_io_tasks( + task_manager, + noise_stream_reader, + noise_stream_writer, + outbound_rx, + inbound_tx, + notify_shutdown, + status_sender, + ); + + debug!("Noise setup done in upstream connection"); + let upstream_data = Arc::new(Mutex::new(UpstreamData)); + let upstream_channel = UpstreamChannel { + channel_manager_receiver, + channel_manager_sender, + upstream_sender: outbound_tx, + upstream_receiver: inbound_rx, + }; + Ok(Upstream { + upstream_data, + upstream_channel, + }) + } + + /// Perform `SetupConnection` handshake with upstream. + /// + /// Sends [`SetupConnection`] and awaits response. + pub async fn setup_connection( + &mut self, + min_version: u16, + max_version: u16, + ) -> Result<(), JDCError> { + info!("Upstream: initiating SV2 handshake..."); + let setup_connection = get_setup_connection_message(min_version, max_version)?; + debug!(?setup_connection, "Prepared `SetupConnection` message"); + let sv2_frame: StdFrame = Message::Common(setup_connection.into()).try_into()?; + debug!(?sv2_frame, "Encoded `SetupConnection` frame"); + + // Send SetupConnection + if let Err(e) = self.upstream_channel.upstream_sender.send(sv2_frame).await { + error!(?e, "Failed to send `SetupConnection` frame to upstream"); + return Err(JDCError::CodecNoise( + codec_sv2::noise_sv2::Error::ExpectedIncomingHandshakeMessage, + )); + } + info!("Sent `SetupConnection` to upstream, awaiting response..."); + + let incoming_frame = match self.upstream_channel.upstream_receiver.recv().await { + Ok(frame) => { + debug!(?frame, "Received raw inbound frame during handshake"); + frame + } + Err(e) => { + error!(?e, "Upstream closed connection during handshake"); + return Err(JDCError::CodecNoise( + codec_sv2::noise_sv2::Error::ExpectedIncomingHandshakeMessage, + )); + } + }; + + let mut incoming: StdFrame = incoming_frame; + debug!(?incoming, "Decoded inbound handshake frame"); + + let message_type = incoming + .get_header() + .ok_or(framing_sv2::Error::ExpectedHandshakeFrame)? + .msg_type(); + + info!(?message_type, "Dispatching inbound handshake message"); + self.handle_common_message_frame_from_server(None, message_type, incoming.payload()) + .await?; + Ok(()) + } + + /// Start unified upstream loop. + /// + /// Responsibilities: + /// - Run `setup_connection` + /// - Handle messages from upstream (pool) and channel manager + /// - React to shutdown signals + /// + /// This function spawns an async task and returns immediately. + pub async fn start( + mut self, + min_version: u16, + max_version: u16, + notify_shutdown: broadcast::Sender, + shutdown_complete_tx: mpsc::Sender<()>, + status_sender: Sender, + task_manager: Arc, + ) { + let status_sender = StatusSender::Upstream(status_sender); + let mut shutdown_rx = notify_shutdown.subscribe(); + + if let Err(e) = self.setup_connection(min_version, max_version).await { + error!(error = ?e, "Upstream: connection setup failed."); + return; + } + + task_manager.spawn(async move { + let mut self_clone_1 = self.clone(); + let mut self_clone_2 = self.clone(); + loop { + tokio::select! { + message = shutdown_rx.recv() => { + match message { + Ok(ShutdownMessage::ShutdownAll) => { + info!("Upstream: received shutdown signal."); + break; + } + Ok(ShutdownMessage::JobDeclaratorShutdownFallback(_)) => { + info!("Upstream: Received Job declarator shutdown."); + break; + } + Ok(ShutdownMessage::UpstreamShutdownFallback(_)) => { + info!("Upstream: Received Upstream shutdown."); + break; + } + Ok(ShutdownMessage::UpstreamShutdown(tx)) => { + info!("Upstream shutdown requested"); + drop(tx); + break; + } + Ok(ShutdownMessage::JobDeclaratorShutdown(tx)) => { + info!("Upstream shutdown requested"); + drop(tx); + break; + } + Err(_) => { + warn!("Upstream: shutdown channel closed unexpectedly."); + break; + } + _ => {} + } + } + res = self_clone_1.handle_pool_message() => { + if let Err(e) = res { + error!(error = ?e, "Upstream: error handling pool message."); + handle_error(&status_sender, e).await; + break; + } + } + res = self_clone_2.handle_channel_manager_message() => { + if let Err(e) = res { + error!(error = ?e, "Upstream: error handling channel manager message."); + handle_error(&status_sender, e).await; + break; + } + } + + } + } + drop(shutdown_complete_tx); + warn!("Upstream: unified message loop exited."); + }); + } + + // Handle incoming frames from upstream (pool). + // + // Routes: + // - `Common` messages → handled locally + // - `Mining` messages → forwarded to channel manager + // - Unsupported → error + async fn handle_pool_message(&mut self) -> Result<(), JDCError> { + let mut sv2_frame = self.upstream_channel.upstream_receiver.recv().await?; + + debug!("Received SV2 frame from upstream."); + let Some(message_type) = sv2_frame.get_header().map(|m| m.msg_type()) else { + return Ok(()); + }; + + match protocol_message_type(message_type) { + MessageType::Common => { + info!(?message_type, "Handling common message from Upstream."); + self.handle_common_message_frame_from_server( + None, + message_type, + sv2_frame.payload(), + ) + .await?; + } + MessageType::Mining => { + self.upstream_channel + .channel_manager_sender + .send(sv2_frame) + .await + .map_err(|e| { + error!(error=?e, "Failed to send mining message to channel manager."); + JDCError::ChannelErrorSender + })?; + } + _ => { + warn!("Received unsupported message type from upstream: {message_type}"); + } + } + Ok(()) + } + + // Handle outbound frames from channel manager → upstream. + // + // Forwards messages upstream. + async fn handle_channel_manager_message(&mut self) -> Result<(), JDCError> { + match self.upstream_channel.channel_manager_receiver.recv().await { + Ok(msg) => { + debug!("Received message from channel manager, forwarding upstream."); + self.upstream_channel + .upstream_sender + .send(msg) + .await + .map_err(|e| { + error!(error=?e, "Failed to send outbound message to upstream."); + JDCError::CodecNoise( + codec_sv2::noise_sv2::Error::ExpectedIncomingHandshakeMessage, + ) + })?; + } + Err(e) => { + warn!(error=?e, "Channel manager receiver closed or errored."); + } + } + Ok(()) + } +} diff --git a/roles/jd-client/src/lib/upstream_sv2/mod.rs b/roles/jd-client/src/lib/upstream_sv2/mod.rs deleted file mode 100644 index c7f9dcc53d..0000000000 --- a/roles/jd-client/src/lib/upstream_sv2/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -use codec_sv2::{StandardEitherFrame, StandardSv2Frame}; -use roles_logic_sv2::parsers::AnyMessage; - -pub mod upstream; -pub use upstream::Upstream; - -pub type Message = AnyMessage<'static>; -pub type StdFrame = StandardSv2Frame; -pub type EitherFrame = StandardEitherFrame; diff --git a/roles/jd-client/src/lib/upstream_sv2/upstream.rs b/roles/jd-client/src/lib/upstream_sv2/upstream.rs deleted file mode 100644 index adc8bce59d..0000000000 --- a/roles/jd-client/src/lib/upstream_sv2/upstream.rs +++ /dev/null @@ -1,960 +0,0 @@ -//! ## Upstream Module -//! -//! The `Upstream` module provides the necessary constructs and methods for establishing and -//! managing connections from the Job Declarator Client (JDC) to upstream pools, as well as handling -//! related message processing. -//! -//! It includes trait implementations required for representing an upstream connection, -//! intercepting messages from upstream nodes (pools), and generating appropriate responses. -//! -//! This module acts as the client-side implementation for communicating with a pool -//! that supports the SV2 Mining Protocol. -//! -//! Trait implementations within this module: -//! - [`IsUpstream`]: Represents a generic upstream connection (possibly redundant in this specific -//! structure). -//! - [`IsMiningUpstream`]: Represents an upstream connection specifically for the Mining Protocol -//! (possibly redundant). -//! - [`ParseCommonMessagesFromUpstream`]: Handles the interpretation of common SV2 messages like -//! `SetupConnectionSuccess` and `SetupConnectionError` received from the upstream pool during the -//! initial handshake. -//! - [`ParseMiningMessagesFromUpstream`]: Processes messages specific to the SV2 Mining -//! Protocol received from the upstream pool and determines how to handle them, potentially -//! relaying them to a downstream mining node or updating internal state. - -use super::super::downstream::DownstreamMiningNode as Downstream; - -use super::super::{ - error::{ - Error::{CodecNoise, PoisonLock, UpstreamIncoming}, - ProxyResult, - }, - status, - upstream_sv2::{EitherFrame, Message, StdFrame}, - PoolChangerTrigger, -}; -use async_channel::{Receiver, Sender}; -use binary_sv2::{Seq0255, U256}; -use codec_sv2::{HandshakeRole, Initiator}; -use error_handling::handle_result; -use key_utils::Secp256k1PublicKey; -use network_helpers_sv2::noise_connection::Connection; -use roles_logic_sv2::{ - channel_logic::channel_factory::PoolChannelFactory, - common_messages_sv2::{Protocol, Reconnect, SetupConnection}, - common_properties::{IsMiningUpstream, IsUpstream}, - handlers::{ - common::{ParseCommonMessagesFromUpstream, SendTo as SendToCommon}, - mining::{ParseMiningMessagesFromUpstream, SendTo, SupportedChannelTypes}, - }, - job_declaration_sv2::DeclareMiningJob, - mining_sv2::{ExtendedExtranonce, Extranonce, SetCustomMiningJob, SetGroupChannel}, - parsers::{AnyMessage, Mining, MiningDeviceMessages}, - utils::{Id, Mutex}, - Error as RolesLogicError, -}; -use std::{ - collections::{HashMap, VecDeque}, - net::SocketAddr, - sync::Arc, - thread::sleep, - time::Duration, -}; -use tokio::{net::TcpStream, task, task::AbortHandle}; -use tracing::{debug, error, info, warn}; - -// A fixed-capacity circular buffer used for storing mappings with a limited history. -// -// When a new element is inserted and the buffer is at capacity, the oldest element -// is automatically removed from the front. -#[derive(Debug)] -struct CircularBuffer { - // The internal `VecDeque` storing the key-value pairs. - buffer: VecDeque<(u64, u32)>, - // The maximum number of elements the buffer can hold. - capacity: usize, -} - -impl CircularBuffer { - // Creates a new `CircularBuffer` with the specified capacity. - fn new(capacity: usize) -> Self { - CircularBuffer { - buffer: VecDeque::with_capacity(capacity), - capacity, - } - } - - // Inserts a new key-value pair into the buffer. - fn insert(&mut self, key: u64, value: u32) { - if self.buffer.len() == self.capacity { - self.buffer.pop_front(); - } - self.buffer.push_back((key, value)); - } - - // Retrieves the value associated with a given key from the buffer. - fn get(&self, id: u64) -> Option { - self.buffer - .iter() - .find_map(|&(key, value)| if key == id { Some(value) } else { None }) - } -} - -impl std::default::Default for CircularBuffer { - fn default() -> Self { - Self::new(10) - } -} - -// Maintains mappings between request IDs, template IDs, and upstream-assigned job IDs. -// -// This struct is used to correlate different identifiers received from the -// Template Provider and the upstream pool throughout the job declaration process. -#[derive(Debug, Default)] -struct TemplateToJobId { - // A circular buffer mapping Template IDs (u64) to upstream-assigned Job IDs (u32). - // This buffer has a limited capacity to store recent mappings. - template_id_to_job_id: CircularBuffer, - // A HashMap mapping Request IDs (u32) from `DeclareMiningJob` messages to Template IDs (u64). - request_id_to_template_id: HashMap, -} - -impl TemplateToJobId { - // Registers a mapping between a Request ID and a Template ID. - // - // This is typically done when a `DeclareMiningJob` is sent, associating the - // request ID with the template ID it's based on. - fn register_template_id(&mut self, template_id: u64, request_id: u32) { - self.request_id_to_template_id - .insert(request_id, template_id); - } - - // Registers a mapping between a Template ID and an upstream-assigned Job ID. - // - // This is typically done when a `SetCustomMiningJobSuccess` message is received - // from the upstream pool, which provides the upstream's job ID for a previously - // declared job (associated with a template ID). This mapping is stored in - // the circular buffer. - fn register_job_id(&mut self, template_id: u64, job_id: u32) { - self.template_id_to_job_id.insert(template_id, job_id); - } - - // Retrieves the upstream-assigned Job ID for a given Template ID from the circular buffer. - fn get_job_id(&mut self, template_id: u64) -> Option { - self.template_id_to_job_id.get(template_id) - } - - // Removes and returns the Template ID associated with a given Request ID. - fn take_template_id(&mut self, request_id: u32) -> Option { - self.request_id_to_template_id.remove(&request_id) - } - - // Creates a new `TemplateToJobId` instance with a default-sized circular buffer. - fn new() -> Self { - Self::default() - } -} - -/// Upstream struct representing all possible requirement for upstream instantiation. -#[derive(Debug)] -pub struct Upstream { - // The channel ID assigned by the upstream pool for this connection. - // This is received in the `OpenExtendedMiningChannelSuccess` message. - channel_id: Option, - /// This allows the upstream threads to be able to communicate back to the main thread its - /// current status. - tx_status: status::Sender, - // The size of the `extranonce1` provided by the upstream pool. - // Currently hardcoded to 16, which is the only size the pool is expected to support. -> - // Inaccuracy. - #[allow(dead_code)] - pub upstream_extranonce1_size: usize, - // Receiver channel for incoming messages from the upstream pool. - pub receiver: Receiver, - // Sender channel for sending messages to the upstream pool. - pub sender: Sender, - /// `DownstreamMiningNode` instance, present when a downstream miner is connected. - pub downstream: Option>>, - task_collector: Arc>>, - // Trigger mechanism to detect unresponsive upstream behavior and initiate a pool change. - pool_chaneger_trigger: Arc>, - // Optional `PoolChannelFactory` instance. This factory is created upon receiving - // `OpenExtendedMiningChannelSuccess` and is used by the template provider client - // to check shares received from the downstream, simulating the upstream's - // channel logic - channel_factory: Option, - // Manager for mapping Template IDs, Request IDs, and upstream Job IDs. - template_to_job_id: TemplateToJobId, - // Simple ID generator for creating unique request IDs for messages sent to the upstream. - req_ids: Id, - // The JDC's signature, used in the `ExtendedExtranonce` calculation. - jdc_signature: String, -} - -impl Upstream { - /// This method sends message to upstream. - pub async fn send(self_: &Arc>, sv2_frame: StdFrame) -> ProxyResult<'static, ()> { - let sender = self_ - .safe_lock(|s| s.sender.clone()) - .map_err(|_| PoisonLock)?; - let either_frame = sv2_frame.into(); - sender.send(either_frame).await.map_err(|e| { - super::super::error::Error::ChannelErrorSender( - super::super::error::ChannelSendError::General(e.to_string()), - ) - })?; - Ok(()) - } - /// Instantiates a new `Upstream` connection to the specified SV2 pool address. - /// - /// This method establishes a TCP connection, performs the Noise handshake - /// , and initializes the `Upstream` struct with - /// the necessary communication channels and state managers. It includes - /// retry logic for the initial TCP connection attempt. - #[allow(clippy::too_many_arguments)] - pub async fn new( - address: SocketAddr, - authority_public_key: Secp256k1PublicKey, - tx_status: status::Sender, - task_collector: Arc>>, - pool_chaneger_trigger: Arc>, - jdc_signature: String, - ) -> ProxyResult<'static, Arc>> { - // Attempt to connect to the SV2 Upstream role (pool) with retry logic. - let socket = loop { - match TcpStream::connect(address).await { - Ok(socket) => break socket, - Err(e) => { - error!( - "Failed to connect to Upstream role at {}, retrying in 5s: {}", - address, e - ); - - sleep(Duration::from_secs(5)); - } - } - }; - - let pub_key: Secp256k1PublicKey = authority_public_key; - let initiator = Initiator::from_raw_k(pub_key.into_bytes())?; - - info!( - "PROXY SERVER - ACCEPTING FROM UPSTREAM: {}", - socket.peer_addr()? - ); - - // Channel to send and receive messages to the SV2 Upstream role - let (receiver, sender) = Connection::new(socket, HandshakeRole::Initiator(initiator)) - .await - .expect("Failed to create connection"); - - Ok(Arc::new(Mutex::new(Self { - channel_id: None, - upstream_extranonce1_size: 16, /* 16 is the default since that is the only value the - * pool supports currently */ - tx_status, - receiver, - sender, - downstream: None, - task_collector, - pool_chaneger_trigger, - channel_factory: None, - template_to_job_id: TemplateToJobId::new(), - req_ids: Id::new(), - jdc_signature, - }))) - } - - /// Setups the connection with the SV2 Upstream role (most typically a SV2 Pool). - pub async fn setup_connection( - self_: Arc>, - min_version: u16, - max_version: u16, - ) -> ProxyResult<'static, ()> { - // Get the `SetupConnection` message with Mining Device information (currently hard coded) - let setup_connection = Self::get_setup_connection_message(min_version, max_version, true)?; - - // Put the `SetupConnection` message in a `StdFrame` to be sent over the wire - let sv2_frame: StdFrame = Message::Common(setup_connection.into()).try_into()?; - // Send the `SetupConnection` frame to the SV2 Upstream role - // Only one Upstream role is supported, panics if multiple connections are encountered - Self::send(&self_, sv2_frame).await?; - - let recv = self_ - .safe_lock(|s| s.receiver.clone()) - .map_err(|_| PoisonLock)?; - - // Wait for the SV2 Upstream to respond with either a `SetupConnectionSuccess` or a - // `SetupConnectionError` inside a SV2 binary message frame - let mut incoming: StdFrame = match recv.recv().await { - Ok(frame) => frame.try_into()?, - Err(e) => { - error!("Upstream connection closed: {}", e); - return Err(CodecNoise( - codec_sv2::noise_sv2::Error::ExpectedIncomingHandshakeMessage, - )); - } - }; - - // Gets the binary frame message type from the message header - let message_type = if let Some(header) = incoming.get_header() { - header.msg_type() - } else { - return Err(framing_sv2::Error::ExpectedHandshakeFrame.into()); - }; - // Gets the message payload - let payload = incoming.payload(); - - // Handle the incoming message (should be either `SetupConnectionSuccess` or - // `SetupConnectionError`) - ParseCommonMessagesFromUpstream::handle_message_common( - self_.clone(), - message_type, - payload, - )?; - Ok(()) - } - - // Constructs and sends a `SetCustomMiningJob` message to the upstream pool. - // - // This method is called after a job is declared to the JDS and validated - // (receiving `DeclareMiningJobSuccess`). It takes the declared job details, - // the latest `SetNewPrevHash` information, and the signed mining job token - // to create the `SetCustomMiningJob` message. This message instructs the - // upstream pool to make this specific job available to connected downstream. - #[allow(clippy::too_many_arguments)] - pub async fn set_custom_jobs( - self_: &Arc>, - declare_mining_job: DeclareMiningJob<'static>, - set_new_prev_hash: roles_logic_sv2::template_distribution_sv2::SetNewPrevHash<'static>, - merkle_path: Seq0255<'static, U256<'static>>, - signed_token: binary_sv2::B0255<'static>, - coinbase_tx_version: u32, - coinbase_prefix: binary_sv2::B0255<'static>, - coinbase_tx_input_n_sequence: u32, - coinbase_tx_value_remaining: u64, - coinbase_tx_outs: Vec, - coinbase_tx_locktime: u32, - template_id: u64, - ) -> ProxyResult<'static, ()> { - info!("Sending set custom mining job"); - - // Get a new request ID for the SetCustomMiningJob message. - let request_id = self_.safe_lock(|s| s.req_ids.next()).unwrap(); - - // Wait until the channel ID is available (received in OpenExtendedMiningChannelSuccess). - let channel_id = loop { - if let Some(id) = self_.safe_lock(|s| s.channel_id).unwrap() { - break id; - }; - tokio::task::yield_now().await; - }; - - // Get the current timestamp for min_ntime. - let updated_timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() as u32; - - // Construct the SetCustomMiningJob message. - let to_send = SetCustomMiningJob { - channel_id, - request_id, - token: signed_token, - version: declare_mining_job.version, - prev_hash: set_new_prev_hash.prev_hash, - min_ntime: updated_timestamp, - nbits: set_new_prev_hash.n_bits, - coinbase_tx_version, - coinbase_prefix, - coinbase_tx_input_n_sequence, - coinbase_tx_value_remaining, - coinbase_tx_outputs: coinbase_tx_outs.try_into().unwrap(), - coinbase_tx_locktime, - merkle_path, - }; - let message = AnyMessage::Mining(Mining::SetCustomMiningJob(to_send)); - let frame: StdFrame = message.try_into().unwrap(); - - // Register the mapping between the template ID and the request ID for this message. - self_ - .safe_lock(|s| { - s.template_to_job_id - .register_template_id(template_id, request_id) - }) - .unwrap(); - Self::send(self_, frame).await - } - - /// Parses incoming SV2 messages from the Upstream role and routes them to the - /// appropriate handler for processing. - /// - /// This is the main loop for receiving and processing messages from the pool. - /// It dispatches mining-specific messages to the `ParseMiningMessagesFromUpstream` - /// trait implementation. Based on the handler's return value (`SendTo`), it - /// either relays the message to the downstream mining node or performs other actions. - /// Errors during message handling or receiving are reported via the status channel. - #[allow(clippy::result_large_err)] - pub fn parse_incoming(self_: Arc>) -> ProxyResult<'static, ()> { - let (recv, tx_status) = self_ - .safe_lock(|s| (s.receiver.clone(), s.tx_status.clone())) - .map_err(|_| PoisonLock)?; - - // Spawn the main task for receiving and processing upstream messages. - let main_task = { - let self_ = self_.clone(); - task::spawn(async move { - loop { - // Waiting to receive a message from the SV2 Upstream role - let incoming = handle_result!(tx_status, recv.recv().await); - let mut incoming: StdFrame = handle_result!(tx_status, incoming.try_into()); - // On message receive, get the message type from the message header and get the - // message payload - let message_type = - incoming - .get_header() - .ok_or(super::super::error::Error::FramingSv2( - framing_sv2::Error::ExpectedSv2Frame, - )); - - let message_type = handle_result!(tx_status, message_type).msg_type(); - - let payload = incoming.payload(); - - // Gets the response message for the received SV2 Upstream role message - // `handle_message_mining` takes care of the SetupConnection + - // SetupConnection.Success - let next_message_to_send = - Upstream::handle_message_mining(self_.clone(), message_type, payload); - - // Routes the incoming messages accordingly - match next_message_to_send { - // This is a transparent proxy it will only relay messages as received - Ok(SendTo::RelaySameMessageToRemote(downstream_mutex)) => { - let sv2_frame: codec_sv2::Sv2Frame< - MiningDeviceMessages, - buffer_sv2::Slice, - > = incoming.map(|payload| payload.try_into().unwrap()); - Downstream::send(&downstream_mutex, sv2_frame) - .await - .unwrap(); - } - // No need to handle impossible state just panic cause are impossible and we - // will never panic ;-) Verified: handle_message_mining only either panics, - // returns Ok(SendTo::None(None)) or Ok(SendTo::None(Some(m))), or returns - // Err This is a transparent proxy it will only - // relay messages as received - Ok(SendTo::None(_)) => (), - Ok(_) => unreachable!(), - Err(e) => { - let status = status::Status { - state: status::State::UpstreamShutdown(UpstreamIncoming(e)), - }; - error!( - "TERMINATING: Error handling pool role message: {:?}", - status - ); - if let Err(e) = tx_status.send(status).await { - error!("Status channel down: {:?}", e); - } - - break; - } - } - } - }) - }; - self_ - .safe_lock(|s| { - s.task_collector - .safe_lock(|c| c.push(main_task.abort_handle())) - .unwrap() - }) - .unwrap(); - Ok(()) - } - - /// Creates the `SetupConnection` message to setup the connection with the SV2 Upstream role. - /// TODO: The Mining Device information is hard coded here, need to receive from Downstream - /// instead. - #[allow(clippy::result_large_err)] - fn get_setup_connection_message( - min_version: u16, - max_version: u16, - is_work_selection_enabled: bool, - ) -> ProxyResult<'static, SetupConnection<'static>> { - let endpoint_host = "0.0.0.0".to_string().into_bytes().try_into()?; - let vendor = String::new().try_into()?; - let hardware_version = String::new().try_into()?; - let firmware = String::new().try_into()?; - let device_id = String::new().try_into()?; - let flags = match is_work_selection_enabled { - false => 0b0000_0000_0000_0000_0000_0000_0000_0100, - true => 0b0000_0000_0000_0000_0000_0000_0000_0110, - }; - Ok(SetupConnection { - protocol: Protocol::MiningProtocol, - min_version, - max_version, - flags, - endpoint_host, - endpoint_port: 50, - vendor, - hardware_version, - firmware, - device_id, - }) - } - - /// This method provides the `PoolChannelFactory` once it has been created - /// (upon receiving `OpenExtendedMiningChannelSuccess`). - /// - /// This method is used by other components (like the template provider client) - /// that need access to the channel factory to perform share validation or - /// other channel-related operations. It waits until the factory is available. - pub async fn take_channel_factory(self_: Arc>) -> PoolChannelFactory { - // Wait until the channel_factory field is populated. - while self_.safe_lock(|s| s.channel_factory.is_none()).unwrap() { - tokio::task::yield_now().await; - } - self_ - .safe_lock(|s| { - let mut factory = None; - std::mem::swap(&mut s.channel_factory, &mut factory); - factory.unwrap() - }) - .unwrap() - } - - /// This method retrieves the upstream-assigned job ID for a given template ID. - /// - /// This method checks the `template_to_job_id` mapper. If the mapping is not - /// immediately available (because `SetCustomMiningJobSuccess` hasn't been - /// processed yet), it waits until the job ID is registered. - pub async fn get_job_id(self_: &Arc>, template_id: u64) -> u32 { - loop { - if let Some(id) = self_ - .safe_lock(|s| s.template_to_job_id.get_job_id(template_id)) - .unwrap() - { - return id; - } - tokio::task::yield_now().await; - } - } -} - -// not really used.. -impl IsUpstream for Upstream { - fn get_version(&self) -> u16 { - todo!() - } - - fn get_flags(&self) -> u32 { - todo!() - } - - fn get_supported_protocols(&self) -> Vec { - todo!() - } - - fn get_id(&self) -> u32 { - todo!() - } - - fn get_mapper(&mut self) -> Option<&mut roles_logic_sv2::common_properties::RequestIdMapper> { - todo!() - } -} - -// Not really used... -impl IsMiningUpstream for Upstream { - fn total_hash_rate(&self) -> u64 { - todo!() - } - - fn add_hash_rate(&mut self, _to_add: u64) { - todo!() - } - - fn get_opened_channels( - &mut self, - ) -> &mut Vec { - todo!() - } - - fn update_channels(&mut self, _c: roles_logic_sv2::common_properties::UpstreamChannel) { - todo!() - } -} - -impl ParseCommonMessagesFromUpstream for Upstream { - // Handles a `SetupConnectionSuccess` message received from the upstream pool. - // - // Returns `Ok(SendToCommon::None(None))` as no immediate response is required. - fn handle_setup_connection_success( - &mut self, - m: roles_logic_sv2::common_messages_sv2::SetupConnectionSuccess, - ) -> Result { - info!( - "Received `SetupConnectionSuccess` from Pool: version={}, flags={:b}", - m.used_version, m.flags - ); - Ok(SendToCommon::None(None)) - } - - fn handle_setup_connection_error( - &mut self, - _: roles_logic_sv2::common_messages_sv2::SetupConnectionError, - ) -> Result { - todo!() - } - - fn handle_channel_endpoint_changed( - &mut self, - _: roles_logic_sv2::common_messages_sv2::ChannelEndpointChanged, - ) -> Result { - todo!() - } - - fn handle_reconnect(&mut self, _m: Reconnect) -> Result { - todo!() - } -} - -/// Connection-wide SV2 Upstream role messages parser implemented by a downstream ("downstream" -/// here is relative to the SV2 Upstream role and is represented by this `Upstream` struct). -impl ParseMiningMessagesFromUpstream for Upstream { - // Returns the channel type supported between the SV2 Upstream role (pool) and this - // `Upstream` instance. For a JDC, this is always `Extended` - fn get_channel_type(&self) -> SupportedChannelTypes { - SupportedChannelTypes::Extended - } - - // Indicates whether work selection is enabled for this connection.. - fn is_work_selection_enabled(&self) -> bool { - true - } - - /// Handles an `OpenStandardMiningChannelSuccess` message. - /// - /// This method panics because standard mining channels are explicitly NOT - /// used between the JDC and the SV2 Upstream role. - /// Only Extended channels are expected. - fn handle_open_standard_mining_channel_success( - &mut self, - _m: roles_logic_sv2::mining_sv2::OpenStandardMiningChannelSuccess, - ) -> Result, RolesLogicError> { - panic!("Standard Mining Channels are not used in Translator Proxy") - } - - // Handles an `OpenExtendedMiningChannelSuccess` message received from the upstream pool. - // - // This message confirms that an extended mining channel has been successfully opened. - // It provides the assigned `channel_id`, `extranonce_prefix`, `extranonce_size`, and `target`. - // This method uses this information to: - // 1. Store the assigned `channel_id`. - // 2. Create a `PoolChannelFactory` instance that simulates the upstream's channel logic for the - // template provider client to use in share validation. - // 3. Relays the original `OpenExtendedMiningChannelSuccess` message to the downstream mining - // node if one is connected. - // - // Returns `Ok(SendTo::RelaySameMessageToRemote(downstream_mutex))` if a downstream - // is connected, indicating the message should be relayed. - fn handle_open_extended_mining_channel_success( - &mut self, - m: roles_logic_sv2::mining_sv2::OpenExtendedMiningChannelSuccess, - ) -> Result, RolesLogicError> { - info!( - "Received OpenExtendedMiningChannelSuccess with request id: {} and channel id: {}", - m.request_id, m.channel_id - ); - debug!("OpenStandardMiningChannelSuccess: {:?}", m); - // --- Create the PoolChannelFactory --- - let ids = Arc::new(Mutex::new(roles_logic_sv2::utils::GroupId::new())); - let jdc_signature_len = self.jdc_signature.len(); - let prefix_len = m.extranonce_prefix.to_vec().len(); - let self_len = 0; - let total_len = prefix_len + m.extranonce_size as usize; - let range_0 = 0..prefix_len; - let range_1 = prefix_len..prefix_len + jdc_signature_len + self_len; - let range_2 = prefix_len + jdc_signature_len + self_len..total_len; - - // Create an ExtendedExtranonce structure defining the layout of the extranonce. - let extranonces = ExtendedExtranonce::new( - range_0, - range_1, - range_2, - Some(self.jdc_signature.as_bytes().to_vec()), - ) - .map_err(|err| RolesLogicError::ExtendedExtranonceCreationFailed(format!("{:?}", err)))?; - - // Job creator for the factory. - let creator = roles_logic_sv2::job_creator::JobsCreators::new(total_len as u8); - // Placeholder shares per minute - let share_per_min = 1.0; - - let channel_kind = - roles_logic_sv2::channel_logic::channel_factory::ExtendedChannelKind::ProxyJd { - upstream_target: m.target.clone().into(), - }; - - // Create the PoolChannelFactory instance. - let mut channel_factory = PoolChannelFactory::new( - ids, - extranonces, - creator, - share_per_min, - channel_kind, - vec![], - ); - - // Replicate the upstream's extended channel information within the factory. - let extranonce: Extranonce = m - .extranonce_prefix - .into_static() - .to_vec() - .try_into() - .unwrap(); - - // Store the assigned channel ID. - self.channel_id = Some(m.channel_id); - channel_factory - .replicate_upstream_extended_channel_only_jd( - m.target.into_static(), - extranonce, - m.channel_id, - m.extranonce_size, - ) - .expect("Impossible to open downstream channel"); - self.channel_factory = Some(channel_factory); - - Ok(SendTo::RelaySameMessageToRemote( - self.downstream.as_ref().unwrap().clone(), - )) - } - - // Handles an `OpenMiningChannelError` message received from the upstream pool. - // - // Returns `Ok(SendTo::RelaySameMessageToRemote(downstream_mutex))` to relay - // the message downstream. - fn handle_open_mining_channel_error( - &mut self, - m: roles_logic_sv2::mining_sv2::OpenMiningChannelError, - ) -> Result, RolesLogicError> { - error!( - "Received OpenExtendedMiningChannelError with error code {}", - std::str::from_utf8(m.error_code.as_ref()).unwrap_or("unknown error code") - ); - Ok(SendTo::RelaySameMessageToRemote( - self.downstream.as_ref().unwrap().clone(), - )) - } - - // Handles an `UpdateChannelError` message received from the upstream pool. - // - // Returns `Ok(SendTo::RelaySameMessageToRemote(downstream_mutex))` to relay - // the message downstream. - fn handle_update_channel_error( - &mut self, - m: roles_logic_sv2::mining_sv2::UpdateChannelError, - ) -> Result, RolesLogicError> { - error!( - "Received UpdateChannelError with error code {}", - std::str::from_utf8(m.error_code.as_ref()).unwrap_or("unknown error code") - ); - Ok(SendTo::RelaySameMessageToRemote( - self.downstream.as_ref().unwrap().clone(), - )) - } - - // Handles a `CloseChannel` message received from the upstream pool. - // - // Returns `Ok(SendTo::RelaySameMessageToRemote(downstream_mutex))` to relay - // the message downstream. - fn handle_close_channel( - &mut self, - m: roles_logic_sv2::mining_sv2::CloseChannel, - ) -> Result, RolesLogicError> { - info!("Received CloseChannel for channel id: {}", m.channel_id); - Ok(SendTo::RelaySameMessageToRemote( - self.downstream.as_ref().unwrap().clone(), - )) - } - - // Handles a `SetExtranoncePrefix` message received from the upstream pool. - // - // Returns `Ok(SendTo::RelaySameMessageToRemote(downstream_mutex))` to relay - // the message downstream. - fn handle_set_extranonce_prefix( - &mut self, - m: roles_logic_sv2::mining_sv2::SetExtranoncePrefix, - ) -> Result, RolesLogicError> { - info!( - "Received SetExtranoncePrefix for channel id: {}", - m.channel_id - ); - debug!("SetExtranoncePrefix: {:?}", m); - Ok(SendTo::RelaySameMessageToRemote( - self.downstream.as_ref().unwrap().clone(), - )) - } - - // Handles a `SubmitSharesSuccess` message received from the upstream pool. - // - // Returns `Ok(SendTo::RelaySameMessageToRemote(downstream_mutex))` to relay - // the message downstream. - fn handle_submit_shares_success( - &mut self, - m: roles_logic_sv2::mining_sv2::SubmitSharesSuccess, - ) -> Result, RolesLogicError> { - info!("Received SubmitSharesSuccess"); - debug!("SubmitSharesSuccess: {:?}", m); - Ok(SendTo::RelaySameMessageToRemote( - self.downstream.as_ref().unwrap().clone(), - )) - } - - // Handles a `SubmitSharesError` message received from the upstream pool. - // - // This message indicates that a share submitted by a miner was rejected by - // the pool. The current implementation logs the error code and triggers - // the `pool_changer_trigger`, which may initiate a pool fallback if multiple - // share errors occur. It does NOT relay the error message downstream, - // as the JDC handles pool fallback. - // - // Returns `Ok(SendTo::None(None))` as no message is relayed downstream in this case. - fn handle_submit_shares_error( - &mut self, - m: roles_logic_sv2::mining_sv2::SubmitSharesError, - ) -> Result, RolesLogicError> { - error!( - "Received SubmitSharesError with error code {}", - std::str::from_utf8(m.error_code.as_ref()).unwrap_or("unknown error code") - ); - self.pool_chaneger_trigger - .safe_lock(|t| t.start(self.tx_status.clone())) - .unwrap(); - Ok(SendTo::None(None)) - } - - // Handles a `NewMiningJob` message. - fn handle_new_mining_job( - &mut self, - _m: roles_logic_sv2::mining_sv2::NewMiningJob, - ) -> Result, RolesLogicError> { - panic!("Standard Mining Channels are not used in Translator Proxy") - } - - // Handles a `NewExtendedMiningJob` message received from the upstream pool. - // - // This message provides a new mining job using the extended format. However, - // in this JDC implementation, the job information is primarily derived from - // the Template Provider and Job Declarator. Therefore, this message from - // the upstream pool is logged as a warning and ignored, as the JDC relies - // on its declared jobs. - // - // Returns `Ok(SendTo::None(None))` indicating that the message is processed - // but no action or response is needed. - fn handle_new_extended_mining_job( - &mut self, - _: roles_logic_sv2::mining_sv2::NewExtendedMiningJob, - ) -> Result, RolesLogicError> { - warn!("Extended job received from upstream, proxy ignore it, and use the one declared by JOB DECLARATOR"); - Ok(SendTo::None(None)) - } - - // Handles a `SetNewPrevHash` message received from the upstream pool. - // - // This message indicates that the previous block hash has changed. Similar - // to `NewExtendedMiningJob`, this message from the upstream pool is logged - // as a warning and ignored, as the JDC relies on the `SetNewPrevHash` received - // from the Template Provider which triggers the promotion of future jobs - // declared via the JDS. - // - // Returns `Ok(SendTo::None(None))` indicating that the message is processed - // but no action or response is needed. - fn handle_set_new_prev_hash( - &mut self, - _: roles_logic_sv2::mining_sv2::SetNewPrevHash, - ) -> Result, RolesLogicError> { - warn!("SNPH received from upstream, proxy ignored it, and used the one declared by JDC"); - Ok(SendTo::None(None)) - } - - // Handles a `SetCustomMiningJobSuccess` message received from the upstream pool. - // - // This message confirms that a `SetCustomMiningJob` request previously sent - // by the JDC has been successfully processed by the upstream pool. It provides - // the upstream's assigned `job_id` for this job. This method logs the success - // and registers the mapping between the original template ID (derived from - // the `request_id`) and the upstream's `job_id` in the `template_to_job_id` mapper. - // - // Returns `Ok(SendTo::None(None))` as no message is relayed downstream for this event. - fn handle_set_custom_mining_job_success( - &mut self, - m: roles_logic_sv2::mining_sv2::SetCustomMiningJobSuccess, - ) -> Result, RolesLogicError> { - // TODO - info!( - "Received SetCustomMiningJobSuccess for channel id: {} for job id: {}", - m.channel_id, m.job_id - ); - debug!("SetCustomMiningJobSuccess: {:?}", m); - if let Some(template_id) = self.template_to_job_id.take_template_id(m.request_id) { - self.template_to_job_id - .register_job_id(template_id, m.job_id); - Ok(SendTo::None(None)) - } else { - error!("Attention received a SetupConnectionSuccess with unknown request_id"); - Ok(SendTo::None(None)) - } - } - - // Handles a `SetCustomMiningJobError` message received from the upstream pool. - fn handle_set_custom_mining_job_error( - &mut self, - _m: roles_logic_sv2::mining_sv2::SetCustomMiningJobError, - ) -> Result, RolesLogicError> { - todo!() - } - - // Handles a `SetTarget` message received from the upstream pool. - // - // This message updates the mining target (difficulty) for a specific channel. - // This method updates the target in the internal `PoolChannelFactory` and - // in the downstream mining node's channel status to ensure miners are working - // on the correct difficulty. It also relays the original message downstream. - // - // Returns `Ok(SendTo::RelaySameMessageToRemote(downstream_mutex))` to relay - // the message downstream. - fn handle_set_target( - &mut self, - m: roles_logic_sv2::mining_sv2::SetTarget, - ) -> Result, RolesLogicError> { - info!("Received SetTarget for channel id: {}", m.channel_id); - debug!("SetTarget: {:?}", m); - if let Some(factory) = self.channel_factory.as_mut() { - factory.update_target_for_channel(m.channel_id, m.maximum_target.clone().into()); - factory.set_target(&mut m.maximum_target.clone().into()); - } - if let Some(downstream) = &self.downstream { - let _ = downstream.safe_lock(|d| { - let factory = d.status.get_channel(); - factory.set_target(&mut m.maximum_target.clone().into()); - factory.update_target_for_channel(m.channel_id, m.maximum_target.into()); - }); - } - Ok(SendTo::RelaySameMessageToRemote( - self.downstream.as_ref().unwrap().clone(), - )) - } - - // Handles a `SetGroupChannel` message received from the upstream pool. Not implemented. - fn handle_set_group_channel( - &mut self, - _m: SetGroupChannel, - ) -> Result, RolesLogicError> { - todo!() - } -} diff --git a/roles/jd-client/src/lib/utils.rs b/roles/jd-client/src/lib/utils.rs new file mode 100644 index 0000000000..bf080a1260 --- /dev/null +++ b/roles/jd-client/src/lib/utils.rs @@ -0,0 +1,582 @@ +//! Utilities for managing JDC communication, connection setup, +//! shutdown signaling, and upstream state tracking. +//! +//! This module provides: +//! - Construction of `SetupConnection` messages for mining, job declarator, and template +//! distribution protocols. +//! - Helpers for parsing frames into typed Stratum messages. +//! - An async I/O task spawner for handling framed network communication with shutdown +//! coordination. +//! - Deserialization of coinbase transaction outputs. +//! - Shutdown signaling types for orchestrating controlled shutdown of upstream, downstream, and +//! job declarator components. +//! - An atomic wrapper for managing the upstream connection state safely across threads. +use std::{ + net::SocketAddr, + sync::{ + atomic::{AtomicU8, Ordering}, + Arc, + }, +}; + +use async_channel::{Receiver, Sender}; +use stratum_common::{ + network_helpers_sv2::noise_stream::{NoiseTcpReadHalf, NoiseTcpWriteHalf}, + roles_logic_sv2::{ + codec_sv2::{binary_sv2::Str0255, Frame, StandardEitherFrame, StandardSv2Frame, Sv2Frame}, + common_messages_sv2::{ + Protocol, SetupConnection, MESSAGE_TYPE_CHANNEL_ENDPOINT_CHANGED, + MESSAGE_TYPE_RECONNECT, MESSAGE_TYPE_SETUP_CONNECTION, + MESSAGE_TYPE_SETUP_CONNECTION_ERROR, MESSAGE_TYPE_SETUP_CONNECTION_SUCCESS, + }, + job_declaration_sv2::{ + MESSAGE_TYPE_ALLOCATE_MINING_JOB_TOKEN, MESSAGE_TYPE_ALLOCATE_MINING_JOB_TOKEN_SUCCESS, + MESSAGE_TYPE_DECLARE_MINING_JOB, MESSAGE_TYPE_DECLARE_MINING_JOB_ERROR, + MESSAGE_TYPE_DECLARE_MINING_JOB_SUCCESS, MESSAGE_TYPE_PROVIDE_MISSING_TRANSACTIONS, + MESSAGE_TYPE_PROVIDE_MISSING_TRANSACTIONS_SUCCESS, MESSAGE_TYPE_PUSH_SOLUTION, + }, + mining_sv2::{ + CloseChannel, OpenExtendedMiningChannel, OpenStandardMiningChannel, + MESSAGE_TYPE_CLOSE_CHANNEL, MESSAGE_TYPE_MINING_SET_NEW_PREV_HASH, + MESSAGE_TYPE_NEW_EXTENDED_MINING_JOB, MESSAGE_TYPE_NEW_MINING_JOB, + MESSAGE_TYPE_OPEN_EXTENDED_MINING_CHANNEL, + MESSAGE_TYPE_OPEN_EXTENDED_MINING_CHANNEL_SUCCESS, + MESSAGE_TYPE_OPEN_MINING_CHANNEL_ERROR, MESSAGE_TYPE_OPEN_STANDARD_MINING_CHANNEL, + MESSAGE_TYPE_OPEN_STANDARD_MINING_CHANNEL_SUCCESS, MESSAGE_TYPE_SET_CUSTOM_MINING_JOB, + MESSAGE_TYPE_SET_CUSTOM_MINING_JOB_ERROR, MESSAGE_TYPE_SET_CUSTOM_MINING_JOB_SUCCESS, + MESSAGE_TYPE_SET_EXTRANONCE_PREFIX, MESSAGE_TYPE_SET_GROUP_CHANNEL, + MESSAGE_TYPE_SET_TARGET, MESSAGE_TYPE_SUBMIT_SHARES_ERROR, + MESSAGE_TYPE_SUBMIT_SHARES_EXTENDED, MESSAGE_TYPE_SUBMIT_SHARES_STANDARD, + MESSAGE_TYPE_SUBMIT_SHARES_SUCCESS, MESSAGE_TYPE_UPDATE_CHANNEL, + MESSAGE_TYPE_UPDATE_CHANNEL_ERROR, + }, + parsers_sv2::{AnyMessage, Mining}, + template_distribution_sv2::{ + MESSAGE_TYPE_COINBASE_OUTPUT_CONSTRAINTS, MESSAGE_TYPE_NEW_TEMPLATE, + MESSAGE_TYPE_REQUEST_TRANSACTION_DATA, MESSAGE_TYPE_REQUEST_TRANSACTION_DATA_ERROR, + MESSAGE_TYPE_REQUEST_TRANSACTION_DATA_SUCCESS, MESSAGE_TYPE_SET_NEW_PREV_HASH, + MESSAGE_TYPE_SUBMIT_SOLUTION, + }, + }, +}; +use tokio::sync::broadcast; +use tracing::{error, trace, warn, Instrument}; + +use crate::{ + config::ConfigJDCMode, + error::JDCError, + status::{StatusSender, StatusType}, + task_manager::TaskManager, +}; + +pub type Message = AnyMessage<'static>; +pub type StdFrame = StandardSv2Frame; +pub type EitherFrame = StandardEitherFrame; +pub type SV2Frame = Sv2Frame; +/// Represents a message that can trigger shutdown of various system components. +#[derive(Debug, Clone)] +pub enum ShutdownMessage { + /// Shutdown all components immediately + ShutdownAll, + /// Shutdown all downstream connections + DownstreamShutdownAll, + /// Shutdown a specific downstream connection by ID + DownstreamShutdown(u32), + /// Shutdown Upstream and JD part of JDC during fallback + JobDeclaratorShutdownFallback((Vec, tokio::sync::mpsc::Sender<()>)), + /// Shutdown Upstream and JD part during fallback + UpstreamShutdownFallback((Vec, tokio::sync::mpsc::Sender<()>)), + /// Shutdown Job Declarator during initialization. + JobDeclaratorShutdown(tokio::sync::mpsc::Sender<()>), + /// Shutdown Job Declarator during initialization. + UpstreamShutdown(tokio::sync::mpsc::Sender<()>), +} + +/// Constructs a `SetupConnection` message for the mining protocol. +pub fn get_setup_connection_message( + min_version: u16, + max_version: u16, +) -> Result, JDCError> { + let endpoint_host = "0.0.0.0".to_string().into_bytes().try_into()?; + let vendor = String::new().try_into()?; + let hardware_version = String::new().try_into()?; + let firmware = String::new().try_into()?; + let device_id = String::new().try_into()?; + let flags = 0b0000_0000_0000_0000_0000_0000_0000_0110; + Ok(SetupConnection { + protocol: Protocol::MiningProtocol, + min_version, + max_version, + flags, + endpoint_host, + endpoint_port: 50, + vendor, + hardware_version, + firmware, + device_id, + }) +} + +/// Constructs a `SetupConnection` message for the Job Declarator (JDS). +pub fn get_setup_connection_message_jds( + proxy_address: &SocketAddr, + mode: &ConfigJDCMode, +) -> SetupConnection<'static> { + let endpoint_host = proxy_address + .ip() + .to_string() + .into_bytes() + .try_into() + .unwrap(); + let vendor = String::new().try_into().unwrap(); + let hardware_version = String::new().try_into().unwrap(); + let firmware = String::new().try_into().unwrap(); + let device_id = String::new().try_into().unwrap(); + let mut setup_connection = SetupConnection { + protocol: Protocol::JobDeclarationProtocol, + min_version: 2, + max_version: 2, + flags: 0b0000_0000_0000_0000_0000_0000_0000_0000, + endpoint_host, + endpoint_port: proxy_address.port(), + vendor, + hardware_version, + firmware, + device_id, + }; + + if matches!(mode, ConfigJDCMode::FullTemplate) { + setup_connection.allow_full_template_mode(); + } + + setup_connection +} + +/// Constructs a `SetupConnection` message for the Template Provider (TP). +pub fn get_setup_connection_message_tp(address: SocketAddr) -> SetupConnection<'static> { + let endpoint_host = address.ip().to_string().into_bytes().try_into().unwrap(); + let vendor = String::new().try_into().unwrap(); + let hardware_version = String::new().try_into().unwrap(); + let firmware = String::new().try_into().unwrap(); + let device_id = String::new().try_into().unwrap(); + SetupConnection { + protocol: Protocol::TemplateDistributionProtocol, + min_version: 2, + max_version: 2, + flags: 0b0000_0000_0000_0000_0000_0000_0000_0000, + endpoint_host, + endpoint_port: address.port(), + vendor, + hardware_version, + firmware, + device_id, + } +} + +/// Spawns async reader and writer tasks for handling framed I/O with shutdown support. +#[track_caller] +#[allow(clippy::too_many_arguments)] +pub fn spawn_io_tasks( + task_manager: Arc, + mut reader: NoiseTcpReadHalf, + mut writer: NoiseTcpWriteHalf, + outbound_rx: Receiver, + inbound_tx: Sender, + notify_shutdown: broadcast::Sender, + status_sender: StatusSender, +) { + let caller = std::panic::Location::caller(); + let inbound_tx_clone = inbound_tx.clone(); + let outbound_rx_clone = outbound_rx.clone(); + { + let mut shutdown_rx = notify_shutdown.subscribe(); + let status_sender = status_sender.clone(); + let status_type: StatusType = StatusType::from(&status_sender); + + task_manager.spawn(async move { + trace!("Reader task started"); + loop { + tokio::select! { + message = shutdown_rx.recv() => { + match message { + Ok(ShutdownMessage::ShutdownAll) => { + trace!("Received global shutdown"); + inbound_tx.close(); + break; + } + Ok(ShutdownMessage::DownstreamShutdown(down_id)) if matches!(status_type, StatusType::Downstream(id) if id == down_id) => { + trace!(down_id, "Received downstream shutdown"); + if status_type != StatusType::TemplateReceiver { + inbound_tx.close(); + break; + } + } + Ok(ShutdownMessage::JobDeclaratorShutdownFallback(_)) if !matches!(status_type, StatusType::TemplateReceiver) => { + trace!("Received job declarator shutdown"); + if status_type != StatusType::TemplateReceiver { + inbound_tx.close(); + break; + } + } + Ok(ShutdownMessage::UpstreamShutdownFallback(_)) if !matches!(status_type, StatusType::TemplateReceiver) => { + trace!("Received upstream shutdown"); + if status_type != StatusType::TemplateReceiver { + inbound_tx.close(); + break; + } + } + + Ok(ShutdownMessage::UpstreamShutdown(tx)) if !matches!(status_type, StatusType::TemplateReceiver) => { + trace!("Received upstream shutdown"); + if status_type != StatusType::TemplateReceiver { + inbound_tx.close(); + break; + } + drop(tx); + } + Ok(ShutdownMessage::JobDeclaratorShutdown(tx)) if !matches!(status_type, StatusType::TemplateReceiver) => { + trace!("Received upstream shutdown"); + if status_type != StatusType::TemplateReceiver { + inbound_tx.close(); + break; + } + drop(tx); + } + _ => {} + } + } + res = reader.read_frame() => { + match res { + Ok(frame) => { + match frame { + Frame::HandShake(frame) => { + error!(?frame, "Received handshake frame"); + drop(frame); + break; + }, + Frame::Sv2(sv2_frame) => { + trace!("Received inbound frame"); + if let Err(e) = inbound_tx.send(sv2_frame).await { + inbound_tx.close(); + error!(error=?e, "Failed to forward inbound frame"); + break; + } + }, + } + } + Err(e) => { + error!(error=?e, "Reader error"); + inbound_tx.close(); + break; + } + } + } + } + } + inbound_tx.close(); + outbound_rx_clone.close(); + drop(inbound_tx); + drop(outbound_rx_clone); + warn!("Reader task exited."); + }.instrument(tracing::info_span!( + "reader_task", + spawned_at = %format!("{}:{}", caller.file(), caller.line()) + ))); + } + + { + let mut shutdown_rx = notify_shutdown.subscribe(); + let status_type: StatusType = StatusType::from(&status_sender); + + task_manager.spawn(async move { + trace!("Writer task started"); + loop { + tokio::select! { + message = shutdown_rx.recv() => { + match message { + Ok(ShutdownMessage::ShutdownAll) => { + trace!("Received global shutdown"); + outbound_rx.close(); + break; + } + Ok(ShutdownMessage::DownstreamShutdown(down_id)) if matches!(status_type, StatusType::Downstream(id) if id == down_id) => { + trace!(down_id, "Received downstream shutdown"); + if status_type != StatusType::TemplateReceiver { + outbound_rx.close(); + break; + } + } + Ok(ShutdownMessage::JobDeclaratorShutdownFallback(_)) if !matches!(status_type, StatusType::TemplateReceiver) => { + trace!("Received job declarator shutdown"); + if status_type != StatusType::TemplateReceiver { + outbound_rx.close(); + break; + } + } + Ok(ShutdownMessage::UpstreamShutdownFallback(_)) if !matches!(status_type, StatusType::TemplateReceiver) => { + trace!("Received upstream shutdown"); + if status_type != StatusType::TemplateReceiver { + outbound_rx.close(); + break; + } + } + Ok(ShutdownMessage::UpstreamShutdown(tx)) if !matches!(status_type, StatusType::TemplateReceiver) => { + trace!("Received upstream shutdown"); + if status_type != StatusType::TemplateReceiver { + outbound_rx.close(); + break; + } + drop(tx); + } + Ok(ShutdownMessage::JobDeclaratorShutdown(tx)) if !matches!(status_type, StatusType::TemplateReceiver) => { + trace!("Received upstream shutdown"); + if status_type != StatusType::TemplateReceiver { + outbound_rx.close(); + break; + } + drop(tx); + } + _ => {} + } + } + res = outbound_rx.recv() => { + match res { + Ok(frame) => { + trace!("Sending outbound frame"); + if let Err(e) = writer.write_frame(frame.into()).await { + error!(error=?e, "Writer error"); + outbound_rx.close(); + break; + } + } + Err(_) => { + outbound_rx.close(); + warn!("Outbound channel closed"); + break; + } + } + } + } + } + outbound_rx.close(); + inbound_tx_clone.close(); + drop(outbound_rx); + drop(inbound_tx_clone); + warn!("Writer task exited."); + }.instrument(tracing::info_span!( + "writer_task", + spawned_at = %format!("{}:{}", caller.file(), caller.line()) + ))); + } +} + +/// Represents the state of the upstream connection. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UpstreamState { + /// No channel established with upstream. + NoChannel = 0, + /// Channel is being established undergoing. + Pending = 1, + /// Channel is active and connected. + Connected = 2, + /// Running in solo mining mode. + SoloMining = 3, +} + +/// Atomic wrapper for managing upstream connection state safely across threads. +#[derive(Clone)] +pub struct AtomicUpstreamState { + inner: Arc, +} + +impl AtomicUpstreamState { + /// Creates a new atomic upstream state. + pub fn new(state: UpstreamState) -> Self { + Self { + inner: Arc::new(AtomicU8::new(state as u8)), + } + } + + /// Returns the current upstream state. + pub fn get(&self) -> UpstreamState { + match self.inner.load(Ordering::SeqCst) { + 0 => UpstreamState::NoChannel, + 1 => UpstreamState::Pending, + 2 => UpstreamState::Connected, + 3 => UpstreamState::SoloMining, + _ => unreachable!("invalid upstream state"), + } + } + + /// Updates the upstream state + pub fn set(&self, state: UpstreamState) { + self.inner.store(state as u8, Ordering::SeqCst); + } + + /// Conditionally updates the upstream state if the current value matches. + pub fn compare_and_set( + &self, + current: UpstreamState, + new: UpstreamState, + ) -> Result<(), UpstreamState> { + self.inner + .compare_exchange(current as u8, new as u8, Ordering::SeqCst, Ordering::SeqCst) + .map(|_| ()) + .map_err(|v| match v { + 0 => UpstreamState::NoChannel, + 1 => UpstreamState::Pending, + 2 => UpstreamState::Connected, + 3 => UpstreamState::SoloMining, + _ => unreachable!("invalid upstream state"), + }) + } +} + +/// Represents a pending channel request during the bootstrap phase +/// of the Job Declarator Client (JDC). +/// +/// These requests are created by downstreams that want to open +/// a mining channel but cannot proceed immediately. +/// They remain queued until an upstream channel is successfully opened, +/// at which point they can be processed. +/// +/// Two types of requests can be pending: +/// - [`OpenExtendedMiningChannel`] for extended mining channels +/// - [`OpenStandardMiningChannel`] for standard mining channels +pub enum PendingChannelRequest { + /// A request to open an extended mining channel. + ExtendedChannel(OpenExtendedMiningChannel<'static>), + /// A request to open a standard mining channel. + StandardChannel(OpenStandardMiningChannel<'static>), +} + +impl From> for PendingChannelRequest { + fn from(value: OpenExtendedMiningChannel<'static>) -> Self { + PendingChannelRequest::ExtendedChannel(value) + } +} + +impl From> for PendingChannelRequest { + fn from(value: OpenStandardMiningChannel<'static>) -> Self { + PendingChannelRequest::StandardChannel(value) + } +} + +impl From for Mining<'_> { + fn from(value: PendingChannelRequest) -> Self { + match value { + PendingChannelRequest::ExtendedChannel(m) => Mining::OpenExtendedMiningChannel(m), + PendingChannelRequest::StandardChannel(m) => Mining::OpenStandardMiningChannel(m), + } + } +} + +impl PendingChannelRequest { + pub fn message_type(&self) -> u8 { + match self { + PendingChannelRequest::ExtendedChannel(_) => MESSAGE_TYPE_OPEN_EXTENDED_MINING_CHANNEL, + PendingChannelRequest::StandardChannel(_) => MESSAGE_TYPE_OPEN_STANDARD_MINING_CHANNEL, + } + } +} + +/// Creates a [`CloseChannel`] message for the given channel ID and reason. +/// +/// The `msg` is converted into a [`Str0255`] reason code. +/// If conversion fails, this function will panic. +pub(crate) fn create_close_channel_msg(channel_id: u32, msg: &str) -> CloseChannel<'_> { + CloseChannel { + channel_id, + reason_code: Str0255::try_from(msg.to_string()).expect("Could not convert message."), + } +} + +pub fn is_common_message(message_type: u8) -> bool { + matches!( + message_type, + MESSAGE_TYPE_SETUP_CONNECTION + | MESSAGE_TYPE_SETUP_CONNECTION_SUCCESS + | MESSAGE_TYPE_SETUP_CONNECTION_ERROR + | MESSAGE_TYPE_CHANNEL_ENDPOINT_CHANGED + | MESSAGE_TYPE_RECONNECT + ) +} + +pub fn is_mining_message(message_type: u8) -> bool { + matches!( + message_type, + MESSAGE_TYPE_OPEN_STANDARD_MINING_CHANNEL + | MESSAGE_TYPE_OPEN_STANDARD_MINING_CHANNEL_SUCCESS + | MESSAGE_TYPE_OPEN_MINING_CHANNEL_ERROR + | MESSAGE_TYPE_OPEN_EXTENDED_MINING_CHANNEL + | MESSAGE_TYPE_OPEN_EXTENDED_MINING_CHANNEL_SUCCESS + | MESSAGE_TYPE_NEW_MINING_JOB + | MESSAGE_TYPE_UPDATE_CHANNEL + | MESSAGE_TYPE_UPDATE_CHANNEL_ERROR + | MESSAGE_TYPE_CLOSE_CHANNEL + | MESSAGE_TYPE_SET_EXTRANONCE_PREFIX + | MESSAGE_TYPE_SUBMIT_SHARES_STANDARD + | MESSAGE_TYPE_SUBMIT_SHARES_EXTENDED + | MESSAGE_TYPE_SUBMIT_SHARES_SUCCESS + | MESSAGE_TYPE_SUBMIT_SHARES_ERROR + // | MESSAGE_TYPE_RESERVED + | 0x1e + | MESSAGE_TYPE_NEW_EXTENDED_MINING_JOB + | MESSAGE_TYPE_MINING_SET_NEW_PREV_HASH + | MESSAGE_TYPE_SET_TARGET + | MESSAGE_TYPE_SET_CUSTOM_MINING_JOB + | MESSAGE_TYPE_SET_CUSTOM_MINING_JOB_SUCCESS + | MESSAGE_TYPE_SET_CUSTOM_MINING_JOB_ERROR + | MESSAGE_TYPE_SET_GROUP_CHANNEL + ) +} + +pub fn is_job_declaration_message(message_type: u8) -> bool { + matches!( + message_type, + MESSAGE_TYPE_ALLOCATE_MINING_JOB_TOKEN + | MESSAGE_TYPE_ALLOCATE_MINING_JOB_TOKEN_SUCCESS + | MESSAGE_TYPE_PROVIDE_MISSING_TRANSACTIONS + | MESSAGE_TYPE_PROVIDE_MISSING_TRANSACTIONS_SUCCESS + | MESSAGE_TYPE_DECLARE_MINING_JOB + | MESSAGE_TYPE_DECLARE_MINING_JOB_SUCCESS + | MESSAGE_TYPE_DECLARE_MINING_JOB_ERROR + | MESSAGE_TYPE_PUSH_SOLUTION + ) +} + +pub fn is_template_distribution_message(message_type: u8) -> bool { + matches!( + message_type, + MESSAGE_TYPE_COINBASE_OUTPUT_CONSTRAINTS + | MESSAGE_TYPE_NEW_TEMPLATE + | MESSAGE_TYPE_SET_NEW_PREV_HASH + | MESSAGE_TYPE_REQUEST_TRANSACTION_DATA + | MESSAGE_TYPE_REQUEST_TRANSACTION_DATA_SUCCESS + | MESSAGE_TYPE_REQUEST_TRANSACTION_DATA_ERROR + | MESSAGE_TYPE_SUBMIT_SOLUTION + ) +} + +#[derive(Debug, PartialEq, Eq)] +pub enum MessageType { + Common, + Mining, + JobDeclaration, + TemplateDistribution, + Unknown, +} + +pub fn protocol_message_type(message_type: u8) -> MessageType { + if is_common_message(message_type) { + MessageType::Common + } else if is_mining_message(message_type) { + MessageType::Mining + } else if is_job_declaration_message(message_type) { + MessageType::JobDeclaration + } else if is_template_distribution_message(message_type) { + MessageType::TemplateDistribution + } else { + MessageType::Unknown + } +} diff --git a/roles/jd-client/src/main.rs b/roles/jd-client/src/main.rs index 75c6e58610..6c257030ae 100644 --- a/roles/jd-client/src/main.rs +++ b/roles/jd-client/src/main.rs @@ -1,76 +1,17 @@ -//! Entry point for the Job Declarator Client (JDC). -//! -//! This binary parses CLI arguments, loads the TOML configuration file, and -//! starts the main runtime defined in `jd_client::JobDeclaratorClient`. -//! -//! The actual task orchestration and shutdown logic are managed in `lib/mod.rs`. +use config_helpers_sv2::logging::init_logging; +use jd_client_sv2::JobDeclaratorClient; -mod args; -use args::process_cli_args; +use crate::args::process_cli_args; -use jd_client::JobDeclaratorClient; -use tracing::error; +mod args; -/// This will start: -/// 1. An Upstream, this will connect with the mining Pool -/// 2. A listener that will wait for a mining downstream with ExtendedChannel capabilities (tproxy, -/// mining-proxy) -/// 3. A JobDeclarator, this will connect with the job-declarator-server -/// 4. A TemplateRx, this will connect with bitcoind -/// -/// Setup phase -/// 1. Upstream: ->SetupConnection, <-SetupConnectionSuccess -/// 2. Downstream: <-SetupConnection, ->SetupConnectionSuccess, <-OpenExtendedMiningChannel -/// 3. Upstream: ->OpenExtendedMiningChannel, <-OpenExtendedMiningChannelSuccess -/// 4. Downstream: ->OpenExtendedMiningChannelSuccess -/// -/// Setup phase -/// 1. JobDeclarator: ->SetupConnection, <-SetupConnectionSuccess, ->AllocateMiningJobToken(x2), -/// <-AllocateMiningJobTokenSuccess (x2) -/// 2. TemplateRx: ->CoinbaseOutputDataSize -/// -/// Main loop: -/// 1. TemplateRx: <-NewTemplate, SetNewPrevHash -/// 2. JobDeclarator: -> CommitMiningJob (JobDeclarator::on_new_template), <-CommitMiningJobSuccess -/// 3. Upstream: ->SetCustomMiningJob, Downstream: ->NewExtendedMiningJob, ->SetNewPrevHash -/// 4. Downstream: <-Share -/// 5. Upstream: ->Share -/// -/// When we have a NewTemplate we send the NewExtendedMiningJob downstream and the CommitMiningJob -/// to the JDS altogether. -/// Then we receive CommitMiningJobSuccess and we use the new token to send SetCustomMiningJob to -/// the pool. -/// When we receive SetCustomMiningJobSuccess we set in Upstream job_id equal to the one received -/// in SetCustomMiningJobSuccess so that we still send shares upstream with the right job_id. -/// -/// The above procedure, let us send NewExtendedMiningJob downstream right after a NewTemplate has -/// been received this will reduce the time that pass from a NewTemplate and the mining-device -/// starting to mine on the new job. -/// -/// In the case a future NewTemplate the SetCustomMiningJob is sent only if the candidate become -/// the actual NewTemplate so that we do not send a lot of useless future Job to the pool. That -/// means that SetCustomMiningJob is sent only when a NewTemplate become "active" -/// -/// The JobDeclarator always have 2 available token, that means that whenever a token is used to -/// commit a job with upstream we require a new one. Having always a token when needed means that -/// whenever we want to commit a mining job we can do that without waiting for upstream to provide -/// a new token. -/// -/// Entrypoint for the Job Declarator Client binary. -/// -/// Loads the configuration from TOML and initializes the main runtime -/// defined in `jd_client::JobDeclaratorClient`. Errors during startup are logged. #[tokio::main] async fn main() { - tracing_subscriber::fmt::init(); - let proxy_config = match process_cli_args() { - Ok(p) => p, - Err(e) => { - error!("Job Declarator Client Config error: {}", e); - return; - } - }; + let jdc_config = process_cli_args().unwrap_or_else(|e| { + eprintln!("Job Declarator Client config error: {e}"); + std::process::exit(1); + }); - let jdc = JobDeclaratorClient::new(proxy_config); - jdc.start().await; + init_logging(jdc_config.log_file()); + JobDeclaratorClient::new(jdc_config).start().await; } diff --git a/roles/jd-server/Cargo.toml b/roles/jd-server/Cargo.toml index 8ba64dcce3..8df2545cba 100644 --- a/roles/jd-server/Cargo.toml +++ b/roles/jd-server/Cargo.toml @@ -2,7 +2,7 @@ name = "jd_server" version = "0.1.3" authors = ["The Stratum V2 Developers"] -edition = "2018" +edition = "2021" description = "Job Declarator Server (JDS) role" documentation = "https://github.com/stratum-mining/stratum" readme = "README.md" @@ -17,19 +17,13 @@ name = "jd_server" path = "src/lib/mod.rs" [dependencies] -stratum-common = { version = "2.0.0", path = "../../common" } +stratum-common = { path = "../../common", features = ["with_network_helpers"] } async-channel = "1.5.1" -binary_sv2 = { path = "../../protocols/v2/binary-sv2" } buffer_sv2 = { path = "../../utils/buffer" } -codec_sv2 = { path = "../../protocols/v2/codec-sv2", features = ["noise_sv2"] } -network_helpers_sv2 = { path = "../roles-utils/network-helpers" } -noise_sv2 = { path = "../../protocols/v2/noise-sv2" } rand = "0.8.4" -roles_logic_sv2 = { path = "../../protocols/v2/roles-logic-sv2" } tokio = { version = "1.44.1", features = ["full"] } ext-config = { version = "0.14.0", features = ["toml"], package = "config" } tracing = { version = "0.1" } -tracing-subscriber = "0.3" error_handling = { path = "../../utils/error-handling" } nohash-hasher = "0.2.0" serde_json = { version = "1.0", default-features = false, features = ["alloc","raw_value"] } @@ -38,5 +32,5 @@ hashbrown = { version = "0.11", default-features = false, features = ["ahash", " key-utils = { path = "../../utils/key-utils" } rpc_sv2 = { path = "../roles-utils/rpc" } hex = "0.4.3" -config-helpers = { path = "../roles-utils/config-helpers" } +config_helpers_sv2 = { path = "../roles-utils/config-helpers" } clap = { version = "4.5.39", features = ["derive"] } diff --git a/roles/jd-server/config-examples/jds-config-hosted-example.toml b/roles/jd-server/config-examples/jds-config-hosted-example.toml index 7d5afbbf75..4f3b4039e6 100644 --- a/roles/jd-server/config-examples/jds-config-hosted-example.toml +++ b/roles/jd-server/config-examples/jds-config-hosted-example.toml @@ -6,17 +6,16 @@ authority_public_key = "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72" authority_secret_key = "mkDLTBBRxdBv998612qipDYoTK3YUrqLe8uWw7gu3iXbSrn2n" cert_validity_sec = 3600 -# List of coinbase outputs used to build the coinbase tx -# ! Right now only one output is supported, so comment all the ones you don't need ! -# For P2PK, P2PKH, P2WPKH, P2TR a public key is needed. For P2SH and P2WSH, a redeem script is needed. -coinbase_outputs = [ - #{ output_script_type = "P2PK", output_script_value = "0372c47307e5b75ce365daf835f226d246c5a7a92fe24395018d5552123354f086" }, - #{ output_script_type = "P2PKH", output_script_value = "0372c47307e5b75ce365daf835f226d246c5a7a92fe24395018d5552123354f086" }, - #{ output_script_type = "P2SH", output_script_value = "00142ef89234bc95136eb9e6fee9d32722ebd8c1f0ab" }, - #{ output_script_type = "P2WSH", output_script_value = "00142ef89234bc95136eb9e6fee9d32722ebd8c1f0ab" }, - { output_script_type = "P2WPKH", output_script_value = "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075" }, - #{ output_script_type = "P2TR", output_script_value = "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075" }, -] +# Coinbase outputs are specified as descriptors. A full list of descriptors is available at +# https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki#appendix-b-index-of-script-expressions +# Although the `musig` descriptor is not yet supported and the legacy `combo` descriptor never +# will be. If you have an address, embed it in a descriptor like `addr(
)`. +coinbase_reward_script = "addr(tb1qa0sm0hxzj0x25rh8gw5xlzwlsfvvyz8u96w3p8)" + +# Enable this option to set a predefined log file path. +# When enabled, logs will always be written to this file. +# The CLI option --log-file (or -f) will override this setting if provided. +# log_file = "./jd-server.log" # SRI Pool JD config listen_jd_address = "0.0.0.0:34264" diff --git a/roles/jd-server/config-examples/jds-config-local-example.toml b/roles/jd-server/config-examples/jds-config-local-example.toml index 91cada6369..f26adfbf48 100644 --- a/roles/jd-server/config-examples/jds-config-local-example.toml +++ b/roles/jd-server/config-examples/jds-config-local-example.toml @@ -6,17 +6,16 @@ authority_public_key = "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72" authority_secret_key = "mkDLTBBRxdBv998612qipDYoTK3YUrqLe8uWw7gu3iXbSrn2n" cert_validity_sec = 3600 -# List of coinbase outputs used to build the coinbase tx -# ! Right now only one output is supported, so comment all the ones you don't need ! -# For P2PK, P2PKH, P2WPKH, P2TR a public key is needed. For P2SH and P2WSH, a redeem script is needed. -coinbase_outputs = [ - #{ output_script_type = "P2PK", output_script_value = "0372c47307e5b75ce365daf835f226d246c5a7a92fe24395018d5552123354f086" }, - #{ output_script_type = "P2PKH", output_script_value = "0372c47307e5b75ce365daf835f226d246c5a7a92fe24395018d5552123354f086" }, - #{ output_script_type = "P2SH", output_script_value = "00142ef89234bc95136eb9e6fee9d32722ebd8c1f0ab" }, - #{ output_script_type = "P2WSH", output_script_value = "00142ef89234bc95136eb9e6fee9d32722ebd8c1f0ab" }, - { output_script_type = "P2WPKH", output_script_value = "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075" }, - #{ output_script_type = "P2TR", output_script_value = "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075" }, -] +# Coinbase outputs are specified as descriptors. A full list of descriptors is available at +# https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki#appendix-b-index-of-script-expressions +# Although the `musig` descriptor is not yet supported and the legacy `combo` descriptor never +# will be. If you have an address, embed it in a descriptor like `addr(
)`. +coinbase_reward_script = "addr(tb1qa0sm0hxzj0x25rh8gw5xlzwlsfvvyz8u96w3p8)" + +# Enable this option to set a predefined log file path. +# When enabled, logs will always be written to this file. +# The CLI option --log-file (or -f) will override this setting if provided. +# log_file = "./jd-server.log" # SRI Pool JD config listen_jd_address = "127.0.0.1:34264" diff --git a/roles/jd-server/src/args.rs b/roles/jd-server/src/args.rs index 9b265670c6..e0e0d26400 100644 --- a/roles/jd-server/src/args.rs +++ b/roles/jd-server/src/args.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use clap::Parser; use ext_config::{Config, File, FileFormat}; use jd_server::{ @@ -23,6 +25,12 @@ pub struct Args { default_value = "jds-config.toml" )] pub config_path: std::path::PathBuf, + #[arg( + short = 'f', + long = "log-file", + help = "Path to the log file. If not set, logs will only be written to stdout." + )] + pub log_file: Option, } /// Process CLI args and load configuration. @@ -46,11 +54,14 @@ pub fn process_cli_args() -> Result { })?; // Deserialize settings into JobDeclaratorServerConfig - let config = settings + let mut config = settings .try_deserialize::() .map_err(|e| { error!("Failed to deserialize config: {}", e); JdsError::BadCliArgs })?; + + config.set_log_file(args.log_file); + Ok(config) } diff --git a/roles/jd-server/src/lib/config.rs b/roles/jd-server/src/lib/config.rs index 26495817eb..3b0480e516 100644 --- a/roles/jd-server/src/lib/config.rs +++ b/roles/jd-server/src/lib/config.rs @@ -11,11 +11,13 @@ //! //! Also defines a helper struct [`CoreRpc`] to group RPC parameters. -use config_helpers::CoinbaseOutput; +use config_helpers_sv2::CoinbaseRewardScript; use key_utils::{Secp256k1PublicKey, Secp256k1SecretKey}; use serde::Deserialize; -use std::{convert::TryInto, time::Duration}; -use stratum_common::bitcoin::{Amount, TxOut}; +use std::{ + path::{Path, PathBuf}, + time::Duration, +}; #[derive(Debug, serde::Deserialize, Clone)] pub struct JobDeclaratorServerConfig { @@ -25,23 +27,28 @@ pub struct JobDeclaratorServerConfig { authority_public_key: Secp256k1PublicKey, authority_secret_key: Secp256k1SecretKey, cert_validity_sec: u64, - coinbase_outputs: Vec, + coinbase_reward_script: CoinbaseRewardScript, core_rpc_url: String, core_rpc_port: u16, core_rpc_user: String, core_rpc_pass: String, - #[serde(deserialize_with = "config_helpers::duration_from_toml")] + #[serde(deserialize_with = "config_helpers_sv2::duration_from_toml")] mempool_update_interval: Duration, + log_file: Option, } impl JobDeclaratorServerConfig { /// Creates a new instance of [`JobDeclaratorServerConfig`]. + /// + /// # Panics + /// + /// Panics if `coinbase_reward_scripts` is empty. pub fn new( listen_jd_address: String, authority_public_key: Secp256k1PublicKey, authority_secret_key: Secp256k1SecretKey, cert_validity_sec: u64, - coinbase_outputs: Vec, + coinbase_reward_script: CoinbaseRewardScript, core_rpc: CoreRpc, mempool_update_interval: Duration, ) -> Self { @@ -51,12 +58,13 @@ impl JobDeclaratorServerConfig { authority_public_key, authority_secret_key, cert_validity_sec, - coinbase_outputs, + coinbase_reward_script, core_rpc_url: core_rpc.url, core_rpc_port: core_rpc.port, core_rpc_user: core_rpc.user, core_rpc_pass: core_rpc.pass, mempool_update_interval, + log_file: None, } } @@ -96,8 +104,8 @@ impl JobDeclaratorServerConfig { } /// Returns the coinbase outputs. - pub fn coinbase_outputs(&self) -> &Vec { - &self.coinbase_outputs + pub fn coinbase_reward_scripts(&self) -> &CoinbaseRewardScript { + &self.coinbase_reward_script } /// Returns the certificate validity in seconds. @@ -125,22 +133,16 @@ impl JobDeclaratorServerConfig { } /// Sets coinbase outputs. - pub fn set_coinbase_outputs(&mut self, outputs: Vec) { - self.coinbase_outputs = outputs; + pub fn set_coinbase_reward_scripts(&mut self, output: CoinbaseRewardScript) { + self.coinbase_reward_script = output; } - pub fn get_txout(&self) -> Result, config_helpers::CoinbaseOutputError> { - let mut result = Vec::new(); - for coinbase_output_pool in &self.coinbase_outputs { - let output_script = coinbase_output_pool.clone().try_into()?; - result.push(TxOut { - value: Amount::from_sat(0), - script_pubkey: output_script, - }); - } - match result.is_empty() { - true => Err(config_helpers::CoinbaseOutputError::EmptyCoinbaseOutputs), - _ => Ok(result), + pub fn log_file(&self) -> Option<&Path> { + self.log_file.as_deref() + } + pub fn set_log_file(&mut self, log_file: Option) { + if let Some(path) = log_file { + self.log_file = Some(path); } } } @@ -171,13 +173,34 @@ impl CoreRpc { #[cfg(test)] mod tests { use super::super::JobDeclaratorServer; - use config_helpers::CoinbaseOutput; - use ext_config::{Config, File, FileFormat}; - use std::{convert::TryInto, path::PathBuf}; - use stratum_common::bitcoin::{Amount, ScriptBuf, TxOut}; + use ext_config::{Config, ConfigError, File, FileFormat}; + use std::path::PathBuf; + use stratum_common::roles_logic_sv2::bitcoin::{self, Amount, ScriptBuf, TxOut}; use crate::config::JobDeclaratorServerConfig; + const COINBASE_CONFIG_TEMPLATE: &'static str = r#" + full_template_mode_required = true + authority_public_key = "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72" + authority_secret_key = "mkDLTBBRxdBv998612qipDYoTK3YUrqLe8uWw7gu3iXbSrn2n" + cert_validity_sec = 3600 + + coinbase_reward_script = %COINBASE_REWARD_SCRIPT% + + listen_jd_address = "127.0.0.1:34264" + core_rpc_url = "http://127.0.0.1" + core_rpc_port = 48332 + core_rpc_user = "username" + core_rpc_pass = "password" + [mempool_update_interval] + unit = "secs" + value = 1 + "#; + const TEST_PK_HEX: &'static str = + "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075"; + const TEST_INVALID_PK_HEX: &'static str = + "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7ffffff"; + fn load_config(path: &str) -> JobDeclaratorServerConfig { let config_path = PathBuf::from(path); assert!( @@ -196,6 +219,16 @@ mod tests { settings.try_deserialize().expect("Failed to parse config") } + fn load_coinbase_config_str(path: &str) -> Result { + let s = COINBASE_CONFIG_TEMPLATE.replace("%COINBASE_REWARD_SCRIPT%", path); + let settings = Config::builder() + .add_source(File::from_str(&s, FileFormat::Toml)) + .build() + .expect("Failed to build config"); + + settings.try_deserialize() + } + #[tokio::test] async fn test_offline_rpc_url() { let mut config = load_config("config-examples/jds-config-hosted-example.toml"); @@ -205,94 +238,57 @@ mod tests { } #[test] - fn test_get_txout_non_empty() { - let config = load_config("config-examples/jds-config-hosted-example.toml"); - let outputs = config.get_txout().expect("Failed to get coinbase output"); - - let expected_output = CoinbaseOutput { - output_script_type: "P2WPKH".to_string(), - output_script_value: - "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075".to_string(), + fn test_get_non_empty_coinbase_reward_script() { + let pk = TEST_PK_HEX + .parse::() + .expect("Failed to parse public key"); + let config = + load_coinbase_config_str(&format!("\"wpkh({pk})\"")).expect("Failed to parse config"); + + let output = TxOut { + value: Amount::from_sat(0), + script_pubkey: config.coinbase_reward_scripts().script_pubkey(), }; - let expected_script: ScriptBuf = expected_output.try_into().unwrap(); + let expected_script = ScriptBuf::from_hex(&format!( + "0014{}", + pk.wpubkey_hash().expect("compressed key") + )) + .expect("hex"); let expected_transaction_output = TxOut { value: Amount::from_sat(0), script_pubkey: expected_script, }; - assert_eq!(outputs[0], expected_transaction_output); - } - - #[test] - fn test_get_txout_empty() { - let mut config = load_config("config-examples/jds-config-hosted-example.toml"); - config.set_coinbase_outputs(Vec::new()); - - let result = &config.get_txout(); - assert!( - matches!( - result, - Err(config_helpers::CoinbaseOutputError::EmptyCoinbaseOutputs) - ), - "Expected an error for empty coinbase outputs" - ); + assert_eq!(output, expected_transaction_output); } #[test] - fn test_try_from_valid_input() { - let input = config_helpers::CoinbaseOutput::new( - "P2PKH".to_string(), - "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075".to_string(), + fn test_get_coinbase_reward_script_empty() { + let error = + load_coinbase_config_str("\"\"").expect_err("cannot parse config with empty txout"); + assert_eq!( + error.to_string(), + "Miniscript: unexpected «(0 args) while parsing Miniscript»", ); - let result: Result = input.try_into(); - assert!(result.is_ok()); } #[test] - fn test_try_from_invalid_input() { - let input = config_helpers::CoinbaseOutput::new( - "INVALID".to_string(), - "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075".to_string(), + fn test_get_invalid_miniscript_in_coinbase_reward_script() { + let error = load_coinbase_config_str(&format!("\"INVALID\"")) + .expect_err("Cannot parse config with bad miniscript"); + assert_eq!( + error.to_string(), + "Miniscript: unexpected «INVALID(0 args) while parsing Miniscript»", ); - let result: Result = input.try_into(); - assert!(matches!( - result, - Err(config_helpers::CoinbaseOutputError::UnknownOutputScriptType) - )); } #[test] - fn get_txout_invalid_output_script_type() { - let mut config = load_config("config-examples/jds-config-hosted-example.toml"); - config.set_coinbase_outputs(vec![config_helpers::CoinbaseOutput::new( - "INVALID".to_string(), - "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075".to_string(), - )]); - let outputs = config.get_txout(); - assert!( - matches!( - outputs, - Err(config_helpers::CoinbaseOutputError::UnknownOutputScriptType) - ), - "Expected an error for unknown output script type" + fn test_get_invalid_value_in_coinbase_reward_script() { + let error = load_coinbase_config_str(&format!("\"wpkh({TEST_INVALID_PK_HEX})\"")) + .expect_err("Cannot parse config with bad pubkeys"); + assert_eq!( + error.to_string(), + "Miniscript: unexpected «Error while parsing simple public key»", ); } - - #[test] - fn get_txout_supported_output_script_types() { - let config = load_config("config-examples/jds-config-hosted-example.toml"); - let outputs = config.get_txout().expect("Failed to get coinbase output"); - let expected_output = CoinbaseOutput { - output_script_type: "P2WPKH".to_string(), - output_script_value: - "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075".to_string(), - }; - let expected_script: ScriptBuf = expected_output.try_into().unwrap(); - let expected_transaction_output = TxOut { - value: Amount::from_sat(0), - script_pubkey: expected_script, - }; - - assert_eq!(outputs[0], expected_transaction_output); - } } diff --git a/roles/jd-server/src/lib/error.rs b/roles/jd-server/src/lib/error.rs index 152d842349..5bb930591c 100644 --- a/roles/jd-server/src/lib/error.rs +++ b/roles/jd-server/src/lib/error.rs @@ -19,7 +19,11 @@ use std::{ sync::{MutexGuard, PoisonError}, }; -use roles_logic_sv2::parsers::Mining; +use stratum_common::roles_logic_sv2::{ + self, + codec_sv2::{self, binary_sv2, noise_sv2}, + parsers_sv2::Mining, +}; use crate::mempool::error::JdsMempoolError; @@ -41,32 +45,38 @@ pub enum JdsError { NoLastDeclaredJob, InvalidRPCUrl, BadCliArgs, + InvalidPrevHash, + InvalidCoinbase, + InvalidMerkleRoot, } impl std::fmt::Display for JdsError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { use JdsError::*; match self { - Io(ref e) => write!(f, "I/O error: `{:?}", e), - ChannelSend(ref e) => write!(f, "Channel send failed: `{:?}`", e), - ChannelRecv(ref e) => write!(f, "Channel recv failed: `{:?}`", e), - BinarySv2(ref e) => write!(f, "Binary SV2 error: `{:?}`", e), - Codec(ref e) => write!(f, "Codec SV2 error: `{:?}", e), - Framing(ref e) => write!(f, "Framing SV2 error: `{:?}`", e), - Noise(ref e) => write!(f, "Noise SV2 error: `{:?}", e), - RolesLogic(ref e) => write!(f, "Roles Logic SV2 error: `{:?}`", e), - PoisonLock(ref e) => write!(f, "Poison lock: {:?}", e), - Custom(ref e) => write!(f, "Custom SV2 error: `{:?}`", e), + Io(ref e) => write!(f, "I/O error: `{e:?}"), + ChannelSend(ref e) => write!(f, "Channel send failed: `{e:?}`"), + ChannelRecv(ref e) => write!(f, "Channel recv failed: `{e:?}`"), + BinarySv2(ref e) => write!(f, "Binary SV2 error: `{e:?}`"), + Codec(ref e) => write!(f, "Codec SV2 error: `{e:?}"), + Framing(ref e) => write!(f, "Framing SV2 error: `{e:?}`"), + Noise(ref e) => write!(f, "Noise SV2 error: `{e:?}"), + RolesLogic(ref e) => write!(f, "Roles Logic SV2 error: `{e:?}`"), + PoisonLock(ref e) => write!(f, "Poison lock: {e:?}"), + Custom(ref e) => write!(f, "Custom SV2 error: `{e:?}`"), Sv2ProtocolError(ref e) => { - write!(f, "Received Sv2 Protocol Error from upstream: `{:?}`", e) + write!(f, "Received Sv2 Protocol Error from upstream: `{e:?}`") } - MempoolError(ref e) => write!(f, "Mempool error: `{:?}`", e), + MempoolError(ref e) => write!(f, "Mempool error: `{e:?}`"), ImpossibleToReconstructBlock(e) => { - write!(f, "Error in reconstructing the block: {:?}", e) + write!(f, "Error in reconstructing the block: {e:?}") } NoLastDeclaredJob => write!(f, "Last declared job not found"), InvalidRPCUrl => write!(f, "Invalid Template Provider RPC URL"), BadCliArgs => write!(f, "Bad CLI arg input"), + InvalidPrevHash => write!(f, "Invalid previous hash"), + InvalidCoinbase => write!(f, "Invalid coinbase"), + InvalidMerkleRoot => write!(f, "Invalid merkle root"), } } } diff --git a/roles/jd-server/src/lib/job_declarator/message_handler.rs b/roles/jd-server/src/lib/job_declarator/message_handler.rs index 9373144506..ab815baafb 100644 --- a/roles/jd-server/src/lib/job_declarator/message_handler.rs +++ b/roles/jd-server/src/lib/job_declarator/message_handler.rs @@ -1,25 +1,29 @@ -use binary_sv2::{Decodable, Serialize, U256}; -use roles_logic_sv2::{ +use std::{ + convert::TryInto, + io::Cursor, + sync::{atomic::Ordering, Arc}, +}; +use stratum_common::roles_logic_sv2::{ + bitcoin::{ + consensus::Decodable as BitcoinDecodable, + hashes::{sha256d, Hash}, + Transaction, Txid, + }, + codec_sv2::binary_sv2::{Decodable, Serialize, U256}, handlers::{job_declaration::ParseJobDeclarationMessagesFromDownstream, SendTo_}, job_declaration_sv2::{ AllocateMiningJobToken, AllocateMiningJobTokenSuccess, DeclareMiningJob, DeclareMiningJobError, DeclareMiningJobSuccess, ProvideMissingTransactions, ProvideMissingTransactionsSuccess, PushSolution, }, - parsers::JobDeclaration, + parsers_sv2::JobDeclaration, utils::Mutex, }; -use std::{convert::TryInto, io::Cursor, sync::Arc}; -use stratum_common::bitcoin::{ - hashes::{sha256d, Hash}, - Transaction, Txid, -}; pub type SendTo = SendTo_, ()>; use crate::mempool::JDsMempool; use super::{signed_token, TransactionState}; -use roles_logic_sv2::{errors::Error, parsers::AnyMessage as AllMessages}; -use stratum_common::bitcoin::consensus::Decodable as BitcoinDecodable; +use stratum_common::roles_logic_sv2::{errors::Error, parsers_sv2::AnyMessage as AllMessages}; use tracing::{debug, info}; use super::JobDeclaratorDownstream; @@ -54,18 +58,16 @@ impl ParseJobDeclarationMessagesFromDownstream for JobDeclaratorDownstream { message.request_id ); debug!("`AllocateMiningJobToken`: {:?}", message.request_id); - let token = self.tokens.next(); + let token = self.tokens.fetch_add(1, Ordering::Relaxed); self.token_to_job_map.insert(token, None); let message_success = AllocateMiningJobTokenSuccess { request_id: message.request_id, mining_job_token: token.to_le_bytes().to_vec().try_into().unwrap(), - coinbase_output_max_additional_size: 100, - coinbase_output: self.coinbase_output.clone().try_into().unwrap(), - coinbase_output_max_additional_sigops: self.coinbase_output_sigops, + coinbase_outputs: self.coinbase_output.clone().try_into().unwrap(), }; let message_enum = JobDeclaration::AllocateMiningJobTokenSuccess(message_success); info!( - "Sending AllocateMiningJobTokenSuccess to proxy {:?}", + "Sending AllocateMiningJobTokenSuccess to proxy {}", message_enum ); Ok(SendTo::Respond(message_enum)) @@ -80,7 +82,7 @@ impl ParseJobDeclarationMessagesFromDownstream for JobDeclaratorDownstream { "Received `DeclareMiningJob` with id: {}", message.request_id ); - debug!("`DeclareMiningJob`: {:?}", message); + debug!("`DeclareMiningJob`: {}", message); if let Some(old_mining_job) = self.declared_mining_job.0.take() { clear_declared_mining_job(old_mining_job, &message, self.mempool.clone())?; } @@ -158,7 +160,7 @@ impl ParseJobDeclarationMessagesFromDownstream for JobDeclaratorDownstream { "Received `ProvideMissingTransactionsSuccess` with id: {}", message.request_id ); - debug!("`ProvideMissingTransactionsSuccess`: {:?}", message); + debug!("`ProvideMissingTransactionsSuccess`: {}", message); let (declared_mining_job, ref mut transactions_with_state, missing_indexes) = &mut self.declared_mining_job; let mut unknown_transactions: Vec = vec![]; @@ -225,7 +227,7 @@ impl ParseJobDeclarationMessagesFromDownstream for JobDeclaratorDownstream { fn handle_push_solution(&mut self, message: PushSolution<'_>) -> Result { info!("Received PushSolution from JDC"); - debug!("`PushSolution`: {:?}", message); + debug!("`PushSolution`: {}", message); let m = JobDeclaration::PushSolution(message.clone().into_static()); Ok(SendTo::None(Some(m))) } @@ -252,28 +254,32 @@ fn clear_declared_mining_job( .filter(|&id| !new_transactions.contains(id)) { if let Some(tx) = mempool_txs.get(*old_txid) { - let txid = tx.as_ref().unwrap().0.compute_txid(); - match mempool_.mempool.get_mut(&txid) { - Some(Some((_transaction, counter))) => { - if *counter > 1 { - *counter -= 1; - debug!( - "Fat transaction {:?} counter decremented; job id {:?} dropped", - txid, old_mining_job.request_id - ); - } else { - mempool_.mempool.remove(&txid); - debug!( - "Fat transaction {:?} with job id {:?} removed from mempool", - txid, old_mining_job.request_id - ); + if let Some((transaction, _)) = tx.as_ref() { + let txid = transaction.compute_txid(); + match mempool_.mempool.get_mut(&txid) { + Some(Some((_transaction, counter))) => { + if *counter > 1 { + *counter -= 1; + debug!( + "Fat transaction {:?} counter decremented; job id {:?} dropped", + txid, old_mining_job.request_id + ); + } else { + mempool_.mempool.remove(&txid); + debug!( + "Fat transaction {:?} with job id {:?} removed from mempool", + txid, old_mining_job.request_id + ); + } } + Some(None) => debug!( + "Thin transaction {:?} with job id {:?} removed from mempool", + txid, old_mining_job.request_id + ), + None => {} } - Some(None) => debug!( - "Thin transaction {:?} with job id {:?} removed from mempool", - txid, old_mining_job.request_id - ), - None => {} + } else { + debug!("Transaction with id {:?} is None in mempool", old_txid); } } else { debug!( diff --git a/roles/jd-server/src/lib/job_declarator/mod.rs b/roles/jd-server/src/lib/job_declarator/mod.rs index ad8729d479..acf34f6e78 100644 --- a/roles/jd-server/src/lib/job_declarator/mod.rs +++ b/roles/jd-server/src/lib/job_declarator/mod.rs @@ -22,31 +22,41 @@ use super::{ error::JdsError, mempool::JDsMempool, status, EitherFrame, JobDeclaratorServerConfig, StdFrame, }; use async_channel::{Receiver, Sender}; -use binary_sv2::{B0255, U256}; -use codec_sv2::{HandshakeRole, Responder}; use core::panic; use error_handling::handle_result; use key_utils::{Secp256k1PublicKey, Secp256k1SecretKey, SignatureService}; -use network_helpers_sv2::noise_connection::Connection; use nohash_hasher::BuildNoHashHasher; -use roles_logic_sv2::{ - common_messages_sv2::{ - Protocol, SetupConnection, SetupConnectionError, SetupConnectionSuccess, +use std::{ + collections::HashMap, + convert::TryInto, + sync::{atomic::AtomicU32, Arc}, +}; +use stratum_common::{ + network_helpers_sv2::noise_connection::Connection, + roles_logic_sv2::{ + self, + bitcoin::{ + block::{Header, Version}, + consensus::{deserialize, encode::serialize}, + hashes::{sha256d::Hash as DHash, Hash}, + Amount, Block, BlockHash, CompactTarget, Transaction, TxOut, Txid, + }, + codec_sv2::{ + binary_sv2::{self, B0255, U256}, + HandshakeRole, Responder, + }, + common_messages_sv2::{ + Protocol, SetupConnection, SetupConnectionError, SetupConnectionSuccess, + }, + handlers::job_declaration::{ParseJobDeclarationMessagesFromDownstream, SendTo}, + job_declaration_sv2::{DeclareMiningJob, PushSolution}, + parsers_sv2::{AnyMessage as JdsMessages, JobDeclaration}, + utils::Mutex, }, - handlers::job_declaration::{ParseJobDeclarationMessagesFromDownstream, SendTo}, - job_declaration_sv2::{DeclareMiningJob, PushSolution}, - parsers::{AnyMessage as JdsMessages, JobDeclaration}, - utils::{Id, Mutex}, }; -use std::{collections::HashMap, convert::TryInto, sync::Arc}; use tokio::{net::TcpListener, time::Duration}; use tracing::{debug, error, info}; -use stratum_common::bitcoin::{ - consensus::{encode::serialize, Encodable}, - Block, Transaction, Txid, -}; - /// Represents whether a transaction declared in a mining job is known to the JDS mempool /// or still missing and needs to be fetched/provided. #[derive(Clone, Debug)] @@ -92,9 +102,8 @@ pub struct JobDeclaratorDownstream { #[allow(dead_code)] // TODO: use coinbase output coinbase_output: Vec, - coinbase_output_sigops: u16, token_to_job_map: HashMap, BuildNoHashHasher>, - tokens: Id, + tokens: AtomicU32, public_key: Secp256k1PublicKey, private_key: Secp256k1SecretKey, mempool: Arc>, @@ -117,31 +126,23 @@ impl JobDeclaratorDownstream { mempool: Arc>, sender_add_txs_to_mempool: Sender, ) -> Self { - let mut coinbase_output = vec![]; // TODO: use next variables let token_to_job_map = HashMap::with_hasher(BuildNoHashHasher::default()); - let tokens = Id::new(); + let tokens = AtomicU32::new(0); let add_txs_to_mempool_inner = AddTrasactionsToMempoolInner { known_transactions: vec![], unknown_transactions: vec![], }; - config - .get_txout() - .expect("Invalid coinbase output in config")[0] - .consensus_encode(&mut coinbase_output) - .expect("Invalid coinbase output in config"); - let coinbase_output_sigops = config - .get_txout() - .expect("Invalid coinbase output in config")[0] - .script_pubkey - .count_sigops() as u16; + let coinbase_output = serialize(&vec![TxOut { + value: Amount::from_sat(0), + script_pubkey: config.coinbase_reward_scripts().script_pubkey().to_owned(), + }]); Self { full_template_mode_required, receiver, sender, coinbase_output, - coinbase_output_sigops, token_to_job_map, tokens, public_key: *config.authority_public_key(), @@ -164,10 +165,46 @@ impl JobDeclaratorDownstream { .safe_lock(|x| x.declared_mining_job.clone()) .map_err(|e| Box::new(JdsError::PoisonLock(e.to_string())))?; let last_declare = last_declare_.ok_or(Box::new(JdsError::NoLastDeclaredJob))?; - let transactions_list = Self::collect_txs_in_job(self_mutex)?; - let block: Block = - roles_logic_sv2::utils::BlockCreator::new(last_declare, transactions_list, message) - .into(); + let mut transactions_list = Self::collect_txs_in_job(self_mutex)?; + + let hash: [u8; 32] = message + .prev_hash + .to_vec() + .try_into() + .map_err(|_| Box::new(JdsError::InvalidPrevHash))?; + let hash = Hash::from_slice(&hash).expect("32 bytes should always be valid sha256d hash"); + let prev_blockhash = BlockHash::from_raw_hash(hash); + + let dummy_merkle_root = + DHash::from_slice(&[0u8; 32]).expect("32 bytes should always be valid sha256d hash"); + + let header = Header { + version: Version::from_consensus(message.version as i32), + prev_blockhash, + merkle_root: dummy_merkle_root.into(), + time: message.ntime, + bits: CompactTarget::from_consensus(message.nbits), + nonce: message.nonce, + }; + + let mut serialized_coinbase = Vec::new(); + serialized_coinbase.extend_from_slice(last_declare.coinbase_tx_prefix.to_vec().as_slice()); + serialized_coinbase.extend_from_slice(message.extranonce.to_vec().as_slice()); + serialized_coinbase.extend_from_slice(last_declare.coinbase_tx_suffix.to_vec().as_slice()); + let coinbase = deserialize(&serialized_coinbase[..]) + .map_err(|_| Box::new(JdsError::InvalidCoinbase))?; + transactions_list.insert(0, coinbase); + + let mut block = Block { + header, + txdata: transactions_list, + }; + + let merkle_root = block + .compute_merkle_root() + .ok_or(Box::new(JdsError::InvalidMerkleRoot))?; + block.header.merkle_root = merkle_root; + Ok(hex::encode(serialize(&block))) } @@ -240,7 +277,7 @@ impl JobDeclaratorDownstream { /// Wraps the message into a `StdFrame` and sends it through the established channel. pub async fn send( self_mutex: Arc>, - message: roles_logic_sv2::parsers::JobDeclaration<'static>, + message: roles_logic_sv2::parsers_sv2::JobDeclaration<'static>, ) -> Result<(), ()> { let sv2_frame: StdFrame = JdsMessages::JobDeclaration(message).try_into().unwrap(); let sender = self_mutex.safe_lock(|self_| self_.sender.clone()).unwrap(); @@ -322,11 +359,11 @@ impl JobDeclaratorDownstream { Self::send(self_mutex.clone(), m).await.unwrap(); } Ok(SendTo::RelayNewMessage(message)) => { - error!("JD Server: unexpected relay new message {:?}", message); + error!("JD Server: unexpected relay new message {}", message); } Ok(SendTo::RelayNewMessageToRemote(remote, message)) => { error!( - "JD Server: unexpected relay new message to remote. Remote: {:?}, Message: {:?}", + "JD Server: unexpected relay new message to remote. Remote: {:?}, Message: {}", remote, message ); diff --git a/roles/jd-server/src/lib/mempool/mod.rs b/roles/jd-server/src/lib/mempool/mod.rs index 0899bdfe23..693af8fee6 100644 --- a/roles/jd-server/src/lib/mempool/mod.rs +++ b/roles/jd-server/src/lib/mempool/mod.rs @@ -19,12 +19,13 @@ pub mod error; use super::job_declarator::AddTrasactionsToMempoolInner; use crate::mempool::error::JdsMempoolError; use async_channel::Receiver; -use bitcoin::blockdata::transaction::Transaction; use hashbrown::HashMap; -use roles_logic_sv2::utils::Mutex; use rpc_sv2::{mini_rpc_client, mini_rpc_client::RpcError}; use std::{str::FromStr, sync::Arc}; -use stratum_common::{bitcoin, bitcoin::hash_types::Txid}; +use stratum_common::roles_logic_sv2::{ + bitcoin::{blockdata::transaction::Transaction, hash_types::Txid}, + utils::Mutex, +}; /// Wrapper around a known transaction and its hash. #[derive(Clone, Debug)] diff --git a/roles/jd-server/src/lib/mod.rs b/roles/jd-server/src/lib/mod.rs index c2e4944b4c..d494c5a681 100644 --- a/roles/jd-server/src/lib/mod.rs +++ b/roles/jd-server/src/lib/mod.rs @@ -23,15 +23,18 @@ pub mod job_declarator; pub mod mempool; pub mod status; use async_channel::{bounded, unbounded, Receiver, Sender}; -use codec_sv2::{StandardEitherFrame, StandardSv2Frame}; use config::JobDeclaratorServerConfig; use error::JdsError; use error_handling::handle_result; use job_declarator::JobDeclarator; use mempool::error::JdsMempoolError; -use roles_logic_sv2::{parsers::AnyMessage as JdsMessages, utils::Mutex}; pub use rpc_sv2::Uri; use std::{ops::Sub, str::FromStr, sync::Arc}; +use stratum_common::roles_logic_sv2::{ + codec_sv2::{StandardEitherFrame, StandardSv2Frame}, + parsers_sv2::AnyMessage as JdsMessages, + utils::Mutex, +}; use tokio::{select, task}; use tracing::{error, info, warn}; diff --git a/roles/jd-server/src/lib/status.rs b/roles/jd-server/src/lib/status.rs index 55a1ec81ce..c3b7aa4406 100644 --- a/roles/jd-server/src/lib/status.rs +++ b/roles/jd-server/src/lib/status.rs @@ -8,7 +8,7 @@ //! //! This allows for centralized, consistent error handling across the application. -use roles_logic_sv2::parsers::Mining; +use stratum_common::roles_logic_sv2::parsers_sv2::Mining; use super::error::JdsError; @@ -153,6 +153,15 @@ pub async fn handle_error(sender: &Sender, e: JdsError) -> error_handling::Error } JdsError::InvalidRPCUrl => send_status(sender, e, error_handling::ErrorBranch::Break).await, JdsError::BadCliArgs => send_status(sender, e, error_handling::ErrorBranch::Break).await, + JdsError::InvalidPrevHash => { + send_status(sender, e, error_handling::ErrorBranch::Break).await + } + JdsError::InvalidCoinbase => { + send_status(sender, e, error_handling::ErrorBranch::Break).await + } + JdsError::InvalidMerkleRoot => { + send_status(sender, e, error_handling::ErrorBranch::Break).await + } } } @@ -162,7 +171,11 @@ mod tests { use super::*; use async_channel::{bounded, RecvError}; - use roles_logic_sv2::mining_sv2::OpenMiningChannelError; + use stratum_common::roles_logic_sv2::{ + self, + codec_sv2::{self, binary_sv2, noise_sv2}, + mining_sv2::OpenMiningChannelError, + }; #[tokio::test] async fn test_send_status_downstream_listener_shutdown() { diff --git a/roles/jd-server/src/main.rs b/roles/jd-server/src/main.rs index 77d874bd0b..f17482595d 100644 --- a/roles/jd-server/src/main.rs +++ b/roles/jd-server/src/main.rs @@ -6,7 +6,8 @@ //! The actual task orchestration and shutdown logic are managed in `lib/mod.rs`. mod args; use args::process_cli_args; -use jd_server::{config::JobDeclaratorServerConfig, JobDeclaratorServer}; +use config_helpers_sv2::logging::init_logging; +use jd_server::JobDeclaratorServer; use tracing::error; /// Entrypoint for the Job Declarator Server binary. @@ -15,14 +16,13 @@ use tracing::error; /// defined in `jd_server::JobDeclaratorServer`. Errors during startup are logged. #[tokio::main] async fn main() { - tracing_subscriber::fmt::init(); - let config: JobDeclaratorServerConfig = match process_cli_args() { + let config = match process_cli_args() { Ok(cfg) => cfg, Err(e) => { error!("Failed to process CLI arguments: {}", e); return; } }; - + init_logging(config.log_file()); let _ = JobDeclaratorServer::new(config).start().await; } diff --git a/roles/mining-proxy/Cargo.toml b/roles/mining-proxy/Cargo.toml deleted file mode 100644 index 6508e00d18..0000000000 --- a/roles/mining-proxy/Cargo.toml +++ /dev/null @@ -1,37 +0,0 @@ -[package] -name = "mining_proxy_sv2" -version = "0.1.3" -authors = ["The Stratum V2 Developers"] -edition = "2018" -description = "SV2 mining proxy role" -documentation = "https://docs.rs/mining_proxy_sv2" -readme = "README.md" -homepage = "https://stratumprotocol.org" -repository = "https://github.com/stratum-mining/stratum" -license = "MIT OR Apache-2.0" -keywords = ["stratum", "mining", "bitcoin", "protocol"] - - -[lib] -name = "mining_proxy_sv2" -path = "src/lib/mod.rs" - -[dependencies] -async-channel = "1.8.0" -async-recursion = "0.3.2" -binary_sv2 = { path = "../../protocols/v2/binary-sv2" } -buffer_sv2 = { path = "../../utils/buffer" } -codec_sv2 = { path = "../../protocols/v2/codec-sv2", features = ["noise_sv2", "with_buffer_pool"] } -futures = "0.3.19" -network_helpers_sv2 = { path = "../roles-utils/network-helpers", features = ["with_buffer_pool"] } -once_cell = "1.12.0" -roles_logic_sv2 = { path = "../../protocols/v2/roles-logic-sv2" } -serde = { version = "1.0.89", features = ["derive", "alloc"], default-features = false } -stratum-common = { path = "../../common" } -tokio = { version = "1.44.1", features = ["full"] } -ext-config = { version = "0.14.0", features = ["toml"], package = "config" } -tracing = {version = "0.1"} -tracing-subscriber = {version = "0.3"} -nohash-hasher = "0.2.0" -key-utils = { path = "../../utils/key-utils" } -clap = { version = "4.5.39", features = ["derive"] } diff --git a/roles/mining-proxy/README.md b/roles/mining-proxy/README.md deleted file mode 100644 index da17d89102..0000000000 --- a/roles/mining-proxy/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# mining-proxy - -## Run - -## proxy-config.toml file - -When spawned the proxy will look in the current working directory (linux) for a -`proxy-config.config` if the file is not available the proxy will panic. We can specify a different -path for the config file with the `-c` option. - -The config need to be a valid toml file with the below values: -1. upstreams: vector of upstreams (likely pools). An upstream is composed by: - 1. channel_kind: can be either `Group`, `Extended`, `ExtendedWithDeclarator`. - * __Group__: Proxy do not open an extended channel with upstream but just relay request to - open standard channel from downstream to upstream, being the proxy non HOM the channels are - grouped. - * __Extended__: Proxy open an extended channel with upstream. When downstream ask to open - standard channels it just use the open extended channel with upstream to itself open - standard channels downstream. - * __ExtendedWithDeclarator__: Like `Extended` but do not relay on the pool to create new job. It - just connect to a TP and communicate to the pool which is the job that it want to work with. - 2. adress: ip address of the upstream - 3. port: upstream's port - 4. pub_key: is the public key that upstream will use to sign the upstream cert needed for the - noise handshake. - 5. jd_values: optional value only needed when `channel_kind` is `ExtendedWithDeclarator` is - composed by: - 1. address: ip of the JD that we want to use with this upstream - 2. port: port of the JD that we want to use with this upstream - 3. pub_key: pub_key of the JD that we want to use with this upstream -2. tp_address: optional value only needed when at least one `upstream` in `upstreams` has the kind - `ExtendedWithDeclarator`. Is the address in the form `[ip:port]` of the TP. -3. listen_address: the address at which the `mining-proxy` will accept downstream connection. -4. listen_mining_port: the port at which the `mining-proxy` will accept downstream connection. -5. max_supported_version: the `mining-proxy` will not connect to upstream the are using an Sv2 - version higher that the one specified here (default to 2) -6. min_supported_version: the `mining-proxy` will not connect to upstream the are using an Sv2 - version smaller that the one specified here (default to 2) -7. downstream_share_per_minute: how many share per minute downstream is supposed to produce. The - `mining-proxy` will use this value and the expected downstream hash rate (communicate vie - `penStandardMiningChannel` to calculate the right downstream target. - -### Test miner <-> proxy <-> pool stack - -Terminal 1: -``` -% cd examples/sv2-proxy -% cargo run --bin pool -``` - -Terminal 2: -Run mining proxy: - -``` -% # For help run `cargo run -- --help` -% cd roles/v2/mining-proxy -% cargo run -``` - -Terminal 3: -``` -% cd examples/sv2-proxy -% cargo run --bin mining-device -``` diff --git a/roles/mining-proxy/config-examples/proxy-config-example.toml b/roles/mining-proxy/config-examples/proxy-config-example.toml deleted file mode 100644 index 5fc7605149..0000000000 --- a/roles/mining-proxy/config-examples/proxy-config-example.toml +++ /dev/null @@ -1,13 +0,0 @@ -upstreams = [ - { channel_kind = "Extended", address = "0.0.0.0", port = 34254, pub_key = "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72"} -] -listen_address = "127.0.0.1" -listen_mining_port = 34255 -max_supported_version = 2 -min_supported_version = 2 -downstream_share_per_minute = 1 -# This value is used by the proxy to communicate to the pool the expected hash rate when we open an -# extended channel, based on it the pool will set a target for the channel -expected_total_downstream_hr = 10_000 -# If set to true the proxy will try to reconnect to an upstream that drop the connection -reconnect = true diff --git a/roles/mining-proxy/flamegraph.svg b/roles/mining-proxy/flamegraph.svg deleted file mode 100644 index 4e44857e02..0000000000 --- a/roles/mining-proxy/flamegraph.svg +++ /dev/null @@ -1,419 +0,0 @@ -Flame Graph Reset ZoomSearch [unknown] (12 samples, 9.09%)[unknown][unknown] (3 samples, 2.27%)[..[unknown] (2 samples, 1.52%)[unknown] (1 samples, 0.76%)[unknown] (22 samples, 16.67%)[unknown][unknown] (21 samples, 15.91%)[unknown][unknown] (14 samples, 10.61%)[unknown][unknown] (10 samples, 7.58%)[unknown][unknown] (4 samples, 3.03%)[un..__GI___ctype_init (2 samples, 1.52%)__GI___sigsetjmp (1 samples, 0.76%)__GI__setjmp (4 samples, 3.03%)__G..__sigjmp_save (1 samples, 0.76%)<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once (1 samples, 0.76%)<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once (1 samples, 0.76%)core::ops::function::FnOnce::call_once{{vtable.shim}} (1 samples, 0.76%)std::thread::Builder::spawn_unchecked::{{closure}} (1 samples, 0.76%)__prctl (1 samples, 0.76%)[unknown] (1 samples, 0.76%)[unknown] (1 samples, 0.76%)[unknown] (1 samples, 0.76%)[unknown] (1 samples, 0.76%)__GI___sigaltstack (5 samples, 3.79%)__GI..[unknown] (5 samples, 3.79%)[unk..[unknown] (5 samples, 3.79%)[unk..[unknown] (5 samples, 3.79%)[unk..[unknown] (4 samples, 3.03%)[un..[unknown] (1 samples, 0.76%)__clone3 (39 samples, 29.55%)__clone3start_thread (39 samples, 29.55%)start_threadstd::sys::unix::thread::Thread::new::thread_start (9 samples, 6.82%)std::sys:..std::sys::unix::stack_overflow::Handler::new (8 samples, 6.06%)std::sys..std::sys::unix::stack_overflow::imp::make_handler (8 samples, 6.06%)std::sys..std::sys::unix::stack_overflow::imp::get_stack (3 samples, 2.27%)s..std::sys::unix::stack_overflow::imp::get_stackp (3 samples, 2.27%)s..std::sys::unix::os::page_size (3 samples, 2.27%)s..__GI___sysconf (3 samples, 2.27%)_..[unknown] (1 samples, 0.76%)[unknown] (1 samples, 0.76%)[unknown] (1 samples, 0.76%)[unknown] (1 samples, 0.76%)[unknown] (1 samples, 0.76%)[unknown] (1 samples, 0.76%)[unknown] (1 samples, 0.76%)_dl_start_final (1 samples, 0.76%)_dl_sysdep_start (1 samples, 0.76%)dl_platform_init (1 samples, 0.76%)init_cpu_features (1 samples, 0.76%)dl_init_cacheinfo (1 samples, 0.76%)handle_amd (1 samples, 0.76%)mining-proxy (58 samples, 43.94%)mining-proxy_start (5 samples, 3.79%)_sta.._dl_start (4 samples, 3.03%)_dl..rtld_timer_start (1 samples, 0.76%)[unknown] (1 samples, 0.76%)[unknown] (1 samples, 0.76%)[unknown] (1 samples, 0.76%)[unknown] (1 samples, 0.76%)[unknown] (1 samples, 0.76%)[unknown] (1 samples, 0.76%)<noise_sv2::Initiator as noise_sv2::handshake::Step>::step (1 samples, 0.76%)snow::handshakestate::HandshakeState::read_message (1 samples, 0.76%)snow::handshakestate::HandshakeState::_read_message (1 samples, 0.76%)snow::handshakestate::HandshakeState::dh (1 samples, 0.76%)<snow::resolvers::default::Dh25519 as snow::types::Dh>::dh (1 samples, 0.76%)curve25519_dalek::montgomery::<impl core::ops::arith::Mul<curve25519_dalek::montgomery::MontgomeryPoint> for curve25519_dalek::scalar::Scalar>::mul (1 samples, 0.76%)curve25519_dalek::montgomery::<impl core::ops::arith::Mul<&curve25519_dalek::montgomery::MontgomeryPoint> for &curve25519_dalek::scalar::Scalar>::mul (1 samples, 0.76%)<&curve25519_dalek::montgomery::MontgomeryPoint as core::ops::arith::Mul<&curve25519_dalek::scalar::Scalar>>::mul (1 samples, 0.76%)curve25519_dalek::montgomery::differential_add_and_double (1 samples, 0.76%)<&curve25519_dalek::backend::serial::u64::field::FieldElement51 as core::ops::arith::Mul<&curve25519_dalek::backend::serial::u64::field::FieldElement51>>::mul (1 samples, 0.76%)<tokio::loom::std::parking_lot::MutexGuard<T> as core::ops::deref::DerefMut>::deref_mut (1 samples, 0.76%)<lock_api::mutex::MutexGuard<R,T> as core::ops::deref::DerefMut>::deref_mut (1 samples, 0.76%)alloc::collections::vec_deque::VecDeque<T,A>::buffer_read (1 samples, 0.76%)alloc::collections::vec_deque::VecDeque<T,A>::ptr (1 samples, 0.76%)alloc::raw_vec::RawVec<T,A>::ptr (1 samples, 0.76%)core::ptr::unique::Unique<T>::as_ptr (1 samples, 0.76%)alloc::collections::vec_deque::VecDeque<T,A>::pop_front (2 samples, 1.52%)alloc::collections::vec_deque::VecDeque<T,A>::is_empty (1 samples, 0.76%)parking_lot::raw_mutex::RawMutex::unlock_slow (1 samples, 0.76%)parking_lot_core::parking_lot::unpark_one (1 samples, 0.76%)parking_lot_core::parking_lot::lock_bucket (1 samples, 0.76%)parking_lot_core::word_lock::WordLock::lock (1 samples, 0.76%)core::sync::atomic::AtomicUsize::compare_exchange_weak (1 samples, 0.76%)core::sync::atomic::atomic_compare_exchange_weak (1 samples, 0.76%)core::mem::drop (2 samples, 1.52%)core::ptr::drop_in_place<tokio::loom::std::parking_lot::MutexGuard<tokio::runtime::blocking::pool::Shared>> (2 samples, 1.52%)core::ptr::drop_in_place<lock_api::mutex::MutexGuard<parking_lot::raw_mutex::RawMutex,tokio::runtime::blocking::pool::Shared>> (2 samples, 1.52%)<lock_api::mutex::MutexGuard<R,T> as core::ops::drop::Drop>::drop (2 samples, 1.52%)<parking_lot::raw_mutex::RawMutex as lock_api::mutex::RawMutex>::unlock (2 samples, 1.52%)parking_lot_core::parking_lot::deadlock::release_resource (1 samples, 0.76%)core::sync::atomic::AtomicU8::load (1 samples, 0.76%)parking_lot_core::parking_lot::park::{{closure}} (4 samples, 3.03%)par..core::cell::Cell<T>::set (1 samples, 0.76%)core::cell::Cell<T>::replace (1 samples, 0.76%)core::mem::replace (1 samples, 0.76%)core::ptr::write (1 samples, 0.76%)parking_lot_core::parking_lot::Bucket::new (1 samples, 0.76%)core::cell::UnsafeCell<T>::new (1 samples, 0.76%)parking_lot_core::parking_lot::park (6 samples, 4.55%)parki..parking_lot_core::parking_lot::with_thread_data (6 samples, 4.55%)parki..std::thread::local::LocalKey<T>::try_with (2 samples, 1.52%)parking_lot_core::parking_lot::with_thread_data::THREAD_DATA::__getit (2 samples, 1.52%)std::thread::local::fast::Key<T>::get (2 samples, 1.52%)std::thread::local::fast::Key<T>::try_initialize (2 samples, 1.52%)std::thread::local::lazy::LazyKeyInner<T>::initialize (2 samples, 1.52%)core::ops::function::FnOnce::call_once (2 samples, 1.52%)parking_lot_core::parking_lot::with_thread_data::THREAD_DATA::__init (2 samples, 1.52%)parking_lot_core::parking_lot::ThreadData::new (2 samples, 1.52%)parking_lot_core::parking_lot::grow_hashtable (2 samples, 1.52%)parking_lot_core::parking_lot::HashTable::new (2 samples, 1.52%)std::time::Instant::now (1 samples, 0.76%)std::sys::unix::time::inner::Instant::now (1 samples, 0.76%)std::sys::unix::time::inner::now (1 samples, 0.76%)__clock_gettime_2 (1 samples, 0.76%)__vdso_clock_gettime (1 samples, 0.76%)[unknown] (1 samples, 0.76%)[unknown] (1 samples, 0.76%)[unknown] (1 samples, 0.76%)[unknown] (1 samples, 0.76%)[unknown] (1 samples, 0.76%)[unknown] (1 samples, 0.76%)<I as core::iter::traits::collect::IntoIterator>::into_iter (2 samples, 1.52%)core::hint::spin_loop (1 samples, 0.76%)parking_lot::raw_mutex::RawMutex::lock_slow (13 samples, 9.85%)parking_lot::r..parking_lot_core::spinwait::SpinWait::spin (4 samples, 3.03%)par..parking_lot_core::spinwait::cpu_relax (4 samples, 3.03%)par..core::iter::range::<impl core::iter::traits::iterator::Iterator for core::ops::range::Range<A>>::next (1 samples, 0.76%)<core::ops::range::Range<T> as core::iter::range::RangeIteratorImpl>::spec_next (1 samples, 0.76%)<u32 as core::iter::range::Step>::forward_unchecked (1 samples, 0.76%)tokio::loom::std::parking_lot::Mutex<T>::lock (14 samples, 10.61%)tokio::loom::st..lock_api::mutex::Mutex<R,T>::lock (14 samples, 10.61%)lock_api::mutex..<parking_lot::raw_mutex::RawMutex as lock_api::mutex::RawMutex>::lock (14 samples, 10.61%)<parking_lot::r..parking_lot_core::parking_lot::deadlock::acquire_resource (1 samples, 0.76%)<parking_lot_core::parking_lot::ParkResult as core::cmp::PartialEq>::eq (2 samples, 1.52%)[unknown] (1 samples, 0.76%)[unknown] (1 samples, 0.76%)[unknown] (1 samples, 0.76%)core::ptr::drop_in_place<core::option::Option<parking_lot_core::parking_lot::ThreadData>> (1 samples, 0.76%)core::sync::atomic::AtomicI32::load (7 samples, 5.30%)core::..core::sync::atomic::atomic_load (5 samples, 3.79%)core..[unknown] (1 samples, 0.76%)<parking_lot_core::thread_parker::imp::ThreadParker as parking_lot_core::thread_parker::ThreadParkerT>::park (12 samples, 9.09%)<parking_lot_..parking_lot_core::thread_parker::imp::ThreadParker::futex_wait (3 samples, 2.27%)p..syscall (1 samples, 0.76%)[unknown] (1 samples, 0.76%)tokio::runtime::thread_pool::park::Inner::park_condvar (20 samples, 15.15%)tokio::runtime::thread_..tokio::loom::std::parking_lot::Condvar::wait (20 samples, 15.15%)tokio::loom::std::parki..parking_lot::condvar::Condvar::wait (20 samples, 15.15%)parking_lot::condvar::C..parking_lot::condvar::Condvar::wait_until_internal (20 samples, 15.15%)parking_lot::condvar::C..parking_lot_core::parking_lot::park (18 samples, 13.64%)parking_lot_core::par..parking_lot_core::parking_lot::with_thread_data (18 samples, 13.64%)parking_lot_core::par..parking_lot_core::parking_lot::park::{{closure}} (17 samples, 12.88%)parking_lot_core::p..parking_lot_core::parking_lot::deadlock::on_unpark (1 samples, 0.76%)<tokio::park::either::Either<A,B> as tokio::park::Park>::park (2 samples, 1.52%)<tokio::process::imp::driver::Driver as tokio::park::Park>::park (2 samples, 1.52%)<tokio::signal::unix::driver::Driver as tokio::park::Park>::park (2 samples, 1.52%)<tokio::io::driver::Driver as tokio::park::Park>::park (2 samples, 1.52%)tokio::io::driver::Driver::turn (2 samples, 1.52%)<tokio::runtime::thread_pool::park::Parker as tokio::park::Park>::park (23 samples, 17.42%)<tokio::runtime::thread_poo..tokio::runtime::thread_pool::park::Inner::park (23 samples, 17.42%)tokio::runtime::thread_pool..tokio::runtime::thread_pool::park::Inner::park_driver (3 samples, 2.27%)t..<tokio::runtime::driver::Driver as tokio::park::Park>::park (3 samples, 2.27%)<..<tokio::park::either::Either<A,B> as tokio::park::Park>::park (3 samples, 2.27%)<..<tokio::time::driver::Driver<P> as tokio::park::Park>::park (3 samples, 2.27%)<..tokio::time::driver::Driver<P>::park_internal (3 samples, 2.27%)t..tokio::time::driver::<impl tokio::time::driver::handle::Handle>::process (1 samples, 0.76%)tokio::time::driver::ClockTime::now (1 samples, 0.76%)tokio::time::driver::ClockTime::instant_to_tick (1 samples, 0.76%)core::time::Duration::as_millis (1 samples, 0.76%)tokio::runtime::thread_pool::worker::Context::park_timeout (24 samples, 18.18%)tokio::runtime::thread_pool:..core::ptr::drop_in_place<core::option::Option<tokio::runtime::thread_pool::park::Parker>> (1 samples, 0.76%)tokio::runtime::thread_pool::worker::Core::maintenance (1 samples, 0.76%)tokio::runtime::task::inject::Inject<T>::is_closed (1 samples, 0.76%)core::ptr::drop_in_place<tokio::loom::std::parking_lot::MutexGuard<tokio::runtime::task::inject::Pointers>> (1 samples, 0.76%)core::ptr::drop_in_place<lock_api::mutex::MutexGuard<parking_lot::raw_mutex::RawMutex,tokio::runtime::task::inject::Pointers>> (1 samples, 0.76%)tokio::runtime::thread_pool::worker::Core::transition_from_parked (1 samples, 0.76%)tokio::runtime::thread_pool::idle::Idle::is_parked (1 samples, 0.76%)core::slice::<impl [T]>::contains (1 samples, 0.76%)<T as core::slice::cmp::SliceContains>::slice_contains (1 samples, 0.76%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::any (1 samples, 0.76%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.76%)tokio::runtime::thread_pool::worker::Context::park (27 samples, 20.45%)tokio::runtime::thread_pool::wor..tokio::runtime::thread_pool::worker::Core::transition_to_parked (1 samples, 0.76%)tokio::runtime::thread_pool::worker::Shared::notify_if_work_pending (1 samples, 0.76%)tokio::runtime::thread_pool::queue::Steal<T>::is_empty (1 samples, 0.76%)tokio::runtime::thread_pool::queue::Inner<T>::is_empty (1 samples, 0.76%)tokio::runtime::thread_pool::queue::Inner<T>::len (1 samples, 0.76%)core::sync::atomic::AtomicU32::load (1 samples, 0.76%)tokio::runtime::task::inject::Inject<T>::pop (1 samples, 0.76%)tokio::macros::scoped_tls::ScopedKey<T>::set (30 samples, 22.73%)tokio::macros::scoped_tls::ScopedKey..tokio::runtime::thread_pool::worker::run::{{closure}} (30 samples, 22.73%)tokio::runtime::thread_pool::worker:..tokio::runtime::thread_pool::worker::Context::run (30 samples, 22.73%)tokio::runtime::thread_pool::worker:..tokio::runtime::thread_pool::worker::Core::next_task (3 samples, 2.27%)t..core::option::Option<T>::or_else (3 samples, 2.27%)c..tokio::runtime::thread_pool::worker::Core::next_task::{{closure}} (2 samples, 1.52%)tokio::runtime::thread_pool::worker::Worker::inject (1 samples, 0.76%)tokio::runtime::blocking::pool::Inner::run (50 samples, 37.88%)tokio::runtime::blocking::pool::Inner::runtokio::runtime::blocking::pool::Task::run (31 samples, 23.48%)tokio::runtime::blocking::pool::Task:..tokio::runtime::task::UnownedTask<S>::run (31 samples, 23.48%)tokio::runtime::task::UnownedTask<S>:..tokio::runtime::task::raw::RawTask::poll (31 samples, 23.48%)tokio::runtime::task::raw::RawTask::p..tokio::runtime::task::raw::poll (31 samples, 23.48%)tokio::runtime::task::raw::polltokio::runtime::task::harness::Harness<T,S>::poll (31 samples, 23.48%)tokio::runtime::task::harness::Harnes..tokio::runtime::task::harness::Harness<T,S>::poll_inner (31 samples, 23.48%)tokio::runtime::task::harness::Harnes..tokio::runtime::task::harness::poll_future (31 samples, 23.48%)tokio::runtime::task::harness::poll_f..std::panic::catch_unwind (31 samples, 23.48%)std::panic::catch_unwindstd::panicking::try (31 samples, 23.48%)std::panicking::try__rust_try (31 samples, 23.48%)__rust_trystd::panicking::try::do_call (31 samples, 23.48%)std::panicking::try::do_call<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce< (31 samples, 23.48%)<core::panic::unwind_safe::AssertUnwi..tokio::runtime::task::harness::poll_future::{{closure}} (31 samples, 23.48%)tokio::runtime::task::harness::poll_f..tokio::runtime::task::core::CoreStage<T>::poll (31 samples, 23.48%)tokio::runtime::task::core::CoreStage..tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (31 samples, 23.48%)tokio::loom::std::unsafe_cell::Unsafe..tokio::runtime::task::core::CoreStage<T>::poll::{{closure}} (31 samples, 23.48%)tokio::runtime::task::core::CoreStage..<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (31 samples, 23.48%)<tokio::runtime::blocking::task::Bloc..tokio::runtime::thread_pool::worker::Launch::launch::{{closure}} (31 samples, 23.48%)tokio::runtime::thread_pool::worker::..tokio::runtime::thread_pool::worker::run (31 samples, 23.48%)tokio::runtime::thread_pool::worker::..tokio::util::atomic_cell::AtomicCell<T>::take (1 samples, 0.76%)tokio::util::atomic_cell::AtomicCell<T>::swap (1 samples, 0.76%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce< (51 samples, 38.64%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::fu..std::thread::Builder::spawn_unchecked::{{closure}}::{{closure}} (51 samples, 38.64%)std::thread::Builder::spawn_unchecked::{{closure}}::{{closure}}std::sys_common::backtrace::__rust_begin_short_backtrace (51 samples, 38.64%)std::sys_common::backtrace::__rust_begin_short_backtracetokio::runtime::blocking::pool::Spawner::spawn_thread::{{closure}} (51 samples, 38.64%)tokio::runtime::blocking::pool::Spawner::spawn_thread::{{closur..tokio::runtime::context::enter (1 samples, 0.76%)tokio::runtime::context::try_enter (1 samples, 0.76%)std::thread::local::LocalKey<T>::try_with (1 samples, 0.76%)tokio::runtime::context::CONTEXT::__getit (1 samples, 0.76%)std::thread::local::fast::Key<T>::get (1 samples, 0.76%)std::thread::local::fast::Key<T>::try_initialize (1 samples, 0.76%)std::thread::local::fast::Key<T>::try_register_dtor (1 samples, 0.76%)core::cell::Cell<T>::set (1 samples, 0.76%)std::panic::catch_unwind (52 samples, 39.39%)std::panic::catch_unwindstd::panicking::try (52 samples, 39.39%)std::panicking::try__rust_try (52 samples, 39.39%)__rust_trystd::panicking::try::do_call (52 samples, 39.39%)std::panicking::try::do_callcore::mem::manually_drop::ManuallyDrop<T>::take (1 samples, 0.76%)core::ptr::read (1 samples, 0.76%)[unknown] (1 samples, 0.76%)[unknown] (1 samples, 0.76%)[unknown] (1 samples, 0.76%)[unknown] (1 samples, 0.76%)__clone3 (56 samples, 42.42%)__clone3start_thread (56 samples, 42.42%)start_threadstd::sys::unix::thread::Thread::new::thread_start (56 samples, 42.42%)std::sys::unix::thread::Thread::new::thread_start<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once (56 samples, 42.42%)<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_o..<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once (56 samples, 42.42%)<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_o..core::ops::function::FnOnce::call_once{{vtable.shim}} (56 samples, 42.42%)core::ops::function::FnOnce::call_once{{vtable.shim}}std::thread::Builder::spawn_unchecked::{{closure}} (56 samples, 42.42%)std::thread::Builder::spawn_unchecked::{{closure}}std::sys::unix::thread::guard::current (4 samples, 3.03%)std..__pthread_getattr_np (4 samples, 3.03%)__p..__GI___libc_malloc (4 samples, 3.03%)__G..tcache_init.part.0 (4 samples, 3.03%)tca..arena_get2.part.0 (4 samples, 3.03%)are..alloc_new_heap (3 samples, 2.27%)a..__GI___mmap64 (2 samples, 1.52%)[unknown] (2 samples, 1.52%)[unknown] (2 samples, 1.52%)[unknown] (2 samples, 1.52%)[unknown] (1 samples, 0.76%)[unknown] (1 samples, 0.76%)__pthread_getaffinity_alias (1 samples, 0.76%)[unknown] (1 samples, 0.76%)core::ops::function::FnOnce::call_once{{vtable.shim}} (2 samples, 1.52%)std::thread::Builder::spawn_unchecked::{{closure}} (2 samples, 1.52%)std::panic::catch_unwind (2 samples, 1.52%)std::panicking::try (2 samples, 1.52%)__rust_try (2 samples, 1.52%)std::panicking::try::do_call (2 samples, 1.52%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce< (2 samples, 1.52%)std::thread::Builder::spawn_unchecked::{{closure}}::{{closure}} (2 samples, 1.52%)std::sys_common::backtrace::__rust_begin_short_backtrace (2 samples, 1.52%)tokio::runtime::blocking::pool::Spawner::spawn_thread::{{closure}} (2 samples, 1.52%)tokio::runtime::blocking::pool::Inner::run (2 samples, 1.52%)tokio::runtime::blocking::pool::Task::run (2 samples, 1.52%)tokio::runtime::task::UnownedTask<S>::run (2 samples, 1.52%)tokio::runtime::task::raw::RawTask::poll (2 samples, 1.52%)tokio::runtime::task::raw::poll (2 samples, 1.52%)tokio::runtime::task::harness::Harness<T,S>::poll (2 samples, 1.52%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (2 samples, 1.52%)tokio::runtime::task::harness::poll_future (2 samples, 1.52%)std::panic::catch_unwind (2 samples, 1.52%)std::panicking::try (2 samples, 1.52%)__rust_try (2 samples, 1.52%)std::panicking::try::do_call (2 samples, 1.52%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce< (2 samples, 1.52%)tokio::runtime::task::harness::poll_future::{{closure}} (2 samples, 1.52%)tokio::runtime::task::core::CoreStage<T>::poll (2 samples, 1.52%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (2 samples, 1.52%)tokio::runtime::task::core::CoreStage<T>::poll::{{closure}} (2 samples, 1.52%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (2 samples, 1.52%)tokio::runtime::thread_pool::worker::Launch::launch::{{closure}} (2 samples, 1.52%)tokio::runtime::thread_pool::worker::run (2 samples, 1.52%)tokio::macros::scoped_tls::ScopedKey<T>::set (2 samples, 1.52%)tokio::runtime::thread_pool::worker::run::{{closure}} (2 samples, 1.52%)tokio::runtime::thread_pool::worker::Context::run (2 samples, 1.52%)tokio::runtime::thread_pool::worker::Context::park (2 samples, 1.52%)tokio::runtime::thread_pool::worker::Context::park_timeout (2 samples, 1.52%)<tokio::runtime::thread_pool::park::Parker as tokio::park::Park>::park (2 samples, 1.52%)tokio::runtime::thread_pool::park::Inner::park (2 samples, 1.52%)tokio::runtime::thread_pool::park::Inner::park_driver (2 samples, 1.52%)<tokio::runtime::driver::Driver as tokio::park::Park>::park (2 samples, 1.52%)<tokio::park::either::Either<A,B> as tokio::park::Park>::park (2 samples, 1.52%)<tokio::time::driver::Driver<P> as tokio::park::Park>::park (2 samples, 1.52%)tokio::time::driver::Driver<P>::park_internal (2 samples, 1.52%)<tokio::park::either::Either<A,B> as tokio::park::Park>::park (2 samples, 1.52%)<tokio::process::imp::driver::Driver as tokio::park::Park>::park (2 samples, 1.52%)<tokio::signal::unix::driver::Driver as tokio::park::Park>::park (2 samples, 1.52%)<tokio::io::driver::Driver as tokio::park::Park>::park (2 samples, 1.52%)tokio::io::driver::Driver::turn (2 samples, 1.52%)mio::poll::Poll::poll (2 samples, 1.52%)mio::sys::unix::selector::epoll::Selector::select (2 samples, 1.52%)core::result::Result<T,E>::map (2 samples, 1.52%)mio::sys::unix::selector::epoll::Selector::select::{{closure}} (2 samples, 1.52%)alloc::vec::Vec<T,A>::set_len (2 samples, 1.52%)mining_proxy::lib::upstream_mining::UpstreamMiningNode::connect::{{closure}} (1 samples, 0.76%)noise_sv2::Initiator::from_raw_k (1 samples, 0.76%)ed25519_dalek::public::PublicKey::from_bytes (1 samples, 0.76%)curve25519_dalek::edwards::CompressedEdwardsY::decompress (1 samples, 0.76%)curve25519_dalek::field::<impl curve25519_dalek::backend::serial::u64::field::FieldElement51>::sqrt_ratio_i (1 samples, 0.76%)<&curve25519_dalek::backend::serial::u64::field::FieldElement51 as core::ops::arith::Mul<&curve25519_dalek::backend::serial::u64::field::FieldElement51>>::mul (1 samples, 0.76%)__memmove_avx_unaligned (1 samples, 0.76%)start_thread (2 samples, 1.52%)std::sys::unix::thread::Thread::new::thread_start (2 samples, 1.52%)<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once (2 samples, 1.52%)<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once (2 samples, 1.52%)core::ops::function::FnOnce::call_once{{vtable.shim}} (2 samples, 1.52%)std::thread::Builder::spawn_unchecked::{{closure}} (2 samples, 1.52%)std::panic::catch_unwind (2 samples, 1.52%)std::panicking::try (2 samples, 1.52%)__rust_try (2 samples, 1.52%)std::panicking::try::do_call (2 samples, 1.52%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce< (2 samples, 1.52%)std::thread::Builder::spawn_unchecked::{{closure}}::{{closure}} (2 samples, 1.52%)std::sys_common::backtrace::__rust_begin_short_backtrace (2 samples, 1.52%)tokio::runtime::blocking::pool::Spawner::spawn_thread::{{closure}} (2 samples, 1.52%)tokio::runtime::blocking::pool::Inner::run (2 samples, 1.52%)tokio::runtime::blocking::pool::Task::run (2 samples, 1.52%)tokio::runtime::task::UnownedTask<S>::run (2 samples, 1.52%)tokio::runtime::task::raw::RawTask::poll (2 samples, 1.52%)tokio::runtime::task::raw::poll (2 samples, 1.52%)tokio::runtime::task::harness::Harness<T,S>::poll (2 samples, 1.52%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (2 samples, 1.52%)tokio::runtime::task::harness::poll_future (2 samples, 1.52%)std::panic::catch_unwind (2 samples, 1.52%)std::panicking::try (2 samples, 1.52%)__rust_try (2 samples, 1.52%)std::panicking::try::do_call (2 samples, 1.52%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce< (2 samples, 1.52%)tokio::runtime::task::harness::poll_future::{{closure}} (2 samples, 1.52%)tokio::runtime::task::core::CoreStage<T>::poll (2 samples, 1.52%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (2 samples, 1.52%)tokio::runtime::task::core::CoreStage<T>::poll::{{closure}} (2 samples, 1.52%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (2 samples, 1.52%)tokio::runtime::thread_pool::worker::Launch::launch::{{closure}} (2 samples, 1.52%)tokio::runtime::thread_pool::worker::run (2 samples, 1.52%)tokio::macros::scoped_tls::ScopedKey<T>::set (2 samples, 1.52%)tokio::runtime::thread_pool::worker::run::{{closure}} (2 samples, 1.52%)tokio::runtime::thread_pool::worker::Context::run (2 samples, 1.52%)tokio::runtime::thread_pool::worker::Context::park (2 samples, 1.52%)tokio::runtime::thread_pool::worker::Context::park_timeout (2 samples, 1.52%)<tokio::runtime::thread_pool::park::Parker as tokio::park::Park>::park (2 samples, 1.52%)tokio::runtime::thread_pool::park::Inner::park (2 samples, 1.52%)tokio::runtime::thread_pool::park::Inner::park_driver (2 samples, 1.52%)<tokio::runtime::driver::Driver as tokio::park::Park>::park (2 samples, 1.52%)<tokio::park::either::Either<A,B> as tokio::park::Park>::park (2 samples, 1.52%)<tokio::time::driver::Driver<P> as tokio::park::Park>::park (2 samples, 1.52%)tokio::time::driver::Driver<P>::park_internal (2 samples, 1.52%)<tokio::park::either::Either<A,B> as tokio::park::Park>::park (2 samples, 1.52%)<tokio::process::imp::driver::Driver as tokio::park::Park>::park (2 samples, 1.52%)<tokio::signal::unix::driver::Driver as tokio::park::Park>::park (2 samples, 1.52%)<tokio::io::driver::Driver as tokio::park::Park>::park (2 samples, 1.52%)tokio::io::driver::Driver::turn (2 samples, 1.52%)mio::poll::Poll::poll (2 samples, 1.52%)mio::sys::unix::selector::epoll::Selector::select (2 samples, 1.52%)std::panic::catch_unwind (1 samples, 0.76%)std::panicking::try (1 samples, 0.76%)__rust_try (1 samples, 0.76%)std::panicking::try::do_call (1 samples, 0.76%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce< (1 samples, 0.76%)std::thread::Builder::spawn_unchecked::{{closure}}::{{closure}} (1 samples, 0.76%)std::sys_common::backtrace::__rust_begin_short_backtrace (1 samples, 0.76%)tokio::runtime::blocking::pool::Spawner::spawn_thread::{{closure}} (1 samples, 0.76%)tokio::runtime::blocking::pool::Inner::run (1 samples, 0.76%)tokio::runtime::blocking::pool::Task::run (1 samples, 0.76%)tokio::runtime::task::UnownedTask<S>::run (1 samples, 0.76%)tokio::runtime::task::raw::RawTask::poll (1 samples, 0.76%)tokio::runtime::task::raw::poll (1 samples, 0.76%)tokio::runtime::task::harness::Harness<T,S>::poll (1 samples, 0.76%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (1 samples, 0.76%)tokio::runtime::task::harness::poll_future (1 samples, 0.76%)std::panic::catch_unwind (1 samples, 0.76%)std::panicking::try (1 samples, 0.76%)__rust_try (1 samples, 0.76%)std::panicking::try::do_call (1 samples, 0.76%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce< (1 samples, 0.76%)tokio::runtime::task::harness::poll_future::{{closure}} (1 samples, 0.76%)tokio::runtime::task::core::CoreStage<T>::poll (1 samples, 0.76%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (1 samples, 0.76%)tokio::runtime::task::core::CoreStage<T>::poll::{{closure}} (1 samples, 0.76%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (1 samples, 0.76%)tokio::runtime::thread_pool::worker::Launch::launch::{{closure}} (1 samples, 0.76%)tokio::runtime::thread_pool::worker::run (1 samples, 0.76%)tokio::macros::scoped_tls::ScopedKey<T>::set (1 samples, 0.76%)tokio::runtime::thread_pool::worker::run::{{closure}} (1 samples, 0.76%)tokio::runtime::thread_pool::worker::Context::run (1 samples, 0.76%)tokio::runtime::thread_pool::worker::Context::park (1 samples, 0.76%)tokio::runtime::thread_pool::worker::Context::park_timeout (1 samples, 0.76%)<tokio::runtime::thread_pool::park::Parker as tokio::park::Park>::park (1 samples, 0.76%)tokio::runtime::thread_pool::park::Inner::park (1 samples, 0.76%)tokio::runtime::thread_pool::park::Inner::park_driver (1 samples, 0.76%)<tokio::runtime::driver::Driver as tokio::park::Park>::park (1 samples, 0.76%)<tokio::park::either::Either<A,B> as tokio::park::Park>::park (1 samples, 0.76%)<tokio::time::driver::Driver<P> as tokio::park::Park>::park (1 samples, 0.76%)tokio::time::driver::Driver<P>::park_internal (1 samples, 0.76%)tokio::time::driver::Driver<P>::park_timeout (1 samples, 0.76%)<tokio::park::either::Either<A,B> as tokio::park::Park>::park_timeout (1 samples, 0.76%)<tokio::process::imp::driver::Driver as tokio::park::Park>::park_timeout (1 samples, 0.76%)<tokio::signal::unix::driver::Driver as tokio::park::Park>::park_timeout (1 samples, 0.76%)tokio::signal::unix::driver::Driver::process (1 samples, 0.76%)tokio::io::driver::registration::Registration::poll_read_ready (1 samples, 0.76%)tokio::io::driver::registration::Registration::poll_ready (1 samples, 0.76%)tokio::coop::poll_proceed (1 samples, 0.76%)std::thread::local::LocalKey<T>::with (1 samples, 0.76%)std::thread::local::LocalKey<T>::try_with (1 samples, 0.76%)tokio::coop::poll_proceed::{{closure}} (1 samples, 0.76%)core::cell::Cell<T>::set (1 samples, 0.76%)core::mem::drop (1 samples, 0.76%)<tokio::park::either::Either<A,B> as tokio::park::Park>::park (1 samples, 0.76%)<tokio::process::imp::driver::Driver as tokio::park::Park>::park (1 samples, 0.76%)<tokio::signal::unix::driver::Driver as tokio::park::Park>::park (1 samples, 0.76%)<tokio::io::driver::Driver as tokio::park::Park>::park (1 samples, 0.76%)tokio::io::driver::Driver::turn (1 samples, 0.76%)tokio::io::driver::Driver::dispatch (1 samples, 0.76%)tokio::io::driver::scheduled_io::ScheduledIo::wake (1 samples, 0.76%)tokio::io::driver::scheduled_io::ScheduledIo::wake0 (1 samples, 0.76%)core::option::Option<T>::take (1 samples, 0.76%)std::panicking::try::do_call (2 samples, 1.52%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce< (2 samples, 1.52%)std::thread::Builder::spawn_unchecked::{{closure}}::{{closure}} (2 samples, 1.52%)std::sys_common::backtrace::__rust_begin_short_backtrace (2 samples, 1.52%)tokio::runtime::blocking::pool::Spawner::spawn_thread::{{closure}} (2 samples, 1.52%)tokio::runtime::blocking::pool::Inner::run (2 samples, 1.52%)tokio::runtime::blocking::pool::Task::run (2 samples, 1.52%)tokio::runtime::task::UnownedTask<S>::run (2 samples, 1.52%)tokio::runtime::task::raw::RawTask::poll (2 samples, 1.52%)tokio::runtime::task::raw::poll (2 samples, 1.52%)tokio::runtime::task::harness::Harness<T,S>::poll (2 samples, 1.52%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (2 samples, 1.52%)tokio::runtime::task::harness::poll_future (2 samples, 1.52%)std::panic::catch_unwind (2 samples, 1.52%)std::panicking::try (2 samples, 1.52%)__rust_try (2 samples, 1.52%)std::panicking::try::do_call (2 samples, 1.52%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce< (2 samples, 1.52%)tokio::runtime::task::harness::poll_future::{{closure}} (2 samples, 1.52%)tokio::runtime::task::core::CoreStage<T>::poll (2 samples, 1.52%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (2 samples, 1.52%)tokio::runtime::task::core::CoreStage<T>::poll::{{closure}} (2 samples, 1.52%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (2 samples, 1.52%)tokio::runtime::thread_pool::worker::Launch::launch::{{closure}} (2 samples, 1.52%)tokio::runtime::thread_pool::worker::run (2 samples, 1.52%)tokio::macros::scoped_tls::ScopedKey<T>::set (2 samples, 1.52%)tokio::runtime::thread_pool::worker::run::{{closure}} (2 samples, 1.52%)tokio::runtime::thread_pool::worker::Context::run (2 samples, 1.52%)tokio::runtime::thread_pool::worker::Context::park (2 samples, 1.52%)tokio::runtime::thread_pool::worker::Context::park_timeout (2 samples, 1.52%)<tokio::runtime::thread_pool::park::Parker as tokio::park::Park>::park (2 samples, 1.52%)tokio::runtime::thread_pool::park::Inner::park (2 samples, 1.52%)tokio::runtime::thread_pool::park::Inner::park_driver (2 samples, 1.52%)<tokio::runtime::driver::Driver as tokio::park::Park>::park (2 samples, 1.52%)<tokio::park::either::Either<A,B> as tokio::park::Park>::park (2 samples, 1.52%)<tokio::time::driver::Driver<P> as tokio::park::Park>::park (2 samples, 1.52%)tokio::time::driver::Driver<P>::park_internal (2 samples, 1.52%)tokio::time::driver::Driver<P>::park_timeout (1 samples, 0.76%)<tokio::park::either::Either<A,B> as tokio::park::Park>::park_timeout (1 samples, 0.76%)<tokio::process::imp::driver::Driver as tokio::park::Park>::park_timeout (1 samples, 0.76%)<tokio::signal::unix::driver::Driver as tokio::park::Park>::park_timeout (1 samples, 0.76%)tokio::signal::unix::driver::Driver::process (1 samples, 0.76%)tokio::io::driver::registration::Registration::poll_read_ready (1 samples, 0.76%)tokio::io::driver::registration::Registration::poll_ready (1 samples, 0.76%)tokio::io::driver::scheduled_io::ScheduledIo::poll_readiness (1 samples, 0.76%)tokio::loom::std::parking_lot::Mutex<T>::lock (1 samples, 0.76%)lock_api::mutex::Mutex<R,T>::lock (1 samples, 0.76%)<parking_lot::raw_mutex::RawMutex as lock_api::mutex::RawMutex>::lock (1 samples, 0.76%)core::sync::atomic::AtomicU8::compare_exchange_weak (1 samples, 0.76%)core::sync::atomic::atomic_compare_exchange_weak (1 samples, 0.76%)<tokio::park::either::Either<A,B> as tokio::park::Park>::park (2 samples, 1.52%)<tokio::process::imp::driver::Driver as tokio::park::Park>::park (2 samples, 1.52%)<tokio::signal::unix::driver::Driver as tokio::park::Park>::park (2 samples, 1.52%)<tokio::io::driver::Driver as tokio::park::Park>::park (2 samples, 1.52%)tokio::io::driver::Driver::turn (2 samples, 1.52%)mio::poll::Poll::poll (2 samples, 1.52%)mio::sys::unix::selector::epoll::Selector::select (2 samples, 1.52%)core::result::Result<T,E>::map (2 samples, 1.52%)std::sys::unix::thread::Thread::new::thread_start (3 samples, 2.27%)s..<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once (3 samples, 2.27%)<..<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once (3 samples, 2.27%)<..core::ops::function::FnOnce::call_once{{vtable.shim}} (3 samples, 2.27%)c..std::thread::Builder::spawn_unchecked::{{closure}} (3 samples, 2.27%)s..std::panic::catch_unwind (3 samples, 2.27%)s..std::panicking::try (3 samples, 2.27%)s..__rust_try (3 samples, 2.27%)_..std::panicking::try::do_call (3 samples, 2.27%)s..<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce< (3 samples, 2.27%)<..std::thread::Builder::spawn_unchecked::{{closure}}::{{closure}} (3 samples, 2.27%)s..std::sys_common::backtrace::__rust_begin_short_backtrace (3 samples, 2.27%)s..tokio::runtime::blocking::pool::Spawner::spawn_thread::{{closure}} (3 samples, 2.27%)t..tokio::runtime::blocking::pool::Inner::run (3 samples, 2.27%)t..tokio::runtime::blocking::pool::Task::run (3 samples, 2.27%)t..tokio::runtime::task::UnownedTask<S>::run (3 samples, 2.27%)t..tokio::runtime::task::raw::RawTask::poll (3 samples, 2.27%)t..tokio::runtime::task::raw::poll (3 samples, 2.27%)t..tokio::runtime::task::harness::Harness<T,S>::poll (3 samples, 2.27%)t..tokio::runtime::task::harness::Harness<T,S>::poll_inner (3 samples, 2.27%)t..tokio::runtime::task::harness::poll_future (3 samples, 2.27%)t..std::panic::catch_unwind (3 samples, 2.27%)s..std::panicking::try (3 samples, 2.27%)s..__rust_try (3 samples, 2.27%)_..std::panicking::try::do_call (3 samples, 2.27%)s..<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce< (3 samples, 2.27%)<..tokio::runtime::task::harness::poll_future::{{closure}} (3 samples, 2.27%)t..tokio::runtime::task::core::CoreStage<T>::poll (3 samples, 2.27%)t..tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (3 samples, 2.27%)t..tokio::runtime::task::core::CoreStage<T>::poll::{{closure}} (3 samples, 2.27%)t..<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (3 samples, 2.27%)<..tokio::runtime::thread_pool::worker::Launch::launch::{{closure}} (3 samples, 2.27%)t..tokio::runtime::thread_pool::worker::run (3 samples, 2.27%)t..tokio::macros::scoped_tls::ScopedKey<T>::set (3 samples, 2.27%)t..tokio::runtime::thread_pool::worker::run::{{closure}} (3 samples, 2.27%)t..tokio::runtime::thread_pool::worker::Context::run (3 samples, 2.27%)t..tokio::runtime::thread_pool::worker::Context::park (3 samples, 2.27%)t..tokio::runtime::thread_pool::worker::Context::park_timeout (3 samples, 2.27%)t..<tokio::runtime::thread_pool::park::Parker as tokio::park::Park>::park (3 samples, 2.27%)<..tokio::runtime::thread_pool::park::Inner::park (3 samples, 2.27%)t..tokio::runtime::thread_pool::park::Inner::park_driver (3 samples, 2.27%)t..<tokio::runtime::driver::Driver as tokio::park::Park>::park (3 samples, 2.27%)<..<tokio::park::either::Either<A,B> as tokio::park::Park>::park (3 samples, 2.27%)<..<tokio::time::driver::Driver<P> as tokio::park::Park>::park (3 samples, 2.27%)<..tokio::time::driver::Driver<P>::park_internal (3 samples, 2.27%)t..tokio::time::driver::Driver<P>::park_timeout (1 samples, 0.76%)<tokio::park::either::Either<A,B> as tokio::park::Park>::park_timeout (1 samples, 0.76%)<tokio::process::imp::driver::Driver as tokio::park::Park>::park_timeout (1 samples, 0.76%)<tokio::signal::unix::driver::Driver as tokio::park::Park>::park_timeout (1 samples, 0.76%)<tokio::io::driver::Driver as tokio::park::Park>::park_timeout (1 samples, 0.76%)tokio::io::driver::Driver::turn (1 samples, 0.76%)mio::poll::Poll::poll (1 samples, 0.76%)mio::sys::unix::selector::epoll::Selector::select (1 samples, 0.76%)core::result::Result<T,E>::map (2 samples, 1.52%)mio::sys::unix::selector::epoll::Selector::select::{{closure}} (1 samples, 0.76%)alloc::vec::Vec<T,A>::set_len (1 samples, 0.76%)<tokio::io::driver::Driver as tokio::park::Park>::park_timeout (3 samples, 2.27%)<..tokio::io::driver::Driver::turn (3 samples, 2.27%)t..mio::poll::Poll::poll (3 samples, 2.27%)m..mio::sys::unix::selector::epoll::Selector::select (3 samples, 2.27%)m..epoll_wait (1 samples, 0.76%)__GI___pthread_disable_asynccancel (1 samples, 0.76%)std::thread::Builder::spawn_unchecked::{{closure}} (4 samples, 3.03%)std..std::panic::catch_unwind (4 samples, 3.03%)std..std::panicking::try (4 samples, 3.03%)std..__rust_try (4 samples, 3.03%)__r..std::panicking::try::do_call (4 samples, 3.03%)std..<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce< (4 samples, 3.03%)<co..std::thread::Builder::spawn_unchecked::{{closure}}::{{closure}} (4 samples, 3.03%)std..std::sys_common::backtrace::__rust_begin_short_backtrace (4 samples, 3.03%)std..tokio::runtime::blocking::pool::Spawner::spawn_thread::{{closure}} (4 samples, 3.03%)tok..tokio::runtime::blocking::pool::Inner::run (4 samples, 3.03%)tok..tokio::runtime::blocking::pool::Task::run (4 samples, 3.03%)tok..tokio::runtime::task::UnownedTask<S>::run (4 samples, 3.03%)tok..tokio::runtime::task::raw::RawTask::poll (4 samples, 3.03%)tok..tokio::runtime::task::raw::poll (4 samples, 3.03%)tok..tokio::runtime::task::harness::Harness<T,S>::poll (4 samples, 3.03%)tok..tokio::runtime::task::harness::Harness<T,S>::poll_inner (4 samples, 3.03%)tok..tokio::runtime::task::harness::poll_future (4 samples, 3.03%)tok..std::panic::catch_unwind (4 samples, 3.03%)std..std::panicking::try (4 samples, 3.03%)std..__rust_try (4 samples, 3.03%)__r..std::panicking::try::do_call (4 samples, 3.03%)std..<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce< (4 samples, 3.03%)<co..tokio::runtime::task::harness::poll_future::{{closure}} (4 samples, 3.03%)tok..tokio::runtime::task::core::CoreStage<T>::poll (4 samples, 3.03%)tok..tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (4 samples, 3.03%)tok..tokio::runtime::task::core::CoreStage<T>::poll::{{closure}} (4 samples, 3.03%)tok..<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (4 samples, 3.03%)<to..tokio::runtime::thread_pool::worker::Launch::launch::{{closure}} (4 samples, 3.03%)tok..tokio::runtime::thread_pool::worker::run (4 samples, 3.03%)tok..tokio::macros::scoped_tls::ScopedKey<T>::set (4 samples, 3.03%)tok..tokio::runtime::thread_pool::worker::run::{{closure}} (4 samples, 3.03%)tok..tokio::runtime::thread_pool::worker::Context::run (4 samples, 3.03%)tok..tokio::runtime::thread_pool::worker::Context::park (4 samples, 3.03%)tok..tokio::runtime::thread_pool::worker::Context::park_timeout (4 samples, 3.03%)tok..<tokio::runtime::thread_pool::park::Parker as tokio::park::Park>::park (4 samples, 3.03%)<to..tokio::runtime::thread_pool::park::Inner::park (4 samples, 3.03%)tok..tokio::runtime::thread_pool::park::Inner::park_driver (4 samples, 3.03%)tok..<tokio::runtime::driver::Driver as tokio::park::Park>::park (4 samples, 3.03%)<to..<tokio::park::either::Either<A,B> as tokio::park::Park>::park (4 samples, 3.03%)<to..<tokio::time::driver::Driver<P> as tokio::park::Park>::park (4 samples, 3.03%)<to..tokio::time::driver::Driver<P>::park_internal (4 samples, 3.03%)tok..tokio::time::driver::Driver<P>::park_timeout (4 samples, 3.03%)tok..<tokio::park::either::Either<A,B> as tokio::park::Park>::park_timeout (4 samples, 3.03%)<to..<tokio::process::imp::driver::Driver as tokio::park::Park>::park_timeout (4 samples, 3.03%)<to..<tokio::signal::unix::driver::Driver as tokio::park::Park>::park_timeout (4 samples, 3.03%)<to..tokio::signal::unix::driver::Driver::process (1 samples, 0.76%)tokio::io::driver::registration::Registration::poll_read_ready (1 samples, 0.76%)tokio::io::driver::registration::Registration::poll_ready (1 samples, 0.76%)tokio::coop::poll_proceed (1 samples, 0.76%)std::thread::local::LocalKey<T>::with (1 samples, 0.76%)std::thread::local::LocalKey<T>::try_with (1 samples, 0.76%)all (132 samples, 100%)tokio-runtime-w (74 samples, 56.06%)tokio-runtime-wtokio::runtime::task::harness::Harness<T,S>::poll_inner (1 samples, 0.76%)tokio::runtime::task::harness::poll_future (1 samples, 0.76%)std::panic::catch_unwind (1 samples, 0.76%)std::panicking::try (1 samples, 0.76%)__rust_try (1 samples, 0.76%)std::panicking::try::do_call (1 samples, 0.76%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce< (1 samples, 0.76%)tokio::runtime::task::harness::poll_future::{{closure}} (1 samples, 0.76%)tokio::runtime::task::core::CoreStage<T>::poll (1 samples, 0.76%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (1 samples, 0.76%)tokio::runtime::task::core::CoreStage<T>::poll::{{closure}} (1 samples, 0.76%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (1 samples, 0.76%)tokio::runtime::thread_pool::worker::Launch::launch::{{closure}} (1 samples, 0.76%)tokio::runtime::thread_pool::worker::run (1 samples, 0.76%)tokio::macros::scoped_tls::ScopedKey<T>::set (1 samples, 0.76%)tokio::runtime::thread_pool::worker::run::{{closure}} (1 samples, 0.76%)tokio::runtime::thread_pool::worker::Context::run (1 samples, 0.76%)tokio::runtime::thread_pool::worker::Context::park (1 samples, 0.76%)tokio::runtime::thread_pool::worker::Context::park_timeout (1 samples, 0.76%)<tokio::runtime::thread_pool::park::Parker as tokio::park::Park>::park (1 samples, 0.76%)tokio::runtime::thread_pool::park::Inner::park (1 samples, 0.76%)tokio::runtime::thread_pool::park::Inner::park_driver (1 samples, 0.76%)<tokio::runtime::driver::Driver as tokio::park::Park>::park (1 samples, 0.76%)<tokio::park::either::Either<A,B> as tokio::park::Park>::park (1 samples, 0.76%)<tokio::time::driver::Driver<P> as tokio::park::Park>::park (1 samples, 0.76%)tokio::time::driver::Driver<P>::park_internal (1 samples, 0.76%)<tokio::park::either::Either<A,B> as tokio::park::Park>::park (1 samples, 0.76%)<tokio::process::imp::driver::Driver as tokio::park::Park>::park (1 samples, 0.76%)<tokio::signal::unix::driver::Driver as tokio::park::Park>::park (1 samples, 0.76%)<tokio::io::driver::Driver as tokio::park::Park>::park (1 samples, 0.76%)tokio::io::driver::Driver::turn (1 samples, 0.76%)tokio::io::driver::Driver::dispatch (1 samples, 0.76%)tokio::io::driver::scheduled_io::ScheduledIo::wake (1 samples, 0.76%)tokio::io::driver::scheduled_io::ScheduledIo::wake0 (1 samples, 0.76%)tokio::util::wake_list::WakeList::new (1 samples, 0.76%)core::mem::maybe_uninit::MaybeUninit<T>::assume_init (1 samples, 0.76%)core::mem::manually_drop::ManuallyDrop<T>::into_inner (1 samples, 0.76%)__memcpy_avx_unaligned_erms (1 samples, 0.76%) \ No newline at end of file diff --git a/roles/mining-proxy/perf.data b/roles/mining-proxy/perf.data deleted file mode 100644 index 149ca1256e..0000000000 Binary files a/roles/mining-proxy/perf.data and /dev/null differ diff --git a/roles/mining-proxy/perf.data.old b/roles/mining-proxy/perf.data.old deleted file mode 100644 index 714684477d..0000000000 Binary files a/roles/mining-proxy/perf.data.old and /dev/null differ diff --git a/roles/mining-proxy/src/args.rs b/roles/mining-proxy/src/args.rs deleted file mode 100644 index 405aeabff8..0000000000 --- a/roles/mining-proxy/src/args.rs +++ /dev/null @@ -1,52 +0,0 @@ -//! CLI argument parsing for the Mining Proxy binary. -//! -//! Defines the `Args` struct and a function to process CLI arguments into a MiningProxyConfig. - -use clap::Parser; -use ext_config::{Config, File, FileFormat}; -use mining_proxy_sv2::{error::Error, MiningProxyConfig}; -use std::path::PathBuf; -use tracing::error; - -/// Holds the parsed CLI arguments for the Mining Proxy binary. -#[derive(Parser, Debug)] -#[command(author, version, about = "Mining Proxy", long_about = None)] -pub struct Args { - #[arg( - short = 'c', - long = "config", - help = "Path to the TOML configuration file", - default_value = "proxy-config.toml" - )] - pub config_path: PathBuf, -} - -/// Process CLI args and load configuration. -#[allow(clippy::result_large_err)] -pub fn process_cli_args() -> Result { - // Parse CLI arguments - let args = Args::parse(); - - // Build configuration from the provided file path - let config_path = args.config_path.to_str().ok_or_else(|| { - error!("Invalid configuration path."); - Error::BadCliArgs - })?; - - let settings = Config::builder() - .add_source(File::new(config_path, FileFormat::Toml)) - .build() - .map_err(|e| { - error!("Failed to build config: {}", e); - Error::BadCliArgs - })?; - - // Deserialize settings into MiningProxyConfig - let config = settings - .try_deserialize::() - .map_err(|e| { - error!("Failed to deserialize config: {}", e); - Error::BadCliArgs - })?; - Ok(config) -} diff --git a/roles/mining-proxy/src/lib/downstream_mining.rs b/roles/mining-proxy/src/lib/downstream_mining.rs deleted file mode 100644 index a71c137678..0000000000 --- a/roles/mining-proxy/src/lib/downstream_mining.rs +++ /dev/null @@ -1,526 +0,0 @@ -use std::{convert::TryInto, sync::Arc}; - -use async_channel::{Receiver, SendError, Sender}; -use tokio::{net::TcpListener, sync::oneshot::Receiver as TokioReceiver}; -use tracing::{debug, info, trace, warn}; - -use super::{ - routing_logic::{CommonRouter, CommonRoutingLogic, MiningRouter, MiningRoutingLogic}, - upstream_mining::{StdFrame as UpstreamFrame, UpstreamMiningNode}, -}; -use codec_sv2::{StandardEitherFrame, StandardSv2Frame}; -use network_helpers_sv2::plain_connection::PlainConnection; -use roles_logic_sv2::{ - common_messages_sv2::{SetupConnection, SetupConnectionSuccess}, - common_properties::{CommonDownstreamData, IsDownstream, IsMiningDownstream}, - errors::Error, - handlers::{ - common::{ParseCommonMessagesFromDownstream, SendTo as SendToCommon}, - mining::{ParseMiningMessagesFromDownstream, SendTo, SupportedChannelTypes}, - }, - mining_sv2::*, - parsers::{AnyMessage, Mining, MiningDeviceMessages}, - utils::Mutex, -}; - -pub type Message = MiningDeviceMessages<'static>; -pub type StdFrame = StandardSv2Frame; -pub type EitherFrame = StandardEitherFrame; - -/// 1 to 1 connection with a downstream node that implement the mining (sub)protocol can be either -/// a mining device or a downstream proxy. -/// A downstream can only be linked with an upstream at a time. Support multi upstreams for -/// downstream do not make much sense. -#[derive(Debug, Clone)] -pub struct DownstreamMiningNode { - id: u32, - receiver: Receiver, - sender: Sender, - pub status: DownstreamMiningNodeStatus, - upstream: Option>>, -} - -#[derive(Debug, Clone)] -pub enum DownstreamMiningNodeStatus { - Initializing, - Paired(CommonDownstreamData), - ChannelOpened(Channel), -} - -#[derive(Debug, Clone)] -#[allow(clippy::enum_variant_names)] -pub enum Channel { - DownstreamHomUpstreamGroup { - data: CommonDownstreamData, - channel_id: u32, - group_id: u32, - }, - DownstreamHomUpstreamExtended { - data: CommonDownstreamData, - channel_id: u32, - }, -} - -impl DownstreamMiningNodeStatus { - fn is_paired(&self) -> bool { - match self { - DownstreamMiningNodeStatus::Initializing => false, - DownstreamMiningNodeStatus::Paired(_) => true, - DownstreamMiningNodeStatus::ChannelOpened(_) => true, - } - } - - fn pair(&mut self, data: CommonDownstreamData) { - match self { - DownstreamMiningNodeStatus::Initializing => { - let self_ = Self::Paired(data); - let _ = std::mem::replace(self, self_); - } - _ => panic!("Try to pair an already paired downstream"), - } - } - - pub fn get_channel(&mut self) -> &mut Channel { - match self { - DownstreamMiningNodeStatus::Initializing => { - panic!("Downstream is not initialized no channle opened yet") - } - DownstreamMiningNodeStatus::Paired(_channels) => { - panic!("Downstream is paired but not channle opened yet") - } - DownstreamMiningNodeStatus::ChannelOpened(k) => k, - } - } - - fn open_channel_for_down_hom_up_group(&mut self, channel_id: u32, group_id: u32) { - match self { - DownstreamMiningNodeStatus::Initializing => panic!(), - DownstreamMiningNodeStatus::Paired(data) => { - let channel = Channel::DownstreamHomUpstreamGroup { - data: *data, - channel_id, - group_id, - }; - let self_ = Self::ChannelOpened(channel); - let _ = std::mem::replace(self, self_); - } - DownstreamMiningNodeStatus::ChannelOpened(..) => panic!("Channel already opened"), - } - } - - fn open_channel_for_down_hom_up_extended(&mut self, channel_id: u32, _group_id: u32) { - match self { - DownstreamMiningNodeStatus::Initializing => panic!(), - DownstreamMiningNodeStatus::Paired(data) => { - let channel = Channel::DownstreamHomUpstreamExtended { - data: *data, - channel_id, - }; - let self_ = Self::ChannelOpened(channel); - let _ = std::mem::replace(self, self_); - } - DownstreamMiningNodeStatus::ChannelOpened(..) => panic!("Channel already opened"), - } - } -} - -impl PartialEq for DownstreamMiningNode { - fn eq(&self, other: &Self) -> bool { - self.id == other.id - } -} - -impl DownstreamMiningNode { - /// Return mining channel specific data - pub fn get_channel(&mut self) -> &mut Channel { - self.status.get_channel() - } - - pub fn open_channel_for_down_hom_up_group(&mut self, channel_id: u32, group_id: u32) { - self.status - .open_channel_for_down_hom_up_group(channel_id, group_id); - } - pub fn open_channel_for_down_hom_up_extended(&mut self, channel_id: u32, group_id: u32) { - self.status - .open_channel_for_down_hom_up_extended(channel_id, group_id); - } - - pub fn new(receiver: Receiver, sender: Sender, id: u32) -> Self { - Self { - receiver, - sender, - status: DownstreamMiningNodeStatus::Initializing, - upstream: None, - id, - } - } - - /// Send SetupConnectionSuccess to donwstream and start processing new messages coming from - /// downstream - pub async fn start( - self_mutex: Arc>, - setup_connection_success: SetupConnectionSuccess, - ) { - if self_mutex - .safe_lock(|self_| self_.status.is_paired()) - .unwrap() - { - let setup_connection_success: MiningDeviceMessages = setup_connection_success.into(); - - { - DownstreamMiningNode::send( - self_mutex.clone(), - setup_connection_success.try_into().unwrap(), - ) - .await - .unwrap(); - } - let receiver = self_mutex - .safe_lock(|self_| self_.receiver.clone()) - .unwrap(); - - while let Ok(message) = receiver.recv().await { - let incoming: StdFrame = message.try_into().unwrap(); - Self::next(self_mutex.clone(), incoming).await; - } - Self::exit(self_mutex); - } else { - panic!() - } - } - - /// Parse the received message and relay it to the right upstream - pub async fn next(self_mutex: Arc>, mut incoming: StdFrame) { - let message_type = incoming.get_header().unwrap().msg_type(); - let payload = incoming.payload(); - - let next_message_to_send = ParseMiningMessagesFromDownstream::handle_message_mining( - self_mutex.clone(), - message_type, - payload, - ); - - match next_message_to_send { - Ok(SendTo::RelaySameMessageToRemote(upstream_mutex)) => { - let sv2_frame: codec_sv2::Sv2Frame = - incoming.map(|payload| payload.try_into().unwrap()); - UpstreamMiningNode::send(upstream_mutex.clone(), sv2_frame) - .await - .unwrap(); - } - Ok(SendTo::RelayNewMessageToRemote(upstream_mutex, message)) => { - let message = AnyMessage::Mining(message); - let frame: UpstreamFrame = message.try_into().unwrap(); - UpstreamMiningNode::send(upstream_mutex.clone(), frame) - .await - .unwrap(); - } - Ok(SendTo::Respond(message)) => { - let message = MiningDeviceMessages::Mining(message); - let frame: StdFrame = message.try_into().unwrap(); - DownstreamMiningNode::send(self_mutex.clone(), frame) - .await - .unwrap(); - } - Ok(SendTo::Multiple(sends_to)) => { - for message in sends_to { - match message { - roles_logic_sv2::handlers::SendTo_::Respond(m) => match m { - Mining::NewMiningJob(_) => { - let message = MiningDeviceMessages::Mining(m); - let frame: StdFrame = message.try_into().unwrap(); - DownstreamMiningNode::send(self_mutex.clone(), frame) - .await - .unwrap(); - } - Mining::OpenStandardMiningChannelSuccess(_) => { - let message = MiningDeviceMessages::Mining(m); - let frame: StdFrame = message.try_into().unwrap(); - DownstreamMiningNode::send(self_mutex.clone(), frame) - .await - .unwrap(); - } - Mining::SetNewPrevHash(_) => { - let message = MiningDeviceMessages::Mining(m); - let frame: StdFrame = message.try_into().unwrap(); - DownstreamMiningNode::send(self_mutex.clone(), frame) - .await - .unwrap(); - } - m => panic!("{:?}", m), - }, - m => panic!("{:?}", m), - } - } - } - Ok(SendTo::None(_)) => (), - Ok(_) => panic!(), - Err(_) => todo!(), - } - } - - /// Send a message downstream - pub async fn send( - self_mutex: Arc>, - sv2_frame: StdFrame, - ) -> Result<(), SendError> { - let either_frame = sv2_frame.into(); - let sender = self_mutex.safe_lock(|self_| self_.sender.clone()).unwrap(); - match sender.send(either_frame).await { - Ok(_) => Ok(()), - Err(_) => { - todo!() - } - } - } - - pub fn exit(self_: Arc>) { - if let Some(up) = self_.safe_lock(|s| s.upstream.clone()).unwrap() { - UpstreamMiningNode::remove_dowstream(up, &self_); - }; - self_ - .safe_lock(|s| { - s.receiver.close(); - }) - .unwrap(); - } -} - -/// It impl UpstreamMining cause the proxy act as an upstream node for the DownstreamMiningNode -impl ParseMiningMessagesFromDownstream for DownstreamMiningNode { - fn get_channel_type(&self) -> SupportedChannelTypes { - SupportedChannelTypes::Group - } - - fn is_work_selection_enabled(&self) -> bool { - false - } - - fn is_downstream_authorized( - _self_mutex: Arc>, - _user_identity: &binary_sv2::Str0255, - ) -> Result { - Ok(true) - } - - fn handle_open_standard_mining_channel( - &mut self, - req: OpenStandardMiningChannel, - ) -> Result, Error> { - info!( - "Received OpenStandardMiningChannel from: {} with id: {}", - std::str::from_utf8(req.user_identity.as_ref()).unwrap_or("Unknown identity"), - req.get_request_id_as_u32() - ); - debug!("OpenStandardMiningChannel: {:?}", req); - let downstream_mining_data = self.get_downstream_mining_data(); - let routing_logic = super::get_routing_logic(); - - let upstream = match routing_logic { - MiningRoutingLogic::Proxy(r_logic) => { - trace!("On OpenStandardMiningChannel r_logic is: {:?}", r_logic); - let up = r_logic - .safe_lock(|r_logic| { - r_logic.on_open_standard_channel( - Arc::new(Mutex::new(self.clone())), - &mut req.clone(), - &downstream_mining_data, - ) - })?; - trace!("On OpenStandardMiningChannel best candidate is: {:?}", up); - Some(up?) - } - // Variant just used for phantom data is ok to panic - MiningRoutingLogic::_P(_) => panic!("Must use either MiningRoutingLogic::None or MiningRoutingLogic::Proxy for `routing_logic` param"), - _ => unreachable!() - }; - - let channel_id = upstream - .as_ref() - .expect("No upstream initialized") - .safe_lock(|s| s.channel_ids.safe_lock(|r| r.next()).unwrap()) - .unwrap(); - let cloned = upstream.as_ref().expect("No upstream initialized").clone(); - - upstream - .as_ref() - .expect("No upstream initialized") - .safe_lock(|up| { - if up.channel_kind.is_extended() { - let messages = up.open_standard_channel_down( - req.request_id.as_u32(), - req.nominal_hash_rate, - true, - channel_id, - ); - for m in &messages { - if let Mining::OpenStandardMiningChannelSuccess(m) = m { - self.open_channel_for_down_hom_up_extended( - m.channel_id, - m.group_channel_id, - ); - } - } - let messages = messages.into_iter().map(SendTo::Respond).collect(); - Ok(SendTo::Multiple(messages)) - } else { - Ok(SendTo::RelaySameMessageToRemote(cloned)) - } - }) - .unwrap() - } - - fn handle_open_extended_mining_channel( - &mut self, - _: OpenExtendedMiningChannel, - ) -> Result, Error> { - todo!() - } - - fn handle_update_channel( - &mut self, - _: UpdateChannel, - ) -> Result, Error> { - todo!() - } - - fn handle_submit_shares_standard( - &mut self, - m: SubmitSharesStandard, - ) -> Result, Error> { - info!("Received SubmitSharesStandard"); - debug!("SubmitSharesStandard {:?}", m); - // TODO maybe we want to check if shares meet target before - // sending them upstream If that is the case it should be - // done by GroupChannel not here - match &self.status { - DownstreamMiningNodeStatus::Initializing => todo!(), - DownstreamMiningNodeStatus::Paired(_) => todo!(), - DownstreamMiningNodeStatus::ChannelOpened(Channel::DownstreamHomUpstreamGroup { - .. - }) => { - let remote = self.upstream.as_ref().unwrap(); - let message = Mining::SubmitSharesStandard(m); - Ok(SendTo::RelayNewMessageToRemote(remote.clone(), message)) - } - DownstreamMiningNodeStatus::ChannelOpened(Channel::DownstreamHomUpstreamExtended { - .. - }) => { - // Safe unwrap is channel have been opened it means that the dowsntream is paired - // with an upstream - let remote = self.upstream.as_ref().unwrap(); - let res = UpstreamMiningNode::handle_std_shr(remote.clone(), m).unwrap(); - Ok(SendTo::Respond(res)) - } - } - } - - fn handle_submit_shares_extended( - &mut self, - _: SubmitSharesExtended, - ) -> Result, Error> { - todo!() - } - - fn handle_set_custom_mining_job( - &mut self, - _: SetCustomMiningJob, - ) -> Result, Error> { - todo!() - } -} - -impl ParseCommonMessagesFromDownstream for DownstreamMiningNode { - fn handle_setup_connection( - &mut self, - m: SetupConnection, - ) -> Result { - info!( - "Received `SetupConnection`: version={}, flags={:b}", - m.min_version, m.flags - ); - let routing_logic = super::get_common_routing_logic(); - match routing_logic { - CommonRoutingLogic::Proxy(r_logic) => { - trace!("On SetupConnection r_logic is {:?}", r_logic); - let result = r_logic.safe_lock(|r_logic| r_logic.on_setup_connection(&m))?; - let (data, message) = result?; - let upstream = match super::get_routing_logic() { - MiningRoutingLogic::Proxy(proxy_routing) => proxy_routing - .safe_lock(|r| r.downstream_to_upstream_map.get(&data).unwrap()[0].clone()) - .unwrap(), - _ => unreachable!(), - }; - self.upstream = Some(upstream); - - self.status.pair(data); - Ok(SendToCommon::RelayNewMessageToRemote( - Arc::new(Mutex::new(())), - message.into(), - )) - } - _ => unreachable!(), - } - } -} - -pub async fn listen_for_downstream_mining( - listener: TcpListener, - mut shutdown_rx: TokioReceiver<()>, -) { - let mut ids = roles_logic_sv2::utils::Id::new(); - loop { - tokio::select! { - accept_result = listener.accept() => { - let (stream, _) = accept_result.expect("failed to accept downstream connection"); - let (receiver, sender): (Receiver, Sender) = - PlainConnection::new(stream).await; - let node = DownstreamMiningNode::new(receiver, sender, ids.next()); - - let mut incoming: StdFrame = - node.receiver.recv().await.unwrap().try_into().unwrap(); - let message_type = incoming.get_header().unwrap().msg_type(); - let payload = incoming.payload(); - let node = Arc::new(Mutex::new(node)); - - // Call handle_setup_connection or fail - let common_msg = DownstreamMiningNode::handle_message_common( - node.clone(), - message_type, - payload, - ).expect("failed to process downstream message"); - - - if let SendToCommon::RelayNewMessageToRemote(_, relay_msg) = common_msg { - if let roles_logic_sv2::parsers::CommonMessages::SetupConnectionSuccess(setup_msg) = relay_msg { - DownstreamMiningNode::start(node, setup_msg).await; - } - } else { - warn!("Received unexpected message from downstream"); - } - } - _ = &mut shutdown_rx => { - info!("Closing listener"); - return; - } - } - } -} - -impl IsDownstream for DownstreamMiningNode { - fn get_downstream_mining_data(&self) -> CommonDownstreamData { - match self.status { - DownstreamMiningNodeStatus::Initializing => panic!(), - DownstreamMiningNodeStatus::Paired(data) => data, - DownstreamMiningNodeStatus::ChannelOpened(Channel::DownstreamHomUpstreamGroup { - data, - .. - }) => data, - DownstreamMiningNodeStatus::ChannelOpened(Channel::DownstreamHomUpstreamExtended { - data, - .. - }) => data, - } - } -} -impl IsMiningDownstream for DownstreamMiningNode {} diff --git a/roles/mining-proxy/src/lib/error.rs b/roles/mining-proxy/src/lib/error.rs deleted file mode 100644 index b28e60c001..0000000000 --- a/roles/mining-proxy/src/lib/error.rs +++ /dev/null @@ -1,36 +0,0 @@ -use async_channel::SendError; -use codec_sv2::StandardEitherFrame; -use core::fmt; -use roles_logic_sv2::parsers::AnyMessage; -use std::net::SocketAddr; - -pub type Message = AnyMessage<'static>; -pub type EitherFrame = StandardEitherFrame; - -#[derive(Debug)] -#[allow(clippy::large_enum_variant)] -#[allow(clippy::enum_variant_names)] -#[allow(dead_code)] -pub enum Error { - SendError(SendError), - UpstreamNotAvailabe(SocketAddr), - SetupConnectionError(String), - BadCliArgs, -} - -impl From> for Error { - fn from(error: SendError) -> Self { - Error::SendError(error) - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Error::SendError(e) => write!(f, "Send error: {}", e), - Error::UpstreamNotAvailabe(addr) => write!(f, "Upstream not available: {}", addr), - Error::SetupConnectionError(msg) => write!(f, "Setup connection error: {}", msg), - Error::BadCliArgs => write!(f, "Bad CLI arguments provided"), - } - } -} diff --git a/roles/mining-proxy/src/lib/mod.rs b/roles/mining-proxy/src/lib/mod.rs deleted file mode 100644 index 769b7ae759..0000000000 --- a/roles/mining-proxy/src/lib/mod.rs +++ /dev/null @@ -1,191 +0,0 @@ -pub mod downstream_mining; -pub mod error; -pub mod routing_logic; -pub mod selectors; -pub mod upstream_mining; - -use once_cell::sync::OnceCell; -use roles_logic_sv2::utils::{GroupId, Id, Mutex}; -use routing_logic::{CommonRoutingLogic, MiningProxyRoutingLogic, MiningRoutingLogic}; -use selectors::GeneralMiningSelector; -use serde::Deserialize; -use std::{net::SocketAddr, sync::Arc}; -use tokio::{net::TcpListener, sync::oneshot}; -use tracing::info; -use upstream_mining::UpstreamMiningNode; - -type RLogic = MiningProxyRoutingLogic< - downstream_mining::DownstreamMiningNode, - upstream_mining::UpstreamMiningNode, - upstream_mining::ProxyRemoteSelector, ->; - -/// Panic whene we are looking one of this 2 global mutex would force the proxy to go down as every -/// part of the program depend on them. -/// SAFTEY note: we use global mutable memory instead of a dedicated struct that use a dedicated -/// task to change the mutable state and communicate with the other parts of the program via -/// messages cause it is impossible for a task to panic while is using one of the two below Mutex. -/// So it make sense to use shared mutable memory to lower the complexity of the codebase and to -/// have some performance gain. -pub static ROUTING_LOGIC: OnceCell> = OnceCell::new(); -static MIN_EXTRANONCE_SIZE: u16 = 6; -static EXTRANONCE_RANGE_1_LENGTH: usize = 4; - -pub async fn initialize_upstreams(min_version: u16, max_version: u16) { - let upstreams = ROUTING_LOGIC - .get() - .expect("BUG: ROUTING_LOGIC has not been set yet") - .safe_lock(|r_logic| r_logic.upstream_selector.upstreams.clone()) - .unwrap(); - let available_upstreams = upstream_mining::scan(upstreams, min_version, max_version).await; - ROUTING_LOGIC - .get() - .unwrap() - .safe_lock(|rl| rl.upstream_selector.update_upstreams(available_upstreams)) - .unwrap(); -} - -fn remove_upstream(id: u32) { - let upstreams = ROUTING_LOGIC - .get() - .expect("BUG: ROUTING_LOGIC has not been set yet") - .safe_lock(|r_logic| r_logic.upstream_selector.upstreams.clone()) - .unwrap(); - let mut updated_upstreams = vec![]; - for upstream in upstreams { - if upstream.safe_lock(|s| s.get_id()).unwrap() != id { - updated_upstreams.push(upstream) - } - } - ROUTING_LOGIC - .get() - .unwrap() - .safe_lock(|rl| rl.upstream_selector.update_upstreams(updated_upstreams)) - .unwrap(); -} - -pub fn get_routing_logic() -> MiningRoutingLogic< - downstream_mining::DownstreamMiningNode, - upstream_mining::UpstreamMiningNode, - upstream_mining::ProxyRemoteSelector, - RLogic, -> { - MiningRoutingLogic::Proxy( - ROUTING_LOGIC - .get() - .expect("BUG: ROUTING_LOGIC was not set yet"), - ) -} -pub fn get_common_routing_logic() -> CommonRoutingLogic { - CommonRoutingLogic::Proxy( - ROUTING_LOGIC - .get() - .expect("BUG: ROUTING_LOGIC was not set yet"), - ) -} - -#[derive(Debug, Deserialize, Clone)] -pub struct UpstreamMiningValues { - pub address: String, - pub port: u16, - pub pub_key: key_utils::Secp256k1PublicKey, - pub channel_kind: ChannelKind, -} - -#[derive(Debug, Deserialize, Clone, Copy)] -pub enum ChannelKind { - Group, - Extended, -} - -#[derive(Debug, Deserialize, Clone)] -pub struct MiningProxyConfig { - pub upstreams: Vec, - pub listen_address: String, - pub listen_mining_port: u16, - pub max_supported_version: u16, - pub min_supported_version: u16, - pub downstream_share_per_minute: f32, - pub expected_total_downstream_hr: f32, - pub reconnect: bool, -} -pub async fn initialize_r_logic( - upstreams: &[UpstreamMiningValues], - group_id: Arc>, - config: MiningProxyConfig, -) -> RLogic { - let channel_ids = Arc::new(Mutex::new(Id::new())); - let mut upstream_mining_nodes = Vec::with_capacity(upstreams.len()); - for (index, upstream_) in upstreams.iter().enumerate() { - let socket = SocketAddr::new(upstream_.address.parse().unwrap(), upstream_.port); - - let upstream = Arc::new(Mutex::new(UpstreamMiningNode::new( - index as u32, - socket, - upstream_.pub_key.into_bytes(), - upstream_.channel_kind, - group_id.clone(), - channel_ids.clone(), - config.downstream_share_per_minute, - None, - None, - config.expected_total_downstream_hr, - config.reconnect, - ))); - - match upstream_.channel_kind { - ChannelKind::Group => (), - ChannelKind::Extended => (), - } - - upstream_mining_nodes.push(upstream); - } - let upstream_selector = GeneralMiningSelector::new(upstream_mining_nodes); - MiningProxyRoutingLogic { - upstream_selector, - downstream_id_generator: Id::new(), - downstream_to_upstream_map: std::collections::HashMap::new(), - } -} - -pub async fn start_mining_proxy(config: MiningProxyConfig) { - let group_id = Arc::new(Mutex::new(GroupId::new())); - ROUTING_LOGIC - .set(Mutex::new( - initialize_r_logic(&config.upstreams, group_id, config.clone()).await, - )) - .expect("BUG: Failed to set ROUTING_LOGIC"); - - info!("Initializing upstream scanner"); - initialize_upstreams(config.min_supported_version, config.max_supported_version).await; - info!("Initializing downstream listener"); - - let socket = SocketAddr::new( - config.listen_address.parse().unwrap(), - config.listen_mining_port, - ); - let listener = TcpListener::bind(socket).await.unwrap(); - - info!("Listening for downstream mining connections on {}", socket); - - let (shutdown_tx, shutdown_rx) = oneshot::channel(); - - let (_, res) = tokio::join!( - // Wait for downstream connection - downstream_mining::listen_for_downstream_mining(listener, shutdown_rx), - // handle SIGTERM/QUIT / ctrl+c - tokio::spawn(async { - tokio::signal::ctrl_c() - .await - .expect("Failed to listen to signals"); - let _ = shutdown_tx.send(()); - info!("Interrupt received"); - }) - ); - - if let Err(e) = res { - panic!("Failed to wait for clean exit: {:?}", e); - } - - info!("Shutdown done"); -} diff --git a/roles/mining-proxy/src/lib/routing_logic.rs b/roles/mining-proxy/src/lib/routing_logic.rs deleted file mode 100644 index 2d42dc81af..0000000000 --- a/roles/mining-proxy/src/lib/routing_logic.rs +++ /dev/null @@ -1,422 +0,0 @@ -//! # Routing Logic -//! -//! This module contains the routing logic used by handlers to determine where a message should be -//! relayed or responded to. -//! -//! The routing logic defines a set of traits and structures to manage message routing in Stratum -//! V2. The following components are included: -//! -//! - **`CommonRouter`**: Trait implemented by routers for the common (sub)protocol. -//! - **`MiningRouter`**: Trait implemented by routers for the mining (sub)protocol. -//! - **`CommonRoutingLogic`**: Enum defining the various routing logic for the common protocol -//! (e.g., Proxy, None). -//! - **`MiningRoutingLogic`**: Enum defining the routing logic for the mining protocol (e.g., -//! Proxy, None). -//! - **`NoRouting`**: Marker router that implements both `CommonRouter` and `MiningRouter` for -//! cases where no routing logic is required. -//! - **`MiningProxyRoutingLogic`**: Routing logic valid for a standard Sv2 mining proxy, -//! implementing both `CommonRouter` and `MiningRouter`. -//! -//! ## Future Work -//! -//! - Consider hiding all traits from the public API and exporting only marker traits. -//! - Improve upstream selection logic to be configurable by the caller. - -use super::{ - downstream_mining::DownstreamMiningNode, - selectors::{ - DownstreamMiningSelector, GeneralMiningSelector, NullDownstreamMiningSelector, - UpstreamMiningSelctor, - }, - upstream_mining::HasDownstreamSelector, -}; -use roles_logic_sv2::{ - common_messages_sv2::{ - has_requires_std_job, Protocol, SetupConnection, SetupConnectionSuccess, - }, - common_properties::{ - CommonDownstreamData, IsDownstream, IsMiningDownstream, IsMiningUpstream, IsUpstream, - PairSettings, - }, - mining_sv2::{OpenStandardMiningChannel, OpenStandardMiningChannelSuccess}, - utils::{Id, Mutex}, - Error, -}; -use std::{collections::HashMap, fmt::Debug as D, marker::PhantomData, sync::Arc}; - -/// Defines routing logic for common protocol messages. -/// -/// Implemented by handlers (such as -/// [`roles_logic_sv2::handlers::common::ParseCommonMessagesFromUpstream`] -/// and [`roles_logic_sv2::handlers::common::ParseCommonMessagesFromDownstream`]) to determine the -/// behavior for common protocol routing. -pub trait CommonRouter: std::fmt::Debug { - /// Handles a `SetupConnection` message for the common protocol. - fn on_setup_connection( - &mut self, - message: &SetupConnection, - ) -> Result<(CommonDownstreamData, SetupConnectionSuccess), Error>; -} - -/// Defines routing logic for mining protocol messages. -/// -/// Implemented by handlers (such as -/// [`roles_logic_sv2::handlers::mining::ParseMiningMessagesFromUpstream`] -/// and [`roles_logic_sv2::handlers::mining::ParseMiningMessagesFromDownstream`]) to determine the -/// behavior for mining protocol routing. This trait extends [`CommonRouter`] to handle -/// mining-specific routing logic. -pub trait MiningRouter< - Down: IsMiningDownstream, - Up: IsMiningUpstream + HasDownstreamSelector, - Sel: DownstreamMiningSelector, ->: CommonRouter -{ - /// Handles an `OpenStandardMiningChannel` message from a downstream. - fn on_open_standard_channel( - &mut self, - downstream: Arc>, - request: &mut OpenStandardMiningChannel, - downstream_mining_data: &CommonDownstreamData, - ) -> Result>, Error>; - - /// Handles an `OpenStandardMiningChannelSuccess` message from an upstream. - fn on_open_standard_channel_success( - &mut self, - upstream: &mut Up, - request: &mut OpenStandardMiningChannelSuccess, - ) -> Result>, Error>; -} - -/// A no-operation router for scenarios where no routing logic is needed. -/// -/// Implements both `CommonRouter` and `MiningRouter` but panics if invoked. -#[derive(Debug)] -pub struct NoRouting(); - -impl CommonRouter for NoRouting { - fn on_setup_connection( - &mut self, - _: &SetupConnection, - ) -> Result<(CommonDownstreamData, SetupConnectionSuccess), Error> { - unreachable!() - } -} - -impl - MiningRouter for NoRouting -{ - fn on_open_standard_channel( - &mut self, - _downstream: Arc>, - _request: &mut OpenStandardMiningChannel, - _downstream_mining_data: &CommonDownstreamData, - ) -> Result>, Error> { - unreachable!() - } - - fn on_open_standard_channel_success( - &mut self, - _upstream: &mut Up, - _request: &mut OpenStandardMiningChannelSuccess, - ) -> Result>, Error> { - unreachable!() - } -} - -/// Routing logic options for the common protocol. -#[derive(Debug)] -pub enum CommonRoutingLogic { - /// Proxy routing logic for the common protocol. - Proxy(&'static Mutex), - /// No routing logic. - None, -} - -/// Routing logic options for the mining protocol. -#[derive(Debug)] -pub enum MiningRoutingLogic< - Down: IsMiningDownstream + D, - Up: IsMiningUpstream + D + HasDownstreamSelector, - Sel: DownstreamMiningSelector + D, - Router: 'static + MiningRouter, -> { - /// Proxy routing logic for the mining protocol. - Proxy(&'static Mutex), - /// No routing logic. - None, - /// Marker for the generic parameters. - _P(PhantomData<(Down, Up, Sel)>), -} - -impl Clone for CommonRoutingLogic { - fn clone(&self) -> Self { - match self { - Self::None => Self::None, - Self::Proxy(x) => Self::Proxy(x), - } - } -} - -impl< - Down: IsMiningDownstream + D, - Up: IsMiningUpstream + D + HasDownstreamSelector, - Sel: DownstreamMiningSelector + D, - Router: MiningRouter, - > Clone for MiningRoutingLogic -{ - fn clone(&self) -> Self { - match self { - Self::None => Self::None, - Self::Proxy(x) => Self::Proxy(x), - // Variant used only for PhantomData safe to panic here - Self::_P(_) => panic!(), - } - } -} - -/// Routing logic for a standard Sv2 mining proxy. -#[derive(Debug)] -pub struct MiningProxyRoutingLogic< - Down: IsMiningDownstream + D, - Up: IsMiningUpstream + D, - Sel: DownstreamMiningSelector + D, -> { - /// Selector for upstream entities. - pub upstream_selector: GeneralMiningSelector, - /// ID generator for downstream entities. - pub downstream_id_generator: Id, - /// Mapping from downstream to upstream entities. - pub downstream_to_upstream_map: HashMap>>>, -} - -impl< - Down: IsMiningDownstream + D, - Up: IsMiningUpstream + D + HasDownstreamSelector, - Sel: DownstreamMiningSelector + D, - > CommonRouter for MiningProxyRoutingLogic -{ - /// Handles the `SetupConnection` message. - /// - /// This method initializes the connection between a downstream and an upstream by determining - /// the appropriate upstream based on the provided protocol, versions, and flags. - fn on_setup_connection( - &mut self, - message: &SetupConnection, - ) -> Result<(CommonDownstreamData, SetupConnectionSuccess), Error> { - let protocol = message.protocol; - let min_v = message.min_version; - let max_v = message.max_version; - let flags = message.flags; - let pair_settings = PairSettings { - protocol, - min_v, - max_v, - flags, - }; - let header_only = has_requires_std_job(pair_settings.flags); - match (protocol, header_only) { - (Protocol::MiningProtocol, true) => { - self.on_setup_connection_mining_header_only(&pair_settings) - } - // TODO: Add handler for other protocols. - _ => Err(Error::UnimplementedProtocol), - } - } -} - -impl< - Up: IsMiningUpstream + D + HasDownstreamSelector, - Sel: DownstreamMiningSelector + D, - > MiningRouter - for MiningProxyRoutingLogic -{ - // Handles the `OpenStandardMiningChannel` message. - // - // This method processes the request to open a standard mining channel. It selects a suitable - // upstream, updates the request ID to ensure uniqueness, and then delegates to - // `on_open_standard_channel_request_header_only` to finalize the process. - fn on_open_standard_channel( - &mut self, - downstream: Arc>, - request: &mut OpenStandardMiningChannel, - downstream_mining_data: &CommonDownstreamData, - ) -> Result>, Error> { - let upstreams = self - .downstream_to_upstream_map - .get(downstream_mining_data) - .ok_or(Error::NoCompatibleUpstream(*downstream_mining_data))?; - // If we are here, a list of possible upstreams has already been selected. - // TODO: The upstream selection logic should be specified by the caller. - let upstream = - Self::select_upstreams(&mut upstreams.to_vec()).ok_or(Error::NoUpstreamsConnected)?; - let old_id = request.get_request_id_as_u32(); - let new_req_id = upstream.safe_lock(|u| u.get_mapper().unwrap().on_open_channel(old_id))?; - request.update_id(new_req_id); - self.on_open_standard_channel_request_header_only(downstream, request) - } - - // Handles the `OpenStandardMiningChannelSuccess` message. - // - // This method processes the success message received from an upstream when a standard mining - // channel is opened. It maps the request ID back to the original ID from the downstream and - // updates the associated group and channel IDs in the upstream. - fn on_open_standard_channel_success( - &mut self, - upstream: &mut Up, - request: &mut OpenStandardMiningChannelSuccess, - ) -> Result>, Error> - where - Up: IsUpstream + HasDownstreamSelector, - { - let upstream_request_id = request.get_request_id_as_u32(); - let original_request_id = upstream - .get_mapper() - .ok_or(Error::RequestIdNotMapped(upstream_request_id))? - .remove(upstream_request_id) - .ok_or(Error::RequestIdNotMapped(upstream_request_id)); - - request.update_id(original_request_id?); - - let selector = upstream.get_remote_selector(); - selector.on_open_standard_channel_success( - upstream_request_id, - request.group_channel_id, - request.channel_id, - ) - } -} - -// Selects the upstream with the lowest total hash rate. -// # Panics -// This function panics if the slice is empty, as it is internally guaranteed that this function -// will only be called with non-empty vectors. -fn minor_total_hr_upstream(ups: &mut [Arc>]) -> Arc> -where - Up: IsMiningUpstream + D, -{ - ups.iter_mut() - .reduce(|acc, item| { - // Safely locks and compares the total hash rate of each upstream. - if acc.safe_lock(|x| x.total_hash_rate()).unwrap() - < item.safe_lock(|x| x.total_hash_rate()).unwrap() - { - acc - } else { - item - } - }) - .unwrap() - .clone() // Unwrap is safe because the function only operates on non-empty vectors. -} - -// Filters upstream entities that are not configured for header-only mining. -fn filter_header_only(ups: &mut [Arc>]) -> Vec>> -where - Up: IsMiningUpstream + D, -{ - ups.iter() - .filter(|up_mutex| { - up_mutex - .safe_lock(|up| !up.is_header_only()) - .unwrap_or_default() - }) - .cloned() - .collect() -} - -// Selects the most appropriate upstream entity based on specific criteria. -// -// # Criteria -// - If only one upstream is available, it is selected. -// - If multiple upstreams exist, preference is given to those not configured as header-only. -// - Among the remaining upstreams, the one with the lowest total hash rate is selected. -fn select_upstream(ups: &mut [Arc>]) -> Option>> -where - Up: IsMiningUpstream + D, -{ - if ups.is_empty() { - None - } else if ups.len() == 1 { - Some(ups[0].clone()) - } else if !filter_header_only::(ups).is_empty() { - Some(minor_total_hr_upstream::( - &mut filter_header_only::(ups), - )) - } else { - Some(minor_total_hr_upstream::(ups)) - } -} - -impl< - Down: IsMiningDownstream + D, - Up: IsMiningUpstream + D + HasDownstreamSelector, - Sel: DownstreamMiningSelector + D, - > MiningProxyRoutingLogic -{ - // Selects an upstream entity from a list of available upstreams. - fn select_upstreams(ups: &mut [Arc>]) -> Option>> { - select_upstream::(ups) - } - - /// Handles the `SetupConnection` process for header-only mining downstream's. - /// - /// This method selects compatible upstreams, assigns connection flags, and maps the - /// downstream to the selected upstreams. - pub fn on_setup_connection_mining_header_only( - &mut self, - pair_settings: &PairSettings, - ) -> Result<(CommonDownstreamData, SetupConnectionSuccess), Error> { - let mut upstreams = self.upstream_selector.on_setup_connection(pair_settings)?; - let upstream = - Self::select_upstreams(&mut upstreams.0).ok_or(Error::NoUpstreamsConnected)?; - let downstream_data = CommonDownstreamData { - header_only: true, - work_selection: false, - version_rolling: false, - }; - let message = SetupConnectionSuccess { - used_version: 2, - flags: upstream.safe_lock(|u| u.get_flags())?, - }; - self.downstream_to_upstream_map - .insert(downstream_data, vec![upstream]); - Ok((downstream_data, message)) - } - - /// Handles a standard channel opening request for header-only mining downstreams. - pub fn on_open_standard_channel_request_header_only( - &mut self, - downstream: Arc>, - request: &OpenStandardMiningChannel, - ) -> Result>, Error> { - let downstream_mining_data = downstream.safe_lock(|d| d.get_downstream_mining_data())?; - let upstream = self - .downstream_to_upstream_map - .get(&downstream_mining_data) - .ok_or(Error::NoCompatibleUpstream(downstream_mining_data))?[0] - .clone(); - upstream.safe_lock(|u| { - let selector = u.get_remote_selector(); - selector.on_open_standard_channel_request(request.request_id.as_u32(), downstream) - })?; - Ok(upstream) - } -} - -//pub type NoRoutingLogic = RoutingLogic; -//impl + D> -// NoRoutingLogic { pub fn new() -> Self -// where -// Self: D, -// { -// RoutingLogic::None -// } -//} -// -//impl + D> -// Default for NoRoutingLogic -//{ -// fn default() -> Self { -// Self::new() -// } -//} diff --git a/roles/mining-proxy/src/lib/selectors.rs b/roles/mining-proxy/src/lib/selectors.rs deleted file mode 100644 index 7a69c0fe01..0000000000 --- a/roles/mining-proxy/src/lib/selectors.rs +++ /dev/null @@ -1,346 +0,0 @@ -//! # Selectors and Message Routing -//! -//! This module provides selectors and routing logic for managing downstream and upstream nodes -//! in a mining proxy environment. Selectors help determine the appropriate remote(s) to relay or -//! send messages to. - -use nohash_hasher::BuildNoHashHasher; -use roles_logic_sv2::{ - common_properties::{IsDownstream, IsMiningDownstream, IsMiningUpstream, PairSettings}, - utils::Mutex, - Error, -}; -use std::{collections::HashMap, fmt::Debug as D, sync::Arc}; - -/// Proxy selector for routing messages to downstream mining nodes. -/// -/// Maintains mappings for request IDs, channel IDs, and downstream nodes to facilitate message -/// routing. -#[derive(Debug, Clone, Default)] -pub struct ProxyDownstreamMiningSelector { - // Maps request IDs to their corresponding downstream nodes. - request_id_to_remotes: HashMap>, BuildNoHashHasher>, - - // Maps group channel IDs to a list of downstream nodes. - channel_id_to_downstreams: HashMap>>, BuildNoHashHasher>, - - // Maps standard channel IDs to a single downstream node. - channel_id_to_downstream: HashMap>, BuildNoHashHasher>, -} - -impl ProxyDownstreamMiningSelector { - /// Creates a new [`ProxyDownstreamMiningSelector`] instance. - pub fn new() -> Self { - // `BuildNoHashHasher` is an optimization to bypass the hashing step for integer keys - Self { - request_id_to_remotes: HashMap::with_hasher(BuildNoHashHasher::default()), - channel_id_to_downstreams: HashMap::with_hasher(BuildNoHashHasher::default()), - channel_id_to_downstream: HashMap::with_hasher(BuildNoHashHasher::default()), - } - } - - /// Creates a new [`ProxyDownstreamMiningSelector`] instance wrapped in an `Arc`. - pub fn new_as_mutex() -> Arc> - where - Self: Sized, - { - Arc::new(Mutex::new(Self::new())) - } -} - -impl ProxyDownstreamMiningSelector { - // Removes a specific downstream node from all mappings. - fn _remove_downstream(&mut self, d: &Arc>) { - self.request_id_to_remotes.retain(|_, v| !Arc::ptr_eq(v, d)); - self.channel_id_to_downstream - .retain(|_, v| !Arc::ptr_eq(v, d)); - } -} - -impl DownstreamMiningSelector - for ProxyDownstreamMiningSelector -{ - /// Records a request to open a standard channel with an associated downstream node. - fn on_open_standard_channel_request(&mut self, request_id: u32, downstream: Arc>) { - self.request_id_to_remotes.insert(request_id, downstream); - } - - fn on_open_standard_channel_success( - &mut self, - request_id: u32, - g_channel_id: u32, - channel_id: u32, - ) -> Result>, Error> { - let downstream = self - .request_id_to_remotes - .remove(&request_id) - .ok_or(Error::UnknownRequestId(request_id))?; - self.channel_id_to_downstream - .insert(channel_id, downstream.clone()); - match self.channel_id_to_downstreams.get_mut(&g_channel_id) { - None => { - self.channel_id_to_downstreams - .insert(g_channel_id, vec![downstream.clone()]); - } - Some(x) => x.push(downstream.clone()), - } - Ok(downstream) - } - - // Retrieves all downstream nodes associated with a standard/group channel ID. - fn get_downstreams_in_channel(&self, channel_id: u32) -> Option<&Vec>>> { - self.channel_id_to_downstreams.get(&channel_id) - } - - fn remove_downstreams_in_channel(&mut self, channel_id: u32) -> Vec>> { - let downs = self - .channel_id_to_downstreams - .remove(&channel_id) - .unwrap_or_default(); - for d in &downs { - self._remove_downstream(d); - } - downs - } - - fn remove_downstream(&mut self, d: &Arc>) { - for dws in self.channel_id_to_downstreams.values_mut() { - dws.retain(|node| !Arc::ptr_eq(node, d)); - } - - self._remove_downstream(d); - } - - fn downstream_from_channel_id(&self, channel_id: u32) -> Option>> { - self.channel_id_to_downstream.get(&channel_id).cloned() - } - - fn get_all_downstreams(&self) -> Vec>> { - self.channel_id_to_downstream.values().cloned().collect() - } -} - -impl DownstreamSelector for ProxyDownstreamMiningSelector {} - -/// Specialized trait for selectors managing downstream mining nodes. -/// -/// Logic for an upstream mining node to locate the correct downstream node to which a message -/// should be sent or relayed. -pub trait DownstreamMiningSelector: - DownstreamSelector -{ - /// Handles a downstream node's request to open a standard channel. - fn on_open_standard_channel_request( - &mut self, - request_id: u32, - downstream: Arc>, - ); - - /// Handles the successful opening of a standard channel with a downstream node. Returns an - /// error if the request ID is unknown. - fn on_open_standard_channel_success( - &mut self, - request_id: u32, - g_channel_id: u32, - channel_id: u32, - ) -> Result>, Error>; - - /// Retrieves all downstream nodes associated with a channel ID. - fn get_downstreams_in_channel(&self, channel_id: u32) -> Option<&Vec>>>; - - /// Removes all downstream nodes associated with a channel, returning all removed downstream - /// nodes. - fn remove_downstreams_in_channel(&mut self, channel_id: u32) -> Vec>>; - - /// Removes a specific downstream. - fn remove_downstream(&mut self, d: &Arc>); - - // Retrieves the downstream node associated with a specific standard channel ID. - // - // Only for standard channels. - fn downstream_from_channel_id(&self, channel_id: u32) -> Option>>; - - /// Retrieves all downstream nodes managed by the selector. - fn get_all_downstreams(&self) -> Vec>>; -} - -/// Base trait for selectors managing downstream nodes. -pub trait DownstreamSelector {} - -/// No-op selector for cases where routing logic is unnecessary. -/// -/// Primarily used for testing, it implements all required traits, but panics with an -/// [`unreachable`] if called. -#[derive(Debug, Clone, Copy, Default)] -pub struct NullDownstreamMiningSelector(); - -impl NullDownstreamMiningSelector { - /// Creates a new [`NullDownstreamMiningSelector`] instance. - pub fn new() -> Self { - NullDownstreamMiningSelector() - } - - /// Creates a new [`NullDownstreamMiningSelector`] instance wrapped in an `Arc`. - pub fn new_as_mutex() -> Arc> - where - Self: Sized, - { - Arc::new(Mutex::new(Self::new())) - } -} - -impl DownstreamMiningSelector for NullDownstreamMiningSelector { - /// [`unreachable`] in this no-op implementation. - fn on_open_standard_channel_request( - &mut self, - _request_id: u32, - _downstream: Arc>, - ) { - unreachable!("on_open_standard_channel_request") - } - - /// [`unreachable`] in this no-op implementation. - fn on_open_standard_channel_success( - &mut self, - _request_id: u32, - _channel_id: u32, - _channel_id_2: u32, - ) -> Result>, Error> { - unreachable!("on_open_standard_channel_success") - } - - /// [`unreachable`] in this no-op implementation. - fn get_downstreams_in_channel(&self, _channel_id: u32) -> Option<&Vec>>> { - unreachable!("get_downstreams_in_channel") - } - - /// [`unreachable`] in this no-op implementation. - fn remove_downstreams_in_channel(&mut self, _channel_id: u32) -> Vec>> { - unreachable!("remove_downstreams_in_channel") - } - - /// [`unreachable`] in this no-op implementation. - fn remove_downstream(&mut self, _d: &Arc>) { - unreachable!("remove_downstream") - } - - /// [`unreachable`] in this no-op implementation. - fn downstream_from_channel_id(&self, _channel_id: u32) -> Option>> { - unreachable!("downstream_from_channel_id") - } - - /// [`unreachable`] in this no-op implementation. - fn get_all_downstreams(&self) -> Vec>> { - unreachable!("get_all_downstreams") - } -} - -impl DownstreamSelector for NullDownstreamMiningSelector {} - -/// Base trait for selectors managing upstream nodes. -pub trait UpstreamSelector {} - -/// Specialized trait for selectors managing upstream mining nodes. -/// -/// This trait is implemented by roles with multiple upstream connections, such as proxies or -/// pools. It provides logic to route messages received by the implementing role (e.g., from mining -/// devices or downstream proxies) to the appropriate upstream nodes. -/// -/// For example, a mining proxy with multiple upstream pools would implement this trait to handle -/// upstream connection setup, failover, and routing logic. -pub trait UpstreamMiningSelctor< - Down: IsMiningDownstream, - Up: IsMiningUpstream, - Sel: DownstreamMiningSelector, ->: UpstreamSelector -{ - /// Handles the setup of connections to upstream nodes with the given [`PairSettings`]. - /// - /// Returns an error if the upstream and downstream node(s) are unpairable. - #[allow(clippy::type_complexity)] - fn on_setup_connection( - &mut self, - pair_settings: &PairSettings, - ) -> Result<(Vec>>, u32), Error>; - - /// Retrieves an upstream node by its ID, if exists. - fn get_upstream(&self, upstream_id: u32) -> Option>>; -} - -/// Selector for routing messages to downstream nodes based on pair settings and flags. -/// -/// Tracks upstream nodes and their IDs for efficient lookups. -#[derive(Debug)] -pub struct GeneralMiningSelector< - Sel: DownstreamMiningSelector, - Down: IsMiningDownstream, - Up: IsMiningUpstream, -> { - /// List of upstream nodes. - pub upstreams: Vec>>, - - /// Mapping of upstream IDs to their corresponding upstream nodes. - pub id_to_upstream: HashMap>, BuildNoHashHasher>, - - sel: std::marker::PhantomData, - - down: std::marker::PhantomData, -} - -impl, Up: IsMiningUpstream, Down: IsMiningDownstream> - GeneralMiningSelector -{ - /// Creates a new [`GeneralMiningSelector`] instance with the given upstream nodes. - pub fn new(upstreams: Vec>>) -> Self { - let mut id_to_upstream = HashMap::with_hasher(BuildNoHashHasher::default()); - for up in &upstreams { - id_to_upstream.insert(up.safe_lock(|u| u.get_id()).unwrap(), up.clone()); - } - Self { - upstreams, - id_to_upstream, - sel: std::marker::PhantomData, - down: std::marker::PhantomData, - } - } - - /// Updates the list of upstream nodes. - pub fn update_upstreams(&mut self, upstreams: Vec>>) { - self.upstreams = upstreams; - } -} - -impl, Down: IsMiningDownstream, Up: IsMiningUpstream> - UpstreamSelector for GeneralMiningSelector -{ -} - -impl, Down: IsMiningDownstream, Up: IsMiningUpstream> - UpstreamMiningSelctor for GeneralMiningSelector -{ - fn on_setup_connection( - &mut self, - pair_settings: &PairSettings, - ) -> Result<(Vec>>, u32), Error> { - let mut supported_upstreams = vec![]; - let mut supported_flags: u32 = 0; - for node in &self.upstreams { - let is_pairable = node - .safe_lock(|node| node.is_pairable(pair_settings)) - .unwrap(); - if is_pairable { - supported_flags |= node.safe_lock(|n| n.get_flags()).unwrap(); - supported_upstreams.push(node.clone()); - } - } - if !supported_upstreams.is_empty() { - return Ok((supported_upstreams, supported_flags)); - } - - Err(Error::NoPairableUpstream((2, 2, 0))) - } - - fn get_upstream(&self, upstream_id: u32) -> Option>> { - self.id_to_upstream.get(&upstream_id).cloned() - } -} diff --git a/roles/mining-proxy/src/lib/upstream_mining.rs b/roles/mining-proxy/src/lib/upstream_mining.rs deleted file mode 100644 index b4d44f6c1a..0000000000 --- a/roles/mining-proxy/src/lib/upstream_mining.rs +++ /dev/null @@ -1,1335 +0,0 @@ -#![allow(dead_code)] - -use core::convert::TryInto; -use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration}; - -use async_channel::{Receiver, SendError, Sender}; -use async_recursion::async_recursion; -use nohash_hasher::BuildNoHashHasher; -use tokio::{net::TcpStream, task}; -use tracing::{debug, error, info}; - -use super::{ - downstream_mining::{Channel, DownstreamMiningNode, StdFrame as DownstreamFrame}, - routing_logic::{MiningRouter, MiningRoutingLogic}, - selectors::{DownstreamMiningSelector, ProxyDownstreamMiningSelector as Prs}, - EXTRANONCE_RANGE_1_LENGTH, -}; -use codec_sv2::{HandshakeRole, Initiator, StandardEitherFrame, StandardSv2Frame}; -use network_helpers_sv2::noise_connection::Connection; -use roles_logic_sv2::{ - channel_logic::{ - channel_factory::{ExtendedChannelKind, OnNewShare, ProxyExtendedChannelFactory, Share}, - proxy_group_channel::GroupChannels, - }, - common_messages_sv2::{Protocol, SetupConnection}, - common_properties::{ - IsMiningDownstream, IsMiningUpstream, IsUpstream, RequestIdMapper, UpstreamChannel, - }, - errors::Error, - handlers::mining::{ParseMiningMessagesFromUpstream, SendTo, SupportedChannelTypes}, - job_dispatcher::GroupChannelJobDispatcher, - mining_sv2::*, - parsers::{AnyMessage, CommonMessages, Mining, MiningDeviceMessages}, - template_distribution_sv2::SubmitSolution, - utils::{GroupId, Id, Mutex}, -}; -use stratum_common::bitcoin::TxOut; - -pub type Message = AnyMessage<'static>; -pub type StdFrame = StandardSv2Frame; -pub type EitherFrame = StandardEitherFrame; -pub type ProxyRemoteSelector = Prs; - -#[derive(Debug)] -#[allow(clippy::large_enum_variant)] -pub enum ChannelKind { - Group(GroupChannels), - Extended(Option), -} -impl ChannelKind { - pub fn is_extended(&self) -> bool { - match self { - ChannelKind::Group(_) => false, - ChannelKind::Extended(_) => true, - } - } - - fn is_initialized(&self) -> bool { - !matches!(self, ChannelKind::Extended(None)) - } - - fn get_factory(&mut self) -> &mut ProxyExtendedChannelFactory { - match self { - ChannelKind::Extended(Some(f)) => f, - _ => panic!("Channel factory not available"), - } - } - - fn initialize_factory( - &mut self, - group_id: Arc>, - extranonces: ExtendedExtranonce, - downstream_share_per_minute: f32, - upstream_target: Target, - up_id: u32, - ) { - match self { - ChannelKind::Group(_) => panic!("Impossible to initialize factory for group channel"), - ChannelKind::Extended(Some(_)) => panic!("Factory already initialized"), - ChannelKind::Extended(None) => { - let kind = ExtendedChannelKind::Proxy { upstream_target }; - let factory = ProxyExtendedChannelFactory::new( - group_id, - extranonces, - None, - downstream_share_per_minute, - kind, - Some(vec![]), - up_id, - ); - *self = Self::Extended(Some(factory)); - } - } - } - - fn reset(&mut self) { - match self { - ChannelKind::Group(_) => { - *self = ChannelKind::Group(GroupChannels::new()); - } - ChannelKind::Extended(_) => { - *self = ChannelKind::Extended(None); - } - } - } -} - -impl From for ChannelKind { - fn from(v: super::ChannelKind) -> Self { - match v { - super::ChannelKind::Group => Self::Group(GroupChannels::new()), - super::ChannelKind::Extended => Self::Extended(None), - } - } -} - -/// 1 to 1 connection with a pool -/// Can be either a mining pool or another proxy -/// 1 to 1 connection with an upstream node that implement the mining (sub)protocol can be either a -/// a pool or an upstream proxy. -#[derive(Debug, Clone)] -struct UpstreamMiningConnection { - receiver: Receiver, - sender: Sender, -} - -impl UpstreamMiningConnection { - async fn send(&mut self, sv2_frame: StdFrame) -> Result<(), SendError> { - info!("SEND"); - let either_frame = sv2_frame.into(); - match self.sender.send(either_frame).await { - Ok(_) => Ok(()), - Err(e) => Err(e), - } - } -} - -#[derive(Clone, Copy, Debug)] -pub struct Sv2MiningConnection { - version: u16, - setup_connection_flags: u32, - #[allow(dead_code)] - setup_connection_success_flags: u32, -} - -// Efficient stack do use JobDispatcher so the smaller variant (None) do not impact performance -// cause is used in already non performant environments. That to justify the below allow. -// https://rust-lang.github.io/rust-clippy/master/index.html#large_enum_varianT -#[allow(clippy::large_enum_variant)] -#[derive(Debug)] -pub enum JobDispatcher { - Group(GroupChannelJobDispatcher), - None, -} - -/// Can be either a mining pool or another proxy -#[derive(Debug)] -pub struct UpstreamMiningNode { - id: u32, - total_hash_rate: u64, - address: SocketAddr, - connection: Option, - sv2_connection: Option, - authority_public_key: [u8; 32], - /// group_channel id/channel_id -> dispatcher - pub channel_id_to_job_dispatcher: HashMap>, - /// Each relayed message that has a `request_id` field must have a unique `request_id` number, - /// connection-wise. - /// The `request_id` from the downstream is NOT guaranteed to be unique, so it must be changed. - request_id_mapper: RequestIdMapper, - downstream_selector: ProxyRemoteSelector, - pub channel_kind: ChannelKind, - group_id: Arc>, - pub channel_ids: Arc>, - downstream_share_per_minute: f32, - pub solution_sender: Option>>, - pub recv_coinbase_out: Option, Vec)>>, - #[allow(dead_code)] - tx_outs: HashMap, Vec>, - // When a future job is received from an extended channel this is transformed to severla std - // job for HOM downstream. If the job is future we need to keep track of the original job id - // and the new job ids used for the std job and also which downstream received which id. - // When a set new prev hash is received if it refer one of these ids we use this map and - // build the right set new pre hash for each downstream. TODO who is clearing the map? - #[allow(clippy::type_complexity)] - job_up_to_down_ids: - HashMap>, u32)>, BuildNoHashHasher>, - downstream_hash_rate: f32, - reconnect: bool, -} - -/// It assume that endpoint NEVER change flags and version! -/// I can open both extended and group channel with upstream. -impl UpstreamMiningNode { - #[allow(clippy::too_many_arguments)] - pub fn new( - id: u32, - address: SocketAddr, - authority_public_key: [u8; 32], - channel_kind: super::ChannelKind, - group_id: Arc>, - channel_ids: Arc>, - downstream_share_per_minute: f32, - solution_sender: Option>>, - recv_coinbase_out: Option, Vec)>>, - downstream_hash_rate: f32, - reconnect: bool, - ) -> Self { - let request_id_mapper = RequestIdMapper::new(); - let downstream_selector = ProxyRemoteSelector::new(); - Self { - id, - total_hash_rate: 0, - address, - connection: None, - sv2_connection: None, - authority_public_key, - channel_id_to_job_dispatcher: HashMap::with_hasher(BuildNoHashHasher::default()), - request_id_mapper, - downstream_selector, - channel_kind: channel_kind.into(), - group_id, - channel_ids, - downstream_share_per_minute, - solution_sender, - recv_coinbase_out, - tx_outs: HashMap::new(), - job_up_to_down_ids: HashMap::with_hasher(BuildNoHashHasher::default()), - downstream_hash_rate, - reconnect, - } - } - fn on_p_hash( - &mut self, - mut m: SetNewPrevHash<'static>, - ) -> Result, Error> { - match self.job_up_to_down_ids.get(&m.job_id) { - Some(downstreams) => { - let mut res = vec![]; - for (downstream, job_id) in downstreams { - m.job_id = *job_id; - let message = Mining::SetNewPrevHash(m.clone().into_static()); - res.push(SendTo::RelayNewMessageToRemote( - downstream.clone(), - message.clone(), - )); - } - self.job_up_to_down_ids = HashMap::with_hasher(BuildNoHashHasher::default()); - Ok(SendTo::Multiple(res)) - } - None => { - let downstrems = self.downstream_selector.get_all_downstreams(); - let mut res = vec![]; - m.job_id = 0; - let message = Mining::SetNewPrevHash(m.into_static()); - for downstream in downstrems { - res.push(SendTo::RelayNewMessageToRemote(downstream, message.clone())); - } - self.job_up_to_down_ids = HashMap::with_hasher(BuildNoHashHasher::default()); - Ok(SendTo::Multiple(res)) - } - } - } - - /// Try send a message to the upstream node. - /// If the node is connected and there are no error return Ok(()) - /// If the node is connected and there is an error the message is not sent and an error is - /// returned and the upstream is marked as not connected. - /// If the node is not connected it try to connect and send the message and everything is ok - /// the upstream is marked as connected and Ok(()) is returned if not an error is returned. - pub async fn send( - self_mutex: Arc>, - sv2_frame: StdFrame, - ) -> Result<(), super::error::Error> { - let (has_sv2_connection, mut connection, address) = self_mutex - .safe_lock(|self_| { - ( - self_.sv2_connection.is_some(), - self_.connection.clone(), - self_.address, - ) - }) - .unwrap(); - //let mut self_ = self_mutex.lock().await; - - match (connection.as_mut(), has_sv2_connection) { - (Some(connection), true) => match connection.send(sv2_frame).await { - Ok(_) => Ok(()), - Err(e) => { - error!( - "Error sending message to upstream node. Trying to reconnect to {}: {}", - address, e - ); - Self::connect(self_mutex.clone()).await.unwrap(); - // It assume that enpoint NEVER change flags and version! - match Self::setup_connection(self_mutex).await { - Ok(()) => Ok(()), - Err(()) => panic!(), - } - } - }, - // It assume that no downstream try to send messages before that the upstream is - // initialized. This assumption is enforced by the fact that - // UpstreamMiningNode::pair only pair downstream noder with already - // initialized upstream nodes! - (Some(connection), false) => match connection.send(sv2_frame).await { - Ok(_) => Ok(()), - Err(e) => Err(e.into()), - }, - (None, _) => { - Self::connect(self_mutex.clone()).await?; - let mut connection = self_mutex - .safe_lock(|self_| self_.connection.clone()) - .unwrap(); - match connection.as_mut().unwrap().send(sv2_frame).await { - Ok(_) => match Self::setup_connection(self_mutex).await { - Ok(()) => Ok(()), - Err(()) => panic!(), - }, - Err(e) => { - error!( - "Error sending message to upstream node at {} with error {}", - address, e - ); - //Self::connect(self_mutex.clone()).await.unwrap(); - Err(e.into()) - } - } - } - } - } - - async fn receive(self_mutex: Arc>) -> Result { - let mut connection = self_mutex - .safe_lock(|self_| self_.connection.clone()) - .unwrap(); - match connection.as_mut() { - Some(connection) => match connection.receiver.recv().await { - Ok(m) => Ok(m.try_into().unwrap()), - Err(_) => { - let address = self_mutex.safe_lock(|s| s.address).unwrap(); - error!("Upstream node {} is not available", address); - Err(super::error::Error::UpstreamNotAvailabe(address)) - } - }, - None => { - error!("No connection was found."); - todo!() - } - } - } - - async fn connect(self_mutex: Arc>) -> Result<(), super::error::Error> { - let has_connection = self_mutex - .safe_lock(|self_| self_.connection.is_some()) - .unwrap(); - match has_connection { - true => Ok(()), - false => { - let (address, authority_public_key) = self_mutex - .safe_lock(|self_| (self_.address, self_.authority_public_key)) - .unwrap(); - let socket = TcpStream::connect(address).await.map_err(|_| { - error!("Upstream node {} is not available", address); - super::error::Error::UpstreamNotAvailabe(address) - })?; - info!( - "Connected to upstream node {}: now handling noise handshake", - address - ); - - let initiator = Initiator::from_raw_k(authority_public_key).unwrap(); - let (receiver, sender) = - Connection::new(socket, HandshakeRole::Initiator(initiator)) - .await - .expect("impossible to conenct"); - let connection = UpstreamMiningConnection { receiver, sender }; - self_mutex - .safe_lock(|self_| { - self_.connection = Some(connection); - }) - .unwrap(); - info!("handshare done"); - Ok(()) - } - } - } - - #[async_recursion] - async fn setup_connection(self_mutex: Arc>) -> Result<(), ()> { - let sv2_connection = self_mutex.safe_lock(|self_| self_.sv2_connection).unwrap(); - - match sv2_connection { - None => Ok(()), - Some(sv2_connection) => { - let flags = sv2_connection.setup_connection_flags; - let version = sv2_connection.version; - let frame = self_mutex - .safe_lock(|self_| self_.new_setup_connection_frame(flags, version, version)) - .unwrap(); - Self::send(self_mutex.clone(), frame) - .await - .map_err(|e| (error!("Failed to send {:?}", e)))?; - - let cloned = self_mutex.clone(); - let mut response = task::spawn(async { Self::receive(cloned).await }) - .await - .unwrap() - .unwrap(); - - let message_type = response.get_header().unwrap().msg_type(); - let payload = response.payload(); - match (message_type, payload).try_into() { - Ok(CommonMessages::SetupConnectionSuccess(_)) => { - let receiver = self_mutex - .safe_lock(|self_| self_.connection.clone().unwrap().receiver) - .unwrap(); - Self::relay_incoming_messages(self_mutex, receiver); - Ok(()) - } - _ => panic!(), - } - } - } - } - - fn relay_incoming_messages( - self_: Arc>, - //_downstreams: HashMap, - receiver: Receiver, - ) { - task::spawn(async move { - loop { - if let Ok(message) = receiver.recv().await { - let m: StdFrame = message.try_into().unwrap(); - let incoming: StdFrame = m; - Self::next(self_.clone(), incoming).await; - } else { - Self::exit(self_); - break; - } - } - }); - } - pub fn get_id(&self) -> u32 { - self.id - } - - pub fn remove_dowstream(self_: Arc>, down: &Arc>) { - self_ - .safe_lock(|s| s.downstream_selector.remove_downstream(down)) - .unwrap(); - } - - fn exit(self_: Arc>) { - if !self_.safe_lock(|s| s.reconnect).unwrap() { - super::remove_upstream(self_.safe_lock(|s| s.id).unwrap()); - } - let downstreams = self_ - .safe_lock(|s| s.downstream_selector.get_all_downstreams()) - .unwrap(); - let mut dowstreams_: Vec>> = vec![]; - for d in downstreams { - if let Some(id) = d - .safe_lock(|d| match &d.status { - super::downstream_mining::DownstreamMiningNodeStatus::Initializing => None, - super::downstream_mining::DownstreamMiningNodeStatus::Paired(_) => None, - super::downstream_mining::DownstreamMiningNodeStatus::ChannelOpened( - channel, - ) => match channel { - Channel::DownstreamHomUpstreamGroup { channel_id, .. } => Some(*channel_id), - Channel::DownstreamHomUpstreamExtended { channel_id, .. } => { - Some(*channel_id) - } - }, - }) - .unwrap() - { - self_ - .safe_lock(|s| s.downstream_selector.remove_downstreams_in_channel(id)) - .unwrap(); - { - dowstreams_.push(d); - } - } - } - for d in dowstreams_ { - // TODO make sure that each reference have been dropped - if Arc::strong_count(&d) > 1 { - //todo!() - } - DownstreamMiningNode::exit(d); - } - if self_.safe_lock(|s| s.reconnect).unwrap() { - self_.safe_lock(|s| s.connection = None).unwrap(); - let flags = self_ - .safe_lock(|s| s.sv2_connection.unwrap().setup_connection_flags) - .unwrap(); - self_.safe_lock(|s| s.sv2_connection = None).unwrap(); - self_.safe_lock(|s| s.channel_kind.reset()).unwrap(); - tokio::task::spawn(async move { - tokio::time::sleep(std::time::Duration::from_secs(10)).await; - Self::setup_flag_and_version(self_, Some(flags), 2, 2) - .await - .unwrap(); - }); - } - } - - async fn match_next_message( - self_mutex: Arc>, - to_send: Result, Error>, - incoming: StdFrame, - ) { - match to_send { - Ok(SendTo::RelaySameMessageToRemote(downstream)) => { - let sv2_frame: codec_sv2::Sv2Frame = - incoming.map(|payload| payload.try_into().unwrap()); - - DownstreamMiningNode::send(downstream.clone(), sv2_frame) - .await - .unwrap(); - } - Ok(SendTo::RelayNewMessageToRemote(downstream_mutex, message)) => { - let message = MiningDeviceMessages::Mining(message); - let frame: DownstreamFrame = message.try_into().unwrap(); - DownstreamMiningNode::send(downstream_mutex, frame) - .await - .unwrap(); - } - Ok(SendTo::Respond(message)) => { - let message = AnyMessage::Mining(message); - let frame: StdFrame = message.try_into().unwrap(); - UpstreamMiningNode::send(self_mutex, frame).await.unwrap(); - } - Ok(SendTo::Multiple(sends_to)) => { - for send_to in sends_to { - match send_to { - SendTo::RelayNewMessageToRemote(downstream_mutex, message) => { - let message = MiningDeviceMessages::Mining(message); - let frame: DownstreamFrame = message.try_into().unwrap(); - DownstreamMiningNode::send(downstream_mutex, frame) - .await - .unwrap(); - } - SendTo::RelaySameMessageToRemote(downstream_mutex) => { - let frame: codec_sv2::Sv2Frame< - MiningDeviceMessages, - buffer_sv2::Slice, - > = incoming.clone().map(|payload| payload.try_into().unwrap()); - DownstreamMiningNode::send(downstream_mutex, frame) - .await - .unwrap(); - } - SendTo::Respond(message) => { - let message = AnyMessage::Mining(message); - let frame: StdFrame = message.try_into().unwrap(); - UpstreamMiningNode::send(self_mutex.clone(), frame) - .await - .unwrap(); - } - SendTo::None(_) => (), - SendTo::Multiple(_) => panic!("Nested SendTo::Multiple not supported"), - _ => panic!(), - } - } - } - Ok(SendTo::None(_)) => (), - Ok(_) => panic!(), - Err(Error::NoDownstreamsConnected) => (), - Err(e) => panic!("{:?}", e), - } - } - - pub async fn next(self_mutex: Arc>, mut incoming: StdFrame) { - let message_type = incoming.get_header().unwrap().msg_type(); - let payload = incoming.payload(); - - let next_message_to_send = - UpstreamMiningNode::handle_message_mining(self_mutex.clone(), message_type, payload); - Self::match_next_message(self_mutex, next_message_to_send, incoming).await; - } - - #[async_recursion] - async fn setup_flag_and_version( - self_mutex: Arc>, - flags: Option, - min_version: u16, - max_version: u16, - ) -> Result<(), super::error::Error> { - let flags = flags.unwrap_or(0b0000_0000_0000_0000_0000_0000_0000_0110); - let (frame, downstream_hr) = self_mutex - .safe_lock(|self_| { - ( - self_.new_setup_connection_frame(flags, min_version, max_version), - self_.downstream_hash_rate, - ) - }) - .unwrap(); - Self::send(self_mutex.clone(), frame).await?; - - let cloned = self_mutex.clone(); - let mut response = task::spawn(async { Self::receive(cloned).await }) - .await - .unwrap() - .unwrap(); - - let message_type = response.get_header().unwrap().msg_type(); - let payload = response.payload(); - match (message_type, payload).try_into() { - Ok(CommonMessages::SetupConnectionSuccess(m)) => { - let receiver = self_mutex - .safe_lock(|self_| { - self_.sv2_connection = Some(Sv2MiningConnection { - version: m.used_version, - setup_connection_flags: flags, - setup_connection_success_flags: m.flags, - }); - self_.connection.clone().unwrap().receiver - }) - .unwrap(); - Self::relay_incoming_messages(self_mutex.clone(), receiver); - if self_mutex - .safe_lock(|s| s.channel_kind.is_extended()) - .unwrap() - { - Self::open_extended_channel(self_mutex.clone(), downstream_hr).await - } - Ok(()) - } - Ok(CommonMessages::SetupConnectionError(m)) => { - if m.flags != 0 { - let flags = flags ^ m.flags; - // We need to send SetupConnection again as we do not yet know the version of - // upstream - // debounce this? - Self::setup_flag_and_version(self_mutex, Some(flags), min_version, max_version) - .await - } else { - let error_message = std::str::from_utf8(m.error_code.inner_as_ref()) - .unwrap() - .to_string(); - Err(super::error::Error::SetupConnectionError(error_message)) - } - } - Ok(_) => todo!(), - Err(_) => todo!(), - } - } - - async fn open_extended_channel(self_mutex: Arc>, nominal_hash_rate: f32) { - let message = AnyMessage::Mining(Mining::OpenExtendedMiningChannel( - OpenExtendedMiningChannel { - request_id: 0, - user_identity: "proxy".to_string().try_into().unwrap(), - nominal_hash_rate, - max_target: [ - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - ] - .into(), - min_extranonce_size: super::MIN_EXTRANONCE_SIZE, - }, - )); - Self::send(self_mutex.clone(), message.try_into().unwrap()) - .await - .unwrap(); - - Self::wait_for_channel_factory(self_mutex).await; - } - - async fn wait_for_channel_factory(self_mutex: Arc>) { - while !self_mutex - .safe_lock(|s| s.channel_kind.is_initialized()) - .unwrap() - { - tokio::time::sleep(Duration::from_secs(1)).await; - } - } - - fn new_setup_connection_frame( - &self, - flags: u32, - min_version: u16, - max_version: u16, - ) -> StdFrame { - let endpoint_host = self - .address - .ip() - .to_string() - .into_bytes() - .try_into() - .unwrap(); - let vendor = String::new().try_into().unwrap(); - let hardware_version = String::new().try_into().unwrap(); - let firmware = String::new().try_into().unwrap(); - let device_id = String::new().try_into().unwrap(); - let setup_connection: AnyMessage = SetupConnection { - protocol: Protocol::MiningProtocol, - min_version, - max_version, - flags, - endpoint_host, - endpoint_port: self.address.port(), - vendor, - hardware_version, - firmware, - device_id, - } - .into(); - setup_connection.try_into().unwrap() - } - - pub fn open_standard_channel_down( - &mut self, - request_id: u32, - downstream_hash_rate: f32, - id_header_only: bool, - channel_id: u32, - ) -> Vec> { - match &mut self.channel_kind { - // When channel kind is Group (that means that no extended channels is open between - // proxy and this upstream) opening channel is handled by upstream and proxy must only - // relay messages - ChannelKind::Group(_) => { - panic!("Open satandard channel down for group up not supported") - } - ChannelKind::Extended(Some(factory)) => { - self.downstream_selector - .on_open_standard_channel_success(request_id, 0, channel_id) - .unwrap(); - let messages = factory - .add_standard_channel( - request_id, - downstream_hash_rate, - id_header_only, - channel_id, - ) - .unwrap(); - messages.into_iter().map(|x| x.into_static()).collect() - } - _ => panic!("Channel factory not initialized"), - } - } - - pub fn handle_std_shr( - self_: Arc>, - share_: SubmitSharesStandard, - ) -> Result, Error> { - if self_.safe_lock(|s| s.channel_kind.is_extended()).unwrap() { - let share = self_ - .safe_lock(|s| { - let factory = s.channel_kind.get_factory(); - factory.on_submit_shares_standard(share_.clone()) - }) - .unwrap()?; - match share { - OnNewShare::SendErrorDownstream(e) => { - tracing::error!("Received invalid share"); - Ok(Mining::SubmitSharesError(e)) - } - OnNewShare::SendSubmitShareUpstream((s, _)) => match s { - Share::Extended(s) => { - let message = Mining::SubmitSharesExtended(s); - let message = AnyMessage::Mining(message); - let frame: StdFrame = message.try_into().unwrap(); - tokio::task::spawn(async move { - UpstreamMiningNode::send(self_.clone(), frame) - .await - .unwrap(); - }); - let success = SubmitSharesSuccess { - channel_id: share_.channel_id, - last_sequence_number: share_.sequence_number, - new_submits_accepted_count: 1, - new_shares_sum: 1, - }; - let message = Mining::SubmitSharesSuccess(success); - Ok(message) - } - Share::Standard(_) => unreachable!(), - }, - OnNewShare::RelaySubmitShareUpstream => todo!(), - OnNewShare::ShareMeetBitcoinTarget((share, Some(template_id), coinbase, _)) => { - match share { - Share::Extended(s) => { - let solution = SubmitSolution { - template_id, - version: s.version, - header_timestamp: s.ntime, - header_nonce: s.nonce, - coinbase_tx: coinbase.try_into().unwrap(), - }; - let sender = self_ - .safe_lock(|s| s.solution_sender.clone()) - .unwrap() - .unwrap(); - // The below channel should never be full is ok to block - sender.send_blocking(solution).unwrap(); - - let message = Mining::SubmitSharesExtended(s); - let message = AnyMessage::Mining(message); - let frame: StdFrame = message.try_into().unwrap(); - tokio::task::spawn(async move { - UpstreamMiningNode::send(self_.clone(), frame) - .await - .unwrap(); - }); - let success = SubmitSharesSuccess { - channel_id: share_.channel_id, - last_sequence_number: share_.sequence_number, - new_submits_accepted_count: 1, - new_shares_sum: 1, - }; - let message = Mining::SubmitSharesSuccess(success); - Ok(message) - } - Share::Standard(_) => { - // on_submit_shares_standard call check_target that in the case of a - // Proxy and a share that is below the - // bitcoin target if the share is a standard - // share call share.into_extended making this branch unreachable. - unreachable!() - } - } - } - // When we have a ShareMeetBitcoinTarget it means that the proxy know the bitcoin - // target that means that the proxy must have JD capabilities that means that the - // second tuple elements can not be None but must be Some(template_id) - OnNewShare::ShareMeetBitcoinTarget(..) => unreachable!(), - OnNewShare::ShareMeetDownstreamTarget => { - let success = SubmitSharesSuccess { - channel_id: share_.channel_id, - last_sequence_number: share_.sequence_number, - new_submits_accepted_count: 1, - new_shares_sum: 1, - }; - let message = Mining::SubmitSharesSuccess(success); - Ok(message) - } - } - } else { - unreachable!("Calling share_into_extended for an non extended upstream make no sense") - } - } - - // Example of how next could be implemented more efficently if no particular good log are - // needed it just relay the majiority of messages downstream without serializing and - // deserializing them. In order to find the Downstream at which the message must bu relayed the - // channel id must be deserialized, but knowing the message type that is a very easy task is - // either 4 bytes after the header or the first 4 bytes after the header + 4 bytes - // #[cfg(test)] - // #[allow(unused)] - // pub async fn next_faster(&mut self, mut incoming: StdFrame) { - // let message_type = incoming.get_header().unwrap().msg_type(); - - // // When a channel is opened we need to setup the channel id in order to relay next - // messages // to the right Downstream - // if todo!() { // check if message_type is channel related - - // // When a mining message is received (that is not a channel related message) always - // relay it downstream } else if todo!() { // check if message_type is is a mining - // message // everything here can be just relayed downstream - - // // Other sub(protocol) messages - // } else { - // todo!() - // } - // } -} - -pub trait HasDownstreamSelector { - fn get_remote_selector(&mut self) -> &mut ProxyRemoteSelector; -} - -impl HasDownstreamSelector for UpstreamMiningNode { - fn get_remote_selector(&mut self) -> &mut ProxyRemoteSelector { - &mut self.downstream_selector - } -} - -impl ParseMiningMessagesFromUpstream for UpstreamMiningNode { - fn get_channel_type(&self) -> SupportedChannelTypes { - SupportedChannelTypes::GroupAndExtended - } - - fn is_work_selection_enabled(&self) -> bool { - true - } - - fn handle_open_standard_mining_channel_success( - &mut self, - m: OpenStandardMiningChannelSuccess, - ) -> Result, Error> { - let routing_logic = super::get_routing_logic(); - let remote = match routing_logic { - MiningRoutingLogic::None => None, - MiningRoutingLogic::Proxy(r_logic) => { - let up = r_logic - .safe_lock(|r_logic| { - r_logic.on_open_standard_channel_success(self, &mut m.clone()) - })?; - Some(up?) - } - MiningRoutingLogic::_P(_) => panic!("Must use either MiningRoutingLogic::None or MiningRoutingLogic::Proxy for `routing_logic` param"), - }; - match &mut self.channel_kind { - ChannelKind::Group(group) => { - let down_is_header_only = remote - .as_ref() - .unwrap() - .safe_lock(|remote| remote.is_header_only()) - .unwrap(); - let remote = remote.unwrap(); - if down_is_header_only { - let mut res = vec![SendTo::RelaySameMessageToRemote(remote.clone())]; - for message in group.on_channel_success_for_hom_downtream(&m)? { - res.push(SendTo::RelayNewMessageToRemote(remote.clone(), message)); - } - remote - .safe_lock(|r| { - r.open_channel_for_down_hom_up_group(m.channel_id, m.group_channel_id) - }) - .unwrap(); - Ok(SendTo::Multiple(res)) - } else { - // Here we want to support only the case where downstream is non HOM and want to - // open extended channels with the proxy. Dowstream non HOM - // that try to open standard channel (grouped in groups) do - // not make much sense so for now is not supported - panic!() - } - } - // If we opened and extended channel upstreams we should not receive this message - ChannelKind::Extended(_) => todo!(), - } - } - - fn handle_open_extended_mining_channel_success( - &mut self, - m: OpenExtendedMiningChannelSuccess, - ) -> Result, Error> { - info!( - "Received OpenExtendedMiningChannelSuccess with request id: {} and channel id: {}", - m.request_id, m.channel_id - ); - debug!("OpenStandardMiningChannelSuccess: {:?}", m); - let extranonce_prefix: Extranonce = m.extranonce_prefix.clone().into(); - let range_0 = 0..m.extranonce_prefix.clone().to_vec().len(); - let range_1 = range_0.end..(range_0.end + EXTRANONCE_RANGE_1_LENGTH); - let range_2 = range_1.end..(range_0.end + m.extranonce_size as usize); - let extranonces = ExtendedExtranonce::from_upstream_extranonce( - extranonce_prefix, - range_0, - range_1, - range_2, - ) - .unwrap(); - - self.channel_kind.initialize_factory( - self.group_id.clone(), - extranonces, - self.downstream_share_per_minute, - m.target.clone().into(), - m.channel_id, - ); - Ok(SendTo::None(None)) - } - - fn handle_open_mining_channel_error( - &mut self, - _m: OpenMiningChannelError, - ) -> Result, Error> { - todo!("460") - } - - fn handle_update_channel_error( - &mut self, - _m: UpdateChannelError, - ) -> Result, Error> { - todo!("470") - } - - fn handle_close_channel( - &mut self, - _m: CloseChannel, - ) -> Result, Error> { - todo!("480") - } - - fn handle_set_extranonce_prefix( - &mut self, - _m: SetExtranoncePrefix, - ) -> Result, Error> { - todo!("490") - } - - fn handle_submit_shares_success( - &mut self, - m: SubmitSharesSuccess, - ) -> Result, Error> { - info!("Received SubmitSharesSuccess"); - debug!("SubmitSharesSuccess: {:?}", m); - match &self - .downstream_selector - .downstream_from_channel_id(m.channel_id) - { - Some(d) => Ok(SendTo::RelaySameMessageToRemote(d.clone())), - None => { - info!("Share success"); - Ok(SendTo::None(None)) - } - } - } - - fn handle_submit_shares_error( - &mut self, - m: SubmitSharesError, - ) -> Result, Error> { - error!( - "Received SubmitSharesError with error code {}", - std::str::from_utf8(m.error_code.as_ref()).unwrap_or("unknown error code") - ); - Ok(SendTo::None(None)) - } - - // TODO this is usefull only for non hom upstream do we really want to support non hom upstream - // it do not make much sense IMO. - // For now I comment the code and put here an Error - fn handle_new_mining_job( - &mut self, - _m: NewMiningJob, - ) -> Result, Error> { - todo!() - //// One and only one downstream cause the message is not extended - //match &self - // .downstream_selector - // .get_downstreams_in_channel(m.channel_id) - //{ - // Some(downstreams) => { - // let downstream = &downstreams[0]; - // crate::add_job_id( - // m.job_id, - // self.id, - // downstream.safe_lock(|d| d.prev_job_id).unwrap(), - // ); - // Ok(SendTo::RelaySameMessageToRemote(downstream.clone())) - // } - // None => Err(Error::NoDownstreamsConnected), - //} - } - - fn handle_new_extended_mining_job( - &mut self, - m: NewExtendedMiningJob, - ) -> Result, Error> { - info!( - "Received new extended mining job for channel id: {} with job id: {} is_future: {}", - m.channel_id, - m.job_id, - m.is_future() - ); - debug!("NewExtendedMiningJob: {:?}", m); - let mut res = vec![]; - match &mut self.channel_kind { - ChannelKind::Group(group) => { - group.on_new_extended_mining_job(&m); - let downstreams = self - .downstream_selector - .get_downstreams_in_channel(m.channel_id) - .ok_or(Error::NoDownstreamsConnected)?; - for downstream in downstreams { - match downstream.safe_lock(|r| r.get_channel().clone()).unwrap() { - Channel::DownstreamHomUpstreamGroup { - channel_id, - group_id, - .. - } => { - let message = - group.last_received_job_to_standard_job(channel_id, group_id)?; - - res.push(SendTo::RelayNewMessageToRemote( - downstream.clone(), - Mining::NewMiningJob(message), - )); - } - _ => unreachable!(), - } - } - } - ChannelKind::Extended(Some(factory)) => { - if let Ok(messages) = factory.on_new_extended_mining_job(m.clone().as_static()) { - let mut new_p_hash_added = false; - let is_future = m.is_future(); - let original_job_id = m.job_id; - if is_future { - self.job_up_to_down_ids.insert(original_job_id, vec![]); - }; - for (id, message) in messages { - match &message { - Mining::NewExtendedMiningJob(_) => { - // TODO implement it if support for non HOM downstream is needed - todo!() - } - Mining::NewMiningJob(m) => { - let downstream = self - .downstream_selector - .downstream_from_channel_id(id) - .ok_or(Error::NoDownstreamsConnected)?; - if is_future { - let ids = - self.job_up_to_down_ids.get_mut(&original_job_id).unwrap(); - ids.push((downstream.clone(), m.job_id)); - }; - res.push(SendTo::RelayNewMessageToRemote( - downstream, - Mining::NewMiningJob(m.clone()), - )); - } - Mining::SetNewPrevHash(m) => { - if !new_p_hash_added { - let downstreams = - self.downstream_selector.get_all_downstreams(); - for downstream in downstreams { - res.push(SendTo::RelayNewMessageToRemote( - downstream.clone(), - Mining::SetNewPrevHash(m.clone()), - )); - } - new_p_hash_added = true; - } - } - _ => todo!(), - } - } - } else { - todo!() - } - } - ChannelKind::Extended(None) => panic!("Factory not initialized"), - } - Ok(SendTo::Multiple(res)) - } - - fn handle_set_new_prev_hash( - &mut self, - m: SetNewPrevHash, - ) -> Result, Error> { - info!( - "Received SetNewPrevHash channel id: {}, job id: {}", - m.channel_id, m.job_id - ); - debug!("SetNewPrevHash: {:?}", m); - match &mut self.channel_kind { - ChannelKind::Group(group) => { - group.update_new_prev_hash(&m); - - let downstreams = self - .downstream_selector - .get_downstreams_in_channel(m.channel_id) - .ok_or(Error::NoDownstreamsConnected)?; - - let mut res = vec![]; - for downstream in downstreams { - let message = Mining::SetNewPrevHash(m.clone().into_static()); - res.push(SendTo::RelayNewMessageToRemote(downstream.clone(), message)); - } - Ok(SendTo::Multiple(res)) - } - ChannelKind::Extended(factory) => { - if factory - .as_mut() - .expect("Factory not initialized") - .on_new_prev_hash(m.clone().into_static()) - .is_ok() - { - self.on_p_hash(m.into_static().clone()) - } else { - todo!() - } - } - } - } - - fn handle_set_custom_mining_job_success( - &mut self, - m: SetCustomMiningJobSuccess, - ) -> Result, Error> { - info!( - "Received SetCustomMiningJobSuccess for channel id: {} for job id: {}", - m.channel_id, m.job_id - ); - debug!("SetCustomMiningJobSuccess: {:?}", m); - Ok(SendTo::None(None)) - } - - fn handle_set_custom_mining_job_error( - &mut self, - _m: SetCustomMiningJobError, - ) -> Result, Error> { - todo!("560") - } - - fn handle_set_target(&mut self, _m: SetTarget) -> Result, Error> { - todo!("570") - } - - fn handle_set_group_channel( - &mut self, - _m: SetGroupChannel, - ) -> Result, Error> { - todo!() - } -} - -pub async fn scan( - nodes: Vec>>, - min_version: u16, - max_version: u16, -) -> Vec>> { - let res = Arc::new(Mutex::new(Vec::with_capacity(nodes.len()))); - let spawn_tasks: Vec> = nodes - .iter() - .map(|node| { - let node = node.clone(); - let cloned = res.clone(); - task::spawn(async move { - if let Err(e) = UpstreamMiningNode::setup_flag_and_version( - node.clone(), - None, - min_version, - max_version, - ) - .await - { - error!("{:?}", e) - } else { - cloned.safe_lock(|r| r.push(node.clone())).unwrap(); - } - }) - }) - .collect(); - for task in spawn_tasks { - task.await.unwrap(); - } - res.safe_lock(|r| r.clone()).unwrap() -} - -impl IsUpstream for UpstreamMiningNode { - fn get_version(&self) -> u16 { - self.sv2_connection.unwrap().version - } - - fn get_flags(&self) -> u32 { - self.sv2_connection.unwrap().setup_connection_flags - } - - fn get_supported_protocols(&self) -> Vec { - vec![Protocol::MiningProtocol] - } - - fn get_id(&self) -> u32 { - self.id - } - - fn get_mapper(&mut self) -> Option<&mut RequestIdMapper> { - Some(&mut self.request_id_mapper) - } -} -impl IsMiningUpstream for UpstreamMiningNode { - fn total_hash_rate(&self) -> u64 { - self.total_hash_rate - } - fn add_hash_rate(&mut self, to_add: u64) { - self.total_hash_rate += to_add; - } - fn get_opened_channels(&mut self) -> &mut Vec { - todo!() - } - fn update_channels(&mut self, _channel: UpstreamChannel) { - todo!() - } -} - -#[cfg(test)] -mod tests { - use std::net::{IpAddr, Ipv4Addr}; - - use super::*; - - #[test] - fn new_upstream_minining_node() { - let id = 0; - let address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); - let authority_public_key = [ - 215, 11, 47, 78, 34, 232, 25, 192, 195, 168, 170, 209, 95, 181, 40, 114, 154, 226, 176, - 190, 90, 169, 238, 89, 191, 183, 97, 63, 194, 119, 11, 31, - ]; - let ids = Arc::new(Mutex::new(GroupId::new())); - let channel_ids = Arc::new(Mutex::new(Id::new())); - let actual = UpstreamMiningNode::new( - id, - address, - authority_public_key, - super::super::ChannelKind::Group, - ids, - channel_ids, - 10.0, - None, - None, - 100_000.0, - false, - ); - - assert_eq!(actual.id, id); - - assert_eq!(actual.total_hash_rate, 0); - assert_eq!(actual.address, address); - - if actual.connection.is_some() { - panic!("`UpstreamMiningNode::connection` should be `None` on call to `UpstreamMiningNode::new()`"); - } - - if actual.sv2_connection.is_some() { - panic!("`UpstreamMiningNode::sv2_connection` should be `None` on call to `UpstreamMiningNode::new()`"); - } - - // How to test - // assert_eq!(actual.downstream_selector, ProxyRemoteSelector::new()); - - assert_eq!(actual.authority_public_key, authority_public_key); - assert!(actual.channel_id_to_job_dispatcher.is_empty()); - assert_eq!(actual.request_id_mapper, RequestIdMapper::new()); - } -} diff --git a/roles/mining-proxy/src/main.rs b/roles/mining-proxy/src/main.rs deleted file mode 100644 index 59bfde9a05..0000000000 --- a/roles/mining-proxy/src/main.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! Configurable Sv2 it support extended and group channel -//! Upstream means another proxy or a pool -//! Downstream means another proxy or a mining device -//! -//! UpstreamMining is the trait that a proxy must implement in order to -//! understand Downstream mining messages. -//! -//! DownstreamMining is the trait that a proxy must implement in order to -//! understand Upstream mining messages -//! -//! Same thing for DownstreamCommon and UpstreamCommon but for common messages -//! -//! DownstreamMiningNode implement both UpstreamMining and UpstreamCommon -//! -//! UpstreamMiningNode implement both DownstreamMining and DownstreamCommon -//! -//! A Downstream that signal the capacity to handle group channels can open more than one channel. -//! A Downstream that signal the incapacity to handle group channels can open only one channel. -use tracing::error; - -use mining_proxy_sv2::start_mining_proxy; - -mod args; -use args::process_cli_args; - -/// 1. the proxy scan all the upstreams and map them -/// 2. downstream open a connection with proxy -/// 3. downstream send SetupConnection -/// 4. a mining_channels::Upstream is created -/// 5. upstream_mining::UpstreamMiningNodes is used to pair this downstream with the most suitable -/// upstream -/// 6. mining_channels::Upstream create a new downstream_mining::DownstreamMiningNode embedding -/// itself in it -/// 7. normal operation between the paired downstream_mining::DownstreamMiningNode and -/// upstream_mining::UpstreamMiningNode begin -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - let config = match process_cli_args() { - Ok(c) => c, - Err(e) => { - error!("Mining Proxy Config error: {}", e); - return; - } - }; - start_mining_proxy(config).await; -} diff --git a/roles/pool/Cargo.toml b/roles/pool/Cargo.toml index bd60449887..08ef66723b 100644 --- a/roles/pool/Cargo.toml +++ b/roles/pool/Cargo.toml @@ -2,7 +2,7 @@ name = "pool_sv2" version = "0.1.3" authors = ["The Stratum V2 Developers"] -edition = "2018" +edition = "2021" description = "SV2 pool role" documentation = "https://docs.rs/pool_sv2" readme = "README.md" @@ -18,24 +18,19 @@ path = "src/lib/mod.rs" [dependencies] async-channel = "1.5.1" -binary_sv2 = { path = "../../protocols/v2/binary-sv2" } +stratum-common = { path = "../../common", features = ["with_network_helpers"] } buffer_sv2 = { path = "../../utils/buffer" } -codec_sv2 = { path = "../../protocols/v2/codec-sv2", features = ["noise_sv2"] } -network_helpers_sv2 = { path = "../roles-utils/network-helpers", features =["with_buffer_pool"] } -noise_sv2 = { path = "../../protocols/v2/noise-sv2" } rand = "0.8.4" -roles_logic_sv2 = { path = "../../protocols/v2/roles-logic-sv2" } serde = { version = "1.0.89", features = ["derive", "alloc"], default-features = false } -stratum-common = { path = "../../common", features = ["bitcoin"] } +secp256k1 = { version = "0.28.2", default-features = false, features = ["alloc", "rand", "rand-std"] } tokio = { version = "1.44.1", features = ["full"] } ext-config = { version = "0.14.0", features = ["toml"], package = "config" } tracing = { version = "0.1" } -tracing-subscriber = "0.3" async-recursion = "1.0.0" error_handling = { path = "../../utils/error-handling" } nohash-hasher = "0.2.0" key-utils = { path = "../../utils/key-utils" } -config-helpers = { path = "../roles-utils/config-helpers" } +config_helpers_sv2 = { path = "../roles-utils/config-helpers" } clap = { version = "4.5.39", features = ["derive"] } [dev-dependencies] diff --git a/roles/pool/config-examples/pool-config-hosted-tp-example.toml b/roles/pool/config-examples/pool-config-hosted-tp-example.toml index 06f3afafab..b9c7a4ae55 100644 --- a/roles/pool/config-examples/pool-config-hosted-tp-example.toml +++ b/roles/pool/config-examples/pool-config-hosted-tp-example.toml @@ -2,24 +2,26 @@ authority_public_key = "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72" authority_secret_key = "mkDLTBBRxdBv998612qipDYoTK3YUrqLe8uWw7gu3iXbSrn2n" cert_validity_sec = 3600 -test_only_listen_adress_plain = "0.0.0.0:34250" +test_only_listen_adress_plain = "0.0.0.0:34250" listen_address = "0.0.0.0:34254" -# List of coinbase outputs used to build the coinbase tx -# ! Right now only one output is supported, so comment all the ones you don't need ! -# For P2PK, P2PKH, P2WPKH, P2TR a public key is needed. For P2SH and P2WSH, a redeem script is needed. -coinbase_outputs = [ - #{ output_script_type = "P2PK", output_script_value = "0372c47307e5b75ce365daf835f226d246c5a7a92fe24395018d5552123354f086" }, - #{ output_script_type = "P2PKH", output_script_value = "0372c47307e5b75ce365daf835f226d246c5a7a92fe24395018d5552123354f086" }, - #{ output_script_type = "P2SH", output_script_value = "00142ef89234bc95136eb9e6fee9d32722ebd8c1f0ab" }, - #{ output_script_type = "P2WSH", output_script_value = "00142ef89234bc95136eb9e6fee9d32722ebd8c1f0ab" }, - { output_script_type = "P2WPKH", output_script_value = "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075" }, - #{ output_script_type = "P2TR", output_script_value = "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075" }, -] +# Coinbase outputs are specified as descriptors. A full list of descriptors is available at +# https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki#appendix-b-index-of-script-expressions +# Although the `musig` descriptor is not yet supported and the legacy `combo` descriptor never +# will be. If you have an address, embed it in a descriptor like `addr(
)`. +coinbase_reward_script = "addr(tb1qa0sm0hxzj0x25rh8gw5xlzwlsfvvyz8u96w3p8)" + +# Server Id (number to guarantee unique search space allocation across different Pool servers) +server_id = 1 # Pool signature (string to be included in coinbase tx) pool_signature = "Stratum V2 SRI Pool" +# Enable this option to set a predefined log file path. +# When enabled, logs will always be written to this file. +# The CLI option --log-file (or -f) will override this setting if provided. +# log_file = "./pool.log" + # Template Provider config # Local TP (this is pointing to localhost so you must run a TP locally for this configuration to work) #tp_address = "127.0.0.1:8442" @@ -27,4 +29,4 @@ pool_signature = "Stratum V2 SRI Pool" tp_address = "75.119.150.111:8442" tp_authority_public_key = "9bwHCYnjhbHm4AS3pWg9MtAH83mzWohoJJJDELYBqZhDNqszDLc" shares_per_minute = 1.0 -share_batch_size = 10 \ No newline at end of file +share_batch_size = 10 diff --git a/roles/pool/config-examples/pool-config-local-tp-example.toml b/roles/pool/config-examples/pool-config-local-tp-example.toml index 54660d0b42..997331d44f 100644 --- a/roles/pool/config-examples/pool-config-local-tp-example.toml +++ b/roles/pool/config-examples/pool-config-local-tp-example.toml @@ -5,23 +5,26 @@ cert_validity_sec = 3600 test_only_listen_adress_plain = "0.0.0.0:34250" listen_address = "0.0.0.0:34254" -# List of coinbase outputs used to build the coinbase tx -# ! Right now only one output is supported, so comment all the ones you don't need ! -# For P2PK, P2PKH, P2WPKH, P2TR a public key is needed. For P2SH and P2WSH, a redeem script is needed. -coinbase_outputs = [ - #{ output_script_type = "P2PK", output_script_value = "0372c47307e5b75ce365daf835f226d246c5a7a92fe24395018d5552123354f086" }, - #{ output_script_type = "P2PKH", output_script_value = "0372c47307e5b75ce365daf835f226d246c5a7a92fe24395018d5552123354f086" }, - #{ output_script_type = "P2SH", output_script_value = "00142ef89234bc95136eb9e6fee9d32722ebd8c1f0ab" }, - #{ output_script_type = "P2WSH", output_script_value = "00142ef89234bc95136eb9e6fee9d32722ebd8c1f0ab" }, - { output_script_type = "P2WPKH", output_script_value = "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075" }, - #{ output_script_type = "P2TR", output_script_value = "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075" }, -] +# Coinbase outputs are specified as descriptors. A full list of descriptors is available at +# https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki#appendix-b-index-of-script-expressions +# Although the `musig` descriptor is not yet supported and the legacy `combo` descriptor never +# will be. If you have an address, embed it in a descriptor like `addr(
)`. +coinbase_reward_script = "addr(tb1qa0sm0hxzj0x25rh8gw5xlzwlsfvvyz8u96w3p8)" + +# Server Id (number to guarantee unique search space allocation across different Pool servers) +server_id = 1 # Pool signature (string to be included in coinbase tx) pool_signature = "Stratum V2 SRI Pool" +# Enable this option to set a predefined log file path. +# When enabled, logs will always be written to this file. +# The CLI option --log-file (or -f) will override this setting if provided. +# log_file = "./pool.log" + + # Template Provider config # Local TP (this is pointing to localhost so you must run a TP locally for this configuration to work) tp_address = "127.0.0.1:8442" shares_per_minute = 1.0 -share_batch_size = 10 \ No newline at end of file +share_batch_size = 10 diff --git a/roles/pool/src/args.rs b/roles/pool/src/args.rs index 3816f5a25d..c2c2186b34 100644 --- a/roles/pool/src/args.rs +++ b/roles/pool/src/args.rs @@ -18,16 +18,25 @@ pub struct Args { default_value = "pool-config.toml" )] pub config_path: PathBuf, + #[arg( + short = 'f', + long = "log-file", + help = "Path to the log file. If not set, logs will only be written to stdout." + )] + pub log_file: Option, } /// Parses CLI arguments and loads the PoolConfig from the specified file. pub fn process_cli_args() -> PoolConfig { let args = Args::parse(); let config_path = args.config_path.to_str().expect("Invalid config path"); - let config: PoolConfig = Config::builder() + let mut config: PoolConfig = Config::builder() .add_source(File::new(config_path, FileFormat::Toml)) .build() .and_then(|settings| settings.try_deserialize::()) .expect("Failed to load or deserialize config"); + + config.set_log_dir(args.log_file); + config } diff --git a/roles/pool/src/lib/config.rs b/roles/pool/src/lib/config.rs index 452c1e9a9c..f55c0564c3 100644 --- a/roles/pool/src/lib/config.rs +++ b/roles/pool/src/lib/config.rs @@ -8,7 +8,9 @@ //! - Managing [`TemplateProviderConfig`], [`AuthorityConfig`], [`CoinbaseOutput`], and //! [`ConnectionConfig`] //! - Validating and converting coinbase outputs -use config_helpers::CoinbaseOutput; +use std::path::{Path, PathBuf}; + +use config_helpers_sv2::CoinbaseRewardScript; use key_utils::{Secp256k1PublicKey, Secp256k1SecretKey}; /// Configuration for the Pool, including connection, authority, and coinbase settings. @@ -20,21 +22,28 @@ pub struct PoolConfig { authority_public_key: Secp256k1PublicKey, authority_secret_key: Secp256k1SecretKey, cert_validity_sec: u64, - coinbase_outputs: Vec, + coinbase_reward_script: CoinbaseRewardScript, pool_signature: String, shares_per_minute: f32, share_batch_size: usize, + log_file: Option, + server_id: u16, } impl PoolConfig { /// Creates a new instance of the [`PoolConfig`]. + /// + /// # Panics + /// + /// Panics if `coinbase_reward_script` is empty. pub fn new( pool_connection: ConnectionConfig, template_provider: TemplateProviderConfig, authority_config: AuthorityConfig, - coinbase_outputs: Vec, + coinbase_reward_script: CoinbaseRewardScript, shares_per_minute: f32, share_batch_size: usize, + server_id: u16, ) -> Self { Self { listen_address: pool_connection.listen_address, @@ -43,16 +52,18 @@ impl PoolConfig { authority_public_key: authority_config.public_key, authority_secret_key: authority_config.secret_key, cert_validity_sec: pool_connection.cert_validity_sec, - coinbase_outputs, + coinbase_reward_script, pool_signature: pool_connection.signature, shares_per_minute, share_batch_size, + log_file: None, + server_id, } } - /// Returns the coinbase outputs. - pub fn coinbase_outputs(&self) -> &Vec { - self.coinbase_outputs.as_ref() + /// Returns the coinbase output. + pub fn coinbase_reward_script(&self) -> &CoinbaseRewardScript { + &self.coinbase_reward_script } /// Returns Pool listenining address. @@ -95,9 +106,9 @@ impl PoolConfig { self.share_batch_size } - /// Sets the coinbase outputs. - pub fn set_coinbase_outputs(&mut self, coinbase_outputs: Vec) { - self.coinbase_outputs = coinbase_outputs; + /// Sets the coinbase output. + pub fn set_coinbase_reward_script(&mut self, coinbase_output: CoinbaseRewardScript) { + self.coinbase_reward_script = coinbase_output; } /// Returns the shares per minute. @@ -109,6 +120,22 @@ impl PoolConfig { pub fn set_tp_address(&mut self, tp_address: String) { self.tp_address = tp_address; } + + /// Sets the log directory. + pub fn set_log_dir(&mut self, log_dir: Option) { + if let Some(dir) = log_dir { + self.log_file = Some(dir); + } + } + /// Returns the log directory. + pub fn log_dir(&self) -> Option<&Path> { + self.log_file.as_deref() + } + + /// Returns the server id. + pub fn server_id(&self) -> u16 { + self.server_id + } } /// Configuration for connecting to a Template Provider. diff --git a/roles/pool/src/lib/error.rs b/roles/pool/src/lib/error.rs index 2f5e21f830..a26e86e8e0 100644 --- a/roles/pool/src/lib/error.rs +++ b/roles/pool/src/lib/error.rs @@ -16,7 +16,12 @@ use std::{ sync::{MutexGuard, PoisonError}, }; -use roles_logic_sv2::parsers::Mining; +use stratum_common::roles_logic_sv2::{ + self, + channels_sv2::vardiff::error::VardiffError, + codec_sv2::{self, binary_sv2, noise_sv2}, + parsers_sv2::{Mining, ParserError}, +}; /// Represents various errors that can occur in the pool implementation. #[derive(std::fmt::Debug)] @@ -32,7 +37,7 @@ pub enum PoolError { /// Error from the `codec_sv2` crate. Codec(codec_sv2::Error), /// Error related to parsing a coinbase output specification. - CoinbaseOutput(config_helpers::CoinbaseOutputError), + CoinbaseOutput(config_helpers_sv2::CoinbaseOutputError), /// Error from the `noise_sv2` crate. Noise(noise_sv2::Error), /// Error from the `roles_logic_sv2` crate. @@ -47,27 +52,45 @@ pub enum PoolError { Custom(String), /// Error related to the SV2 protocol, including an error code and a `Mining` message. Sv2ProtocolError((u32, Mining<'static>)), + Vardiff(VardiffError), + Parser(ParserError), +} + +impl From for PoolError { + fn from(value: VardiffError) -> Self { + PoolError::Vardiff(value) + } +} + +impl From for PoolError { + fn from(value: ParserError) -> Self { + PoolError::Parser(value) + } } impl std::fmt::Display for PoolError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { use PoolError::*; match self { - Io(ref e) => write!(f, "I/O error: `{:?}", e), - ChannelSend(ref e) => write!(f, "Channel send failed: `{:?}`", e), - ChannelRecv(ref e) => write!(f, "Channel recv failed: `{:?}`", e), - BinarySv2(ref e) => write!(f, "Binary SV2 error: `{:?}`", e), - Codec(ref e) => write!(f, "Codec SV2 error: `{:?}", e), - CoinbaseOutput(ref e) => write!(f, "Coinbase output error: `{:?}", e), - Framing(ref e) => write!(f, "Framing SV2 error: `{:?}`", e), - Noise(ref e) => write!(f, "Noise SV2 error: `{:?}", e), - RolesLogic(ref e) => write!(f, "Roles Logic SV2 error: `{:?}`", e), - PoisonLock(ref e) => write!(f, "Poison lock: {:?}", e), - ComponentShutdown(ref e) => write!(f, "Component shutdown: {:?}", e), - Custom(ref e) => write!(f, "Custom SV2 error: `{:?}`", e), + Io(ref e) => write!(f, "I/O error: `{e:?}"), + ChannelSend(ref e) => write!(f, "Channel send failed: `{e:?}`"), + ChannelRecv(ref e) => write!(f, "Channel recv failed: `{e:?}`"), + BinarySv2(ref e) => write!(f, "Binary SV2 error: `{e:?}`"), + Codec(ref e) => write!(f, "Codec SV2 error: `{e:?}"), + CoinbaseOutput(ref e) => write!(f, "Coinbase output error: `{e:?}"), + Framing(ref e) => write!(f, "Framing SV2 error: `{e:?}`"), + Noise(ref e) => write!(f, "Noise SV2 error: `{e:?}"), + RolesLogic(ref e) => write!(f, "Roles Logic SV2 error: `{e:?}`"), + PoisonLock(ref e) => write!(f, "Poison lock: {e:?}"), + ComponentShutdown(ref e) => write!(f, "Component shutdown: {e:?}"), + Custom(ref e) => write!(f, "Custom SV2 error: `{e:?}`"), Sv2ProtocolError(ref e) => { - write!(f, "Received Sv2 Protocol Error from upstream: `{:?}`", e) + write!(f, "Received Sv2 Protocol Error from upstream: `{e:?}`") + } + PoolError::Vardiff(ref e) => { + write!(f, "Received Vardiff Error : {e:?}") } + Parser(ref e) => write!(f, "Parser error: `{e:?}`"), } } } @@ -98,8 +121,8 @@ impl From for PoolError { } } -impl From for PoolError { - fn from(e: config_helpers::CoinbaseOutputError) -> PoolError { +impl From for PoolError { + fn from(e: config_helpers_sv2::CoinbaseOutputError) -> PoolError { PoolError::CoinbaseOutput(e) } } diff --git a/roles/pool/src/lib/mining_pool/message_handler.rs b/roles/pool/src/lib/mining_pool/message_handler.rs index ca1ebc9907..7dae25e2f3 100644 --- a/roles/pool/src/lib/mining_pool/message_handler.rs +++ b/roles/pool/src/lib/mining_pool/message_handler.rs @@ -6,26 +6,31 @@ //! reacts to various mining-related messages received from a connected downstream miner. use super::super::mining_pool::Downstream; -use binary_sv2::Str0255; -use roles_logic_sv2::{ - channels::server::{ - error::{ExtendedChannelError, StandardChannelError}, - extended::ExtendedChannel, - share_accounting::{ShareValidationError, ShareValidationResult}, - standard::StandardChannel, +use std::{ + convert::TryInto, + sync::{atomic::Ordering, Arc, RwLock}, +}; +use stratum_common::roles_logic_sv2::{ + bitcoin::{consensus::Decodable, transaction::TxOut, Amount}, + channels_sv2::{ + server::{ + error::{ExtendedChannelError, StandardChannelError}, + extended::ExtendedChannel, + group::GroupChannel, + jobs::job_store::DefaultJobStore, + share_accounting::{ShareValidationError, ShareValidationResult}, + standard::StandardChannel, + }, + Vardiff, VardiffState, }, + codec_sv2::binary_sv2::Str0255, errors::Error, handlers::mining::{ParseMiningMessagesFromDownstream, SendTo, SupportedChannelTypes}, mining_sv2::*, - parsers::Mining, + parsers_sv2::Mining, template_distribution_sv2::SubmitSolution, utils::Mutex, }; -use std::{ - convert::TryInto, - sync::{Arc, RwLock}, -}; -use stratum_common::bitcoin::Amount; use tracing::{error, info}; impl ParseMiningMessagesFromDownstream<()> for Downstream { @@ -69,7 +74,54 @@ impl ParseMiningMessagesFromDownstream<()> for Downstream { .map(|s| s.to_string()) .map_err(|e| Error::InvalidUserIdentity(e.to_string()))?; - info!("Received OpenStandardMiningChannel: {:?}", incoming); + info!("Received OpenStandardMiningChannel: {}", incoming); + + if self.requires_custom_work { + error!("OpenStandardMiningChannel: Standard Channels are not supported for this connection"); + let open_standard_mining_channel_error = OpenMiningChannelError { + request_id, + error_code: "standard-channels-not-supported-for-custom-work" + .to_string() + .try_into() + .expect("error code must be valid string"), + }; + return Ok(SendTo::Respond(Mining::OpenMiningChannelError( + open_standard_mining_channel_error, + ))); + } + + let last_future_template = self.last_future_template.clone(); + let last_set_new_prev_hash_tdp = self.last_new_prev_hash.clone(); + + let pool_coinbase_output = TxOut { + value: Amount::from_sat(last_future_template.coinbase_tx_value_remaining), + script_pubkey: self.coinbase_reward_script.script_pubkey(), + }; + + if !self.requires_standard_jobs && self.group_channel.is_none() { + // we only create one group channel for all standard channels + + let group_channel_id = self.channel_id_factory.fetch_add(1, Ordering::Relaxed); + let job_store = DefaultJobStore::new(); + + let mut group_channel = GroupChannel::new_for_pool( + group_channel_id, + job_store, + self.pool_tag_string.clone(), + ); + group_channel + .on_new_template( + last_future_template.clone(), + vec![pool_coinbase_output.clone()], + ) + .map_err(Error::FailedToProcessNewTemplateGroupChannel)?; + + group_channel + .on_set_new_prev_hash(last_set_new_prev_hash_tdp.clone()) + .map_err(Error::FailedToProcessSetNewPrevHashGroupChannel)?; + + self.group_channel = Some(Arc::new(RwLock::new(group_channel))); + } let nominal_hash_rate = incoming.nominal_hash_rate; let requested_max_target = incoming.max_target.into_static(); @@ -81,9 +133,9 @@ impl ParseMiningMessagesFromDownstream<()> for Downstream { .and_then(|res| res.map_err(Error::ExtranoncePrefixFactoryError))? .to_vec(); - let channel_id = self.channel_id_factory.next(); - - let mut standard_channel = match StandardChannel::new( + let channel_id = self.channel_id_factory.fetch_add(1, Ordering::Relaxed); + let job_store = DefaultJobStore::new(); + let mut standard_channel = match StandardChannel::new_for_pool( channel_id, user_identity, extranonce_prefix.clone(), @@ -91,6 +143,8 @@ impl ParseMiningMessagesFromDownstream<()> for Downstream { nominal_hash_rate, self.share_batch_size, self.shares_per_minute, + job_store, + self.pool_tag_string.clone(), ) { Ok(channel) => channel, Err(e) => match e { @@ -155,17 +209,12 @@ impl ParseMiningMessagesFromDownstream<()> for Downstream { open_standard_mining_channel_success, )); - let last_future_template = self.last_future_template.clone(); - - // note: the fact that we're parsing a Vec from the config file is a bit of a hack - // so while we don't clean that up, we only set the value of the first output - let mut pool_coinbase_outputs = self.empty_pool_coinbase_outputs.clone(); - pool_coinbase_outputs[0].value = - Amount::from_sat(last_future_template.coinbase_tx_value_remaining); - // create a future standard job based on the last future template standard_channel - .on_new_template(last_future_template.clone(), pool_coinbase_outputs) + .on_new_template( + last_future_template.clone(), + vec![pool_coinbase_output.clone()], + ) .map_err(Error::FailedToCreateStandardChannel)?; let future_standard_job_id = standard_channel @@ -182,7 +231,6 @@ impl ParseMiningMessagesFromDownstream<()> for Downstream { messages.push(Mining::NewMiningJob(future_standard_job_message)); // SetNewPrevHash message activates the future job - let last_set_new_prev_hash_tdp = self.last_new_prev_hash.clone(); let prev_hash = last_set_new_prev_hash_tdp.prev_hash.clone(); let header_timestamp = last_set_new_prev_hash_tdp.header_timestamp; let n_bits = last_set_new_prev_hash_tdp.n_bits; @@ -200,8 +248,13 @@ impl ParseMiningMessagesFromDownstream<()> for Downstream { let messages = messages.into_iter().map(SendTo::Respond).collect(); + let vardiff = VardiffState::new()?; + self.standard_channels - .insert(channel_id, Arc::new(RwLock::new(standard_channel.clone()))); + .insert(channel_id, Arc::new(RwLock::new(standard_channel))); + + self.vardiff + .insert(channel_id, Arc::new(RwLock::new(vardiff))); if let Some(group_channel_guard) = &self.group_channel { let mut group_channel = group_channel_guard @@ -232,7 +285,7 @@ impl ParseMiningMessagesFromDownstream<()> for Downstream { .map(|s| s.to_string()) .map_err(|e| Error::InvalidUserIdentity(e.to_string()))?; - info!("Received OpenExtendedMiningChannel: {:?}", m); + info!("Received OpenExtendedMiningChannel: {}", m); let nominal_hash_rate = m.nominal_hash_rate; let requested_max_target = m.max_target.into_static(); @@ -262,9 +315,9 @@ impl ParseMiningMessagesFromDownstream<()> for Downstream { } }; - let channel_id = self.channel_id_factory.next(); - - let mut extended_channel = match ExtendedChannel::new( + let channel_id = self.channel_id_factory.fetch_add(1, Ordering::Relaxed); + let job_store = DefaultJobStore::new(); + let mut extended_channel = match ExtendedChannel::new_for_pool( channel_id, user_identity, extranonce_prefix, @@ -274,6 +327,8 @@ impl ParseMiningMessagesFromDownstream<()> for Downstream { requested_min_rollable_extranonce_size, self.share_batch_size, self.shares_per_minute, + job_store, + self.pool_tag_string.clone(), ) { Ok(channel) => channel, Err(e) => match e { @@ -341,56 +396,70 @@ impl ParseMiningMessagesFromDownstream<()> for Downstream { open_extended_mining_channel_success, )); - let last_future_template = self.last_future_template.clone(); - - // note: the fact that we're parsing a Vec from the config file is a bit of a hack - // so while we don't clean that up, we only set the value of the first output - let mut pool_coinbase_outputs = self.empty_pool_coinbase_outputs.clone(); - pool_coinbase_outputs[0].value = - Amount::from_sat(last_future_template.coinbase_tx_value_remaining); - - // create a future extended job based on the last future template - extended_channel - .on_new_template(last_future_template.clone(), pool_coinbase_outputs) - .map_err(Error::FailedToCreateExtendedChannel)?; - - let future_extended_job_id = extended_channel - .get_future_template_to_job_id() - .get(&last_future_template.template_id) - .expect("future job id must exist"); - let future_extended_job = extended_channel - .get_future_jobs() - .get(future_extended_job_id) - .expect("future job must exist"); + let last_set_new_prev_hash_tdp = self.last_new_prev_hash.clone(); - let future_extended_job_message = - future_extended_job.get_job_message().clone().into_static(); + // if the client requires custom work, we don't need to send any extended jobs + // so we just process the SetNewPrevHash message + if self.requires_custom_work { + extended_channel + .on_set_new_prev_hash(last_set_new_prev_hash_tdp) + .map_err(Error::FailedToCreateExtendedChannel)?; + // if the client does not require custom work, we need to send the future extended job + // and the SetNewPrevHash message + } else { + let last_future_template = self.last_future_template.clone(); - // send this future job as new job message - // to be immediately activated with the subsequent SetNewPrevHash message - messages.push(Mining::NewExtendedMiningJob(future_extended_job_message)); + let pool_coinbase_output = TxOut { + value: Amount::from_sat(last_future_template.coinbase_tx_value_remaining), + script_pubkey: self.coinbase_reward_script.script_pubkey(), + }; - // SetNewPrevHash message activates the future job - let last_set_new_prev_hash_tdp = self.last_new_prev_hash.clone(); - let prev_hash = last_set_new_prev_hash_tdp.prev_hash.clone(); - let header_timestamp = last_set_new_prev_hash_tdp.header_timestamp; - let n_bits = last_set_new_prev_hash_tdp.n_bits; - let set_new_prev_hash_mining = SetNewPrevHash { - channel_id, - job_id: *future_extended_job_id, - prev_hash, - min_ntime: header_timestamp, - nbits: n_bits, - }; - extended_channel - .on_set_new_prev_hash(self.last_new_prev_hash.clone()) - .map_err(Error::FailedToCreateExtendedChannel)?; - messages.push(Mining::SetNewPrevHash(set_new_prev_hash_mining)); + // create a future extended job based on the last future template + extended_channel + .on_new_template(last_future_template.clone(), vec![pool_coinbase_output]) + .map_err(Error::FailedToCreateExtendedChannel)?; + + let future_extended_job_id = extended_channel + .get_future_template_to_job_id() + .get(&last_future_template.template_id) + .expect("future job id must exist"); + let future_extended_job = extended_channel + .get_future_jobs() + .get(future_extended_job_id) + .expect("future job must exist"); + + let future_extended_job_message = + future_extended_job.get_job_message().clone().into_static(); + + // send this future job as new job message + // to be immediately activated with the subsequent SetNewPrevHash message + messages.push(Mining::NewExtendedMiningJob(future_extended_job_message)); + + // SetNewPrevHash message activates the future job + let prev_hash = last_set_new_prev_hash_tdp.prev_hash.clone(); + let header_timestamp = last_set_new_prev_hash_tdp.header_timestamp; + let n_bits = last_set_new_prev_hash_tdp.n_bits; + let set_new_prev_hash_mining = SetNewPrevHash { + channel_id, + job_id: *future_extended_job_id, + prev_hash, + min_ntime: header_timestamp, + nbits: n_bits, + }; + extended_channel + .on_set_new_prev_hash(last_set_new_prev_hash_tdp) + .map_err(Error::FailedToCreateExtendedChannel)?; + messages.push(Mining::SetNewPrevHash(set_new_prev_hash_mining)); + } let messages = messages.into_iter().map(SendTo::Respond).collect(); + let vardiff = VardiffState::new()?; + self.extended_channels - .insert(channel_id, Arc::new(RwLock::new(extended_channel.clone()))); + .insert(channel_id, Arc::new(RwLock::new(extended_channel))); + self.vardiff + .insert(channel_id, Arc::new(RwLock::new(vardiff))); Ok(SendTo::Multiple(messages)) } @@ -405,7 +474,7 @@ impl ParseMiningMessagesFromDownstream<()> for Downstream { // target difficulty. // - `Err(Error)` - If calculating the target fails or the channel factory interaction fails. fn handle_update_channel(&mut self, m: UpdateChannel) -> Result, Error> { - info!("Received UpdateChannel message: {:?}", m); + info!("Received UpdateChannel message: {}", m); let channel_id = m.channel_id; let new_nominal_hash_rate = m.nominal_hash_rate; @@ -517,7 +586,7 @@ impl ParseMiningMessagesFromDownstream<()> for Downstream { channel_id, maximum_target: new_target.clone().into(), }; - return Ok(SendTo::Respond(Mining::SetTarget(set_target))); + Ok(SendTo::Respond(Mining::SetTarget(set_target))) } else { error!("UpdateChannelError: invalid-channel-id"); let update_channel_error = UpdateChannelError { @@ -527,9 +596,9 @@ impl ParseMiningMessagesFromDownstream<()> for Downstream { .try_into() .expect("error code must be valid string"), }; - return Ok(SendTo::Respond(Mining::UpdateChannelError( + Ok(SendTo::Respond(Mining::UpdateChannelError( update_channel_error, - ))); + ))) } } @@ -547,7 +616,7 @@ impl ParseMiningMessagesFromDownstream<()> for Downstream { &mut self, m: SubmitSharesStandard, ) -> Result, Error> { - info!("Received: {:?}", m); + info!("Received: {}", m); let channel_id = m.channel_id; if !self.standard_channels.contains_key(&channel_id) { @@ -572,7 +641,15 @@ impl ParseMiningMessagesFromDownstream<()> for Downstream { .write() .map_err(|e| Error::PoisonLock(e.to_string()))?; + let mut vardiff = self + .vardiff + .get(&channel_id) + .expect("Vardiff must exist") + .write() + .map_err(|e| Error::PoisonLock(e.to_string()))?; + let res = standard_channel.validate_share(m.clone()); + vardiff.increment_shares_since_last_update(); match res { Ok(ShareValidationResult::Valid) => { info!( @@ -592,7 +669,7 @@ impl ParseMiningMessagesFromDownstream<()> for Downstream { new_submits_accepted_count, new_shares_sum, }; - info!("SubmitSharesStandard: {:?} ✅", success); + info!("SubmitSharesStandard: {} ✅", success); Ok(SendTo::Respond(Mining::SubmitSharesSuccess(success))) } Ok(ShareValidationResult::BlockFound(template_id, coinbase)) => { @@ -701,7 +778,7 @@ impl ParseMiningMessagesFromDownstream<()> for Downstream { &mut self, m: SubmitSharesExtended, ) -> Result, Error> { - info!("Received: {:?}", m); + info!("Received: {}", m); let channel_id = m.channel_id; if !self.extended_channels.contains_key(&channel_id) { @@ -724,7 +801,15 @@ impl ParseMiningMessagesFromDownstream<()> for Downstream { .write() .map_err(|e| Error::PoisonLock(e.to_string()))?; + let mut vardiff = self + .vardiff + .get(&channel_id) + .expect("Vardiff must exist") + .write() + .map_err(|e| Error::PoisonLock(e.to_string()))?; + let res = extended_channel.validate_share(m.clone()); + vardiff.increment_shares_since_last_update(); match res { Ok(ShareValidationResult::Valid) => { info!( @@ -744,7 +829,7 @@ impl ParseMiningMessagesFromDownstream<()> for Downstream { new_submits_accepted_count, new_shares_sum, }; - info!("SubmitSharesExtended: {:?} ✅", success); + info!("SubmitSharesExtended: {} ✅", success); Ok(SendTo::Respond(Mining::SubmitSharesSuccess(success))) } Ok(ShareValidationResult::BlockFound(template_id, coinbase)) => { @@ -850,15 +935,38 @@ impl ParseMiningMessagesFromDownstream<()> for Downstream { // custom job setup. // - `Err(Error)` - If the channel factory interaction fails. fn handle_set_custom_mining_job(&mut self, m: SetCustomMiningJob) -> Result, Error> { - info!("Received SetCustomMiningJob: {:?}", m); + info!("Received SetCustomMiningJob: {}", m); // this is a naive implementation, but ideally we should check the SetCustomMiningJob // message parameters, especially: // - the mining_job_token - // - the coinbase reward outputs + // - the amount of the pool payout output + + let custom_job_coinbase_outputs = Vec::::consensus_decode( + &mut m.coinbase_tx_outputs.inner_as_ref().to_vec().as_slice(), + ) + .map_err(|_| Error::FailedToDeserializeCoinbaseOutputs)?; + + // check that the script_pubkey from self.coinbase_reward_script + // is present in the custom job coinbase outputs + let missing_script = !custom_job_coinbase_outputs.iter().any(|pool_output| { + *pool_output.script_pubkey == *self.coinbase_reward_script.script_pubkey() + }); + + if missing_script { + error!("SetCustomMiningJobError: pool-payout-script-missing"); + + let error = SetCustomMiningJobError { + request_id: m.request_id, + channel_id: m.channel_id, + error_code: "pool-payout-script-missing" + .to_string() + .try_into() + .expect("error code must be valid string"), + }; - // some of these checks are actually pending on spec discussion of - // https://github.com/stratum-mining/sv2-spec/issues/133 + return Ok(SendTo::Respond(Mining::SetCustomMiningJobError(error))); + } let channel_id = m.channel_id; if !self.extended_channels.contains_key(&channel_id) { @@ -892,4 +1000,12 @@ impl ParseMiningMessagesFromDownstream<()> for Downstream { }; Ok(SendTo::Respond(Mining::SetCustomMiningJobSuccess(success))) } + + fn handle_close_channel(&mut self, m: CloseChannel) -> Result, Error> { + info!("Received Close Channel: {m}"); + self.extended_channels.remove(&m.channel_id); + self.standard_channels.remove(&m.channel_id); + self.vardiff.remove(&m.channel_id); + Ok(SendTo::None(None)) + } } diff --git a/roles/pool/src/lib/mining_pool/mod.rs b/roles/pool/src/lib/mining_pool/mod.rs index 3de6b4e35b..3462b13801 100644 --- a/roles/pool/src/lib/mining_pool/mod.rs +++ b/roles/pool/src/lib/mining_pool/mod.rs @@ -19,40 +19,60 @@ //! - `Pool`: Central manager for all downstream connections and job updates. //! - `Downstream`: Represents a miner and handles its connection lifecycle. //! - `PoolChannelFactory`: Manages the creation and tracking of mining channels. -use crate::config::PoolConfig; - use super::{ error::{PoolError, PoolResult}, status, }; +use crate::config::PoolConfig; use async_channel::{Receiver, Sender}; -use binary_sv2::U256; -use codec_sv2::{HandshakeRole, Responder, StandardEitherFrame, StandardSv2Frame}; -use config_helpers::CoinbaseOutputError; +use config_helpers_sv2::CoinbaseRewardScript; use error_handling::handle_result; use key_utils::SignatureService; -use network_helpers_sv2::noise_connection::Connection; use nohash_hasher::BuildNoHashHasher; -use roles_logic_sv2::{ - channels::server::{extended::ExtendedChannel, group::GroupChannel, standard::StandardChannel}, - common_properties::{CommonDownstreamData, IsDownstream, IsMiningDownstream}, - errors::Error, - handlers::mining::{ParseMiningMessagesFromDownstream, SendTo}, - mining_sv2::{ExtendedExtranonce, SetNewPrevHash as SetNewPrevHashMp, MAX_EXTRANONCE_LEN}, - parsers::{AnyMessage, Mining}, - template_distribution_sv2::{NewTemplate, SetNewPrevHash as SetNewPrevHashTdp, SubmitSolution}, - utils::{Id as IdFactory, Mutex}, -}; +use secp256k1; use std::{ collections::HashMap, convert::TryInto, net::SocketAddr, - sync::{Arc, RwLock}, + sync::{ + atomic::{AtomicU32, Ordering}, + Arc, RwLock, + }, + time::Duration, }; use stratum_common::{ - bitcoin::{Amount, ScriptBuf, TxOut}, - secp256k1, + network_helpers_sv2::noise_connection::Connection, + roles_logic_sv2::{ + self, + bitcoin::{Amount, TxOut}, + channels_sv2::{ + server::{ + extended::ExtendedChannel, + group::GroupChannel, + jobs::{extended::ExtendedJob, job_store::DefaultJobStore, standard::StandardJob}, + standard::StandardChannel, + }, + VardiffState, + }, + codec_sv2::{ + self, binary_sv2::U256, HandshakeRole, Responder, StandardEitherFrame, StandardSv2Frame, + }, + errors::Error, + handlers::mining::{ParseMiningMessagesFromDownstream, SendTo}, + mining_sv2::{ + ExtendedExtranonce, SetNewPrevHash as SetNewPrevHashMp, SetTarget, Target, + MAX_EXTRANONCE_LEN, + }, + parsers_sv2::{AnyMessage, Mining}, + template_distribution_sv2::{ + NewTemplate, SetNewPrevHash as SetNewPrevHashTdp, SubmitSolution, + }, + utils::Mutex, + }, }; + +use roles_logic_sv2::channels_sv2::Vardiff; + use tokio::{net::TcpListener, task}; use tracing::{debug, error, info, warn}; @@ -67,27 +87,6 @@ pub type StdFrame = StandardSv2Frame; /// A standard SV2 frame that can contain either type of frame. pub type EitherFrame = StandardEitherFrame; -/// Parses the coinbase output configurations from the [`PoolConfig`] and converts them -/// into `bitcoin::TxOut` objects required by the pool logic. -/// -/// It iterates through the configured outputs, attempts to convert them into the -/// internal `CoinbaseOutput_` representation and then into `bitcoin::ScriptBuf`. -/// Sets the value to 0 sats as per SV2 pool requirements (actual value determined later) -pub fn get_coinbase_output(config: &PoolConfig) -> Result, CoinbaseOutputError> { - let mut result = Vec::new(); - for coinbase_output_pool in config.coinbase_outputs() { - let output_script: ScriptBuf = coinbase_output_pool.clone().try_into()?; - result.push(TxOut { - value: Amount::from_sat(0), - script_pubkey: output_script, - }); - } - match result.is_empty() { - true => Err(CoinbaseOutputError::EmptyCoinbaseOutputs), - _ => Ok(result), - } -} - /// Represents a single connection to a downstream miner. /// /// Encapsulates the state and communication channels for one miner. An instance @@ -102,28 +101,36 @@ pub struct Downstream { receiver: Receiver, // Channel sender for outgoing SV2 frames to the network connection task. sender: Sender, - // Common data negotiated during the connection setup (e.g., protocol version, flags). - downstream_data: CommonDownstreamData, + // Whether the downstream requires standard jobs. + requires_standard_jobs: bool, + // Whether the downstream requires custom work. + requires_custom_work: bool, // Sender channel to forward valid `SubmitSolution` messages received from this // downstream miner to the main [`Pool`] task, which then sends them upstream. solution_sender: Sender>, - channel_id_factory: IdFactory, + channel_id_factory: AtomicU32, extranonce_prefix_factory_extended: Arc>, extranonce_prefix_factory_standard: Arc>, // A map of all extended channels, keyed by their ID. - extended_channels: HashMap>>>, + extended_channels: + HashMap>>>>>, // A map of all standard channels, keyed by their ID. - standard_channels: HashMap>>>, + standard_channels: + HashMap>>>>>, + vardiff: HashMap>>, // naive approach: // we create one group channel for the connection // and add all standard channels to this same single group channel // (that is, only if SetupConnection.REQUIRES_STANDARD_JOBS flag is set) - group_channel: Option>>>, + group_channel: + Option>>>>>, share_batch_size: usize, shares_per_minute: f32, last_future_template: NewTemplate<'static>, last_new_prev_hash: SetNewPrevHashTdp<'static>, - empty_pool_coinbase_outputs: Vec, + coinbase_reward_script: CoinbaseRewardScript, + // string to be written into the coinbase scriptSig on non-JD jobs + pool_tag_string: String, } /// The central state manager for the mining pool. @@ -142,7 +149,7 @@ pub struct Pool { // Flag indicating whether at least one `NewTemplate` has been received and processed. // Might be used to ensure initial jobs are sent before accepting solutions??. new_template_processed: bool, - downstream_id_factory: IdFactory, + downstream_id_factory: AtomicU32, // Sender channel for reporting status updates and errors to the main monitoring loop. status_tx: status::Sender, extranonce_prefix_factory_extended: Arc>, @@ -150,6 +157,8 @@ pub struct Pool { share_batch_size: usize, last_future_template: Option>, last_new_prev_hash: Option>, + // string to be written into the coinbase scriptSig on non-JD jobs + pool_tag_string: String, } impl Downstream { @@ -169,17 +178,17 @@ impl Downstream { status_tx: status::Sender, address: SocketAddr, shares_per_minute: f32, - empty_pool_coinbase_outputs: Vec, + coinbase_reward_script: CoinbaseRewardScript, ) -> PoolResult>> { // Handle the SV2 SetupConnection message exchange. let setup_connection = Arc::new(Mutex::new(SetupConnectionHandler::new())); - let downstream_data = + let (requires_standard_jobs, requires_custom_work) = SetupConnectionHandler::setup(setup_connection, &mut receiver, &mut sender, address) .await?; - let id = pool.safe_lock(|p| p.downstream_id_factory.next())?; + let id = pool.safe_lock(|p| p.downstream_id_factory.fetch_add(1, Ordering::Relaxed))?; - let mut channel_id_factory = IdFactory::new(); + let channel_id_factory = AtomicU32::new(0); // extranonce prefix factories are shared across all downstreams // that avoids extranonce_prefix collision across different downstreams @@ -214,55 +223,33 @@ impl Downstream { .expect("last_new_prev_hash must be Some") })?; - // note: the fact that we're parsing a Vec from the config file is a bit of a hack - // so while we don't clean that up, we only set the value of the first output - let mut pool_coinbase_outputs = empty_pool_coinbase_outputs.clone(); - pool_coinbase_outputs[0].value = - Amount::from_sat(last_future_template.coinbase_tx_value_remaining); - - let group_channel = if !downstream_data.header_only { - // naive approach: - // we create one group channel for the entire connection - // and add all standard channels to this same single group channel - // we know this will result in group_channel_id == 1 - // so we use that for every standard channel - let group_channel_id = channel_id_factory.next(); - - let mut group_channel = GroupChannel::new(group_channel_id); - - group_channel - .on_new_template(last_future_template.clone(), pool_coinbase_outputs) - .map_err(Error::FailedToProcessNewTemplateGroupChannel)?; - - group_channel - .on_set_new_prev_hash(last_new_prev_hash.clone()) - .map_err(Error::FailedToProcessSetNewPrevHashGroupChannel)?; - - Some(Arc::new(RwLock::new(group_channel))) - } else { - None - }; + let pool_tag = pool.safe_lock(|p| p.pool_tag_string.clone())?; // Create the Downstream instance, wrapped for shared access. let self_ = Arc::new(Mutex::new(Downstream { id, receiver, - sender, - downstream_data, + sender: sender.clone(), + requires_standard_jobs, + requires_custom_work, solution_sender, channel_id_factory, extended_channels: HashMap::new(), standard_channels: HashMap::new(), - group_channel, + vardiff: HashMap::new(), + group_channel: None, extranonce_prefix_factory_extended, extranonce_prefix_factory_standard, share_batch_size, shares_per_minute, last_future_template, last_new_prev_hash, - empty_pool_coinbase_outputs, + coinbase_reward_script, + pool_tag_string: pool_tag, })); + tokio::spawn(spawn_vardiff_loop(self_.clone(), sender.clone(), id)); + let cloned = self_.clone(); // Spawn a dedicated task to continuously receive and process messages from this downstream. @@ -277,8 +264,7 @@ impl Downstream { if let Err(e) = status_tx .send(status::Status { state: status::State::Healthy(format!( - "Downstream connection dropped: {}", - e + "Downstream connection dropped: {e}" )), }) .await @@ -309,11 +295,13 @@ impl Downstream { .map_err(|e| PoolError::PoisonLock(e.to_string())); handle_result!(status_tx, res); error!("Downstream {} disconnected", id); + break; } } } warn!("Downstream connection dropped"); + sender.close(); }); Ok(self_) } @@ -361,7 +349,7 @@ impl Downstream { ) -> PoolResult<()> { match send_to { Ok(SendTo::Respond(message)) => { - debug!("Sending to downstream: {:?}", message); + debug!("Sending to downstream: {}", message); // returning an error will send the error to the main thread, // and the main thread will drop the downstream from the pool if let &Mining::OpenMiningChannelError(_) = &message { @@ -400,7 +388,7 @@ impl Downstream { /// This method is used to send message to downstream. async fn send( self_mutex: Arc>, - message: roles_logic_sv2::parsers::Mining<'static>, + message: roles_logic_sv2::parsers_sv2::Mining<'static>, ) -> PoolResult<()> { //let message = if let Mining::NewExtendedMiningJob(job) = message { // Mining::NewExtendedMiningJob(extended_job_to_non_segwit(job, 32)?) @@ -434,17 +422,6 @@ pub fn verify_token( is_verified } -impl IsDownstream for Downstream { - // Returns the `CommonDownstreamData` negotiated during connection setup. - fn get_downstream_mining_data(&self) -> CommonDownstreamData { - self.downstream_data - } -} - -// Marker trait implementation indicating this struct represents a mining downstream. Do we really -// need this? -impl IsMiningDownstream for Downstream {} - impl Pool { /// Binds to the configured listen address and starts accepting incoming TCP connections. /// @@ -456,7 +433,6 @@ impl Pool { config: PoolConfig, mut recv_stop_signal: tokio::sync::watch::Receiver<()>, shares_per_minute: f32, - pool_coinbase_outputs: Vec, ) -> PoolResult<()> { let status_tx = self_.safe_lock(|s| s.status_tx.clone())?; // Bind the TCP listener to the address specified in the config. @@ -486,7 +462,7 @@ impl Pool { match responder { Ok(resp) => { - if let Ok((receiver, sender)) = Connection::new(stream, HandshakeRole::Responder(resp)).await { + if let Ok((receiver, sender)) = Connection::new::(stream, HandshakeRole::Responder(resp)).await { handle_result!( status_tx, Self::accept_incoming_connection_( @@ -495,7 +471,7 @@ impl Pool { sender, address, shares_per_minute, - pool_coinbase_outputs.clone() + config.coinbase_reward_script().clone() ).await ); } @@ -527,7 +503,7 @@ impl Pool { sender: Sender, address: SocketAddr, shares_per_minute: f32, - pool_coinbase_outputs: Vec, + coinbase_reward_script: CoinbaseRewardScript, ) -> PoolResult<()> { let solution_sender = self_.safe_lock(|p| p.solution_sender.clone())?; let status_tx = self_.safe_lock(|s| s.status_tx.clone())?; @@ -542,12 +518,12 @@ impl Pool { status_tx.listener_to_connection(), address, shares_per_minute, - pool_coinbase_outputs, + coinbase_reward_script, ) .await?; // Extract the assigned ID after successful creation. - let (_, channel_id) = downstream.safe_lock(|d| (d.downstream_data.header_only, d.id))?; + let channel_id = downstream.safe_lock(|d| d.id)?; // Add the new downstream to the central map. self_.safe_lock(|p| { @@ -572,7 +548,7 @@ impl Pool { .safe_lock(|s| s.status_tx.clone()) .map_err(|e| PoolError::PoisonLock(e.to_string()))?; while let Ok(new_prev_hash) = rx.recv().await { - debug!("New prev hash received: {:?}", new_prev_hash); + debug!("New prev hash received: {}", new_prev_hash); let res = self_ .safe_lock(|s| { s.last_new_prev_hash = Some(new_prev_hash.clone()); @@ -659,6 +635,13 @@ impl Pool { extended_channel .on_set_new_prev_hash(new_prev_hash.clone()) .map_err(Error::FailedToProcessSetNewPrevHashExtendedChannel)?; + + // don't send any SetNewPrevHash messages to Extended Channels + // if the downstream requires custom work + if d.requires_custom_work { + continue; + } + let activated_extended_job_id = extended_channel .get_active_job() .expect("active job must exist") @@ -705,7 +688,7 @@ impl Pool { let status_tx = self_.safe_lock(|s| s.status_tx.clone())?; while let Ok(new_template) = rx.recv().await { info!( - "New template received, creating a new mining job(s): {:?}", + "New template received, creating a new mining job(s): {}", new_template ); @@ -724,12 +707,10 @@ impl Pool { let standard_job_messages = downstream.safe_lock(|d| { let mut messages = Vec::new(); - // note: the fact that we're parsing a Vec from the config file is a - // bit of a hack so while we don't clean that up, we - // only set the value of the first output - let mut pool_coinbase_outputs = d.empty_pool_coinbase_outputs.clone(); - pool_coinbase_outputs[0].value = - Amount::from_sat(new_template.coinbase_tx_value_remaining); + let pool_coinbase_output = TxOut { + value: Amount::from_sat(new_template.coinbase_tx_value_remaining), + script_pubkey: d.coinbase_reward_script.script_pubkey(), + }; match new_template.future_template { true => { @@ -740,22 +721,21 @@ impl Pool { .write() .map_err(|e| Error::PoisonLock(e.to_string()))?; - // process the NewTemplate for the standard channel - // regardless of the REQUIRES_STANDARD_JOBS flag - // because this is the only way we can verify shares later - standard_channel - .on_new_template( - new_template.clone(), - pool_coinbase_outputs.clone(), - ) - .map_err(Error::FailedToProcessNewTemplateStandardChannel)?; - // did SetupConnection have the REQUIRES_STANDARD_JOBS flag set? // if yes, there's no group channel, so we need to send the future // job to each standard channel // if no, there's a group channel and there's no standard job to // send if d.group_channel.is_none() { + standard_channel + .on_new_template( + new_template.clone(), + vec![pool_coinbase_output.clone()], + ) + .map_err( + Error::FailedToProcessNewTemplateStandardChannel, + )?; + let standard_job_id = standard_channel .get_future_template_to_job_id() .get(&new_template.template_id) @@ -777,22 +757,21 @@ impl Pool { .write() .map_err(|e| Error::PoisonLock(e.to_string()))?; - // process the NewTemplate for the standard channel - // regardless of the REQUIRES_STANDARD_JOBS flag - // because this is the only way we can verify shares later - standard_channel - .on_new_template( - new_template.clone(), - pool_coinbase_outputs.clone(), - ) - .map_err(Error::FailedToProcessNewTemplateStandardChannel)?; - // did SetupConnection have the REQUIRES_STANDARD_JOBS flag set? // if yes, there's no group channel, so we need to send the // non-future job to each standard channel // if no, there is a group channel, so there's no standard job to // send if d.group_channel.is_none() { + standard_channel + .on_new_template( + new_template.clone(), + vec![pool_coinbase_output.clone()], + ) + .map_err( + Error::FailedToProcessNewTemplateStandardChannel, + )?; + let standard_job = standard_channel .get_active_job() .expect("standard job must exist"); @@ -815,12 +794,16 @@ impl Pool { } let extended_job_messages = downstream.safe_lock(|d| { - // note: the fact that we're parsing a Vec from the config file is a bit - // of a hack so while we don't clean that up, we only set - // the value of the first output - let mut pool_coinbase_outputs = d.empty_pool_coinbase_outputs.clone(); - pool_coinbase_outputs[0].value = - Amount::from_sat(new_template.coinbase_tx_value_remaining); + // if the downstream requires custom work, we don't need to send any extended + // jobs + if d.requires_custom_work { + return Ok(Vec::new()); + } + + let pool_coinbase_output = TxOut { + value: Amount::from_sat(new_template.coinbase_tx_value_remaining), + script_pubkey: d.coinbase_reward_script.script_pubkey(), + }; let mut messages = Vec::new(); match new_template.future_template { @@ -835,7 +818,7 @@ impl Pool { group_channel .on_new_template( new_template.clone(), - pool_coinbase_outputs.clone(), + vec![pool_coinbase_output.clone()], ) .map_err(|e| { Error::FailedToProcessNewTemplateGroupChannel(e) @@ -848,6 +831,21 @@ impl Pool { .get_future_jobs() .get(future_job_id) .expect("future job must exist"); + + // also update the standard channels states with the future job + // so they're able to validate shares later + for (_standard_channel_id, standard_channel_lock) in + d.standard_channels.iter() + { + let mut standard_channel = standard_channel_lock + .write() + .map_err(|e| Error::PoisonLock(e.to_string()))?; + + standard_channel + .on_group_channel_job(future_job.clone()) + .map_err(Error::FailedToProcessGroupChannelJob)?; + } + let future_job_message = future_job.get_job_message(); messages.push(future_job_message.clone().into_static()); } @@ -863,7 +861,7 @@ impl Pool { extended_channel .on_new_template( new_template.clone(), - pool_coinbase_outputs.clone(), + vec![pool_coinbase_output.clone()], ) .map_err(|e| { Error::FailedToProcessNewTemplateExtendedChannel(e) @@ -894,7 +892,7 @@ impl Pool { group_channel .on_new_template( new_template.clone(), - pool_coinbase_outputs.clone(), + vec![pool_coinbase_output.clone()], ) .map_err(|e| { Error::FailedToProcessNewTemplateGroupChannel(e) @@ -902,6 +900,20 @@ impl Pool { let active_job = group_channel .get_active_job() .expect("active job must exist"); + + // also update the standard channels states with the active job + // so they're able to validate shares later + for (_standard_channel_id, standard_channel_lock) in + d.standard_channels.iter() + { + let mut standard_channel = standard_channel_lock + .write() + .map_err(|e| Error::PoisonLock(e.to_string()))?; + + standard_channel + .on_group_channel_job(active_job.clone()) + .map_err(Error::FailedToProcessGroupChannelJob)?; + } let active_job_message = active_job.get_job_message(); messages.push(active_job_message.clone().into_static()); } @@ -917,7 +929,7 @@ impl Pool { extended_channel .on_new_template( new_template.clone(), - pool_coinbase_outputs.clone(), + vec![pool_coinbase_output.clone()], ) .map_err(|e| { Error::FailedToProcessNewTemplateExtendedChannel(e) @@ -980,12 +992,18 @@ impl Pool { shares_per_minute: f32, recv_stop_signal: tokio::sync::watch::Receiver<()>, ) -> Result>, PoolError> { - let range_0 = std::ops::Range { start: 0, end: 0 }; - - let pool_signature_len = config.pool_signature().len(); - let range_1_end = pool_signature_len + 8; + // range_1 is used for dynamically allocating extranonce_prefix across different channels + // from these 8 bytes, the first 2 bytes are statically defined by static_prefix + let range_1_start = 0; + let range_1_end = 8; + + // range_0 is not used here + let range_0 = std::ops::Range { + start: range_1_start, + end: range_1_start, + }; let range_1 = std::ops::Range { - start: 0, + start: range_1_start, end: range_1_end, }; let range_2 = std::ops::Range { @@ -993,30 +1011,29 @@ impl Pool { end: MAX_EXTRANONCE_LEN, }; - let pool_coinbase_outputs = get_coinbase_output(&config); - info!("PUB KEY: {:?}", pool_coinbase_outputs); + // simulating a scenario where there are multiple mining servers + // this static prefix allows unique extranonce_prefix allocation + // for this mining server + let static_prefix = config.server_id().to_be_bytes().to_vec(); + let extranonce_prefix_factory_extended = ExtendedExtranonce::new( range_0.clone(), range_1.clone(), range_2.clone(), - Some(config.pool_signature().as_bytes().to_vec()), + Some(static_prefix.clone()), ) .expect("Failed to create ExtendedExtranonce with valid ranges"); - let extranonce_prefix_factory_standard = ExtendedExtranonce::new( - range_0, - range_1, - range_2, - Some(config.pool_signature().as_bytes().to_vec()), - ) - .expect("Failed to create ExtendedExtranonce with valid ranges"); + let extranonce_prefix_factory_standard = + ExtendedExtranonce::new(range_0, range_1, range_2, Some(static_prefix.clone())) + .expect("Failed to create ExtendedExtranonce with valid ranges"); // --- Initialize Pool State --- let pool = Arc::new(Mutex::new(Pool { downstreams: HashMap::with_hasher(BuildNoHashHasher::default()), solution_sender, new_template_processed: false, - downstream_id_factory: IdFactory::new(), + downstream_id_factory: AtomicU32::new(0), status_tx: status_tx.clone(), extranonce_prefix_factory_extended: Arc::new(Mutex::new( extranonce_prefix_factory_extended, @@ -1027,25 +1044,19 @@ impl Pool { share_batch_size: config.share_batch_size(), last_future_template: None, last_new_prev_hash: None, + pool_tag_string: config.pool_signature().clone(), })); let cloned = pool.clone(); let cloned2 = pool.clone(); let cloned3 = pool.clone(); - let pool_coinbase_outputs = get_coinbase_output(&config); - info!("Starting up Pool server"); let status_tx_clone = status_tx.clone(); // Task to handle multiple downstream connection. - if let Err(e) = Self::accept_incoming_connection( - cloned, - config, - recv_stop_signal, - shares_per_minute, - pool_coinbase_outputs.expect("Invalid coinbase output in config"), - ) - .await + if let Err(e) = + Self::accept_incoming_connection(cloned, config, recv_stop_signal, shares_per_minute) + .await { error!("Pool stopped accepting connections due to: {}", &e); let _ = status_tx_clone @@ -1118,17 +1129,185 @@ impl Pool { } } +async fn send_set_target_downstream( + sender: Sender, + channel_id: u32, + target: Target, +) -> Result<(), PoolError> { + debug!("Attempting to send `SetTarget` for channel_id={channel_id}"); + + let target_message = SetTarget { + channel_id, + maximum_target: target.into(), + }; + + let mining_msg = Mining::SetTarget(target_message); + + info!("Sending SetTarget message to downstream: {}", mining_msg); + + let sv2_frame: StdFrame = AnyMessage::Mining(mining_msg).try_into()?; + + sender.send(sv2_frame.into()).await?; + + Ok(()) +} + +fn run_vardiff_on_extended_channel( + channel_id: u32, + channel: Arc>>>>, + vardiff_state: Arc>, + updates: &mut Vec<(u32, Target)>, +) { + let Ok(mut channel_state) = channel.write() else { + debug!("Failed to lock extended channel {channel_id}"); + return; + }; + + let Ok(mut vardiff_state) = vardiff_state.write() else { + debug!("Failed to lock vardiff state for extended channel {channel_id}"); + return; + }; + + let hashrate = channel_state.get_nominal_hashrate(); + let target = channel_state.get_target(); + let shares_per_minute = channel_state.get_shares_per_minute(); + + let Ok(new_hashrate_opt) = vardiff_state.try_vardiff(hashrate, target, shares_per_minute) + else { + debug!("Vardiff computation failed for extended channel {channel_id}"); + return; + }; + + if let Some(new_hashrate) = new_hashrate_opt { + match channel_state.update_channel(new_hashrate, None) { + Ok(()) => { + let updated_target = channel_state.get_target(); + updates.push((channel_id, updated_target.clone())); + + debug!( + "Updated target for extended channel channel_id={channel_id} to {:?}", + updated_target + ); + } + Err(e) => warn!( + "Failed to update extended channel channel_id={channel_id} during vardiff {e:?}" + ), + }; + } +} + +fn run_vardiff_on_standard_channel( + channel_id: u32, + channel: Arc>>>>, + vardiff_state: &Arc>, + updates: &mut Vec<(u32, Target)>, +) { + let Ok(mut channel_state) = channel.write() else { + debug!("Failed to lock standard channel {channel_id}"); + return; + }; + + let Ok(mut vardiff_state) = vardiff_state.write() else { + debug!("Failed to lock vardiff state for standard channel {channel_id}"); + return; + }; + + let hashrate = channel_state.get_nominal_hashrate(); + let target = channel_state.get_target(); + let shares_per_minute = channel_state.get_shares_per_minute(); + + let Ok(new_hashrate_opt) = vardiff_state.try_vardiff(hashrate, target, shares_per_minute) + else { + debug!("Vardiff computation failed for standard channel {channel_id}"); + return; + }; + + if let Some(new_hashrate) = new_hashrate_opt { + match channel_state.update_channel(new_hashrate, None) { + Ok(()) => { + let updated_target = channel_state.get_target(); + updates.push((channel_id, updated_target.clone())); + + debug!( + "Updated target for standard channel channel_id={channel_id} to {:?}", + updated_target + ); + } + Err(e) => warn!( + "Failed to update standard channel channel_id={channel_id} during vardiff {e:?}" + ), + }; + } +} + +/// This method implements the pool's variable difficulty logic for a single downstream. +/// A downstream can have multiple active channels connected to the pool. +/// Every 60 seconds, this method updates the difficulty state for each channel belonging to the +/// downstream. +async fn spawn_vardiff_loop( + downstream: Arc>, + sender: Sender, + downstream_id: u32, +) { + info!("Spawning vardiff adjustment loop for downstream: {downstream_id}"); + + 'vardiff_loop: loop { + if sender.is_closed() { + debug!("Downstream {downstream_id} closed, stopping vardiff loop"); + break; + } + + tokio::time::sleep(Duration::from_secs(60)).await; + + debug!("Starting vardiff updates for downstream: {downstream_id}"); + let mut updates = Vec::new(); + + _ = downstream.safe_lock(|d| { + for (channel_id, vardiff_state) in &d.vardiff { + if let Some(channel) = d.extended_channels.get(channel_id) { + run_vardiff_on_extended_channel( + *channel_id, + channel.clone(), + vardiff_state.clone(), + &mut updates, + ); + } + + if let Some(channel) = d.standard_channels.get(channel_id) { + run_vardiff_on_standard_channel( + *channel_id, + channel.clone(), + vardiff_state, + &mut updates, + ); + } + } + }); + + for (channel_id, target) in updates { + if let Err(e) = send_set_target_downstream(sender.clone(), channel_id, target).await { + error!( + "Failed to send SetTarget message downstream for channel {channel_id}: {:?}", + e + ); + break 'vardiff_loop; + } + } + } +} + #[cfg(test)] mod test { - use binary_sv2::{B0255, B064K}; use ext_config::{Config, File, FileFormat}; use std::convert::TryInto; - use tracing::error; - - use stratum_common::{ - bitcoin, - bitcoin::{absolute::LockTime, consensus, transaction::Version, Transaction, Witness}, + use stratum_common::roles_logic_sv2::{ + bitcoin::{ + self, absolute::LockTime, consensus, transaction::Version, Amount, Transaction, TxOut, + Witness, + }, + codec_sv2::binary_sv2::{B0255, B064K}, }; + use tracing::error; use super::PoolConfig; @@ -1163,10 +1342,9 @@ mod test { let _version = 536870912; let coinbase_tx_version = 2; let coinbase_tx_input_sequence = 4294967295; - let _coinbase_tx_value_remaining: u64 = 625000000; + let coinbase_tx_value_remaining: u64 = 0; let _coinbase_tx_outputs_count = 0; let coinbase_tx_locktime = 0; - let coinbase_tx_outputs: Vec = super::get_coinbase_output(&config).unwrap(); // extranonce len set to max_extranonce_size in `ChannelFactory::new_extended_channel()` let extranonce_len = 32; @@ -1187,11 +1365,16 @@ mod test { sequence: bitcoin::Sequence(coinbase_tx_input_sequence), witness, }; + + let coinbase_tx_output = TxOut { + value: Amount::from_sat(coinbase_tx_value_remaining), + script_pubkey: config.coinbase_reward_script().script_pubkey(), + }; let coinbase = Transaction { version: Version::non_standard(coinbase_tx_version), lock_time: LockTime::from_consensus(coinbase_tx_locktime), input: vec![tx_in], - output: coinbase_tx_outputs, + output: vec![coinbase_tx_output], }; let coinbase_tx_prefix = coinbase_tx_prefix(&coinbase, script_prefix_length); diff --git a/roles/pool/src/lib/mining_pool/setup_connection.rs b/roles/pool/src/lib/mining_pool/setup_connection.rs index 3c7d2869d7..b8a06e9603 100644 --- a/roles/pool/src/lib/mining_pool/setup_connection.rs +++ b/roles/pool/src/lib/mining_pool/setup_connection.rs @@ -9,18 +9,17 @@ use super::super::{ mining_pool::{EitherFrame, StdFrame}, }; use async_channel::{Receiver, Sender}; -use roles_logic_sv2::{ +use std::{convert::TryInto, net::SocketAddr, sync::Arc}; +use stratum_common::roles_logic_sv2::{ + self, common_messages_sv2::{ - has_requires_std_job, has_version_rolling, has_work_selection, SetupConnection, - SetupConnectionSuccess, + has_requires_std_job, has_work_selection, SetupConnection, SetupConnectionSuccess, }, - common_properties::CommonDownstreamData, errors::Error, handlers::common::ParseCommonMessagesFromDownstream, - parsers::{AnyMessage, CommonMessages}, + parsers_sv2::{AnyMessage, CommonMessages}, utils::Mutex, }; -use std::{convert::TryInto, net::SocketAddr, sync::Arc}; use tracing::{debug, error, info}; /// Handles the `SetupConnection` message for downstream connections. @@ -47,7 +46,7 @@ impl SetupConnectionHandler { receiver: &mut Receiver, sender: &mut Sender, address: SocketAddr, - ) -> PoolResult { + ) -> PoolResult<(bool, bool)> { // read stdFrame from receiver let mut incoming: StdFrame = match receiver.recv().await { @@ -91,11 +90,7 @@ impl SetupConnectionHandler { match message { CommonMessages::SetupConnectionSuccess(m) => { debug!("Sent back SetupConnectionSuccess: {:?}", m); - Ok(CommonDownstreamData { - header_only: has_requires_std_job(m.flags), - work_selection: has_work_selection(m.flags), - version_rolling: has_version_rolling(m.flags), - }) + Ok((has_requires_std_job(m.flags), has_work_selection(m.flags))) } _ => panic!(), } diff --git a/roles/pool/src/lib/mod.rs b/roles/pool/src/lib/mod.rs index e8872b8695..e9fd062f0d 100644 --- a/roles/pool/src/lib/mod.rs +++ b/roles/pool/src/lib/mod.rs @@ -13,12 +13,18 @@ pub mod template_receiver; use async_channel::{bounded, unbounded}; use config::PoolConfig; use error::PoolError; -use mining_pool::{get_coinbase_output, Pool}; +use mining_pool::Pool; use std::sync::{Arc, Mutex}; +use stratum_common::roles_logic_sv2::bitcoin::{ + absolute::LockTime, + blockdata::witness::Witness, + script::ScriptBuf, + transaction::{OutPoint, Transaction, Version}, + Amount, Sequence, TxIn, TxOut, +}; use template_receiver::TemplateRx; use tokio::select; use tracing::{error, info, warn}; - /// Represents the PoolSv2 instance, which manages the pool's operations. /// /// This struct holds the pool configuration and provides functionality to start @@ -71,13 +77,30 @@ impl PoolSv2 { let (s_message_recv_signal, r_message_recv_signal) = bounded(10); // Prepare coinbase output information required by TemplateRx. - let coinbase_output_result = get_coinbase_output(&config)?; - let coinbase_output_len = coinbase_output_result.len() as u32; + // We use an empty output here only for calculation of the size and sigops of the coinbase + // output. We still don't know the template revenue. + let empty_coinbase_output = TxOut { + value: Amount::from_sat(0), + script_pubkey: config.coinbase_reward_script().script_pubkey(), + }; + let coinbase_output_len = empty_coinbase_output.size() as u32; let tp_authority_public_key = config.tp_authority_public_key().cloned(); - let coinbase_output_sigops = coinbase_output_result - .iter() - .map(|output| output.script_pubkey.count_sigops() as u16) - .sum::(); + + // create a dummy coinbase transaction with the empty output + // this is used to calculate the sigops of the coinbase output + let dummy_coinbase = Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint::null(), + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::from(vec![vec![0; 32]]), + }], + output: vec![empty_coinbase_output], + }; + + let coinbase_output_sigops = dummy_coinbase.total_sigop_cost(|_| None) as u16; // --- Spawn Template Receiver Task --- let tp_address = config.tp_address().clone(); @@ -206,39 +229,12 @@ impl PoolSv2 { mod tests { use super::*; use ext_config::{Config, File, FileFormat}; - - #[tokio::test] - async fn pool_bad_coinbase_output() { - let invalid_coinbase_output = vec![config_helpers::CoinbaseOutput::new( - "P2PK".to_string(), - "wrong".to_string(), - )]; - let config_path = "config-examples/pool-config-hosted-tp-example.toml"; - let mut config: PoolConfig = match Config::builder() - .add_source(File::new(config_path, FileFormat::Toml)) - .build() - { - Ok(settings) => match settings.try_deserialize::() { - Ok(c) => c, - Err(e) => { - error!("Failed to deserialize config: {}", e); - return; - } - }, - Err(e) => { - error!("Failed to build config: {}", e); - return; - } - }; - config.set_coinbase_outputs(invalid_coinbase_output); - let pool = PoolSv2::new(config); - let result = pool.start().await; - assert!(result.is_err()); - } + use integration_tests_sv2::template_provider::DifficultyLevel; #[tokio::test] async fn shutdown_pool() { - let template_provider = integration_tests_sv2::start_template_provider(None); + let template_provider = + integration_tests_sv2::start_template_provider(None, DifficultyLevel::Low); let config_path = "config-examples/pool-config-local-tp-example.toml"; let mut config: PoolConfig = match Config::builder() .add_source(File::new(config_path, FileFormat::Toml)) diff --git a/roles/pool/src/lib/status.rs b/roles/pool/src/lib/status.rs index 58130c0cab..6e7a78927a 100644 --- a/roles/pool/src/lib/status.rs +++ b/roles/pool/src/lib/status.rs @@ -6,7 +6,7 @@ //! Centralizes and simplifies error handling across the system. /// Identifies which component sent a status update. -use roles_logic_sv2::parsers::Mining; +use stratum_common::roles_logic_sv2::{self, parsers_sv2::Mining}; use super::error::PoolError; @@ -167,5 +167,9 @@ pub async fn handle_error(sender: &Sender, e: PoolError) -> error_handling::Erro PoolError::Sv2ProtocolError(_) => { send_status(sender, e, error_handling::ErrorBranch::Break).await } + PoolError::Vardiff(_) => { + send_status(sender, e, error_handling::ErrorBranch::Continue).await + } + PoolError::Parser(_) => send_status(sender, e, error_handling::ErrorBranch::Break).await, } } diff --git a/roles/pool/src/lib/template_receiver/message_handler.rs b/roles/pool/src/lib/template_receiver/message_handler.rs index 2d611bf1e8..ba57446f08 100644 --- a/roles/pool/src/lib/template_receiver/message_handler.rs +++ b/roles/pool/src/lib/template_receiver/message_handler.rs @@ -3,14 +3,14 @@ //! Handles incoming template distribution messages from the Template Provider and forwards them //! as needed. use super::TemplateRx; -use roles_logic_sv2::{ +use std::sync::Arc; +use stratum_common::roles_logic_sv2::{ errors::Error, handlers::template_distribution::{ParseTemplateDistributionMessagesFromServer, SendTo}, - parsers::TemplateDistribution, + parsers_sv2::TemplateDistribution, template_distribution_sv2::*, utils::Mutex, }; -use std::sync::Arc; use tracing::{debug, error, info}; impl ParseTemplateDistributionMessagesFromServer for TemplateRx { @@ -20,7 +20,7 @@ impl ParseTemplateDistributionMessagesFromServer for TemplateRx { "Received NewTemplate with id: {}, is future: {}", m.template_id, m.future_template ); - debug!("NewTemplate: {:?}", m); + debug!("NewTemplate: {}", m); let new_template = TemplateDistribution::NewTemplate(m.into_static()); Ok(SendTo::RelayNewMessageToRemote( Arc::new(Mutex::new(())), @@ -31,7 +31,7 @@ impl ParseTemplateDistributionMessagesFromServer for TemplateRx { // Handles a `SetNewPrevHash` and return `RelayNewMessageToRemote` fn handle_set_new_prev_hash(&mut self, m: SetNewPrevHash) -> Result { info!("Received SetNewPrevHash for template: {}", m.template_id); - debug!("SetNewPrevHash: {:?}", m); + debug!("SetNewPrevHash: {}", m); let new_prev_hash = TemplateDistribution::SetNewPrevHash(m.into_static()); Ok(SendTo::RelayNewMessageToRemote( Arc::new(Mutex::new(())), @@ -52,7 +52,7 @@ impl ParseTemplateDistributionMessagesFromServer for TemplateRx { "Received RequestTransactionDataSuccess for template: {}", m.template_id ); - debug!("RequestTransactionDataSuccess: {:?}", m); + debug!("RequestTransactionDataSuccess: {}", m); // Just ignore tx data messages this are meant for the declarators Ok(SendTo::None(None)) } diff --git a/roles/pool/src/lib/template_receiver/mod.rs b/roles/pool/src/lib/template_receiver/mod.rs index 30c3014d9d..45ea1bff46 100644 --- a/roles/pool/src/lib/template_receiver/mod.rs +++ b/roles/pool/src/lib/template_receiver/mod.rs @@ -13,19 +13,22 @@ use super::{ status, }; use async_channel::{Receiver, Sender}; -use codec_sv2::{HandshakeRole, Initiator}; use error_handling::handle_result; use key_utils::Secp256k1PublicKey; -use network_helpers_sv2::noise_connection::Connection; -use roles_logic_sv2::{ - handlers::template_distribution::ParseTemplateDistributionMessagesFromServer, - parsers::{AnyMessage, TemplateDistribution}, - template_distribution_sv2::{ - CoinbaseOutputConstraints, NewTemplate, SetNewPrevHash, SubmitSolution, +use std::{convert::TryInto, net::SocketAddr, sync::Arc}; +use stratum_common::{ + network_helpers_sv2::noise_connection::Connection, + roles_logic_sv2::{ + self, codec_sv2, + codec_sv2::{HandshakeRole, Initiator}, + handlers::template_distribution::ParseTemplateDistributionMessagesFromServer, + parsers_sv2::{AnyMessage, TemplateDistribution}, + template_distribution_sv2::{ + CoinbaseOutputConstraints, NewTemplate, SetNewPrevHash, SubmitSolution, + }, + utils::Mutex, }, - utils::Mutex, }; -use std::{convert::TryInto, net::SocketAddr, sync::Arc}; use tokio::{net::TcpStream, task}; use tracing::{info, warn}; @@ -218,7 +221,7 @@ impl TemplateRx { async fn on_new_solution(self_: Arc>, rx: Receiver>) { let status_tx = self_.safe_lock(|s| s.status_tx.clone()).unwrap(); while let Ok(solution) = rx.recv().await { - info!("Sending Solution to TP: {:?}", &solution); + info!("Sending Solution to TP: {}", &solution); let sv2_frame_res: Result = AnyMessage::TemplateDistribution(TemplateDistribution::SubmitSolution(solution)) .try_into(); diff --git a/roles/pool/src/lib/template_receiver/setup_connection.rs b/roles/pool/src/lib/template_receiver/setup_connection.rs index 59561dd933..556576b06c 100644 --- a/roles/pool/src/lib/template_receiver/setup_connection.rs +++ b/roles/pool/src/lib/template_receiver/setup_connection.rs @@ -8,14 +8,15 @@ use super::super::{ mining_pool::{EitherFrame, StdFrame}, }; use async_channel::{Receiver, Sender}; -use roles_logic_sv2::{ +use std::{convert::TryInto, net::SocketAddr, sync::Arc}; +use stratum_common::roles_logic_sv2::{ + self, codec_sv2, common_messages_sv2::{Protocol, Reconnect, SetupConnection, SetupConnectionError}, errors::Error, handlers::common::{ParseCommonMessagesFromUpstream, SendTo}, - parsers::{AnyMessage, CommonMessages}, + parsers_sv2::{AnyMessage, CommonMessages}, utils::Mutex, }; -use std::{convert::TryInto, net::SocketAddr, sync::Arc}; use tracing::{error, info}; /// Handles the connection setup process with the Template Provider. diff --git a/roles/pool/src/main.rs b/roles/pool/src/main.rs index 3b380be56d..78920debef 100644 --- a/roles/pool/src/main.rs +++ b/roles/pool/src/main.rs @@ -11,12 +11,13 @@ use tracing::{error, info}; mod args; use args::process_cli_args; +use config_helpers_sv2::logging::init_logging; /// Initializes logging, parses arguments, loads configuration, and starts the Pool runtime. #[tokio::main] async fn main() { - tracing_subscriber::fmt::init(); let config = process_cli_args(); + init_logging(config.log_dir()); let _ = PoolSv2::new(config).start().await; select! { interrupt_signal = tokio::signal::ctrl_c() => { diff --git a/roles/roles-utils/config-helpers/Cargo.toml b/roles/roles-utils/config-helpers/Cargo.toml index 3b6858a2ec..fc841e46aa 100644 --- a/roles/roles-utils/config-helpers/Cargo.toml +++ b/roles/roles-utils/config-helpers/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "config-helpers" +name = "config_helpers_sv2" authors = ["The Stratum V2 Developers"] version = "0.1.0" -edition = "2018" +edition = "2021" description = "Helpers for working with Stratum V2 configuration files" -documentation = "" +documentation = "https://docs.rs/config_helpers_sv2" readme = "README.md" homepage = "https://stratumprotocol.org" repository = "https://github.com/stratum-mining/stratum" @@ -13,5 +13,6 @@ keywords = ["stratum", "mining", "bitcoin", "protocol"] [dependencies] serde = { version = "1.0.89", features = ["derive","alloc"], default-features = false } -miniscript = { version = "12.3.2", default-features = false, features = [ "no-std" ] } -roles_logic_sv2 = { path = "../../../protocols/v2/roles-logic-sv2" } +miniscript = { version = "12.3.4", default-features = false, features = [ "no-std" ] } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing = { version = "0.1" } diff --git a/roles/roles-utils/config-helpers/src/coinbase_output/errors.rs b/roles/roles-utils/config-helpers/src/coinbase_output/errors.rs index 19e9c6e0bd..e3c1492b30 100644 --- a/roles/roles-utils/config-helpers/src/coinbase_output/errors.rs +++ b/roles/roles-utils/config-helpers/src/coinbase_output/errors.rs @@ -1,23 +1,65 @@ use core::fmt; +use miniscript::bitcoin::{address, hex}; + /// Error enum #[derive(Debug)] pub enum Error { - /// Empty coinbase outputs in config - EmptyCoinbaseOutputs, + /// Error parsing a Bitcoin address + Address(address::ParseError), + // TODO rust-miniscript 13 will have functions to do these checks for us so we don't + // need to pollute our own error enum with this fiddly stuff + /// addr() descriptor did not have exactly 1 child + AddrDescriptorNChildren(usize), + /// raw() descriptor child did not have 0 children + AddrDescriptorGrandchild, + /// raw() descriptor did not have exactly 1 child + RawDescriptorNChildren(usize), + /// addr() descriptor child did not have 0 children + RawDescriptorGrandchild, + /// Error parsing a raw descriptor as hex. + Hex(hex::HexToBytesError), /// Invalid `output_script_value` for script type. It must be a valid public key/script InvalidOutputScript, /// Unknown script type in config UnknownOutputScriptType, + /// Error from the `miniscript` crate. + Miniscript(miniscript::Error), } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use Error::*; match self { - EmptyCoinbaseOutputs => write!(f, "Empty coinbase outputs in config"), + Address(ref e) => write!(f, "Bitcoin address: {e}"), + AddrDescriptorNChildren(0) => write!(f, "Found addr() descriptor with no address"), + AddrDescriptorNChildren(n) => write!(f, "Found addr() descriptor with {n} children; must be exactly one valid address"), + AddrDescriptorGrandchild => write!(f, "Found descriptor of the form addr(X(y)); X must be a valid address and have no subexpression"), + RawDescriptorNChildren(0) => write!(f, "Found raw() descriptor with no hex-encoded script"), + RawDescriptorNChildren(n) => write!(f, "Found raw() descriptor with {n} children; must be exactly one hex-encoded script"), + RawDescriptorGrandchild => write!(f, "Found descriptor of the form raw(X(y)); X must be a hex-encoded script and have no subexpression"), + Hex(ref e) => write!(f, "Decoding hex-formatted script: {e}"), UnknownOutputScriptType => write!(f, "Unknown script type in config"), InvalidOutputScript => write!(f, "Invalid output_script_value for your script type. It must be a valid public key/script"), + Miniscript(ref e) => write!(f, "Miniscript: {e}"), } } } + +impl From for Error { + fn from(e: address::ParseError) -> Self { + Error::Address(e) + } +} + +impl From for Error { + fn from(e: hex::HexToBytesError) -> Self { + Error::Hex(e) + } +} + +impl From for Error { + fn from(e: miniscript::Error) -> Self { + Error::Miniscript(e) + } +} diff --git a/roles/roles-utils/config-helpers/src/coinbase_output/mod.rs b/roles/roles-utils/config-helpers/src/coinbase_output/mod.rs index 2d0a0a4567..d4657a3530 100644 --- a/roles/roles-utils/config-helpers/src/coinbase_output/mod.rs +++ b/roles/roles-utils/config-helpers/src/coinbase_output/mod.rs @@ -1,10 +1,9 @@ mod errors; +mod serde_types; -use core::convert::TryFrom; - -use miniscript::bitcoin::{ - secp256k1::{All, Secp256k1}, - PublicKey, ScriptBuf, ScriptHash, WScriptHash, XOnlyPublicKey, +use miniscript::{ + bitcoin::{address::NetworkUnchecked, hex::FromHex as _, Address, Network, ScriptBuf}, + DefiniteDescriptorKey, Descriptor, }; pub use errors::Error; @@ -13,108 +12,366 @@ pub use errors::Error; /// /// Typically used for parsing coinbase outputs defined in SRI role configuration files. #[derive(Debug, serde::Deserialize, Clone)] -pub struct CoinbaseOutput { - /// Specifies type of the script used in the output. - /// - /// Supported values include: - /// - `"P2PK"`: Pay-to-Public-Key - /// - `"P2PKH"`: Pay-to-Public-Key-Hash - /// - `"P2SH"`: Pay-to-Script-Hash - /// - `"P2WPKH"`: Pay-to-Witness-Public-Key-Hash - /// - `"P2WSH"`: Pay-to-Witness-Script-Hash - /// - `"P2TR"`: Pay-to-Taproot - pub output_script_type: String, - - /// Value associated with the script, typically a public key or script hash. - /// - /// This field's interpretation depends on the `output_script_type`: - /// - For `"P2PK"`: The raw public key. - /// - For `"P2PKH"`: A public key hash. - /// - For `"P2WPKH"`: A witness public key hash. - /// - For `"P2SH"`: A script hash. - /// - For `"P2WSH"`: A witness script hash. - /// - For `"P2TR"`: An x-only public key. - pub output_script_value: String, +#[serde(try_from = "serde_types::SerdeCoinbaseOutput")] +pub struct CoinbaseRewardScript { + script_pubkey: ScriptBuf, + ok_for_mainnet: bool, } -impl CoinbaseOutput { - /// Creates a new [`CoinbaseOutput`]. - pub fn new(output_script_type: String, output_script_value: String) -> Self { - Self { - output_script_type, - output_script_value, +impl CoinbaseRewardScript { + /// Creates a new [`CoinbaseRewardScript`] from a descriptor string. + pub fn from_descriptor(mut s: &str) -> Result { + // Taproot descriptors cannot be parsed with `expression::Tree::from_str` and + // need special handling. So we special-case them early and just pass to + // rust-miniscript. In Miniscript 13 we will not need to do this. + if s.starts_with("tr") { + let desc = s.parse::>()?; + return Ok(Self { + script_pubkey: desc.script_pubkey(), + // Descriptors don't have a way to specify a network, so we assume + // they are OK to be used on mainnet. + ok_for_mainnet: true, + }); } - } -} -impl TryFrom for ScriptBuf { - type Error = Error; - - fn try_from(value: CoinbaseOutput) -> Result { - match value.output_script_type.as_str() { - "TEST" => { - let pub_key_hash = value - .output_script_value - .parse::() - .map_err(|_| Error::InvalidOutputScript)? - .pubkey_hash(); - Ok(ScriptBuf::new_p2pkh(&pub_key_hash)) - } - "P2PK" => { - let pub_key = value - .output_script_value - .parse::() - .map_err(|_| Error::InvalidOutputScript)?; - Ok(ScriptBuf::new_p2pk(&pub_key)) - } - "P2PKH" => { - let pub_key_hash = value - .output_script_value - .parse::() - .map_err(|_| Error::InvalidOutputScript)? - .pubkey_hash(); - Ok(ScriptBuf::new_p2pkh(&pub_key_hash)) - } - "P2WPKH" => { - let w_pub_key_hash = value - .output_script_value - .parse::() - .map_err(|_| Error::InvalidOutputScript)? - .wpubkey_hash() - .unwrap(); - Ok(ScriptBuf::new_p2wpkh(&w_pub_key_hash)) + // Manually verify the checksum. FIXME in Miniscript 13 we will not need + // to do this, since `expression::Tree::from_str` will do the checksum + // validation for us. (And yield a much less horrible error type.) + if let Some((desc_str, checksum_str)) = s.rsplit_once('#') { + let expected_sum = miniscript::descriptor::checksum::desc_checksum(desc_str)?; + if checksum_str != expected_sum { + return Err(miniscript::Error::BadDescriptor(format!( + "Invalid checksum '{checksum_str}', expected '{expected_sum}'" + )) + .into()); } - "P2SH" => { - let script_hashed = value - .output_script_value - .parse::() - .map_err(|_| Error::InvalidOutputScript)?; - Ok(ScriptBuf::new_p2sh(&script_hashed)) + s = desc_str; + } + + let tree = miniscript::expression::Tree::from_str(s)?; + match tree.name { + "addr" => { + // In rust-miniscript 13 these can be replaced with a call to + // TreeIterItem::verify_toplevel which will these checks for us + // in a uniform way. + if tree.args.len() != 1 { + return Err(Error::AddrDescriptorNChildren(tree.args.len())); + } + if !tree.args[0].args.is_empty() { + return Err(Error::AddrDescriptorGrandchild); + } + + let addr = tree.args[0].name.parse::>()?; + Ok(Self { + script_pubkey: addr.assume_checked_ref().script_pubkey(), + ok_for_mainnet: addr.is_valid_for_network(Network::Bitcoin), + }) } - "P2WSH" => { - let w_script_hashed = value - .output_script_value - .parse::() - .map_err(|_| Error::InvalidOutputScript)?; - Ok(ScriptBuf::new_p2wsh(&w_script_hashed)) + "raw" => { + // In rust-miniscript 13 these can be replaced with a call to + // TreeIterItem::verify_toplevel which will these checks for us + // in a uniform way. + if tree.args.len() != 1 { + return Err(Error::RawDescriptorNChildren(tree.args.len())); + } + if !tree.args[0].args.is_empty() { + return Err(Error::RawDescriptorGrandchild); + } + + let bytes = Vec::::from_hex(tree.args[0].name)?; + Ok(Self { + script_pubkey: ScriptBuf::from(bytes), + // Users of hex scriptpubkeys are on their own. + ok_for_mainnet: true, + }) } - "P2TR" => { - // From the bip - // - // Conceptually, every Taproot output corresponds to a combination of - // a single public key condition (the internal key), - // and zero or more general conditions encoded in scripts organized in a tree. - let pub_key = value - .output_script_value - .parse::() - .map_err(|_| Error::InvalidOutputScript)?; - Ok(ScriptBuf::new_p2tr::( - &Secp256k1::::new(), - pub_key, - None, - )) + _ => { + let desc = s.parse::>()?; + Ok(Self { + script_pubkey: desc.script_pubkey(), + // Descriptors don't have a way to specify a network, so we assume + // they are OK to be used on mainnet. + ok_for_mainnet: true, + }) } - _ => Err(Error::UnknownOutputScriptType), } } + + /// Whether this coinbase output is okay for use on mainnet. + /// + /// This is a "best effort" check and currently only returns false if the user + /// provides an addr() descriptor in which they specified a testnet or regtest + /// address. + pub fn ok_for_mainnet(&self) -> bool { + self.ok_for_mainnet + } + + /// The `scriptPubKey` associated with the coinbase output + pub fn script_pubkey(&self) -> ScriptBuf { + self.script_pubkey.clone() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn fixed_vector_addr() { + // Valid + assert_eq!( + CoinbaseRewardScript::from_descriptor( + "addr(1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2)#wdnlkpe8" + ) + .unwrap() + .script_pubkey() + .to_hex_string(), + "76a91477bff20c60e522dfaa3350c39b030a5d004e839a88ac", + ); + assert_eq!( + CoinbaseRewardScript::from_descriptor( + "addr(3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy)#rsjl0crt" + ) + .unwrap() + .script_pubkey() + .to_hex_string(), + "a914b472a266d0bd89c13706a4132ccfb16f7c3b9fcb87", + ); + assert_eq!( + CoinbaseRewardScript::from_descriptor( + "addr(bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4)#uyjndxcw" + ) + .unwrap() + .script_pubkey() + .to_hex_string(), + "0014751e76e8199196d454941c45d1b3a323f1433bd6", + ); + assert_eq!( + CoinbaseRewardScript::from_descriptor( + "addr(bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3)#8kzm8txf" + ) + .unwrap() + .script_pubkey() + .to_hex_string(), + "00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262", + ); + // no checksum is ok + assert_eq!( + CoinbaseRewardScript::from_descriptor("addr(1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2)") + .unwrap() + .script_pubkey() + .to_hex_string(), + "76a91477bff20c60e522dfaa3350c39b030a5d004e839a88ac", + ); + assert_eq!( + CoinbaseRewardScript::from_descriptor("addr(1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2,)") + .unwrap_err() + .to_string(), + "Found addr() descriptor with 2 children; must be exactly one valid address", + ); + + // Invalid + // But empty checksum is not (in Miniscript 13 these error messages will be cleaner) + assert_eq!( + CoinbaseRewardScript::from_descriptor("addr(1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2)#") + .unwrap_err() + .to_string(), + "Miniscript: Invalid descriptor: Invalid checksum '', expected 'wdnlkpe8'", + ); + assert_eq!( + CoinbaseRewardScript::from_descriptor( + "addr(1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2)#wdnlkpe7" + ) + .unwrap_err() + .to_string(), + "Miniscript: Invalid descriptor: Invalid checksum 'wdnlkpe7', expected 'wdnlkpe8'", + ); + // Bad base58ck checksum even though the descriptor checksum is OK. Note that rust-bitcoin + // 0.32 interprets bad bech32 checksums as "base58 errors" because it doesn't know + // what encoding an invalid string is supposed to have. See https://github.com/rust-bitcoin/rust-bitcoin/issues/3044 + // Expected error: "Bitcoin address: base58 error: incorrect checksum: base58 checksum + // 0x6c7615f4 does not match expected 0x6b7615f4" (hex-conservative v0.3.0) + // or "Bitcoin address: base58 error" (hex-conservative v0.2.1) + assert!(CoinbaseRewardScript::from_descriptor( + "addr(1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN3)#5v55uzec" + ) + .is_err()); + // Expected error: "Bitcoin address: base58 error: decode: invalid base58 character 0x30" + // (hex-conservative v0.3.0) or "Bitcoin address: base58 error" (hex-conservative + // v0.2.1) + assert!(CoinbaseRewardScript::from_descriptor( + "addr(bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t3)#wfr7lfxf" + ) + .is_err()); + // Flagrantly bad stuff -- should probably PR these upstream to rust-miniscript. + // Expected error: "Bitcoin address: base58 error: too short: base58 decoded data was not + // long enough, must be at least 4 byte: 0" (hex-conservative v0.3.0) or "Bitcoin + // address: base58 error" (hex-conservative v0.2.1) + assert!(CoinbaseRewardScript::from_descriptor("addr()").is_err()); + assert_eq!( + CoinbaseRewardScript::from_descriptor("addr(It's a mad mad world!?! 🙃)") + .unwrap_err() + .to_string(), + "Miniscript: unprintable character 0xf0", + ); + // This error is just wrong lol. Fixed in Miniscript 13. + assert_eq!( + CoinbaseRewardScript::from_descriptor("addr(It's a mad mad world!?! 🙃)#abcdefg") + .unwrap_err() + .to_string(), + "Miniscript: Invalid descriptor: Invalid character in checksum: '🙃'", + ); + // Expected error: "Bitcoin address: base58 error: decode: invalid base58 character 0x49" + // (hex-conservative v0.3.0) or "Bitcoin address: base58 error" (hex-conservative + // v0.2.1) + assert!( + CoinbaseRewardScript::from_descriptor("addr(It's a mad mad world!?!)#hmeprl29") + .is_err() + ); + assert_eq!( + CoinbaseRewardScript::from_descriptor("addr(It's a mad mad world!?!)#🙃🙃🙃🙃🙃🙃") + .unwrap_err() + .to_string(), + "Miniscript: Invalid descriptor: Invalid checksum '🙃🙃🙃🙃🙃🙃', expected 'hmeprl29'", + ); + } + + #[test] + fn fixed_vector_combo() { + // We do not support combo descriptors. Nobody should. + assert_eq!( + CoinbaseRewardScript::from_descriptor( + "combo(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)" + ) + .unwrap_err() + .to_string(), + "Miniscript: unexpected «combo(1 args) while parsing Miniscript»" + ); + } + + #[test] + fn fixed_vector_musig() { + // We do not support musig descriptors. One day. + assert_eq!( + CoinbaseRewardScript::from_descriptor("musig(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556)").unwrap_err().to_string(), + "Miniscript: unexpected «musig(2 args) while parsing Miniscript»" + ); + assert_eq!( + CoinbaseRewardScript::from_descriptor("tr(musig(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556))").unwrap_err().to_string(), + "Miniscript: expected )", + ); + } + + #[test] + fn fixed_vector_raw() { + // Empty raw descriptors are OK; correspond to the empty script. + assert_eq!( + CoinbaseRewardScript::from_descriptor("raw()") + .unwrap() + .script_pubkey() + .to_hex_string(), + "", + ); + assert_eq!( + CoinbaseRewardScript::from_descriptor("raw(deadbeef)") + .unwrap() + .script_pubkey() + .to_hex_string(), + "deadbeef", + ); + assert_eq!( + CoinbaseRewardScript::from_descriptor("raw(DEADBEEF)") + .unwrap() + .script_pubkey() + .to_hex_string(), + "deadbeef", + ); + // Should we allow this? We do, so I guess we should test it and make sure we don't stop.. + assert_eq!( + CoinbaseRewardScript::from_descriptor("raw(DEADbeef)") + .unwrap() + .script_pubkey() + .to_hex_string(), + "deadbeef", + ); + // Expected error: "Decoding hex-formatted script: odd length, failed to create bytes from + // hex: odd hex string length 1" (hex-conservative v0.3.0) or "Decoding + // hex-formatted script: odd length, failed to create bytes from hex" (hex-conservative + // v0.2.1) + assert!(CoinbaseRewardScript::from_descriptor("raw(0)").is_err()); + assert_eq!( + CoinbaseRewardScript::from_descriptor("raw(0,1)") + .unwrap_err() + .to_string(), + "Found raw() descriptor with 2 children; must be exactly one hex-encoded script", + ); + } + + #[test] + fn fixed_vector_miniscript() { + assert_eq!( + CoinbaseRewardScript::from_descriptor("sh(wsh(multi(2,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556)))#qpcmf2lu").unwrap().script_pubkey().to_hex_string(), + "a9141cb55de50b72c67709ab16307d69557e6bb1a98787", + ); + assert_eq!( + CoinbaseRewardScript::from_descriptor( + "tr(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)" + ) + .unwrap() + .script_pubkey() + .to_hex_string(), + "5120da4710964f7852695de2da025290e24af6d8c281de5a0b902b7135fd9fd74d21", + ); + assert_eq!( + CoinbaseRewardScript::from_descriptor("tr(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,{pk(03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556),{multi_a(2,026a245bf6dc698504c89a20cfded60853152b695336c28063b61c65cbd269e6b4,0231ecbfac95d972f0b8f81ec6e01e9c621d91a4b48d5f9d12d7e95febe9f34d64),multi_a(2,026a245bf6dc698504c89a20cfded60853152b695336c28063b61c65cbd269e6b4,0231ecbfac95d972f0b8f81ec6e01e9c621d91a4b48d5f9d12d7e95febe9f34d64)}})") + .unwrap() + .script_pubkey() + .to_hex_string(), + "5120493bdae0d225af5cb88c4cb2a1e1e89e391153ba7699c91ebee2fd082ed1636c", + ); + } + + #[test] + fn fixed_vector_keys() { + // xpub + assert_eq!( + CoinbaseRewardScript::from_descriptor("pkh(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8)").unwrap().script_pubkey().to_hex_string(), + "76a9143442193e1bb70916e914552172cd4e2dbc9df81188ac", + ); + // xpub with non-hardened path + assert_eq!( + CoinbaseRewardScript::from_descriptor("pkh(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/1/2/3)").unwrap().script_pubkey().to_hex_string(), + "76a914f2d2e1401c88353c2298d1a928d4ed827ff46ff688ac", + ); + // xpub with hardened path (not allowed) + assert_eq!( + CoinbaseRewardScript::from_descriptor("pkh(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/1'/2/3)").unwrap_err().to_string(), + "Miniscript: unexpected «cannot parse multi-path keys, keys with a wildcard or keys with hardened derivation steps as a DerivedDescriptorKey»", + ); + // no wildcards allowed (at least for now; gmax thinks it would be cool if we would + // instantiate it with the blockheight or something, but need to work out UX) + assert_eq!( + CoinbaseRewardScript::from_descriptor("pkh(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/*)").unwrap_err().to_string(), + "Miniscript: unexpected «cannot parse multi-path keys, keys with a wildcard or keys with hardened derivation steps as a DerivedDescriptorKey»", + ); + // No multipath descriptors allowed; this is not a wallet with change + assert_eq!( + CoinbaseRewardScript::from_descriptor("pkh(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/<0;1>)").unwrap_err().to_string(), + "Miniscript: unexpected «cannot parse multi-path keys, keys with a wildcard or keys with hardened derivation steps as a DerivedDescriptorKey»", + ); + // Private keys are not allowed, or xprvs. + assert_eq!( + CoinbaseRewardScript::from_descriptor( + "pkh(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)" + ) + .unwrap_err() + .to_string(), + "Miniscript: unexpected «Key too short (<66 char), doesn't match any format»", + ); + // This is a confusing error message which should be fixed in Miniscript 13. + assert_eq!( + CoinbaseRewardScript::from_descriptor("pkh(xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi)").unwrap_err().to_string(), + "Miniscript: unexpected «Public keys must be 64/66/130 characters in size»", + ); + } } diff --git a/roles/roles-utils/config-helpers/src/coinbase_output/serde_types.rs b/roles/roles-utils/config-helpers/src/coinbase_output/serde_types.rs new file mode 100644 index 0000000000..d0399fef92 --- /dev/null +++ b/roles/roles-utils/config-helpers/src/coinbase_output/serde_types.rs @@ -0,0 +1,136 @@ +use core::convert::TryFrom; +use miniscript::bitcoin::{ + secp256k1::{All, Secp256k1}, + PublicKey, ScriptBuf, ScriptHash, WScriptHash, XOnlyPublicKey, +}; + +use super::Error; + +#[derive(serde::Deserialize)] +pub(super) struct LegacyCoinbaseOutput { + /// Specifies type of the script used in the output. + /// + /// Supported values include: + /// - `"P2PK"`: Pay-to-Public-Key + /// - `"P2PKH"`: Pay-to-Public-Key-Hash + /// - `"P2SH"`: Pay-to-Script-Hash + /// - `"P2WPKH"`: Pay-to-Witness-Public-Key-Hash:w + + /// - `"P2WSH"`: Pay-to-Witness-Script-Hash + /// - `"P2TR"`: Pay-to-Taproot + pub(super) output_script_type: String, + + /// Value associated with the script, typically a public key or script hash. + /// + /// This field's interpretation depends on the `output_script_type`: + /// - For `"P2PK"`: The raw public key. + /// - For `"P2PKH"`: A public key hash. + /// - For `"P2WPKH"`: A witness public key hash. + /// - For `"P2SH"`: A script hash. + /// - For `"P2WSH"`: A witness script hash. + /// - For `"P2TR"`: An x-only public key. + pub(super) output_script_value: String, +} + +impl TryFrom for super::CoinbaseRewardScript { + type Error = super::Error; + fn try_from(value: LegacyCoinbaseOutput) -> Result { + let script_pubkey = match value.output_script_type.as_str() { + "TEST" => { + let pub_key_hash = value + .output_script_value + .parse::() + .map_err(|_| Error::InvalidOutputScript)? + .pubkey_hash(); + ScriptBuf::new_p2pkh(&pub_key_hash) + } + "P2PK" => { + let pub_key = value + .output_script_value + .parse::() + .map_err(|_| Error::InvalidOutputScript)?; + ScriptBuf::new_p2pk(&pub_key) + } + "P2PKH" => { + let pub_key_hash = value + .output_script_value + .parse::() + .map_err(|_| Error::InvalidOutputScript)? + .pubkey_hash(); + ScriptBuf::new_p2pkh(&pub_key_hash) + } + "P2WPKH" => { + let w_pub_key_hash = value + .output_script_value + .parse::() + .map_err(|_| Error::InvalidOutputScript)? + .wpubkey_hash() + .unwrap(); + ScriptBuf::new_p2wpkh(&w_pub_key_hash) + } + "P2SH" => { + let script_hashed = value + .output_script_value + .parse::() + .map_err(|_| Error::InvalidOutputScript)?; + ScriptBuf::new_p2sh(&script_hashed) + } + "P2WSH" => { + let w_script_hashed = value + .output_script_value + .parse::() + .map_err(|_| Error::InvalidOutputScript)?; + ScriptBuf::new_p2wsh(&w_script_hashed) + } + "P2TR" => { + // From the bip + // + // Conceptually, every Taproot output corresponds to a combination of + // a single public key condition (the internal key), + // and zero or more general conditions encoded in scripts organized in a tree. + let pub_key = value + .output_script_value + .parse::() + .map_err(|_| Error::InvalidOutputScript)?; + ScriptBuf::new_p2tr::(&Secp256k1::::new(), pub_key, None) + } + _ => return Err(Error::UnknownOutputScriptType), + }; + Ok(Self { + script_pubkey, + // legacy encoding gives no way to specify testnet or mainnet + ok_for_mainnet: true, + }) + } +} + +/// A coinbase output script as it appears in a configuration file. +/// +/// Private to avoid exposing the enum constructors. +#[derive(serde::Deserialize)] +#[serde(untagged)] // decode as whichever variant makes sense for the input +enum SerdeCoinbaseOutputInner { + Legacy(LegacyCoinbaseOutput), + Descriptor(String), +} + +/// A structure representing a coinbase output script as it appears in a +/// configuration file. +/// +/// Can only be constructed via serde, and supports no operations except conversion +/// to a [`super::CoinbaseOutput`] via [`TryFrom`]. +#[derive(serde::Deserialize)] +#[serde(transparent)] +pub struct SerdeCoinbaseOutput { + inner: SerdeCoinbaseOutputInner, +} + +impl TryFrom for super::CoinbaseRewardScript { + type Error = super::Error; + fn try_from(value: SerdeCoinbaseOutput) -> Result { + match value.inner { + SerdeCoinbaseOutputInner::Legacy(legacy) => Self::try_from(legacy), + SerdeCoinbaseOutputInner::Descriptor(ref s) => Self::from_descriptor(s), + } + } +} diff --git a/roles/roles-utils/config-helpers/src/lib.rs b/roles/roles-utils/config-helpers/src/lib.rs index e15fec1b5f..cfef645517 100644 --- a/roles/roles-utils/config-helpers/src/lib.rs +++ b/roles/roles-utils/config-helpers/src/lib.rs @@ -1,5 +1,7 @@ mod coinbase_output; -pub use coinbase_output::{CoinbaseOutput, Error as CoinbaseOutputError}; +pub use coinbase_output::{CoinbaseRewardScript, Error as CoinbaseOutputError}; + +pub mod logging; mod toml; pub use toml::duration_from_toml; diff --git a/roles/roles-utils/config-helpers/src/logging.rs b/roles/roles-utils/config-helpers/src/logging.rs new file mode 100644 index 0000000000..4273c8c602 --- /dev/null +++ b/roles/roles-utils/config-helpers/src/logging.rs @@ -0,0 +1,49 @@ +use std::{ + fs::OpenOptions, + io::{self, IsTerminal}, + path::Path, + str::FromStr, +}; +use tracing::level_filters::LevelFilter; +use tracing_subscriber::{fmt, prelude::*, EnvFilter, Registry}; + +/// Initialize logging to stdout and optionally to a file. +/// +/// If `log_file` is Some, logs will be written to both stdout and the file. +/// If `log_level` is not provided or is invalid, it defaults to "info". +pub fn init_logging(log_file: Option<&Path>) { + let rust_log = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()); + let log_level_filter = LevelFilter::from_str(&rust_log).unwrap_or(LevelFilter::INFO); + let env_filter = EnvFilter::new(log_level_filter.to_string()); + let stdout_layer = fmt::layer() + .with_writer(io::stdout) + .with_ansi(io::stdout().is_terminal()); + + let subscriber: Box = match log_file { + Some(path) => { + // Log to both file and stdout + let path = path.to_owned(); + let file_layer = fmt::layer() + .with_writer(move || { + OpenOptions::new() + .create(true) + .append(true) + .open(&path) + .expect("Failed to open log file") + }) + .with_ansi(false); + Box::new( + Registry::default() + .with(env_filter) + .with(stdout_layer) + .with(file_layer), + ) + } + None => { + // Log only to stdout + Box::new(Registry::default().with(env_filter).with(stdout_layer)) + } + }; + + tracing::subscriber::set_global_default(subscriber).expect("Failed to set global subscriber"); +} diff --git a/roles/roles-utils/network-helpers/Cargo.toml b/roles/roles-utils/network-helpers/Cargo.toml index 7df0426cf0..b0b0ca9f0b 100644 --- a/roles/roles-utils/network-helpers/Cargo.toml +++ b/roles/roles-utils/network-helpers/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "network_helpers_sv2" -version = "3.1.0" +version = "4.0.1" authors = ["The Stratum V2 Developers"] -edition = "2018" +edition = "2021" description = "Networking utils for SV2 roles" documentation = "https://docs.rs/network_helpers_sv2" homepage = "https://stratumprotocol.org" @@ -17,17 +17,15 @@ keywords = ["stratum", "mining", "bitcoin", "protocol"] async-std = { version = "1.8.0", optional = true } async-channel = { version = "1.8.0", optional = true } tokio = { version = "1.44.1", features = ["full"] } -binary_sv2 = { path = "../../../protocols/v2/binary-sv2", version = "^3.0.0", optional = true } -codec_sv2 = { path = "../../../protocols/v2/codec-sv2", version = "^2.0.0", features=["noise_sv2"], optional = true } -stratum-common = { path = "../../../common" } -sv1_api = { path = "../../../protocols/v1/", version = "^1.0.0", optional = true } +codec_sv2 = { path = "../../../protocols/v2/codec-sv2", version = "^3.0.0", features=["noise_sv2"], optional = true } +sv1_api = { path = "../../../protocols/v1/", version = "^2.1.0", optional = true } tracing = { version = "0.1" } futures = "0.3.28" tokio-util = { version = "0.7.10", default-features = false, features = ["codec"], optional = true } serde_json = { version = "1.0.138", default-features = false, optional = true } [features] -default = ["async-channel", "binary_sv2", "codec_sv2"] +default = ["async-channel", "codec_sv2"] with_buffer_pool = ["codec_sv2/with_buffer_pool"] sv1 = ["sv1_api", "tokio-util", "serde_json"] diff --git a/roles/roles-utils/network-helpers/src/lib.rs b/roles/roles-utils/network-helpers/src/lib.rs index 6c377f954e..2c51f058d8 100644 --- a/roles/roles-utils/network-helpers/src/lib.rs +++ b/roles/roles-utils/network-helpers/src/lib.rs @@ -1,19 +1,13 @@ -use binary_sv2::{Deserialize, GetSize, Serialize}; pub mod noise_connection; +pub mod noise_stream; pub mod plain_connection; #[cfg(feature = "sv1")] pub mod sv1_connection; -use async_channel::{Receiver, RecvError, SendError, Sender}; -use codec_sv2::{ - noise_sv2::{ELLSWIFT_ENCODING_SIZE, INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_SIZE}, - Error as CodecError, HandShakeFrame, HandshakeRole, StandardEitherFrame, -}; -use futures::lock::Mutex; -use std::{ - convert::TryInto, - sync::{atomic::AtomicBool, Arc}, -}; +use async_channel::{RecvError, SendError}; +use codec_sv2::Error as CodecError; + +pub use codec_sv2; #[derive(Debug)] pub enum Error { @@ -41,81 +35,3 @@ impl From> for Error { Error::SendError } } - -trait SetState { - async fn set_state(self_: Arc>, state: codec_sv2::State); -} - -async fn initialize_as_downstream< - 'a, - Message: Serialize + Deserialize<'a> + GetSize, - T: SetState, ->( - self_: Arc>, - role: HandshakeRole, - sender_outgoing: Sender>, - receiver_incoming: Receiver>, -) -> Result<(), Error> { - let mut state = codec_sv2::State::initialized(role); - - // Create and send first handshake message - let first_message = state.step_0()?; - sender_outgoing.send(first_message.into()).await?; - - // Receive and deserialize second handshake message - let second_message = receiver_incoming.recv().await?; - let second_message: HandShakeFrame = second_message - .try_into() - .map_err(|_| Error::HandshakeRemoteInvalidMessage)?; - let second_message: [u8; INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_SIZE] = second_message - .get_payload_when_handshaking() - .try_into() - .map_err(|_| Error::HandshakeRemoteInvalidMessage)?; - - // Create and send thirth handshake message - let transport_mode = state.step_2(second_message)?; - - T::set_state(self_, transport_mode).await; - while !TRANSPORT_READY.load(std::sync::atomic::Ordering::SeqCst) { - std::hint::spin_loop() - } - Ok(()) -} - -async fn initialize_as_upstream<'a, Message: Serialize + Deserialize<'a> + GetSize, T: SetState>( - self_: Arc>, - role: HandshakeRole, - sender_outgoing: Sender>, - receiver_incoming: Receiver>, -) -> Result<(), Error> { - let mut state = codec_sv2::State::initialized(role); - - // Receive and deserialize first handshake message - let first_message: HandShakeFrame = receiver_incoming - .recv() - .await? - .try_into() - .map_err(|_| Error::HandshakeRemoteInvalidMessage)?; - let first_message: [u8; ELLSWIFT_ENCODING_SIZE] = first_message - .get_payload_when_handshaking() - .try_into() - .map_err(|_| Error::HandshakeRemoteInvalidMessage)?; - - // Create and send second handshake message - let (second_message, transport_mode) = state.step_1(first_message)?; - HANDSHAKE_READY.store(false, std::sync::atomic::Ordering::SeqCst); - sender_outgoing.send(second_message.into()).await?; - - // This sets the state to Handshake state - this prompts the task above to move the state - // to transport mode so that the next incoming message will be decoded correctly - // It is important to do this directly before sending the fourth message - T::set_state(self_, transport_mode).await; - while !TRANSPORT_READY.load(std::sync::atomic::Ordering::SeqCst) { - std::hint::spin_loop() - } - - Ok(()) -} - -static HANDSHAKE_READY: AtomicBool = AtomicBool::new(false); -static TRANSPORT_READY: AtomicBool = AtomicBool::new(false); diff --git a/roles/roles-utils/network-helpers/src/noise_connection.rs b/roles/roles-utils/network-helpers/src/noise_connection.rs index 40119dcaf2..6a50427ba4 100644 --- a/roles/roles-utils/network-helpers/src/noise_connection.rs +++ b/roles/roles-utils/network-helpers/src/noise_connection.rs @@ -1,39 +1,37 @@ -use crate::Error; +#![allow(clippy::new_ret_no_self)] +use crate::{ + noise_stream::{NoiseTcpReadHalf, NoiseTcpStream, NoiseTcpWriteHalf}, + Error, +}; use async_channel::{unbounded, Receiver, Sender}; -use binary_sv2::{Deserialize, GetSize, Serialize}; -use codec_sv2::{HandshakeRole, StandardEitherFrame, StandardNoiseDecoder}; -use futures::lock::Mutex; -use std::sync::Arc; -use tokio::{ - io::{AsyncReadExt, AsyncWriteExt}, - net::TcpStream, - select, task, +use codec_sv2::{ + binary_sv2::{Deserialize, GetSize, Serialize}, + HandshakeRole, StandardEitherFrame, }; +use std::sync::Arc; +use tokio::{net::TcpStream, task}; use tracing::{debug, error}; -#[derive(Debug)] -pub struct Connection { - pub state: codec_sv2::State, +pub struct Connection; + +struct ConnectionState { + sender_incoming: Sender>, + receiver_incoming: Receiver>, + sender_outgoing: Sender>, + receiver_outgoing: Receiver>, } -impl crate::SetState for Connection { - async fn set_state(self_: Arc>, state: codec_sv2::State) { - loop { - if crate::HANDSHAKE_READY.load(std::sync::atomic::Ordering::SeqCst) { - if let Some(mut connection) = self_.try_lock() { - connection.state = state; - crate::TRANSPORT_READY.store(true, std::sync::atomic::Ordering::Relaxed); - break; - }; - } - task::yield_now().await; - } +impl ConnectionState { + fn close_all(&self) { + self.sender_incoming.close(); + self.receiver_incoming.close(); + self.sender_outgoing.close(); + self.receiver_outgoing.close(); } } impl Connection { - #[allow(clippy::new_ret_no_self)] - pub async fn new<'a, Message: Serialize + Deserialize<'a> + GetSize + Send + 'static>( + pub async fn new( stream: TcpStream, role: HandshakeRole, ) -> Result< @@ -42,140 +40,100 @@ impl Connection { Sender>, ), Error, - > { - let address = stream.peer_addr().map_err(|_| Error::SocketClosed)?; - - let (mut reader, mut writer) = stream.into_split(); + > + where + Message: Serialize + Deserialize<'static> + GetSize + Send + 'static, + { + let (sender_incoming, receiver_incoming) = unbounded(); + let (sender_outgoing, receiver_outgoing) = unbounded(); - let (sender_incoming, receiver_incoming): ( - Sender>, - Receiver>, - ) = unbounded(); - let (sender_outgoing, receiver_outgoing): ( - Sender>, - Receiver>, - ) = unbounded(); + let conn_state = Arc::new(ConnectionState { + sender_incoming, + receiver_incoming: receiver_incoming.clone(), + sender_outgoing: sender_outgoing.clone(), + receiver_outgoing, + }); - let state = codec_sv2::State::not_initialized(&role); + let (read_half, write_half) = NoiseTcpStream::::new(stream, role) + .await? + .into_split(); - let connection = Arc::new(Mutex::new(Self { state })); + Self::spawn_reader(read_half, Arc::clone(&conn_state)); + Self::spawn_writer(write_half, conn_state); - let cloned1 = connection.clone(); - let cloned2 = connection.clone(); + Ok((receiver_incoming, sender_outgoing)) + } + fn spawn_reader( + mut read_half: NoiseTcpReadHalf, + conn_state: Arc>, + ) -> task::JoinHandle<()> + where + Message: Serialize + Deserialize<'static> + GetSize + Send + 'static, + { + let sender_incoming = conn_state.sender_incoming.clone(); task::spawn(async move { - select!( - _ = tokio::signal::ctrl_c() => { }, - _ = async { - let mut decoder = StandardNoiseDecoder::::new(); - loop { - let writable = decoder.writable(); - match reader.read_exact(writable).await { - Ok(_) => { - let mut connection = cloned1.lock().await; - let decoded = decoder.next_frame(&mut connection.state); - drop(connection); - match decoded { - Ok(x) => { - if sender_incoming.send(x).await.is_err() { - error!("Shutting down noise stream reader!"); - break; - } + loop { + tokio::select! { + _ = tokio::signal::ctrl_c() => { + debug!("Reader received shutdown signal."); + break; + } + res = read_half.read_frame() => match res { + Ok(frame) => { + if sender_incoming.send(frame).await.is_err() { + error!("Reader: channel closed, shutting down."); + break; + } } Err(e) => { - if let codec_sv2::Error::MissingBytes(_) = e { - } else { - error!("Shutting down noise stream reader! {:#?}", e); - sender_incoming.close(); + error!("Reader: error while reading frame: {e:?}"); break; - } } - } - } - Err(e) => { - error!( - "Disconnected from client while reading : {} - {}", - e, &address - ); - sender_incoming.close(); - break; } - } } - } => {} - ); - }); + } - let receiver_outgoing_cloned = receiver_outgoing.clone(); - task::spawn(async move { - select!( - _ = tokio::signal::ctrl_c() => { }, - _ = async { - let mut encoder = codec_sv2::NoiseEncoder::::new(); - loop { - let received = receiver_outgoing_cloned.recv().await; + conn_state.close_all(); + }) + } - match received { - Ok(frame) => { - let mut connection = cloned2.lock().await; - let b = encoder.encode(frame, &mut connection.state).unwrap(); - drop(connection); - let b = b.as_ref(); - match (writer).write_all(b).await { - Ok(_) => (), - Err(e) => { - let _ = writer.shutdown().await; - // Just fail and force to reinitialize everything - error!( - "Disconnecting from client due to error writing: {} - {}", - e, &address - ); - task::yield_now().await; - break; - } - } + fn spawn_writer( + mut write_half: NoiseTcpWriteHalf, + conn_state: Arc>, + ) -> task::JoinHandle<()> + where + Message: Serialize + Deserialize<'static> + GetSize + Send + 'static, + { + let receiver_outgoing = conn_state.receiver_outgoing.clone(); + + task::spawn(async move { + loop { + tokio::select! { + _ = tokio::signal::ctrl_c() => { + debug!("Writer received shutdown signal."); + break; } - Err(e) => { - // Just fail and force to reinitialize everything - let _ = writer.shutdown().await; - error!( - "Disconnecting from client due to error receiving: {} - {}", - e, &address - ); - task::yield_now().await; - break; + res = receiver_outgoing.recv() => match res { + Ok(frame) => { + if let Err(e) = write_half.write_frame(frame).await { + error!("Writer: error while writing frame: {e:?}"); + break; + } + } + Err(_) => { + debug!("Writer: channel closed, shutting down."); + break; + } } - }; - crate::HANDSHAKE_READY.store(true, std::sync::atomic::Ordering::Relaxed); } - } => {} - ); - }); - - // DO THE NOISE HANDSHAKE - match role { - HandshakeRole::Initiator(_) => { - debug!("Initializing as downstream for - {}", &address); - crate::initialize_as_downstream( - connection.clone(), - role, - sender_outgoing.clone(), - receiver_incoming.clone(), - ) - .await? } - HandshakeRole::Responder(_) => { - debug!("Initializing as upstream for - {}", &address); - crate::initialize_as_upstream( - connection.clone(), - role, - sender_outgoing.clone(), - receiver_incoming.clone(), - ) - .await? + + if let Err(e) = write_half.shutdown().await { + error!("Writer: error during shutdown: {e:?}"); } - }; - debug!("Noise handshake complete - {}", &address); - Ok((receiver_incoming, sender_outgoing)) + + conn_state.close_all(); + }) } } diff --git a/roles/roles-utils/network-helpers/src/noise_stream.rs b/roles/roles-utils/network-helpers/src/noise_stream.rs new file mode 100644 index 0000000000..69a31d83b8 --- /dev/null +++ b/roles/roles-utils/network-helpers/src/noise_stream.rs @@ -0,0 +1,330 @@ +//! A Noise-encrypted wrapper around a `TcpStream`, providing framed read/write I/O using the SV2 +//! protocol and a stateful Noise handshake. +//! +//! This module provides `NoiseTcpStream`, which wraps a `TcpStream` and performs a Noise-based +//! authenticated key exchange based on the provided [`HandshakeRole`]. +//! +//! After a successful handshake, the stream can be split into a `NoiseTcpReadHalf` and +//! `NoiseTcpWriteHalf`, which support frame-based encoding/decoding of SV2 messages with optional +//! non-blocking behavior. + +use crate::Error; +use codec_sv2::{ + binary_sv2::{Deserialize, GetSize, Serialize}, + noise_sv2::INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_SIZE, + HandshakeRole, NoiseEncoder, StandardNoiseDecoder, State, +}; +use tokio::net::{ + tcp::{OwnedReadHalf, OwnedWriteHalf}, + TcpStream, +}; + +use codec_sv2::{noise_sv2::ELLSWIFT_ENCODING_SIZE, HandShakeFrame, StandardEitherFrame}; +use std::convert::TryInto; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tracing::{debug, error}; + +/// A Noise-secured duplex stream over TCP that wraps a `TcpStream` +/// and provides secure read/write capabilities using the Noise protocol. +/// +/// This stream performs the full Noise handshake during construction +/// and returns a bidirectional encrypted stream split into read and write halves. +/// +/// **Note:** This struct is **not cancellation-safe**. +/// If `read_frame()` or `write_frame()` is canceled mid-way, +/// internal state may be left in an inconsistent state, which can lead to +/// protocol errors or dropped frames. +pub struct NoiseTcpStream + GetSize + Send + 'static> { + reader: NoiseTcpReadHalf, + writer: NoiseTcpWriteHalf, +} + +/// The reading half of a `NoiseTcpStream`. +/// +/// It buffers incoming encrypted bytes, attempts to decode full Noise frames, +/// and exposes a method to retrieve structured messages of type `Message`. +pub struct NoiseTcpReadHalf + GetSize + Send + 'static> { + reader: OwnedReadHalf, + decoder: StandardNoiseDecoder, + state: State, + current_frame_buf: Vec, + bytes_read: usize, +} + +/// The writing half of a `NoiseTcpStream`. +/// +/// It accepts structured messages, encodes them via the Noise protocol, +/// and writes the result to the socket. +pub struct NoiseTcpWriteHalf + GetSize + Send + 'static> { + writer: OwnedWriteHalf, + encoder: NoiseEncoder, + state: State, +} + +impl NoiseTcpStream +where + Message: Serialize + Deserialize<'static> + GetSize + Send + 'static, +{ + /// Constructs a new `NoiseTcpStream` over the given TCP stream, + /// performing the Noise handshake in the given `role`. + /// + /// On success, returns a stream with encrypted communication channels. + pub async fn new(stream: TcpStream, role: HandshakeRole) -> Result { + let (mut reader, mut writer) = stream.into_split(); + + let mut decoder = StandardNoiseDecoder::::new(); + let mut encoder = NoiseEncoder::::new(); + let mut state = State::initialized(role.clone()); + + match role { + HandshakeRole::Initiator(_) => { + let mut responder_state = codec_sv2::State::not_initialized(&role); + let first_msg = state.step_0()?; + send_message(&mut writer, first_msg.into(), &mut state, &mut encoder).await?; + debug!("First handshake message sent"); + + loop { + match receive_message(&mut reader, &mut responder_state, &mut decoder).await { + Ok(second_msg) => { + debug!("Second handshake message received"); + let handshake_frame: HandShakeFrame = second_msg + .try_into() + .map_err(|_| Error::HandshakeRemoteInvalidMessage)?; + let payload: [u8; INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_SIZE] = + handshake_frame + .get_payload_when_handshaking() + .try_into() + .map_err(|_| Error::HandshakeRemoteInvalidMessage)?; + let transport_state = state.step_2(payload)?; + state = transport_state; + break; + } + Err(Error::CodecError(codec_sv2::Error::MissingBytes(_))) => { + debug!("Waiting for more bytes during handshake"); + } + Err(e) => { + error!("Handshake failed with upstream: {:?}", e); + return Err(e); + } + } + } + } + HandshakeRole::Responder(_) => { + let mut initiator_state = codec_sv2::State::not_initialized(&role); + + loop { + match receive_message(&mut reader, &mut initiator_state, &mut decoder).await { + Ok(first_msg) => { + debug!("First handshake message received"); + let handshake_frame: HandShakeFrame = first_msg + .try_into() + .map_err(|_| Error::HandshakeRemoteInvalidMessage)?; + let payload: [u8; ELLSWIFT_ENCODING_SIZE] = handshake_frame + .get_payload_when_handshaking() + .try_into() + .map_err(|_| Error::HandshakeRemoteInvalidMessage)?; + let (second_msg, transport_state) = state.step_1(payload)?; + send_message(&mut writer, second_msg.into(), &mut state, &mut encoder) + .await?; + debug!("Second handshake message sent"); + state = transport_state; + break; + } + Err(Error::CodecError(codec_sv2::Error::MissingBytes(_))) => { + debug!("Waiting for more bytes during handshake"); + } + Err(e) => { + error!("Handshake failed with downstream: {:?}", e); + return Err(e); + } + } + } + } + }; + Ok(Self { + reader: NoiseTcpReadHalf { + reader, + decoder, + state: state.clone(), + current_frame_buf: vec![], + bytes_read: 0, + }, + writer: NoiseTcpWriteHalf { + writer, + encoder, + state, + }, + }) + } + + /// Consumes the stream and returns its reader and writer halves. + pub fn into_split(self) -> (NoiseTcpReadHalf, NoiseTcpWriteHalf) { + (self.reader, self.writer) + } +} + +impl NoiseTcpWriteHalf +where + Message: Serialize + Deserialize<'static> + GetSize + Send + 'static, +{ + /// Encrypts and writes a full message frame to the socket. + /// + /// Returns an error if the socket is closed or the message cannot be encoded. + /// + /// Not cancellation-safe: A canceled write may cause partial writes or state corruption. + pub async fn write_frame(&mut self, frame: StandardEitherFrame) -> Result<(), Error> { + let buf = self.encoder.encode(frame, &mut self.state)?; + self.writer + .write_all(buf.as_ref()) + .await + .map_err(|_| Error::SocketClosed)?; + Ok(()) + } + + /// Attempts to write a message without blocking. + /// + /// Returns: + /// - `Ok(true)` if the entire frame was written successfully. + /// - `Ok(false)` if the socket is not ready (would block). + /// - `Err(_)` on socket or encoding errors. + pub fn try_write_frame(&mut self, frame: StandardEitherFrame) -> Result { + let buf = self.encoder.encode(frame, &mut self.state)?; + + match self.writer.try_write(buf.as_ref()) { + Ok(n) if n == buf.len() => Ok(true), + Ok(_) => Err(Error::SocketClosed), + Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => Ok(false), + Err(_) => Err(Error::SocketClosed), + } + } + + /// Gracefully shuts down the writing half of the stream. + /// + /// Returns an error if the shutdown fails. + pub async fn shutdown(&mut self) -> Result<(), Error> { + self.writer + .shutdown() + .await + .map_err(|_| Error::SocketClosed) + } +} + +impl NoiseTcpReadHalf +where + Message: Serialize + Deserialize<'static> + GetSize + Send + 'static, +{ + /// Reads and decodes a complete frame from the socket. + /// + /// This method blocks until a full frame is read and decoded, + /// handling `MissingBytes` errors from the codec automatically. + /// + /// Not cancellation-safe: Cancellation may leave partially-read state behind. + pub async fn read_frame(&mut self) -> Result, Error> { + loop { + let expected = self.decoder.writable_len(); + + if self.current_frame_buf.len() != expected { + self.current_frame_buf.resize(expected, 0); + self.bytes_read = 0; + } + + while self.bytes_read < expected { + let n = self + .reader + .read(&mut self.current_frame_buf[self.bytes_read..]) + .await + .map_err(|_| Error::SocketClosed)?; + + if n == 0 { + return Err(Error::SocketClosed); + } + + self.bytes_read += n; + } + + self.decoder + .writable() + .copy_from_slice(&self.current_frame_buf[..]); + + self.bytes_read = 0; + + match self.decoder.next_frame(&mut self.state) { + Ok(frame) => return Ok(frame), + Err(codec_sv2::Error::MissingBytes(_)) => { + tokio::task::yield_now().await; + continue; + } + Err(e) => return Err(Error::CodecError(e)), + } + } + } + + /// Attempts to read and decode a frame without blocking. + /// + /// Returns: + /// - `Ok(Some(frame))` if a full frame is successfully decoded. + /// - `Ok(None)` if not enough data is available yet. + /// - `Err(_)` on socket or decoding errors. + pub fn try_read_frame(&mut self) -> Result>, Error> { + let expected = self.decoder.writable_len(); + + if self.current_frame_buf.len() != expected { + self.current_frame_buf.resize(expected, 0); + self.bytes_read = 0; + } + + match self + .reader + .try_read(&mut self.current_frame_buf[self.bytes_read..]) + { + Ok(0) => return Err(Error::SocketClosed), + Ok(n) => self.bytes_read += n, + Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => return Ok(None), + Err(_) => return Err(Error::SocketClosed), + } + + if self.bytes_read < expected { + return Ok(None); + } + + self.decoder + .writable() + .copy_from_slice(&self.current_frame_buf[..]); + + self.bytes_read = 0; + + match self.decoder.next_frame(&mut self.state) { + Ok(frame) => Ok(Some(frame)), + Err(codec_sv2::Error::MissingBytes(_)) => Ok(None), + Err(e) => Err(Error::CodecError(e)), + } + } +} + +async fn send_message + GetSize + Send + 'static>( + writer: &mut OwnedWriteHalf, + msg: StandardEitherFrame, + state: &mut State, + encoder: &mut NoiseEncoder, +) -> Result<(), Error> { + let buffer = encoder.encode(msg, state)?; + writer + .write_all(buffer.as_ref()) + .await + .map_err(|_| Error::SocketClosed)?; + Ok(()) +} + +async fn receive_message + GetSize + Send + 'static>( + reader: &mut OwnedReadHalf, + state: &mut State, + decoder: &mut StandardNoiseDecoder, +) -> Result, Error> { + let mut buffer = vec![0u8; decoder.writable_len()]; + reader + .read_exact(&mut buffer) + .await + .map_err(|_| Error::SocketClosed)?; + decoder.writable().copy_from_slice(&buffer); + decoder.next_frame(state).map_err(Error::CodecError) +} diff --git a/roles/roles-utils/network-helpers/src/plain_connection.rs b/roles/roles-utils/network-helpers/src/plain_connection.rs index a269f4424a..e70e8e9a40 100644 --- a/roles/roles-utils/network-helpers/src/plain_connection.rs +++ b/roles/roles-utils/network-helpers/src/plain_connection.rs @@ -1,5 +1,5 @@ use async_channel::{bounded, Receiver, Sender}; -use binary_sv2::{Deserialize, Serialize}; +use codec_sv2::binary_sv2::{Deserialize, Serialize}; use core::convert::TryInto; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, @@ -7,8 +7,7 @@ use tokio::{ task, }; -use binary_sv2::GetSize; -use codec_sv2::{Error::MissingBytes, StandardDecoder, StandardEitherFrame}; +use codec_sv2::{binary_sv2::GetSize, Error::MissingBytes, StandardDecoder, StandardEitherFrame}; use tracing::{error, trace}; #[derive(Debug)] diff --git a/roles/roles-utils/network-helpers/src/sv1_connection.rs b/roles/roles-utils/network-helpers/src/sv1_connection.rs index d416a84041..5b254533c6 100644 --- a/roles/roles-utils/network-helpers/src/sv1_connection.rs +++ b/roles/roles-utils/network-helpers/src/sv1_connection.rs @@ -1,11 +1,12 @@ use async_channel::{unbounded, Receiver, Sender}; -use futures::{FutureExt, StreamExt}; +use futures::StreamExt; use sv1_api::json_rpc; use tokio::{ - io::{AsyncWriteExt, BufReader}, + io::{AsyncWriteExt, BufReader, BufWriter}, net::TcpStream, }; use tokio_util::codec::{FramedRead, LinesCodec}; +use tracing::{error, trace, warn}; /// Represents a connection between two roles communicating using SV1 protocol. /// @@ -19,79 +20,121 @@ pub struct ConnectionSV1 { sender: Sender, } -const MAX_LINE_LENGTH: usize = 2_usize.pow(16); +struct ConnectionState { + receiver_outgoing: Receiver, + sender_outgoing: Sender, + receiver_incoming: Receiver, + sender_incoming: Sender, +} + +impl ConnectionState { + fn new( + receiver_outgoing: Receiver, + sender_outgoing: Sender, + receiver_incoming: Receiver, + sender_incoming: Sender, + ) -> Self { + Self { + receiver_incoming, + receiver_outgoing, + sender_incoming, + sender_outgoing, + } + } + + fn close(&self) { + self.receiver_incoming.close(); + self.receiver_outgoing.close(); + self.sender_incoming.close(); + self.sender_outgoing.close(); + } +} + +const MAX_LINE_LENGTH: usize = 1 << 16; impl ConnectionSV1 { - /// Create a new connection set up to communicate with the other side of the given stream. - /// - /// Two tasks are spawned to handle reading and writing messages. The reading task will read - /// messages from the stream and send them to the receiver channel. The writing task will read - /// messages from the sender channel and write them to the stream. pub async fn new(stream: TcpStream) -> Self { - let (reader_stream, mut writer_stream) = stream.into_split(); + let (read_half, write_half) = stream.into_split(); let (sender_incoming, receiver_incoming) = unbounded(); - let (sender_outgoing, receiver_outgoing) = unbounded::(); - - // Read Job - tokio::task::spawn(async move { - let reader = BufReader::new(reader_stream); - let mut messages = - FramedRead::new(reader, LinesCodec::new_with_max_length(MAX_LINE_LENGTH)); - loop { - tokio::select! { - res = messages.next().fuse() => { - match res { - Some(Ok(incoming)) => { - let incoming: json_rpc::Message = serde_json::from_str(&incoming).expect("Failed to parse incoming message"); - if sender_incoming - .send(incoming) - .await - .is_err() - { - break; - } - - } - Some(Err(e)) => { - break tracing::error!("Error reading from stream: {:?}", e); - } - None => { - tracing::error!("No message received"); - } + let (sender_outgoing, receiver_outgoing) = unbounded(); + + let buffer_read_half = BufReader::new(read_half); + let buffer_write_half = BufWriter::new(write_half); + + let connection_state = ConnectionState::new( + receiver_outgoing.clone(), + sender_outgoing.clone(), + receiver_incoming.clone(), + sender_incoming.clone(), + ); + + tokio::spawn(async move { + tokio::select! { + _ = Self::run_reader(buffer_read_half, sender_incoming.clone()) => { + trace!("Reader task exited. Closing writer sender."); + connection_state.close(); + } + _ = Self::run_writer(buffer_write_half, receiver_outgoing.clone()) => { + trace!("Writer task exited. Closing reader sender."); + connection_state.close(); + } + } + }); + + Self { + receiver: receiver_incoming, + sender: sender_outgoing, + } + } + + async fn run_reader( + reader: BufReader, + sender: Sender, + ) { + let mut lines = FramedRead::new(reader, LinesCodec::new_with_max_length(MAX_LINE_LENGTH)); + while let Some(result) = lines.next().await { + match result { + Ok(line) => match serde_json::from_str::(&line) { + Ok(msg) => { + if sender.send(msg).await.is_err() { + warn!("Receiver dropped, stopping reader"); + break; } - }, - _ = tokio::signal::ctrl_c().fuse() => { - break; } - }; + Err(e) => { + error!("Failed to deserialize message: {e:?}"); + } + }, + Err(e) => { + error!("Error reading from stream: {e:?}"); + break; + } } - }); + } + } - // Write Job - tokio::task::spawn(async move { - loop { - tokio::select! { - res = receiver_outgoing.recv().fuse() => { - let to_send = res.expect("Failed to receive message"); - let to_send = match serde_json::to_string(&to_send) { - Ok(string) => format!("{}\n", string), - Err(_e) => { - break; - } - }; - let _ = writer_stream - .write_all(to_send.as_bytes()) - .await; - }, - _ = tokio::signal::ctrl_c().fuse() => { + async fn run_writer( + mut writer: BufWriter, + receiver: Receiver, + ) { + while let Ok(msg) = receiver.recv().await { + match serde_json::to_string(&msg) { + Ok(line) => { + let data = format!("{line}\n"); + if writer.write_all(data.as_bytes()).await.is_err() { + error!("Failed to write to stream"); + break; + } + if writer.flush().await.is_err() { + error!("Failed to flush writer."); break; } - }; + } + Err(e) => { + error!("Failed to serialize message: {e:?}"); + break; + } } - }); - Self { - receiver: receiver_incoming, - sender: sender_outgoing, } } diff --git a/roles/roles-utils/rpc/Cargo.toml b/roles/roles-utils/rpc/Cargo.toml index ea23134654..828f963084 100644 --- a/roles/roles-utils/rpc/Cargo.toml +++ b/roles/roles-utils/rpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rpc_sv2" -version = "1.0.0" +version = "1.1.1" authors = ["The Stratum V2 Developers"] edition = "2021" description = "SV2 JD Server RPC" @@ -14,7 +14,7 @@ keywords = ["stratum", "mining", "bitcoin", "protocol"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -stratum-common = { path = "../../../common", features=["bitcoin"], version = "^2.0.0" } +stratum-common = { path = "../../../common", version = "4.0.1" } serde = { version = "1.0.89", features = ["derive", "alloc"], default-features = false } serde_json = { version = "1.0", default-features = false, features = ["alloc","raw_value"] } hex = "0.4.3" diff --git a/roles/roles-utils/rpc/src/mini_rpc_client.rs b/roles/roles-utils/rpc/src/mini_rpc_client.rs index aa7e59aa5c..61e064d7e1 100644 --- a/roles/roles-utils/rpc/src/mini_rpc_client.rs +++ b/roles/roles-utils/rpc/src/mini_rpc_client.rs @@ -14,7 +14,9 @@ use hyper_util::{ }; use serde::{Deserialize, Serialize}; use serde_json::json; -use stratum_common::bitcoin::{consensus::encode::deserialize as consensus_decode, Transaction}; +use stratum_common::roles_logic_sv2::bitcoin::{ + consensus::encode::deserialize as consensus_decode, Transaction, +}; use super::BlockHash; @@ -127,7 +129,7 @@ impl MiniRpcClient { format!( "Basic {}", base64::engine::general_purpose::STANDARD - .encode(format!("{}:{}", username, password)) + .encode(format!("{username}:{password}")) ), ) .body(Full::::from(request_body)) diff --git a/roles/roles-utils/stratum-translation/Cargo.toml b/roles/roles-utils/stratum-translation/Cargo.toml new file mode 100644 index 0000000000..4adeb40d2b --- /dev/null +++ b/roles/roles-utils/stratum-translation/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "stratum_translation" +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +description = "Stratum V1 ↔ Stratum V2 translation utilities for reuse across proxies, apps, and firmware" + +[lib] +name = "stratum_translation" +path = "src/lib.rs" + +[dependencies] +binary_sv2 = { path = "../../../protocols/v2/binary-sv2", version = "^4.0.0" } +mining_sv2 = { path = "../../../protocols/v2/subprotocols/mining", version = "^5.0.0" } +channels_sv2 = { path = "../../../protocols/v2/channels-sv2", version = "^2.0.0" } +v1 = { path = "../../../protocols/v1", package = "sv1_api", version = "^2.0.0" } +tracing = "0.1" \ No newline at end of file diff --git a/roles/roles-utils/stratum-translation/src/error.rs b/roles/roles-utils/stratum-translation/src/error.rs new file mode 100644 index 0000000000..1be5dd7fac --- /dev/null +++ b/roles/roles-utils/stratum-translation/src/error.rs @@ -0,0 +1,15 @@ +use channels_sv2::bip141::StripBip141Error; + +#[derive(Debug)] +pub enum StratumTranslationError { + // SV1 -> SV2 + InvalidJobId, + IncompatibleVersionRollingMask, + InvalidExtranonceLength, + InvalidUserIdentity(String), + // SV2 -> SV1 + FailedToTryToStripBip141(StripBip141Error), + FailedToSerializeToB064K, +} + +pub type Result = core::result::Result; diff --git a/roles/roles-utils/stratum-translation/src/lib.rs b/roles/roles-utils/stratum-translation/src/lib.rs new file mode 100644 index 0000000000..bbed4c58c6 --- /dev/null +++ b/roles/roles-utils/stratum-translation/src/lib.rs @@ -0,0 +1,22 @@ +//! Stratum Translation +//! +//! A small, runtime-free library that provides pure SV1↔SV2 translation helpers +//! that can be reused by different apps (translator proxy, firmware) without +//! pulling in async runtimes or networking. +//! +//! What it contains: +//! - Converters from SV2 messages/values to SV1 messages (e.g. SetTarget → mining.set_difficulty) +//! - Converters from SV1 messages/values to SV2 messages (e.g. mining.submit → +//! SubmitSharesExtended) +//! - Uses existing utilities from channels_sv2 (e.g. target_to_difficulty) +//! +//! What it does not contain: +//! - Networking, async runtimes, channels, or long-running tasks +//! +//! Error handling: +//! - All public functions return `Result<_, error::StratumTranslationError>` with specific error +//! kinds to aid debugging and integration. + +pub mod error; +pub mod sv1_to_sv2; +pub mod sv2_to_sv1; diff --git a/roles/roles-utils/stratum-translation/src/sv1_to_sv2.rs b/roles/roles-utils/stratum-translation/src/sv1_to_sv2.rs new file mode 100644 index 0000000000..23e52cbd08 --- /dev/null +++ b/roles/roles-utils/stratum-translation/src/sv1_to_sv2.rs @@ -0,0 +1,195 @@ +use crate::error::{Result, StratumTranslationError}; +use mining_sv2::{OpenExtendedMiningChannel, SubmitSharesExtended, Target}; +use v1::{client_to_server, utils::HexU32Be}; + +/// Builds an SV2 `OpenExtendedMiningChannel` message from the provided inputs. +/// +/// # Arguments +/// * `request_id` - Unique identifier for the channel open request. +/// * `user_identity` - String representing the client's user identity. +/// * `nominal_hash_rate` - The client's nominal hashrate in H/s (as a float). +/// * `max_target` - The maximum target (difficulty) for the channel. +/// * `min_extranonce_size` - The minimum extranonce2 size required by the client. +/// +/// # Returns +/// * `Ok(OpenExtendedMiningChannel)` if the message is constructed successfully. +/// * `Err(())` if any input is invalid or conversion fails. +pub fn build_sv2_open_extended_mining_channel( + request_id: u32, + user_identity: String, + nominal_hash_rate: f32, + max_target: Target, + min_extranonce_size: u16, +) -> Result> { + Ok(OpenExtendedMiningChannel { + request_id, + user_identity: user_identity + .clone() + .try_into() + .map_err(|_| StratumTranslationError::InvalidUserIdentity(user_identity))?, + nominal_hash_rate, + max_target: max_target.into(), + min_extranonce_size, + }) +} + +/// Builds an SV2 `SubmitSharesExtended` from an SV1 `mining.submit`. +/// +/// # Arguments +/// * `submit` - Reference to the SV1 `mining.submit` message to convert. +/// * `channel_id` - The SV2 channel ID associated with this share submission. +/// * `sequence_number` - The SV2 sequence number for this share submission. +/// * `job_version` - The SV2 job version (from the last job sent to the client). +/// * `version_rolling_mask` - Optional SV1 version rolling mask, used to compute the SV2 version +/// field. +/// +/// # Returns +/// * `Ok(SubmitSharesExtended)` if the conversion is successful. +/// * `Err(())` if any required field is missing or conversion fails. +pub fn build_sv2_submit_shares_extended_from_sv1_submit( + submit: &client_to_server::Submit<'_>, + channel_id: u32, + sequence_number: u32, + job_version: u32, + version_rolling_mask: Option, +) -> Result> { + let version = match (submit.version_bits.clone(), version_rolling_mask) { + (Some(version_bits), Some(rolling_mask)) => { + (job_version & !rolling_mask.0) | (version_bits.0 & rolling_mask.0) + } + (None, None) => job_version, + _ => return Err(StratumTranslationError::IncompatibleVersionRollingMask), + }; + + let extranonce: Vec = submit.extra_nonce2.clone().into(); + let submit_share_extended = SubmitSharesExtended { + channel_id, + sequence_number, + job_id: submit + .job_id + .parse::() + .map_err(|_| StratumTranslationError::InvalidJobId)?, + nonce: submit.nonce.0, + ntime: submit.time.0, + version, + extranonce: extranonce + .try_into() + .map_err(|_| StratumTranslationError::InvalidExtranonceLength)?, + }; + Ok(submit_share_extended) +} + +#[cfg(test)] +mod tests { + use super::*; + use v1::{client_to_server::Submit, utils::HexU32Be}; + + fn submit_template() -> Submit<'static> { + Submit { + user_name: "w".to_string(), + job_id: "1".to_string(), + extra_nonce2: v1::utils::Extranonce::try_from(vec![0, 1, 2, 3]).unwrap(), + time: HexU32Be(0), + nonce: HexU32Be(0), + version_bits: Some(HexU32Be(0)), + id: 0, + } + } + + #[test] + fn test_build_sv2_submit_from_sv1_submit_happy() { + let s = submit_template(); + let res = build_sv2_submit_shares_extended_from_sv1_submit( + &s, + 1, + 100, + 0x20000000, + Some(HexU32Be(0x1fffe000)), + ); + assert!(res.is_ok()); + + let submit = res.unwrap(); + assert_eq!(submit.channel_id, 1); + assert_eq!(submit.sequence_number, 100); + assert_eq!(submit.job_id, 1); // from job_id "1" in template + assert_eq!(submit.nonce, 0); + assert_eq!(submit.ntime, 0); + // Version should be computed from job_version and version rolling + assert_eq!(submit.version, 0x20000000); // (0x20000000 & !0x1fffe000) | (0 & 0x1fffe000) + assert_eq!(submit.extranonce.len(), 4); // from vec![0, 1, 2, 3] + } + + #[test] + fn test_build_sv2_submit_from_sv1_submit_incompatible_mask() { + let mut s = submit_template(); + s.version_bits = None; + let res = build_sv2_submit_shares_extended_from_sv1_submit(&s, 1, 1, 0, Some(HexU32Be(0))); + assert!(res.is_err()); + + // Verify it's the specific error we expect + if let Err(e) = res { + assert!(matches!( + e, + StratumTranslationError::IncompatibleVersionRollingMask + )); + } + } + + #[test] + fn test_build_sv2_submit_no_version_bits_no_mask() { + let mut s = submit_template(); + s.version_bits = None; + let res = build_sv2_submit_shares_extended_from_sv1_submit(&s, 5, 10, 0x20000000, None); + assert!(res.is_ok()); + + let submit = res.unwrap(); + assert_eq!(submit.channel_id, 5); + assert_eq!(submit.sequence_number, 10); + assert_eq!(submit.version, 0x20000000); // Should use job_version directly + } + + #[test] + fn test_build_sv2_submit_invalid_job_id() { + let mut s = submit_template(); + s.job_id = "invalid_number".to_string(); + s.version_bits = None; // Ensure version compatibility + let res = build_sv2_submit_shares_extended_from_sv1_submit(&s, 1, 1, 0, None); + assert!(res.is_err()); + + if let Err(e) = res { + assert!(matches!(e, StratumTranslationError::InvalidJobId)); + } + } + + #[test] + fn test_build_sv2_open_extended_mining_channel_happy() { + let max_target: Target = [0xffu8; 32].into(); + let res = build_sv2_open_extended_mining_channel( + 123, + "user.worker1".to_string(), + 1000.5, + max_target, + 8, + ); + assert!(res.is_ok()); + + let channel = res.unwrap(); + assert_eq!(channel.request_id, 123); + assert_eq!(channel.nominal_hash_rate, 1000.5); + assert_eq!(channel.min_extranonce_size, 8); + // user_identity and max_target should be properly set but are internal types + } + + #[test] + fn test_build_sv2_open_extended_mining_channel_invalid_user() { + let max_target: Target = [0xffu8; 32].into(); + // Create a user identity that's too long (> 255 chars) + let long_user = "x".repeat(300); + let res = build_sv2_open_extended_mining_channel(1, long_user, 1.0, max_target, 8); + assert!(res.is_err()); + + if let Err(e) = res { + assert!(matches!(e, StratumTranslationError::InvalidUserIdentity(_))); + } + } +} diff --git a/roles/roles-utils/stratum-translation/src/sv2_to_sv1.rs b/roles/roles-utils/stratum-translation/src/sv2_to_sv1.rs new file mode 100644 index 0000000000..40c346e6b1 --- /dev/null +++ b/roles/roles-utils/stratum-translation/src/sv2_to_sv1.rs @@ -0,0 +1,269 @@ +//! SV2 to SV1 translation module +//! +//! This module provides functions to convert Stratum V2 (SV2) mining protocol messages +//! to Stratum V1 (SV1) format. It handles BIP141 (SegWit) data stripping and +//! protocol compatibility between the two versions. +//! +//! The main functions convert: +//! - SV2 mining jobs to SV1 notify messages +//! - SV2 difficulty targets to SV1 set_difficulty messages + +use crate::error::{Result, StratumTranslationError}; +use channels_sv2::{bip141::try_strip_bip141, target::target_to_difficulty}; +use mining_sv2::{NewExtendedMiningJob, SetNewPrevHash, SetTarget, Target}; +use tracing::debug; +use v1::{ + json_rpc, server_to_client, + utils::{HexU32Be, MerkleNode, PrevHash}, +}; +/// Builds an SV1 `mining.notify` message from SV2 messages. +/// +/// This function attempts to strip BIP141 (SegWit) data from the coinbase transaction +/// if present, creating a compatible SV1 mining job. If BIP141 data is not present, +/// the original job is used unchanged. +/// +/// # Arguments +/// * `new_prev_hash` - The SV2 `SetNewPrevHash` message containing the new previous hash and +/// related fields. +/// * `new_job` - The SV2 `NewExtendedMiningJob` message containing the new mining job details. +/// * `clean_jobs` - Boolean indicating whether the mining jobs should be cleaned (true if a new +/// block is found). +/// +/// # Returns +/// * `Ok(server_to_client::Notify<'static>)` - The constructed SV1 mining.notify message. +/// * `Err(StratumTranslationError)` - If BIP141 stripping or serialization fails. +/// +/// # Errors +/// * `FailedToTryToStripBip141` - When BIP141 data stripping fails +/// * `FailedToSerializeToB064K` - When serializing stripped data to B064K format fails +pub fn build_sv1_notify_from_sv2( + new_prev_hash: SetNewPrevHash<'static>, + new_job: NewExtendedMiningJob<'static>, + clean_jobs: bool, +) -> Result> { + let new_job = match try_strip_bip141( + new_job.coinbase_tx_prefix.inner_as_ref(), + new_job.coinbase_tx_suffix.inner_as_ref(), + ) + .map_err(StratumTranslationError::FailedToTryToStripBip141)? + { + Some((coinbase_tx_prefix_stripped, coinbase_tx_suffix_stripped)) => { + // Create a new job with stripped BIP141 data + let mut new_job_stripped = new_job.clone(); + new_job_stripped.coinbase_tx_prefix = coinbase_tx_prefix_stripped + .try_into() + .map_err(|_| StratumTranslationError::FailedToSerializeToB064K)?; + new_job_stripped.coinbase_tx_suffix = coinbase_tx_suffix_stripped + .try_into() + .map_err(|_| StratumTranslationError::FailedToSerializeToB064K)?; + new_job_stripped + } + None => new_job, + }; + + let job_id = new_job.job_id.to_string(); + let prev_hash = PrevHash(new_prev_hash.prev_hash.clone()); + let coin_base1 = new_job.coinbase_tx_prefix.to_vec().into(); + let coin_base2 = new_job.coinbase_tx_suffix.to_vec().into(); + let merkle_path = new_job.merkle_path.clone().into_static().0; + let merkle_branch: Vec = merkle_path.into_iter().map(MerkleNode).collect(); + let version = HexU32Be(new_job.version); + let bits = HexU32Be(new_prev_hash.nbits); + let time = HexU32Be(if new_job.is_future() { + new_prev_hash.min_ntime + } else { + new_job.min_ntime.clone().into_inner().unwrap() + }); + + let notify_response = server_to_client::Notify { + job_id, + prev_hash, + coin_base1, + coin_base2, + merkle_branch, + version, + bits, + time, + clean_jobs, + }; + debug!("\nNextMiningNotify: {:?}\n", notify_response); + Ok(notify_response) +} + +/// Builds an SV1 `mining.set_difficulty` JSON-RPC message from an SV2 `SetTarget`. +/// +/// # Arguments +/// * `set_target` - The SV2 `SetTarget` message containing the new maximum target. +/// +/// # Returns +/// * `Ok(json_rpc::Message)` - The constructed SV1 mining.set_difficulty message. +pub fn build_sv1_set_difficulty_from_sv2_set_target( + set_target: SetTarget<'_>, +) -> Result { + build_sv1_set_difficulty_from_sv2_target(set_target.maximum_target.into()) +} + +/// Builds an SV1 `mining.set_difficulty` JSON-RPC message from an SV2 target. +/// +/// # Arguments +/// * `target` - The SV2 `Target` value to convert to SV1 set_difficulty. +/// +/// # Returns +/// * `Ok(json_rpc::Message)` - The constructed SV1 mining.set_difficulty message. +pub fn build_sv1_set_difficulty_from_sv2_target(target: Target) -> Result { + let value = target_to_difficulty(target); + let set_target = v1::methods::server_to_client::SetDifficulty { value }; + Ok(set_target.into()) +} + +#[cfg(test)] +mod tests { + use super::*; + use binary_sv2::{Seq0255, Sv2Option, U256}; + use mining_sv2::{NewExtendedMiningJob, SetNewPrevHash, SetTarget as Sv2SetTarget, Target}; + + fn dummy_target() -> Target { + [0xffu8; 32].into() + } + + #[test] + fn test_build_sv1_set_difficulty_from_sv2_target() { + let msg = build_sv1_set_difficulty_from_sv2_target(dummy_target()) + .expect("Should convert target to difficulty"); + + // Check that we get a JSON-RPC Notification message + assert!(matches!(msg, v1::json_rpc::Message::Notification(_))); + + if let v1::json_rpc::Message::Notification(notif) = msg { + assert_eq!(notif.method, "mining.set_difficulty"); + assert!(!notif.params.is_null()); + // Just verify it has parameters - detailed checking would require serde_json + } + } + + #[test] + fn test_build_sv1_set_difficulty_from_sv2_set_target() { + let set_target = Sv2SetTarget { + channel_id: 1, + maximum_target: dummy_target().into(), + }; + let msg = build_sv1_set_difficulty_from_sv2_set_target(set_target) + .expect("Should convert SetTarget to difficulty"); + + // Verify the result is a proper JSON-RPC Notification message + assert!(matches!(msg, v1::json_rpc::Message::Notification(_))); + + if let v1::json_rpc::Message::Notification(notif) = msg { + assert_eq!(notif.method, "mining.set_difficulty"); + assert!(!notif.params.is_null()); + // Just verify it has parameters - detailed checking would require serde_json + } + } + + #[test] + fn test_build_sv1_notify_from_sv2_with_future_job() { + // Test with a future job using realistic data from existing tests + let new_prev = SetNewPrevHash { + channel_id: 1, + job_id: 456, + prev_hash: [ + 200, 53, 253, 129, 214, 31, 43, 84, 179, 58, 58, 76, 128, 213, 24, 53, 38, 144, + 205, 88, 172, 20, 251, 22, 217, 141, 21, 221, 21, 0, 0, 0, + ] + .into(), + min_ntime: 1746839904, + nbits: 503543726, + }; + + // A future job (min_ntime is None) + let job = NewExtendedMiningJob { + channel_id: 1, + job_id: 456, + version: 536870912, + version_rolling_allowed: true, + merkle_path: Seq0255::new(vec![U256::from([0x03u8; 32])]).unwrap(), + min_ntime: Sv2Option::new(None), // Future job + coinbase_tx_prefix: vec![ + 2, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 34, 82, 0, + ] + .try_into() + .unwrap(), + coinbase_tx_suffix: vec![ + 255, 255, 255, 255, 2, 0, 242, 5, 42, 1, 0, 0, 0, 22, 0, 20, 235, 225, 183, 220, + 194, 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194, 8, 252, 0, 0, 0, + 0, 0, 0, 0, 0, 38, 106, 36, 170, 33, 169, 237, 226, 246, 28, 63, 113, 209, 222, + 253, 63, 169, 153, 223, 163, 105, 83, 117, 92, 105, 6, 137, 121, 153, 98, 180, 139, + 235, 216, 54, 151, 78, 140, 249, 1, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ] + .try_into() + .unwrap(), + }; + + let res = build_sv1_notify_from_sv2(new_prev.into_static(), job.into_static(), true); + assert!(res.is_ok()); + + // Verify it uses prev_hash.min_ntime since job is future + let notify = res.unwrap(); + assert_eq!(notify.time.0, 1746839904); + } + + #[test] + fn test_build_sv1_notify_from_sv2_with_non_future_job() { + // Test with a non-future job using realistic data from existing tests + let new_prev = SetNewPrevHash { + channel_id: 1, + job_id: 456, + prev_hash: [ + 200, 53, 253, 129, 214, 31, 43, 84, 179, 58, 58, 76, 128, 213, 24, 53, 38, 144, + 205, 88, 172, 20, 251, 22, 217, 141, 21, 221, 21, 0, 0, 0, + ] + .into(), + min_ntime: 1746839904, + nbits: 503543726, + }; + + // A non-future job with realistic coinbase and merkle data from existing tests + let job = NewExtendedMiningJob { + channel_id: 1, + job_id: 456, + version: 536870912, + version_rolling_allowed: true, + merkle_path: Seq0255::new(vec![U256::from([0x03u8; 32])]).unwrap(), + min_ntime: Sv2Option::new(Some(1746839905)), // Non-future job with specific timestamp + coinbase_tx_prefix: vec![ + 2, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 34, 82, 0, + ] + .try_into() + .unwrap(), + coinbase_tx_suffix: vec![ + 255, 255, 255, 255, 2, 0, 242, 5, 42, 1, 0, 0, 0, 22, 0, 20, 235, 225, 183, 220, + 194, 147, 204, 170, 14, 231, 67, 168, 111, 137, 223, 130, 88, 194, 8, 252, 0, 0, 0, + 0, 0, 0, 0, 0, 38, 106, 36, 170, 33, 169, 237, 226, 246, 28, 63, 113, 209, 222, + 253, 63, 169, 153, 223, 163, 105, 83, 117, 92, 105, 6, 137, 121, 153, 98, 180, 139, + 235, 216, 54, 151, 78, 140, 249, 1, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ] + .try_into() + .unwrap(), + }; + + let res = build_sv1_notify_from_sv2(new_prev.into_static(), job.into_static(), false); + assert!(res.is_ok()); + + // Verify the notify message structure for non-future job + let notify = res.unwrap(); + assert_eq!(notify.job_id, "456"); + assert!(!notify.clean_jobs); // clean_jobs set to false + assert_eq!(notify.merkle_branch.len(), 1); // One merkle node + assert_eq!(notify.version.0, 536870912); + assert_eq!(notify.bits.0, 503543726); + assert_eq!(notify.time.0, 1746839905); // Should use job's min_ntime since not future + + // Verify coinbase prefix and suffix are properly set + assert!(!notify.coin_base1.is_empty()); + assert!(!notify.coin_base2.is_empty()); + } +} diff --git a/roles/test-utils/mining-device-sv1/Cargo.toml b/roles/test-utils/mining-device-sv1/Cargo.toml deleted file mode 100644 index 69cc37441a..0000000000 --- a/roles/test-utils/mining-device-sv1/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "mining_device_sv1" -version = "0.1.0" -authors = ["The Stratum V2 Developers"] -edition = "2018" -publish = false -documentation = "https://github.com/stratum-mining/stratum" -readme = "README.md" -homepage = "https://stratumprotocol.org" -repository = "https://github.com/stratum-mining/stratum" -license = "MIT OR Apache-2.0" -keywords = ["stratum", "mining", "bitcoin", "protocol"] - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[lib] -name = "mining_device_sv1" -path = "src/lib.rs" - -[dependencies] -stratum-common = { path = "../../../common" } -async-channel = "1.5.1" -roles_logic_sv2 = { path = "../../../protocols/v2/roles-logic-sv2" } -serde = { version = "1.0.89", default-features = false, features = ["derive", "alloc"] } -serde_json = { version = "1.0.64", default-features = false, features = ["alloc"] } -v1 = { path="../../../protocols/v1", package="sv1_api" } -num-bigint = "0.4.3" -num-traits = "0.2.15" -tracing = "0.1.41" -tracing-subscriber = "0.3.19" -tokio = { version = "1.44.1", features = ["full"] } -primitive-types = "0.13.1" diff --git a/roles/test-utils/mining-device-sv1/src/client.rs b/roles/test-utils/mining-device-sv1/src/client.rs deleted file mode 100644 index 3b6ff89218..0000000000 --- a/roles/test-utils/mining-device-sv1/src/client.rs +++ /dev/null @@ -1,532 +0,0 @@ -use crate::{job::Job, miner::Miner}; -use async_channel::{unbounded, Receiver, Sender}; -use num_bigint::BigUint; -use num_traits::FromPrimitive; -use primitive_types::U256; -use roles_logic_sv2::utils::Mutex; -use std::{ - convert::TryInto, - net::SocketAddr, - ops::Div, - sync::Arc, - time::{self, Duration}, -}; -use tokio::{ - io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, - net::TcpStream, - task, - time::sleep, -}; -use tracing::{error, info, warn}; -use v1::{ - client_to_server, - error::Error, - json_rpc, server_to_client, - utils::{Extranonce, HexU32Be}, - ClientStatus, IsClient, -}; - -/// Represents the Mining Device client which is connected to a Upstream node (either a SV1 Pool -/// server or a SV1 <-> SV2 Translator Proxy server). -#[derive(Debug, Clone)] -pub struct Client { - client_id: u32, - extranonce1: Option>, - extranonce2_size: Option, - version_rolling_mask: Option, - version_rolling_min_bit: Option, - pub(crate) status: ClientStatus, - sented_authorize_request: Vec<(u64, String)>, // (id, user_name) - authorized: Vec, - /// Receives incoming messages from the SV1 Upstream node. - receiver_incoming: Receiver, - /// Sends outgoing messages to the SV1 Upstream node. - sender_outgoing: Sender, - /// Representation of the Mining Devices - miner: Arc>, -} - -impl Client { - /// Outgoing channels are used to send messages to the Upstream - /// Incoming channels are used to receive messages from the Upstream - /// There are three separate channels, the first two are responsible for receiving and sending - /// messages to the Upstream, and the third is responsible for pass valid job submissions to - /// the first set of channels: - /// 1. `(sender_incoming, receiver_incoming)`: `sender_incoming` listens on the socket where - /// messages are being sent from the Upstream node. From the socket, it reads the incoming - /// bytes from the Upstream into a `BufReader`. The incoming bytes represent a message from - /// the Upstream, and each new line is a new message. When it gets this line (a message) from - /// the Upstream, it sends them to the `receiver_incoming` which is listening in a loop. The - /// message line received by the `receiver_incoming` are then parsed by the `Client` in the - /// `parse_message` method to be handled. - /// 2. `(sender_outgoing, receiver_outgoing)`: When the `parse_message` method on the `Client` - /// is called, it handles the message and formats the a new message to be sent to the - /// Upstream in response. It sends the response message via the `sender_outgoing` to the - /// `receiver_outgoing` which is waiting to receive a message in its own task. When the - /// `receiver_outgoing` receives the response message from the the `sender_outgoing`, it - /// writes this message to the socket connected to the Upstream via `write_all`. - /// 3. `(sender_share, receiver_share)`: A new thread is spawned to mock the act of a Miner - /// hashing over a candidate block without blocking the rest of the program. Since this in - /// its own thread, we need a channel to communicate with it, which is `(sender_share, - /// receiver_share)`. In this thread, on each new share, `sender_share` sends the pertinent - /// information to create a `mining.submit` message to the `receiver_share` that is waiting - /// to receive this information in a separate task. In this task, once `receiver_share` gets - /// the information from `sender_share`, it is formatted as a `v1::client_to_server::Submit` - /// and then serialized into a json message that is sent to the Upstream via - /// `sender_outgoing`. - pub async fn connect( - client_id: u32, - upstream_addr: SocketAddr, - single_submit: bool, - custom_target: Option<[u8; 32]>, - ) { - let stream = loop { - if let Ok(stream) = TcpStream::connect(upstream_addr).await { - break stream; - } - info!( - "SV1 Miner: Failed to connect to upstream at {} Retrying in 1 second.", - upstream_addr - ); - sleep(Duration::from_secs(1)).await; - }; - let (reader, mut writer) = stream.into_split(); - - // `sender_incoming` listens on socket for incoming messages from the Upstream and sends - // messages to the `receiver_incoming` to be parsed and handled by the `Client` - let (sender_incoming, receiver_incoming) = unbounded(); - // `sender_outgoing` sends the message parsed by the `Client` to the `receiver_outgoing` - // which writes the messages to the socket to the Upstream - let (sender_outgoing, receiver_outgoing) = unbounded(); - // `sender_share` sends job share results to the `receiver_share` where the job share - // results are formated into a "mining.submit" messages that is then sent to the - // Upstream via `sender_outgoing` - let (sender_share, receiver_share) = unbounded(); - - let (send_stop_submitting, mut recv_stop_submitting) = tokio::sync::watch::channel(false); - // Instantiates a new `Miner` (a mock of an actual Mining Device) with a job id of 0. - let miner = Arc::new(Mutex::new(Miner::new(0))); - - // Sets an initial target for the `Miner`. - // TODO: This is hard coded for the purposes of a demo, should be set by the SV1 - // `mining.set_difficulty` message received from the Upstream role - let target_vec: [u8; 32] = custom_target.unwrap_or([ - 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, - ]); - let default_target = U256::from_big_endian(target_vec.as_ref()); - miner.safe_lock(|m| m.new_target(default_target)).unwrap(); - - let miner_cloned = miner.clone(); - - // Reads messages sent by the Upstream from the socket to be passed to the - // `receiver_incoming` - task::spawn(async move { - tokio::select!( - _ = tokio::signal::ctrl_c() => { }, - _ = async { - let mut messages = BufReader::new(reader).lines(); - while let Ok(message) = messages.next_line().await { - match message { - Some(msg) => { - if let Err(e) = sender_incoming.send(msg).await { - error!("Failed to send message to receiver_incoming: {:?}", e); - break; // Exit the loop if sending fails - } - } - None => { - error!("Error reading from socket"); - break; // Exit the loop on read failure - } - } - } - error!("Reader task terminated."); - } => {} - ) - }); - - // Waits to receive a message from `sender_outgoing` and writes it to the socket for the - // Upstream to receive - task::spawn(async move { - tokio::select!( - _ = tokio::signal::ctrl_c() => { }, - _ = async { - loop { - let message: String = receiver_outgoing.recv().await.expect("SV1 Miner: Failed to receive message"); - (writer).write_all(message.as_bytes()).await.expect("SV1 Miner: Failed to write message to socket"); - if message.contains("mining.submit") && single_submit { - send_stop_submitting.send(true).expect("SV1 Miner: Failed to send stop submitting"); - } - } - } => {} - ) - }); - - // Clone the sender to the Upstream node to use it in another task below as - // `sender_outgoing` is consumed by the initialization of `Client` - let sender_outgoing_clone = sender_outgoing.clone(); - - // Initialize Client - let client = Arc::new(Mutex::new(Client { - client_id, - extranonce1: None, - extranonce2_size: None, - version_rolling_mask: None, - version_rolling_min_bit: None, - status: ClientStatus::Init, - sented_authorize_request: vec![], - authorized: vec![], - receiver_incoming, - sender_outgoing, - miner, - })); - - // configure subscribe and authorize - Self::send_configure(client.clone()).await; - - // Gets the latest candidate block header hash from the `Miner` by calling the `next_share` - // method. Mocks the act of the `Miner` incrementing the nonce. Performs this in a loop, - // incrementing the nonce each time, to mimic a Mining Device generating continuous hashes. - // For each generated block header, sends to the `receiver_share` the relevant values that - // generated the candidate block header needed to then format and send as a "mining.submit" - // message to the Upstream node. - // Is a separate thread as it can be CPU intensive and we do not want to block the reading - // and writing of messages to the socket. - std::thread::spawn(move || loop { - if miner_cloned.safe_lock(|m| m.next_share()).unwrap().is_ok() { - let nonce = miner_cloned.safe_lock(|m| m.header.unwrap().nonce).unwrap(); - let time = miner_cloned.safe_lock(|m| m.header.unwrap().time).unwrap(); - let job_id = miner_cloned.safe_lock(|m| m.job_id).unwrap(); - let version = miner_cloned.safe_lock(|m| m.version).unwrap(); - // Sends relevant candidate block header values needed to construct a - // `mining.submit` message to the `receiver_share` in the task that is responsible - // for sending messages to the Upstream node. - if sender_share - .try_send((nonce, job_id.unwrap(), version.unwrap(), time)) - .is_err() - { - warn!("Share channel is not available"); - break; - } - // Introduce a delay of 0.2 seconds after sending a share - std::thread::sleep(Duration::from_millis(200)); - } - miner_cloned - .safe_lock(|m| m.header.as_mut().map(|h| h.nonce += 1)) - .unwrap(); - }); - // Task to receive relevant candidate block header values needed to construct a - // `mining.submit` message. This message is contructed as a `client_to_server::Submit` and - // then serialized into json to be sent to the Upstream via the `sender_outgoing` sender. - let cloned = client.clone(); - task::spawn(async move { - tokio::select!( - _ = recv_stop_submitting.changed() => { - warn!("Stopping miner") - }, - _ = tokio::signal::ctrl_c() => { - info!("Stopping miner"); - }, - _ = async { - let recv = receiver_share.clone(); - loop { - let (nonce, job_id, _version, ntime) = recv.recv().await.unwrap(); - if cloned.clone().safe_lock(|c| c.status).unwrap() != ClientStatus::Subscribed { - continue; - } - let extra_nonce2: Extranonce = - vec![0; cloned.safe_lock(|c| c.extranonce2_size.unwrap()).unwrap()] - .try_into() - .unwrap(); - let submit = client_to_server::Submit { - id: 0, - user_name: "user".into(), // TODO: user name should NOT be hardcoded - job_id: job_id.to_string(), - extra_nonce2, - time: HexU32Be(ntime), - nonce: HexU32Be(nonce), - version_bits: None, - }; - let message: json_rpc::Message = submit.into(); - let message = format!("{}\n", serde_json::to_string(&message).unwrap()); - sender_outgoing_clone.send(message).await.unwrap(); - } - } => {} - ) - }); - let recv_incoming = client.safe_lock(|c| c.receiver_incoming.clone()).unwrap(); - - loop { - match client.clone().safe_lock(|c| c.status).unwrap() { - ClientStatus::Init => panic!("impossible state"), - ClientStatus::Configured => { - let incoming = recv_incoming.clone().recv().await.unwrap(); - Self::parse_message(client.clone(), Ok(incoming)).await; - } - ClientStatus::Subscribed => { - Self::send_authorize(client.clone()).await; - break; - } - } - } - // Waits for the `sender_incoming` to get message line from socket to be parsed by the - // `Client` - tokio::select!( - _ = tokio::signal::ctrl_c() => { - warn!("Stopping sv1 miner"); - }, - _ = async { - loop { - if let Ok(incoming) = recv_incoming.clone().recv().await { - Self::parse_message(client.clone(), Ok(incoming)).await; - } else { - warn!("Error reading from socket via `recv_incoming` channel"); - break; - } - } - } => {} - ); - } - - /// Parse SV1 messages received from the Upstream node. - async fn parse_message( - self_: Arc>, - incoming_message: Result, - ) { - // If we have a line (1 line represents 1 sv1 incoming message), then handle that message - if let Ok(line) = incoming_message { - info!( - "CLIENT {} - Received: {}", - self_.safe_lock(|s| s.client_id).unwrap(), - line - ); - let message: json_rpc::Message = serde_json::from_str(&line).unwrap(); - // If has a message, it sends it back - if let Some(m) = self_ - .safe_lock(|s| s.handle_message(message).unwrap()) - .unwrap() - { - let sender = self_.safe_lock(|s| s.sender_outgoing.clone()).unwrap(); - Self::send_message(sender, m).await; - } - }; - } - - /// Send SV1 messages to the receiver_outgoing which writes to the socket (aka Upstream node) - async fn send_message(sender: Sender, msg: json_rpc::Message) { - let msg = format!("{}\n", serde_json::to_string(&msg).unwrap()); - info!(" - Send: {}", &msg); - sender.send(msg).await.unwrap(); - } - - pub(crate) async fn send_configure(self_: Arc>) { - // This loop is probably unnecessary as the first state is `Init` - loop { - if let ClientStatus::Init = self_.safe_lock(|s| s.status).unwrap() { - break; - } - } - let id = time::SystemTime::now() - .duration_since(time::SystemTime::UNIX_EPOCH) - .unwrap() - .as_secs(); - let configure = self_.safe_lock(|s| s.configure(id)).unwrap(); - let sender = self_.safe_lock(|s| s.sender_outgoing.clone()).unwrap(); - Self::send_message(sender, configure).await; - // Update status as configured - self_ - .safe_lock(|s| s.status = ClientStatus::Configured) - .unwrap(); - } - - pub async fn send_authorize(self_: Arc>) { - let id = time::SystemTime::now() - .duration_since(time::SystemTime::UNIX_EPOCH) - .unwrap() - .as_secs(); - let authorize = self_ - .safe_lock(|s| { - s.authorize(id, "user".to_string(), "password".to_string()) - .unwrap() - }) - .unwrap(); - self_ - .safe_lock(|s| s.sented_authorize_request.push((id, "user".to_string()))) - .unwrap(); - let sender = self_.safe_lock(|s| s.sender_outgoing.clone()).unwrap(); - - Self::send_message(sender, authorize).await; - } -} - -impl IsClient<'static> for Client { - /// Updates miner with new job - fn handle_notify( - &mut self, - notify: server_to_client::Notify<'static>, - ) -> Result<(), Error<'static>> { - let mut extranonce: Vec = self.extranonce1.clone().unwrap().into(); - for _ in 0..self.extranonce2_size.unwrap() { - extranonce.push(0) - } - - let new_job = Job::from_notify(notify, extranonce); - self.miner.safe_lock(|m| m.new_header(new_job)).unwrap(); - Ok(()) - } - - fn handle_configure( - &mut self, - _conf: &mut server_to_client::Configure, - ) -> Result<(), Error<'static>> { - Ok(()) - } - - fn handle_subscribe( - &mut self, - _subscribe: &server_to_client::Subscribe, - ) -> Result<(), Error<'static>> { - Ok(()) - } - - fn set_extranonce1(&mut self, extranonce1: Extranonce<'static>) { - self.extranonce1 = Some(extranonce1); - } - - fn extranonce1(&self) -> Extranonce<'static> { - self.extranonce1.clone().unwrap() - } - - fn set_extranonce2_size(&mut self, extra_nonce2_size: usize) { - self.extranonce2_size = Some(extra_nonce2_size); - } - - fn extranonce2_size(&self) -> usize { - self.extranonce2_size.unwrap() - } - - fn version_rolling_mask(&self) -> Option { - self.version_rolling_mask.clone() - } - - fn set_version_rolling_mask(&mut self, mask: Option) { - self.version_rolling_mask = mask; - } - - fn set_version_rolling_min_bit(&mut self, min: Option) { - self.version_rolling_min_bit = min; - } - - fn set_status(&mut self, status: ClientStatus) { - self.status = status; - } - - fn signature(&self) -> String { - format!("{}", self.client_id) - } - - fn status(&self) -> ClientStatus { - self.status - } - - fn version_rolling_min_bit(&mut self) -> Option { - self.version_rolling_min_bit.clone() - } - - fn id_is_authorize(&mut self, id: &u64) -> Option { - let req: Vec<&(u64, String)> = self - .sented_authorize_request - .iter() - .filter(|x| x.0 == *id) - .collect(); - match req.len() { - 0 => None, - _ => Some(req[0].1.clone()), - } - } - - fn id_is_submit(&mut self, _: &u64) -> bool { - false - } - - fn authorize_user_name(&mut self, name: String) { - self.authorized.push(name) - } - - fn is_authorized(&self, name: &String) -> bool { - self.authorized.contains(name) - } - - fn authorize( - &mut self, - id: u64, - name: String, - password: String, - ) -> Result { - match self.status() { - ClientStatus::Init => Err(Error::IncorrectClientStatus("mining.authorize".to_string())), - _ => { - self.sented_authorize_request.push((id, "user".to_string())); - Ok(client_to_server::Authorize { id, name, password }.into()) - } - } - } - - fn last_notify(&self) -> Option { - None - } - - fn handle_error_message( - &mut self, - _message: v1::Message, - ) -> Result, Error<'static>> { - Ok(None) - } - - fn handle_set_difficulty( - &mut self, - conf: &mut server_to_client::SetDifficulty, - ) -> Result<(), Error<'static>> { - let dif = conf.value; - let target = - target_from_difficulty(dif).unwrap_or_else(|| panic!("Invalid difficulty: {}", dif)); - self.miner.safe_lock(|m| m.target = Some(target)).unwrap(); - Ok(()) - } - - fn handle_set_extranonce( - &mut self, - _conf: &mut server_to_client::SetExtranonce, - ) -> Result<(), Error<'static>> { - Ok(()) - } - - fn handle_set_version_mask( - &mut self, - _conf: &mut server_to_client::SetVersionMask, - ) -> Result<(), Error<'static>> { - Ok(()) - } -} - -fn target_from_difficulty(diff: f64) -> Option { - let pdiff = 26959946667150639794667015087019630673637144422540572481103610249215.0; - if diff == 0.0 { - Some(U256::from_big_endian(&[0; 32])) - } else { - let t = pdiff.div(diff); - let as_big_int: BigUint = match t > 0.0 { - true => BigUint::from_f64(t)?, - false => BigUint::from_f64(1.0 / t)?, - }; - let mut bytes = as_big_int.to_bytes_be(); - if bytes.len() > 32 { - None - } else { - let mut front_padding = vec![0; 32 - bytes.len()]; - front_padding.append(&mut bytes); - let as_u256: [u8; 32] = front_padding.try_into().unwrap(); - Some(U256::from_big_endian(as_u256.as_ref())) - } - } -} diff --git a/roles/test-utils/mining-device-sv1/src/job.rs b/roles/test-utils/mining-device-sv1/src/job.rs deleted file mode 100644 index 1d6b3d2bcd..0000000000 --- a/roles/test-utils/mining-device-sv1/src/job.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::convert::TryInto; -use v1::server_to_client; - -/// Represents a new Job built from an incoming `mining.notify` message from the Upstream server. -pub(crate) struct Job { - /// ID of the job used while submitting share generated from this job. - /// TODO: Currently is `u32` and is hardcoded, but should be String and set by the incoming - /// `mining.notify` message. - pub(crate) job_id: u32, - /// Hash of previous block - pub(crate) prev_hash: [u8; 32], - /// Merkle root - /// TODO: Currently is hardcoded. This field should be replaced with three fields: 1) - /// `coinbase_1` - the first half of the coinbase transaction before the `extranonce` which is - /// inserted by the miner, 2) `coinbase_2` - the second half of the coinbase transaction after - /// the `extranonce` which is inserted by the miner, and 3) `merkle_branches` - the merkle - /// branches to build the merkle root sans the coinbase transaction - // coinbase_1: Vec, - // coinbase_2: Vec, - // merkle_brances: Vec<[u8; 32]>, - pub(crate) merkle_root: [u8; 32], - pub(crate) version: u32, - pub(crate) nbits: u32, -} - -impl Job { - pub fn from_notify(notify_msg: server_to_client::Notify<'_>, extranonce: Vec) -> Self { - let job_id = notify_msg - .job_id - .parse::() - .expect("expect valid job_id on String"); - - // Convert prev hash from Vec into expected [u32; 8] - let prev_hash_vec: Vec = notify_msg.prev_hash.into(); - let prev_hash_slice: &[u8] = prev_hash_vec.as_slice(); - let prev_hash: &[u8; 32] = prev_hash_slice.try_into().expect("Expected len 32"); - let prev_hash = *prev_hash; - - let coinbase_tx_prefix: Vec = notify_msg.coin_base1.into(); - let coinbase_tx_suffix: Vec = notify_msg.coin_base2.into(); - let path: Vec> = notify_msg - .merkle_branch - .into_iter() - .map(|node| node.into()) - .collect(); - - let merkle_root = roles_logic_sv2::utils::merkle_root_from_path( - &coinbase_tx_prefix, - &coinbase_tx_suffix, - &extranonce, - &path, - ) - .unwrap(); - let merkle_root: [u8; 32] = merkle_root.try_into().unwrap(); - - Job { - job_id, - prev_hash, - nbits: notify_msg.bits.0, - version: notify_msg.version.0, - merkle_root, - } - } -} diff --git a/roles/test-utils/mining-device-sv1/src/lib.rs b/roles/test-utils/mining-device-sv1/src/lib.rs deleted file mode 100644 index 4a3dd2a1e3..0000000000 --- a/roles/test-utils/mining-device-sv1/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod client; -pub mod job; -pub mod miner; diff --git a/roles/test-utils/mining-device-sv1/src/main.rs b/roles/test-utils/mining-device-sv1/src/main.rs deleted file mode 100644 index a89ff8d383..0000000000 --- a/roles/test-utils/mining-device-sv1/src/main.rs +++ /dev/null @@ -1,20 +0,0 @@ -pub(crate) mod client; -pub(crate) mod job; -pub(crate) mod miner; -use std::{net::SocketAddr, str::FromStr}; - -pub(crate) use client::Client; - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt().init(); - - const ADDR: &str = "127.0.0.1:34255"; - Client::connect( - 80, - SocketAddr::from_str(ADDR).expect("Invalid upstream address"), - false, - None, - ) - .await -} diff --git a/roles/test-utils/mining-device-sv1/src/miner.rs b/roles/test-utils/mining-device-sv1/src/miner.rs deleted file mode 100644 index ecd31eb496..0000000000 --- a/roles/test-utils/mining-device-sv1/src/miner.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::job::Job; -use primitive_types::U256; -use std::convert::TryInto; -use stratum_common::bitcoin::{ - blockdata::block::{Header, Version}, - hash_types::{BlockHash, TxMerkleNode}, - hashes::{sha256d::Hash as DHash, Hash}, - CompactTarget, -}; -use tracing::info; - -/// A mock representation of a Mining Device that produces block header hashes to be submitted by -/// the `Client` to the Upstream node (either a SV1 Pool server or a SV1 <-> SV2 Translator Proxy -/// server). -#[derive(Debug)] -pub(crate) struct Miner { - /// Mock of mined candidate block header. - pub(crate) header: Option
, - /// Current mining target. - pub(crate) target: Option, - /// ID of the job used while submitting share generated from this job. - pub(crate) job_id: Option, - /// Block header version - pub(crate) version: Option, - /// TODO: RRQ: Remove? - pub(crate) _handicap: u32, -} - -impl Miner { - /// Instantiates a new Miner instance. - pub(crate) fn new(handicap: u32) -> Self { - Self { - target: None, - header: None, - job_id: None, - version: None, - _handicap: handicap, - } - } - - /// Updates target when a new target is received by the SV1 `Client`. - pub(crate) fn new_target(&mut self, target: U256) { - self.target = Some(target); - } - - /// Mocks out the mining of a new candidate block header. - /// `Client` calls `new_header` when it receives a new `mining.notify` message from the - /// Upstream node indicating the `Miner` should start mining on a new job. - pub(crate) fn new_header(&mut self, new_job: Job) { - self.job_id = Some(new_job.job_id); - self.version = Some(new_job.version); - let prev_hash: [u8; 32] = new_job.prev_hash; - let prev_hash = DHash::from_byte_array(prev_hash); - let merkle_root: [u8; 32] = new_job.merkle_root.to_vec().try_into().unwrap(); - let merkle_root = DHash::from_byte_array(merkle_root); - let header = Header { - version: Version::from_consensus(new_job.version as i32), - prev_blockhash: BlockHash::from_raw_hash(prev_hash), - merkle_root: TxMerkleNode::from_raw_hash(merkle_root), - time: std::time::SystemTime::now() - .duration_since( - std::time::SystemTime::UNIX_EPOCH - std::time::Duration::from_secs(60), - ) - .unwrap() - .as_secs() as u32, - bits: CompactTarget::from_consensus(new_job.nbits), - nonce: 0, - }; - self.header = Some(header); - } - - /// Called by the `Client` to retrieve the latest candidate block header hash. The actual - /// incrementing of the nonce is mocked out in a thread in `Client::new()`. - pub(crate) fn next_share(&mut self) -> Result<(), ()> { - let header = self.header.as_ref().ok_or(())?; - let hash_ = header.block_hash(); - let mut hash: [u8; 32] = *hash_.to_raw_hash().as_ref(); - hash.reverse(); - let hash = U256::from_big_endian(hash.as_ref()); - if hash < *self.target.as_ref().ok_or(())? { - info!( - "Found share with nonce: {}, for target: {:?}, hash: {:?}", - header.nonce, self.target, hash - ); - Ok(()) - } else { - Err(()) - } - } -} diff --git a/roles/test-utils/mining-device/Cargo.toml b/roles/test-utils/mining-device/Cargo.toml index a9d6e386e9..b688b67006 100644 --- a/roles/test-utils/mining-device/Cargo.toml +++ b/roles/test-utils/mining-device/Cargo.toml @@ -2,7 +2,7 @@ name = "mining_device" version = "0.1.3" authors = ["The Stratum V2 Developers"] -edition = "2018" +edition = "2021" publish = false documentation = "https://github.com/stratum-mining/stratum" readme = "README.md" @@ -20,13 +20,9 @@ path = "src/lib/mod.rs" [dependencies] -codec_sv2 = { path = "../../../protocols/v2/codec-sv2", features=["noise_sv2"] } -roles_logic_sv2 = { path = "../../../protocols/v2/roles-logic-sv2" } -stratum-common = { path = "../../../common" } +stratum-common = { path = "../../../common", features = ["with_network_helpers"] } async-channel = "1.5.1" -binary_sv2 = { path = "../../../protocols/v2/binary-sv2" } -network_helpers_sv2 = { path = "../../roles-utils/network-helpers" } -buffer_sv2 = { path = "../../../utils/buffer"} +buffer_sv2 = { path = "../../../utils/buffer" } async-recursion = "0.3.2" rand = "0.8.4" futures = "0.3.5" @@ -34,6 +30,25 @@ key-utils = { path = "../../../utils/key-utils" } clap = { version = "^4.5.4", features = ["derive"] } tracing = { version = "0.1" } tracing-subscriber = "0.3" -sha2 = "0.10.6" +sha2 = { version = "0.10.6", features = ["compress", "asm"] } tokio = "1.44.1" primitive-types = "0.13.1" +num-format = "0.4" + +[dev-dependencies] +# Criterion 0.5 without default features; combined with a dev pin of `half = 2.3.1` to stay Rust 1.75-compatible. +criterion = { version = "0.5", default-features = false, features = ["stable"] } +half = "=2.3.1" +num_cpus = "1" + +[[bench]] +name = "hasher_bench" +harness = false + +[[bench]] +name = "microbatch_bench" +harness = false + +[[bench]] +name = "scaling_bench" +harness = false diff --git a/roles/test-utils/mining-device/README.md b/roles/test-utils/mining-device/README.md index 03ad7241a5..dc9db27d7e 100644 --- a/roles/test-utils/mining-device/README.md +++ b/roles/test-utils/mining-device/README.md @@ -21,6 +21,10 @@ Options: If 0.0 < nominal_hashrate_multiplier < 1.0, the CPU miner will advertise a nominal hashrate that is smaller than its real capacity. If nominal_hashrate_multiplier > 1.0, the CPU miner will advertise a nominal hashrate that is bigger than its real capacity. If empty, the CPU miner will simply advertise its real capacity. + --nonces-per-call + Number of nonces to try per mining loop iteration when fast hashing is available (micro-batching). [default: 32] + --cores + Number of worker threads to use for mining. Defaults to logical CPUs minus one (leaves one core free). -h, --help Print help -V, --version @@ -32,6 +36,14 @@ Usage example: cargo run --release -- --address-pool 127.0.0.1:20000 --id-device device_id::SOLO::bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh ``` +To adjust micro-batching (see below), you can pass for example `--nonces-per-call 64`: + +``` +cargo run --release -- --address-pool 127.0.0.1:20000 \ + --id-device device_id::SOLO::bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh \ + --nonces-per-call 64 +``` + ## handicap CPU mining could damage the system due to excessive heat. @@ -51,10 +63,75 @@ difficulty target. The `--nominal-hashrate-multiplier` can be used to advertise a custom nominal hashrate. -In the scenario described above, we could launch the CPU miner with `--nominal-hashrate-multiplier 0.01`. +In the scenario described above, we could launch the CPU miner with `--nominal-hashrate-multiplier 0.01`. The CPU miner would advertise 0.01k H/s, which would cause the upstream to set the difficulty target such that the CPU miner would find a share within ~1s. This feature can also be used to advertise a bigger nominal hashrate by using values above `1.0`. -That can also be useful for testing difficulty adjustment algorithms on Sv2 upstreams. \ No newline at end of file +That can also be useful for testing difficulty adjustment algorithms on Sv2 upstreams. + +## Micro-batching (nonces per call) + +The miner supports hashing multiple consecutive nonces per loop iteration when the fast hashing path is available. This reduces outer-loop overhead and can slightly increase throughput on some CPUs. + +- Flag: `--nonces-per-call ` +- Default: `32` +- Trade-off: larger batches can increase latency to detecting a found share because the loop advances in steps of `N`. Choose smaller values (e.g., `4`–`16`) if you care more about latency; larger values (e.g., `32`–`128`) may squeeze a bit more throughput. + +This setting only affects the CPU loop structure; it does not change the hash function or correctness. + +## Worker threads + +By default, the miner uses one worker thread per logical CPU minus one (N-1). This leaves a core available for the operating system and scheduling overhead. + +You can override this with `--cores `, clamped between `1` and the number of logical CPUs. + +Examples: + +```zsh +# Pin to a small fixed number of workers +cargo run --release -- --address-pool 127.0.0.1:20000 --cores 2 +``` + +If `--cores` is omitted, auto mode (N-1) is used. + +## Benchmarks + +You can measure performance with Criterion. From this directory: + +```zsh +cargo bench --bench hasher_bench -- --quiet +``` + +- `hasher_bench` compares baseline `block_hash()` against the optimized midstate+compress256 path. + +To analyze the effect of micro-batching on an end-of-loop iteration, run: + +```zsh +cargo bench --bench microbatch_bench -- --quiet +``` + +- `microbatch_bench` sweeps several batch sizes and sets Criterion throughput to `Elements = N` where each element is one nonce. This means: + - The reported time per iteration divides roughly by `N` to get per-nonce time. + - Criterion also prints throughput in elements/s (hashes/s). For convenience, the bench additionally prints a concise `MH/s` per configuration. + +By default the bench runs a concise subset of batch sizes: `1,8,32,128`. You can override the list via an environment variable: + +```zsh +MINING_DEVICE_BATCH_SIZES=1,4,8,16,32,64,128 cargo bench --bench microbatch_bench -- --quiet +``` + +Tip: pick the smallest `N` that gives you near-peak throughput to keep share-finding latency low. + +### Total scaling (multi-core) + +Total throughput doesn’t always scale linearly with more workers (due to CPU topology, turbo, thermal limits, etc.). Use the scaling bench to measure aggregate MH/s while ramping worker counts from 1 up to your number of logical CPUs: + +```zsh +cargo bench --bench scaling_bench -- --quiet +``` + +- The bench automatically detects the number of logical CPUs and iterates workers from `1..=N` (no environment variable needed). + +The bench prints one concise summary line per configuration and shows incremental improvements versus the previous worker count, including the approximate MH/s gained per additional worker. It also sets Criterion throughput to Elements equal to total nonces hashed, so Elements/s equals total hashes/s. diff --git a/roles/test-utils/mining-device/benches/hasher_bench.rs b/roles/test-utils/mining-device/benches/hasher_bench.rs new file mode 100644 index 0000000000..bbc2e5e003 --- /dev/null +++ b/roles/test-utils/mining-device/benches/hasher_bench.rs @@ -0,0 +1,54 @@ +use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; +use mining_device::FastSha256d; +use rand::{thread_rng, Rng}; +use stratum_common::roles_logic_sv2::bitcoin::{ + block::Version, blockdata::block::Header, hash_types::BlockHash, hashes::Hash, CompactTarget, +}; + +fn random_header() -> Header { + let mut rng = thread_rng(); + let prev_hash: [u8; 32] = rng.gen(); + let prev_hash = Hash::from_byte_array(prev_hash); + let merkle_root: [u8; 32] = rng.gen(); + let merkle_root = Hash::from_byte_array(merkle_root); + Header { + version: Version::from_consensus(rng.gen::()), + prev_blockhash: BlockHash::from_raw_hash(prev_hash), + merkle_root, + time: std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH - std::time::Duration::from_secs(60)) + .unwrap() + .as_secs() as u32, + bits: CompactTarget::from_consensus(rng.gen()), + nonce: 0, + } +} + +fn bench_hasher(c: &mut Criterion) { + let mut group = c.benchmark_group("mining_device_hasher"); + let header = random_header(); + + // Baseline using rust-bitcoin block_hash() + group.bench_function(BenchmarkId::new("baseline_block_hash", "full"), |b| { + let mut h = header; + b.iter(|| { + h.nonce = h.nonce.wrapping_add(1); + let _ = black_box(h.block_hash()); + }); + }); + + // Optimized midstate+compress256 + group.bench_function(BenchmarkId::new("fast_midstate", "compress256"), |b| { + let mut h = header; + let mut fast = FastSha256d::from_header_static(&h); + b.iter(|| { + h.nonce = h.nonce.wrapping_add(1); + let _ = black_box(fast.hash_with_nonce_time(h.nonce, h.time)); + }); + }); + + group.finish(); +} + +criterion_group!(benches, bench_hasher); +criterion_main!(benches); diff --git a/roles/test-utils/mining-device/benches/microbatch_bench.rs b/roles/test-utils/mining-device/benches/microbatch_bench.rs new file mode 100644 index 0000000000..ebb3683421 --- /dev/null +++ b/roles/test-utils/mining-device/benches/microbatch_bench.rs @@ -0,0 +1,105 @@ +use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use mining_device::{set_nonces_per_call, FastSha256d}; +use rand::{thread_rng, Rng}; +use std::time::Duration; +use stratum_common::roles_logic_sv2::bitcoin::{ + block::Version, blockdata::block::Header, hash_types::BlockHash, hashes::Hash, CompactTarget, +}; + +fn random_header() -> Header { + let mut rng = thread_rng(); + let prev_hash: [u8; 32] = rng.gen(); + let prev_hash = Hash::from_byte_array(prev_hash); + let merkle_root: [u8; 32] = rng.gen(); + let merkle_root = Hash::from_byte_array(merkle_root); + Header { + version: Version::from_consensus(rng.gen::()), + prev_blockhash: BlockHash::from_raw_hash(prev_hash), + merkle_root, + time: std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH - std::time::Duration::from_secs(60)) + .unwrap() + .as_secs() as u32, + bits: CompactTarget::from_consensus(rng.gen()), + nonce: 0, + } +} + +fn bench_microbatch(c: &mut Criterion) { + // Report hardware SHA availability once at start + #[cfg(target_arch = "x86_64")] + println!( + "Hardware SHA available (x86 SHA-NI): {}", + std::is_x86_feature_detected!("sha") + ); + #[cfg(target_arch = "aarch64")] + println!( + "Hardware SHA available (ARMv8 SHA2): {}", + std::arch::is_aarch64_feature_detected!("sha2") + ); + #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] + println!("Hardware SHA detection: not applicable for this arch"); + + let mut group = c.benchmark_group("mining_device_microbatch"); + // Keep output and run-time concise + group.sample_size(10); + group.warm_up_time(Duration::from_millis(100)); + group.measurement_time(Duration::from_secs(1)); + let header = random_header(); + let mut fast = FastSha256d::from_header_static(&header); + // Fewer defaults for less verbose output; allow override via env var + let batches: Vec = std::env::var("MINING_DEVICE_BATCH_SIZES") + .ok() + .and_then(|s| { + s.split(',') + .map(|p| p.trim().parse::().ok()) + .collect::>>() + }) + .filter(|v| !v.is_empty()) + .unwrap_or_else(|| vec![1, 8, 32, 128]); + + for &b in &batches { + group.throughput(Throughput::Elements(b as u64)); + group.bench_function(BenchmarkId::from_parameter(b), |bencher| { + set_nonces_per_call(b); + let mut h = header; + bencher.iter(|| { + // Simulate one mining-loop iteration: hash "b" nonces + let start = h.nonce; + let time = h.time; + for i in 0..b { + let hsh = fast.hash_with_nonce_time(start.wrapping_add(i), time); + black_box(hsh); + } + h.nonce = start.wrapping_add(b); + }); + }); + + // Print a concise MH/s estimate per configuration (outside Criterion's stats) + // Do a quick one-shot timing over a small fixed workload to avoid noisy output. + // Note: This is a convenience display; for rigorous numbers, rely on Criterion results. + let mut h = header; + set_nonces_per_call(b); + let reps: u32 = 200_000 / b.max(1); // ~200k hashes in total; fast and stable + let total_hashes: u64 = reps as u64 * b as u64; + let start_inst = std::time::Instant::now(); + for _ in 0..reps { + let start = h.nonce; + let time = h.time; + for i in 0..b { + let _ = black_box(fast.hash_with_nonce_time(start.wrapping_add(i), time)); + } + h.nonce = start.wrapping_add(b); + } + let dur = start_inst.elapsed(); + let secs = dur.as_secs_f64().max(1e-9); + let hps = (total_hashes as f64) / secs; // hashes per second + let mhps = hps / 1_000_000.0; + println!("batch={b}: ~{mhps:.3} MH/s"); + } + + group.finish(); +} + +criterion_group!(benches, bench_microbatch); +criterion_main!(benches); diff --git a/roles/test-utils/mining-device/benches/scaling_bench.rs b/roles/test-utils/mining-device/benches/scaling_bench.rs new file mode 100644 index 0000000000..c60ff6d79b --- /dev/null +++ b/roles/test-utils/mining-device/benches/scaling_bench.rs @@ -0,0 +1,169 @@ +use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use mining_device::FastSha256d; +use rand::{thread_rng, Rng}; +use std::{ + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Barrier, + }, + thread, + time::Instant, +}; +use stratum_common::roles_logic_sv2::bitcoin::{ + block::Version, blockdata::block::Header, hash_types::BlockHash, hashes::Hash, CompactTarget, +}; + +fn random_header() -> Header { + let mut rng = thread_rng(); + let prev_hash: [u8; 32] = rng.gen(); + let prev_hash = Hash::from_byte_array(prev_hash); + let merkle_root: [u8; 32] = rng.gen(); + let merkle_root = Hash::from_byte_array(merkle_root); + Header { + version: Version::from_consensus(rng.gen::()), + prev_blockhash: BlockHash::from_raw_hash(prev_hash), + merkle_root, + time: std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH - std::time::Duration::from_secs(60)) + .unwrap() + .as_secs() as u32, + bits: CompactTarget::from_consensus(rng.gen()), + nonce: 0, + } +} + +fn bench_scaling(c: &mut Criterion) { + let mut group = c.benchmark_group("mining_device_scaling"); + // Measure logical CPUs and test scaling from 1..=N + let logical_cpus = num_cpus::get().max(1); + let workers: Vec = (1..=logical_cpus).collect(); + + // Keep runs short but representative + group.sample_size(10); + group.warm_up_time(std::time::Duration::from_millis(100)); + group.measurement_time(std::time::Duration::from_secs(1)); + + let header = random_header(); + + // Helper: quick one-shot timing used only for concise logging (outside Criterion loop) + let quick_measure_total_mhps = |n: usize| -> f64 { + // Each measurement hashes this many nonces total across n threads + let per_thread: u32 = 200_000 / (n as u32).max(1); + let total_hashes = per_thread as u64 * n as u64; + let stop = Arc::new(AtomicBool::new(false)); + let barrier = Arc::new(Barrier::new(n)); + let mut handles = Vec::with_capacity(n); + for i in 0..n { + let stop = stop.clone(); + let barrier = barrier.clone(); + let mut h = header; + h.nonce = i as u32; + let mut fast = FastSha256d::from_header_static(&h); + let per = per_thread; + handles.push(thread::spawn(move || { + barrier.wait(); + let start = Instant::now(); + let time = h.time; + let mut nonce = h.nonce; + for _ in 0..per { + let _ = black_box(fast.hash_with_nonce_time(nonce, time)); + nonce = nonce.wrapping_add(n as u32); // stride to avoid overlap + if stop.load(Ordering::Relaxed) { + break; + } + } + start.elapsed() + })); + } + let mut max_elapsed = std::time::Duration::ZERO; + for h in handles { + let d = h.join().unwrap(); + if d > max_elapsed { + max_elapsed = d; + } + } + let secs = max_elapsed.as_secs_f64().max(1e-9); + let hps = (total_hashes as f64) / secs; + hps / 1_000_000.0 + }; + + // Print one concise line per worker count, including incremental gain vs previous + let mut prev_workers: Option = None; + let mut prev_mhps: Option = None; + + for &n in &workers { + // Each iteration hashes this many nonces total across n threads + let per_thread: u32 = 200_000 / (n as u32).max(1); + let total_hashes = per_thread as u64 * n as u64; + group.throughput(Throughput::Elements(total_hashes)); + + // One-shot concise summary print (not part of Criterion timing) + let mhps = quick_measure_total_mhps(n); + if let (Some(pn), Some(prev)) = (prev_workers, prev_mhps) { + let added = n.saturating_sub(pn).max(1); + let delta = mhps - prev; + let pct = if prev > 0.0 { + (delta / prev) * 100.0 + } else { + 0.0 + }; + let per_cpu = delta / (added as f64); + println!( + "workers={n}: ~{mhps:.3} MH/s (total) | +{delta:.3} vs prev (+{pct:.1}%), ~{per_cpu:.3} MH/s per added worker" + ); + } else { + println!("workers={n}: ~{mhps:.3} MH/s (total)"); + } + prev_workers = Some(n); + prev_mhps = Some(mhps); + + group.bench_function(BenchmarkId::from_parameter(n), |b| { + b.iter(|| { + let stop = Arc::new(AtomicBool::new(false)); + let barrier = Arc::new(Barrier::new(n)); + let mut handles = Vec::with_capacity(n); + for i in 0..n { + let stop = stop.clone(); + let barrier = barrier.clone(); + let mut h = header; + h.nonce = i as u32; + let mut fast = FastSha256d::from_header_static(&h); + let per = per_thread; + handles.push(thread::spawn(move || { + // start together + barrier.wait(); + let start = Instant::now(); + let time = h.time; + let mut nonce = h.nonce; + for _ in 0..per { + // One hash per step; inner batching isn't necessary here + let _ = black_box(fast.hash_with_nonce_time(nonce, time)); + nonce = nonce.wrapping_add(n as u32); // stride to avoid overlap + if stop.load(Ordering::Relaxed) { + break; + } + } + start.elapsed() + })); + } + // Collect times and compute MH/s + let mut max_elapsed = std::time::Duration::ZERO; + for h in handles { + let d = h.join().unwrap(); + if d > max_elapsed { + max_elapsed = d; + } + } + let _secs = max_elapsed.as_secs_f64().max(1e-9); + let _hps = (total_hashes as f64) / _secs; + let _mhps = _hps / 1_000_000.0; + // Intentionally no println! inside Criterion iteration to keep output concise + }); + }); + } + + group.finish(); +} + +criterion_group!(benches, bench_scaling); +criterion_main!(benches); diff --git a/roles/test-utils/mining-device/src/lib/mod.rs b/roles/test-utils/mining-device/src/lib/mod.rs index ea17b8ed4f..c4160caccd 100644 --- a/roles/test-utils/mining-device/src/lib/mod.rs +++ b/roles/test-utils/mining-device/src/lib/mod.rs @@ -1,21 +1,9 @@ #![allow(clippy::option_map_unit_fn)] use async_channel::{Receiver, Sender}; -use codec_sv2::{Initiator, StandardEitherFrame, StandardSv2Frame}; use key_utils::Secp256k1PublicKey; -use network_helpers_sv2::noise_connection::Connection; +use num_format::{Locale, ToFormattedString}; use primitive_types::U256; use rand::{thread_rng, Rng}; -use roles_logic_sv2::{ - common_messages_sv2::{Protocol, SetupConnection, SetupConnectionSuccess}, - errors::Error, - handlers::{ - common::ParseCommonMessagesFromUpstream, - mining::{ParseMiningMessagesFromUpstream, SendTo, SupportedChannelTypes}, - }, - mining_sv2::*, - parsers::{Mining, MiningDeviceMessages}, - utils::{Id, Mutex}, -}; use std::{ net::{SocketAddr, ToSocketAddrs}, sync::{ @@ -25,12 +13,85 @@ use std::{ thread::available_parallelism, time::{Duration, Instant}, }; -use stratum_common::bitcoin::{ - blockdata::block::Header, hash_types::BlockHash, hashes::Hash, CompactTarget, +use stratum_common::{ + network_helpers_sv2::noise_connection::Connection, + roles_logic_sv2::{ + self, + bitcoin::{blockdata::block::Header, hash_types::BlockHash, hashes::Hash, CompactTarget}, + codec_sv2, + codec_sv2::{Initiator, StandardEitherFrame, StandardSv2Frame}, + common_messages_sv2::{Protocol, SetupConnection, SetupConnectionSuccess}, + errors::Error, + handlers::{ + common::ParseCommonMessagesFromUpstream, + mining::{ParseMiningMessagesFromUpstream, SendTo, SupportedChannelTypes}, + }, + mining_sv2::*, + parsers_sv2::{Mining, MiningDeviceMessages}, + utils::Mutex, + }, }; use tokio::net::TcpStream; use tracing::{debug, error, info}; +// Fast SHA256d midstate hasher +use sha2::{ + compress256, + digest::generic_array::{typenum::U64, GenericArray}, +}; +use stratum_common::roles_logic_sv2::bitcoin::consensus::encode::serialize as btc_serialize; + +// Tuneable: how many nonces to try per mining loop iteration when fast hasher is available. +// Runtime-configurable so the binary and benches can adjust it without changing code. +use std::sync::atomic::AtomicU32; +static NONCES_PER_CALL_RUNTIME: AtomicU32 = AtomicU32::new(32); +// Runtime-configurable number of worker threads; 0 means "auto" (N-1) +static WORKER_OVERRIDE: AtomicU32 = AtomicU32::new(0); + +#[inline] +pub fn set_nonces_per_call(n: u32) { + // Avoid zero (would stall the loop); clamp to at least 1 + let n = n.max(1); + NONCES_PER_CALL_RUNTIME.store(n, Ordering::Relaxed); +} + +#[inline] +fn nonces_per_call() -> u32 { + NONCES_PER_CALL_RUNTIME.load(Ordering::Relaxed).max(1) +} + +/// Override the number of mining worker threads. If set to 0, auto mode (N-1) is used. +#[inline] +pub fn set_cores(n: u32) { + WORKER_OVERRIDE.store(n, Ordering::Relaxed); +} + +/// Resolve effective worker count: if override is 0, use max(1, logical_cpus-1). +#[inline] +fn worker_count() -> u32 { + let total_cpus = available_parallelism().map(|p| p.get()).unwrap_or(1) as u32; + let auto = total_cpus.saturating_sub(1).max(1); + let override_n = WORKER_OVERRIDE.load(Ordering::Relaxed); + if override_n == 0 { + auto + } else { + // Clamp to [1, total_cpus] to avoid oversubscription or zero + override_n.clamp(1, total_cpus) + } +} + +/// Public helper: current effective worker threads (after considering override and auto mode) +#[inline] +pub fn effective_worker_count() -> u32 { + worker_count() +} + +/// Public helper: total logical CPUs detected +#[inline] +pub fn total_logical_cpus() -> u32 { + available_parallelism().map(|p| p.get()).unwrap_or(1) as u32 +} + pub async fn connect( address: String, pub_key: Option, @@ -92,9 +153,8 @@ pub type StdFrame = StandardSv2Frame; pub type EitherFrame = StandardEitherFrame; struct SetupConnectionHandler {} -use roles_logic_sv2::common_messages_sv2::Reconnect; use std::convert::TryInto; -use stratum_common::bitcoin::block::Version; +use stratum_common::roles_logic_sv2::{bitcoin::block::Version, common_messages_sv2::Reconnect}; impl SetupConnectionHandler { pub fn new() -> Self { @@ -203,7 +263,7 @@ pub struct Device { miner: Arc>, jobs: Vec>, prev_hash: Option>, - sequence_numbers: Id, + sequence_numbers: AtomicU32, notify_changes_to_mining_thread: NewWorkNotifier, } @@ -215,8 +275,13 @@ fn open_channel( let user_identity = device_id.unwrap_or_default().try_into().unwrap(); let id: u32 = 10; info!("Measuring CPU hashrate"); - let measured_hashrate = measure_hashrate(5, handicap) as f32; - info!("Measured CPU hashrate is {}", measured_hashrate); + let measured_total_hs = measure_hashrate(5, handicap); + let measured_total_mhs = measured_total_hs / 1_000_000.0; + info!( + "Measured CPU hashrate ≈ {} MH/s", + format_mhs(measured_total_mhs) + ); + let measured_hashrate = measured_total_hs as f32; let nominal_hash_rate = match nominal_hashrate_multiplier { Some(m) => measured_hashrate * m, None => measured_hashrate, @@ -264,7 +329,7 @@ impl Device { jobs: Vec::new(), prev_hash: None, channel_id: None, - sequence_numbers: Id::new(), + sequence_numbers: AtomicU32::new(0), notify_changes_to_mining_thread: NewWorkNotifier { should_send: true, sender: notify_changes_to_mining_thread, @@ -336,7 +401,9 @@ impl Device { let share = MiningDeviceMessages::Mining(Mining::SubmitSharesStandard(SubmitSharesStandard { channel_id: self_mutex.safe_lock(|s| s.channel_id.unwrap()).unwrap(), - sequence_number: self_mutex.safe_lock(|s| s.sequence_numbers.next()).unwrap(), + sequence_number: self_mutex + .safe_lock(|s| s.sequence_numbers.fetch_add(1, Ordering::Relaxed)) + .unwrap(), job_id, nonce, ntime, @@ -409,7 +476,7 @@ impl ParseMiningMessagesFromUpstream<()> for Device { m: SubmitSharesSuccess, ) -> Result, Error> { info!("Received SubmitSharesSuccess"); - debug!("SubmitSharesSuccess: {:?}", m); + debug!("SubmitSharesSuccess: {}", m); Ok(SendTo::None(None)) } @@ -428,7 +495,7 @@ impl ParseMiningMessagesFromUpstream<()> for Device { m.job_id, m.is_future() ); - debug!("NewMiningJob: {:?}", m); + debug!("NewMiningJob: {}", m); match (m.is_future(), self.prev_hash.as_ref()) { (false, Some(p_h)) => { self.miner @@ -457,7 +524,7 @@ impl ParseMiningMessagesFromUpstream<()> for Device { "Received SetNewPrevHash channel id: {}, job id: {}", m.channel_id, m.job_id ); - debug!("SetNewPrevHash: {:?}", m); + debug!("SetNewPrevHash: {}", m); let jobs: Vec<&NewMiningJob<'static>> = self .jobs .iter() @@ -496,7 +563,7 @@ impl ParseMiningMessagesFromUpstream<()> for Device { fn handle_set_target(&mut self, m: SetTarget) -> Result, Error> { info!("Received SetTarget for channel id: {}", m.channel_id); - debug!("SetTarget: {:?}", m); + debug!("SetTarget: {}", m); self.miner .safe_lock(|miner| miner.new_target(m.maximum_target.to_vec())) .unwrap(); @@ -516,6 +583,8 @@ struct Miner { job_id: Option, version: Option, handicap: u32, + // Optimized hashing state + fast_hasher: Option, } impl Miner { @@ -526,6 +595,7 @@ impl Miner { job_id: None, version: None, handicap, + fast_hasher: None, } } @@ -533,12 +603,17 @@ impl Miner { // target is sent in LE format, we'll keep it that way let hex_string = target .iter() - .fold("".to_string(), |acc, b| acc + format!("{:02x}", b).as_str()); + .fold("".to_string(), |acc, b| acc + format!("{b:02x}").as_str()); info!("Set target to {}", hex_string); // Store the target as U256 in little-endian format self.target = Some(U256::from_little_endian(target.as_slice())); } + // Same as new_target but without logging (useful for internal probes) + fn new_target_silent(&mut self, target: Vec) { + self.target = Some(U256::from_little_endian(target.as_slice())); + } + fn new_header(&mut self, set_new_prev_hash: &SetNewPrevHash, new_job: &NewMiningJob) { self.job_id = Some(new_job.job_id); self.version = Some(new_job.version); @@ -562,22 +637,61 @@ impl Miner { nonce: 0, }; self.header = Some(header); + // Build a fast hasher with midstate prepared for the static parts of the header + if let Some(h) = &self.header { + self.fast_hasher = Some(FastSha256d::from_header_static(h)); + } else { + self.fast_hasher = None; + } } pub fn next_share(&mut self) -> NextShareOutcome { if let Some(header) = self.header.as_ref() { - let hash_ = header.block_hash(); - let hash: [u8; 32] = *hash_.to_raw_hash().as_ref(); - - // Convert both hash and target to Target type for comparison - let hash_target: Target = hash.into(); + // Use optimized path if available + let hash: [u8; 32] = if let Some(fast) = &mut self.fast_hasher { + fast.hash_with_nonce_time(header.nonce, header.time) + } else { + let hash_ = header.block_hash(); + *hash_.to_raw_hash().as_ref() + }; - // Convert U256 target to [u8; 32] array and then to Target + // Compare hash against target quickly in little-endian u32 words (most significant at + // index 7) if let Some(target) = self.target { - let target_bytes = target.to_little_endian(); - let mut target_array = [0u8; 32]; - target_array.copy_from_slice(&target_bytes); - let target: Target = target_array.into(); - if hash_target <= target { + let tgt_le = target.to_little_endian(); + // Interpret as 8 little-endian u32 words + let mut is_below = false; + let mut is_equal = true; + // Compare from most significant word (index 7) to least (index 0) + for i in (0..8).rev() { + let off = i * 4; + let hw = u32::from_le_bytes([ + hash[off], + hash[off + 1], + hash[off + 2], + hash[off + 3], + ]); + let tw = u32::from_le_bytes([ + tgt_le[off], + tgt_le[off + 1], + tgt_le[off + 2], + tgt_le[off + 3], + ]); + match hw.cmp(&tw) { + core::cmp::Ordering::Less => { + is_below = true; + is_equal = false; + break; + } + core::cmp::Ordering::Greater => { + is_below = false; + is_equal = false; + break; + } + core::cmp::Ordering::Equal => {} + } + } + + if is_below || is_equal { info!( "Found share with nonce: {}, for target: {:?}, with hash: {:?}", header.nonce, self.target, hash, @@ -597,6 +711,104 @@ impl Miner { } } +// A fast double-SHA256 hasher specialized for Bitcoin block headers. +// It precomputes the midstate of the first 64 bytes (version, prev_blockhash, merkle_root[0..28]) +// and allows quickly hashing varying (time, nonce) fields. +#[derive(Clone, Debug)] +pub struct FastSha256d { + // Midstate after processing the first 64 bytes of the header (chunk 0) + state0: [u32; 8], + // Second block for the first SHA256 (contains merkle tail, time, bits, nonce, padding, length) + // We mutate only the time (bytes 4..8) and nonce (bytes 12..16) per attempt. + block1: GenericArray, + // Reusable buffer for the second SHA256 block. Bytes 32 and 56..64 are constant; we only + // overwrite the first 32 bytes with the first digest each attempt. + second_block: GenericArray, +} + +impl FastSha256d { + pub fn from_header_static(h: &Header) -> Self { + // Use consensus serialization to get correct 80-byte header (proper endianness). + let header_ser = btc_serialize(h); + debug_assert_eq!(header_ser.len(), 80, "Serialized header must be 80 bytes"); + let mut header_bytes = [0u8; 80]; + header_bytes.copy_from_slice(&header_ser); + + // First SHA256 pass: split into two 64-byte chunks + let chunk0 = &header_bytes[0..64]; + let chunk1_last16 = &header_bytes[64..80]; // 16 bytes: merkle_tail(4), time(4), bits(4), nonce(4) + + // Compute midstate after chunk0 using compress256 on an initial state + let mut state0 = sha256_initial_state(); + let mut block = [0u8; 64]; + block.copy_from_slice(chunk0); + let ga0 = GenericArray::::clone_from_slice(&block); + compress256(&mut state0, std::slice::from_ref(&ga0)); + + // Prepare block1 template (64 bytes) which will be: + // bytes 0..16: last 16 bytes of header (time, bits, nonce) + // bytes 16: 0x80 padding + // bytes 17..56: zeros + // bytes 56..64: length in bits of the message (80 bytes -> 640 bits) in big-endian + let mut block1 = GenericArray::::default(); + block1[0..16].copy_from_slice(chunk1_last16); + block1[16] = 0x80; + block1[56..64].copy_from_slice(&640u64.to_be_bytes()); + + // Prepare reusable second block: set constants once + let mut second_block = GenericArray::::default(); + second_block[32] = 0x80; + // 33..56 are already zero via default + second_block[56..64].copy_from_slice(&256u64.to_be_bytes()); + + Self { + state0, + block1, + second_block, + } + } + + // Hashes header where only time and nonce vary, returns double-SHA256 as [u8;32] (little-endian + // like rust-bitcoin output) + pub fn hash_with_nonce_time(&mut self, nonce: u32, time: u32) -> [u8; 32] { + // First SHA256 second chunk: update time and nonce at offsets 68..72 and 76..80 within + // 80-byte header In our block1_template (offset 0..16 == 64..80 of header): + // time at 0..4, bits at 4..8, nonce at 12..16 + // Update time and nonce in place + self.block1[4..8].copy_from_slice(&time.to_le_bytes()); + self.block1[12..16].copy_from_slice(&nonce.to_le_bytes()); + + // Compute first SHA256 digest using midstate + block1 + let mut state1 = self.state0; + compress256(&mut state1, std::slice::from_ref(&self.block1)); + + // Now perform the second SHA256 over the 32-byte first digest Build 64-byte block: + // [digest(32)] + [0x80] + [zeros] + [length=256 bits] + // state1 words -> big-endian bytes per SHA-256 spec (fill first 32 bytes) + for (i, word) in state1.iter().enumerate() { + self.second_block[i * 4..i * 4 + 4].copy_from_slice(&word.to_be_bytes()); + } + + let mut state2 = sha256_initial_state(); + compress256(&mut state2, std::slice::from_ref(&self.second_block)); + + // Convert state2 words to bytes (big-endian), then reverse for Bitcoin-style + // little-endian + let mut out = [0u8; 32]; + for (i, word) in state2.iter().enumerate() { + out[i * 4..i * 4 + 4].copy_from_slice(&word.to_be_bytes()); + } + out + } +} + +fn sha256_initial_state() -> [u32; 8] { + [ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, + 0x5be0cd19, + ] +} + enum NextShareOutcome { ValidShare, InvalidShare, @@ -610,16 +822,54 @@ impl NextShareOutcome { } } -// returns hashrate based on how fast the device hashes over the given duration +#[inline] +fn hash_meets_target_le(hash: &[u8; 32], tgt_le: &[u8; 32]) -> bool { + // Compare from most significant u32 word (index 7) to least (index 0) + let mut is_below = false; + let mut is_equal = true; + for i in (0..8).rev() { + let off = i * 4; + let hw = u32::from_le_bytes([hash[off], hash[off + 1], hash[off + 2], hash[off + 3]]); + let tw = u32::from_le_bytes([ + tgt_le[off], + tgt_le[off + 1], + tgt_le[off + 2], + tgt_le[off + 3], + ]); + match hw.cmp(&tw) { + core::cmp::Ordering::Less => { + is_below = true; + is_equal = false; + break; + } + core::cmp::Ordering::Greater => { + is_below = false; + is_equal = false; + break; + } + core::cmp::Ordering::Equal => {} + } + } + is_below || is_equal +} + +// Format MH/s with thousands separators and 2 decimal places using en locale separators +fn format_mhs(val_mhs: f64) -> String { + let rounded = val_mhs.round() as i64; + rounded.to_formatted_string(&Locale::en) +} + +// returns hashrate by running all worker threads in parallel for the given duration fn measure_hashrate(duration_secs: u64, handicap: u32) -> f64 { + use std::sync::Barrier; + + // Prepare a random header template to hash let mut rng = thread_rng(); let prev_hash: [u8; 32] = generate_random_32_byte_array().to_vec().try_into().unwrap(); let prev_hash = Hash::from_byte_array(prev_hash); - // We create a random block that we can hash, we are only interested in knowing how many hashes - // per unit of time we can do let merkle_root: [u8; 32] = generate_random_32_byte_array().to_vec().try_into().unwrap(); let merkle_root = Hash::from_byte_array(merkle_root); - let header = Header { + let header_template = Header { version: Version::from_consensus(rng.gen()), prev_blockhash: BlockHash::from_raw_hash(prev_hash), merkle_root, @@ -630,27 +880,46 @@ fn measure_hashrate(duration_secs: u64, handicap: u32) -> f64 { bits: CompactTarget::from_consensus(rng.gen()), nonce: 0, }; - let start_time = Instant::now(); - let mut hashes: u64 = 0; + let duration = Duration::from_secs(duration_secs); - let mut miner = Miner::new(handicap); - // We put the target to 0 we are only interested in how many hashes per unit of time we can do - // and do not want to be botherd by messages about valid shares found. - miner.new_target(vec![0_u8; 32]); - miner.header = Some(header); + let p = worker_count() as usize; + let barrier = Arc::new(Barrier::new(p + 1)); // +1 for coordinator - while start_time.elapsed() < duration { - miner.next_share(); - hashes += 1; + let mut handles = Vec::with_capacity(p); + // Log a single consolidated target-setting message for the probe + info!("Set target to {}", "0".repeat(64)); + for _ in 0..p { + let barrier = barrier.clone(); + // Each thread gets its own miner and header copy + let mut miner = Miner::new(handicap); + // Set target to zero (silently) so we never trigger share submits; we're only counting + // hashes + miner.new_target_silent(vec![0_u8; 32]); + miner.header = Some(header_template); + if let Some(h) = miner.header.as_ref() { + miner.fast_hasher = Some(FastSha256d::from_header_static(h)); + } + handles.push(std::thread::spawn(move || { + // Synchronize start across threads + barrier.wait(); + let start = Instant::now(); + let mut hashes: u64 = 0; + while start.elapsed() < duration { + miner.next_share(); + hashes += 1; + } + hashes + })); } - let elapsed_secs = start_time.elapsed().as_secs_f64(); - let hashrate_single_thread = hashes as f64 / elapsed_secs; - - // we just measured for a single thread, need to multiply by the available parallelism - let p = available_parallelism().unwrap().get(); - - hashrate_single_thread * p as f64 + // Release all workers simultaneously + barrier.wait(); + let mut total_hashes: u64 = 0; + for h in handles { + total_hashes += h.join().unwrap_or(0); + } + // Each thread ran for approximately `duration`, so total hashes per second is total/duration + (total_hashes as f64) / (duration_secs as f64) } fn generate_random_32_byte_array() -> [u8; 32] { let mut rng = thread_rng(); @@ -667,7 +936,8 @@ fn start_mining_threads( tokio::task::spawn(async move { let mut killers: Vec> = vec![]; loop { - let p = available_parallelism().unwrap().get() as u32; + // Determine number of workers based on override or auto (N-1) + let p = worker_count(); let unit = u32::MAX / p; while have_new_job.recv().await.is_ok() { while let Some(killer) = killers.pop() { @@ -696,38 +966,111 @@ fn mine(mut miner: Miner, share_send: Sender<(u32, u32, u32, u32)>, kill: Arc, + #[arg( + long, + help = "Number of nonces to try per mining loop iteration when fast hashing is available (micro-batching)", + default_value = "32" + )] + nonces_per_call: u32, + #[arg( + long, + help = "Number of worker threads to use for mining. Defaults to logical CPUs minus one (leaves one core free)." + )] + cores: Option, } #[tokio::main(flavor = "current_thread")] @@ -52,6 +63,19 @@ async fn main() { let args = Args::parse(); tracing_subscriber::fmt::init(); info!("start"); + // Configure micro-batch size + mining_device::set_nonces_per_call(args.nonces_per_call); + // Optional override of worker threads + if let Some(n) = args.cores { + mining_device::set_cores(n); + } + // Log worker usage (after applying overrides) + let used = mining_device::effective_worker_count(); + let total = mining_device::total_logical_cpus(); + info!( + "Using {} worker threads out of {} logical CPUs", + used, total + ); let _ = mining_device::connect( args.address_pool, args.pubkey_pool, diff --git a/roles/test-utils/mining-device/tests/fast_hasher_equivalence.rs b/roles/test-utils/mining-device/tests/fast_hasher_equivalence.rs new file mode 100644 index 0000000000..c4e67e43b4 --- /dev/null +++ b/roles/test-utils/mining-device/tests/fast_hasher_equivalence.rs @@ -0,0 +1,41 @@ +use mining_device::FastSha256d; +use rand::{thread_rng, Rng}; +use stratum_common::roles_logic_sv2::bitcoin::{ + block::Version, blockdata::block::Header, hash_types::BlockHash, hashes::Hash, CompactTarget, +}; + +fn random_header() -> Header { + let mut rng = thread_rng(); + let prev_hash: [u8; 32] = rng.gen(); + let prev_hash = Hash::from_byte_array(prev_hash); + let merkle_root: [u8; 32] = rng.gen(); + let merkle_root = Hash::from_byte_array(merkle_root); + Header { + version: Version::from_consensus(rng.gen::()), + prev_blockhash: BlockHash::from_raw_hash(prev_hash), + merkle_root, + time: std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH - std::time::Duration::from_secs(60)) + .unwrap() + .as_secs() as u32, + bits: CompactTarget::from_consensus(rng.gen()), + nonce: 0, + } +} + +#[test] +fn fast_hasher_matches_baseline() { + let mut h = random_header(); + let mut fast = FastSha256d::from_header_static(&h); + + for _ in 0..1000 { + // Advance nonce, occasionally tweak time + h.nonce = h.nonce.wrapping_add(1); + if h.nonce % 128 == 0 { + h.time = h.time.wrapping_add(1); + } + let fast_hash = fast.hash_with_nonce_time(h.nonce, h.time); + let baseline: [u8; 32] = *h.block_hash().to_raw_hash().as_ref(); + assert_eq!(fast_hash, baseline, "Fast hasher must match baseline"); + } +} diff --git a/roles/translator/Cargo.toml b/roles/translator/Cargo.toml index 302c5bb64b..a68d386dd2 100644 --- a/roles/translator/Cargo.toml +++ b/roles/translator/Cargo.toml @@ -1,15 +1,15 @@ [package] name = "translator_sv2" -version = "1.0.0" +version = "2.0.0" authors = ["The Stratum V2 Developers"] edition = "2021" -description = "Server used to bridge SV1 miners to SV2 pools" +description = "SV1 to SV2 translation proxy" documentation = "https://docs.rs/translator_sv2" readme = "README.md" homepage = "https://stratumprotocol.org" repository = "https://github.com/stratum-mining/stratum" license = "MIT OR Apache-2.0" -keywords = ["stratum", "mining", "bitcoin", "protocol"] +keywords = ["stratum", "mining", "bitcoin", "protocol", "translator", "proxy"] [lib] name = "translator_sv2" @@ -22,28 +22,19 @@ path = "src/main.rs" [dependencies] stratum-common = { path = "../../common" } async-channel = "1.5.1" -async-recursion = "0.3.2" -binary_sv2 = { path = "../../protocols/v2/binary-sv2" } buffer_sv2 = { path = "../../utils/buffer" } -codec_sv2 = { path = "../../protocols/v2/codec-sv2", features = ["noise_sv2", "with_buffer_pool"] } -framing_sv2 = { path = "../../protocols/v2/framing-sv2" } -network_helpers_sv2 = { path = "../roles-utils/network-helpers", features=["with_buffer_pool"] } -once_cell = "1.12.0" -roles_logic_sv2 = { path = "../../protocols/v2/roles-logic-sv2" } +network_helpers_sv2 = { path = "../roles-utils/network-helpers", features=["with_buffer_pool", "sv1"] } +stratum_translation = { path = "../roles-utils/stratum-translation" } serde = { version = "1.0.89", default-features = false, features = ["derive", "alloc"] } serde_json = { version = "1.0.64", default-features = false, features = ["alloc"] } -futures = "0.3.25" tokio = { version = "1.44.1", features = ["full"] } ext-config = { version = "0.14.0", features = ["toml"], package = "config" } tracing = { version = "0.1" } -tracing-subscriber = { version = "0.3" } v1 = { path = "../../protocols/v1", package="sv1_api" } -error_handling = { path = "../../utils/error-handling" } key-utils = { path = "../../utils/key-utils" } -tokio-util = { version = "0.7.10", features = ["codec"] } -rand = "0.8.4" -primitive-types = "0.13.1" clap = { version = "4.5.39", features = ["derive"] } +config_helpers_sv2 = { path = "../roles-utils/config-helpers" } + [dev-dependencies] sha2 = "0.10.6" diff --git a/roles/translator/README.md b/roles/translator/README.md index 705f605a9d..ee14bf3aa1 100644 --- a/roles/translator/README.md +++ b/roles/translator/README.md @@ -1,11 +1,10 @@ - # SV1 to SV2 Translator Proxy -This proxy is designed to sit in between a SV1 Downstream role (most typically Mining Device(s) -running SV1 firmware) and a SV2 Upstream role (most typically a SV2 Pool Server with Extended -Channel support). +A proxy that translates between Stratum V1 (SV1) and Stratum V2 (SV2) mining protocols. This translator enables SV1 mining devices to connect to SV2 pools and infrastructure, bridging the gap between legacy mining hardware and modern mining protocols. + +## Architecture Overview -The most typical high level configuration is: +The translator sits between SV1 downstream roles (mining devices) and SV2 upstream roles (pool servers or proxies), providing seamless protocol translation and advanced features like channel aggregation and failover. ``` <--- Most Downstream ----------------------------------------- Most Upstream ---> @@ -18,45 +17,188 @@ The most typical high level configuration is: | +-------------------+ +------------------+ | | +-----------------+ | | | | | +---------------------------------------------------+ +------------------------+ +``` + +## Configuration + +### Configuration File Structure + +The translator uses TOML configuration files with the following structure: + +```toml +# Downstream SV1 Connection (where miners connect) +downstream_address = "0.0.0.0" +downstream_port = 34255 + +# Protocol Version Support +max_supported_version = 2 +min_supported_version = 2 + +# Extranonce Configuration +downstream_extranonce2_size = 4 # Min: 2, Max: 16 (CGminer max: 8) + +# User Identity (appended with counter for each miner) +user_identity = "your_username_here" + +# Channel Configuration +aggregate_channels = true # true: shared channel, false: individual channels +# Downstream Difficulty Configuration +[downstream_difficulty_config] +min_individual_miner_hashrate = 10_000_000_000_000.0 # 10 TH/s +shares_per_minute = 6.0 +enable_vardiff = true # Set to false when using with Job Declarator Client (JDC) + +# Upstream SV2 Connections (supports multiple with failover) +[[upstreams]] +address = "127.0.0.1" +port = 34254 +authority_pubkey = "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72" + +[[upstreams]] +address = "backup.pool.com" +port = 34254 +authority_pubkey = "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72" ``` -## Setup +### Configuration Parameters + +#### **Downstream Configuration** +- `downstream_address`: IP address for SV1 miners to connect to +- `downstream_port`: Port for SV1 miners to connect to -### Configuration File +#### **Protocol Configuration** +- `max_supported_version`/`min_supported_version`: SV2 protocol version support +- `min_extranonce2_size`: Minimum extranonce2 size (affects mining efficiency) -`tproxy-config-local-jdc-example.toml` and `tproxy-config-local-pool-example.toml` are examples of configuration files for the Translator Proxy. +#### **Channel Configuration** +- `aggregate_channels`: + - `true`: All miners share one upstream extended channel (more efficient) + - `false`: Each miner gets its own upstream extended channel (more isolated) +- `user_identity`: Username for pool authentication (auto-suffixed per miner) -The configuration file contains the following information: +#### **Difficulty Configuration** +- `min_individual_miner_hashrate`: Expected hashrate of weakest miner (in H/s) +- `shares_per_minute`: Target share submission rate +- `enable_vardiff`: Enable/disable variable difficulty adjustment (set to false when using with JDC) + - When `true`: Translator manages difficulty adjustments based on share submission rates + - When `false`: Upstream manages difficulty, translator forwards SetTarget messages to miners -1. The SV2 Upstream connection information which includes the SV2 Pool authority public key - (`upstream_authority_pubkey`) and the SV2 Pool connection address (`upstream_address`) and port - (`upstream_port`). -2. The SV1 Downstream socket information which includes the listening IP address - (`downstream_address`) and port (`downstream_port`). -3. The maximum and minimum SRI versions (`max_supported_version` and `min_supported_version`) that - the Translator Proxy implementer wants to support. Currently the only available version is `2`. -4. The desired minimum `extranonce2` size that the Translator Proxy implementer wants to use - (`min_extranonce2_size`). The `extranonce2` size is ultimately decided by the SV2 Upstream role, - but if the specified size meets the SV2 Upstream role's requirements, the size specified in this - configuration file should be favored. -5. The downstream difficulty params such as: -- the hashrate (hashes/s) of the weakest Mining Device that will be connecting to the Translator Proxy (`min_individual_miner_hashrate`) -- the number of shares per minute that Mining Devices should be sending to the Translator Proxy (`shares_per_minute`). -6. The upstream difficulty params such as: -- the interval in seconds to elapse before updating channel hashrate with the pool (`channel_diff_update_interval`) -- the estimated aggregate hashrate of all SV1 Downstream roles (`channel_nominal_hashrate`) +#### **Upstream Configuration** +- `address`/`port`: SV2 upstream server connection details +- `authority_pubkey`: Public key for SV2 connection authentication -### Run +## Usage + +### Installation & Build + +```bash +# Clone the repository +git clone https://github.com/stratum-mining/stratum.git +cd stratum -There are two files in `roles/translator/config-examples`: -- `tproxy-config-local-jdc-example.toml` which assumes the Job Declaration protocol is used and a JD Client is deployed locally -- `tproxy-config-local-pool-example.toml` which assumes Job Declaration protocol is NOT used, and a Pool is deployed locally +# Build the translator +cargo build --release -p translator_sv2 +``` + +### Running the Translator + +#### **With Local Pool** +```bash +cd roles/translator +cargo run -- -c config-examples/tproxy-config-local-pool-example.toml +``` +#### **With Job Declaration Client** ```bash -cd roles/translator/config-examples/ -cargo run -- -c tproxy-config-local-jdc-example.toml +cd roles/translator +cargo run -- -c config-examples/tproxy-config-local-jdc-example.toml +``` + +#### **With Hosted Pool** +```bash +cd roles/translator +cargo run -- -c config-examples/tproxy-config-hosted-pool-example.toml +``` + +### Command Line Options + +```bash +# Use specific config file +translator_sv2 -c /path/to/config.toml +translator_sv2 --config /path/to/config.toml + +# Show help +translator_sv2 -h +translator_sv2 --help +``` + +## Configuration Examples + +### Example 1: Local Pool Setup +For connecting to a local SV2 pool server: + +```toml +downstream_address = "0.0.0.0" +downstream_port = 34255 +user_identity = "miner_farm_1" +aggregate_channels = true + +[downstream_difficulty_config] +min_individual_miner_hashrate = 10_000_000_000_000.0 +shares_per_minute = 6.0 +enable_vardiff = true + +[[upstreams]] +address = "127.0.0.1" +port = 34254 +authority_pubkey = "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72" +``` + +### Example 2: High-Availability Setup +For production environments with failover: + +```toml +downstream_address = "0.0.0.0" +downstream_port = 34255 +user_identity = "production_farm" +aggregate_channels = true + +[downstream_difficulty_config] +min_individual_miner_hashrate = 50_000_000_000_000.0 # 50 TH/s +shares_per_minute = 10.0 +enable_vardiff = true + +# Primary upstream +[[upstreams]] +address = "primary.pool.com" +port = 34254 +authority_pubkey = "primary_pool_pubkey" + +# Backup upstream +[[upstreams]] +address = "backup.pool.com" +port = 34254 +authority_pubkey = "backup_pool_pubkey" +``` + +## Architecture Details + +### **Component Overview** + +1. **SV1 Server**: Handles incoming SV1 connections from mining devices +2. **SV2 Upstream**: Manages connections to SV2 pool servers with failover +3. **Channel Manager**: Orchestrates message routing and protocol translation +4. **Task Manager**: Manages async task lifecycle and coordination +5. **Status System**: Provides real-time monitoring and health reporting + +### **Channel Modes** -### Limitations +- **Aggregated Mode**: All miners share one extended channel + - More efficient for large farms + - Reduced upstream connection overhead + - Shared work distribution -The current implementation always replies to Sv1 `mining.submit` with `"result": true`, regardless of whether the share was rejected on Sv2 upstream. \ No newline at end of file +- **Non-Aggregated Mode**: Each miner gets individual upstream channel + - Better isolation between miners + - Individual difficulty adjustment by the upstream Pool \ No newline at end of file diff --git a/roles/translator/config-examples/tproxy-config-hosted-pool-example.toml b/roles/translator/config-examples/tproxy-config-hosted-pool-example.toml index ec706471c9..020c66a404 100644 --- a/roles/translator/config-examples/tproxy-config-hosted-pool-example.toml +++ b/roles/translator/config-examples/tproxy-config-hosted-pool-example.toml @@ -1,13 +1,3 @@ -# Braiins Pool Upstream Connection -# upstream_authority_pubkey = "u95GEReVMjK6k5YqiSFNqqTnKU4ypU2Wm8awa6tmbmDmk1bWt" -# upstream_address = "18.196.32.109" -# upstream_port = 3336 - -# Hosted SRI Pool Upstream Connection -upstream_address = "75.119.150.111" -upstream_port = 34254 -upstream_authority_pubkey = "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72" - # Local Mining Device Downstream Connection downstream_address = "0.0.0.0" downstream_port = 34255 @@ -16,11 +6,23 @@ downstream_port = 34255 max_supported_version = 2 min_supported_version = 2 -# Minimum extranonce2 size for downstream -# Max value: 16 (leaves 0 bytes for search space splitting of downstreams) +# Extranonce2 size for downstream connections +# This controls the rollable part of the extranonce for downstream SV1 miners # Max value for CGminer: 8 # Min value: 2 -min_extranonce2_size = 4 +downstream_extranonce2_size = 4 + +# User identity/username for pool connection +# This will be appended with a counter for each mining client (e.g., username.miner1, username.miner2) +user_identity = "your_username_here" + +# Aggregate channels: if true, all miners share one upstream channel; if false, each miner gets its own channel +aggregate_channels = true + +# Enable this option to set a predefined log file path. +# When enabled, logs will always be written to this file. +# The CLI option --log-file (or -f) will override this setting if provided. +# log_file = "./tproxy.log" # Difficulty params [downstream_difficulty_config] @@ -28,9 +30,17 @@ min_extranonce2_size = 4 min_individual_miner_hashrate=10_000_000_000_000.0 # target number of shares per minute the miner should be sending shares_per_minute = 6.0 +# enable variable difficulty adjustment (true by default, set to false when using with JDC) +enable_vardiff = true + +[[upstreams]] +# SRI Pool Primary Pool +address = "75.119.150.111" +port = 34254 +authority_pubkey = "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72" -[upstream_difficulty_config] -# interval in seconds to elapse before updating channel hashrate with the pool -channel_diff_update_interval = 60 -# estimated accumulated hashrate of all downstream miners (e.g.: 10 Th/s = 10_000_000_000_000.0) -channel_nominal_hashrate = 10_000_000_000_000.0 +# Braiins Pool Backup Pool +[[upstreams]] +address = "107.170.42.64" +port = 3333 +authority_pubkey = "9awtMD5KQgvRUh2yFbjVeT7b6hjipWcAsQHd6wEhgtDT9soosna" \ No newline at end of file diff --git a/roles/translator/config-examples/tproxy-config-local-jdc-example.toml b/roles/translator/config-examples/tproxy-config-local-jdc-example.toml index 62a5a5ac68..28645afa8d 100644 --- a/roles/translator/config-examples/tproxy-config-local-jdc-example.toml +++ b/roles/translator/config-examples/tproxy-config-local-jdc-example.toml @@ -1,13 +1,3 @@ -# Braiins Pool Upstream Connection -# upstream_authority_pubkey = "u95GEReVMjK6k5YqiSFNqqTnKU4ypU2Wm8awa6tmbmDmk1bWt" -# upstream_address = "18.196.32.109" -# upstream_port = 3336 - -# Local SRI JDC Upstream Connection -upstream_address = "127.0.0.1" -upstream_port = 34265 -upstream_authority_pubkey = "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72" - # Local Mining Device Downstream Connection downstream_address = "0.0.0.0" downstream_port = 34255 @@ -16,11 +6,23 @@ downstream_port = 34255 max_supported_version = 2 min_supported_version = 2 -# Minimum extranonce2 size for downstream -# Max value: 16 (leaves 0 bytes for search space splitting of downstreams) +# Extranonce2 size for downstream connections +# This controls the rollable part of the extranonce for downstream miners # Max value for CGminer: 8 # Min value: 2 -min_extranonce2_size = 4 +downstream_extranonce2_size = 4 + +# User identity/username for pool connection +# This will be appended with a counter for each mining client (e.g., username.miner1, username.miner2) +user_identity = "your_username_here" + +# Aggregate channels: if true, all miners share one upstream channel; if false, each miner gets its own channel +aggregate_channels = false + +# Enable this option to set a predefined log file path. +# When enabled, logs will always be written to this file. +# The CLI option --log-file (or -f) will override this setting if provided. +# log_file = "./tproxy.log" # Difficulty params [downstream_difficulty_config] @@ -28,9 +30,11 @@ min_extranonce2_size = 4 min_individual_miner_hashrate=10_000_000_000_000.0 # target number of shares per minute the miner should be sending shares_per_minute = 6.0 +# disable variable difficulty adjustment when using with JDC (JDC handles vardiff) +enable_vardiff = false + -[upstream_difficulty_config] -# interval in seconds to elapse before updating channel hashrate with the pool -channel_diff_update_interval = 60 -# estimated accumulated hashrate of all downstream miners (e.g.: 10 Th/s = 10_000_000_000_000.0) -channel_nominal_hashrate = 10_000_000_000_000.0 +[[upstreams]] +address = "127.0.0.1" +port = 34265 +authority_pubkey = "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72" \ No newline at end of file diff --git a/roles/translator/config-examples/tproxy-config-local-pool-example.toml b/roles/translator/config-examples/tproxy-config-local-pool-example.toml index 22c3dc1775..65ee173af8 100644 --- a/roles/translator/config-examples/tproxy-config-local-pool-example.toml +++ b/roles/translator/config-examples/tproxy-config-local-pool-example.toml @@ -1,13 +1,3 @@ -# Braiins Pool Upstream Connection -# upstream_authority_pubkey = "u95GEReVMjK6k5YqiSFNqqTnKU4ypU2Wm8awa6tmbmDmk1bWt" -# upstream_address = "18.196.32.109" -# upstream_port = 3336 - -# Local SRI Pool Upstream Connection -upstream_address = "127.0.0.1" -upstream_port = 34254 -upstream_authority_pubkey = "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72" - # Local Mining Device Downstream Connection downstream_address = "0.0.0.0" downstream_port = 34255 @@ -16,11 +6,23 @@ downstream_port = 34255 max_supported_version = 2 min_supported_version = 2 -# Minimum extranonce2 size for downstream -# Max value: 16 (leaves 0 bytes for search space splitting of downstreams) +# Extranonce2 size for downstream connections +# This controls the rollable part of the extranonce for downstream miners # Max value for CGminer: 8 # Min value: 2 -min_extranonce2_size = 4 +downstream_extranonce2_size = 4 + +# User identity/username for pool connection +# This will be appended with a counter for each mining client (e.g., username.miner1, username.miner2) +user_identity = "your_username_here" + +# Aggregate channels: if true, all miners share one upstream channel; if false, each miner gets its own channel +aggregate_channels = true + +# Enable this option to set a predefined log file path. +# When enabled, logs will always be written to this file. +# The CLI option --log-file (or -f) will override this setting if provided. +# log_file = "./tproxy.log" # Difficulty params [downstream_difficulty_config] @@ -28,9 +30,10 @@ min_extranonce2_size = 4 min_individual_miner_hashrate=10_000_000_000_000.0 # target number of shares per minute the miner should be sending shares_per_minute = 6.0 +# enable variable difficulty adjustment (true by default, set to false when using with JDC) +enable_vardiff = true -[upstream_difficulty_config] -# interval in seconds to elapse before updating channel hashrate with the pool -channel_diff_update_interval = 60 -# estimated accumulated hashrate of all downstream miners (e.g.: 10 Th/s = 10_000_000_000_000.0) -channel_nominal_hashrate = 10_000_000_000_000.0 +[[upstreams]] +address = "127.0.0.1" +port = 34254 +authority_pubkey = "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72" \ No newline at end of file diff --git a/roles/translator/src/args.rs b/roles/translator/src/args.rs index 91df433085..e43746ccaa 100644 --- a/roles/translator/src/args.rs +++ b/roles/translator/src/args.rs @@ -6,10 +6,7 @@ use clap::Parser; use ext_config::{Config, File, FileFormat}; use std::path::PathBuf; use tracing::error; -use translator_sv2::{ - config::TranslatorConfig, - error::{Error, ProxyResult}, -}; +use translator_sv2::{config::TranslatorConfig, error::TproxyError}; /// Holds the parsed CLI arguments. #[derive(Parser, Debug)] @@ -22,18 +19,24 @@ pub struct Args { default_value = "proxy-config.toml" )] pub config_path: PathBuf, + #[arg( + short = 'f', + long = "log-file", + help = "Path to the log file. If not set, logs will only be written to stdout." + )] + pub log_file: Option, } /// Process CLI args, if any. #[allow(clippy::result_large_err)] -pub fn process_cli_args<'a>() -> ProxyResult<'a, TranslatorConfig> { +pub fn process_cli_args() -> Result { // Parse CLI arguments let args = Args::parse(); // Build configuration from the provided file path let config_path = args.config_path.to_str().ok_or_else(|| { error!("Invalid configuration path."); - Error::BadCliArgs + TproxyError::BadCliArgs })?; let settings = Config::builder() @@ -41,6 +44,9 @@ pub fn process_cli_args<'a>() -> ProxyResult<'a, TranslatorConfig> { .build()?; // Deserialize settings into TranslatorConfig - let config = settings.try_deserialize::()?; + let mut config = settings.try_deserialize::()?; + + config.set_log_dir(args.log_file); + Ok(config) } diff --git a/roles/translator/src/lib/config.rs b/roles/translator/src/lib/config.rs index 91c0f54f41..0d6600330e 100644 --- a/roles/translator/src/lib/config.rs +++ b/roles/translator/src/lib/config.rs @@ -10,19 +10,15 @@ //! - Downstream interface address and port ([`DownstreamConfig`]) //! - Supported protocol versions //! - Downstream difficulty adjustment parameters ([`DownstreamDifficultyConfig`]) -//! - Upstream difficulty adjustment parameters ([`UpstreamDifficultyConfig`]) +use std::path::{Path, PathBuf}; + use key_utils::Secp256k1PublicKey; use serde::Deserialize; /// Configuration for the Translator. #[derive(Debug, Deserialize, Clone)] pub struct TranslatorConfig { - /// The address of the upstream server. - pub upstream_address: String, - /// The port of the upstream server. - pub upstream_port: u16, - /// The Secp256k1 public key used to authenticate the upstream authority. - pub upstream_authority_pubkey: Secp256k1PublicKey, + pub upstreams: Vec, /// The address for the downstream interface. pub downstream_address: String, /// The port for the downstream interface. @@ -31,86 +27,79 @@ pub struct TranslatorConfig { pub max_supported_version: u16, /// The minimum supported protocol version for communication. pub min_supported_version: u16, - /// The minimum size required for the extranonce2 field in mining submissions. - pub min_extranonce2_size: u16, + /// The size of the extranonce2 field for downstream mining connections. + pub downstream_extranonce2_size: u16, + /// The user identity/username to use when connecting to the pool. + /// This will be appended with a counter for each mining channel (e.g., username.miner1, + /// username.miner2). + pub user_identity: String, /// Configuration settings for managing difficulty on the downstream connection. pub downstream_difficulty_config: DownstreamDifficultyConfig, - /// Configuration settings for managing difficulty on the upstream connection. - pub upstream_difficulty_config: UpstreamDifficultyConfig, + /// Whether to aggregate all downstream connections into a single upstream channel. + /// If true, all miners share one channel. If false, each miner gets its own channel. + pub aggregate_channels: bool, + /// The path to the log file for the Translator. + log_file: Option, } -/// Configuration settings specific to the upstream connection. -pub struct UpstreamConfig { + +#[derive(Debug, Deserialize, Clone)] +pub struct Upstream { /// The address of the upstream server. - address: String, + pub address: String, /// The port of the upstream server. - port: u16, + pub port: u16, /// The Secp256k1 public key used to authenticate the upstream authority. - authority_pubkey: Secp256k1PublicKey, - /// Configuration settings for managing difficulty on the upstream connection. - difficulty_config: UpstreamDifficultyConfig, + pub authority_pubkey: Secp256k1PublicKey, } -impl UpstreamConfig { +impl Upstream { /// Creates a new `UpstreamConfig` instance. - pub fn new( - address: String, - port: u16, - authority_pubkey: Secp256k1PublicKey, - difficulty_config: UpstreamDifficultyConfig, - ) -> Self { + pub fn new(address: String, port: u16, authority_pubkey: Secp256k1PublicKey) -> Self { Self { address, port, authority_pubkey, - difficulty_config, - } - } -} - -/// Configuration settings specific to the downstream connection. -pub struct DownstreamConfig { - /// The address for the downstream interface. - address: String, - /// The port for the downstream interface. - port: u16, - /// Configuration settings for managing difficulty on the downstream connection. - difficulty_config: DownstreamDifficultyConfig, -} - -impl DownstreamConfig { - /// Creates a new `DownstreamConfig` instance. - pub fn new(address: String, port: u16, difficulty_config: DownstreamDifficultyConfig) -> Self { - Self { - address, - port, - difficulty_config, } } } impl TranslatorConfig { - /// Creates a new `TranslatorConfig` instance by combining upstream and downstream - /// configurations and specifying version and extranonce constraints. + /// Creates a new `TranslatorConfig` instance with the specified upstream and downstream + /// configurations and version constraints. + #[allow(clippy::too_many_arguments)] pub fn new( - upstream: UpstreamConfig, - downstream: DownstreamConfig, + upstreams: Vec, + downstream_address: String, + downstream_port: u16, + downstream_difficulty_config: DownstreamDifficultyConfig, max_supported_version: u16, min_supported_version: u16, - min_extranonce2_size: u16, + downstream_extranonce2_size: u16, + user_identity: String, + aggregate_channels: bool, ) -> Self { Self { - upstream_address: upstream.address, - upstream_port: upstream.port, - upstream_authority_pubkey: upstream.authority_pubkey, - downstream_address: downstream.address, - downstream_port: downstream.port, + upstreams, + downstream_address, + downstream_port, max_supported_version, min_supported_version, - min_extranonce2_size, - downstream_difficulty_config: downstream.difficulty_config, - upstream_difficulty_config: upstream.difficulty_config, + downstream_extranonce2_size, + user_identity, + downstream_difficulty_config, + aggregate_channels, + log_file: None, } } + + pub fn set_log_dir(&mut self, log_dir: Option) { + if let Some(dir) = log_dir { + self.log_file = Some(dir); + } + } + pub fn log_dir(&self) -> Option<&Path> { + self.log_file.as_deref() + } } /// Configuration settings for managing difficulty adjustments on the downstream connection. @@ -120,12 +109,9 @@ pub struct DownstreamDifficultyConfig { pub min_individual_miner_hashrate: f32, /// The target number of shares per minute for difficulty adjustment. pub shares_per_minute: f32, - /// The number of shares submitted since the last difficulty update. - #[serde(default = "u32::default")] - pub submits_since_last_update: u32, - /// The timestamp of the last difficulty update. - #[serde(default = "u64::default")] - pub timestamp_of_last_update: u64, + /// Whether to enable variable difficulty adjustment mechanism. + /// If false, difficulty will be managed by upstream (useful with JDC). + pub enable_vardiff: bool, } impl DownstreamDifficultyConfig { @@ -133,52 +119,150 @@ impl DownstreamDifficultyConfig { pub fn new( min_individual_miner_hashrate: f32, shares_per_minute: f32, - submits_since_last_update: u32, - timestamp_of_last_update: u64, + enable_vardiff: bool, ) -> Self { Self { min_individual_miner_hashrate, shares_per_minute, - submits_since_last_update, - timestamp_of_last_update, + enable_vardiff, } } } -impl PartialEq for DownstreamDifficultyConfig { - fn eq(&self, other: &Self) -> bool { - other.min_individual_miner_hashrate.round() as u32 - == self.min_individual_miner_hashrate.round() as u32 + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + fn create_test_upstream() -> Upstream { + // Use a valid base58-encoded public key from the key-utils test cases + let pubkey_str = "9bDuixKmZqAJnrmP746n8zU1wyAQRrus7th9dxnkPg6RzQvCnan"; + let pubkey = Secp256k1PublicKey::from_str(pubkey_str).unwrap(); + Upstream::new("127.0.0.1".to_string(), 4444, pubkey) } -} -/// Configuration settings for difficulty adjustments on the upstream connection. -#[derive(Debug, Deserialize, Clone)] -pub struct UpstreamDifficultyConfig { - /// The interval in seconds at which the channel difficulty should be updated. - pub channel_diff_update_interval: u32, - /// The nominal hashrate for the channel, used in difficulty calculations. - pub channel_nominal_hashrate: f32, - /// The timestamp of the last difficulty update for the channel. - #[serde(default = "u64::default")] - pub timestamp_of_last_update: u64, - /// Indicates whether shares from downstream should be aggregated before submitting upstream. - #[serde(default = "bool::default")] - pub should_aggregate: bool, -} + fn create_test_difficulty_config() -> DownstreamDifficultyConfig { + DownstreamDifficultyConfig::new(100.0, 5.0, true) + } -impl UpstreamDifficultyConfig { - /// Creates a new `UpstreamDifficultyConfig` instance. - pub fn new( - channel_diff_update_interval: u32, - channel_nominal_hashrate: f32, - timestamp_of_last_update: u64, - should_aggregate: bool, - ) -> Self { - Self { - channel_diff_update_interval, - channel_nominal_hashrate, - timestamp_of_last_update, - should_aggregate, - } + #[test] + fn test_upstream_creation() { + let upstream = create_test_upstream(); + assert_eq!(upstream.address, "127.0.0.1"); + assert_eq!(upstream.port, 4444); + } + + #[test] + fn test_downstream_difficulty_config_creation() { + let config = create_test_difficulty_config(); + assert_eq!(config.min_individual_miner_hashrate, 100.0); + assert_eq!(config.shares_per_minute, 5.0); + assert!(config.enable_vardiff); + } + + #[test] + fn test_translator_config_creation() { + let upstreams = vec![create_test_upstream()]; + let difficulty_config = create_test_difficulty_config(); + + let config = TranslatorConfig::new( + upstreams, + "0.0.0.0".to_string(), + 3333, + difficulty_config, + 2, + 1, + 4, + "test_user".to_string(), + true, + ); + + assert_eq!(config.upstreams.len(), 1); + assert_eq!(config.downstream_address, "0.0.0.0"); + assert_eq!(config.downstream_port, 3333); + assert_eq!(config.max_supported_version, 2); + assert_eq!(config.min_supported_version, 1); + assert_eq!(config.downstream_extranonce2_size, 4); + assert_eq!(config.user_identity, "test_user"); + assert!(config.aggregate_channels); + assert!(config.log_file.is_none()); + } + + #[test] + fn test_translator_config_log_dir() { + let upstreams = vec![create_test_upstream()]; + let difficulty_config = create_test_difficulty_config(); + + let mut config = TranslatorConfig::new( + upstreams, + "0.0.0.0".to_string(), + 3333, + difficulty_config, + 2, + 1, + 4, + "test_user".to_string(), + false, + ); + + assert!(config.log_dir().is_none()); + + let log_path = PathBuf::from("/tmp/logs"); + config.set_log_dir(Some(log_path.clone())); + assert_eq!(config.log_dir(), Some(log_path.as_path())); + + config.set_log_dir(None); + assert_eq!(config.log_dir(), Some(log_path.as_path())); // Should remain unchanged + } + + #[test] + fn test_multiple_upstreams() { + let upstream1 = create_test_upstream(); + let mut upstream2 = create_test_upstream(); + upstream2.address = "192.168.1.1".to_string(); + upstream2.port = 5555; + + let upstreams = vec![upstream1, upstream2]; + let difficulty_config = create_test_difficulty_config(); + + let config = TranslatorConfig::new( + upstreams, + "0.0.0.0".to_string(), + 3333, + difficulty_config, + 2, + 1, + 4, + "test_user".to_string(), + true, + ); + + assert_eq!(config.upstreams.len(), 2); + assert_eq!(config.upstreams[0].address, "127.0.0.1"); + assert_eq!(config.upstreams[0].port, 4444); + assert_eq!(config.upstreams[1].address, "192.168.1.1"); + assert_eq!(config.upstreams[1].port, 5555); + } + + #[test] + fn test_vardiff_disabled_config() { + let mut difficulty_config = create_test_difficulty_config(); + difficulty_config.enable_vardiff = false; + + let upstreams = vec![create_test_upstream()]; + let config = TranslatorConfig::new( + upstreams, + "0.0.0.0".to_string(), + 3333, + difficulty_config, + 2, + 1, + 4, + "test_user".to_string(), + false, + ); + + assert!(!config.downstream_difficulty_config.enable_vardiff); + assert!(!config.aggregate_channels); } } diff --git a/roles/translator/src/lib/downstream_sv1/diff_management.rs b/roles/translator/src/lib/downstream_sv1/diff_management.rs deleted file mode 100644 index 6831bf63d9..0000000000 --- a/roles/translator/src/lib/downstream_sv1/diff_management.rs +++ /dev/null @@ -1,421 +0,0 @@ -//! ## Downstream SV1 Difficulty Management Module -//! -//! This module contains the logic and helper functions -//! for managing difficulty and hashrate adjustments for downstream mining clients -//! communicating via the SV1 protocol. -//! -//! It handles tasks such as: -//! - Converting SV2 targets received from upstream into SV1 difficulty values. -//! - Calculating and updating individual miner hashrates based on submitted shares. -//! - Preparing SV1 `mining.set_difficulty` messages. -//! - Potentially managing difficulty thresholds and adjustment logic for downstream miners. - -use super::{Downstream, DownstreamMessages, SetDownstreamTarget}; - -use super::super::error::{Error, ProxyResult}; -use primitive_types::U256; -use roles_logic_sv2::{ - mining_sv2::Target, - utils::{hash_rate_to_target, Mutex}, -}; -use std::{ops::Div, sync::Arc}; -use tracing::debug; -use v1::json_rpc; - -impl Downstream { - /// Initializes the difficulty management parameters for a downstream connection. - /// - /// This function sets the initial timestamp for the last difficulty update and - /// resets the count of submitted shares. It also adds the miner's configured - /// minimum hashrate to the aggregated channel nominal hashrate stored in the - /// upstream difficulty configuration.Finally, it sends a `SetDownstreamTarget` message upstream - /// to the Bridge to inform it of the initial target for this new connection, derived from - /// the provided `init_target`.This should typically be called once when a downstream connection - /// is established. - pub async fn init_difficulty_management(self_: Arc>) -> ProxyResult<'static, ()> { - let (connection_id, upstream_difficulty_config, miner_hashrate, init_target) = self_ - .safe_lock(|d| { - _ = d.difficulty_mgmt.reset_counter(); - ( - d.connection_id, - d.upstream_difficulty_config.clone(), - d.hashrate, - d.target.clone(), - ) - })?; - // add new connection hashrate to channel hashrate - upstream_difficulty_config.safe_lock(|u| { - u.channel_nominal_hashrate += miner_hashrate; - })?; - // update downstream target with bridge - let init_target = binary_sv2::U256::from(init_target); - Self::send_message_upstream( - self_, - DownstreamMessages::SetDownstreamTarget(SetDownstreamTarget { - channel_id: connection_id, - new_target: init_target.into(), - }), - ) - .await?; - - Ok(()) - } - - /// Removes the disconnecting miner's hashrate from the aggregated channel nominal hashrate. - /// - /// This function is called when a downstream miner disconnects to ensure that their - /// individual hashrate is subtracted from the total nominal hashrate reported for - /// the channel to the upstream server. - #[allow(clippy::result_large_err)] - pub fn remove_miner_hashrate_from_channel(self_: Arc>) -> ProxyResult<'static, ()> { - self_.safe_lock(|d| { - d.upstream_difficulty_config - .safe_lock(|u| { - let hashrate_to_subtract = d.hashrate; - if u.channel_nominal_hashrate >= hashrate_to_subtract { - u.channel_nominal_hashrate -= hashrate_to_subtract; - } else { - u.channel_nominal_hashrate = 0.0; - } - }) - .map_err(|_e| Error::PoisonLock) - })??; - Ok(()) - } - - /// Attempts to update the difficulty settings for a downstream miner based on their - /// performance. - /// - /// This function is triggered periodically or based on share submissions. It calculates - /// the miner's estimated hashrate based on the number of shares submitted and the elapsed - /// time since the last update. If the estimated hashrate has changed significantly according to - /// predefined thresholds, a new target is calculated, a `mining.set_difficulty` message is - /// sent to the miner, and a `SetDownstreamTarget` message is sent upstream to the Bridge to - /// notify it of the target change for this channel. The difficulty management parameters - /// (timestamp and share count) are then reset. - pub async fn try_update_difficulty_settings( - self_: Arc>, - ) -> ProxyResult<'static, ()> { - let (timestamp_of_last_update, shares_since_last_update, channel_id, shares_per_minute) = - self_.clone().safe_lock(|d| { - ( - d.difficulty_mgmt.last_update_timestamp(), - d.difficulty_mgmt.shares_since_last_update(), - d.connection_id, - d.shares_per_minute, - ) - })?; - debug!("Time of last diff update: {:?}", timestamp_of_last_update); - debug!("Number of shares submitted: {:?}", shares_since_last_update); - - if let Some(new_hashrate) = Self::update_miner_hashrate(self_.clone())? { - let new_target: Target = - hash_rate_to_target(new_hashrate.into(), shares_per_minute.into())?.into(); - debug!("New target from hashrate: {:?}", new_target); - let message = Self::get_set_difficulty(new_target.clone())?; - let target = binary_sv2::U256::from(new_target); - Downstream::send_message_downstream(self_.clone(), message).await?; - let update_target_msg = SetDownstreamTarget { - channel_id, - new_target: target.into(), - }; - // notify bridge of target update - Downstream::send_message_upstream( - self_.clone(), - DownstreamMessages::SetDownstreamTarget(update_target_msg), - ) - .await?; - } - Ok(()) - } - - /// Increments the counter for shares submitted by this downstream miner. - /// - /// This function is called each time a valid share is received from the miner. - /// The count is used in the difficulty adjustment logic to estimate the miner's - /// performance over a period. - #[allow(clippy::result_large_err)] - pub(super) fn save_share(self_: Arc>) -> ProxyResult<'static, ()> { - self_.safe_lock(|d| { - d.difficulty_mgmt.increment_shares_since_last_update(); - })?; - Ok(()) - } - - /// Converts an SV2 target received from upstream into an SV1 difficulty value - /// and formats it as a `mining.set_difficulty` JSON-RPC message. - #[allow(clippy::result_large_err)] - pub(super) fn get_set_difficulty(target: Target) -> ProxyResult<'static, json_rpc::Message> { - let value = Downstream::difficulty_from_target(target)?; - debug!("Difficulty from target: {:?}", value); - let set_target = v1::methods::server_to_client::SetDifficulty { value }; - let message: json_rpc::Message = set_target.into(); - Ok(message) - } - - /// Converts target received by the `SetTarget` SV2 message from the Upstream role into the - /// difficulty for the Downstream role sent via the SV1 `mining.set_difficulty` message. - #[allow(clippy::result_large_err)] - pub(super) fn difficulty_from_target(target: Target) -> ProxyResult<'static, f64> { - // reverse because target is LE and this function relies on BE - let mut target = binary_sv2::U256::from(target).to_vec(); - - target.reverse(); - - let target = target.as_slice(); - debug!("Target: {:?}", target); - - // If received target is 0, return 0 - if Downstream::is_zero(target) { - return Ok(0.0); - } - let target = U256::from_big_endian(target); - let pdiff: [u8; 32] = [ - 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - ]; - let pdiff = U256::from_big_endian(pdiff.as_ref()); - - if pdiff > target { - let diff = pdiff.div(target); - Ok(diff.low_u64() as f64) - } else { - let diff = target.div(pdiff); - let diff = diff.low_u64() as f64; - // TODO still results in a difficulty that is too low - Ok(1.0 / diff) - } - } - - /// Updates the miner's estimated hashrate and adjusts the aggregated channel nominal hashrate. - /// - /// This function calculates the miner's realized shares per minute over the period - /// since the last update and uses it, along with the current target, to estimate - /// their hashrate. It then compares this new estimate to the previous one and - /// updates the miner's stored hashrate and the channel's aggregated hashrate - /// if the change is significant based on time-dependent thresholds. - #[allow(clippy::result_large_err)] - pub fn update_miner_hashrate(self_: Arc>) -> ProxyResult<'static, Option> { - let update = self_.super_safe_lock(|d| { - let previous_hashrate = d.hashrate; - let previous_target = d.target.clone(); - let update = d.difficulty_mgmt.try_vardiff( - previous_hashrate, - &previous_target, - d.shares_per_minute, - ); - if let Ok(Some(new_hashrate)) = update { - // update channel hashrate and target - let new_target: Target = - hash_rate_to_target(new_hashrate.into(), d.shares_per_minute.into()) - .expect("Something went wrong while target calculation") - .into(); - d.hashrate = new_hashrate; - d.target = new_target.clone(); - let hashrate_delta = new_hashrate - previous_hashrate; - d.upstream_difficulty_config.super_safe_lock(|c| { - if c.channel_nominal_hashrate + hashrate_delta > 0.0 { - c.channel_nominal_hashrate += hashrate_delta; - } else { - c.channel_nominal_hashrate = 0.0; - } - }); - } - update - })?; - Ok(update) - } - - /// Helper function to check if target is set to zero for some reason (typically happens when - /// Downstream role first connects). - /// https://stackoverflow.com/questions/65367552/checking-a-vecu8-to-see-if-its-all-zero - fn is_zero(buf: &[u8]) -> bool { - let (prefix, aligned, suffix) = unsafe { buf.align_to::() }; - - prefix.iter().all(|&x| x == 0) - && suffix.iter().all(|&x| x == 0) - && aligned.iter().all(|&x| x == 0) - } -} - -#[cfg(test)] -mod test { - - use crate::config::{DownstreamDifficultyConfig, UpstreamDifficultyConfig}; - use async_channel::unbounded; - use binary_sv2::U256; - use rand::{thread_rng, Rng}; - use roles_logic_sv2::{mining_sv2::Target, utils::Mutex}; - use sha2::{Digest, Sha256}; - use std::{ - sync::Arc, - time::{Duration, Instant}, - }; - - use crate::downstream_sv1::Downstream; - - #[ignore] // as described in issue #988 - #[test] - fn test_diff_management() { - let expected_shares_per_minute = 1000.0; - let total_run_time = std::time::Duration::from_secs(60); - let initial_nominal_hashrate = measure_hashrate(5); - let target = match roles_logic_sv2::utils::hash_rate_to_target( - initial_nominal_hashrate, - expected_shares_per_minute, - ) { - Ok(target) => target, - Err(_) => panic!(), - }; - - let mut share = generate_random_80_byte_array(); - let timer = std::time::Instant::now(); - let mut elapsed = std::time::Duration::from_secs(0); - let mut count = 0; - while elapsed <= total_run_time { - // start hashing util a target is met and submit to - mock_mine(target.clone().into(), &mut share); - elapsed = timer.elapsed(); - count += 1; - } - - let calculated_share_per_min = count as f32 / (elapsed.as_secs_f32() / 60.0); - // This is the error margin for a confidence of 99.99...% given the expect number of shares - // per minute TODO the review the math under it - let error_margin = get_error(expected_shares_per_minute); - let error = (calculated_share_per_min - expected_shares_per_minute as f32).abs(); - assert!( - error <= error_margin as f32, - "Calculated shares per minute are outside the 99.99...% confidence interval. Error: {:?}, Error margin: {:?}, {:?}", error, error_margin,calculated_share_per_min - ); - } - - fn get_error(lambda: f64) -> f64 { - let z_score_99 = 6.0; - z_score_99 * lambda.sqrt() - } - - fn mock_mine(target: Target, share: &mut [u8; 80]) { - let mut hashed: Target = [255_u8; 32].into(); - while hashed > target { - hashed = hash(share); - } - } - - // returns hashrate based on how fast the device hashes over the given duration - fn measure_hashrate(duration_secs: u64) -> f64 { - let mut share = generate_random_80_byte_array(); - let start_time = Instant::now(); - let mut hashes: u64 = 0; - let duration = Duration::from_secs(duration_secs); - - while start_time.elapsed() < duration { - for _ in 0..10000 { - hash(&mut share); - hashes += 1; - } - } - - let elapsed_secs = start_time.elapsed().as_secs_f64(); - - hashes as f64 / elapsed_secs - } - - fn hash(share: &mut [u8; 80]) -> Target { - let nonce: [u8; 8] = share[0..8].try_into().unwrap(); - let mut nonce = u64::from_le_bytes(nonce); - nonce += 1; - share[0..8].copy_from_slice(&nonce.to_le_bytes()); - let hash = Sha256::digest(&share).to_vec(); - let hash: U256<'static> = hash.try_into().unwrap(); - hash.into() - } - - fn generate_random_80_byte_array() -> [u8; 80] { - let mut rng = thread_rng(); - let mut arr = [0u8; 80]; - rng.fill(&mut arr[..]); - arr - } - - #[tokio::test] - async fn test_converge_to_spm_from_low() { - test_converge_to_spm(1.0).await - } - //TODO - //#[tokio::test] - //async fn test_converge_to_spm_from_high() { - // test_converge_to_spm(1_000_000_000_000).await - //} - - async fn test_converge_to_spm(start_hashrate: f64) { - let downstream_conf = DownstreamDifficultyConfig { - min_individual_miner_hashrate: start_hashrate as f32, // updated below - shares_per_minute: 1000.0, // 1000 shares per minute - submits_since_last_update: 0, - timestamp_of_last_update: 0, // updated below - }; - let upstream_config = UpstreamDifficultyConfig { - channel_diff_update_interval: 60, - channel_nominal_hashrate: 0.0, - timestamp_of_last_update: 0, - should_aggregate: false, - }; - let (tx_sv1_submit, _rx_sv1_submit) = unbounded(); - let (tx_outgoing, _rx_outgoing) = unbounded(); - let downstream = Downstream::new( - 1, - vec![], - vec![], - None, - None, - tx_sv1_submit, - tx_outgoing, - false, - 0, - downstream_conf.clone(), - Arc::new(Mutex::new(upstream_config)), - ); - - let total_run_time = std::time::Duration::from_secs(75); - let config_shares_per_minute = downstream_conf.shares_per_minute; - let timer = std::time::Instant::now(); - let mut elapsed = std::time::Duration::from_secs(0); - - let expected_nominal_hashrate = measure_hashrate(5); - let expected_target = match roles_logic_sv2::utils::hash_rate_to_target( - expected_nominal_hashrate, - config_shares_per_minute.into(), - ) { - Ok(target) => target, - Err(_) => panic!(), - }; - - let mut initial_target = downstream.target.clone(); - let downstream = Arc::new(Mutex::new(downstream)); - Downstream::init_difficulty_management(downstream.clone()) - .await - .unwrap(); - let mut share = generate_random_80_byte_array(); - while elapsed <= total_run_time { - mock_mine(initial_target.clone().into(), &mut share); - Downstream::save_share(downstream.clone()).unwrap(); - Downstream::try_update_difficulty_settings(downstream.clone()) - .await - .unwrap(); - initial_target = downstream.safe_lock(|d| d.target.clone()).unwrap(); - elapsed = timer.elapsed(); - } - let expected_0s = trailing_0s(expected_target.inner_as_ref().to_vec()); - let actual_0s = trailing_0s(binary_sv2::U256::from(initial_target.clone()).to_vec()); - assert!(expected_0s.abs_diff(actual_0s) <= 1); - } - - fn trailing_0s(mut v: Vec) -> usize { - let mut ret = 0; - while v.pop() == Some(0) { - ret += 1; - } - ret - } -} diff --git a/roles/translator/src/lib/downstream_sv1/downstream.rs b/roles/translator/src/lib/downstream_sv1/downstream.rs deleted file mode 100644 index 48cba8970b..0000000000 --- a/roles/translator/src/lib/downstream_sv1/downstream.rs +++ /dev/null @@ -1,743 +0,0 @@ -//! ## Downstream SV1 Module: Downstream Connection Logic -//! -//! Defines the [`Downstream`] structure, which represents and manages an -//! individual connection from a downstream SV1 mining client. -//! -//! This module is responsible for: -//! - Accepting incoming TCP connections from SV1 miners. -//! - Handling the SV1 protocol handshake (`mining.subscribe`, `mining.authorize`, -//! `mining.configure`). -//! - Receiving SV1 `mining.submit` messages from miners. -//! - Translating SV1 `mining.submit` messages into internal [`DownstreamMessages`] (specifically -//! [`SubmitShareWithChannelId`]) and sending them to the Bridge. -//! - Receiving translated SV1 `mining.notify` messages from the Bridge and sending them to the -//! connected miner. -//! - Managing the miner's extranonce1, extranonce2 size, and version rolling parameters. -//! - Implementing downstream-specific difficulty management logic, including tracking submitted -//! shares and updating the miner's difficulty target. -//! - Implementing the necessary SV1 server traits ([`IsServer`]) and SV2 roles logic traits -//! ([`IsMiningDownstream`], [`IsDownstream`]). - -use crate::{ - config::{DownstreamDifficultyConfig, UpstreamDifficultyConfig}, - downstream_sv1, - error::ProxyResult, - status, -}; -use async_channel::{bounded, Receiver, Sender}; -use error_handling::handle_result; -use futures::{FutureExt, StreamExt}; -use tokio::{ - io::{AsyncWriteExt, BufReader}, - net::{TcpListener, TcpStream}, - sync::broadcast, - task::AbortHandle, -}; - -use super::{kill, DownstreamMessages, SubmitShareWithChannelId, SUBSCRIBE_TIMEOUT_SECS}; - -use roles_logic_sv2::{ - common_properties::{IsDownstream, IsMiningDownstream}, - mining_sv2::Target, - utils::{hash_rate_to_target, Mutex}, - vardiff::Vardiff, - VardiffState, -}; - -use crate::error::Error; -use futures::select; -use tokio_util::codec::{FramedRead, LinesCodec}; - -use std::{net::SocketAddr, sync::Arc}; -use tracing::{debug, info, warn}; -use v1::{ - client_to_server::{self, Submit}, - json_rpc, server_to_client, - utils::{Extranonce, HexU32Be}, - IsServer, -}; - -/// The maximum allowed length for a single line (JSON-RPC message) received from an SV1 client. -const MAX_LINE_LENGTH: usize = 2_usize.pow(16); - -/// Handles the sending and receiving of messages to and from an SV2 Upstream role (most typically -/// a SV2 Pool server). -#[derive(Debug)] -pub struct Downstream { - /// The unique identifier assigned to this downstream connection/channel. - pub(super) connection_id: u32, - /// List of authorized Downstream Mining Devices. - authorized_names: Vec, - /// The extranonce1 value assigned to this downstream miner. - extranonce1: Vec, - /// `extranonce1` to be sent to the Downstream in the SV1 `mining.subscribe` message response. - //extranonce1: Vec, - //extranonce2_size: usize, - /// Version rolling mask bits - version_rolling_mask: Option, - /// Minimum version rolling mask bits size - version_rolling_min_bit: Option, - /// Sends a SV1 `mining.submit` message received from the Downstream role to the `Bridge` for - /// translation into a SV2 `SubmitSharesExtended`. - tx_sv1_bridge: Sender, - /// Sends message to the SV1 Downstream role. - tx_outgoing: Sender, - /// True if this is the first job received from `Upstream`. - first_job_received: bool, - /// The expected size of the extranonce2 field provided by the miner. - extranonce2_len: usize, - // Current Channel target - pub target: Target, - // Current channel hashrate - pub hashrate: f32, - // Shares_per_minute - pub shares_per_minute: f32, - /// Configuration and state for managing difficulty adjustments specific - /// to this individual downstream miner. - pub(super) difficulty_mgmt: Box, - /// Configuration settings for the upstream channel's difficulty management. - pub(super) upstream_difficulty_config: Arc>, -} - -impl Downstream { - // not huge fan of test specific code in codebase. - #[cfg(test)] - pub fn new( - connection_id: u32, - authorized_names: Vec, - extranonce1: Vec, - version_rolling_mask: Option, - version_rolling_min_bit: Option, - tx_sv1_bridge: Sender, - tx_outgoing: Sender, - first_job_received: bool, - extranonce2_len: usize, - difficulty_mgmt: DownstreamDifficultyConfig, - upstream_difficulty_config: Arc>, - ) -> Self { - use roles_logic_sv2::utils::hash_rate_to_target; - - let hashrate = difficulty_mgmt.min_individual_miner_hashrate; - let target = hash_rate_to_target(hashrate.into(), difficulty_mgmt.shares_per_minute.into()) - .unwrap() - .into(); - let downstream_difficulty_state = VardiffState::new().unwrap(); - Downstream { - connection_id, - authorized_names, - extranonce1, - version_rolling_mask, - version_rolling_min_bit, - tx_sv1_bridge, - tx_outgoing, - first_job_received, - extranonce2_len, - hashrate, - target, - shares_per_minute: difficulty_mgmt.shares_per_minute, - difficulty_mgmt: Box::new(downstream_difficulty_state), - upstream_difficulty_config, - } - } - /// Instantiates and manages a new handler for a single downstream SV1 client connection. - /// - /// This is the primary function called for each new incoming TCP stream from a miner. - /// It sets up the communication channels, initializes the `Downstream` struct state, - /// and spawns the necessary tasks to handle: - /// 1. Reading incoming messages from the miner's socket. - /// 2. Writing outgoing messages to the miner's socket. - /// 3. Sending job notifications to the miner (handling initial job and subsequent updates). - /// - /// It uses shutdown channels to coordinate graceful termination of the spawned tasks. - #[allow(clippy::too_many_arguments)] - pub async fn new_downstream( - stream: TcpStream, - connection_id: u32, - tx_sv1_bridge: Sender, - mut rx_sv1_notify: broadcast::Receiver>, - tx_status: status::Sender, - extranonce1: Vec, - last_notify: Option>, - extranonce2_len: usize, - host: String, - difficulty_config: DownstreamDifficultyConfig, - upstream_difficulty_config: Arc>, - task_collector: Arc>>, - ) { - let hashrate = difficulty_config.min_individual_miner_hashrate; - let target = - hash_rate_to_target(hashrate.into(), difficulty_config.shares_per_minute.into()) - .expect("Couldn't convert hashrate to target") - .into(); - - let downstream_difficulty_state = - VardiffState::new().expect("Couldn't initialize vardiff module"); - // Reads and writes from Downstream SV1 Mining Device Client - let (socket_reader, mut socket_writer) = stream.into_split(); - let (tx_outgoing, receiver_outgoing) = bounded(10); - - let downstream = Arc::new(Mutex::new(Downstream { - connection_id, - authorized_names: vec![], - extranonce1, - //extranonce1: extranonce1.to_vec(), - version_rolling_mask: None, - version_rolling_min_bit: None, - tx_sv1_bridge, - tx_outgoing, - first_job_received: false, - extranonce2_len, - hashrate, - target, - shares_per_minute: difficulty_config.shares_per_minute, - difficulty_mgmt: Box::new(downstream_difficulty_state), - upstream_difficulty_config, - })); - let self_ = downstream.clone(); - - let host_ = host.clone(); - // The shutdown channel is used local to the `Downstream::new_downstream()` function. - // Each task is set broadcast a shutdown message at the end of their lifecycle with - // `kill()`, and each task has a receiver to listen for the shutdown message. When a - // shutdown message is received the task should `break` its loop. For any errors that should - // shut a task down, we should `break` out of the loop, so that the `kill` function - // can send the shutdown broadcast. EXTRA: The since all downstream tasks rely on - // receiving messages with a future (either TCP recv or Receiver<_>) we use the - // futures::select! macro to merge the receiving end of a task channels into a single loop - // within the task - let (tx_shutdown, rx_shutdown): (Sender, Receiver) = async_channel::bounded(3); - - let rx_shutdown_clone = rx_shutdown.clone(); - let tx_shutdown_clone = tx_shutdown.clone(); - let tx_status_reader = tx_status.clone(); - let task_collector_mining_device = task_collector.clone(); - // Task to read from SV1 Mining Device Client socket via `socket_reader`. Depending on the - // SV1 message received, a message response is sent directly back to the SV1 Downstream - // role, or the message is sent upwards to the Bridge for translation into a SV2 message - // and then sent to the SV2 Upstream role. - let socket_reader_task = tokio::task::spawn(async move { - let reader = BufReader::new(socket_reader); - let mut messages = - FramedRead::new(reader, LinesCodec::new_with_max_length(MAX_LINE_LENGTH)); - loop { - // Read message from SV1 Mining Device Client socket - // On message receive, parse to `json_rpc:Message` and send to Upstream - // `Translator.receive_downstream` via `sender_upstream` done in - // `send_message_upstream`. - select! { - res = messages.next().fuse() => { - match res { - Some(Ok(incoming)) => { - debug!("Receiving from Mining Device {}: {:?}", &host_, &incoming); - let incoming: json_rpc::Message = handle_result!(tx_status_reader, serde_json::from_str(&incoming)); - // Handle what to do with message - // if let json_rpc::Message - - // if message is Submit Shares update difficulty management - if let v1::Message::StandardRequest(standard_req) = incoming.clone() { - if let Ok(Submit{..}) = standard_req.try_into() { - handle_result!(tx_status_reader, Self::save_share(self_.clone())); - } - } - - let res = Self::handle_incoming_sv1(self_.clone(), incoming).await; - handle_result!(tx_status_reader, res); - } - Some(Err(_)) => { - handle_result!(tx_status_reader, Err(Error::Sv1MessageTooLong)); - } - None => { - handle_result!(tx_status_reader, Err( - std::io::Error::new( - std::io::ErrorKind::ConnectionAborted, - "Connection closed by client" - ) - )); - } - } - }, - _ = rx_shutdown_clone.recv().fuse() => { - break; - } - }; - } - kill(&tx_shutdown_clone).await; - warn!("Downstream: Shutting down sv1 downstream reader"); - }); - let _ = task_collector_mining_device.safe_lock(|a| { - a.push(( - socket_reader_task.abort_handle(), - "socket_reader_task".to_string(), - )) - }); - - let rx_shutdown_clone = rx_shutdown.clone(); - let tx_shutdown_clone = tx_shutdown.clone(); - let tx_status_writer = tx_status.clone(); - let host_ = host.clone(); - - let task_collector_new_sv1_message_no_transl = task_collector.clone(); - // Task to receive SV1 message responses to SV1 messages that do NOT need translation. - // These response messages are sent directly to the SV1 Downstream role. - let socket_writer_task = tokio::task::spawn(async move { - loop { - select! { - res = receiver_outgoing.recv().fuse() => { - let to_send = handle_result!(tx_status_writer, res); - let to_send = match serde_json::to_string(&to_send) { - Ok(string) => format!("{}\n", string), - Err(_e) => { - debug!("\nDownstream: Bad SV1 server message\n"); - break; - } - }; - debug!("Sending to Mining Device: {} - {:?}", &host_, &to_send); - let res = socket_writer - .write_all(to_send.as_bytes()) - .await; - handle_result!(tx_status_writer, res); - }, - _ = rx_shutdown_clone.recv().fuse() => { - break; - } - }; - } - kill(&tx_shutdown_clone).await; - warn!( - "Downstream: Shutting down sv1 downstream writer: {}", - &host_ - ); - }); - let _ = task_collector_new_sv1_message_no_transl.safe_lock(|a| { - a.push(( - socket_writer_task.abort_handle(), - "socket_writer_task".to_string(), - )) - }); - - let tx_status_notify = tx_status; - let self_ = downstream.clone(); - - let task_collector_notify_task = task_collector.clone(); - let notify_task = tokio::task::spawn(async move { - let timeout_timer = std::time::Instant::now(); - let mut first_sent = false; - loop { - let is_a = match downstream.safe_lock(|d| !d.authorized_names.is_empty()) { - Ok(is_a) => is_a, - Err(_e) => { - debug!("\nDownstream: Poison Lock - authorized_names\n"); - break; - } - }; - if is_a && !first_sent && last_notify.is_some() { - let target = downstream - .safe_lock(|d| d.target.clone()) - .expect("downstream target couldn't be computed"); - // make sure the mining start time is initialized and reset number of shares - // submitted - handle_result!( - tx_status_notify, - Self::init_difficulty_management(downstream.clone()).await - ); - let message = - handle_result!(tx_status_notify, Self::get_set_difficulty(target)); - handle_result!( - tx_status_notify, - Downstream::send_message_downstream(downstream.clone(), message).await - ); - - let sv1_mining_notify_msg = last_notify.clone().unwrap(); - - let message: json_rpc::Message = sv1_mining_notify_msg.into(); - handle_result!( - tx_status_notify, - Downstream::send_message_downstream(downstream.clone(), message).await - ); - if let Err(_e) = downstream.clone().safe_lock(|s| { - s.first_job_received = true; - }) { - debug!("\nDownstream: Poison Lock - first_job_received\n"); - break; - } - first_sent = true; - } else if is_a { - // if hashrate has changed, update difficulty management, and send new - // mining.set_difficulty - select! { - res = rx_sv1_notify.recv().fuse() => { - // if hashrate has changed, update difficulty management, and send new mining.set_difficulty - handle_result!(tx_status_notify, Self::try_update_difficulty_settings(downstream.clone()).await); - - let sv1_mining_notify_msg = handle_result!(tx_status_notify, res); - let message: json_rpc::Message = sv1_mining_notify_msg.clone().into(); - - handle_result!(tx_status_notify, Downstream::send_message_downstream(downstream.clone(), message).await); - }, - _ = rx_shutdown.recv().fuse() => { - break; - } - }; - } else { - // timeout connection if miner does not send the authorize message after sending - // a subscribe - if timeout_timer.elapsed().as_secs() > SUBSCRIBE_TIMEOUT_SECS { - debug!( - "Downstream: miner.subscribe/miner.authorize TIMOUT for {}", - &host - ); - break; - } - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - } - } - let _ = Self::remove_miner_hashrate_from_channel(self_); - kill(&tx_shutdown).await; - warn!( - "Downstream: Shutting down sv1 downstream job notifier for {}", - &host - ); - }); - - let _ = task_collector_notify_task - .safe_lock(|a| a.push((notify_task.abort_handle(), "notify_task".to_string()))); - } - - /// Accepts incoming TCP connections from SV1 mining clients on the configured address. - /// - /// For each new connection, it attempts to open a new SV1 downstream channel - /// via the Bridge (`bridge.on_new_sv1_connection`). If successful, it spawns - /// a new task using `Downstream::new_downstream` to handle - /// the communication and logic for that specific miner connection. - /// This method runs indefinitely, listening for and accepting new connections. - #[allow(clippy::too_many_arguments)] - pub fn accept_connections( - downstream_addr: SocketAddr, - tx_sv1_submit: Sender, - tx_mining_notify: broadcast::Sender>, - tx_status: status::Sender, - bridge: Arc>, - downstream_difficulty_config: DownstreamDifficultyConfig, - upstream_difficulty_config: Arc>, - task_collector: Arc>>, - ) { - let accept_connections = tokio::task::spawn({ - let task_collector = task_collector.clone(); - async move { - let listener = TcpListener::bind(downstream_addr).await.unwrap(); - - while let Ok((stream, _)) = listener.accept().await { - let expected_hash_rate = - downstream_difficulty_config.min_individual_miner_hashrate; - let open_sv1_downstream = bridge - .safe_lock(|s| s.on_new_sv1_connection(expected_hash_rate)) - .unwrap(); - - let host = stream.peer_addr().unwrap().to_string(); - - match open_sv1_downstream { - Ok(opened) => { - info!("PROXY SERVER - ACCEPTING FROM DOWNSTREAM: {}", host); - Downstream::new_downstream( - stream, - opened.channel_id, - tx_sv1_submit.clone(), - tx_mining_notify.subscribe(), - tx_status.listener_to_connection(), - opened.extranonce, - opened.last_notify, - opened.extranonce2_len as usize, - host, - downstream_difficulty_config.clone(), - upstream_difficulty_config.clone(), - task_collector.clone(), - ) - .await; - } - Err(e) => { - tracing::error!( - "Failed to create a new downstream connection: {:?}", - e - ); - } - } - } - } - }); - let _ = task_collector.safe_lock(|a| { - a.push(( - accept_connections.abort_handle(), - "accept_connections".to_string(), - )) - }); - } - - /// Handles incoming SV1 JSON-RPC messages from a downstream miner. - /// - /// This function acts as the entry point for processing messages received - /// from a miner after framing. It uses the `IsServer` trait implementation - /// to parse and handle standard SV1 requests (`mining.subscribe`, `mining.authorize`, - /// `mining.submit`, `mining.configure`). Depending on the message type, it may generate a - /// direct SV1 response to be sent back to the miner or indicate that the message needs to - /// be translated and sent upstream (handled elsewhere, typically by the Bridge). - async fn handle_incoming_sv1( - self_: Arc>, - message_sv1: json_rpc::Message, - ) -> Result<(), super::super::error::Error<'static>> { - // `handle_message` in `IsServer` trait + calls `handle_request` - // TODO: Map err from V1Error to Error::V1Error - let response = self_.safe_lock(|s| s.handle_message(message_sv1)).unwrap(); - match response { - Ok(res) => { - if let Some(r) = res { - // If some response is received, indicates no messages translation is needed - // and response should be sent directly to the SV1 Downstream. Otherwise, - // message will be sent to the upstream Translator to be translated to SV2 and - // forwarded to the `Upstream` - // let sender = self_.safe_lock(|s| s.connection.sender_upstream) - if let Err(e) = Self::send_message_downstream(self_, r.into()).await { - return Err(e.into()); - } - Ok(()) - } else { - // If None response is received, indicates this SV1 message received from the - // Downstream MD is passed to the `Translator` for translation into SV2 - Ok(()) - } - } - Err(e) => Err(e.into()), - } - } - - /// Sends a SV1 JSON-RPC message to the downstream miner's socket writer task. - /// - /// This method is used to send response messages or notifications (like - /// `mining.notify` or `mining.set_difficulty`) to the connected miner. - /// The message is sent over the internal `tx_outgoing` channel, which is - /// read by the socket writer task responsible for serializing and writing - /// the message to the TCP stream. - pub(super) async fn send_message_downstream( - self_: Arc>, - response: json_rpc::Message, - ) -> Result<(), async_channel::SendError> { - let sender = self_.safe_lock(|s| s.tx_outgoing.clone()).unwrap(); - debug!("To DOWN: {:?}", response); - sender.send(response).await - } - - /// Sends a message originating from the downstream handler to the Bridge. - /// - /// This function is used to forward messages that require translation or - /// central processing by the Bridge, such as `SubmitShares` or `SetDownstreamTarget`. - /// The message is sent over the internal `tx_sv1_bridge` channel. - pub(super) async fn send_message_upstream( - self_: Arc>, - msg: DownstreamMessages, - ) -> ProxyResult<'static, ()> { - let sender = self_.safe_lock(|s| s.tx_sv1_bridge.clone()).unwrap(); - debug!("To Bridge: {:?}", msg); - let _ = sender.send(msg).await; - Ok(()) - } -} - -/// Implements `IsServer` for `Downstream` to handle the SV1 messages. -impl IsServer<'static> for Downstream { - /// Handles the incoming SV1 `mining.configure` message. - /// - /// This message is received after `mining.subscribe` and `mining.authorize`. - /// It allows the miner to negotiate capabilities, particularly regarding - /// version rolling. This method processes the version rolling mask and - /// minimum bit count provided by the client. - /// - /// Returns a tuple containing: - /// 1. `Option`: The version rolling parameters - /// negotiated by the server (proxy). - /// 2. `Option`: A boolean indicating whether the server (proxy) supports version rolling - /// (always `Some(false)` for TProxy according to the SV1 spec when not supporting work - /// selection). - fn handle_configure( - &mut self, - request: &client_to_server::Configure, - ) -> (Option, Option) { - info!("Down: Configuring"); - debug!("Down: Handling mining.configure: {:?}", &request); - - // TODO 0x1FFFE000 should be configured - // = 11111111111111110000000000000 - // this is a reasonable default as it allows all 16 version bits to be used - // If the tproxy/pool needs to use some version bits this needs to be configurable - // so upstreams can negotiate with downstreams. When that happens this should consider - // the min_bit_count in the mining.configure message - self.version_rolling_mask = request - .version_rolling_mask() - .map(|mask| HexU32Be(mask & 0x1FFFE000)); - self.version_rolling_min_bit = request.version_rolling_min_bit_count(); - - debug!( - "Negotiated version_rolling_mask is {:?}", - self.version_rolling_mask - ); - ( - Some(server_to_client::VersionRollingParams::new( - self.version_rolling_mask.clone().unwrap_or(HexU32Be(0)), - self.version_rolling_min_bit.clone().unwrap_or(HexU32Be(0)), - ).expect("Version mask invalid, automatic version mask selection not supported, please change it in carte::downstream_sv1::mod.rs")), - Some(false), - ) - } - - /// Handles the incoming SV1 `mining.subscribe` message. - /// - /// This is typically the first message received from a new client. In the SV1 - /// protocol, it's used to subscribe to job notifications and receive session - /// details like extranonce1 and extranonce2 size. This method acknowledges the subscription and - /// provides the necessary details derived from the upstream SV2 connection (extranonce1 and - /// extranonce2 size). It also provides subscription IDs for the - /// `mining.set_difficulty` and `mining.notify` methods. - fn handle_subscribe(&self, request: &client_to_server::Subscribe) -> Vec<(String, String)> { - info!("Down: Subscribing"); - debug!("Down: Handling mining.subscribe: {:?}", &request); - - let set_difficulty_sub = ( - "mining.set_difficulty".to_string(), - downstream_sv1::new_subscription_id(), - ); - let notify_sub = ( - "mining.notify".to_string(), - "ae6812eb4cd7735a302a8a9dd95cf71f".to_string(), - ); - - vec![set_difficulty_sub, notify_sub] - } - - /// Any numbers of workers may be authorized at any time during the session. In this way, a - /// large number of independent Mining Devices can be handled with a single SV1 connection. - /// https://bitcoin.stackexchange.com/questions/29416/how-do-pool-servers-handle-multiple-workers-sharing-one-connection-with-stratum - fn handle_authorize(&self, request: &client_to_server::Authorize) -> bool { - info!("Down: Authorizing"); - debug!("Down: Handling mining.authorize: {:?}", &request); - true - } - - /// Handles the incoming SV1 `mining.submit` message. - /// - /// This message is sent by the miner when they find a share that meets - /// their current difficulty target. It contains the job ID, ntime, nonce, - /// and extranonce2. - /// - /// This method processes the submitted share, potentially validates it - /// against the downstream target (although this might happen in the Bridge - /// or difficulty management logic), translates it into a - /// [`SubmitShareWithChannelId`], and sends it to the Bridge for - /// translation to SV2 and forwarding upstream if it meets the upstream target. - fn handle_submit(&self, request: &client_to_server::Submit<'static>) -> bool { - info!("Down: Submitting Share {:?}", request); - debug!("Down: Handling mining.submit: {:?}", &request); - - // TODO: Check if receiving valid shares by adding diff field to Downstream - - let to_send = SubmitShareWithChannelId { - channel_id: self.connection_id, - share: request.clone(), - extranonce: self.extranonce1.clone(), - extranonce2_len: self.extranonce2_len, - version_rolling_mask: self.version_rolling_mask.clone(), - }; - - self.tx_sv1_bridge - .try_send(DownstreamMessages::SubmitShares(to_send)) - .unwrap(); - - true - } - - /// Indicates to the server that the client supports the mining.set_extranonce method. - fn handle_extranonce_subscribe(&self) {} - - /// Checks if a Downstream role is authorized. - fn is_authorized(&self, name: &str) -> bool { - self.authorized_names.contains(&name.to_string()) - } - - /// Authorizes a Downstream role. - fn authorize(&mut self, name: &str) { - self.authorized_names.push(name.to_string()); - } - - /// Sets the `extranonce1` field sent in the SV1 `mining.notify` message to the value specified - /// by the SV2 `OpenExtendedMiningChannelSuccess` message sent from the Upstream role. - fn set_extranonce1( - &mut self, - _extranonce1: Option>, - ) -> Extranonce<'static> { - self.extranonce1.clone().try_into().unwrap() - } - - /// Returns the `Downstream`'s `extranonce1` value. - fn extranonce1(&self) -> Extranonce<'static> { - self.extranonce1.clone().try_into().unwrap() - } - - /// Sets the `extranonce2_size` field sent in the SV1 `mining.notify` message to the value - /// specified by the SV2 `OpenExtendedMiningChannelSuccess` message sent from the Upstream role. - fn set_extranonce2_size(&mut self, _extra_nonce2_size: Option) -> usize { - self.extranonce2_len - } - - /// Returns the `Downstream`'s `extranonce2_size` value. - fn extranonce2_size(&self) -> usize { - self.extranonce2_len - } - - /// Returns the version rolling mask. - fn version_rolling_mask(&self) -> Option { - self.version_rolling_mask.clone() - } - - /// Sets the version rolling mask. - fn set_version_rolling_mask(&mut self, mask: Option) { - self.version_rolling_mask = mask; - } - - /// Sets the minimum version rolling bit. - fn set_version_rolling_min_bit(&mut self, mask: Option) { - self.version_rolling_min_bit = mask - } - - fn notify(&mut self) -> Result { - unreachable!() - } -} - -// Can we remove this? -impl IsMiningDownstream for Downstream {} -// Can we remove this? -impl IsDownstream for Downstream { - fn get_downstream_mining_data( - &self, - ) -> roles_logic_sv2::common_properties::CommonDownstreamData { - todo!() - } -} - -#[cfg(test)] -mod tests { - use binary_sv2::U256; - use roles_logic_sv2::mining_sv2::Target; - - use super::*; - - #[test] - fn gets_difficulty_from_target() { - let target = vec![ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 255, 127, - 0, 0, 0, 0, 0, - ]; - let target_u256 = U256::Owned(target); - let target = Target::from(target_u256); - let actual = Downstream::difficulty_from_target(target).unwrap(); - let expect = 512.0; - assert_eq!(actual, expect); - } -} diff --git a/roles/translator/src/lib/downstream_sv1/mod.rs b/roles/translator/src/lib/downstream_sv1/mod.rs deleted file mode 100644 index f0847acb92..0000000000 --- a/roles/translator/src/lib/downstream_sv1/mod.rs +++ /dev/null @@ -1,71 +0,0 @@ -//! ## Downstream SV1 Module -//! -//! This module defines the structures, messages, and utility functions -//! used for handling the downstream connection with SV1 mining clients. -//! -//! It includes definitions for messages exchanged with a Bridge component, -//! structures for submitting shares and updating targets, and constants -//! and functions for managing client interactions. -//! -//! The module is organized into the following sub-modules: -//! - [`diff_management`]: (Declared here, likely contains downstream difficulty logic) -//! - [`downstream`]: Defines the core [`Downstream`] struct and its functionalities. - -use roles_logic_sv2::mining_sv2::Target; -use v1::{client_to_server::Submit, utils::HexU32Be}; -pub mod diff_management; -pub mod downstream; -pub use downstream::Downstream; - -/// This constant defines a timeout duration. It is used to enforce -/// that clients sending a `mining.subscribe` message must follow up -/// with a `mining.authorize` within this period. This prevents -/// resource exhaustion attacks where clients open connections -/// with only `mining.subscribe` without intending to mine. -const SUBSCRIBE_TIMEOUT_SECS: u64 = 10; - -/// The messages that are sent from the downstream handling logic -/// to a central "Bridge" component for further processing. -#[derive(Debug)] -pub enum DownstreamMessages { - /// Represents a submitted share from a downstream miner, - /// wrapped with the relevant channel ID. - SubmitShares(SubmitShareWithChannelId), - /// Represents an update to the downstream target for a specific channel. - SetDownstreamTarget(SetDownstreamTarget), -} - -/// wrapper around a `mining.submit` with extra channel informationfor the Bridge to -/// process -#[derive(Debug)] -pub struct SubmitShareWithChannelId { - pub channel_id: u32, - pub share: Submit<'static>, - pub extranonce: Vec, - pub extranonce2_len: usize, - pub version_rolling_mask: Option, -} - -/// message for notifying the bridge that a downstream target has updated -/// so the Bridge can process the update -#[derive(Debug)] -pub struct SetDownstreamTarget { - pub channel_id: u32, - pub new_target: Target, -} - -/// This is just a wrapper function to send a message on the Downstream task shutdown channel -/// it does not matter what message is sent because the receiving ends should shutdown on any -/// message -pub async fn kill(sender: &async_channel::Sender) { - // safe to unwrap since the only way this can fail is if all receiving channels are dropped - // meaning all tasks have already dropped - sender.send(true).await.unwrap(); -} - -/// Generates a new, hardcoded string intended to be used as a subscription ID. -/// -/// FIXME -pub fn new_subscription_id() -> String { - "ae6812eb4cd7735a302a8a9dd95cf71f".into() -} diff --git a/roles/translator/src/lib/error.rs b/roles/translator/src/lib/error.rs index 03c6ff7ea6..20959a0c89 100644 --- a/roles/translator/src/lib/error.rs +++ b/roles/translator/src/lib/error.rs @@ -8,57 +8,28 @@ //! - A specific `ChannelSendError` enum for errors occurring during message sending over //! asynchronous channels. -use codec_sv2::Frame; use ext_config::ConfigError; -use roles_logic_sv2::{ - mining_sv2::{ExtendedExtranonce, NewExtendedMiningJob, SetCustomMiningJob}, - parsers::{AnyMessage, Mining}, - vardiff::error::VardiffError, -}; use std::{fmt, sync::PoisonError}; -use v1::server_to_client::{Notify, SetDifficulty}; - -pub type ProxyResult<'a, T> = core::result::Result>; - -/// Represents specific errors that can occur when sending messages over various -/// channels used within the translator. -/// -/// Each variant corresponds to a failure in sending a particular type of message -/// on its designated channel. -#[derive(Debug)] -pub enum ChannelSendError<'a> { - /// Failure sending an SV2 `SubmitSharesExtended` message. - SubmitSharesExtended( - async_channel::SendError>, - ), - /// Failure sending an SV2 `SetNewPrevHash` message. - SetNewPrevHash(async_channel::SendError>), - /// Failure sending an SV2 `NewExtendedMiningJob` message. - NewExtendedMiningJob(async_channel::SendError>), - /// Failure broadcasting an SV1 `Notify` message - Notify(tokio::sync::broadcast::error::SendError>), - /// Failure sending a generic SV1 message. - V1Message(async_channel::SendError), - /// Represents a generic channel send failure, described by a string. - General(String), - /// Failure sending extranonce information. - Extranonce(async_channel::SendError<(ExtendedExtranonce, u32)>), - /// Failure sending an SV2 `SetCustomMiningJob` message. - SetCustomMiningJob( - async_channel::SendError>, - ), - /// Failure sending new template information (prevhash and coinbase). - NewTemplate( - async_channel::SendError<( - roles_logic_sv2::template_distribution_sv2::SetNewPrevHash<'a>, - Vec, - )>, - ), -} +use stratum_common::roles_logic_sv2::{ + codec_sv2::{self, binary_sv2, framing_sv2}, + errors::Error as RolesLogicError, + handlers_sv2::HandlerErrorType, + parsers_sv2::ParserError as RolesParserError, + Error as RolesSv2Error, +}; +use tokio::sync::broadcast; +use v1::server_to_client::SetDifficulty; #[derive(Debug)] -pub enum Error<'a> { - VecToSlice32(Vec), +pub enum TproxyError { + /// Generic SV1 protocol error + SV1Error, + /// Error from the network helpers library + NetworkHelpersError(network_helpers_sv2::Error), + /// Error from the roles logic library + RolesSv2LogicError(RolesSv2Error), + /// Error from roles logic parser library + ParserError(RolesParserError), /// Errors on bad CLI argument input. BadCliArgs, /// Errors on bad `serde_json` serialize/deserialize. @@ -73,249 +44,176 @@ pub enum Error<'a> { FramingSv2(framing_sv2::Error), /// Errors on bad `TcpStream` connection. Io(std::io::Error), - /// Errors due to invalid extranonce from upstream - InvalidExtranonce(String), /// Errors on bad `String` to `int` conversion. ParseInt(std::num::ParseIntError), - /// Errors from `roles_logic_sv2` crate. - RolesSv2Logic(roles_logic_sv2::errors::Error), - UpstreamIncoming(roles_logic_sv2::errors::Error), - /// SV1 protocol library error - V1Protocol(v1::error::Error<'a>), - #[allow(dead_code)] - SubprotocolMining(String), - // Locking Errors + /// Error parsing incoming upstream messages + UpstreamIncoming(RolesLogicError), + /// Mutex poison lock error PoisonLock, - // Channel Receiver Error + /// Channel receiver error ChannelErrorReceiver(async_channel::RecvError), + /// Channel sender error + ChannelErrorSender, + /// Broadcast channel receiver error + BroadcastChannelErrorReceiver(broadcast::error::RecvError), + /// Tokio channel receiver error TokioChannelErrorRecv(tokio::sync::broadcast::error::RecvError), - // Channel Sender Errors - ChannelErrorSender(ChannelSendError<'a>), + /// Error converting SetDifficulty to Message SetDifficultyToMessage(SetDifficulty), - Infallible(std::convert::Infallible), - // used to handle SV2 protocol error messages from pool - #[allow(clippy::enum_variant_names)] - Sv2ProtocolError(Mining<'a>), - #[allow(clippy::enum_variant_names)] - TargetError(roles_logic_sv2::errors::Error), - Sv1MessageTooLong, + /// Received an unexpected message type + UnexpectedMessage(u8), + /// Job not found during share validation + JobNotFound, + /// Invalid merkle root during share validation + InvalidMerkleRoot, + /// Shutdown signal received + Shutdown, + /// Pending channel not found for the given request ID + PendingChannelNotFound(u32), + /// Represents a generic channel send failure, described by a string. + General(String), + /// Error bubbling up from translator-core library + TranslatorCore(stratum_translation::error::StratumTranslationError), } -impl fmt::Display for Error<'_> { +impl std::error::Error for TproxyError {} + +impl fmt::Display for TproxyError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use Error::*; + use TproxyError::*; match self { + General(e) => write!(f, "{e}"), BadCliArgs => write!(f, "Bad CLI arg input"), - BadSerdeJson(ref e) => write!(f, "Bad serde json: `{:?}`", e), - BadConfigDeserialize(ref e) => write!(f, "Bad `config` TOML deserialize: `{:?}`", e), - BinarySv2(ref e) => write!(f, "Binary SV2 error: `{:?}`", e), - CodecNoise(ref e) => write!(f, "Noise error: `{:?}", e), - FramingSv2(ref e) => write!(f, "Framing SV2 error: `{:?}`", e), - InvalidExtranonce(ref e) => write!(f, "Invalid Extranonce error: `{:?}", e), - Io(ref e) => write!(f, "I/O error: `{:?}", e), - ParseInt(ref e) => write!(f, "Bad convert from `String` to `int`: `{:?}`", e), - RolesSv2Logic(ref e) => write!(f, "Roles SV2 Logic Error: `{:?}`", e), - V1Protocol(ref e) => write!(f, "V1 Protocol Error: `{:?}`", e), - SubprotocolMining(ref e) => write!(f, "Subprotocol Mining Error: `{:?}`", e), - UpstreamIncoming(ref e) => write!(f, "Upstream parse incoming error: `{:?}`", e), + BadSerdeJson(ref e) => write!(f, "Bad serde json: `{e:?}`"), + BadConfigDeserialize(ref e) => write!(f, "Bad `config` TOML deserialize: `{e:?}`"), + BinarySv2(ref e) => write!(f, "Binary SV2 error: `{e:?}`"), + CodecNoise(ref e) => write!(f, "Noise error: `{e:?}"), + FramingSv2(ref e) => write!(f, "Framing SV2 error: `{e:?}`"), + Io(ref e) => write!(f, "I/O error: `{e:?}"), + ParseInt(ref e) => write!(f, "Bad convert from `String` to `int`: `{e:?}`"), + UpstreamIncoming(ref e) => write!(f, "Upstream parse incoming error: `{e:?}`"), PoisonLock => write!(f, "Poison Lock error"), - ChannelErrorReceiver(ref e) => write!(f, "Channel receive error: `{:?}`", e), - TokioChannelErrorRecv(ref e) => write!(f, "Channel receive error: `{:?}`", e), - ChannelErrorSender(ref e) => write!(f, "Channel send error: `{:?}`", e), - SetDifficultyToMessage(ref e) => { - write!(f, "Error converting SetDifficulty to Message: `{:?}`", e) + ChannelErrorReceiver(ref e) => write!(f, "Channel receive error: `{e:?}`"), + BroadcastChannelErrorReceiver(ref e) => { + write!(f, "Broadcast channel receive error: {e:?}") } - VecToSlice32(ref e) => write!(f, "Standard Error: `{:?}`", e), - Infallible(ref e) => write!(f, "Infallible Error:`{:?}`", e), - Sv2ProtocolError(ref e) => { - write!(f, "Received Sv2 Protocol Error from upstream: `{:?}`", e) + ChannelErrorSender => write!(f, "Sender error"), + TokioChannelErrorRecv(ref e) => write!(f, "Channel receive error: `{e:?}`"), + SetDifficultyToMessage(ref e) => { + write!(f, "Error converting SetDifficulty to Message: `{e:?}`") } - TargetError(ref e) => { - write!(f, "Impossible to get target from hashrate: `{:?}`", e) + UnexpectedMessage(message_type) => { + write!( + f, + "Received a message type that was not expected: {message_type}" + ) } - Sv1MessageTooLong => { - write!(f, "Received an sv1 message that is longer than max len") + JobNotFound => write!(f, "Job not found during share validation"), + InvalidMerkleRoot => write!(f, "Invalid merkle root during share validation"), + Shutdown => write!(f, "Shutdown signal"), + PendingChannelNotFound(request_id) => { + write!(f, "No pending channel found for request_id: {}", request_id) } + SV1Error => write!(f, "Sv1 error"), + TranslatorCore(ref e) => write!(f, "Translator core error: {e:?}"), + NetworkHelpersError(ref e) => write!(f, "Network helpers error: {e:?}"), + RolesSv2LogicError(ref e) => write!(f, "Roles logic error: {e:?}"), + ParserError(ref e) => write!(f, "Roles logic parser error: {e:?}"), } } } -impl From for Error<'_> { +impl From for TproxyError { fn from(e: binary_sv2::Error) -> Self { - Error::BinarySv2(e) + TproxyError::BinarySv2(e) } } -impl From for Error<'_> { +impl From for TproxyError { fn from(e: codec_sv2::noise_sv2::Error) -> Self { - Error::CodecNoise(e) + TproxyError::CodecNoise(e) } } -impl From for Error<'_> { +impl From for TproxyError { fn from(e: framing_sv2::Error) -> Self { - Error::FramingSv2(e) + TproxyError::FramingSv2(e) } } -impl From for Error<'_> { +impl From for TproxyError { fn from(e: std::io::Error) -> Self { - Error::Io(e) + TproxyError::Io(e) } } -impl From for Error<'_> { +impl From for TproxyError { fn from(e: std::num::ParseIntError) -> Self { - Error::ParseInt(e) - } -} - -impl From for Error<'_> { - fn from(e: roles_logic_sv2::errors::Error) -> Self { - Error::RolesSv2Logic(e) + TproxyError::ParseInt(e) } } -impl From for Error<'_> { +impl From for TproxyError { fn from(e: serde_json::Error) -> Self { - Error::BadSerdeJson(e) + TproxyError::BadSerdeJson(e) } } -impl From for Error<'_> { +impl From for TproxyError { fn from(e: ConfigError) -> Self { - Error::BadConfigDeserialize(e) + TproxyError::BadConfigDeserialize(e) } } -impl<'a> From> for Error<'a> { - fn from(e: v1::error::Error<'a>) -> Self { - Error::V1Protocol(e) - } -} - -impl From for Error<'_> { +impl From for TproxyError { fn from(e: async_channel::RecvError) -> Self { - Error::ChannelErrorReceiver(e) + TproxyError::ChannelErrorReceiver(e) } } -impl From for Error<'_> { +impl From for TproxyError { fn from(e: tokio::sync::broadcast::error::RecvError) -> Self { - Error::TokioChannelErrorRecv(e) + TproxyError::TokioChannelErrorRecv(e) } } //*** LOCK ERRORS *** -impl From> for Error<'_> { +impl From> for TproxyError { fn from(_e: PoisonError) -> Self { - Error::PoisonLock - } -} - -// *** CHANNEL SENDER ERRORS *** -impl<'a> From>> - for Error<'a> -{ - fn from( - e: async_channel::SendError>, - ) -> Self { - Error::ChannelErrorSender(ChannelSendError::SubmitSharesExtended(e)) - } -} - -impl<'a> From>> - for Error<'a> -{ - fn from(e: async_channel::SendError>) -> Self { - Error::ChannelErrorSender(ChannelSendError::SetNewPrevHash(e)) - } -} - -impl<'a> From>> for Error<'a> { - fn from(e: tokio::sync::broadcast::error::SendError>) -> Self { - Error::ChannelErrorSender(ChannelSendError::Notify(e)) - } -} - -impl From> for Error<'_> { - fn from(e: async_channel::SendError) -> Self { - Error::ChannelErrorSender(ChannelSendError::V1Message(e)) - } -} - -impl From> for Error<'_> { - fn from(e: async_channel::SendError<(ExtendedExtranonce, u32)>) -> Self { - Error::ChannelErrorSender(ChannelSendError::Extranonce(e)) + TproxyError::PoisonLock } } -impl<'a> From>> for Error<'a> { - fn from(e: async_channel::SendError>) -> Self { - Error::ChannelErrorSender(ChannelSendError::NewExtendedMiningJob(e)) - } -} - -impl<'a> From>> for Error<'a> { - fn from(e: async_channel::SendError>) -> Self { - Error::ChannelErrorSender(ChannelSendError::SetCustomMiningJob(e)) - } -} - -impl<'a> - From< - async_channel::SendError<( - roles_logic_sv2::template_distribution_sv2::SetNewPrevHash<'a>, - Vec, - )>, - > for Error<'a> -{ - fn from( - e: async_channel::SendError<( - roles_logic_sv2::template_distribution_sv2::SetNewPrevHash<'a>, - Vec, - )>, - ) -> Self { - Error::ChannelErrorSender(ChannelSendError::NewTemplate(e)) - } -} - -impl From> for Error<'_> { - fn from(e: Vec) -> Self { - Error::VecToSlice32(e) +impl From for TproxyError { + fn from(e: SetDifficulty) -> Self { + TproxyError::SetDifficultyToMessage(e) } } -impl From for Error<'_> { - fn from(e: SetDifficulty) -> Self { - Error::SetDifficultyToMessage(e) +impl<'a> From> for TproxyError { + fn from(_: v1::error::Error<'a>) -> Self { + TproxyError::SV1Error } } -impl From for Error<'_> { - fn from(e: std::convert::Infallible) -> Self { - Error::Infallible(e) +impl From for TproxyError { + fn from(value: network_helpers_sv2::Error) -> Self { + TproxyError::NetworkHelpersError(value) } } -impl<'a> From> for Error<'a> { - fn from(e: Mining<'a>) -> Self { - Error::Sv2ProtocolError(e) +impl From for TproxyError { + fn from(e: stratum_translation::error::StratumTranslationError) -> Self { + TproxyError::TranslatorCore(e) } } -impl From, codec_sv2::buffer_sv2::Slice>>> - for Error<'_> -{ - fn from( - value: async_channel::SendError, codec_sv2::buffer_sv2::Slice>>, - ) -> Self { - Error::ChannelErrorSender(ChannelSendError::General(value.to_string())) +impl HandlerErrorType for TproxyError { + fn parse_error(error: RolesParserError) -> Self { + TproxyError::ParserError(error) } -} -impl<'a> From for Error<'a> { - fn from(value: VardiffError) -> Self { - Self::RolesSv2Logic(value.into()) + fn unexpected_message(message_type: u8) -> Self { + TproxyError::UnexpectedMessage(message_type) } } diff --git a/roles/translator/src/lib/mod.rs b/roles/translator/src/lib/mod.rs index 26eca7dc25..262cb87c08 100644 --- a/roles/translator/src/lib/mod.rs +++ b/roles/translator/src/lib/mod.rs @@ -10,43 +10,36 @@ //! provides the `start` method as the main entry point for running the translator service. //! It relies on several sub-modules (`config`, `downstream_sv1`, `upstream_sv2`, `proxy`, `status`, //! etc.) for specialized functionalities. -use async_channel::{bounded, unbounded}; -use futures::FutureExt; -use rand::Rng; -pub use roles_logic_sv2::utils::Mutex; -use status::Status; -use std::{ - net::{IpAddr, SocketAddr}, - str::FromStr, - sync::Arc, -}; - -use tokio::{ - select, - sync::{broadcast, Notify}, - task::{self, AbortHandle}, -}; +#![allow(clippy::module_inception)] +use async_channel::unbounded; +use std::{net::SocketAddr, sync::Arc}; +use tokio::sync::mpsc; use tracing::{debug, error, info, warn}; + pub use v1::server_to_client; use config::TranslatorConfig; -use crate::status::State; +use crate::{ + status::{State, Status}, + sv1::sv1_server::sv1_server::Sv1Server, + sv2::{channel_manager::ChannelMode, ChannelManager, Upstream}, + task_manager::TaskManager, + utils::ShutdownMessage, +}; pub mod config; -pub mod downstream_sv1; pub mod error; -pub mod proxy; pub mod status; -pub mod upstream_sv2; +pub mod sv1; +pub mod sv2; +mod task_manager; pub mod utils; /// The main struct that manages the SV1/SV2 translator. #[derive(Clone, Debug)] pub struct TranslatorSv2 { config: TranslatorConfig, - reconnect_wait_time: u64, - shutdown: Arc, } impl TranslatorSv2 { @@ -55,13 +48,7 @@ impl TranslatorSv2 { /// Initializes the translator with the given configuration and sets up /// the reconnect wait time. pub fn new(config: TranslatorConfig) -> Self { - let mut rng = rand::thread_rng(); - let wait_time = rng.gen_range(0..=3000); - Self { - config, - reconnect_wait_time: wait_time, - shutdown: Arc::new(Notify::new()), - } + Self { config } } /// Starts the translator. @@ -69,319 +56,200 @@ impl TranslatorSv2 { /// This method starts the main event loop, which handles connections, /// protocol translation, job management, and status reporting. pub async fn start(self) { - // Status channel for components to signal errors or state changes. - let (tx_status, rx_status) = unbounded(); - - // Shared mutable state for the current mining target. - let target = Arc::new(Mutex::new(vec![0; 32])); + info!("Starting Translator Proxy..."); + + let (notify_shutdown, _) = tokio::sync::broadcast::channel::(1); + let (shutdown_complete_tx, mut shutdown_complete_rx) = mpsc::channel::<()>(1); + let task_manager = Arc::new(TaskManager::new()); + + let (status_sender, status_receiver) = async_channel::unbounded::(); + + let (channel_manager_to_upstream_sender, channel_manager_to_upstream_receiver) = + unbounded(); + let (upstream_to_channel_manager_sender, upstream_to_channel_manager_receiver) = + unbounded(); + let (channel_manager_to_sv1_server_sender, channel_manager_to_sv1_server_receiver) = + unbounded(); + let (sv1_server_to_channel_manager_sender, sv1_server_to_channel_manager_receiver) = + unbounded(); + + debug!("Channels initialized."); + + let upstream_addresses = self + .config + .upstreams + .iter() + .map(|upstream| { + let upstream_addr = + SocketAddr::new(upstream.address.parse().unwrap(), upstream.port); + (upstream_addr, upstream.authority_pubkey) + }) + .collect::>(); + + let upstream = match Upstream::new( + &upstream_addresses, + upstream_to_channel_manager_sender.clone(), + channel_manager_to_upstream_receiver.clone(), + notify_shutdown.clone(), + shutdown_complete_tx.clone(), + ) + .await + { + Ok(upstream) => { + debug!("Upstream initialized successfully."); + upstream + } + Err(e) => { + error!("Failed to initialize upstream connection: {e:?}"); + return; + } + }; - // Broadcast channel to send SV1 `mining.notify` messages from the Bridge - // to all connected Downstream (SV1) clients. - let (tx_sv1_notify, _rx_sv1_notify): ( - broadcast::Sender, - broadcast::Receiver, - ) = broadcast::channel(10); + let channel_manager = Arc::new(ChannelManager::new( + channel_manager_to_upstream_sender, + upstream_to_channel_manager_receiver, + channel_manager_to_sv1_server_sender.clone(), + sv1_server_to_channel_manager_receiver, + if self.config.aggregate_channels { + ChannelMode::Aggregated + } else { + ChannelMode::NonAggregated + }, + )); - // FIXME: Remove this task collector mechanism. - // Collector for holding handles to spawned tasks for potential abortion. - let task_collector: Arc>> = - Arc::new(Mutex::new(Vec::new())); + let downstream_addr = SocketAddr::new( + self.config.downstream_address.parse().unwrap(), + self.config.downstream_port, + ); - // Delegate initial setup and connection logic to internal_start. - Self::internal_start( + let sv1_server = Arc::new(Sv1Server::new( + downstream_addr, + channel_manager_to_sv1_server_receiver, + sv1_server_to_channel_manager_sender, self.config.clone(), - tx_sv1_notify.clone(), - target.clone(), - tx_status.clone(), - task_collector.clone(), + )); + + ChannelManager::run_channel_manager_tasks( + channel_manager.clone(), + notify_shutdown.clone(), + shutdown_complete_tx.clone(), + status_sender.clone(), + task_manager.clone(), ) .await; - debug!("Starting up signal listener"); - let task_collector_ = task_collector.clone(); - - debug!("Starting up status listener"); - let wait_time = self.reconnect_wait_time; - // Check all tasks if is_finished() is true, if so exit - // Spawn a task to listen for Ctrl+C signal. - tokio::spawn({ - let shutdown_signal = self.shutdown.clone(); - async move { - if tokio::signal::ctrl_c().await.is_ok() { - info!("Interrupt received"); - // Notify the main loop to begin shutdown. - shutdown_signal.notify_one(); - } - } - }); - - // Main status loop. - loop { - select! { - // Listen for status updates from components. - task_status = rx_status.recv().fuse() => { - if let Ok(task_status_) = task_status { - match task_status_.state { - // If any critical component shuts down due to error, shut down the whole translator. - // Logic needs to be improved, maybe respawn rather than a total shutdown. - State::DownstreamShutdown(err) | State::BridgeShutdown(err) | State::UpstreamShutdown(err) => { - error!("SHUTDOWN from: {}", err); - self.shutdown(); - } - // If the upstream signals a need to reconnect. - State::UpstreamTryReconnect(err) => { - error!("Trying to reconnect the Upstream because of: {}", err); - let task_collector1 = task_collector_.clone(); - let tx_sv1_notify1 = tx_sv1_notify.clone(); - let target = target.clone(); - let tx_status = tx_status.clone(); - let proxy_config = self.config.clone(); - // Spawn a new task to handle the reconnection process. - tokio::spawn (async move { - // Wait for the randomized delay to avoid thundering herd issues. - tokio::time::sleep(std::time::Duration::from_millis(wait_time)).await; - - // Abort all existing tasks before restarting. - let task_collector_aborting = task_collector1.clone(); - kill_tasks(task_collector_aborting.clone()); + if let Err(e) = upstream + .start( + notify_shutdown.clone(), + shutdown_complete_tx.clone(), + status_sender.clone(), + task_manager.clone(), + ) + .await + { + error!("Failed to start upstream listener: {e:?}"); + return; + } - warn!("Trying reconnecting to upstream"); - // Restart the internal components. - Self::internal_start( - proxy_config, - tx_sv1_notify1, - target.clone(), - tx_status.clone(), - task_collector1, - ) - .await; - }); - } - // Log healthy status messages. - State::Healthy(msg) => { - info!("HEALTHY message: {}", msg); + let notify_shutdown_clone = notify_shutdown.clone(); + let shutdown_complete_tx_clone = shutdown_complete_tx.clone(); + let status_sender_clone = status_sender.clone(); + let task_manager_clone = task_manager.clone(); + task_manager.spawn(async move { + loop { + tokio::select! { + _ = tokio::signal::ctrl_c() => { + info!("Ctrl+C received — initiating graceful shutdown..."); + let _ = notify_shutdown_clone.send(ShutdownMessage::ShutdownAll); + break; + } + message = status_receiver.recv() => { + if let Ok(status) = message { + match status.state { + State::DownstreamShutdown{downstream_id,..} => { + warn!("Downstream {downstream_id:?} disconnected — notifying SV1 server."); + let _ = notify_shutdown_clone.send(ShutdownMessage::DownstreamShutdown(downstream_id)); + } + State::Sv1ServerShutdown(_) => { + warn!("SV1 Server shutdown requested — initiating full shutdown."); + let _ = notify_shutdown_clone.send(ShutdownMessage::ShutdownAll); + break; + } + State::ChannelManagerShutdown(_) => { + warn!("Channel Manager shutdown requested — initiating full shutdown."); + let _ = notify_shutdown_clone.send(ShutdownMessage::ShutdownAll); + break; + } + State::UpstreamShutdown(msg) => { + warn!("Upstream connection dropped: {msg:?} — attempting reconnection..."); + + match Upstream::new( + &upstream_addresses, + upstream_to_channel_manager_sender.clone(), + channel_manager_to_upstream_receiver.clone(), + notify_shutdown_clone.clone(), + shutdown_complete_tx_clone.clone(), + ).await { + Ok(upstream) => { + if let Err(e) = upstream + .start( + notify_shutdown_clone.clone(), + shutdown_complete_tx_clone.clone(), + status_sender_clone.clone(), + task_manager_clone.clone() + ) + .await + { + error!("Restarted upstream failed to start: {e:?}"); + let _ = notify_shutdown_clone.send(ShutdownMessage::ShutdownAll); + break; + } else { + info!("Upstream restarted successfully."); + // Reset channel manager state and shutdown downstreams in one message + let _ = notify_shutdown_clone.send(ShutdownMessage::UpstreamReconnectedResetAndShutdownDownstreams); + } + } + Err(e) => { + error!("Failed to reinitialize upstream after disconnect: {e:?}"); + let _ = notify_shutdown_clone.send(ShutdownMessage::ShutdownAll); + break; + } + } + } } } - } else { - info!("Channel closed"); - kill_tasks(task_collector.clone()); - break; // Channel closed } } - // Listen for the shutdown signal (from Ctrl+C or explicit call). - _ = self.shutdown.notified() => { - info!("Shutting down gracefully..."); - kill_tasks(task_collector.clone()); - break; - } } - } - } - - /// Internal helper function to initialize and start the core components. - /// - /// Sets up communication channels between the Bridge, Upstream, and Downstream. - /// Creates, connects, and starts the Upstream (SV2) handler. - /// Waits for initial data (extranonce, target) from the Upstream. - /// Creates and starts the Bridge (protocol translation logic). - /// Starts the Downstream (SV1) listener to accept miner connections. - /// Collects task handles for graceful shutdown management. - async fn internal_start( - proxy_config: TranslatorConfig, - tx_sv1_notify: broadcast::Sender>, - target: Arc>>, - tx_status: async_channel::Sender>, - task_collector: Arc>>, - ) { - // Channel: Bridge -> Upstream (SV2 SubmitSharesExtended) - let (tx_sv2_submit_shares_ext, rx_sv2_submit_shares_ext) = bounded(10); - - // Channel: Downstream -> Bridge (SV1 Messages) - let (tx_sv1_bridge, rx_sv1_downstream) = unbounded(); - - // Channel: Upstream -> Bridge (SV2 NewExtendedMiningJob) - let (tx_sv2_new_ext_mining_job, rx_sv2_new_ext_mining_job) = bounded(10); - - // Channel: Upstream -> internal_start -> Bridge (Initial Extranonce) - let (tx_sv2_extranonce, rx_sv2_extranonce) = bounded(1); - - // Channel: Upstream -> Bridge (SV2 SetNewPrevHash) - let (tx_sv2_set_new_prev_hash, rx_sv2_set_new_prev_hash) = bounded(10); - - // Prepare upstream connection address. - let upstream_addr = SocketAddr::new( - IpAddr::from_str(&proxy_config.upstream_address) - .expect("Failed to parse upstream address!"), - proxy_config.upstream_port, - ); + }); - // Shared difficulty configuration - let diff_config = Arc::new(Mutex::new(proxy_config.upstream_difficulty_config.clone())); - let task_collector_upstream = task_collector.clone(); - // Instantiate the Upstream (SV2) component. - let upstream = match upstream_sv2::Upstream::new( - upstream_addr, - proxy_config.upstream_authority_pubkey, - rx_sv2_submit_shares_ext, // Receives shares from Bridge - tx_sv2_set_new_prev_hash, // Sends prev hash updates to Bridge - tx_sv2_new_ext_mining_job, // Sends new jobs to Bridge - proxy_config.min_extranonce2_size, - tx_sv2_extranonce, // Sends initial extranonce - status::Sender::Upstream(tx_status.clone()), // Sends status updates - target.clone(), // Shares target state - diff_config.clone(), // Shares difficulty config - task_collector_upstream, + if let Err(e) = Sv1Server::start( + sv1_server, + notify_shutdown.clone(), + shutdown_complete_tx.clone(), + status_sender.clone(), + task_manager.clone(), ) .await { - Ok(upstream) => upstream, - Err(e) => { - // FIXME: Send error to status main loop, and then exit. - error!("Failed to create upstream: {}", e); - return; - } - }; - let task_collector_init_task = task_collector.clone(); - - // Spawn the core initialization logic in a separate task. - // This allows the main `start` loop to remain responsive to shutdown signals - // even during potentially long-running connection attempts. - let task = task::spawn(async move { - // Connect to the SV2 Upstream role - match upstream_sv2::Upstream::connect( - upstream.clone(), - proxy_config.min_supported_version, - proxy_config.max_supported_version, - ) - .await - { - Ok(_) => info!("Connected to Upstream!"), - Err(e) => { - // FIXME: Send error to status main loop, and then exit. - error!("Failed to connect to Upstream EXITING! : {}", e); - return; - } - } - - // Start the task to parse incoming messages from the Upstream. - if let Err(e) = upstream_sv2::Upstream::parse_incoming(upstream.clone()) { - error!("failed to create sv2 parser: {}", e); - return; - } + error!("SV1 server startup failed: {e:?}"); + notify_shutdown.send(ShutdownMessage::ShutdownAll).unwrap(); + } - debug!("Finished starting upstream listener"); - // Start the task handler to process share submissions received from the Bridge. - if let Err(e) = upstream_sv2::Upstream::handle_submit(upstream.clone()) { - error!("Failed to create submit handler: {}", e); - return; + drop(shutdown_complete_tx); + info!("Waiting for shutdown completion signals from subsystems..."); + let shutdown_timeout = tokio::time::Duration::from_secs(5); + tokio::select! { + _ = shutdown_complete_rx.recv() => { + info!("All subsystems reported shutdown complete."); } - - // Wait to receive the initial extranonce information from the Upstream. - // This is needed before the Bridge can be fully initialized. - let (extended_extranonce, up_id) = rx_sv2_extranonce.recv().await.unwrap(); - loop { - let target: [u8; 32] = target.safe_lock(|t| t.clone()).unwrap().try_into().unwrap(); - if target != [0; 32] { - break; - }; - tokio::time::sleep(std::time::Duration::from_millis(100)).await; + _ = tokio::time::sleep(shutdown_timeout) => { + warn!("Graceful shutdown timed out after {shutdown_timeout:?} — forcing shutdown."); + task_manager.abort_all().await; } - - let task_collector_bridge = task_collector_init_task.clone(); - // Instantiate the Bridge component. - let b = proxy::Bridge::new( - rx_sv1_downstream, - tx_sv2_submit_shares_ext, - rx_sv2_set_new_prev_hash, - rx_sv2_new_ext_mining_job, - tx_sv1_notify.clone(), - status::Sender::Bridge(tx_status.clone()), - extended_extranonce, - target, - up_id, - task_collector_bridge, - ); - // Start the Bridge's main processing loop. - proxy::Bridge::start(b.clone()); - - // Prepare downstream listening address. - let downstream_addr = SocketAddr::new( - IpAddr::from_str(&proxy_config.downstream_address).unwrap(), - proxy_config.downstream_port, - ); - - let task_collector_downstream = task_collector_init_task.clone(); - // Start accepting connections from Downstream (SV1) miners. - downstream_sv1::Downstream::accept_connections( - downstream_addr, - tx_sv1_bridge, - tx_sv1_notify, - status::Sender::DownstreamListener(tx_status.clone()), - b, - proxy_config.downstream_difficulty_config, - diff_config, - task_collector_downstream, - ); - }); // End of init task - let _ = - task_collector.safe_lock(|t| t.push((task.abort_handle(), "init task".to_string()))); - } - - /// Closes Translator role and any open connection associated with it. - /// - /// Note that this method will result in a full exit of the running - /// Translator and any open connection most be re-initiated upon new - /// start. - pub fn shutdown(&self) { - self.shutdown.notify_one(); - } -} - -// Helper function to iterate through the collected task handles and abort them -fn kill_tasks(task_collector: Arc>>) { - let _ = task_collector.safe_lock(|t| { - while let Some(handle) = t.pop() { - handle.0.abort(); - warn!("Killed task: {:?}", handle.1); } - }); -} - -#[cfg(test)] -mod tests { - use super::TranslatorSv2; - use ext_config::{Config, File, FileFormat}; - - use crate::*; - - #[tokio::test] - async fn test_shutdown() { - let config_path = "config-examples/tproxy-config-hosted-pool-example.toml"; - let config: TranslatorConfig = match Config::builder() - .add_source(File::new(config_path, FileFormat::Toml)) - .build() - { - Ok(settings) => match settings.try_deserialize::() { - Ok(c) => c, - Err(e) => { - dbg!(&e); - return; - } - }, - Err(e) => { - dbg!(&e); - return; - } - }; - let translator = TranslatorSv2::new(config.clone()); - let cloned = translator.clone(); - tokio::spawn(async move { - cloned.start().await; - }); - translator.shutdown(); - let ip = config.downstream_address.clone(); - let port = config.downstream_port; - let translator_addr = format!("{}:{}", ip, port); - assert!(std::net::TcpListener::bind(translator_addr).is_ok()); + info!("Joining remaining tasks..."); + task_manager.join_all().await; + info!("TranslatorSv2 shutdown complete."); } } diff --git a/roles/translator/src/lib/proxy/bridge.rs b/roles/translator/src/lib/proxy/bridge.rs deleted file mode 100644 index 5790d31d5a..0000000000 --- a/roles/translator/src/lib/proxy/bridge.rs +++ /dev/null @@ -1,738 +0,0 @@ -//! ## Proxy Bridge Module -//! -//! This module defines the [`Bridge`] structure, which acts as the central component -//! responsible for translating messages and coordinating communication between -//! the upstream SV2 role and the downstream SV1 mining clients. -//! -//! The Bridge manages message queues, maintains the state required for translation -//! (such as job IDs, previous hashes, and mining jobs), handles share submissions -//! from downstream, and forwards translated jobs received from upstream to downstream miners. -//! -//! This module handles: -//! - Receiving SV1 `mining.submit` messages from [`Downstream`] connections. -//! - Translating SV1 submits into SV2 `SubmitSharesExtended`. -//! - Receiving SV2 `SetNewPrevHash` and `NewExtendedMiningJob` from the upstream. -//! - Translating SV2 job messages into SV1 `mining.notify` messages. -//! - Sending translated SV2 submits to the upstream. -//! - Broadcasting translated SV1 notifications to connected downstream miners. -//! - Managing channel state and difficulty related to job translation. -//! - Handling new downstream SV1 connections. -use super::super::{ - downstream_sv1::{DownstreamMessages, SetDownstreamTarget, SubmitShareWithChannelId}, - error::{ - Error::{self, PoisonLock}, - ProxyResult, - }, - status, -}; -use async_channel::{Receiver, Sender}; -use error_handling::handle_result; -use roles_logic_sv2::{ - channel_logic::channel_factory::{ - ExtendedChannelKind, OnNewShare, ProxyExtendedChannelFactory, Share, - }, - mining_sv2::{ - ExtendedExtranonce, NewExtendedMiningJob, SetNewPrevHash, SubmitSharesExtended, Target, - }, - parsers::Mining, - utils::{GroupId, Mutex}, - Error as RolesLogicError, -}; -use std::sync::Arc; -use tokio::{sync::broadcast, task::AbortHandle}; -use tracing::{debug, error, info, warn}; -use v1::{client_to_server::Submit, server_to_client, utils::HexU32Be}; - -/// Bridge between the SV2 `Upstream` and SV1 `Downstream` responsible for the following messaging -/// translation: -/// 1. SV1 `mining.submit` -> SV2 `SubmitSharesExtended` -/// 2. SV2 `SetNewPrevHash` + `NewExtendedMiningJob` -> SV1 `mining.notify` -#[derive(Debug)] -pub struct Bridge { - /// Receives a SV1 `mining.submit` message from the Downstream role. - rx_sv1_downstream: Receiver, - /// Sends SV2 `SubmitSharesExtended` messages translated from SV1 `mining.submit` messages to - /// the `Upstream`. - tx_sv2_submit_shares_ext: Sender>, - /// Receives a SV2 `SetNewPrevHash` message from the `Upstream` to be translated (along with a - /// SV2 `NewExtendedMiningJob` message) to a SV1 `mining.submit` for the `Downstream`. - rx_sv2_set_new_prev_hash: Receiver>, - /// Receives a SV2 `NewExtendedMiningJob` message from the `Upstream` to be translated (along - /// with a SV2 `SetNewPrevHash` message) to a SV1 `mining.submit` to be sent to the - /// `Downstream`. - rx_sv2_new_ext_mining_job: Receiver>, - /// Sends SV1 `mining.notify` message (translated from the SV2 `SetNewPrevHash` and - /// `NewExtendedMiningJob` messages stored in the `NextMiningNotify`) to the `Downstream`. - tx_sv1_notify: broadcast::Sender>, - /// Allows the bridge the ability to communicate back to the main thread any status updates - /// that would interest the main thread for error handling - tx_status: status::Sender, - /// Stores the most recent SV1 `mining.notify` values to be sent to the `Downstream` upon - /// receiving a new SV2 `SetNewPrevHash` and `NewExtendedMiningJob` messages **before** any - /// Downstream role connects to the proxy. - /// - /// Once the proxy establishes a connection with the SV2 Upstream role, it immediately receives - /// a SV2 `SetNewPrevHash` and `NewExtendedMiningJob` message. This happens before the - /// connection to the Downstream role(s) occur. The `last_notify` member fields allows these - /// first notify values to be relayed to the `Downstream` once a Downstream role connects. Once - /// a Downstream role connects and receives the first notify values, this member field is no - /// longer used. - last_notify: Option>, - pub(self) channel_factory: ProxyExtendedChannelFactory, - /// Stores `NewExtendedMiningJob` messages received from the upstream with the `is_future` flag - /// set. These jobs are buffered until a corresponding `SetNewPrevHash` message is - /// received. - future_jobs: Vec>, - /// Stores the last received SV2 `SetNewPrevHash` message. Used in conjunction with - /// `future_jobs` to construct `mining.notify` messages. - last_p_hash: Option>, - /// The mining target currently in use by the downstream miners connected to this bridge. - /// This target is derived from the upstream's requirements but may be adjusted locally. - target: Arc>>, - /// The job ID of the last sent `mining.notify` message. - last_job_id: u32, - task_collector: Arc>>, -} - -impl Bridge { - #[allow(clippy::too_many_arguments)] - /// Instantiates a new `Bridge` with the provided communication channels and initial - /// configurations. - /// - /// Sets up the core communication pathways between upstream and downstream handlers - /// and initializes the internal state, including the channel factory. - pub fn new( - rx_sv1_downstream: Receiver, - tx_sv2_submit_shares_ext: Sender>, - rx_sv2_set_new_prev_hash: Receiver>, - rx_sv2_new_ext_mining_job: Receiver>, - tx_sv1_notify: broadcast::Sender>, - tx_status: status::Sender, - extranonces: ExtendedExtranonce, - target: Arc>>, - up_id: u32, - task_collector: Arc>>, - ) -> Arc> { - let ids = Arc::new(Mutex::new(GroupId::new())); - let share_per_min = 1.0; - let upstream_target: [u8; 32] = - target.safe_lock(|t| t.clone()).unwrap().try_into().unwrap(); - let upstream_target: Target = upstream_target.into(); - Arc::new(Mutex::new(Self { - rx_sv1_downstream, - tx_sv2_submit_shares_ext, - rx_sv2_set_new_prev_hash, - rx_sv2_new_ext_mining_job, - tx_sv1_notify, - tx_status, - last_notify: None, - channel_factory: ProxyExtendedChannelFactory::new( - ids, - extranonces, - None, - share_per_min, - ExtendedChannelKind::Proxy { upstream_target }, - None, - up_id, - ), - future_jobs: vec![], - last_p_hash: None, - target, - last_job_id: 0, - task_collector, - })) - } - - /// Handles the event of a new SV1 downstream client connecting. - /// - /// Creates a new extended channel using the internal `channel_factory` for the - /// new connection. It assigns a unique channel ID, determines the initial - /// extranonce and target for the miner, and provides the last known - /// `mining.notify` message to immediately send to the new client. - #[allow(clippy::result_large_err)] - pub fn on_new_sv1_connection( - &mut self, - hash_rate: f32, - ) -> ProxyResult<'static, OpenSv1Downstream> { - match self.channel_factory.new_extended_channel(0, hash_rate, 0) { - Ok(messages) => { - for message in messages { - match message { - Mining::OpenExtendedMiningChannelSuccess(success) => { - let extranonce = success.extranonce_prefix.to_vec(); - let extranonce2_len = success.extranonce_size; - self.target.safe_lock(|t| *t = success.target.to_vec())?; - return Ok(OpenSv1Downstream { - channel_id: success.channel_id, - last_notify: self.last_notify.clone(), - extranonce, - target: self.target.clone(), - extranonce2_len, - }); - } - Mining::OpenMiningChannelError(_) => todo!(), - Mining::SetNewPrevHash(_) => (), - Mining::NewExtendedMiningJob(_) => (), - _ => unreachable!(), - } - } - } - Err(_) => { - return Err(Error::SubprotocolMining( - "Bridge: failed to open new extended channel".to_string(), - )) - } - }; - Err(Error::SubprotocolMining( - "Bridge: Invalid mining message when opening downstream connection".to_string(), - )) - } - - /// Starts the tasks responsible for receiving and processing - /// messages from both upstream SV2 and downstream SV1 connections. - /// - /// This function spawns three main tasks: - /// 1. `handle_new_prev_hash`: Listens for SV2 `SetNewPrevHash` messages. - /// 2. `handle_new_extended_mining_job`: Listens for SV2 `NewExtendedMiningJob` messages. - /// 3. `handle_downstream_messages`: Listens for `DownstreamMessages` (e.g., submit shares) from - /// downstream clients. - pub fn start(self_: Arc>) { - Self::handle_new_prev_hash(self_.clone()); - Self::handle_new_extended_mining_job(self_.clone()); - Self::handle_downstream_messages(self_); - } - - /// Task handler that receives `DownstreamMessages` and dispatches them. - /// - /// This loop continuously receives messages from the `rx_sv1_downstream` channel. - /// It matches on the `DownstreamMessages` variant and calls the appropriate - /// handler function (`handle_submit_shares` or `handle_update_downstream_target`). - fn handle_downstream_messages(self_: Arc>) { - let task_collector_handle_downstream = - self_.safe_lock(|b| b.task_collector.clone()).unwrap(); - let (rx_sv1_downstream, tx_status) = self_ - .safe_lock(|s| (s.rx_sv1_downstream.clone(), s.tx_status.clone())) - .unwrap(); - let handle_downstream = tokio::task::spawn(async move { - loop { - let msg = handle_result!(tx_status, rx_sv1_downstream.clone().recv().await); - - match msg { - DownstreamMessages::SubmitShares(share) => { - handle_result!( - tx_status, - Self::handle_submit_shares(self_.clone(), share).await - ); - } - DownstreamMessages::SetDownstreamTarget(new_target) => { - handle_result!( - tx_status, - Self::handle_update_downstream_target(self_.clone(), new_target) - ); - } - }; - } - }); - let _ = task_collector_handle_downstream.safe_lock(|a| { - a.push(( - handle_downstream.abort_handle(), - "handle_downstream_message".to_string(), - )) - }); - } - - /// Receives a `SetDownstreamTarget` message and updates the downstream target for a specific - /// channel. - /// - /// This function is called when the downstream logic determines that a miner's - /// target needs to be updated (e.g., due to difficulty adjustment). It updates - /// the target within the internal `channel_factory` for the specified channel ID. - #[allow(clippy::result_large_err)] - fn handle_update_downstream_target( - self_: Arc>, - new_target: SetDownstreamTarget, - ) -> ProxyResult<'static, ()> { - self_.safe_lock(|b| { - b.channel_factory - .update_target_for_channel(new_target.channel_id, new_target.new_target); - })?; - Ok(()) - } - /// Receives a `SubmitShareWithChannelId` message from a downstream miner, - /// validates the share, and sends it upstream if it meets the upstream target. - async fn handle_submit_shares( - self_: Arc>, - share: SubmitShareWithChannelId, - ) -> ProxyResult<'static, ()> { - let (tx_sv2_submit_shares_ext, target_mutex, tx_status) = self_.safe_lock(|s| { - ( - s.tx_sv2_submit_shares_ext.clone(), - s.target.clone(), - s.tx_status.clone(), - ) - })?; - let upstream_target: [u8; 32] = target_mutex.safe_lock(|t| t.clone())?.try_into()?; - let mut upstream_target: Target = upstream_target.into(); - self_.safe_lock(|s| s.channel_factory.set_target(&mut upstream_target))?; - - let sv2_submit = self_.safe_lock(|s| { - s.translate_submit(share.channel_id, share.share, share.version_rolling_mask) - })??; - let res = self_ - .safe_lock(|s| s.channel_factory.on_submit_shares_extended(sv2_submit)) - .map_err(|_| PoisonLock); - - match res { - Ok(Ok(OnNewShare::SendErrorDownstream(e))) => { - warn!( - "Submit share error {:?}", - std::str::from_utf8(&e.error_code.to_vec()[..]) - ); - } - Ok(Ok(OnNewShare::SendSubmitShareUpstream((share, _)))) => { - info!("SHARE MEETS UPSTREAM TARGET"); - match share { - Share::Extended(share) => { - tx_sv2_submit_shares_ext.send(share).await?; - } - // We are in an extended channel shares are extended - Share::Standard(_) => unreachable!(), - } - } - // We are in an extended channel this variant is group channle only - Ok(Ok(OnNewShare::RelaySubmitShareUpstream)) => unreachable!(), - Ok(Ok(OnNewShare::ShareMeetDownstreamTarget)) => { - debug!("SHARE MEETS DOWNSTREAM TARGET"); - } - // Proxy do not have JD capabilities - Ok(Ok(OnNewShare::ShareMeetBitcoinTarget(..))) => unreachable!(), - Ok(Err(e)) => error!("Error: {:?}", e), - Err(e) => { - let _ = tx_status - .send(status::Status { - state: status::State::BridgeShutdown(e), - }) - .await; - } - } - Ok(()) - } - - /// Translates a SV1 `mining.submit` message into an SV2 `SubmitSharesExtended` message. - /// - /// This function performs the necessary transformations to convert the data - /// format used by SV1 submissions (`job_id`, `nonce`, `time`, `extra_nonce2`, - /// `version_bits`) into the SV2 `SubmitSharesExtended` structure, - /// taking into account version rolling if a mask is provided. - #[allow(clippy::result_large_err)] - fn translate_submit( - &self, - channel_id: u32, - sv1_submit: Submit, - version_rolling_mask: Option, - ) -> ProxyResult<'static, SubmitSharesExtended<'static>> { - let last_version = self - .channel_factory - .last_valid_job_version() - .ok_or(Error::RolesSv2Logic(RolesLogicError::NoValidJob))?; - let version = match (sv1_submit.version_bits, version_rolling_mask) { - // regarding version masking see https://github.com/slushpool/stratumprotocol/blob/master/stratum-extensions.mediawiki#changes-in-request-miningsubmit - (Some(vb), Some(mask)) => (last_version & !mask.0) | (vb.0 & mask.0), - (None, None) => last_version, - _ => return Err(Error::V1Protocol(v1::error::Error::InvalidSubmission)), - }; - let mining_device_extranonce: Vec = sv1_submit.extra_nonce2.into(); - let extranonce2 = mining_device_extranonce; - Ok(SubmitSharesExtended { - channel_id, - // I put 0 below cause sequence_number is not what should be TODO - sequence_number: 0, - job_id: sv1_submit.job_id.parse::()?, - nonce: sv1_submit.nonce.0, - ntime: sv1_submit.time.0, - version, - extranonce: extranonce2.try_into()?, - }) - } - - /// Internal helper function to handle a received SV2 `SetNewPrevHash` message. - /// - /// This function processes a `SetNewPrevHash` message received from the upstream. - /// It updates the Bridge's stored last previous hash, informs the `channel_factory` - /// about the new previous hash, and then checks the `future_jobs` buffer for - /// a corresponding `NewExtendedMiningJob`. If a matching future job is found, it constructs a - /// SV1 `mining.notify` message and broadcasts it to all downstream clients. It also updates - /// the `last_notify` state for new connections. - async fn handle_new_prev_hash_( - self_: Arc>, - sv2_set_new_prev_hash: SetNewPrevHash<'static>, - tx_sv1_notify: broadcast::Sender>, - ) -> Result<(), Error<'static>> { - while !crate::upstream_sv2::upstream::IS_NEW_JOB_HANDLED - .load(std::sync::atomic::Ordering::SeqCst) - { - tokio::task::yield_now().await; - } - self_.safe_lock(|s| s.last_p_hash = Some(sv2_set_new_prev_hash.clone()))?; - - let on_new_prev_hash_res = self_.safe_lock(|s| { - s.channel_factory - .on_new_prev_hash(sv2_set_new_prev_hash.clone()) - })?; - on_new_prev_hash_res?; - - let mut future_jobs = self_.safe_lock(|s| { - let future_jobs = s.future_jobs.clone(); - s.future_jobs = vec![]; - future_jobs - })?; - - let mut match_a_future_job = false; - while let Some(job) = future_jobs.pop() { - if job.job_id == sv2_set_new_prev_hash.job_id { - let j_id = job.job_id; - // Create the mining.notify to be sent to the Downstream. - let notify = crate::proxy::next_mining_notify::create_notify( - sv2_set_new_prev_hash.clone(), - job, - true, - ); - - // Get the sender to send the mining.notify to the Downstream - tx_sv1_notify.send(notify.clone())?; - match_a_future_job = true; - self_.safe_lock(|s| { - s.last_notify = Some(notify); - s.last_job_id = j_id; - })?; - break; - } - } - if !match_a_future_job { - debug!("No future jobs for {:?}", sv2_set_new_prev_hash); - } - Ok(()) - } - - /// Task handler that receives SV2 `SetNewPrevHash` messages from the upstream. - /// - /// This loop continuously receives `SetNewPrevHash` messages. It calls the - /// internal `handle_new_prev_hash_` helper function to process each message. - fn handle_new_prev_hash(self_: Arc>) { - let task_collector_handle_new_prev_hash = - self_.safe_lock(|b| b.task_collector.clone()).unwrap(); - let (tx_sv1_notify, rx_sv2_set_new_prev_hash, tx_status) = self_ - .safe_lock(|s| { - ( - s.tx_sv1_notify.clone(), - s.rx_sv2_set_new_prev_hash.clone(), - s.tx_status.clone(), - ) - }) - .unwrap(); - debug!("Starting handle_new_prev_hash task"); - let handle_new_prev_hash = tokio::task::spawn(async move { - loop { - // Receive `SetNewPrevHash` from `Upstream` - let sv2_set_new_prev_hash: SetNewPrevHash = - handle_result!(tx_status, rx_sv2_set_new_prev_hash.clone().recv().await); - debug!( - "handle_new_prev_hash job_id: {:?}", - &sv2_set_new_prev_hash.job_id - ); - handle_result!( - tx_status.clone(), - Self::handle_new_prev_hash_( - self_.clone(), - sv2_set_new_prev_hash, - tx_sv1_notify.clone(), - ) - .await - ) - } - }); - let _ = task_collector_handle_new_prev_hash.safe_lock(|a| { - a.push(( - handle_new_prev_hash.abort_handle(), - "handle_new_prev_hash".to_string(), - )) - }); - } - - /// Internal helper function to handle a received SV2 `NewExtendedMiningJob` message. - /// - /// This function processes a `NewExtendedMiningJob` message received from the upstream. - /// It first informs the `channel_factory` about the new job. If the job's `is_future` is true, - /// the job is buffered in `future_jobs`. If `is_future` is false, it expects a - /// corresponding `SetNewPrevHash` (which should have been received prior according to the - /// protocol) and immediately constructs and broadcasts a SV1 `mining.notify` message to - /// downstream clients, updating the `last_notify` state. - async fn handle_new_extended_mining_job_( - self_: Arc>, - sv2_new_extended_mining_job: NewExtendedMiningJob<'static>, - tx_sv1_notify: broadcast::Sender>, - ) -> Result<(), Error<'static>> { - // convert to non segwit jobs so we dont have to depend if miner's support segwit or not - self_.safe_lock(|s| { - s.channel_factory - .on_new_extended_mining_job(sv2_new_extended_mining_job.as_static().clone()) - })??; - - // If future_job=true, this job is meant for a future SetNewPrevHash that the proxy - // has yet to receive. Insert this new job into the job_mapper . - if sv2_new_extended_mining_job.is_future() { - self_.safe_lock(|s| s.future_jobs.push(sv2_new_extended_mining_job.clone()))?; - Ok(()) - - // If future_job=false, this job is meant for the current SetNewPrevHash. - } else { - let last_p_hash_option = self_.safe_lock(|s| s.last_p_hash.clone())?; - - // last_p_hash is an Option so we need to map to the correct error type - // to be handled - let last_p_hash = last_p_hash_option.ok_or(Error::RolesSv2Logic( - RolesLogicError::JobIsNotFutureButPrevHashNotPresent, - ))?; - - let j_id = sv2_new_extended_mining_job.job_id; - // Create the mining.notify to be sent to the Downstream. - // clean_jobs must be false because it's not a NewPrevHash template - let notify = crate::proxy::next_mining_notify::create_notify( - last_p_hash, - sv2_new_extended_mining_job.clone(), - false, - ); - // Get the sender to send the mining.notify to the Downstream - tx_sv1_notify.send(notify.clone())?; - self_.safe_lock(|s| { - s.last_notify = Some(notify); - s.last_job_id = j_id; - })?; - Ok(()) - } - } - - /// Task handler that receives SV2 `NewExtendedMiningJob` messages from the upstream. - /// - /// This loop continuously receives `NewExtendedMiningJob` messages. It calls the - /// internal `handle_new_extended_mining_job_` helper function to process each message. - /// After processing, it signals that a new job has been handled (used for synchronization - /// with the `handle_new_prev_hash` task). - fn handle_new_extended_mining_job(self_: Arc>) { - let task_collector_new_extended_mining_job = - self_.safe_lock(|b| b.task_collector.clone()).unwrap(); - let (tx_sv1_notify, rx_sv2_new_ext_mining_job, tx_status) = self_ - .safe_lock(|s| { - ( - s.tx_sv1_notify.clone(), - s.rx_sv2_new_ext_mining_job.clone(), - s.tx_status.clone(), - ) - }) - .unwrap(); - debug!("Starting handle_new_extended_mining_job task"); - let handle_new_extended_mining_job = tokio::task::spawn(async move { - loop { - // Receive `NewExtendedMiningJob` from `Upstream` - let sv2_new_extended_mining_job: NewExtendedMiningJob = handle_result!( - tx_status.clone(), - rx_sv2_new_ext_mining_job.clone().recv().await - ); - debug!( - "handle_new_extended_mining_job job_id: {:?}", - &sv2_new_extended_mining_job.job_id - ); - handle_result!( - tx_status, - Self::handle_new_extended_mining_job_( - self_.clone(), - sv2_new_extended_mining_job, - tx_sv1_notify.clone(), - ) - .await - ); - crate::upstream_sv2::upstream::IS_NEW_JOB_HANDLED - .store(true, std::sync::atomic::Ordering::SeqCst); - } - }); - let _ = task_collector_new_extended_mining_job.safe_lock(|a| { - a.push(( - handle_new_extended_mining_job.abort_handle(), - "handle_new_extended_mining_job".to_string(), - )) - }); - } -} - -/// Represents the necessary information to initialize a new SV1 downstream connection -/// after it has been registered with the Bridge's channel factory. -/// -/// This structure is returned by `Bridge::on_new_sv1_connection` and contains the -/// channel ID assigned to the connection, the initial job notification to send, -/// and the extranonce and target specific to this channel. -pub struct OpenSv1Downstream { - /// The unique ID assigned to this downstream channel by the channel factory. - pub channel_id: u32, - /// The most recent `mining.notify` message to send to the new client immediately - /// upon connection to provide them with a job. - pub last_notify: Option>, - /// The extranonce prefix assigned to this channel. - pub extranonce: Vec, - /// The mining target assigned to this channel - pub target: Arc>>, - /// The size of the extranonce2 field expected from the miner for this channel. - pub extranonce2_len: u16, -} - -#[cfg(test)] -mod test { - use super::*; - use async_channel::bounded; - use stratum_common::bitcoin::{absolute::LockTime, consensus, transaction::Version}; - - pub mod test_utils { - use super::*; - - #[allow(dead_code)] - pub struct BridgeInterface { - pub tx_sv1_submit: Sender, - pub rx_sv2_submit_shares_ext: Receiver>, - pub tx_sv2_set_new_prev_hash: Sender>, - pub tx_sv2_new_ext_mining_job: Sender>, - pub rx_sv1_notify: broadcast::Receiver>, - } - - pub fn create_bridge( - extranonces: ExtendedExtranonce, - ) -> (Arc>, BridgeInterface) { - let (tx_sv1_submit, rx_sv1_submit) = bounded(1); - let (tx_sv2_submit_shares_ext, rx_sv2_submit_shares_ext) = bounded(1); - let (tx_sv2_set_new_prev_hash, rx_sv2_set_new_prev_hash) = bounded(1); - let (tx_sv2_new_ext_mining_job, rx_sv2_new_ext_mining_job) = bounded(1); - let (tx_sv1_notify, rx_sv1_notify) = broadcast::channel(1); - let (tx_status, _rx_status) = bounded(1); - let upstream_target = vec![ - 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, - ]; - let interface = BridgeInterface { - tx_sv1_submit, - rx_sv2_submit_shares_ext, - tx_sv2_set_new_prev_hash, - tx_sv2_new_ext_mining_job, - rx_sv1_notify, - }; - - let task_collector = Arc::new(Mutex::new(vec![])); - let b = Bridge::new( - rx_sv1_submit, - tx_sv2_submit_shares_ext, - rx_sv2_set_new_prev_hash, - rx_sv2_new_ext_mining_job, - tx_sv1_notify, - status::Sender::Bridge(tx_status), - extranonces, - Arc::new(Mutex::new(upstream_target)), - 1, - task_collector, - ); - (b, interface) - } - - pub fn create_sv1_submit(job_id: u32) -> Submit<'static> { - Submit { - user_name: "test_user".to_string(), - job_id: job_id.to_string(), - extra_nonce2: v1::utils::Extranonce::try_from([0; 32].to_vec()).unwrap(), - time: v1::utils::HexU32Be(1), - nonce: v1::utils::HexU32Be(1), - version_bits: None, - id: 0, - } - } - } - - #[test] - fn test_version_bits_insert() { - use stratum_common::{ - bitcoin, - bitcoin::{blockdata::witness::Witness, hashes::Hash}, - }; - - let extranonces = ExtendedExtranonce::new(0..6, 6..8, 8..16, None) - .expect("Failed to create ExtendedExtranonce with valid ranges"); - let (bridge, _) = test_utils::create_bridge(extranonces); - bridge - .safe_lock(|bridge| { - let channel_id = 1; - let out_id = bitcoin::hashes::sha256d::Hash::from_slice(&[ - 0_u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, - ]) - .unwrap(); - let p_out = bitcoin::OutPoint { - txid: bitcoin::Txid::from_raw_hash(out_id), - vout: 0xffff_ffff, - }; - let in_ = bitcoin::TxIn { - previous_output: p_out, - script_sig: vec![89_u8; 16].into(), - sequence: bitcoin::Sequence(0), - witness: Witness::from(vec![] as Vec>), - }; - let tx = bitcoin::Transaction { - version: Version::ONE, - lock_time: LockTime::from_consensus(0), - input: vec![in_], - output: vec![], - }; - let tx = consensus::serialize(&tx); - let _down = bridge - .channel_factory - .add_standard_channel(0, 10_000_000_000.0, true, 1) - .unwrap(); - let prev_hash = SetNewPrevHash { - channel_id, - job_id: 0, - prev_hash: [ - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, - ] - .into(), - min_ntime: 989898, - nbits: 9, - }; - bridge.channel_factory.on_new_prev_hash(prev_hash).unwrap(); - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() as u32; - let new_mining_job = NewExtendedMiningJob { - channel_id, - job_id: 0, - min_ntime: binary_sv2::Sv2Option::new(Some(now)), - version: 0b0000_0000_0000_0000, - version_rolling_allowed: false, - merkle_path: vec![].into(), - coinbase_tx_prefix: tx[0..42].to_vec().try_into().unwrap(), - coinbase_tx_suffix: tx[58..].to_vec().try_into().unwrap(), - }; - bridge - .channel_factory - .on_new_extended_mining_job(new_mining_job.clone()) - .unwrap(); - - // pass sv1_submit into Bridge::translate_submit - let sv1_submit = test_utils::create_sv1_submit(0); - let sv2_message = bridge - .translate_submit(channel_id, sv1_submit, None) - .unwrap(); - // assert sv2 message equals sv1 with version bits added - assert_eq!( - new_mining_job.version, sv2_message.version, - "Version bits were not inserted for non version rolling sv1 message" - ); - }) - .unwrap(); - } -} diff --git a/roles/translator/src/lib/proxy/mod.rs b/roles/translator/src/lib/proxy/mod.rs deleted file mode 100644 index e2231be1dd..0000000000 --- a/roles/translator/src/lib/proxy/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod bridge; -pub mod next_mining_notify; -pub use bridge::Bridge; diff --git a/roles/translator/src/lib/proxy/next_mining_notify.rs b/roles/translator/src/lib/proxy/next_mining_notify.rs deleted file mode 100644 index 14abc715ef..0000000000 --- a/roles/translator/src/lib/proxy/next_mining_notify.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! Provides functionality to convert Stratum V2 job into a -//! Stratum V1 `mining.notify` message. -use roles_logic_sv2::{ - job_creator::extended_job_to_non_segwit, - mining_sv2::{NewExtendedMiningJob, SetNewPrevHash}, -}; -use tracing::debug; -use v1::{ - server_to_client, - utils::{HexU32Be, MerkleNode, PrevHash}, -}; - -/// Creates a new SV1 `mining.notify` message if both SV2 `SetNewPrevHash` and -/// `NewExtendedMiningJob` messages have been received. If one of these messages is still being -/// waited on, the function returns `None`. -/// If clean_jobs = false, it means a new job is created, with the same PrevHash -pub fn create_notify( - new_prev_hash: SetNewPrevHash<'static>, - new_job: NewExtendedMiningJob<'static>, - clean_jobs: bool, -) -> server_to_client::Notify<'static> { - // TODO 32 must be changed! - let new_job = extended_job_to_non_segwit(new_job, 32) - .expect("failed to convert extended job to non segwit"); - // Make sure that SetNewPrevHash + NewExtendedMiningJob is matching (not future) - let job_id = new_job.job_id.to_string(); - - // U256<'static> -> MerkleLeaf - let prev_hash = PrevHash(new_prev_hash.prev_hash.clone()); - - // B064K<'static'> -> HexBytes - let coin_base1 = new_job.coinbase_tx_prefix.to_vec().into(); - let coin_base2 = new_job.coinbase_tx_suffix.to_vec().into(); - - // Seq0255<'static, U56<'static>> -> Vec> - let merkle_path = new_job.merkle_path.clone().into_static().0; - let merkle_branch: Vec = merkle_path.into_iter().map(MerkleNode).collect(); - - // u32 -> HexBytes - let version = HexU32Be(new_job.version); - let bits = HexU32Be(new_prev_hash.nbits); - let time = HexU32Be(match new_job.is_future() { - true => new_prev_hash.min_ntime, - false => new_job.min_ntime.clone().into_inner().unwrap(), - }); - - let notify_response = server_to_client::Notify { - job_id, - prev_hash, - coin_base1, - coin_base2, - merkle_branch, - version, - bits, - time, - clean_jobs, - }; - debug!("\nNextMiningNotify: {:?}\n", notify_response); - notify_response -} diff --git a/roles/translator/src/lib/status.rs b/roles/translator/src/lib/status.rs index 879697bdf2..896cff9a93 100644 --- a/roles/translator/src/lib/status.rs +++ b/roles/translator/src/lib/status.rs @@ -1,223 +1,117 @@ -//! ## Status Reporting System for Translator +//! ## Status Reporting System //! -//! This module defines how internal components of the Translator report -//! health, errors, and shutdown conditions back to the main runtime loop in `lib/mod.rs`. +//! This module provides a centralized way for components of the Translator to report +//! health updates, shutdown reasons, or fatal errors to the main runtime loop. //! -//! At the core, tasks send a [`Status`] (wrapping a [`State`]) through a channel, -//! which is tagged with a [`Sender`] enum to indicate the origin of the message. -//! -//! This allows for centralized, consistent error handling across the application. +//! Each task wraps its report in a [`Status`] and sends it over an async channel, +//! tagged with a [`Sender`] variant that identifies the source subsystem. + +use tracing::{debug, error, warn}; -use crate::error::{self, Error}; +use crate::error::TproxyError; /// Identifies the component that originated a [`Status`] update. /// -/// Each sender is associated with a dedicated side of the status channel. -/// This lets the central loop distinguish between errors from different parts of the system. -#[derive(Debug)] -pub enum Sender { - /// Sender for downstream connections. - Downstream(async_channel::Sender>), - /// Sender for downstream listener. - DownstreamListener(async_channel::Sender>), - /// Sender for bridge connections. - Bridge(async_channel::Sender>), - /// Sender for upstream connections. - Upstream(async_channel::Sender>), - /// Sender for template receiver. - TemplateReceiver(async_channel::Sender>), +/// Each variant contains a channel to the main coordinator, and optionally a component ID +/// (e.g. a downstream connection ID). +#[derive(Debug, Clone)] +pub enum StatusSender { + /// A specific downstream connection. + Downstream { + downstream_id: u32, + tx: async_channel::Sender, + }, + /// The SV1 server listener. + Sv1Server(async_channel::Sender), + /// The SV2 <-> SV1 bridge manager. + ChannelManager(async_channel::Sender), + /// The upstream SV2 connection handler. + Upstream(async_channel::Sender), } -impl Sender { - /// Converts a `DownstreamListener` sender to a `Downstream` sender. - /// FIXME: Use `From` trait and remove this - pub fn listener_to_connection(&self) -> Self { +impl StatusSender { + /// Sends a [`Status`] update. + pub async fn send(&self, status: Status) -> Result<(), async_channel::SendError> { match self { - Self::DownstreamListener(inner) => Self::Downstream(inner.clone()), - _ => unreachable!(), - } - } - - /// Sends a status update. - pub async fn send( - &self, - status: Status<'static>, - ) -> Result<(), async_channel::SendError>> { - match self { - Self::Downstream(inner) => inner.send(status).await, - Self::DownstreamListener(inner) => inner.send(status).await, - Self::Bridge(inner) => inner.send(status).await, - Self::Upstream(inner) => inner.send(status).await, - Self::TemplateReceiver(inner) => inner.send(status).await, - } - } -} - -impl Clone for Sender { - fn clone(&self) -> Self { - match self { - Self::Downstream(inner) => Self::Downstream(inner.clone()), - Self::DownstreamListener(inner) => Self::DownstreamListener(inner.clone()), - Self::Bridge(inner) => Self::Bridge(inner.clone()), - Self::Upstream(inner) => Self::Upstream(inner.clone()), - Self::TemplateReceiver(inner) => Self::TemplateReceiver(inner.clone()), + Self::Downstream { downstream_id, tx } => { + debug!( + "Sending status from Downstream [{}]: {:?}", + downstream_id, status.state + ); + tx.send(status).await + } + Self::Sv1Server(tx) => { + debug!("Sending status from Sv1Server: {:?}", status.state); + tx.send(status).await + } + Self::ChannelManager(tx) => { + debug!("Sending status from ChannelManager: {:?}", status.state); + tx.send(status).await + } + Self::Upstream(tx) => { + debug!("Sending status from Upstream: {:?}", status.state); + tx.send(status).await + } } } } -/// The kind of event or status being reported by a task. +/// The type of event or error being reported by a component. #[derive(Debug)] -pub enum State<'a> { - /// Downstream connection shutdown. - DownstreamShutdown(Error<'a>), - /// Bridge connection shutdown. - BridgeShutdown(Error<'a>), - /// Upstream connection shutdown. - UpstreamShutdown(Error<'a>), - /// Upstream connection trying to reconnect. - UpstreamTryReconnect(Error<'a>), - /// Component is healthy. - Healthy(String), +pub enum State { + /// Downstream task exited or encountered an unrecoverable error. + DownstreamShutdown { + downstream_id: u32, + reason: TproxyError, + }, + /// SV1 server listener exited unexpectedly. + Sv1ServerShutdown(TproxyError), + /// Channel manager shut down (SV2 bridge manager). + ChannelManagerShutdown(TproxyError), + /// Upstream SV2 connection closed or failed. + UpstreamShutdown(TproxyError), } -/// Wraps a status update, to be passed through a status channel. +/// A message reporting the current [`State`] of a component. #[derive(Debug)] -pub struct Status<'a> { - pub state: State<'a>, +pub struct Status { + pub state: State, } -/// Sends a [`Status`] message tagged with its [`Sender`] to the central loop. -/// -/// This is the core logic used to determine which status variant should be sent -/// based on the error type and sender context. -async fn send_status( - sender: &Sender, - e: error::Error<'static>, - outcome: error_handling::ErrorBranch, -) -> error_handling::ErrorBranch { - match sender { - Sender::Downstream(tx) => { - tx.send(Status { - state: State::Healthy(e.to_string()), - }) - .await - .unwrap_or(()); +/// Constructs and sends a [`Status`] update based on the [`Sender`] and error context. +async fn send_status(sender: &StatusSender, error: TproxyError) { + let state = match sender { + StatusSender::Downstream { downstream_id, .. } => { + warn!("Downstream [{downstream_id}] shutting down due to error: {error:?}"); + State::DownstreamShutdown { + downstream_id: *downstream_id, + reason: error, + } } - Sender::DownstreamListener(tx) => { - tx.send(Status { - state: State::DownstreamShutdown(e), - }) - .await - .unwrap_or(()); + StatusSender::Sv1Server(_) => { + warn!("Sv1Server shutting down due to error: {error:?}"); + State::Sv1ServerShutdown(error) } - Sender::Bridge(tx) => { - tx.send(Status { - state: State::BridgeShutdown(e), - }) - .await - .unwrap_or(()); + StatusSender::ChannelManager(_) => { + warn!("ChannelManager shutting down due to error: {error:?}"); + State::ChannelManagerShutdown(error) } - Sender::Upstream(tx) => match e { - Error::ChannelErrorReceiver(_) => { - tx.send(Status { - state: State::UpstreamTryReconnect(e), - }) - .await - .unwrap_or(()); - } - _ => { - tx.send(Status { - state: State::UpstreamShutdown(e), - }) - .await - .unwrap_or(()); - } - }, - Sender::TemplateReceiver(tx) => { - tx.send(Status { - state: State::UpstreamShutdown(e), - }) - .await - .unwrap_or(()); + StatusSender::Upstream(_) => { + warn!("Upstream shutting down due to error: {error:?}"); + State::UpstreamShutdown(error) } + }; + + if let Err(e) = sender.send(Status { state }).await { + error!("Failed to send status update from {sender:?}: {e:?}"); } - outcome } /// Centralized error dispatcher for the Translator. /// /// Used by the `handle_result!` macro across the codebase. /// Decides whether the task should `Continue` or `Break` based on the error type and source. -pub async fn handle_error( - sender: &Sender, - e: error::Error<'static>, -) -> error_handling::ErrorBranch { - tracing::error!("Error: {:?}", &e); - match e { - Error::VecToSlice32(_) => send_status(sender, e, error_handling::ErrorBranch::Break).await, - // Errors on bad CLI argument input. - Error::BadCliArgs => send_status(sender, e, error_handling::ErrorBranch::Break).await, - // Errors on bad `serde_json` serialize/deserialize. - Error::BadSerdeJson(_) => send_status(sender, e, error_handling::ErrorBranch::Break).await, - // Errors on bad `config` TOML deserialize. - Error::BadConfigDeserialize(_) => { - send_status(sender, e, error_handling::ErrorBranch::Break).await - } - // Errors from `binary_sv2` crate. - Error::BinarySv2(_) => send_status(sender, e, error_handling::ErrorBranch::Break).await, - // Errors on bad noise handshake. - Error::CodecNoise(_) => send_status(sender, e, error_handling::ErrorBranch::Break).await, - // Errors from `framing_sv2` crate. - Error::FramingSv2(_) => send_status(sender, e, error_handling::ErrorBranch::Break).await, - //If the pool sends the tproxy an invalid extranonce - Error::InvalidExtranonce(_) => { - send_status(sender, e, error_handling::ErrorBranch::Break).await - } - // Errors on bad `TcpStream` connection. - Error::Io(_) => send_status(sender, e, error_handling::ErrorBranch::Break).await, - // Errors on bad `String` to `int` conversion. - Error::ParseInt(_) => send_status(sender, e, error_handling::ErrorBranch::Break).await, - // Errors from `roles_logic_sv2` crate. - Error::RolesSv2Logic(_) => send_status(sender, e, error_handling::ErrorBranch::Break).await, - Error::UpstreamIncoming(_) => { - send_status(sender, e, error_handling::ErrorBranch::Break).await - } - // SV1 protocol library error - Error::V1Protocol(_) => send_status(sender, e, error_handling::ErrorBranch::Break).await, - Error::SubprotocolMining(_) => { - send_status(sender, e, error_handling::ErrorBranch::Break).await - } - // Locking Errors - Error::PoisonLock => send_status(sender, e, error_handling::ErrorBranch::Break).await, - // Channel Receiver Error - Error::ChannelErrorReceiver(_) => { - send_status(sender, e, error_handling::ErrorBranch::Break).await - } - Error::TokioChannelErrorRecv(_) => { - send_status(sender, e, error_handling::ErrorBranch::Break).await - } - // Channel Sender Errors - Error::ChannelErrorSender(_) => { - send_status(sender, e, error_handling::ErrorBranch::Break).await - } - Error::SetDifficultyToMessage(_) => { - send_status(sender, e, error_handling::ErrorBranch::Break).await - } - Error::Infallible(_) => send_status(sender, e, error_handling::ErrorBranch::Break).await, - Error::Sv2ProtocolError(ref inner) => { - match inner { - // dont notify main thread just continue - roles_logic_sv2::parsers::Mining::SubmitSharesError(_) => { - error_handling::ErrorBranch::Continue - } - _ => send_status(sender, e, error_handling::ErrorBranch::Break).await, - } - } - Error::TargetError(_) => { - send_status(sender, e, error_handling::ErrorBranch::Continue).await - } - Error::Sv1MessageTooLong => { - send_status(sender, e, error_handling::ErrorBranch::Break).await - } - } +pub async fn handle_error(sender: &StatusSender, e: TproxyError) { + error!("Error in {:?}: {:?}", sender, e); + send_status(sender, e).await; } diff --git a/roles/translator/src/lib/sv1/downstream/channel.rs b/roles/translator/src/lib/sv1/downstream/channel.rs new file mode 100644 index 0000000000..897ee0060a --- /dev/null +++ b/roles/translator/src/lib/sv1/downstream/channel.rs @@ -0,0 +1,35 @@ +use super::DownstreamMessages; +use async_channel::{Receiver, Sender}; +use tokio::sync::broadcast; +use tracing::debug; +use v1::json_rpc; + +#[derive(Debug)] +pub struct DownstreamChannelState { + pub downstream_sv1_sender: Sender, + pub downstream_sv1_receiver: Receiver, + pub sv1_server_sender: Sender, + pub sv1_server_receiver: broadcast::Receiver<(u32, Option, json_rpc::Message)>, /* channel_id, optional downstream_id, message */ +} + +impl DownstreamChannelState { + pub fn new( + downstream_sv1_sender: Sender, + downstream_sv1_receiver: Receiver, + sv1_server_sender: Sender, + sv1_server_receiver: broadcast::Receiver<(u32, Option, json_rpc::Message)>, + ) -> Self { + Self { + downstream_sv1_receiver, + downstream_sv1_sender, + sv1_server_receiver, + sv1_server_sender, + } + } + + pub fn drop(&self) { + debug!("Dropping downstream channel state"); + self.downstream_sv1_receiver.close(); + self.downstream_sv1_sender.close(); + } +} diff --git a/roles/translator/src/lib/sv1/downstream/data.rs b/roles/translator/src/lib/sv1/downstream/data.rs new file mode 100644 index 0000000000..66f52e59fd --- /dev/null +++ b/roles/translator/src/lib/sv1/downstream/data.rs @@ -0,0 +1,93 @@ +use std::{ + cell::RefCell, + sync::{atomic::AtomicBool, Arc}, +}; +use stratum_common::roles_logic_sv2::{mining_sv2::Target, utils::Mutex}; +use tracing::debug; +use v1::{json_rpc, utils::HexU32Be}; + +use super::SubmitShareWithChannelId; +use crate::sv1::sv1_server::data::Sv1ServerData; + +#[derive(Debug)] +pub struct DownstreamData { + pub channel_id: Option, + pub downstream_id: u32, + pub extranonce1: Vec, + pub extranonce2_len: usize, + pub version_rolling_mask: Option, + pub version_rolling_min_bit: Option, + pub last_job_version_field: Option, + pub authorized_worker_name: String, + pub user_identity: String, + pub target: Target, + pub hashrate: Option, + pub cached_set_difficulty: Option, + pub cached_notify: Option, + pub pending_target: Option, + pub pending_hashrate: Option, + // Flag to track if SV1 handshake is complete (subscribe + authorize) + pub sv1_handshake_complete: AtomicBool, + // Queue of Sv1 handshake messages received while waiting for SV2 channel to open + pub queued_sv1_handshake_messages: Vec, + // Flag to indicate we're processing queued Sv1 handshake message responses + pub processing_queued_sv1_handshake_responses: AtomicBool, + // Stores pending shares to be sent to the sv1_server + pub pending_share: RefCell>, + // Reference to shared sv1_server data for accessing valid_jobs during downstream sv1 + // validation + pub sv1_server_data: Arc>, + // Tracks the upstream target for this downstream, used for vardiff target comparison + pub upstream_target: Option, +} + +impl DownstreamData { + pub fn new( + downstream_id: u32, + target: Target, + hashrate: Option, + sv1_server_data: Arc>, + ) -> Self { + DownstreamData { + channel_id: None, + downstream_id, + extranonce1: vec![0; 8], + extranonce2_len: 4, + version_rolling_mask: None, + version_rolling_min_bit: None, + last_job_version_field: None, + authorized_worker_name: String::new(), + user_identity: String::new(), + target, + hashrate, + cached_set_difficulty: None, + cached_notify: None, + pending_target: None, + pending_hashrate: None, + sv1_handshake_complete: AtomicBool::new(false), + queued_sv1_handshake_messages: Vec::new(), + processing_queued_sv1_handshake_responses: AtomicBool::new(false), + pending_share: RefCell::new(None), + sv1_server_data, + upstream_target: None, + } + } + + pub fn set_pending_target(&mut self, new_target: Target) { + self.pending_target = Some(new_target); + debug!("Downstream {}: Set pending target", self.downstream_id); + } + + pub fn set_pending_hashrate(&mut self, new_hashrate: Option) { + self.pending_hashrate = new_hashrate; + debug!("Downstream {}: Set pending hashrate", self.downstream_id); + } + + pub fn set_upstream_target(&mut self, upstream_target: Target) { + self.upstream_target = Some(upstream_target.clone()); + debug!( + "Downstream {}: Set upstream target to {:?}", + self.downstream_id, upstream_target + ); + } +} diff --git a/roles/translator/src/lib/sv1/downstream/downstream.rs b/roles/translator/src/lib/sv1/downstream/downstream.rs new file mode 100644 index 0000000000..96ab003b7d --- /dev/null +++ b/roles/translator/src/lib/sv1/downstream/downstream.rs @@ -0,0 +1,523 @@ +use super::DownstreamMessages; +use crate::{ + error::TproxyError, + status::{handle_error, StatusSender}, + sv1::{ + downstream::{channel::DownstreamChannelState, data::DownstreamData}, + sv1_server::data::Sv1ServerData, + }, + task_manager::TaskManager, + utils::ShutdownMessage, +}; +use async_channel::{Receiver, Sender}; +use std::sync::Arc; +use stratum_common::roles_logic_sv2::{mining_sv2::Target, utils::Mutex}; +use tokio::sync::{broadcast, mpsc}; +use tracing::{debug, error, info, warn}; +use v1::{ + json_rpc::{self, Message}, + server_to_client, IsServer, +}; + +/// Represents a downstream SV1 miner connection. +/// +/// This struct manages the state and communication for a single SV1 miner connected +/// to the translator. It handles: +/// - SV1 protocol message processing (subscribe, authorize, submit) +/// - Bidirectional message routing between miner and SV1 server +/// - Mining job tracking and share validation +/// - Difficulty adjustment coordination +/// - Connection lifecycle management +/// +/// Each downstream connection runs in its own async task that processes messages +/// from both the miner and the server, ensuring proper message ordering and +/// handling connection-specific state. +#[derive(Debug)] +pub struct Downstream { + pub downstream_data: Arc>, + downstream_channel_state: DownstreamChannelState, +} + +impl Downstream { + /// Creates a new downstream connection instance. + #[allow(clippy::too_many_arguments)] + pub fn new( + downstream_id: u32, + downstream_sv1_sender: Sender, + downstream_sv1_receiver: Receiver, + sv1_server_sender: Sender, + sv1_server_receiver: broadcast::Receiver<(u32, Option, json_rpc::Message)>, + target: Target, + hashrate: Option, + sv1_server_data: Arc>, + ) -> Self { + let downstream_data = Arc::new(Mutex::new(DownstreamData::new( + downstream_id, + target, + hashrate, + sv1_server_data, + ))); + let downstream_channel_state = DownstreamChannelState::new( + downstream_sv1_sender, + downstream_sv1_receiver, + sv1_server_sender, + sv1_server_receiver, + ); + Self { + downstream_data, + downstream_channel_state, + } + } + + /// Spawns and runs the main task loop for this downstream connection. + /// + /// This method creates an async task that handles all communication for this + /// downstream connection. The task runs a select loop that processes: + /// - Shutdown signals (global, targeted, or all-downstream) + /// - Messages from the miner (subscribe, authorize, submit) + /// - Messages from the SV1 server (notify, set_difficulty, etc.) + /// + /// The task will continue running until a shutdown signal is received or + /// an unrecoverable error occurs. It ensures graceful cleanup of resources + /// and proper error reporting. + pub fn run_downstream_tasks( + self: Arc, + notify_shutdown: broadcast::Sender, + shutdown_complete_tx: mpsc::Sender<()>, + status_sender: StatusSender, + task_manager: Arc, + ) { + let mut sv1_server_receiver = self + .downstream_channel_state + .sv1_server_receiver + .resubscribe(); + let mut shutdown_rx = notify_shutdown.subscribe(); + let downstream_id = self.downstream_data.super_safe_lock(|d| d.downstream_id); + task_manager.spawn(async move { + loop { + tokio::select! { + msg = shutdown_rx.recv() => { + match msg { + Ok(ShutdownMessage::ShutdownAll) => { + info!("Downstream {downstream_id}: received global shutdown"); + break; + } + Ok(ShutdownMessage::DownstreamShutdown(id)) if id == downstream_id => { + info!("Downstream {downstream_id}: received targeted shutdown"); + break; + } + Ok(ShutdownMessage::DownstreamShutdownAll) => { + info!("All downstream shutdown message received"); + break; + } + Ok(ShutdownMessage::UpstreamReconnectedResetAndShutdownDownstreams) => { + info!("All downstream shutdown message received (upstream reconnected)"); + break; + } + Ok(_) => { + // shutdown for other downstream + } + Err(e) => { + warn!("Downstream {downstream_id}: shutdown channel closed: {e}"); + break; + } + } + } + + // Handle downstream -> server message + res = Self::handle_downstream_message(self.clone()) => { + if let Err(e) = res { + error!("Downstream {downstream_id}: error in downstream message handler: {e:?}"); + handle_error(&status_sender, e).await; + break; + } + } + + // Handle server -> downstream message + res = Self::handle_sv1_server_message(self.clone(),&mut sv1_server_receiver) => { + if let Err(e) = res { + error!("Downstream {downstream_id}: error in server message handler: {e:?}"); + handle_error(&status_sender, e).await; + break; + } + } + + else => { + warn!("Downstream {downstream_id}: all channels closed; exiting task"); + break; + } + } + } + + warn!("Downstream {downstream_id}: unified task shutting down"); + self.downstream_channel_state.drop(); + drop(shutdown_complete_tx); + }); + } + + /// Handles messages received from the SV1 server. + /// + /// This method processes messages broadcast from the SV1 server to downstream + /// connections. Since `mining.notify` messages are guaranteed to never arrive + /// before their corresponding `mining.set_difficulty` message, the logic is + /// simplified to handle only handshake completion timing. + /// + /// Key behaviors: + /// - Filters messages by channel ID and downstream ID + /// - For `mining.set_difficulty`: Always caches the message (never sent immediately) + /// - For `mining.notify`: Sends any pending set_difficulty first, then forwards the notify + /// - For other messages: Forwards directly to the miner + /// - Caches both `mining.set_difficulty` and `mining.notify` messages if handshake is not yet + /// complete + /// - On handshake completion: sends cached messages in correct order (set_difficulty first, + /// then notify) + pub async fn handle_sv1_server_message( + self: Arc, + sv1_server_receiver: &mut broadcast::Receiver<(u32, Option, json_rpc::Message)>, + ) -> Result<(), TproxyError> { + match sv1_server_receiver.recv().await { + Ok((channel_id, downstream_id, message)) => { + let (my_channel_id, my_downstream_id, handshake_complete) = + self.downstream_data.super_safe_lock(|d| { + ( + d.channel_id, + d.downstream_id, + d.sv1_handshake_complete + .load(std::sync::atomic::Ordering::SeqCst), + ) + }); + let id_matches = (my_channel_id == Some(channel_id) || channel_id == 0) + && (downstream_id.is_none() || downstream_id == Some(my_downstream_id)); + if !id_matches { + return Ok(()); // Message not intended for this downstream + } + + // Check if this is a queued message response + let is_queued_sv1_handshake_response = self.downstream_data.super_safe_lock(|d| { + d.processing_queued_sv1_handshake_responses + .load(std::sync::atomic::Ordering::SeqCst) + }); + + // Handle messages based on message type and handshake state + if let Message::Notification(notification) = &message { + // For notifications (mining.set_difficulty, mining.notify), only send if + // handshake is complete + if handshake_complete { + match notification.method.as_str() { + "mining.set_difficulty" => { + // Cache the Sv1 set_difficulty message to be sent before the next + // notify + debug!("Down: Caching mining.set_difficulty to send before next mining.notify"); + self.downstream_data.super_safe_lock(|d| { + d.cached_set_difficulty = Some(message); + }); + return Ok(()); + } + "mining.notify" => { + let (pending_set_difficulty, notify_opt) = + self.downstream_data.super_safe_lock(|d| { + let cached_set_difficulty = d.cached_set_difficulty.take(); + + // Prepare the notify message and update state + let notify_result = server_to_client::Notify::try_from( + notification.clone(), + ); + if let Ok(mut notify) = notify_result { + if cached_set_difficulty.is_some() { + notify.clean_jobs = true; + } + d.last_job_version_field = Some(notify.version.0); + + // Update target and hashrate if we're sending + // set_difficulty + if cached_set_difficulty.is_some() { + if let Some(new_target) = d.pending_target.take() { + d.target = new_target; + } + if let Some(new_hashrate) = + d.pending_hashrate.take() + { + d.hashrate = Some(new_hashrate); + } + } + + (cached_set_difficulty, Some(notify)) + } else { + (cached_set_difficulty, None) + } + }); + + if let Some(set_difficulty_msg) = &pending_set_difficulty { + debug!("Down: Sending pending mining.set_difficulty before mining.notify"); + self.downstream_channel_state + .downstream_sv1_sender + .send(set_difficulty_msg.clone()) + .await + .map_err(|e| { + error!( + "Down: Failed to send mining.set_difficulty to downstream: {:?}", + e + ); + TproxyError::ChannelErrorSender + })?; + } + + if let Some(notify) = notify_opt { + debug!("Down: Sending mining.notify"); + self.downstream_channel_state + .downstream_sv1_sender + .send(notify.into()) + .await + .map_err(|e| { + error!("Down: Failed to send mining.notify to downstream: {:?}", e); + TproxyError::ChannelErrorSender + })?; + } + return Ok(()); + } + _ => { + // Other notifications - forward if handshake complete + self.downstream_channel_state + .downstream_sv1_sender + .send(message.clone()) + .await + .map_err(|e| { + error!( + "Down: Failed to send notification to downstream: {:?}", + e + ); + TproxyError::ChannelErrorSender + })?; + } + } + } else { + // Handshake not complete - cache mining notifications but skip others + match notification.method.as_str() { + "mining.set_difficulty" => { + debug!("Down: SV1 handshake not complete, caching mining.set_difficulty"); + self.downstream_data.super_safe_lock(|d| { + d.cached_set_difficulty = Some(message); + }); + } + "mining.notify" => { + debug!("Down: SV1 handshake not complete, caching mining.notify"); + self.downstream_data.super_safe_lock(|d| { + d.cached_notify = Some(message.clone()); + let notify = + server_to_client::Notify::try_from(notification.clone()) + .expect("this must be a mining.notify"); + d.last_job_version_field = Some(notify.version.0); + }); + } + _ => { + debug!( + "Down: SV1 handshake not complete, skipping other notification" + ); + } + } + } + } else if is_queued_sv1_handshake_response { + // For non-notification messages, send if processing queued handshake responses + self.downstream_channel_state + .downstream_sv1_sender + .send(message.clone()) + .await + .map_err(|e| { + error!("Down: Failed to send queued message to downstream: {:?}", e); + TproxyError::ChannelErrorSender + })?; + } else { + // Neither handshake complete nor queued response - skip non-notification + // messages + debug!("Down: SV1 handshake not complete, skipping non-notification message"); + } + } + Err(e) => { + let downstream_id = self.downstream_data.super_safe_lock(|d| d.downstream_id); + error!( + "Sv1 message handler error for downstream {}: {:?}", + downstream_id, e + ); + return Err(TproxyError::BroadcastChannelErrorReceiver(e)); + } + } + + Ok(()) + } + + /// Handles messages received from the downstream SV1 miner. + /// + /// This method processes SV1 protocol messages sent by the miner, including: + /// - `mining.subscribe` - Subscription requests + /// - `mining.authorize` - Authorization requests + /// - `mining.submit` - Share submissions + /// - Other SV1 protocol messages + /// + /// The method delegates message processing to the downstream data handler, + /// which implements the SV1 protocol logic and generates appropriate responses. + /// Responses are sent back to the miner, while share submissions are forwarded + /// to the SV1 server for upstream processing. + pub async fn handle_downstream_message(self: Arc) -> Result<(), TproxyError> { + let message = match self + .downstream_channel_state + .downstream_sv1_receiver + .recv() + .await + { + Ok(msg) => msg, + Err(e) => { + error!("Error receiving downstream message: {:?}", e); + return Err(TproxyError::ChannelErrorReceiver(e)); + } + }; + + // Check if channel is established + let channel_established = self + .downstream_data + .super_safe_lock(|d| d.channel_id.is_some()); + + if !channel_established { + // Check if this is the first message (queue is empty) and send OpenChannel request + let is_first_message = self + .downstream_data + .super_safe_lock(|d| d.queued_sv1_handshake_messages.is_empty()); + + if is_first_message { + let downstream_id = self.downstream_data.super_safe_lock(|d| d.downstream_id); + self.downstream_channel_state + .sv1_server_sender + .send(DownstreamMessages::OpenChannel(downstream_id)) + .await + .map_err(|e| { + error!("Down: Failed to send OpenChannel request: {:?}", e); + TproxyError::ChannelErrorSender + })?; + debug!( + "Down: Sent OpenChannel request for downstream {}", + downstream_id + ); + } + + // Queue all messages until channel is established + debug!("Down: Queuing Sv1 message until channel is established"); + self.downstream_data.safe_lock(|d| { + d.queued_sv1_handshake_messages.push(message.clone()); + })?; + return Ok(()); + } + + // Channel is established, process message normally + let response = self + .downstream_data + .super_safe_lock(|data| data.handle_message(message.clone())); + + match response { + Ok(Some(response_msg)) => { + debug!( + "Down: Sending Sv1 message to downstream: {:?}", + response_msg + ); + self.downstream_channel_state + .downstream_sv1_sender + .send(response_msg.into()) + .await + .map_err(|e| { + error!("Down: Failed to send message to downstream: {:?}", e); + TproxyError::ChannelErrorSender + })?; + + // Check if this was an authorize message and handle sv1 handshake completion + if let v1::json_rpc::Message::StandardRequest(request) = &message { + if request.method == "mining.authorize" { + info!("Down: Handling mining.authorize after handshake completion"); + if let Err(e) = self.handle_sv1_handshake_completion().await { + error!("Down: Failed to handle handshake completion: {:?}", e); + return Err(e); + } + } + } + } + Ok(None) => { + // Message was handled but no response needed + } + Err(e) => { + error!("Down: Error handling downstream message: {:?}", e); + return Err(e.into()); + } + } + + // Check if there's a pending share to send to the Sv1Server + let pending_share = self + .downstream_data + .super_safe_lock(|d| d.pending_share.take()); + if let Some(share) = pending_share { + self.downstream_channel_state + .sv1_server_sender + .send(DownstreamMessages::SubmitShares(share)) + .await + .map_err(|e| { + error!("Down: Failed to send share to SV1 server: {:?}", e); + TproxyError::ChannelErrorSender + })?; + } + + Ok(()) + } + + /// Handles SV1 handshake completion after mining.authorize. + /// + /// This method is called when the downstream completes the SV1 handshake + /// (subscribe + authorize). It sends any cached messages in the correct order: + /// set_difficulty first, then notify. + async fn handle_sv1_handshake_completion(self: &Arc) -> Result<(), TproxyError> { + let (cached_set_difficulty, cached_notify) = self.downstream_data.super_safe_lock(|d| { + d.sv1_handshake_complete + .store(true, std::sync::atomic::Ordering::SeqCst); + (d.cached_set_difficulty.take(), d.cached_notify.take()) + }); + debug!("Down: SV1 handshake completed for downstream"); + + // Send cached messages in correct order: set_difficulty first, then notify + if let Some(set_difficulty_msg) = cached_set_difficulty { + debug!("Down: Sending cached mining.set_difficulty after handshake completion"); + self.downstream_channel_state + .downstream_sv1_sender + .send(set_difficulty_msg) + .await + .map_err(|e| { + error!( + "Down: Failed to send cached mining.set_difficulty to downstream: {:?}", + e + ); + TproxyError::ChannelErrorSender + })?; + + // Update target and hashrate after sending set_difficulty + self.downstream_data.super_safe_lock(|d| { + if let Some(new_target) = d.pending_target.take() { + d.target = new_target; + } + if let Some(new_hashrate) = d.pending_hashrate.take() { + d.hashrate = Some(new_hashrate); + } + }); + } + + if let Some(notify_msg) = cached_notify { + debug!("Down: Sending cached mining.notify after handshake completion"); + self.downstream_channel_state + .downstream_sv1_sender + .send(notify_msg) + .await + .map_err(|e| { + error!( + "Down: Failed to send cached mining.notify to downstream: {:?}", + e + ); + TproxyError::ChannelErrorSender + })?; + } + + Ok(()) + } +} diff --git a/roles/translator/src/lib/sv1/downstream/message_handler.rs b/roles/translator/src/lib/sv1/downstream/message_handler.rs new file mode 100644 index 0000000000..291221bd98 --- /dev/null +++ b/roles/translator/src/lib/sv1/downstream/message_handler.rs @@ -0,0 +1,159 @@ +use tracing::{debug, error, info, warn}; +use v1::{ + client_to_server, json_rpc, server_to_client, + utils::{Extranonce, HexU32Be}, + IsServer, +}; + +use crate::{ + sv1::downstream::{data::DownstreamData, SubmitShareWithChannelId}, + utils::validate_sv1_share, +}; + +// Implements `IsServer` for `Downstream` to handle the Sv1 messages. +impl IsServer<'static> for DownstreamData { + fn handle_configure( + &mut self, + request: &client_to_server::Configure, + ) -> (Option, Option) { + info!("Received mining.configure from Sv1 downstream"); + debug!("Down: Handling mining.configure: {:?}", request); + self.version_rolling_mask = request + .version_rolling_mask() + .map(|mask| HexU32Be(mask & 0x1FFFE000)); + self.version_rolling_min_bit = request.version_rolling_min_bit_count(); + + debug!( + "Negotiated version_rolling_mask is {:?}", + self.version_rolling_mask + ); + ( + Some(server_to_client::VersionRollingParams::new( + self.version_rolling_mask.clone().unwrap_or(HexU32Be(0)), + self.version_rolling_min_bit.clone().unwrap_or(HexU32Be(0)), + ).expect("Version mask invalid, automatic version mask selection not supported, please change it in crate::downstream::mod.rs")), + Some(false), + ) + } + + fn handle_subscribe(&self, request: &client_to_server::Subscribe) -> Vec<(String, String)> { + info!("Received mining.subscribe from Sv1 downstream"); + debug!("Down: Handling mining.subscribe: {:?}", request); + + let set_difficulty_sub = ( + "mining.set_difficulty".to_string(), + self.downstream_id.to_string(), + ); + + let notify_sub = ( + "mining.notify".to_string(), + "ae6812eb4cd7735a302a8a9dd95cf71f".to_string(), + ); + + vec![set_difficulty_sub, notify_sub] + } + + fn handle_authorize(&self, request: &client_to_server::Authorize) -> bool { + info!("Received mining.authorize from Sv1 downstream"); + debug!("Down: Handling mining.authorize: {:?}", request); + true + } + + fn handle_submit(&self, request: &client_to_server::Submit<'static>) -> bool { + if let Some(channel_id) = self.channel_id { + info!( + "Received mining.submit from SV1 downstream for channel id: {}", + channel_id + ); + let is_valid_share = validate_sv1_share( + request, + self.target.clone(), + self.extranonce1.clone(), + self.version_rolling_mask.clone(), + self.sv1_server_data.clone(), + channel_id, + ) + .unwrap_or(false); + if !is_valid_share { + error!("Invalid share for channel id: {}", channel_id); + return false; + } + let to_send: SubmitShareWithChannelId = SubmitShareWithChannelId { + channel_id, + downstream_id: self.downstream_id, + share: request.clone(), + extranonce: self.extranonce1.clone(), + extranonce2_len: self.extranonce2_len, + version_rolling_mask: self.version_rolling_mask.clone(), + job_version: self.last_job_version_field, + }; + // Store the share to be sent to the Sv1Server + self.pending_share.replace(Some(to_send)); + true + } else { + error!("Cannot submit share: channel_id is None (waiting for OpenExtendedMiningChannelSuccess)"); + false + } + } + + /// Indicates to the server that the client supports the mining.set_extranonce method. + fn handle_extranonce_subscribe(&self) {} + + /// Checks if a Downstream role is authorized. + fn is_authorized(&self, name: &str) -> bool { + self.authorized_worker_name == *name + } + + /// Authorizes a Downstream role. + fn authorize(&mut self, name: &str) { + let name: String = name.into(); + if !self.is_authorized(&name) { + self.authorized_worker_name = name.to_string(); + } + } + + /// Sets the `extranonce1` field sent in the SV1 `mining.notify` message to the value specified + /// by the SV2 `OpenExtendedMiningChannelSuccess` message sent from the Upstream role. + fn set_extranonce1( + &mut self, + _extranonce1: Option>, + ) -> Extranonce<'static> { + self.extranonce1.clone().try_into().unwrap() + } + + /// Returns the `Downstream`'s `extranonce1` value. + fn extranonce1(&self) -> Extranonce<'static> { + self.extranonce1.clone().try_into().unwrap() + } + + /// Sets the `extranonce2_size` field sent in the SV1 `mining.notify` message to the value + /// specified by the SV2 `OpenExtendedMiningChannelSuccess` message sent from the Upstream role. + fn set_extranonce2_size(&mut self, _extra_nonce2_size: Option) -> usize { + self.extranonce2_len + } + + /// Returns the `Downstream`'s `extranonce2_size` value. + fn extranonce2_size(&self) -> usize { + self.extranonce2_len + } + + /// Returns the version rolling mask. + fn version_rolling_mask(&self) -> Option { + self.version_rolling_mask.clone() + } + + /// Sets the version rolling mask. + fn set_version_rolling_mask(&mut self, mask: Option) { + self.version_rolling_mask = mask; + } + + /// Sets the minimum version rolling bit. + fn set_version_rolling_min_bit(&mut self, mask: Option) { + self.version_rolling_min_bit = mask + } + + fn notify(&'_ mut self) -> Result> { + warn!("notify() called on DownstreamData - this method is not implemented for the translator proxy"); + Err(v1::error::Error::UnexpectedMessage("notify".to_string())) + } +} diff --git a/roles/translator/src/lib/sv1/downstream/mod.rs b/roles/translator/src/lib/sv1/downstream/mod.rs new file mode 100644 index 0000000000..900f77b538 --- /dev/null +++ b/roles/translator/src/lib/sv1/downstream/mod.rs @@ -0,0 +1,43 @@ +pub(super) mod channel; +pub(super) mod data; +pub mod downstream; +mod message_handler; + +use v1::{client_to_server::Submit, utils::HexU32Be}; + +/// Messages sent from downstream handling logic to the SV1 server. +/// +/// This enum defines the types of messages that downstream connections can send +/// to the central SV1 server for processing and forwarding to upstream. +#[derive(Debug)] +pub enum DownstreamMessages { + /// Represents a submitted share from a downstream miner, + /// wrapped with the relevant channel ID. + SubmitShares(SubmitShareWithChannelId), + /// Request to open an extended mining channel for a downstream that just sent its first + /// message. + OpenChannel(u32), // downstream_id +} + +/// A wrapper around a `mining.submit` message with additional channel information. +/// +/// This struct contains all the necessary information to process a share submission +/// from an SV1 miner, including the share data itself and metadata needed for +/// proper routing and validation. +#[derive(Debug, Clone)] +pub struct SubmitShareWithChannelId { + /// The SV2 channel ID this share belongs to + pub channel_id: u32, + /// The downstream connection ID that submitted this share + pub downstream_id: u32, + /// The actual SV1 share submission data + pub share: Submit<'static>, + /// The complete extranonce used for this share + pub extranonce: Vec, + /// The length of the extranonce2 field + pub extranonce2_len: usize, + /// Optional version rolling mask for the share + pub version_rolling_mask: Option, + /// The version field from the job, used for validation + pub job_version: Option, +} diff --git a/roles/translator/src/lib/sv1/mod.rs b/roles/translator/src/lib/sv1/mod.rs new file mode 100644 index 0000000000..0b62d78494 --- /dev/null +++ b/roles/translator/src/lib/sv1/mod.rs @@ -0,0 +1,16 @@ +//! ## Downstream SV1 Module +//! +//! This module defines the structures, messages, and utility functions +//! used for handling the downstream connection with SV1 mining clients. +//! +//! It includes definitions for messages exchanged with a Bridge component, +//! structures for submitting shares and updating targets, and constants +//! and functions for managing client interactions. +//! +//! The module is organized into the following sub-modules: +//! - [`diff_management`]: (Declared here, likely contains downstream difficulty logic) +//! - [`downstream`]: Defines the core [`Downstream`] struct and its functionalities. + +pub mod downstream; +pub mod sv1_server; +pub use sv1_server::sv1_server::Sv1Server; diff --git a/roles/translator/src/lib/sv1/sv1_server/channel.rs b/roles/translator/src/lib/sv1/sv1_server/channel.rs new file mode 100644 index 0000000000..94c5feb235 --- /dev/null +++ b/roles/translator/src/lib/sv1/sv1_server/channel.rs @@ -0,0 +1,41 @@ +use crate::sv1::downstream::DownstreamMessages; +use async_channel::{unbounded, Receiver, Sender}; +use stratum_common::roles_logic_sv2::parsers_sv2::Mining; + +use tokio::sync::broadcast; +use v1::json_rpc; + +pub struct Sv1ServerChannelState { + pub sv1_server_to_downstream_sender: broadcast::Sender<(u32, Option, json_rpc::Message)>, + pub downstream_to_sv1_server_sender: Sender, + pub downstream_to_sv1_server_receiver: Receiver, + pub channel_manager_receiver: Receiver>, + pub channel_manager_sender: Sender>, +} + +impl Sv1ServerChannelState { + pub fn new( + channel_manager_receiver: Receiver>, + channel_manager_sender: Sender>, + ) -> Self { + let (sv1_server_to_downstream_sender, _) = broadcast::channel(100); + let (downstream_to_sv1_server_sender, downstream_to_sv1_server_receiver) = unbounded(); + + Self { + sv1_server_to_downstream_sender, + downstream_to_sv1_server_receiver, + downstream_to_sv1_server_sender, + channel_manager_receiver, + channel_manager_sender, + } + } + + pub fn drop(&self) { + self.channel_manager_receiver.close(); + self.channel_manager_sender.close(); + self.downstream_to_sv1_server_receiver.close(); + self.downstream_to_sv1_server_sender.close(); + self.channel_manager_receiver.close(); + self.channel_manager_sender.close(); + } +} diff --git a/roles/translator/src/lib/sv1/sv1_server/data.rs b/roles/translator/src/lib/sv1/sv1_server/data.rs new file mode 100644 index 0000000000..fcba5b0e3d --- /dev/null +++ b/roles/translator/src/lib/sv1/sv1_server/data.rs @@ -0,0 +1,48 @@ +use crate::sv1::downstream::downstream::Downstream; +use std::{ + collections::HashMap, + sync::{atomic::AtomicU32, Arc, RwLock}, +}; +use stratum_common::roles_logic_sv2::{ + channels_sv2::vardiff::classic::VardiffState, + mining_sv2::{SetNewPrevHash, Target}, +}; +use v1::server_to_client; + +#[derive(Debug, Clone)] +pub struct PendingTargetUpdate { + pub downstream_id: u32, + pub new_target: Target, + pub new_hashrate: f32, +} + +#[derive(Debug)] +pub struct Sv1ServerData { + pub downstreams: HashMap>, + pub vardiff: HashMap>>, + pub prevhash: Option>, + pub downstream_id_factory: AtomicU32, + /// Job storage for aggregated mode - all Sv1 downstreams share the same jobs + pub aggregated_valid_jobs: Option>>, + /// Job storage for non-aggregated mode - each Sv1 downstream has its own jobs + pub non_aggregated_valid_jobs: Option>>>, + /// Tracks pending target updates that are waiting for SetTarget response from upstream + pub pending_target_updates: Vec, + /// The initial target used when opening channels - used when no downstreams remain + pub initial_target: Option, +} + +impl Sv1ServerData { + pub fn new(aggregate_channels: bool) -> Self { + Self { + downstreams: HashMap::new(), + vardiff: HashMap::new(), + prevhash: None, + downstream_id_factory: AtomicU32::new(0), + aggregated_valid_jobs: aggregate_channels.then(Vec::new), + non_aggregated_valid_jobs: (!aggregate_channels).then(HashMap::new), + pending_target_updates: Vec::new(), + initial_target: None, + } + } +} diff --git a/roles/translator/src/lib/sv1/sv1_server/difficulty_manager.rs b/roles/translator/src/lib/sv1/sv1_server/difficulty_manager.rs new file mode 100644 index 0000000000..5face5ae17 --- /dev/null +++ b/roles/translator/src/lib/sv1/sv1_server/difficulty_manager.rs @@ -0,0 +1,755 @@ +use crate::{ + sv1::sv1_server::data::{PendingTargetUpdate, Sv1ServerData}, + utils::ShutdownMessage, +}; +use async_channel::Sender; +use std::{collections::HashMap, sync::Arc, time::Duration}; +use stratum_common::roles_logic_sv2::{ + channels_sv2::{target::hash_rate_to_target, Vardiff}, + mining_sv2::{SetTarget, Target, UpdateChannel}, + parsers_sv2::Mining, + utils::Mutex, +}; +use stratum_translation::sv2_to_sv1::build_sv1_set_difficulty_from_sv2_target; +use tokio::{sync::broadcast, time}; +use tracing::{debug, error, info, trace, warn}; +use v1::json_rpc; + +/// Handles all variable difficulty adjustment logic for the SV1 server. +/// +/// This module contains the core vardiff implementation that: +/// - Periodically adjusts difficulty targets based on share submission rates +/// - Manages the relationship between upstream and downstream targets +/// - Handles both aggregated and non-aggregated channel modes +/// - Coordinates with the channel manager for target updates +pub struct DifficultyManager { + shares_per_minute: f32, + is_aggregated: bool, +} + +impl DifficultyManager { + /// Creates a new difficulty manager instance. + /// + /// # Arguments + /// * `shares_per_minute` - Target shares per minute for difficulty adjustment + /// * `is_aggregated` - Whether channels are operating in aggregated mode + pub fn new(shares_per_minute: f32, is_aggregated: bool) -> Self { + Self { + shares_per_minute, + is_aggregated, + } + } + + /// Spawns the variable difficulty adjustment loop. + /// + /// This method implements the SV1 server's variable difficulty logic for all downstreams. + /// Every 60 seconds, this method updates the difficulty state for each downstream. + pub async fn spawn_vardiff_loop( + sv1_server_data: Arc>, + channel_manager_sender: Sender>, + sv1_server_to_downstream_sender: broadcast::Sender<(u32, Option, json_rpc::Message)>, + shares_per_minute: f32, + is_aggregated: bool, + mut notify_shutdown: broadcast::Receiver, + shutdown_complete_tx: tokio::sync::mpsc::Sender<()>, + ) { + let difficulty_manager = DifficultyManager::new(shares_per_minute, is_aggregated); + + 'vardiff_loop: loop { + tokio::select! { + message = notify_shutdown.recv() => { + match message { + Ok(ShutdownMessage::ShutdownAll) => { + debug!("SV1 Server: Vardiff loop received shutdown signal. Exiting."); + break 'vardiff_loop; + } + Ok(ShutdownMessage::DownstreamShutdown(downstream_id)) => { + sv1_server_data.super_safe_lock(|d| { + d.vardiff.remove(&downstream_id); + }); + } + Ok(ShutdownMessage::DownstreamShutdownAll) => { + sv1_server_data.super_safe_lock(|d|{ + d.vardiff = HashMap::new(); + d.downstreams = HashMap::new(); + }); + info!("🔌 All downstreams removed from sv1 server as upstream changed"); + + // In aggregated mode, send UpdateChannel to reflect the new state (no downstreams) + Self::send_update_channel_on_downstream_state_change( + &sv1_server_data, + &channel_manager_sender, + is_aggregated, + ).await; + } + Ok(ShutdownMessage::UpstreamReconnectedResetAndShutdownDownstreams) => { + sv1_server_data.super_safe_lock(|d|{ + d.vardiff = HashMap::new(); + d.downstreams = HashMap::new(); + }); + info!("🔌 All downstreams removed from sv1 server as upstream reconnected"); + + // In aggregated mode, send UpdateChannel to reflect the new state (no downstreams) + Self::send_update_channel_on_downstream_state_change( + &sv1_server_data, + &channel_manager_sender, + is_aggregated, + ).await; + } + _ => {} + } + } + _ = time::sleep(Duration::from_secs(60)) => { + difficulty_manager.handle_vardiff_updates( + &sv1_server_data, + &channel_manager_sender, + &sv1_server_to_downstream_sender, + ).await; + } + } + } + drop(shutdown_complete_tx); + debug!("SV1 Server: Vardiff loop exited."); + } + + /// Handles variable difficulty adjustments for all connected downstreams. + /// + /// This method implements the core vardiff logic: + /// 1. For each downstream, calculate if a target update is needed + /// 2. Always send UpdateChannel to keep upstream informed + /// 3. Compare new target with upstream target to decide when to send set_difficulty: + /// - If new_target >= upstream_target: send set_difficulty immediately + /// - If new_target < upstream_target: wait for SetTarget response before sending + /// set_difficulty + /// 4. Handle aggregated vs non-aggregated modes for UpdateChannel messages + async fn handle_vardiff_updates( + &self, + sv1_server_data: &Arc>, + channel_manager_sender: &Sender>, + sv1_server_to_downstream_sender: &broadcast::Sender<(u32, Option, json_rpc::Message)>, + ) { + let vardiff_map = sv1_server_data.super_safe_lock(|v| v.vardiff.clone()); + let mut immediate_updates = Vec::new(); + let mut all_updates = Vec::new(); // All updates will generate UpdateChannel messages + + // Process each downstream and determine update strategy + for (downstream_id, vardiff_state) in vardiff_map.iter() { + debug!("Updating vardiff for downstream_id: {}", downstream_id); + let mut vardiff = vardiff_state.write().unwrap(); + + // Get current state from downstream + let Some((channel_id, hashrate, target, upstream_target)) = sv1_server_data + .super_safe_lock(|data| { + data.downstreams.get(downstream_id).and_then(|ds| { + ds.downstream_data.super_safe_lock(|d| { + Some(( + d.channel_id, + d.hashrate.unwrap(), /* It's safe to unwrap because we know that + * the downstream has a hashrate (we are + * doing vardiff) */ + d.target.clone(), + d.upstream_target.clone(), + )) + }) + }) + }) + else { + continue; + }; + + let Some(channel_id) = channel_id else { + error!("Channel id is none for downstream_id: {}", downstream_id); + continue; + }; + + let new_hashrate_opt = vardiff.try_vardiff(hashrate, &target, self.shares_per_minute); + + if let Ok(Some(new_hashrate)) = new_hashrate_opt { + // Calculate new target based on new hashrate + let new_target: Target = + match hash_rate_to_target(new_hashrate as f64, self.shares_per_minute as f64) { + Ok(target) => target.into(), + Err(e) => { + error!( + "Failed to calculate target for hashrate {}: {:?}", + new_hashrate, e + ); + continue; + } + }; + + // Always update the downstream's pending target and hashrate + _ = sv1_server_data.safe_lock(|dmap| { + if let Some(d) = dmap.downstreams.get(downstream_id) { + _ = d.downstream_data.safe_lock(|d| { + d.set_pending_target(new_target.clone()); + d.set_pending_hashrate(Some(new_hashrate)); + }); + } + }); + + // All updates will be sent as UpdateChannel messages + all_updates.push((*downstream_id, channel_id, new_target.clone(), new_hashrate)); + + // Determine if we should send set_difficulty immediately or wait + match upstream_target { + Some(upstream_target) => { + if new_target >= upstream_target { + // Case 1: new_target >= upstream_target, send set_difficulty + // immediately + trace!( + "✅ Target comparison: new_target ({:?}) >= upstream_target ({:?}) for downstream {}, will send set_difficulty immediately", + new_target, upstream_target, downstream_id + ); + immediate_updates.push(( + channel_id, + Some(*downstream_id), + new_target.clone(), + )); + } else { + // Case 2: new_target < upstream_target, delay set_difficulty until + // SetTarget + trace!( + "â³ Target comparison: new_target ({:?}) < upstream_target ({:?}) for downstream {}, will delay set_difficulty until SetTarget", + new_target, upstream_target, downstream_id + ); + // Store as pending update for when SetTarget arrives + sv1_server_data.super_safe_lock(|data| { + data.pending_target_updates.push(PendingTargetUpdate { + downstream_id: *downstream_id, + new_target: new_target.clone(), + new_hashrate, + }); + }); + } + } + None => { + // No upstream target set yet, send set_difficulty immediately as fallback + trace!( + "No upstream target set for downstream {}, will send set_difficulty immediately", + downstream_id + ); + immediate_updates.push(( + channel_id, + Some(*downstream_id), + new_target.clone(), + )); + } + } + } + } + + // Send UpdateChannel messages for ALL updates (both immediate and delayed) + if !all_updates.is_empty() { + self.send_update_channel_messages(all_updates, sv1_server_data, channel_manager_sender) + .await; + } + + // Process immediate set_difficulty updates (for new_target >= upstream_target) + for (channel_id, downstream_id, target) in immediate_updates { + // Send set_difficulty message immediately + if let Ok(set_difficulty_msg) = build_sv1_set_difficulty_from_sv2_target(target) { + if let Err(e) = sv1_server_to_downstream_sender.send(( + channel_id, + downstream_id, + set_difficulty_msg, + )) { + error!( + "Failed to send immediate SetDifficulty message to downstream {}: {:?}", + downstream_id.unwrap_or(0), + e + ); + } else { + trace!( + "Sent immediate SetDifficulty to downstream {} (new_target >= upstream_target)", + downstream_id.unwrap_or(0) + ); + } + } + } + } + + /// Sends UpdateChannel messages for all target updates. + /// + /// Always sends UpdateChannel to keep upstream informed about target changes. + /// Handles both aggregated and non-aggregated modes: + /// - Aggregated: Send single UpdateChannel with minimum target and sum of hashrates + /// - Non-aggregated: Send individual UpdateChannel for each downstream + async fn send_update_channel_messages( + &self, + all_updates: Vec<(u32, u32, Target, f32)>, /* (downstream_id, channel_id, new_target, + * new_hashrate) */ + sv1_server_data: &Arc>, + channel_manager_sender: &Sender>, + ) { + if self.is_aggregated { + // Aggregated mode: Send single UpdateChannel with minimum target and total hashrate of + // ALL downstreams + if let Some((_, channel_id, _, _)) = all_updates.first() { + // Get minimum target among ALL downstreams, not just the ones with updates + let min_target = sv1_server_data.super_safe_lock(|data| { + data.downstreams + .values() + .map(|downstream| { + downstream.downstream_data.super_safe_lock(|d| { + // Use pending_target if available, otherwise current target + d.pending_target.as_ref().unwrap_or(&d.target).clone() + }) + }) + .min() + .expect("At least one downstream should exist") + }); + + // Get total hashrate of ALL downstreams, not just the ones with updates + let total_hashrate: f32 = sv1_server_data.super_safe_lock(|data| { + data.downstreams + .values() + .map(|downstream| { + downstream.downstream_data.super_safe_lock(|d| { + // Use pending_hashrate if available, otherwise current hashrate + // It's safe to unwrap because we know that the downstream has a + // hashrate (we are doing vardiff) + d.pending_hashrate.unwrap_or(d.hashrate.unwrap()) + }) + }) + .sum() + }); + + let update_channel = UpdateChannel { + channel_id: *channel_id, + nominal_hash_rate: total_hashrate, + maximum_target: min_target.clone().into(), + }; + + debug!( + "Sending UpdateChannel for aggregated mode: channel_id={}, total_hashrate={} (all {} downstreams), min_target={:?}, vardiff_updates={}", + channel_id, total_hashrate, + sv1_server_data.super_safe_lock(|data| data.downstreams.len()), + &min_target, all_updates.len() + ); + + if let Err(e) = channel_manager_sender + .send(Mining::UpdateChannel(update_channel)) + .await + { + error!("Failed to send UpdateChannel message: {:?}", e); + } + } + } else { + // Non-aggregated mode: Send individual UpdateChannel for each downstream + for (downstream_id, channel_id, new_target, new_hashrate) in &all_updates { + let update_channel = UpdateChannel { + channel_id: *channel_id, + nominal_hash_rate: *new_hashrate, + maximum_target: new_target.clone().into(), + }; + + debug!( + "Sending UpdateChannel for downstream {}: channel_id={}, hashrate={}, target={:?}", + downstream_id, channel_id, new_hashrate, new_target + ); + + if let Err(e) = channel_manager_sender + .send(Mining::UpdateChannel(update_channel)) + .await + { + error!( + "Failed to send UpdateChannel message for downstream {}: {:?}", + downstream_id, e + ); + } + } + } + } + + /// Handles SetTarget messages from the ChannelManager. + /// + /// Aggregated mode: Single SetTarget updates all downstreams and processes all pending updates + /// Non-aggregated mode: Each SetTarget updates one specific downstream and processes its + /// pending update + pub async fn handle_set_target_message( + set_target: SetTarget<'_>, + sv1_server_data: &Arc>, + channel_manager_sender: &Sender>, + sv1_server_to_downstream_sender: &broadcast::Sender<(u32, Option, json_rpc::Message)>, + is_aggregated: bool, + ) { + let new_upstream_target: Target = set_target.maximum_target.clone().into(); + debug!( + "Received SetTarget for channel {}: new_upstream_target = {:?}", + set_target.channel_id, new_upstream_target + ); + + if is_aggregated { + Self::handle_aggregated_set_target( + new_upstream_target, + set_target.channel_id, + sv1_server_data, + channel_manager_sender, + sv1_server_to_downstream_sender, + ) + .await; + } else { + Self::handle_non_aggregated_set_target( + set_target.channel_id, + new_upstream_target, + sv1_server_data, + channel_manager_sender, + sv1_server_to_downstream_sender, + ) + .await; + } + } + + /// Handles SetTarget in aggregated mode. + /// Updates all downstreams and processes all pending set_difficulty messages. + async fn handle_aggregated_set_target( + new_upstream_target: Target, + channel_id: u32, + sv1_server_data: &Arc>, + _channel_manager_sender: &Sender>, + sv1_server_to_downstream_sender: &broadcast::Sender<(u32, Option, json_rpc::Message)>, + ) { + debug!("Aggregated mode: Updating upstream target for all downstreams"); + + // Update upstream target for ALL downstreams + let downstream_ids: Vec = + sv1_server_data.super_safe_lock(|data| data.downstreams.keys().cloned().collect()); + + for downstream_id in downstream_ids { + _ = sv1_server_data.safe_lock(|data| { + if let Some(downstream) = data.downstreams.get(&downstream_id) { + _ = downstream.downstream_data.safe_lock(|d| { + d.set_upstream_target(new_upstream_target.clone()); + }); + } + }); + } + + // Process ALL pending difficulty updates that can now be sent downstream + let applicable_updates = Self::get_pending_difficulty_updates( + new_upstream_target, + None, + channel_id, + sv1_server_data, + ); + Self::send_pending_set_difficulty_messages_to_downstream( + applicable_updates, + sv1_server_data, + sv1_server_to_downstream_sender, + ) + .await; + } + + /// Handles SetTarget in non-aggregated mode. + /// Updates the specific downstream and processes its pending set_difficulty message. + async fn handle_non_aggregated_set_target( + channel_id: u32, + new_upstream_target: Target, + sv1_server_data: &Arc>, + _channel_manager_sender: &Sender>, + sv1_server_to_downstream_sender: &broadcast::Sender<(u32, Option, json_rpc::Message)>, + ) { + debug!( + "Non-aggregated mode: Processing SetTarget for channel {}", + channel_id + ); + + let affected_downstream = sv1_server_data.super_safe_lock(|data| { + data.downstreams + .iter() + .find_map(|(downstream_id, downstream)| { + downstream.downstream_data.super_safe_lock(|d| { + if d.channel_id == Some(channel_id) { + Some(*downstream_id) + } else { + None + } + }) + }) + }); + + if let Some(downstream_id) = affected_downstream { + // Update upstream target for this specific downstream + _ = sv1_server_data.safe_lock(|data| { + if let Some(downstream) = data.downstreams.get(&downstream_id) { + _ = downstream.downstream_data.safe_lock(|d| { + d.set_upstream_target(new_upstream_target.clone()); + }); + } + }); + trace!("Updated upstream target for downstream {}", downstream_id); + + // Process pending difficulty updates for this specific downstream only + let applicable_updates = Self::get_pending_difficulty_updates( + new_upstream_target, + Some(downstream_id), + channel_id, + sv1_server_data, + ); + Self::send_pending_set_difficulty_messages_to_downstream( + applicable_updates, + sv1_server_data, + sv1_server_to_downstream_sender, + ) + .await; + } else { + warn!("No downstream found for channel {}", channel_id); + } + } + + /// Gets pending updates that can now be applied based on the new upstream target. + /// If downstream_id is provided, only returns updates for that specific downstream. + /// Logs a warning if the upstream target is higher than any requested target. + fn get_pending_difficulty_updates( + new_upstream_target: Target, + downstream_id: Option, + channel_id: u32, + sv1_server_data: &Arc>, + ) -> Vec { + let mut applicable_updates = Vec::new(); + + sv1_server_data.super_safe_lock(|data| { + data.pending_target_updates.retain(|pending_update| { + // Check if we should process this update + let should_process = match downstream_id { + Some(downstream_id) => pending_update.downstream_id == downstream_id, + None => true, // Process all in aggregated mode + }; + + if should_process { + if pending_update.new_target >= new_upstream_target { + // Target is acceptable, can apply immediately + applicable_updates.push(pending_update.clone()); + false // remove from pending list + } else { + // WARNING: Upstream gave us a target higher than what we requested + error!( + "⌠Protocol issue: SetTarget response has target ({:?}) which is higher than requested target ({:?}) in UpdateChannel for channel {:?}. Ignoring this pending update for downstream {:?}.", + new_upstream_target, pending_update.new_target, channel_id, pending_update.downstream_id + ); + false // remove from pending list (don't keep invalid requests) + } + } else { + true // keep in pending list (not relevant for this SetTarget) + } + }); + }); + applicable_updates + } + + /// Sends set_difficulty messages for all applicable pending updates. + async fn send_pending_set_difficulty_messages_to_downstream( + difficulty_updates: Vec, + sv1_server_data: &Arc>, + sv1_server_to_downstream_sender: &broadcast::Sender<(u32, Option, json_rpc::Message)>, + ) { + for pending_update in &difficulty_updates { + // Get channel_id for this downstream + let channel_id = sv1_server_data.super_safe_lock(|data| { + data.downstreams + .get(&pending_update.downstream_id) + .and_then(|ds| ds.downstream_data.super_safe_lock(|d| d.channel_id)) + }); + + if let Some(channel_id) = channel_id { + // Send set_difficulty message + if let Ok(set_difficulty_msg) = + build_sv1_set_difficulty_from_sv2_target(pending_update.new_target.clone()) + { + if let Err(e) = sv1_server_to_downstream_sender.send(( + channel_id, + Some(pending_update.downstream_id), + set_difficulty_msg, + )) { + error!( + "Failed to send SetDifficulty to downstream {}: {:?}", + pending_update.downstream_id, e + ); + } else { + trace!( + "Sent SetDifficulty to downstream {}", + pending_update.downstream_id + ); + } + } + } + } + } + + /// Sends an UpdateChannel message for aggregated mode when downstream state changes + /// (e.g., disconnect). Calculates total hashrate and minimum target among all remaining + /// downstreams. + pub async fn send_update_channel_on_downstream_state_change( + sv1_server_data: &Arc>, + channel_manager_sender: &Sender>, + is_aggregated: bool, + ) { + if !is_aggregated { + return; // Only applies to aggregated mode + } + + let (total_hashrate, min_target, channel_id, downstream_count) = sv1_server_data + .super_safe_lock(|data| { + // Hardcoded channel_id 0 (the ChannelManager will set this channel_id to the + // upstream extended channel id) + let channel_id = 0; + + let total_hashrate: f32 = data + .downstreams + .values() + .map(|downstream| { + downstream.downstream_data.super_safe_lock(|d| { + // Use pending_hashrate if available, otherwise current hashrate + // It's safe to unwrap because we know that the downstream has a + // hashrate (we are doing vardiff) + d.pending_hashrate.unwrap_or(d.hashrate.unwrap()) + }) + }) + .sum(); + + let min_target = data + .downstreams + .values() + .map(|downstream| { + downstream.downstream_data.super_safe_lock(|d| { + // Use pending_target if available, otherwise current target + d.pending_target.as_ref().unwrap_or(&d.target).clone() + }) + }) + .min(); + + ( + total_hashrate, + min_target, + Some(channel_id), + data.downstreams.len(), + ) + }); + + if let (Some(min_target), Some(channel_id)) = (min_target, channel_id) { + let update_channel = UpdateChannel { + channel_id, + nominal_hash_rate: total_hashrate, + maximum_target: min_target.clone().into(), + }; + + if let Err(e) = channel_manager_sender + .send(Mining::UpdateChannel(update_channel)) + .await + { + error!( + "Failed to send UpdateChannel message after downstream state change: {:?}", + e + ); + } + } else if downstream_count == 0 { + // No downstreams remaining, send UpdateChannel with maximum possible target + let update_channel = UpdateChannel { + channel_id: 0, + nominal_hash_rate: 0.0, // No hashrate when no downstreams + maximum_target: [0xFF; 32].into(), + }; + + if let Err(e) = channel_manager_sender + .send(Mining::UpdateChannel(update_channel)) + .await + { + error!( + "Failed to send UpdateChannel message with maximum target: {:?}", + e + ); + } + } else { + warn!("Cannot send UpdateChannel after downstream state change: no downstreams remaining or no channel_id"); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::sv1::sv1_server::data::Sv1ServerData; + use async_channel::unbounded; + use std::sync::Arc; + + fn create_test_difficulty_manager() -> DifficultyManager { + DifficultyManager::new(5.0, true) // 5 shares per minute, aggregated mode + } + + fn create_test_sv1_server_data() -> Arc> { + let data = Sv1ServerData::new(true); // aggregated mode + Arc::new(Mutex::new(data)) + } + + #[test] + fn test_difficulty_manager_creation() { + let manager = create_test_difficulty_manager(); + assert_eq!(manager.shares_per_minute, 5.0); + assert!(manager.is_aggregated); + + let non_agg_manager = DifficultyManager::new(10.0, false); + assert_eq!(non_agg_manager.shares_per_minute, 10.0); + assert!(!non_agg_manager.is_aggregated); + } + + #[tokio::test] + async fn test_send_update_channel_on_downstream_state_change_aggregated() { + let sv1_server_data = create_test_sv1_server_data(); + let (sender, receiver) = unbounded(); + + // Test with no downstreams + DifficultyManager::send_update_channel_on_downstream_state_change( + &sv1_server_data, + &sender, + true, // aggregated + ) + .await; + + // Should send UpdateChannel with maximum target when no downstreams + let received_message = receiver + .try_recv() + .expect("Should receive UpdateChannel message"); + if let Mining::UpdateChannel(update_channel) = received_message { + assert_eq!(update_channel.channel_id, 0); + assert_eq!(update_channel.nominal_hash_rate, 0.0); + assert_eq!(update_channel.maximum_target, [0xFF; 32].into()); + } else { + panic!( + "Expected UpdateChannel message, got: {:?}", + received_message + ); + } + } + + #[tokio::test] + async fn test_send_update_channel_on_downstream_state_change_non_aggregated() { + let sv1_server_data = create_test_sv1_server_data(); + let (sender, _receiver) = unbounded(); + + DifficultyManager::send_update_channel_on_downstream_state_change( + &sv1_server_data, + &sender, + false, // non-aggregated + ) + .await; + + // Non-aggregated mode should return early and not crash + } + + #[test] + fn test_get_pending_difficulty_updates_basic() { + let sv1_server_data = create_test_sv1_server_data(); + let upstream_target: Target = hash_rate_to_target(150.0, 5.0).unwrap().into(); + + // Test with empty pending updates + let applicable_updates = DifficultyManager::get_pending_difficulty_updates( + upstream_target, + None, // All downstreams + 1, // channel_id + &sv1_server_data, + ); + + assert_eq!(applicable_updates.len(), 0); + } +} diff --git a/roles/translator/src/lib/sv1/sv1_server/mod.rs b/roles/translator/src/lib/sv1/sv1_server/mod.rs new file mode 100644 index 0000000000..4491b592cc --- /dev/null +++ b/roles/translator/src/lib/sv1/sv1_server/mod.rs @@ -0,0 +1,4 @@ +pub(super) mod channel; +pub mod data; +pub mod difficulty_manager; +pub mod sv1_server; diff --git a/roles/translator/src/lib/sv1/sv1_server/sv1_server.rs b/roles/translator/src/lib/sv1/sv1_server/sv1_server.rs new file mode 100644 index 0000000000..23fcfc57ac --- /dev/null +++ b/roles/translator/src/lib/sv1/sv1_server/sv1_server.rs @@ -0,0 +1,1023 @@ +use crate::{ + config::TranslatorConfig, + error::TproxyError, + status::{handle_error, Status, StatusSender}, + sv1::{ + downstream::{downstream::Downstream, DownstreamMessages}, + sv1_server::{ + channel::Sv1ServerChannelState, data::Sv1ServerData, + difficulty_manager::DifficultyManager, + }, + }, + task_manager::TaskManager, + utils::ShutdownMessage, +}; +use async_channel::{Receiver, Sender}; +use network_helpers_sv2::{codec_sv2::binary_sv2::Str0255, sv1_connection::ConnectionSV1}; +use std::{ + collections::HashMap, + net::SocketAddr, + sync::{ + atomic::{AtomicBool, AtomicU32, Ordering}, + Arc, RwLock, + }, +}; +use stratum_common::roles_logic_sv2::{ + channels_sv2::{target::hash_rate_to_target, Vardiff, VardiffState}, + mining_sv2::{CloseChannel, SetTarget, Target}, + parsers_sv2::Mining, + utils::Mutex, +}; +use stratum_translation::{ + sv1_to_sv2::{ + build_sv2_open_extended_mining_channel, build_sv2_submit_shares_extended_from_sv1_submit, + }, + sv2_to_sv1::{build_sv1_notify_from_sv2, build_sv1_set_difficulty_from_sv2_target}, +}; +use tokio::{ + net::TcpListener, + sync::{broadcast, mpsc}, +}; +use tracing::{debug, error, info, warn}; +use v1::IsServer; + +/// SV1 server that handles connections from SV1 miners. +/// +/// This struct manages the SV1 server component of the translator, which: +/// - Accepts connections from SV1 miners +/// - Manages difficulty adjustment for connected miners +/// - Coordinates with the SV2 channel manager for upstream communication +/// - Tracks mining jobs and share submissions +/// +/// The server maintains state for multiple downstream connections and implements +/// variable difficulty adjustment based on share submission rates. +pub struct Sv1Server { + sv1_server_channel_state: Sv1ServerChannelState, + sv1_server_data: Arc>, + shares_per_minute: f32, + listener_addr: SocketAddr, + config: TranslatorConfig, + clean_job: AtomicBool, + sequence_counter: AtomicU32, + miner_counter: AtomicU32, +} + +impl Sv1Server { + /// Drops the server's channel state, cleaning up resources. + pub fn drop(&self) { + self.sv1_server_channel_state.drop(); + } + + /// Creates a new SV1 server instance. + /// + /// # Arguments + /// * `listener_addr` - The socket address to bind the server to + /// * `channel_manager_receiver` - Channel to receive messages from the channel manager + /// * `channel_manager_sender` - Channel to send messages to the channel manager + /// * `config` - Configuration settings for the translator + /// + /// # Returns + /// A new Sv1Server instance ready to accept connections + pub fn new( + listener_addr: SocketAddr, + channel_manager_receiver: Receiver>, + channel_manager_sender: Sender>, + config: TranslatorConfig, + ) -> Self { + let shares_per_minute = config.downstream_difficulty_config.shares_per_minute; + let sv1_server_channel_state = + Sv1ServerChannelState::new(channel_manager_receiver, channel_manager_sender); + let sv1_server_data = Arc::new(Mutex::new(Sv1ServerData::new(config.aggregate_channels))); + Self { + sv1_server_channel_state, + sv1_server_data, + config, + listener_addr, + shares_per_minute, + clean_job: AtomicBool::new(true), + miner_counter: AtomicU32::new(0), + sequence_counter: AtomicU32::new(0), + } + } + + /// Starts the SV1 server and begins accepting connections. + /// + /// This method: + /// - Binds to the configured listening address + /// - Spawns the variable difficulty adjustment loop + /// - Enters the main event loop to handle: + /// - New miner connections + /// - Shutdown signals + /// - Messages from downstream miners (submit shares) + /// - Messages from upstream SV2 channel manager + /// + /// The server will continue running until a shutdown signal is received. + /// + /// # Arguments + /// * `notify_shutdown` - Broadcast channel for shutdown coordination + /// * `shutdown_complete_tx` - Channel to signal shutdown completion + /// * `status_sender` - Channel for sending status updates + /// * `task_manager` - Manager for spawned async tasks + /// + /// # Returns + /// * `Ok(())` - Server shut down gracefully + /// * `Err(TproxyError)` - Server encountered an error + pub async fn start( + self: Arc, + notify_shutdown: broadcast::Sender, + shutdown_complete_tx: mpsc::Sender<()>, + status_sender: Sender, + task_manager: Arc, + ) -> Result<(), TproxyError> { + info!("Starting SV1 server on {}", self.listener_addr); + let mut shutdown_rx_main = notify_shutdown.subscribe(); + let shutdown_complete_tx_main_clone = shutdown_complete_tx.clone(); + + // get the first target for the first set difficulty message + let first_target: Target = hash_rate_to_target( + self.config + .downstream_difficulty_config + .min_individual_miner_hashrate as f64, + self.config.downstream_difficulty_config.shares_per_minute as f64, + ) + .unwrap() + .into(); + + // Spawn vardiff loop only if enabled + if self.config.downstream_difficulty_config.enable_vardiff { + info!("Variable difficulty adjustment enabled - starting vardiff loop"); + task_manager.spawn(DifficultyManager::spawn_vardiff_loop( + self.sv1_server_data.clone(), + self.sv1_server_channel_state.channel_manager_sender.clone(), + self.sv1_server_channel_state + .sv1_server_to_downstream_sender + .clone(), + self.shares_per_minute, + self.config.aggregate_channels, + notify_shutdown.subscribe(), + shutdown_complete_tx_main_clone.clone(), + )); + } else { + info!("Variable difficulty adjustment disabled - upstream will manage difficulty, SV1 server will forward SetTarget messages to downstreams"); + } + + let listener = TcpListener::bind(self.listener_addr).await.map_err(|e| { + error!("Failed to bind to {}: {}", self.listener_addr, e); + e + })?; + + info!("Translator Proxy: listening on {}", self.listener_addr); + + let sv1_status_sender = StatusSender::Sv1Server(status_sender.clone()); + + loop { + tokio::select! { + message = shutdown_rx_main.recv() => { + match message { + Ok(ShutdownMessage::ShutdownAll) => { + debug!("SV1 Server: Vardiff loop received shutdown signal. Exiting."); + break; + } + Ok(ShutdownMessage::DownstreamShutdown(downstream_id)) => { + let current_downstream = self.sv1_server_data.super_safe_lock(|d| { + // Only remove from vardiff map if vardiff is enabled + if self.config.downstream_difficulty_config.enable_vardiff { + d.vardiff.remove(&downstream_id); + } + d.downstreams.remove(&downstream_id) + }); + if let Some(downstream) = current_downstream { + info!("🔌 Downstream: {downstream_id} disconnected and removed from sv1 server downstreams"); + + // In aggregated mode, send UpdateChannel to reflect the new state (only if vardiff enabled) + if self.config.downstream_difficulty_config.enable_vardiff { + DifficultyManager::send_update_channel_on_downstream_state_change( + &self.sv1_server_data, + &self.sv1_server_channel_state.channel_manager_sender, + self.config.aggregate_channels, + ).await; + } + + let channel_id = downstream.downstream_data.super_safe_lock(|d| d.channel_id); + + if let Some(channel_id) = channel_id { + if !self.config.aggregate_channels { + info!("Sending CloseChannel message: {channel_id} for downstream: {downstream_id}"); + let reason_code = Str0255::try_from("downstream disconnected".to_string()).unwrap(); + _ = self.sv1_server_channel_state + .channel_manager_sender + .send(Mining::CloseChannel(CloseChannel { + channel_id, + reason_code, + })) + .await; + } + } + } + } + Ok(ShutdownMessage::DownstreamShutdownAll) => { + self.sv1_server_data.super_safe_lock(|d|{ + if self.config.downstream_difficulty_config.enable_vardiff { + d.vardiff = HashMap::new(); + } + d.downstreams = HashMap::new(); + }); + info!("🔌 All downstreams removed from sv1 server as upstream changed"); + + // In aggregated mode, send UpdateChannel to reflect the new state (no downstreams) + if self.config.downstream_difficulty_config.enable_vardiff { + DifficultyManager::send_update_channel_on_downstream_state_change( + &self.sv1_server_data, + &self.sv1_server_channel_state.channel_manager_sender, + self.config.aggregate_channels, + ).await; + } + } + Ok(ShutdownMessage::UpstreamReconnectedResetAndShutdownDownstreams) => { + self.sv1_server_data.super_safe_lock(|d|{ + if self.config.downstream_difficulty_config.enable_vardiff { + d.vardiff = HashMap::new(); + } + d.downstreams = HashMap::new(); + }); + info!("🔌 All downstreams removed from sv1 server as upstream reconnected"); + + // In aggregated mode, send UpdateChannel to reflect the new state (no downstreams) + if self.config.downstream_difficulty_config.enable_vardiff { + DifficultyManager::send_update_channel_on_downstream_state_change( + &self.sv1_server_data, + &self.sv1_server_channel_state.channel_manager_sender, + self.config.aggregate_channels, + ).await; + } + } + _ => {} + } + } + result = listener.accept() => { + match result { + Ok((stream, addr)) => { + info!("New SV1 downstream connection from {}", addr); + + let connection = ConnectionSV1::new(stream).await; + let downstream_id = self.sv1_server_data.super_safe_lock(|v| v.downstream_id_factory.fetch_add(1, Ordering::Relaxed)); + let downstream = Arc::new(Downstream::new( + downstream_id, + connection.sender().clone(), + connection.receiver().clone(), + self.sv1_server_channel_state.downstream_to_sv1_server_sender.clone(), + self.sv1_server_channel_state.sv1_server_to_downstream_sender.clone().subscribe(), + first_target.clone(), + Some(self.config.downstream_difficulty_config.min_individual_miner_hashrate), + self.sv1_server_data.clone(), + )); + // vardiff initialization (only if enabled) + _ = self.sv1_server_data + .safe_lock(|d| { + d.downstreams.insert(downstream_id, downstream.clone()); + // Insert vardiff state for this downstream only if vardiff is enabled + if self.config.downstream_difficulty_config.enable_vardiff { + let vardiff = Arc::new(RwLock::new(VardiffState::new().expect("Failed to create vardiffstate"))); + d.vardiff.insert(downstream_id, vardiff); + } + }); + info!("Downstream {} registered successfully (channel will be opened after first message)", downstream_id); + + // Start downstream tasks immediately, but defer channel opening until first message + let status_sender = StatusSender::Downstream { + downstream_id, + tx: status_sender.clone(), + }; + + Downstream::run_downstream_tasks( + downstream, + notify_shutdown.clone(), + shutdown_complete_tx.clone(), + status_sender, + task_manager.clone(), + ); + } + Err(e) => { + warn!("Failed to accept new connection: {:?}", e); + } + } + } + res = Self::handle_downstream_message( + Arc::clone(&self) + ) => { + if let Err(e) = res { + handle_error(&sv1_status_sender, e).await; + break; + } + } + res = Self::handle_upstream_message( + Arc::clone(&self), + first_target.clone(), + ) => { + if let Err(e) = res { + handle_error(&sv1_status_sender, e).await; + break; + } + } + } + } + self.sv1_server_channel_state.drop(); + drop(shutdown_complete_tx); + debug!("SV1 Server main listener loop exited."); + Ok(()) + } + + /// Handles messages received from downstream SV1 miners. + /// + /// This method processes share submissions from miners by: + /// - Updating variable difficulty counters + /// - Extracting and validating share data + /// - Converting SV1 share format to SV2 SubmitSharesExtended + /// - Forwarding the share to the channel manager for upstream submission + /// + /// # Returns + /// * `Ok(())` - Message processed successfully + /// * `Err(TproxyError)` - Error processing the message + pub async fn handle_downstream_message(self: Arc) -> Result<(), TproxyError> { + let downstream_message = self + .sv1_server_channel_state + .downstream_to_sv1_server_receiver + .recv() + .await + .map_err(TproxyError::ChannelErrorReceiver)?; + + match downstream_message { + DownstreamMessages::SubmitShares(message) => { + return self.handle_submit_shares(message).await; + } + DownstreamMessages::OpenChannel(downstream_id) => { + return self.handle_open_channel_request(downstream_id).await; + } + } + } + + /// Handles share submission messages from downstream. + async fn handle_submit_shares( + self: &Arc, + message: crate::sv1::downstream::SubmitShareWithChannelId, + ) -> Result<(), TproxyError> { + // Increment vardiff counter for this downstream (only if vardiff is enabled) + if self.config.downstream_difficulty_config.enable_vardiff { + self.sv1_server_data.safe_lock(|v| { + if let Some(vardiff_state) = v.vardiff.get(&message.downstream_id) { + vardiff_state + .write() + .unwrap() + .increment_shares_since_last_update(); + } + })?; + } + + let job_version = match message.job_version { + Some(version) => version, + None => { + warn!("Received share submission without valid job version, skipping"); + return Ok(()); + } + }; + + let submit_share_extended = build_sv2_submit_shares_extended_from_sv1_submit( + &message.share, + message.channel_id, + self.sequence_counter.load(Ordering::SeqCst), + job_version, + message.version_rolling_mask, + ) + .map_err(|_| TproxyError::SV1Error)?; + + self.sv1_server_channel_state + .channel_manager_sender + .send(Mining::SubmitSharesExtended(submit_share_extended)) + .await + .map_err(|_| TproxyError::ChannelErrorSender)?; + + self.sequence_counter.fetch_add(1, Ordering::SeqCst); + + Ok(()) + } + + /// Handles channel opening requests from downstream when they send their first message. + async fn handle_open_channel_request( + self: &Arc, + downstream_id: u32, + ) -> Result<(), TproxyError> { + info!("SV1 Server: Opening extended mining channel for downstream {} after receiving first message", downstream_id); + + let downstreams = self + .sv1_server_data + .super_safe_lock(|v| v.downstreams.clone()); + if let Some(downstream) = Self::get_downstream(downstream_id, downstreams) { + self.open_extended_mining_channel(downstream).await?; + } else { + error!( + "Downstream {} not found when trying to open channel", + downstream_id + ); + } + + Ok(()) + } + + /// Handles messages received from the upstream SV2 server via the channel manager. + /// + /// This method processes various SV2 messages including: + /// - OpenExtendedMiningChannelSuccess: Sets up downstream connections + /// - NewExtendedMiningJob: Converts to SV1 notify messages + /// - SetNewPrevHash: Updates block template information + /// - Channel error messages (TODO: implement proper handling) + /// + /// # Arguments + /// * `first_target` - Initial difficulty target for new connections + /// * `notify_shutdown` - Broadcast channel for shutdown coordination + /// * `shutdown_complete_tx` - Channel to signal shutdown completion + /// * `status_sender` - Channel for sending status updates + /// * `task_manager` - Manager for spawned async tasks + /// + /// # Returns + /// * `Ok(())` - Message processed successfully + /// * `Err(TproxyError)` - Error processing the message + pub async fn handle_upstream_message( + self: Arc, + first_target: Target, + ) -> Result<(), TproxyError> { + let message = self + .sv1_server_channel_state + .channel_manager_receiver + .recv() + .await + .map_err(TproxyError::ChannelErrorReceiver)?; + + match message { + Mining::OpenExtendedMiningChannelSuccess(m) => { + debug!( + "Received OpenExtendedMiningChannelSuccess for channel id: {}", + m.channel_id + ); + let downstream_id = m.request_id; + let downstreams = self + .sv1_server_data + .super_safe_lock(|v| v.downstreams.clone()); + if let Some(downstream) = Self::get_downstream(downstream_id, downstreams) { + let initial_target: Target = m.target.clone().into(); + downstream.downstream_data.safe_lock(|d| { + d.extranonce1 = m.extranonce_prefix.to_vec(); + d.extranonce2_len = m.extranonce_size.into(); + d.channel_id = Some(m.channel_id); + // Set the initial upstream target from OpenExtendedMiningChannelSuccess + d.set_upstream_target(initial_target.clone()); + })?; + + // Process all queued messages now that channel is established + if let Ok(queued_messages) = downstream.downstream_data.safe_lock(|d| { + let messages = d.queued_sv1_handshake_messages.clone(); + d.queued_sv1_handshake_messages.clear(); + messages + }) { + if !queued_messages.is_empty() { + info!( + "Processing {} queued Sv1 messages for downstream {}", + queued_messages.len(), + downstream_id + ); + + // Set flag to indicate we're processing queued responses + downstream.downstream_data.super_safe_lock(|data| { + data.processing_queued_sv1_handshake_responses + .store(true, std::sync::atomic::Ordering::SeqCst); + }); + + for message in queued_messages { + if let Ok(Some(response_msg)) = downstream + .downstream_data + .super_safe_lock(|data| data.handle_message(message)) + { + self.sv1_server_channel_state + .sv1_server_to_downstream_sender + .send(( + m.channel_id, + Some(downstream_id), + response_msg.into(), + )) + .map_err(|_| TproxyError::ChannelErrorSender)?; + } + } + } + } + + let set_difficulty = build_sv1_set_difficulty_from_sv2_target(first_target) + .map_err(|_| { + TproxyError::General("Failed to generate set_difficulty".into()) + })?; + // send the set_difficulty message to the downstream + self.sv1_server_channel_state + .sv1_server_to_downstream_sender + .send((m.channel_id, None, set_difficulty)) + .map_err(|_| TproxyError::ChannelErrorSender)?; + } else { + error!("Downstream not found for downstream_id: {}", downstream_id); + } + } + + Mining::NewExtendedMiningJob(m) => { + debug!( + "Received NewExtendedMiningJob for channel id: {}", + m.channel_id + ); + if let Some(prevhash) = self.sv1_server_data.super_safe_lock(|v| v.prevhash.clone()) + { + let notify = build_sv1_notify_from_sv2( + prevhash, + m.clone().into_static(), + self.clean_job.load(Ordering::SeqCst), + )?; + let clean_jobs = self.clean_job.load(Ordering::SeqCst); + self.clean_job.store(false, Ordering::SeqCst); + + // Update job storage based on the configured mode + let notify_parsed = notify.clone(); + self.sv1_server_data.super_safe_lock(|server_data| { + if let Some(ref mut aggregated_jobs) = server_data.aggregated_valid_jobs { + // Aggregated mode: all downstreams share the same jobs + if clean_jobs { + aggregated_jobs.clear(); + } + aggregated_jobs.push(notify_parsed); + } else if let Some(ref mut non_aggregated_jobs) = + server_data.non_aggregated_valid_jobs + { + // Non-aggregated mode: per-downstream jobs + let channel_jobs = non_aggregated_jobs + .entry(m.channel_id) + .or_insert_with(Vec::new); + if clean_jobs { + channel_jobs.clear(); + } + channel_jobs.push(notify_parsed); + } + }); + + let _ = self + .sv1_server_channel_state + .sv1_server_to_downstream_sender + .send((m.channel_id, None, notify.into())); + } + } + + Mining::SetNewPrevHash(m) => { + debug!("Received SetNewPrevHash for channel id: {}", m.channel_id); + self.clean_job.store(true, Ordering::SeqCst); + self.sv1_server_data + .super_safe_lock(|v| v.prevhash = Some(m.clone().into_static())); + } + + Mining::SetTarget(m) => { + debug!("Received SetTarget for channel id: {}", m.channel_id); + if self.config.downstream_difficulty_config.enable_vardiff { + // Vardiff enabled - use full difficulty management + DifficultyManager::handle_set_target_message( + m, + &self.sv1_server_data, + &self.sv1_server_channel_state.channel_manager_sender, + &self + .sv1_server_channel_state + .sv1_server_to_downstream_sender, + self.config.aggregate_channels, + ) + .await; + } else { + // Vardiff disabled - just forward the difficulty to downstreams + debug!("Vardiff disabled - forwarding SetTarget to downstreams"); + self.handle_set_target_without_vardiff(m).await; + } + } + + Mining::CloseChannel(_) => { + todo!("Handle CloseChannel message from upstream"); + } + + Mining::OpenMiningChannelError(_) => { + todo!("Handle OpenMiningChannelError message from upstream"); + } + + Mining::UpdateChannelError(_) => { + todo!("Handle UpdateChannelError message from upstream"); + } + + _ => unreachable!("Unexpected message type received from upstream"), + } + + Ok(()) + } + + /// Opens an extended mining channel for a downstream connection. + /// + /// This method initiates the SV2 channel setup process by: + /// - Calculating the initial target based on configuration + /// - Generating a unique user identity for the miner + /// - Creating an OpenExtendedMiningChannel message + /// - Sending the request to the channel manager + /// + /// # Arguments + /// * `downstream` - The downstream connection to set up a channel for + /// + /// # Returns + /// * `Ok(())` - Channel setup request sent successfully + /// * `Err(TproxyError)` - Error setting up the channel + pub async fn open_extended_mining_channel( + &self, + downstream: Arc, + ) -> Result<(), TproxyError> { + let config = &self.config.downstream_difficulty_config; + + let hashrate = config.min_individual_miner_hashrate as f64; + let shares_per_min = config.shares_per_minute as f64; + let min_extranonce_size = self.config.downstream_extranonce2_size; + let vardiff_enabled = config.enable_vardiff; + + let max_target = if vardiff_enabled { + hash_rate_to_target(hashrate, shares_per_min) + .unwrap() + .into() + } else { + // If translator doesn't manage vardiff, we rely on upstream to do that, + // so we give it more freedom by setting max_target to maximum possible value + Target::from([0xff; 32]) + }; + + // Store the initial target for use when no downstreams remain + self.sv1_server_data.super_safe_lock(|data| { + if data.initial_target.is_none() { + data.initial_target = Some(max_target.clone()); + } + }); + + let miner_id = self.miner_counter.fetch_add(1, Ordering::SeqCst) + 1; + let user_identity = format!("{}.miner{}", self.config.user_identity, miner_id); + + downstream + .downstream_data + .safe_lock(|d| d.user_identity = user_identity.clone())?; + + if let Ok(open_channel_msg) = build_sv2_open_extended_mining_channel( + downstream + .downstream_data + .super_safe_lock(|d| d.downstream_id), + user_identity.clone(), + hashrate as f32, + max_target, + min_extranonce_size, + ) { + self.sv1_server_channel_state + .channel_manager_sender + .send(Mining::OpenExtendedMiningChannel(open_channel_msg)) + .await + .map_err(|_| TproxyError::ChannelErrorSender)?; + } else { + error!("Failed to build OpenExtendedMiningChannel message"); + } + + Ok(()) + } + + /// Retrieves a downstream connection by ID from the provided map. + /// + /// # Arguments + /// * `downstream_id` - The ID of the downstream connection to find + /// * `downstream` - HashMap containing downstream connections + /// + /// # Returns + /// * `Some(Downstream)` - If a downstream with the given ID exists + /// * `None` - If no downstream with the given ID is found + pub fn get_downstream( + downstream_id: u32, + downstream: HashMap>, + ) -> Option> { + downstream.get(&downstream_id).cloned() + } + + /// Extracts the downstream ID from a Downstream instance. + /// + /// # Arguments + /// * `downstream` - The downstream connection to get the ID from + /// + /// # Returns + /// The downstream ID as a u32 + pub fn get_downstream_id(downstream: Downstream) -> u32 { + downstream + .downstream_data + .super_safe_lock(|s| s.downstream_id) + } + + /// Handles SetTarget messages when vardiff is disabled. + /// + /// This method forwards difficulty changes from upstream directly to downstream miners + /// without any variable difficulty logic. It respects the aggregated/non-aggregated + /// channel configuration. + async fn handle_set_target_without_vardiff(&self, set_target: SetTarget<'_>) { + let new_target: Target = set_target.maximum_target.clone().into(); + debug!( + "Forwarding SetTarget to downstreams: channel_id={}, target={:?}", + set_target.channel_id, new_target + ); + + if self.config.aggregate_channels { + // Aggregated mode: send set_difficulty to ALL downstreams + self.send_set_difficulty_to_all_downstreams(new_target) + .await; + } else { + // Non-aggregated mode: send set_difficulty to specific downstream for this channel + self.send_set_difficulty_to_specific_downstream(set_target.channel_id, new_target) + .await; + } + } + + /// Sends set_difficulty to all downstreams (aggregated mode). + /// Used only when vardiff is disabled. + async fn send_set_difficulty_to_all_downstreams(&self, target: Target) { + let downstreams = self + .sv1_server_data + .super_safe_lock(|data| data.downstreams.clone()); + + for (downstream_id, downstream) in downstreams { + let channel_id = downstream.downstream_data.super_safe_lock(|d| d.channel_id); + + if let Some(channel_id) = channel_id { + // Update the downstream's target + _ = downstream.downstream_data.safe_lock(|d| { + d.set_upstream_target(target.clone()); + d.set_pending_target(target.clone()); + }); + + // Send set_difficulty message + if let Ok(set_difficulty_msg) = + build_sv1_set_difficulty_from_sv2_target(target.clone()) + { + if let Err(e) = self + .sv1_server_channel_state + .sv1_server_to_downstream_sender + .send((channel_id, Some(downstream_id), set_difficulty_msg)) + { + error!( + "Failed to send SetDifficulty to downstream {}: {:?}", + downstream_id, e + ); + } else { + debug!( + "Sent SetDifficulty to downstream {} (vardiff disabled)", + downstream_id + ); + } + } + } + } + } + + /// Sends set_difficulty to the specific downstream associated with a channel (non-aggregated + /// mode). + /// Used only when vardiff is disabled. + async fn send_set_difficulty_to_specific_downstream(&self, channel_id: u32, target: Target) { + let affected_downstream = self.sv1_server_data.super_safe_lock(|data| { + data.downstreams + .iter() + .find_map(|(downstream_id, downstream)| { + downstream.downstream_data.super_safe_lock(|d| { + if d.channel_id == Some(channel_id) { + Some((*downstream_id, downstream.clone())) + } else { + None + } + }) + }) + }); + + if let Some((downstream_id, downstream)) = affected_downstream { + // Update the downstream's target + _ = downstream.downstream_data.safe_lock(|d| { + d.set_upstream_target(target.clone()); + d.set_pending_target(target.clone()); + }); + + // Send set_difficulty message + if let Ok(set_difficulty_msg) = build_sv1_set_difficulty_from_sv2_target(target) { + if let Err(e) = self + .sv1_server_channel_state + .sv1_server_to_downstream_sender + .send((channel_id, Some(downstream_id), set_difficulty_msg)) + { + error!( + "Failed to send SetDifficulty to downstream {}: {:?}", + downstream_id, e + ); + } else { + debug!( + "Sent SetDifficulty to downstream {} for channel {} (vardiff disabled)", + downstream_id, channel_id + ); + } + } + } else { + warn!( + "No downstream found for channel {} when vardiff disabled", + channel_id + ); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::{DownstreamDifficultyConfig, TranslatorConfig, Upstream}; + use async_channel::unbounded; + use key_utils::Secp256k1PublicKey; + use std::{collections::HashMap, str::FromStr}; + + fn create_test_config() -> TranslatorConfig { + let pubkey_str = "9bDuixKmZqAJnrmP746n8zU1wyAQRrus7th9dxnkPg6RzQvCnan"; + let pubkey = Secp256k1PublicKey::from_str(pubkey_str).unwrap(); + + let upstream = Upstream::new("127.0.0.1".to_string(), 4444, pubkey); + let difficulty_config = DownstreamDifficultyConfig::new(100.0, 5.0, true); + + TranslatorConfig::new( + vec![upstream], + "0.0.0.0".to_string(), // downstream_address + 3333, // downstream_port + difficulty_config, // downstream_difficulty_config + 2, // max_supported_version + 1, // min_supported_version + 4, // downstream_extranonce2_size + "test_user".to_string(), + true, // aggregate_channels + ) + } + + fn create_test_sv1_server() -> Sv1Server { + let (cm_sender, _cm_receiver) = unbounded(); + let (_downstream_sender, cm_receiver) = unbounded(); + let config = create_test_config(); + let addr = "127.0.0.1:3333".parse().unwrap(); + + Sv1Server::new(addr, cm_receiver, cm_sender, config) + } + + #[test] + fn test_sv1_server_creation() { + let server = create_test_sv1_server(); + + assert_eq!(server.shares_per_minute, 5.0); + assert_eq!(server.listener_addr.ip().to_string(), "127.0.0.1"); + assert_eq!(server.listener_addr.port(), 3333); + assert_eq!(server.config.user_identity, "test_user"); + assert!(server.config.aggregate_channels); + } + + #[test] + fn test_sv1_server_aggregated_config() { + let mut config = create_test_config(); + config.aggregate_channels = true; + config.downstream_difficulty_config.enable_vardiff = true; + + let (cm_sender, _cm_receiver) = unbounded(); + let (_downstream_sender, cm_receiver) = unbounded(); + let addr = "127.0.0.1:3333".parse().unwrap(); + + let server = Sv1Server::new(addr, cm_receiver, cm_sender, config); + + assert!(server.config.aggregate_channels); + assert!(server.config.downstream_difficulty_config.enable_vardiff); + } + + #[test] + fn test_sv1_server_non_aggregated_config() { + let mut config = create_test_config(); + config.aggregate_channels = false; + config.downstream_difficulty_config.enable_vardiff = false; + + let (cm_sender, _cm_receiver) = unbounded(); + let (_downstream_sender, cm_receiver) = unbounded(); + let addr = "127.0.0.1:3333".parse().unwrap(); + + let server = Sv1Server::new(addr, cm_receiver, cm_sender, config); + + assert!(!server.config.aggregate_channels); + assert!(!server.config.downstream_difficulty_config.enable_vardiff); + } + + #[test] + fn test_get_downstream_basic() { + let downstreams = HashMap::new(); + + // Test non-existing downstream + let not_found = Sv1Server::get_downstream(999, downstreams); + assert!(not_found.is_none()); + } + + #[tokio::test] + async fn test_send_set_difficulty_to_all_downstreams_empty() { + let server = create_test_sv1_server(); + let target: Target = hash_rate_to_target(200.0, 5.0).unwrap().into(); + + // Test with empty downstreams + server.send_set_difficulty_to_all_downstreams(target).await; + + // Should not crash with empty downstreams + } + + #[tokio::test] + async fn test_send_set_difficulty_to_specific_downstream_not_found() { + let server = create_test_sv1_server(); + let target: Target = hash_rate_to_target(200.0, 5.0).unwrap().into(); + let channel_id = 1u32; + + // Test with no downstreams + server + .send_set_difficulty_to_specific_downstream(channel_id, target) + .await; + + // Should not crash when no downstreams are found + } + + #[tokio::test] + async fn test_handle_set_target_without_vardiff_aggregated() { + let mut config = create_test_config(); + config.downstream_difficulty_config.enable_vardiff = false; + config.aggregate_channels = true; + + let (cm_sender, _cm_receiver) = unbounded(); + let (_downstream_sender, cm_receiver) = unbounded(); + let addr = "127.0.0.1:3333".parse().unwrap(); + + let server = Sv1Server::new(addr, cm_receiver, cm_sender, config); + let target: Target = hash_rate_to_target(200.0, 5.0).unwrap().into(); + + let set_target = SetTarget { + channel_id: 1, + maximum_target: target.clone().into(), + }; + + // Test should not panic and should handle the message + server.handle_set_target_without_vardiff(set_target).await; + } + + #[tokio::test] + async fn test_handle_set_target_without_vardiff_non_aggregated() { + let mut config = create_test_config(); + config.downstream_difficulty_config.enable_vardiff = false; + config.aggregate_channels = false; + + let (cm_sender, _cm_receiver) = unbounded(); + let (_downstream_sender, cm_receiver) = unbounded(); + let addr = "127.0.0.1:3333".parse().unwrap(); + + let server = Sv1Server::new(addr, cm_receiver, cm_sender, config); + let target: Target = hash_rate_to_target(200.0, 5.0).unwrap().into(); + + let set_target = SetTarget { + channel_id: 1, + maximum_target: target.clone().into(), + }; + + // Test should not panic and should handle the message + server.handle_set_target_without_vardiff(set_target).await; + } + + #[test] + fn test_sv1_server_counters() { + let server = create_test_sv1_server(); + + // Test initial values + assert_eq!(server.miner_counter.load(Ordering::SeqCst), 0); + assert_eq!(server.sequence_counter.load(Ordering::SeqCst), 0); + + // Test incrementing + let miner_id = server.miner_counter.fetch_add(1, Ordering::SeqCst); + assert_eq!(miner_id, 0); + assert_eq!(server.miner_counter.load(Ordering::SeqCst), 1); + + let seq_id = server.sequence_counter.fetch_add(1, Ordering::SeqCst); + assert_eq!(seq_id, 0); + assert_eq!(server.sequence_counter.load(Ordering::SeqCst), 1); + } + + #[test] + fn test_sv1_server_clean_job_flag() { + let server = create_test_sv1_server(); + + // Test initial value + assert!(server.clean_job.load(Ordering::SeqCst)); + + // Test setting to false + server.clean_job.store(false, Ordering::SeqCst); + assert!(!server.clean_job.load(Ordering::SeqCst)); + + // Test setting back to true + server.clean_job.store(true, Ordering::SeqCst); + assert!(server.clean_job.load(Ordering::SeqCst)); + } +} diff --git a/roles/translator/src/lib/sv2/channel_manager/channel.rs b/roles/translator/src/lib/sv2/channel_manager/channel.rs new file mode 100644 index 0000000000..3c867cf205 --- /dev/null +++ b/roles/translator/src/lib/sv2/channel_manager/channel.rs @@ -0,0 +1,36 @@ +use crate::sv2::upstream::upstream::EitherFrame; +use async_channel::{Receiver, Sender}; +use stratum_common::roles_logic_sv2::parsers_sv2::Mining; +use tracing::debug; + +#[derive(Clone, Debug)] +pub struct ChannelState { + pub upstream_sender: Sender, + pub upstream_receiver: Receiver, + pub sv1_server_sender: Sender>, + pub sv1_server_receiver: Receiver>, +} + +impl ChannelState { + pub fn new( + upstream_sender: Sender, + upstream_receiver: Receiver, + sv1_server_sender: Sender>, + sv1_server_receiver: Receiver>, + ) -> Self { + Self { + upstream_sender, + upstream_receiver, + sv1_server_sender, + sv1_server_receiver, + } + } + + pub fn drop(&self) { + debug!("Dropping channel manager channels"); + self.upstream_receiver.close(); + self.upstream_sender.close(); + self.sv1_server_receiver.close(); + self.sv1_server_sender.close(); + } +} diff --git a/roles/translator/src/lib/sv2/channel_manager/channel_manager.rs b/roles/translator/src/lib/sv2/channel_manager/channel_manager.rs new file mode 100644 index 0000000000..400adc6ef2 --- /dev/null +++ b/roles/translator/src/lib/sv2/channel_manager/channel_manager.rs @@ -0,0 +1,863 @@ +use crate::{ + error::TproxyError, + status::{handle_error, Status, StatusSender}, + sv2::{ + channel_manager::{ + channel::ChannelState, + data::{ChannelManagerData, ChannelMode}, + }, + upstream::upstream::{EitherFrame, Message, StdFrame}, + }, + task_manager::TaskManager, + utils::{into_static, ShutdownMessage}, +}; +use async_channel::{Receiver, Sender}; +use std::sync::{Arc, RwLock}; +use stratum_common::roles_logic_sv2::{ + channels_sv2::client::extended::ExtendedChannel, + codec_sv2::Frame, + handlers_sv2::HandleMiningMessagesFromServerAsync, + mining_sv2::OpenExtendedMiningChannelSuccess, + parsers_sv2::{AnyMessage, Mining}, + utils::Mutex, +}; +use tokio::sync::{broadcast, mpsc}; +use tracing::{debug, error, info, warn}; + +/// Extra bytes allocated for translator search space in aggregated mode. +/// This allows the translator to manage multiple downstream connections +/// by allocating unique extranonce prefixes to each downstream. +const AGGREGATED_MODE_TRANSLATOR_SEARCH_SPACE_BYTES: usize = 4; + +/// Type alias for SV2 mining messages with static lifetime +pub type Sv2Message = Mining<'static>; + +/// Manages SV2 channels and message routing between upstream and downstream. +/// +/// The ChannelManager serves as the central component that bridges SV2 upstream +/// connections with SV1 downstream connections. It handles: +/// - SV2 channel lifecycle management (open, close, error handling) +/// - Message translation and routing between protocols +/// - Extranonce management for aggregated vs non-aggregated modes +/// - Share submission processing and validation +/// - Job distribution to downstream connections +/// +/// The manager supports two operational modes: +/// - Aggregated: All downstream connections share a single extended channel +/// - Non-aggregated: Each downstream connection gets its own extended channel +/// +/// This design allows the translator to efficiently manage multiple mining +/// connections while maintaining proper isolation and state management. +#[derive(Debug, Clone)] +pub struct ChannelManager { + pub channel_state: ChannelState, + pub channel_manager_data: Arc>, +} + +impl ChannelManager { + /// Creates a new ChannelManager instance. + /// + /// # Arguments + /// * `upstream_sender` - Channel to send messages to upstream + /// * `upstream_receiver` - Channel to receive messages from upstream + /// * `sv1_server_sender` - Channel to send messages to SV1 server + /// * `sv1_server_receiver` - Channel to receive messages from SV1 server + /// * `mode` - Operating mode (Aggregated or NonAggregated) + /// + /// # Returns + /// A new ChannelManager instance ready to handle message routing + pub fn new( + upstream_sender: Sender, + upstream_receiver: Receiver, + sv1_server_sender: Sender>, + sv1_server_receiver: Receiver>, + mode: ChannelMode, + ) -> Self { + let channel_state = ChannelState::new( + upstream_sender, + upstream_receiver, + sv1_server_sender, + sv1_server_receiver, + ); + let channel_manager_data = Arc::new(Mutex::new(ChannelManagerData::new(mode))); + Self { + channel_state, + channel_manager_data, + } + } + + /// Spawns and runs the main channel manager task loop. + /// + /// This method creates an async task that handles all message routing for the + /// channel manager. The task runs a select loop that processes: + /// - Shutdown signals for graceful termination + /// - Messages from upstream SV2 server + /// - Messages from downstream SV1 server + /// + /// The task continues running until a shutdown signal is received or an + /// unrecoverable error occurs. It ensures proper cleanup of resources + /// and error reporting. + /// + /// # Arguments + /// * `notify_shutdown` - Broadcast channel for receiving shutdown signals + /// * `shutdown_complete_tx` - Channel to signal when shutdown is complete + /// * `status_sender` - Channel for sending status updates and errors + /// * `task_manager` - Manager for tracking spawned tasks + pub async fn run_channel_manager_tasks( + self: Arc, + notify_shutdown: broadcast::Sender, + shutdown_complete_tx: mpsc::Sender<()>, + status_sender: Sender, + task_manager: Arc, + ) { + let mut shutdown_rx = notify_shutdown.subscribe(); + let status_sender = StatusSender::ChannelManager(status_sender); + task_manager.spawn(async move { + loop { + tokio::select! { + message = shutdown_rx.recv() => { + match message { + Ok(ShutdownMessage::ShutdownAll) => { + info!("ChannelManager: received shutdown signal."); + break; + } + Ok(ShutdownMessage::UpstreamReconnectedResetAndShutdownDownstreams) => { + info!("ChannelManager: upstream reconnected, resetting channel state."); + self.channel_manager_data.super_safe_lock(|data| { + data.reset_for_upstream_reconnection(); + }); + // Note: DownstreamShutdownAll handling is done by SV1Server separately + } + Ok(_) => { + // Ignore other shutdown message types + } + Err(e) => { + // Handle channel lag gracefully - don't shutdown on lag errors + if let tokio::sync::broadcast::error::RecvError::Lagged(_) = e { + warn!("ChannelManager: broadcast channel lagged, continuing: {e}"); + } else { + error!("ChannelManager: failed to receive shutdown signal: {e}"); + break; + } + } + } + } + res = Self::handle_upstream_message(self.clone()) => { + if let Err(e) = res { + handle_error(&status_sender, e).await; + break; + } + }, + res = Self::handle_downstream_message(self.clone()) => { + if let Err(e) = res { + handle_error(&status_sender, e).await; + break; + } + }, + else => { + warn!("All channel manager message streams closed. Exiting..."); + break; + } + } + } + + self.channel_state.drop(); + drop(shutdown_complete_tx); + warn!("ChannelManager: unified message loop exited."); + }); + } + + /// Handles messages received from the upstream SV2 server. + /// + /// This method processes SV2 messages from upstream and routes them appropriately: + /// - Mining messages: Processed through the roles logic and forwarded to SV1 server + /// - Channel responses: Handled to manage channel lifecycle + /// - Job notifications: Converted and distributed to downstream connections + /// - Error messages: Logged and handled appropriately + /// + /// The method implements the core SV2 protocol logic for channel management, + /// including handling both aggregated and non-aggregated channel modes. + /// + /// # Returns + /// * `Ok(())` - Message processed successfully + /// * `Err(TproxyError)` - Error processing the message + pub async fn handle_upstream_message(self: Arc) -> Result<(), TproxyError> { + let mut channel_manager = self.get_channel_manager(); + let message = self + .channel_state + .upstream_receiver + .recv() + .await + .map_err(TproxyError::ChannelErrorReceiver)?; + + let Frame::Sv2(mut frame) = message else { + warn!("Received non-SV2 frame from upstream"); + return Ok(()); + }; + + let header = frame.get_header().ok_or_else(|| { + error!("Missing header in SV2 frame"); + TproxyError::General("Missing frame header".into()) + })?; + + let message_type = header.msg_type(); + let mut payload = frame.payload().to_vec(); + + let message: AnyMessage<'_> = into_static( + (message_type, payload.as_mut_slice()) + .try_into() + .map_err(|e| { + error!("Failed to parse upstream frame into AnyMessage: {:?}", e); + TproxyError::General("Failed to parse AnyMessage".into()) + })?, + )?; + + match message { + Message::Mining(_) => { + channel_manager + .handle_mining_message_frame_from_server(None, message_type, &mut payload) + .await?; + } + _ => { + warn!("Unhandled upstream message type: {:?}", message); + } + } + + Ok(()) + } + + /// Handles messages received from the downstream SV1 server. + /// + /// This method processes requests from the SV1 server, primarily: + /// - OpenExtendedMiningChannel: Sets up new SV2 channels for downstream connections + /// - SubmitSharesExtended: Processes share submissions from miners + /// + /// For channel opening, the method handles both aggregated and non-aggregated modes: + /// - Aggregated: Creates extended channels using extranonce prefixes + /// - Non-aggregated: Opens individual extended channels with the upstream for each downstream + /// + /// Share submissions are validated, processed through the channel logic, + /// and forwarded to the upstream server with appropriate extranonce handling. + /// + /// # Returns + /// * `Ok(())` - Message processed successfully + /// * `Err(TproxyError)` - Error processing the message + pub async fn handle_downstream_message(self: Arc) -> Result<(), TproxyError> { + let message = self + .channel_state + .sv1_server_receiver + .recv() + .await + .map_err(TproxyError::ChannelErrorReceiver)?; + match message { + Mining::OpenExtendedMiningChannel(m) => { + let mut open_channel_msg = m.clone(); + let mut user_identity = std::str::from_utf8(m.user_identity.as_ref()) + .map(|s| s.to_string()) + .unwrap_or_else(|_| "unknown".to_string()); + let hashrate = m.nominal_hash_rate; + let min_extranonce_size = m.min_extranonce_size as usize; + let mode = self + .channel_manager_data + .super_safe_lock(|c| c.mode.clone()); + + if mode == ChannelMode::Aggregated { + if self + .channel_manager_data + .super_safe_lock(|c| c.upstream_extended_channel.is_some()) + { + // We already have the unique channel open and so we create a new + // extranonce prefix and we send the + // OpenExtendedMiningChannelSuccess message directly to the sv1 + // server + let target = self.channel_manager_data.super_safe_lock(|c| { + c.upstream_extended_channel + .as_ref() + .unwrap() + .read() + .unwrap() + .get_target() + .clone() + }); + let new_extranonce_prefix = + self.channel_manager_data.super_safe_lock(|c| { + c.extranonce_prefix_factory + .as_ref() + .unwrap() + .safe_lock(|e| { + e.next_prefix_extended( + open_channel_msg.min_extranonce_size.into(), + ) + }) + .ok() + .and_then(|r| r.ok()) + }); + let new_extranonce_size = self.channel_manager_data.super_safe_lock(|c| { + c.extranonce_prefix_factory + .as_ref() + .unwrap() + .safe_lock(|e| e.get_range2_len()) + .unwrap() + }); + if let Some(new_extranonce_prefix) = new_extranonce_prefix { + if new_extranonce_size >= open_channel_msg.min_extranonce_size as usize + { + let next_channel_id = + self.channel_manager_data.super_safe_lock(|c| { + c.extended_channels.keys().max().unwrap_or(&0) + 1 + }); + let new_downstream_extended_channel = ExtendedChannel::new( + next_channel_id, + user_identity.clone(), + new_extranonce_prefix + .clone() + .into_b032() + .into_static() + .to_vec(), + target.clone(), + hashrate, + true, + new_extranonce_size as u16, + ); + self.channel_manager_data.super_safe_lock(|c| { + c.extended_channels.insert( + next_channel_id, + Arc::new(RwLock::new(new_downstream_extended_channel)), + ); + }); + let success_message = Mining::OpenExtendedMiningChannelSuccess( + OpenExtendedMiningChannelSuccess { + request_id: open_channel_msg.request_id, + channel_id: next_channel_id, + target: target.clone().into(), + extranonce_size: new_extranonce_size as u16, + extranonce_prefix: new_extranonce_prefix.clone().into(), + }, + ); + + self.channel_state + .sv1_server_sender + .send(success_message) + .await + .map_err(|e| { + error!( + "Failed to send open channel message to upstream: {:?}", + e + ); + TproxyError::ChannelErrorSender + })?; + // get the last active job from the upstream extended channel + let last_active_job = + self.channel_manager_data.super_safe_lock(|c| { + c.upstream_extended_channel + .as_ref() + .and_then(|ch| ch.read().ok()) + .and_then(|ch| ch.get_active_job().map(|j| j.0.clone())) + }); + + // get the last chain tip from the upstream extended channel + let last_chain_tip = + self.channel_manager_data.super_safe_lock(|c| { + c.upstream_extended_channel + .as_ref() + .and_then(|ch| ch.read().ok()) + .and_then(|ch| ch.get_chain_tip().cloned()) + }); + // update the downstream channel with the active job and the chain + // tip + if let Some(mut job) = last_active_job { + if let Some(last_chain_tip) = last_chain_tip { + // update the downstream channel with the active chain tip + self.channel_manager_data.super_safe_lock(|c| { + if let Some(ch) = + c.extended_channels.get(&next_channel_id) + { + ch.write() + .unwrap() + .set_chain_tip(last_chain_tip.clone()); + } + }); + } + job.channel_id = next_channel_id; + // update the downstream channel with the active job + self.channel_manager_data.super_safe_lock(|c| { + if let Some(ch) = c.extended_channels.get(&next_channel_id) + { + let _ = ch + .write() + .unwrap() + .on_new_extended_mining_job(job.clone()); + } + }); + + self.channel_state + .sv1_server_sender + .send(Mining::NewExtendedMiningJob(job.clone())) + .await + .map_err(|e| { + error!("Failed to send last new extended mining job to upstream: {:?}", e); + TproxyError::ChannelErrorSender + })?; + } + } + } + return Ok(()); + } else { + // We don't have the unique channel open yet and so we send the + // OpenExtendedMiningChannel message to the upstream + // Before doing that we need to truncate the user identity at the + // first dot and append .translator-proxy + // Truncate at the first dot and append .translator-proxy + let translator_identity = if let Some(dot_index) = user_identity.find('.') { + format!("{}.translator-proxy", &user_identity[..dot_index]) + } else { + format!("{user_identity}.translator-proxy") + }; + user_identity = translator_identity; + open_channel_msg.user_identity = + user_identity.as_bytes().to_vec().try_into().unwrap(); + } + } + // In aggregated mode, add extra bytes for translator search space allocation + let upstream_min_extranonce_size = self.channel_manager_data.super_safe_lock(|c| { + if c.mode == ChannelMode::Aggregated { + min_extranonce_size + AGGREGATED_MODE_TRANSLATOR_SEARCH_SPACE_BYTES + } else { + min_extranonce_size + } + }); + + // Update the message with the adjusted extranonce size for upstream + open_channel_msg.min_extranonce_size = upstream_min_extranonce_size as u16; + + // Store the user identity, hashrate, and original downstream extranonce size + self.channel_manager_data.super_safe_lock(|c| { + c.pending_channels.insert( + open_channel_msg.request_id, + (user_identity, hashrate, min_extranonce_size), + ); + }); + + info!( + "Sending OpenExtendedMiningChannel message to upstream: {:?}", + open_channel_msg + ); + + let frame = StdFrame::try_from(Message::Mining(Mining::OpenExtendedMiningChannel( + open_channel_msg, + ))) + .map_err(TproxyError::ParserError)?; + self.channel_state + .upstream_sender + .send(frame.into()) + .await + .map_err(|e| { + error!("Failed to send open channel message to upstream: {:?}", e); + TproxyError::ChannelErrorSender + })?; + } + Mining::SubmitSharesExtended(mut m) => { + let value = self.channel_manager_data.super_safe_lock(|c| { + let extended_channel = c.extended_channels.get(&m.channel_id); + if let Some(extended_channel) = extended_channel { + let channel = extended_channel.write(); + if let Ok(mut channel) = channel { + return Some(( + channel.validate_share(m.clone()), + channel.get_share_accounting().clone(), + )); + } + } + None + }); + if let Some((Ok(_result), _share_accounting)) = value { + let mode = self + .channel_manager_data + .super_safe_lock(|c| c.mode.clone()); + + if mode == ChannelMode::Aggregated + && self + .channel_manager_data + .super_safe_lock(|c| c.upstream_extended_channel.is_some()) + { + let upstream_extended_channel_id = + self.channel_manager_data.super_safe_lock(|c| { + let upstream_extended_channel = c + .upstream_extended_channel + .as_ref() + .unwrap() + .read() + .unwrap(); + upstream_extended_channel.get_channel_id() + }); + + // In aggregated mode, use a single sequence counter for all valid shares + m.sequence_number = self.channel_manager_data.super_safe_lock(|c| { + c.next_share_sequence_number(upstream_extended_channel_id) + }); + // Get the downstream channel's extranonce prefix (contains + // upstream prefix + translator proxy prefix) + let downstream_extranonce_prefix = + self.channel_manager_data.super_safe_lock(|c| { + c.extended_channels.get(&m.channel_id).map(|channel| { + channel.read().unwrap().get_extranonce_prefix().clone() + }) + }); + // Get the length of the upstream prefix (range0) + let range0_len = self.channel_manager_data.super_safe_lock(|c| { + c.extranonce_prefix_factory + .as_ref() + .unwrap() + .safe_lock(|e| e.get_range0_len()) + .unwrap() + }); + if let Some(downstream_extranonce_prefix) = downstream_extranonce_prefix { + // Skip the upstream prefix (range0) and take the remaining + // bytes (translator proxy prefix) + let translator_prefix = &downstream_extranonce_prefix[range0_len..]; + // Create new extranonce: translator proxy prefix + miner's + // extranonce + let mut new_extranonce = translator_prefix.to_vec(); + new_extranonce.extend_from_slice(m.extranonce.as_ref()); + // Replace the original extranonce with the modified one for + // upstream submission + m.extranonce = new_extranonce.try_into()?; + } + // We need to set the channel id to the upstream extended + // channel id + m.channel_id = upstream_extended_channel_id; + } else { + // In non-aggregated mode, each downstream channel has its own sequence + // counter + m.sequence_number = self + .channel_manager_data + .super_safe_lock(|c| c.next_share_sequence_number(m.channel_id)); + + // Check if we have a per-channel factory for extranonce adjustment + let channel_factory = self.channel_manager_data.super_safe_lock(|c| { + c.extranonce_factories + .as_ref() + .and_then(|factories| factories.get(&m.channel_id).cloned()) + }); + + if let Some(factory) = channel_factory { + // We need to adjust the extranonce for this channel + let downstream_extranonce_prefix = + self.channel_manager_data.super_safe_lock(|c| { + c.extended_channels.get(&m.channel_id).map(|channel| { + channel.read().unwrap().get_extranonce_prefix().clone() + }) + }); + let range0_len = factory + .safe_lock(|e| e.get_range0_len()) + .expect("Failed to access extranonce factory range - this should not happen"); + if let Some(downstream_extranonce_prefix) = downstream_extranonce_prefix + { + // Skip the upstream prefix (range0) and take the remaining + // bytes (translator proxy prefix) + let translator_prefix = &downstream_extranonce_prefix[range0_len..]; + // Create new extranonce: translator proxy prefix + miner's + // extranonce + let mut new_extranonce = translator_prefix.to_vec(); + new_extranonce.extend_from_slice(m.extranonce.as_ref()); + // Replace the original extranonce with the modified one for + // upstream submission + m.extranonce = new_extranonce.try_into()?; + } + } + } + + info!( + "SubmitSharesExtended: valid share, forwarding it to upstream | channel_id: {}, sequence_number: {} ☑ï¸", + m.channel_id, m.sequence_number + ); + let frame: StdFrame = Message::Mining(Mining::SubmitSharesExtended(m)) + .try_into() + .map_err(TproxyError::ParserError)?; + let frame: EitherFrame = frame.into(); + self.channel_state + .upstream_sender + .send(frame) + .await + .map_err(|e| { + error!("Error while sending message to upstream: {e:?}"); + TproxyError::ChannelErrorSender + })?; + } + } + Mining::UpdateChannel(mut m) => { + debug!("Received UpdateChannel from SV1Server: {:?}", m); + let mode = self + .channel_manager_data + .super_safe_lock(|c| c.mode.clone()); + + if mode == ChannelMode::Aggregated { + let upstream_extended_channel_id = + self.channel_manager_data.super_safe_lock(|c| { + c.upstream_extended_channel + .as_ref() + .unwrap() + .read() + .unwrap() + .get_channel_id() + }); + // We need to set the channel id to the upstream extended + // channel id + m.channel_id = upstream_extended_channel_id; + } + info!( + "Sending UpdateChannel message to upstream for channel_id: {:?}", + m.channel_id + ); + // Forward UpdateChannel message to upstream + let frame = StdFrame::try_from(Message::Mining(Mining::UpdateChannel(m))) + .map_err(TproxyError::ParserError)?; + + self.channel_state + .upstream_sender + .send(frame.into()) + .await + .map_err(|e| { + error!("Failed to send UpdateChannel message to upstream: {:?}", e); + TproxyError::ChannelErrorSender + })?; + } + Mining::CloseChannel(m) => { + debug!("Received CloseChannel from SV1Server: {m}"); + let frame = StdFrame::try_from(Message::Mining(Mining::CloseChannel(m))) + .map_err(TproxyError::ParserError)?; + + self.channel_state + .upstream_sender + .send(frame.into()) + .await + .map_err(|e| { + error!("Failed to send UpdateChannel message to upstream: {:?}", e); + TproxyError::ChannelErrorSender + })?; + } + _ => { + warn!("Unhandled downstream message: {:?}", message); + } + } + + Ok(()) + } + + pub fn get_channel_manager(&self) -> ChannelManager { + ChannelManager { + channel_manager_data: self.channel_manager_data.clone(), + channel_state: self.channel_state.clone(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::sv2::channel_manager::data::ChannelMode; + use async_channel::unbounded; + use stratum_common::roles_logic_sv2::mining_sv2::{ + OpenExtendedMiningChannel, SubmitSharesExtended, UpdateChannel, + }; + + fn create_test_channel_manager(mode: ChannelMode) -> ChannelManager { + let (upstream_sender, _upstream_receiver) = unbounded(); + let (_upstream_sender2, upstream_receiver) = unbounded(); + let (sv1_server_sender, _sv1_server_receiver) = unbounded(); + let (_sv1_server_sender2, sv1_server_receiver) = unbounded(); + + ChannelManager::new( + upstream_sender, + upstream_receiver, + sv1_server_sender, + sv1_server_receiver, + mode, + ) + } + + #[test] + fn test_channel_manager_creation_aggregated() { + let manager = create_test_channel_manager(ChannelMode::Aggregated); + + let mode = manager + .channel_manager_data + .super_safe_lock(|data| data.mode.clone()); + assert_eq!(mode, ChannelMode::Aggregated); + } + + #[test] + fn test_channel_manager_creation_non_aggregated() { + let manager = create_test_channel_manager(ChannelMode::NonAggregated); + + let mode = manager + .channel_manager_data + .super_safe_lock(|data| data.mode.clone()); + assert_eq!(mode, ChannelMode::NonAggregated); + } + + #[test] + fn test_get_channel_manager() { + let manager = create_test_channel_manager(ChannelMode::Aggregated); + let cloned_manager = manager.get_channel_manager(); + + // Should be a different instance but share the same data + let original_mode = manager + .channel_manager_data + .super_safe_lock(|data| data.mode.clone()); + let cloned_mode = cloned_manager + .channel_manager_data + .super_safe_lock(|data| data.mode.clone()); + + assert_eq!(original_mode, cloned_mode); + } + + #[tokio::test] + async fn test_handle_downstream_open_channel_message() { + let manager = create_test_channel_manager(ChannelMode::NonAggregated); + + // Create an OpenExtendedMiningChannel message + let open_channel = OpenExtendedMiningChannel { + request_id: 1, + user_identity: "test_user".as_bytes().to_vec().try_into().unwrap(), + nominal_hash_rate: 1000.0, + max_target: vec![0xFFu8; 32].try_into().unwrap(), + min_extranonce_size: 4, + }; + + // Store the pending channel information + manager.channel_manager_data.super_safe_lock(|data| { + data.pending_channels + .insert(1, ("test_user".to_string(), 1000.0, 4)); + }); + + // Test that the message can be handled without panicking + // In a real test environment, we would need to mock the upstream sender + // For now, we just verify the channel manager can process the message type + let mining_message = Mining::OpenExtendedMiningChannel(open_channel); + + // Verify the message can be processed (would normally be sent to upstream) + match mining_message { + Mining::OpenExtendedMiningChannel(msg) => { + assert_eq!(msg.request_id, 1); + assert_eq!(msg.nominal_hash_rate, 1000.0); + assert_eq!(msg.min_extranonce_size, 4); + } + _ => panic!("Expected OpenExtendedMiningChannel"), + } + } + + #[tokio::test] + async fn test_handle_downstream_submit_shares_message() { + let _manager = create_test_channel_manager(ChannelMode::NonAggregated); + + // Create a SubmitSharesExtended message + let submit_shares = SubmitSharesExtended { + channel_id: 1, + sequence_number: 100, + job_id: 42, + nonce: 0x12345678, + ntime: 1234567890, + version: 0x20000000, + extranonce: vec![0x01, 0x02, 0x03, 0x04].try_into().unwrap(), + }; + + // Test that the message can be handled + let mining_message = Mining::SubmitSharesExtended(submit_shares); + + // Verify the message structure + match mining_message { + Mining::SubmitSharesExtended(msg) => { + assert_eq!(msg.channel_id, 1); + assert_eq!(msg.sequence_number, 100); + assert_eq!(msg.job_id, 42); + assert_eq!(msg.nonce, 0x12345678); + } + _ => panic!("Expected SubmitSharesExtended"), + } + } + + #[tokio::test] + async fn test_handle_downstream_update_channel_message() { + let _manager = create_test_channel_manager(ChannelMode::Aggregated); + + // Create an UpdateChannel message + let update_channel = UpdateChannel { + channel_id: 1, + nominal_hash_rate: 2000.0, + maximum_target: [0xFFu8; 32].try_into().unwrap(), + }; + + // Test that the message can be handled + let mining_message = Mining::UpdateChannel(update_channel); + + // Verify the message structure + match mining_message { + Mining::UpdateChannel(msg) => { + assert_eq!(msg.channel_id, 1); + assert_eq!(msg.nominal_hash_rate, 2000.0); + } + _ => panic!("Expected UpdateChannel"), + } + } + + #[test] + fn test_channel_manager_debug() { + let manager = create_test_channel_manager(ChannelMode::Aggregated); + + // Test that Debug trait is implemented + let debug_str = format!("{:?}", manager); + assert!(debug_str.contains("ChannelManager")); + } + + #[test] + fn test_channel_manager_clone() { + let manager = create_test_channel_manager(ChannelMode::Aggregated); + let cloned = manager.clone(); + + // Verify that both managers share the same underlying data + let original_mode = manager + .channel_manager_data + .super_safe_lock(|data| data.mode.clone()); + let cloned_mode = cloned + .channel_manager_data + .super_safe_lock(|data| data.mode.clone()); + + assert_eq!(original_mode, cloned_mode); + } + + #[test] + fn test_channel_manager_data_access() { + let manager = create_test_channel_manager(ChannelMode::NonAggregated); + + // Test that we can access and modify channel manager data + manager.channel_manager_data.super_safe_lock(|data| { + // Add a pending channel + data.pending_channels + .insert(1, ("test".to_string(), 100.0, 4)); + }); + + let has_pending = manager + .channel_manager_data + .super_safe_lock(|data| data.pending_channels.contains_key(&1)); + + assert!(has_pending); + } + + #[test] + fn test_channel_manager_mode_consistency() { + let aggregated_manager = create_test_channel_manager(ChannelMode::Aggregated); + let non_aggregated_manager = create_test_channel_manager(ChannelMode::NonAggregated); + + let agg_mode = aggregated_manager + .channel_manager_data + .super_safe_lock(|data| data.mode.clone()); + let non_agg_mode = non_aggregated_manager + .channel_manager_data + .super_safe_lock(|data| data.mode.clone()); + + assert_eq!(agg_mode, ChannelMode::Aggregated); + assert_eq!(non_agg_mode, ChannelMode::NonAggregated); + assert_ne!(agg_mode, non_agg_mode); + } +} diff --git a/roles/translator/src/lib/sv2/channel_manager/data.rs b/roles/translator/src/lib/sv2/channel_manager/data.rs new file mode 100644 index 0000000000..b4d7928b20 --- /dev/null +++ b/roles/translator/src/lib/sv2/channel_manager/data.rs @@ -0,0 +1,107 @@ +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, +}; +use stratum_common::roles_logic_sv2::{ + channels_sv2::client::extended::ExtendedChannel, mining_sv2::ExtendedExtranonce, utils::Mutex, +}; + +/// Defines the operational mode for channel management. +/// +/// The channel manager can operate in two different modes that affect how +/// downstream connections are mapped to upstream SV2 channels: +#[derive(Debug, Clone, PartialEq, serde::Deserialize)] +pub enum ChannelMode { + /// All downstream connections share a single extended SV2 channel. + /// This mode uses extranonce prefix allocation to distinguish between + /// different downstream miners while presenting them as a single entity + /// to the upstream server. This is more efficient for pools with many + /// miners. + Aggregated, + /// Each downstream connection gets its own dedicated extended SV2 channel. + /// This mode provides complete isolation between downstream connections + /// but may be less efficient for large numbers of miners. + NonAggregated, +} + +/// Internal data structure for the ChannelManager. +/// +/// This struct maintains all the state needed for SV2 channel management, +/// including pending channel requests, active channels, and mode-specific +/// data structures like extranonce factories for aggregated mode. +#[derive(Debug, Clone)] +pub struct ChannelManagerData { + /// Store pending channel info by downstream_id: (user_identity, hashrate, + /// downstream_extranonce_len) + pub pending_channels: HashMap, + /// Map of active extended channels by channel ID + pub extended_channels: HashMap>>>, + /// The upstream extended channel used in aggregated mode + pub upstream_extended_channel: Option>>>, + /// Extranonce prefix factory for allocating unique prefixes in aggregated mode + pub extranonce_prefix_factory: Option>>, + /// Current operational mode + pub mode: ChannelMode, + /// Share sequence number counter for tracking valid shares forwarded upstream. + /// In aggregated mode: single counter for all shares going to the upstream channel. + /// In non-aggregated mode: one counter per downstream channel. + pub share_sequence_counters: HashMap, + /// Per-channel extranonce factories for non-aggregated mode when extranonce adjustment is + /// needed + pub extranonce_factories: Option>>>, +} + +impl ChannelManagerData { + /// Creates a new ChannelManagerData instance. + /// + /// # Arguments + /// * `mode` - The operational mode (Aggregated or NonAggregated) + /// + /// # Returns + /// A new ChannelManagerData instance with empty state + pub fn new(mode: ChannelMode) -> Self { + Self { + pending_channels: HashMap::new(), + extended_channels: HashMap::new(), + upstream_extended_channel: None, + extranonce_prefix_factory: None, + mode, + share_sequence_counters: HashMap::new(), + extranonce_factories: None, + } + } + + /// Resets all channel state for upstream reconnection. + /// + /// This method clears all existing channel state that becomes invalid + /// when the upstream connection is lost and reestablished. It preserves + /// the operational mode but clears: + /// - All pending channel requests + /// - All active extended channels + /// - The upstream extended channel + /// - The extranonce prefix factory + /// + /// This ensures that new channels will be properly opened with the + /// newly connected upstream server. + pub fn reset_for_upstream_reconnection(&mut self) { + self.pending_channels.clear(); + self.extended_channels.clear(); + self.upstream_extended_channel = None; + self.extranonce_prefix_factory = None; + self.share_sequence_counters.clear(); + self.extranonce_factories = None; + // Note: we intentionally preserve `mode` as it's a configuration setting + } + + /// Gets the next sequence number for a valid share and increments the counter. + /// + /// The counter_key determines which counter to use: + /// - In aggregated mode: use upstream channel ID (single counter for all shares) + /// - In non-aggregated mode: use downstream channel ID (one counter per channel) + pub fn next_share_sequence_number(&mut self, counter_key: u32) -> u32 { + let counter = self.share_sequence_counters.entry(counter_key).or_insert(1); + let current = *counter; + *counter += 1; + current + } +} diff --git a/roles/translator/src/lib/sv2/channel_manager/message_handler.rs b/roles/translator/src/lib/sv2/channel_manager/message_handler.rs new file mode 100644 index 0000000000..ee510d8b49 --- /dev/null +++ b/roles/translator/src/lib/sv2/channel_manager/message_handler.rs @@ -0,0 +1,519 @@ +use std::sync::{Arc, RwLock}; + +use crate::{ + error::TproxyError, + sv2::{channel_manager::ChannelMode, ChannelManager}, + utils::proxy_extranonce_prefix_len, +}; +use stratum_common::roles_logic_sv2::{ + channels_sv2::client::extended::ExtendedChannel, + handlers_sv2::{HandleMiningMessagesFromServerAsync, SupportedChannelTypes}, + mining_sv2::{ + CloseChannel, ExtendedExtranonce, Extranonce, NewExtendedMiningJob, NewMiningJob, + OpenExtendedMiningChannelSuccess, OpenMiningChannelError, OpenStandardMiningChannelSuccess, + SetCustomMiningJobError, SetCustomMiningJobSuccess, SetExtranoncePrefix, SetGroupChannel, + SetNewPrevHash, SetTarget, SubmitSharesError, SubmitSharesSuccess, UpdateChannelError, + MESSAGE_TYPE_OPEN_STANDARD_MINING_CHANNEL_SUCCESS, + MESSAGE_TYPE_SET_CUSTOM_MINING_JOB_ERROR, MESSAGE_TYPE_SET_CUSTOM_MINING_JOB_SUCCESS, + MESSAGE_TYPE_SET_GROUP_CHANNEL, + }, + parsers_sv2::Mining, + utils::Mutex, +}; + +use tracing::{debug, error, info, warn}; + +impl HandleMiningMessagesFromServerAsync for ChannelManager { + type Error = TproxyError; + + fn get_channel_type_for_server(&self, _server_id: Option) -> SupportedChannelTypes { + SupportedChannelTypes::Extended + } + + fn is_work_selection_enabled_for_server(&self, _server_id: Option) -> bool { + false + } + + async fn handle_open_standard_mining_channel_success( + &mut self, + _server_id: Option, + m: OpenStandardMiningChannelSuccess<'_>, + ) -> Result<(), Self::Error> { + warn!("Received: {}", m); + Err(Self::Error::UnexpectedMessage( + MESSAGE_TYPE_OPEN_STANDARD_MINING_CHANNEL_SUCCESS, + )) + } + + async fn handle_open_extended_mining_channel_success( + &mut self, + _server_id: Option, + m: OpenExtendedMiningChannelSuccess<'_>, + ) -> Result<(), Self::Error> { + // Check if we have the pending channel data, return error if not + let (user_identity, nominal_hashrate, downstream_extranonce_len) = self + .channel_manager_data + .safe_lock(|channel_manager_data| { + channel_manager_data.pending_channels.remove(&m.request_id) + }) + .map_err(|e| { + error!("Failed to lock channel manager data: {:?}", e); + TproxyError::PoisonLock + })? + .ok_or_else(|| { + error!("No pending channel found for request_id: {}", m.request_id); + TproxyError::PendingChannelNotFound(m.request_id) + })?; + + let success = self + .channel_manager_data + .safe_lock(|channel_manager_data| { + info!( + "Received: {}, user_identity: {}, nominal_hashrate: {}", + m, user_identity, nominal_hashrate + ); + let extranonce_prefix = m.extranonce_prefix.clone().into_static().to_vec(); + let target = m.target.clone().into_static(); + let version_rolling = true; // we assume this is always true on extended channels + let extended_channel = ExtendedChannel::new( + m.channel_id, + user_identity.clone(), + extranonce_prefix.clone(), + target.clone().into(), + nominal_hashrate, + version_rolling, + m.extranonce_size, + ); + + // If we are in aggregated mode, we need to create a new extranonce prefix and + // insert the extended channel into the map + if channel_manager_data.mode == ChannelMode::Aggregated { + channel_manager_data.upstream_extended_channel = + Some(Arc::new(RwLock::new(extended_channel.clone()))); + + let upstream_extranonce_prefix: Extranonce = m.extranonce_prefix.clone().into(); + let translator_proxy_extranonce_prefix_len = proxy_extranonce_prefix_len( + m.extranonce_size.into(), + downstream_extranonce_len, + ); + + // range 0 is the extranonce1 from upstream + // range 1 is the extranonce1 added by the tproxy + // range 2 is the extranonce2 used by the miner for rolling (this is the one + // that is used for rolling) + let range_0 = 0..extranonce_prefix.len(); + let range1 = range_0.end..range_0.end + translator_proxy_extranonce_prefix_len; + let range2 = range1.end..range1.end + downstream_extranonce_len; + debug!("\n\nrange_0: {:?}, range1: {:?}, range2: {:?}\n\n", range_0, range1, range2); + let extended_extranonce_factory = ExtendedExtranonce::from_upstream_extranonce( + upstream_extranonce_prefix, + range_0, + range1, + range2, + ) + .expect("Failed to create ExtendedExtranonce from upstream extranonce"); + channel_manager_data.extranonce_prefix_factory = + Some(Arc::new(Mutex::new(extended_extranonce_factory))); + + let factory = channel_manager_data + .extranonce_prefix_factory + .as_ref() + .expect("extranonce_prefix_factory should be set after creation"); + let new_extranonce_size = factory + .safe_lock(|f| f.get_range2_len()) + .expect("extranonce_prefix_factory mutex should not be poisoned") + as u16; + let new_extranonce_prefix = factory + .safe_lock(|f| f.next_prefix_extended(new_extranonce_size as usize)) + .expect("extranonce_prefix_factory mutex should not be poisoned") + .expect("next_prefix_extended should return a value for valid input") + .into_b032(); + let new_downstream_extended_channel = ExtendedChannel::new( + m.channel_id, + user_identity.clone(), + new_extranonce_prefix.clone().into_static().to_vec(), + target.clone().into(), + nominal_hashrate, + true, + new_extranonce_size, + ); + channel_manager_data.extended_channels.insert( + m.channel_id, + Arc::new(RwLock::new(new_downstream_extended_channel)), + ); + let new_open_extended_mining_channel_success = + OpenExtendedMiningChannelSuccess { + request_id: m.request_id, + channel_id: m.channel_id, + extranonce_prefix: new_extranonce_prefix, + extranonce_size: new_extranonce_size, + target: m.target.clone(), + }; + new_open_extended_mining_channel_success.into_static() + } else { + // Non-aggregated mode: check if we need to adjust extranonce size + if m.extranonce_size as usize != downstream_extranonce_len { + // We need to create an extranonce factory to ensure proper extranonce2_size + let upstream_extranonce_prefix: Extranonce = m.extranonce_prefix.clone().into(); + let translator_proxy_extranonce_prefix_len = proxy_extranonce_prefix_len( + m.extranonce_size.into(), + downstream_extranonce_len, + ); + + // range 0 is the extranonce1 from upstream + // range 1 is the extranonce1 added by the tproxy + // range 2 is the extranonce2 used by the miner for rolling + let range_0 = 0..extranonce_prefix.len(); + let range1 = range_0.end..range_0.end + translator_proxy_extranonce_prefix_len; + let range2 = range1.end..range1.end + downstream_extranonce_len; + debug!("\n\nrange_0: {:?}, range1: {:?}, range2: {:?}\n\n", range_0, range1, range2); + // Create the factory - this should succeed if configuration is valid + let extended_extranonce_factory = ExtendedExtranonce::from_upstream_extranonce( + upstream_extranonce_prefix, + range_0, + range1, + range2, + ) + .expect("Failed to create ExtendedExtranonce factory - likely extranonce size configuration issue"); + // Store the factory for this specific channel + let factory = Arc::new(Mutex::new(extended_extranonce_factory)); + let new_extranonce_prefix = factory + .safe_lock(|f| f.next_prefix_extended(downstream_extranonce_len)) + .expect("Failed to access extranonce factory") + .expect("Failed to generate extranonce prefix") + .into_b032(); + // Create channel with the configured extranonce size + let new_downstream_extended_channel = ExtendedChannel::new( + m.channel_id, + user_identity.clone(), + new_extranonce_prefix.clone().into_static().to_vec(), + target.clone().into(), + nominal_hashrate, + true, + downstream_extranonce_len as u16, + ); + channel_manager_data.extended_channels.insert( + m.channel_id, + Arc::new(RwLock::new(new_downstream_extended_channel)), + ); + // Store factory for this channel (we'll need it for share processing) + if channel_manager_data.extranonce_factories.is_none() { + channel_manager_data.extranonce_factories = Some(std::collections::HashMap::new()); + } + if let Some(ref mut factories) = channel_manager_data.extranonce_factories { + factories.insert(m.channel_id, factory); + } + let new_open_extended_mining_channel_success = OpenExtendedMiningChannelSuccess { + request_id: m.request_id, + channel_id: m.channel_id, + extranonce_prefix: new_extranonce_prefix, + extranonce_size: downstream_extranonce_len as u16, + target: m.target.clone(), + }; + new_open_extended_mining_channel_success.into_static() + } else { + // Extranonce size matches, use as-is + channel_manager_data + .extended_channels + .insert(m.channel_id, Arc::new(RwLock::new(extended_channel))); + m.into_static() + } + } + }) + .map_err(|e| { + error!("Failed to lock channel manager data: {:?}", e); + TproxyError::PoisonLock + })?; + + self.channel_state + .sv1_server_sender + .send(Mining::OpenExtendedMiningChannelSuccess(success.clone())) + .await + .map_err(|e| { + error!("Failed to send OpenExtendedMiningChannelSuccess: {:?}", e); + TproxyError::ChannelErrorSender + })?; + + Ok(()) + } + + async fn handle_open_mining_channel_error( + &mut self, + _server_id: Option, + m: OpenMiningChannelError<'_>, + ) -> Result<(), Self::Error> { + warn!("Received: {}", m); + todo!("OpenMiningChannelError not handled yet"); + } + + async fn handle_update_channel_error( + &mut self, + _server_id: Option, + m: UpdateChannelError<'_>, + ) -> Result<(), Self::Error> { + warn!("Received: {}", m); + Ok(()) + } + + async fn handle_close_channel( + &mut self, + _server_id: Option, + m: CloseChannel<'_>, + ) -> Result<(), Self::Error> { + info!("Received: {}", m); + _ = self.channel_manager_data.safe_lock(|channel_data_manager| { + if channel_data_manager.mode == ChannelMode::Aggregated { + if channel_data_manager.upstream_extended_channel.is_some() { + channel_data_manager.upstream_extended_channel = None; + } + } else { + channel_data_manager.extended_channels.remove(&m.channel_id); + } + }); + Ok(()) + } + + async fn handle_set_extranonce_prefix( + &mut self, + _server_id: Option, + m: SetExtranoncePrefix<'_>, + ) -> Result<(), Self::Error> { + warn!("Received: {}", m); + warn!("âš ï¸ Cannot process SetExtranoncePrefix since set_extranonce is not supported for majority of sv1 clients. Ignoring."); + Ok(()) + } + + async fn handle_submit_shares_success( + &mut self, + _server_id: Option, + m: SubmitSharesSuccess, + ) -> Result<(), Self::Error> { + info!("Received: {} ✅", m); + Ok(()) + } + + async fn handle_submit_shares_error( + &mut self, + _server_id: Option, + m: SubmitSharesError<'_>, + ) -> Result<(), Self::Error> { + warn!("Received: {} âŒ", m); + Ok(()) + } + + async fn handle_new_mining_job( + &mut self, + _server_id: Option, + m: NewMiningJob<'_>, + ) -> Result<(), Self::Error> { + warn!("Received: {}", m); + warn!("âš ï¸ Cannot process NewMiningJob since Translator Proxy supports only extended mining jobs. Ignoring."); + Ok(()) + } + + async fn handle_new_extended_mining_job( + &mut self, + _server_id: Option, + m: NewExtendedMiningJob<'_>, + ) -> Result<(), Self::Error> { + info!("Received: {}", m); + let mut m_static = m.clone().into_static(); + _ = self.channel_manager_data.safe_lock(|channel_manage_data| { + if channel_manage_data.mode == ChannelMode::Aggregated { + if let Some(upstream_channel) = &channel_manage_data.upstream_extended_channel { + if let Ok(mut upstream_extended_channel) = upstream_channel.write() { + let _ = + upstream_extended_channel.on_new_extended_mining_job(m_static.clone()); + m_static.channel_id = 0; // this is done so that every aggregated downstream + // will + // receive the NewExtendedMiningJob message + } + } + channel_manage_data + .extended_channels + .iter() + .for_each(|(_, channel)| { + if let Ok(mut channel) = channel.write() { + let _ = channel.on_new_extended_mining_job(m_static.clone()); + } + }); + } else if let Some(channel) = channel_manage_data + .extended_channels + .get(&m_static.channel_id) + { + if let Ok(mut channel) = channel.write() { + let _ = channel.on_new_extended_mining_job(m_static.clone()); + } + } + }); + let job = m_static; + if !job.is_future() { + self.channel_state + .sv1_server_sender + .send(Mining::NewExtendedMiningJob(job)) + .await + .map_err(|e| { + error!("Failed to send immediate NewExtendedMiningJob: {:?}", e); + TproxyError::ChannelErrorSender + })?; + } + Ok(()) + } + + async fn handle_set_new_prev_hash( + &mut self, + _server_id: Option, + m: SetNewPrevHash<'_>, + ) -> Result<(), Self::Error> { + info!("Received: {}", m); + let m_static = m.clone().into_static(); + _ = self.channel_manager_data.safe_lock(|channel_manager_data| { + if channel_manager_data.mode == ChannelMode::Aggregated { + if let Some(upstream_channel) = &channel_manager_data.upstream_extended_channel { + if let Ok(mut upstream_extended_channel) = upstream_channel.write() { + _ = upstream_extended_channel.on_set_new_prev_hash(m_static.clone()); + } + } + channel_manager_data + .extended_channels + .iter() + .for_each(|(_, channel)| { + if let Ok(mut channel) = channel.write() { + _ = channel.on_set_new_prev_hash(m_static.clone()); + } + }); + } else if let Some(channel) = channel_manager_data + .extended_channels + .get(&m_static.channel_id) + { + if let Ok(mut channel) = channel.write() { + _ = channel.on_set_new_prev_hash(m_static.clone()); + } + } + }); + + self.channel_state + .sv1_server_sender + .send(Mining::SetNewPrevHash(m_static.clone())) + .await + .map_err(|e| { + error!("Failed to send SetNewPrevHash: {:?}", e); + TproxyError::ChannelErrorSender + })?; + + let mode = self + .channel_manager_data + .super_safe_lock(|c| c.mode.clone()); + + let active_job = if mode == ChannelMode::Aggregated { + self.channel_manager_data.super_safe_lock(|c| { + c.upstream_extended_channel + .as_ref() + .and_then(|ch| ch.read().ok()) + .and_then(|ch| ch.get_active_job().map(|j| j.0.clone())) + }) + } else { + self.channel_manager_data.super_safe_lock(|c| { + c.extended_channels + .get(&m.channel_id) + .and_then(|ch| ch.read().ok()) + .and_then(|ch| ch.get_active_job().map(|j| j.0.clone())) + }) + }; + + if let Some(mut job) = active_job { + if mode == ChannelMode::Aggregated { + job.channel_id = 0; + } + self.channel_state + .sv1_server_sender + .send(Mining::NewExtendedMiningJob(job)) + .await + .map_err(|e| { + error!("Failed to send NewExtendedMiningJob: {:?}", e); + TproxyError::ChannelErrorSender + })?; + } + Ok(()) + } + + async fn handle_set_custom_mining_job_success( + &mut self, + _server_id: Option, + m: SetCustomMiningJobSuccess, + ) -> Result<(), Self::Error> { + warn!("Received: {}", m); + warn!("âš ï¸ Cannot process SetCustomMiningJobSuccess since Translator Proxy does not support custom mining jobs. Ignoring."); + Err(Self::Error::UnexpectedMessage( + MESSAGE_TYPE_SET_CUSTOM_MINING_JOB_SUCCESS, + )) + } + + async fn handle_set_custom_mining_job_error( + &mut self, + _server_id: Option, + m: SetCustomMiningJobError<'_>, + ) -> Result<(), Self::Error> { + warn!("Received: {}", m); + warn!("âš ï¸ Cannot process SetCustomMiningJobError since Translator Proxy does not support custom mining jobs. Ignoring."); + Err(Self::Error::UnexpectedMessage( + MESSAGE_TYPE_SET_CUSTOM_MINING_JOB_ERROR, + )) + } + + async fn handle_set_target( + &mut self, + _server_id: Option, + m: SetTarget<'_>, + ) -> Result<(), Self::Error> { + info!("Received: {}", m); + + // Update the channel targets in the channel manager + _ = self.channel_manager_data.safe_lock(|channel_manager_data| { + if channel_manager_data.mode == ChannelMode::Aggregated { + if let Some(upstream_channel) = &channel_manager_data.upstream_extended_channel { + if let Ok(mut upstream_extended_channel) = upstream_channel.write() { + upstream_extended_channel.set_target(m.maximum_target.clone().into()); + } + } + channel_manager_data + .extended_channels + .iter() + .for_each(|(_, channel)| { + if let Ok(mut channel) = channel.write() { + channel.set_target(m.maximum_target.clone().into()); + } + }); + } else if let Some(channel) = channel_manager_data.extended_channels.get(&m.channel_id) + { + if let Ok(mut channel) = channel.write() { + channel.set_target(m.maximum_target.clone().into()); + } + } + }); + + // Forward SetTarget message to SV1Server for vardiff processing + self.channel_state + .sv1_server_sender + .send(Mining::SetTarget(m.clone().into_static())) + .await + .map_err(|e| { + error!("Failed to forward SetTarget message to SV1Server: {:?}", e); + TproxyError::ChannelErrorSender + })?; + + Ok(()) + } + + async fn handle_set_group_channel( + &mut self, + _server_id: Option, + m: SetGroupChannel<'_>, + ) -> Result<(), Self::Error> { + warn!("Received: {}", m); + warn!("âš ï¸ Cannot process SetGroupChannel since Translator Proxy does not support group channels. Ignoring."); + Err(Self::Error::UnexpectedMessage( + MESSAGE_TYPE_SET_GROUP_CHANNEL, + )) + } +} diff --git a/roles/translator/src/lib/sv2/channel_manager/mod.rs b/roles/translator/src/lib/sv2/channel_manager/mod.rs new file mode 100644 index 0000000000..689a6efc7f --- /dev/null +++ b/roles/translator/src/lib/sv2/channel_manager/mod.rs @@ -0,0 +1,6 @@ +pub mod channel_manager; +pub mod message_handler; +pub use channel_manager::ChannelManager; +pub(super) mod channel; +pub(crate) mod data; +pub use data::ChannelMode; diff --git a/roles/translator/src/lib/sv2/mod.rs b/roles/translator/src/lib/sv2/mod.rs new file mode 100644 index 0000000000..d8cb5e360c --- /dev/null +++ b/roles/translator/src/lib/sv2/mod.rs @@ -0,0 +1,5 @@ +pub mod channel_manager; +pub mod upstream; + +pub use channel_manager::channel_manager::ChannelManager; +pub use upstream::upstream::Upstream; diff --git a/roles/translator/src/lib/sv2/upstream/channel.rs b/roles/translator/src/lib/sv2/upstream/channel.rs new file mode 100644 index 0000000000..9df9335807 --- /dev/null +++ b/roles/translator/src/lib/sv2/upstream/channel.rs @@ -0,0 +1,40 @@ +use async_channel::{Receiver, Sender}; +use stratum_common::roles_logic_sv2::{codec_sv2::StandardEitherFrame, parsers_sv2::AnyMessage}; +use tracing::debug; + +pub type Message = AnyMessage<'static>; +pub type EitherFrame = StandardEitherFrame; + +#[derive(Debug, Clone)] +pub struct UpstreamChannelState { + /// Receiver for the SV2 Upstream role + pub upstream_receiver: Receiver, + /// Sender for the SV2 Upstream role + pub upstream_sender: Sender, + /// Sender for the ChannelManager thread + pub channel_manager_sender: Sender, + /// Receiver for the ChannelManager thread + pub channel_manager_receiver: Receiver, +} + +impl UpstreamChannelState { + pub fn new( + channel_manager_sender: Sender, + channel_manager_receiver: Receiver, + upstream_receiver: Receiver, + upstream_sender: Sender, + ) -> Self { + Self { + channel_manager_sender, + channel_manager_receiver, + upstream_receiver, + upstream_sender, + } + } + + pub fn drop(&self) { + debug!("Closing all upstream channels"); + self.upstream_receiver.close(); + self.upstream_receiver.close(); + } +} diff --git a/roles/translator/src/lib/sv2/upstream/message_handler.rs b/roles/translator/src/lib/sv2/upstream/message_handler.rs new file mode 100644 index 0000000000..5386f870e3 --- /dev/null +++ b/roles/translator/src/lib/sv2/upstream/message_handler.rs @@ -0,0 +1,48 @@ +use crate::{error::TproxyError, sv2::Upstream}; +use stratum_common::roles_logic_sv2::{ + common_messages_sv2::{ + ChannelEndpointChanged, Reconnect, SetupConnectionError, SetupConnectionSuccess, + }, + handlers_sv2::HandleCommonMessagesFromServerAsync, +}; +use tracing::{error, info}; + +impl HandleCommonMessagesFromServerAsync for Upstream { + type Error = TproxyError; + + async fn handle_setup_connection_error( + &mut self, + _server_id: Option, + msg: SetupConnectionError<'_>, + ) -> Result<(), Self::Error> { + error!("Received: {}", msg); + todo!() + } + + async fn handle_setup_connection_success( + &mut self, + _server_id: Option, + msg: SetupConnectionSuccess, + ) -> Result<(), Self::Error> { + info!("Received: {}", msg); + Ok(()) + } + + async fn handle_channel_endpoint_changed( + &mut self, + _server_id: Option, + msg: ChannelEndpointChanged, + ) -> Result<(), Self::Error> { + info!("Received: {}", msg); + todo!() + } + + async fn handle_reconnect( + &mut self, + _server_id: Option, + msg: Reconnect<'_>, + ) -> Result<(), Self::Error> { + info!("Received: {}", msg); + todo!() + } +} diff --git a/roles/translator/src/lib/sv2/upstream/mod.rs b/roles/translator/src/lib/sv2/upstream/mod.rs new file mode 100644 index 0000000000..52cef24ca7 --- /dev/null +++ b/roles/translator/src/lib/sv2/upstream/mod.rs @@ -0,0 +1,4 @@ +pub mod message_handler; +pub mod upstream; +pub use upstream::Upstream; +pub(super) mod channel; diff --git a/roles/translator/src/lib/sv2/upstream/upstream.rs b/roles/translator/src/lib/sv2/upstream/upstream.rs new file mode 100644 index 0000000000..87bd12587f --- /dev/null +++ b/roles/translator/src/lib/sv2/upstream/upstream.rs @@ -0,0 +1,452 @@ +use crate::{ + error::TproxyError, + status::{handle_error, Status, StatusSender}, + sv2::upstream::channel::UpstreamChannelState, + task_manager::TaskManager, + utils::{message_from_frame, ShutdownMessage}, +}; +use async_channel::{Receiver, Sender}; +use key_utils::Secp256k1PublicKey; +use network_helpers_sv2::noise_connection::Connection; +use std::{net::SocketAddr, sync::Arc}; +use stratum_common::roles_logic_sv2::{ + codec_sv2::{ + self, framing_sv2, HandshakeRole, Initiator, StandardEitherFrame, StandardSv2Frame, + }, + common_messages_sv2::{Protocol, SetupConnection}, + handlers_sv2::HandleCommonMessagesFromServerAsync, + parsers_sv2::AnyMessage, +}; +use tokio::{ + net::TcpStream, + sync::{broadcast, mpsc}, + time::{sleep, Duration}, +}; +use tracing::{debug, error, info, warn}; + +/// Type alias for SV2 messages with static lifetime +pub type Message = AnyMessage<'static>; +/// Type alias for standard SV2 frames +pub type StdFrame = StandardSv2Frame; +/// Type alias for either handshake or SV2 frames +pub type EitherFrame = StandardEitherFrame; + +/// Manages the upstream SV2 connection to a mining pool or proxy. +/// +/// This struct handles the SV2 protocol communication with upstream servers, +/// including: +/// - Connection establishment with multiple upstream fallbacks +/// - SV2 handshake and setup procedures +/// - Message routing between channel manager and upstream +/// - Connection monitoring and error handling +/// - Graceful shutdown coordination +/// +/// The upstream connection supports automatic failover between multiple +/// configured upstream servers and implements retry logic for connection +/// establishment. +#[derive(Debug, Clone)] +pub struct Upstream { + upstream_channel_state: UpstreamChannelState, +} + +impl Upstream { + /// Creates a new upstream connection by attempting to connect to configured servers. + /// + /// This method tries to establish a connection to one of the provided upstream + /// servers, implementing retry logic and fallback behavior. It will attempt + /// to connect to each server multiple times before giving up. + /// + /// # Arguments + /// * `upstreams` - List of (address, public_key) pairs for upstream servers + /// * `channel_manager_sender` - Channel to send messages to the channel manager + /// * `channel_manager_receiver` - Channel to receive messages from the channel manager + /// * `notify_shutdown` - Broadcast channel for shutdown coordination + /// * `shutdown_complete_tx` - Channel to signal shutdown completion + /// + /// # Returns + /// * `Ok(Upstream)` - Successfully connected to an upstream server + /// * `Err(TproxyError)` - Failed to connect to any upstream server + pub async fn new( + upstreams: &[(SocketAddr, Secp256k1PublicKey)], + channel_manager_sender: Sender, + channel_manager_receiver: Receiver, + notify_shutdown: broadcast::Sender, + shutdown_complete_tx: mpsc::Sender<()>, + ) -> Result { + let mut shutdown_rx = notify_shutdown.subscribe(); + const RETRIES_PER_UPSTREAM: u8 = 3; + + for (index, (addr, pubkey)) in upstreams.iter().enumerate() { + info!("Trying to connect to upstream {} at {}", index, addr); + + for attempt in 1..=RETRIES_PER_UPSTREAM { + if shutdown_rx.try_recv().is_ok() { + info!("Shutdown signal received during upstream connection attempt. Aborting."); + drop(shutdown_complete_tx); + return Err(TproxyError::Shutdown); + } + + match TcpStream::connect(addr).await { + Ok(socket) => { + info!( + "Connected to upstream at {addr} (attempt {attempt}/{RETRIES_PER_UPSTREAM})" + ); + let initiator = Initiator::from_raw_k(pubkey.into_bytes())?; + match Connection::new(socket, HandshakeRole::Initiator(initiator)).await { + Ok((receiver, sender)) => { + let upstream_channel_state = UpstreamChannelState::new( + channel_manager_sender, + channel_manager_receiver, + receiver, + sender, + ); + debug!("Successfully initialized upstream channel with {addr}"); + + return Ok(Self { + upstream_channel_state, + }); + } + Err(e) => { + error!("Failed Noise handshake with {addr}: {e:?}. Retrying..."); + } + } + } + Err(e) => { + error!( + "Failed to connect to {addr}: {e}. Retry {attempt}/{RETRIES_PER_UPSTREAM}..." + ); + } + } + + sleep(Duration::from_secs(5)).await; + } + + warn!("Exhausted retries for upstream {index} at {addr}"); + } + + error!("Failed to connect to any configured upstream."); + drop(shutdown_complete_tx); + Err(TproxyError::Shutdown) + } + + /// Starts the upstream connection and begins message processing. + /// + /// This method: + /// - Completes the SV2 handshake with the upstream server + /// - Spawns the main message processing task + /// - Handles graceful shutdown coordination + /// + /// The method will first attempt to complete the SV2 setup connection + /// handshake. If successful, it spawns a task to handle bidirectional + /// message flow between the channel manager and upstream server. + pub async fn start( + mut self, + notify_shutdown: broadcast::Sender, + shutdown_complete_tx: mpsc::Sender<()>, + status_sender: Sender, + task_manager: Arc, + ) -> Result<(), TproxyError> { + let mut shutdown_rx = notify_shutdown.subscribe(); + // Wait for connection setup or shutdown signal + tokio::select! { + result = self.setup_connection() => { + if let Err(e) = result { + error!("Upstream: failed to set up SV2 connection: {e:?}"); + drop(shutdown_complete_tx); + return Err(e); + } + } + message = shutdown_rx.recv() => { + match message { + Ok(ShutdownMessage::ShutdownAll) => { + info!("Upstream: shutdown signal received during connection setup."); + drop(shutdown_complete_tx); + return Ok(()); + } + Ok(_) => {} + + Err(e) => { + error!("Upstream: failed to receive shutdown signal: {e}"); + drop(shutdown_complete_tx); + return Ok(()); + } + } + } + } + + // Wrap status sender and start upstream task + let wrapped_status_sender = StatusSender::Upstream(status_sender); + + self.run_upstream_task( + notify_shutdown, + shutdown_complete_tx, + wrapped_status_sender, + task_manager, + )?; + + Ok(()) + } + + /// Performs the SV2 handshake setup with the upstream server. + /// + /// This method handles the initial SV2 protocol handshake by: + /// - Creating and sending a SetupConnection message + /// - Waiting for the handshake response + /// - Validating and processing the response + /// + /// The handshake establishes the protocol version, capabilities, and + /// other connection parameters needed for SV2 communication. + pub async fn setup_connection(&mut self) -> Result<(), TproxyError> { + debug!("Upstream: initiating SV2 handshake..."); + // Build SetupConnection message + let setup_conn_msg = Self::get_setup_connection_message(2, 2, false)?; + let sv2_frame: StdFrame = + Message::Common(setup_conn_msg.into()) + .try_into() + .map_err(|e| { + error!("Failed to serialize SetupConnection message: {:?}", e); + TproxyError::ParserError(e) + })?; + + // Send SetupConnection message to upstream + self.upstream_channel_state + .upstream_sender + .send(sv2_frame.into()) + .await + .map_err(|e| { + error!("Failed to send SetupConnection to upstream: {:?}", e); + TproxyError::ChannelErrorSender + })?; + + let mut incoming: StdFrame = + match self.upstream_channel_state.upstream_receiver.recv().await { + Ok(frame) => { + debug!("Received handshake response from upstream."); + frame.try_into()? + } + Err(e) => { + error!("Failed to receive handshake response from upstream: {}", e); + return Err(TproxyError::CodecNoise( + codec_sv2::noise_sv2::Error::ExpectedIncomingHandshakeMessage, + )); + } + }; + + let message_type = incoming + .get_header() + .ok_or_else(|| { + error!("Expected handshake frame but no header found."); + framing_sv2::Error::ExpectedHandshakeFrame + })? + .msg_type(); + + let payload = incoming.payload(); + + self.handle_common_message_frame_from_server(None, message_type, payload) + .await?; + debug!("Upstream: handshake completed successfully."); + Ok(()) + } + + /// Processes incoming messages from the upstream SV2 server. + /// + /// This method handles different types of frames received from upstream: + /// - SV2 frames: Parses and routes mining/common messages appropriately + /// - Handshake frames: Logs for debugging (shouldn't occur during normal operation) + /// + /// Common messages are handled directly, while mining messages are forwarded + /// to the channel manager for processing and distribution to downstream connections. + pub async fn on_upstream_message(&self, message: EitherFrame) -> Result<(), TproxyError> { + let mut upstream = self.get_upstream(); + match message { + EitherFrame::Sv2(sv2_frame) => { + // Convert to standard frame + let std_frame: StdFrame = sv2_frame; + + // Parse message from frame + let mut frame: codec_sv2::Frame, buffer_sv2::Slice> = + std_frame.clone().into(); + + let (messsage_type, mut payload, parsed_message) = message_from_frame(&mut frame)?; + + match parsed_message { + AnyMessage::Common(_) => { + // Handle common upstream messages + upstream + .handle_common_message_frame_from_server( + None, + messsage_type, + &mut payload, + ) + .await?; + } + + AnyMessage::Mining(_) => { + // Forward mining message to channel manager + let frame_to_forward = EitherFrame::Sv2(std_frame.clone()); + self.upstream_channel_state + .channel_manager_sender + .send(frame_to_forward) + .await + .map_err(|e| { + error!("Failed to send mining message to channel manager: {:?}", e); + TproxyError::ChannelErrorSender + })?; + } + + _ => { + error!("Received unsupported message type from upstream."); + return Err(TproxyError::UnexpectedMessage(0)); + } + } + } + + EitherFrame::HandShake(handshake_frame) => { + debug!("Received handshake frame: {:?}", handshake_frame); + } + } + Ok(()) + } + + /// Spawns a unified task to handle upstream message I/O and shutdown logic. + fn run_upstream_task( + self, + notify_shutdown: broadcast::Sender, + shutdown_complete_tx: mpsc::Sender<()>, + status_sender: StatusSender, + task_manager: Arc, + ) -> Result<(), TproxyError> { + let mut shutdown_rx = notify_shutdown.subscribe(); + let shutdown_complete_tx = shutdown_complete_tx.clone(); + + task_manager.spawn(async move { + loop { + tokio::select! { + // Handle shutdown signals + shutdown = shutdown_rx.recv() => { + match shutdown { + Ok(ShutdownMessage::ShutdownAll) => { + info!("Upstream: received ShutdownAll signal. Exiting loop."); + break; + } + Ok(_) => { + // Ignore other shutdown variants for upstream + } + Err(e) => { + error!("Upstream: failed to receive shutdown signal: {e}"); + break; + } + } + } + + // Handle incoming SV2 messages from upstream + result = self.upstream_channel_state.upstream_receiver.recv() => { + match result { + Ok(frame) => { + debug!("Upstream: received frame."); + if let Err(e) = self.on_upstream_message(frame).await { + error!("Upstream: error while processing message: {e:?}"); + handle_error(&status_sender, TproxyError::ChannelErrorSender).await; + } + } + Err(e) => { + error!("Upstream: receiver channel closed unexpectedly: {e}"); + handle_error(&status_sender, TproxyError::ChannelErrorReceiver(e)).await; + break; + } + } + } + + // Handle messages from channel manager to send upstream + result = self.upstream_channel_state.channel_manager_receiver.recv() => { + match result { + Ok(msg) => { + debug!("Upstream: sending message from channel manager: {:?}", msg); + if let Err(e) = self.send_upstream(msg).await { + error!("Upstream: failed to send message: {e:?}"); + handle_error(&status_sender, TproxyError::ChannelErrorSender).await; + } + } + Err(e) => { + error!("Upstream: channel manager receiver closed: {e}"); + handle_error(&status_sender, TproxyError::ChannelErrorReceiver(e)).await; + break; + } + } + } + } + } + + self.upstream_channel_state.drop(); + warn!("Upstream: task shutting down cleanly."); + drop(shutdown_complete_tx); + }); + + Ok(()) + } + + /// Sends a message to the upstream SV2 server. + /// + /// This method forwards messages from the channel manager to the upstream + /// server. Messages are typically mining-related (share submissions, channel + /// requests, etc.) that need to be sent upstream. + /// + /// # Arguments + /// * `sv2_frame` - The SV2 frame to send to the upstream server + /// + /// # Returns + /// * `Ok(())` - Message sent successfully + /// * `Err(TproxyError)` - Error sending the message + pub async fn send_upstream(&self, sv2_frame: EitherFrame) -> Result<(), TproxyError> { + debug!("Sending message to upstream."); + + self.upstream_channel_state + .upstream_sender + .send(sv2_frame) + .await + .map_err(|e| { + error!("Failed to send message to upstream: {:?}", e); + TproxyError::ChannelErrorSender + })?; + + Ok(()) + } + + /// Constructs the `SetupConnection` message. + #[allow(clippy::result_large_err)] + fn get_setup_connection_message( + min_version: u16, + max_version: u16, + is_work_selection_enabled: bool, + ) -> Result, TproxyError> { + let endpoint_host = "0.0.0.0".to_string().into_bytes().try_into()?; + let vendor = "SRI".to_string().try_into()?; + let hardware_version = "Translator Proxy".to_string().try_into()?; + let firmware = String::new().try_into()?; + let device_id = String::new().try_into()?; + let flags = if is_work_selection_enabled { + 0b110 + } else { + 0b100 + }; + + Ok(SetupConnection { + protocol: Protocol::MiningProtocol, + min_version, + max_version, + flags, + endpoint_host, + endpoint_port: 50, + vendor, + hardware_version, + firmware, + device_id, + }) + } + + fn get_upstream(&self) -> Upstream { + Upstream { + upstream_channel_state: self.upstream_channel_state.clone(), + } + } +} diff --git a/roles/translator/src/lib/task_manager.rs b/roles/translator/src/lib/task_manager.rs new file mode 100644 index 0000000000..ac615a3a7b --- /dev/null +++ b/roles/translator/src/lib/task_manager.rs @@ -0,0 +1,80 @@ +use std::sync::Mutex as StdMutex; +use tokio::task::JoinHandle; + +/// Manages a collection of spawned tokio tasks. +/// +/// This struct provides a centralized way to spawn, track, and manage the lifecycle +/// of async tasks in the translator. It maintains a list of join handles that can +/// be used to wait for all tasks to complete or abort them during shutdown. +pub struct TaskManager { + tasks: StdMutex>>, +} + +impl Default for TaskManager { + fn default() -> Self { + Self::new() + } +} + +impl TaskManager { + /// Creates a new TaskManager instance. + /// + /// Initializes an empty task manager ready to spawn and track tasks. + pub fn new() -> Self { + Self { + tasks: StdMutex::new(Vec::new()), + } + } + + /// Spawns a new async task and adds it to the managed collection. + /// + /// The task will be tracked by this manager and can be waited for or aborted + /// using the other methods. + /// + /// # Arguments + /// * `fut` - The future to spawn as a task + #[track_caller] + pub fn spawn(&self, fut: F) + where + F: std::future::Future + Send + 'static, + { + use tracing::Instrument; + let location = std::panic::Location::caller(); + let span = tracing::trace_span!( + "task", + file = location.file(), + line = location.line(), + column = location.column(), + ); + + let handle = tokio::spawn(fut.instrument(span)); + self.tasks.lock().unwrap().push(handle); + } + + /// Waits for all managed tasks to complete. + /// + /// This method will block until all tasks that were spawned through this + /// manager have finished executing. Tasks are joined in reverse order + /// (most recently spawned first). + pub async fn join_all(&self) { + let handles = { + let mut tasks = self.tasks.lock().unwrap(); + std::mem::take(&mut *tasks) + }; + + for handle in handles { + let _ = handle.await; + } + } + + /// Aborts all managed tasks. + /// + /// This method immediately cancels all tasks that were spawned through this + /// manager. The tasks will be terminated without waiting for them to complete. + pub async fn abort_all(&self) { + let mut tasks = self.tasks.lock().unwrap(); + for handle in tasks.drain(..) { + handle.abort(); + } + } +} diff --git a/roles/translator/src/lib/upstream_sv2/diff_management.rs b/roles/translator/src/lib/upstream_sv2/diff_management.rs deleted file mode 100644 index 941f55123f..0000000000 --- a/roles/translator/src/lib/upstream_sv2/diff_management.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! ## Upstream SV2 Difficulty Management -//! -//! This module contains logic for managing difficulty and hashrate updates -//! specifically for the upstream SV2 connection. -//! -//! It defines method for the [`Upstream`] struct -//! related to checking configuration intervals and sending -//! `UpdateChannel` messages to the upstream server -//! based on configured nominal hashrate changes. - -use super::Upstream; - -use super::super::{ - error::ProxyResult, - upstream_sv2::{EitherFrame, Message, StdFrame}, -}; -use binary_sv2::U256; -use roles_logic_sv2::{ - mining_sv2::UpdateChannel, parsers::Mining, utils::Mutex, Error as RolesLogicError, -}; -use std::{sync::Arc, time::Duration}; - -impl Upstream { - /// Attempts to update the upstream channel's nominal hashrate if the configured - /// update interval has elapsed or if the nominal hashrate has changed - pub(super) async fn try_update_hashrate(self_: Arc>) -> ProxyResult<'static, ()> { - let (channel_id_option, diff_mgmt, tx_frame, last_sent_hashrate) = - self_.safe_lock(|u| { - ( - u.channel_id, - u.difficulty_config.clone(), - u.connection.sender.clone(), - u.last_sent_hashrate, - ) - })?; - - let channel_id = channel_id_option.ok_or(super::super::error::Error::RolesSv2Logic( - RolesLogicError::NotFoundChannelId, - ))?; - - let (timeout, new_hashrate) = diff_mgmt - .safe_lock(|d| (d.channel_diff_update_interval, d.channel_nominal_hashrate))?; - - let has_changed = Some(new_hashrate) != last_sent_hashrate; - - if has_changed { - // Send UpdateChannel only if hashrate actually changed - let update_channel = UpdateChannel { - channel_id, - nominal_hash_rate: new_hashrate, - maximum_target: U256::from([0xff; 32]), - }; - let message = Message::Mining(Mining::UpdateChannel(update_channel)); - let either_frame: StdFrame = message.try_into()?; - let frame: EitherFrame = either_frame.into(); - - tx_frame.send(frame).await?; - - self_.safe_lock(|u| u.last_sent_hashrate = Some(new_hashrate))?; - } - - // Always sleep, regardless of update - tokio::time::sleep(Duration::from_secs(timeout as u64)).await; - Ok(()) - } -} diff --git a/roles/translator/src/lib/upstream_sv2/mod.rs b/roles/translator/src/lib/upstream_sv2/mod.rs deleted file mode 100644 index 64f24acd32..0000000000 --- a/roles/translator/src/lib/upstream_sv2/mod.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! ## Upstream SV2 Module -//! -//! This module encapsulates the logic for handling the upstream connection using the SV2 protocol. -//! -//! The module is organized into the following sub-modules: -//! - [`diff_management`]: Contains logic related to managing difficulty and hashrate updates. -//! - [`upstream`]: Defines the main [`Upstream`] struct and its core functionalities. -//! - [`upstream_connection`]: Handles the underlying connection details and frame -//! sending/receiving. - -use codec_sv2::{StandardEitherFrame, StandardSv2Frame}; -use roles_logic_sv2::parsers::AnyMessage; - -pub mod diff_management; -pub mod upstream; -pub mod upstream_connection; -pub use upstream::Upstream; -pub use upstream_connection::UpstreamConnection; - -pub type Message = AnyMessage<'static>; -pub type StdFrame = StandardSv2Frame; -pub type EitherFrame = StandardEitherFrame; - -/// Represents the state or parameters negotiated during an SV2 Setup Connection message. -#[derive(Clone, Copy, Debug)] -pub struct Sv2MiningConnection { - _version: u16, - _setup_connection_flags: u32, - #[allow(dead_code)] - setup_connection_success_flags: u32, -} diff --git a/roles/translator/src/lib/upstream_sv2/upstream.rs b/roles/translator/src/lib/upstream_sv2/upstream.rs deleted file mode 100644 index 841daf05e5..0000000000 --- a/roles/translator/src/lib/upstream_sv2/upstream.rs +++ /dev/null @@ -1,919 +0,0 @@ -//! ## Upstream SV2 Module: Upstream Connection Logic -//! -//! Defines the [`Upstream`] structure, which represents and manages the connection -//! to a single upstream role. -//! -//! This module is responsible for: -//! - Establishing and maintaining the network connection to the upstream role. -//! - Performing the SV2 handshake and opening mining channels. -//! - Sending translated SV2 `SubmitSharesExtended` messages received from the Bridge to the -//! upstream pool. -//! - Receiving SV2 job messages (`SetNewPrevHash`, `NewExtendedMiningJob`, etc.) from the upstream -//! pool and forwarding them to the Bridge for translation. -//! - Handling various SV2 messages related to connection setup, channel management, and mining -//! operations. -//! - Managing difficulty updates for the upstream channel based on aggregated hashrate from -//! downstream miners. -//! - Implementing the necessary SV2 roles logic traits (`IsUpstream`, `IsMiningUpstream`, -//! `ParseCommonMessagesFromUpstream`, `ParseMiningMessagesFromUpstream`). - -use crate::{ - config::UpstreamDifficultyConfig, - downstream_sv1::Downstream, - error::{ - Error::{CodecNoise, InvalidExtranonce, PoisonLock, UpstreamIncoming}, - ProxyResult, - }, - status, - upstream_sv2::{EitherFrame, Message, StdFrame, UpstreamConnection}, -}; -use async_channel::{Receiver, Sender}; -use binary_sv2::u256_from_int; -use codec_sv2::{HandshakeRole, Initiator}; -use error_handling::handle_result; -use key_utils::Secp256k1PublicKey; -use network_helpers_sv2::noise_connection::Connection; -use roles_logic_sv2::{ - common_messages_sv2::{Protocol, SetupConnection}, - common_properties::{IsMiningUpstream, IsUpstream}, - handlers::{ - common::{ParseCommonMessagesFromUpstream, SendTo as SendToCommon}, - mining::{ParseMiningMessagesFromUpstream, SendTo}, - }, - mining_sv2::{ - ExtendedExtranonce, Extranonce, NewExtendedMiningJob, OpenExtendedMiningChannel, - SetNewPrevHash, SubmitSharesExtended, - }, - parsers::Mining, - utils::Mutex, - Error as RolesLogicError, - Error::NoUpstreamsConnected, -}; -use std::{ - net::SocketAddr, - sync::{atomic::AtomicBool, Arc}, -}; -use tokio::{ - net::TcpStream, - task::AbortHandle, - time::{sleep, Duration}, -}; -use tracing::{debug, error, info, warn}; - -use roles_logic_sv2::{ - common_messages_sv2::Reconnect, handlers::mining::SupportedChannelTypes, - mining_sv2::SetGroupChannel, -}; -use stratum_common::bitcoin::BlockHash; - -/// Atomic boolean flag used for synchronization between receiving a new job -/// and handling a new previous hash. Indicates whether a `NewExtendedMiningJob` -/// has been fully processed. -pub static IS_NEW_JOB_HANDLED: AtomicBool = AtomicBool::new(true); -/// Represents the currently active `prevhash` of the mining job being worked on OR being submitted -/// from the Downstream role. -#[derive(Debug, Clone)] -#[allow(dead_code)] -struct PrevHash { - /// `prevhash` of mining job. - prev_hash: BlockHash, - /// `nBits` encoded difficulty target. - nbits: u32, -} - -/// Represents a connection to a single SV2 Upstream role. -/// -/// This struct holds the state and communication channels necessary to interact -/// with the upstream server, including sending share submissions, receiving job -/// templates, and managing the SV2 protocol handshake and channel lifecycle. -#[derive(Debug, Clone)] -pub struct Upstream { - /// Newly assigned identifier of the channel, stable for the whole lifetime of the connection, - /// e.g. it is used for broadcasting new jobs by the `NewExtendedMiningJob` message. - pub(super) channel_id: Option, - /// Identifier of the job as provided by the `NewExtendedMiningJob` message. - job_id: Option, - /// Identifier of the job as provided by the ` SetCustomMiningJobSucces` message - last_job_id: Option, - /// Bytes used as implicit first part of `extranonce`. - extranonce_prefix: Option>, - /// Represents a connection to a SV2 Upstream role. - pub(super) connection: UpstreamConnection, - /// Receives SV2 `SubmitSharesExtended` messages translated from SV1 `mining.submit` messages. - /// Translated by and sent from the `Bridge`. - rx_sv2_submit_shares_ext: Receiver>, - /// Sends SV2 `SetNewPrevHash` messages to be translated (along with SV2 `NewExtendedMiningJob` - /// messages) into SV1 `mining.notify` messages. Received and translated by the `Bridge`. - tx_sv2_set_new_prev_hash: Sender>, - /// Sends SV2 `NewExtendedMiningJob` messages to be translated (along with SV2 `SetNewPrevHash` - /// messages) into SV1 `mining.notify` messages. Received and translated by the `Bridge`. - tx_sv2_new_ext_mining_job: Sender>, - /// Sends the extranonce1 and the channel id received in the SV2 - /// `OpenExtendedMiningChannelSuccess` message to be used by the `Downstream` and sent to - /// the Downstream role in a SV2 `mining.subscribe` response message. Passed to the - /// `Downstream` on connection creation. - tx_sv2_extranonce: Sender<(ExtendedExtranonce, u32)>, - /// This allows the upstream threads to be able to communicate back to the main thread its - /// current status. - tx_status: status::Sender, - /// The first `target` is received by the Upstream role in the SV2 - /// `OpenExtendedMiningChannelSuccess` message, then updated periodically via SV2 `SetTarget` - /// messages. Passed to the `Downstream` on connection creation and sent to the Downstream role - /// via the SV1 `mining.set_difficulty` message. - target: Arc>>, - /// Tracks the most recently sent nominal hashrate to prevent unnecessary updates. - pub last_sent_hashrate: Option, - /// Minimum `extranonce2` size. Initially requested in the `proxy-config.toml`, and ultimately - /// set by the SV2 Upstream via the SV2 `OpenExtendedMiningChannelSuccess` message. - pub min_extranonce_size: u16, - /// The size of the extranonce1 provided by the upstream role. - pub upstream_extranonce1_size: usize, - // values used to update the channel with the correct nominal hashrate. - // each Downstream instance will add and subtract their hashrates as needed - // and the upstream just needs to occasionally check if it has changed more than - // than the configured percentage - pub(super) difficulty_config: Arc>, - task_collector: Arc>>, -} - -impl PartialEq for Upstream { - fn eq(&self, other: &Self) -> bool { - self.channel_id == other.channel_id - } -} - -impl Upstream { - /// Instantiate a new `Upstream`. - /// Connect to the SV2 Upstream role (most typically a SV2 Pool). Initializes the - /// `UpstreamConnection` with a channel to send and receive messages from the SV2 Upstream - /// role and uses channels provided in the function arguments to send and receive messages - /// from the `Downstream`. - #[allow(clippy::too_many_arguments)] - pub async fn new( - address: SocketAddr, - authority_public_key: Secp256k1PublicKey, - rx_sv2_submit_shares_ext: Receiver>, - tx_sv2_set_new_prev_hash: Sender>, - tx_sv2_new_ext_mining_job: Sender>, - min_extranonce_size: u16, - tx_sv2_extranonce: Sender<(ExtendedExtranonce, u32)>, - tx_status: status::Sender, - target: Arc>>, - difficulty_config: Arc>, - task_collector: Arc>>, - ) -> ProxyResult<'static, Arc>> { - // Connect to the SV2 Upstream role retry connection every 5 seconds. - let socket = loop { - match TcpStream::connect(address).await { - Ok(socket) => break socket, - Err(e) => { - error!( - "Failed to connect to Upstream role at {}, retrying in 5s: {}", - address, e - ); - - sleep(Duration::from_secs(5)).await; - } - } - }; - - let pub_key: Secp256k1PublicKey = authority_public_key; - let initiator = Initiator::from_raw_k(pub_key.into_bytes())?; - - info!( - "PROXY SERVER - ACCEPTING FROM UPSTREAM: {}", - socket.peer_addr()? - ); - - // Channel to send and receive messages to the SV2 Upstream role - let (receiver, sender) = Connection::new(socket, HandshakeRole::Initiator(initiator)) - .await - .unwrap(); - // Initialize `UpstreamConnection` with channel for SV2 Upstream role communication and - // channel for downstream Translator Proxy communication - let connection = UpstreamConnection { receiver, sender }; - - Ok(Arc::new(Mutex::new(Self { - connection, - rx_sv2_submit_shares_ext, - extranonce_prefix: None, - tx_sv2_set_new_prev_hash, - tx_sv2_new_ext_mining_job, - channel_id: None, - job_id: None, - last_job_id: None, - min_extranonce_size, - upstream_extranonce1_size: 16, /* 16 is the default since that is the only value the - * pool supports currently */ - tx_sv2_extranonce, - tx_status, - target, - last_sent_hashrate: None, - difficulty_config, - task_collector, - }))) - } - - /// Performs the SV2 connection setup handshake with the Upstream role. - /// - /// Sends a `SetupConnection` message specifying supported protocol versions - /// and flags. Waits for the upstream to respond with either `SetupConnectionSuccess` - /// or `SetupConnectionError`.Upon successful setup, it then sends an - /// `OpenExtendedMiningChannel` request to establish a mining channel, including the - /// negotiated minimum extranonce size and initial nominal hashrate. - pub async fn connect( - self_: Arc>, - min_version: u16, - max_version: u16, - ) -> ProxyResult<'static, ()> { - // Get the `SetupConnection` message with Mining Device information (currently hard coded) - let setup_connection = Self::get_setup_connection_message(min_version, max_version, false)?; - let mut connection = self_.safe_lock(|s| s.connection.clone())?; - - // Put the `SetupConnection` message in a `StdFrame` to be sent over the wire - let sv2_frame: StdFrame = Message::Common(setup_connection.into()).try_into()?; - // Send the `SetupConnection` frame to the SV2 Upstream role - // Only one Upstream role is supported, panics if multiple connections are encountered - connection.send(sv2_frame).await?; - - // Wait for the SV2 Upstream to respond with either a `SetupConnectionSuccess` or a - // `SetupConnectionError` inside a SV2 binary message frame - let mut incoming: StdFrame = match connection.receiver.recv().await { - Ok(frame) => frame.try_into()?, - Err(e) => { - error!("Upstream connection closed: {}", e); - return Err(CodecNoise( - codec_sv2::noise_sv2::Error::ExpectedIncomingHandshakeMessage, - )); - } - }; - - // Gets the binary frame message type from the message header - let message_type = if let Some(header) = incoming.get_header() { - header.msg_type() - } else { - return Err(framing_sv2::Error::ExpectedHandshakeFrame.into()); - }; - // Gets the message payload - let payload = incoming.payload(); - - // Handle the incoming message (should be either `SetupConnectionSuccess` or - // `SetupConnectionError`) - ParseCommonMessagesFromUpstream::handle_message_common( - self_.clone(), - message_type, - payload, - )?; - - // Send open channel request before returning - let nominal_hash_rate = self_.safe_lock(|u| { - u.difficulty_config - .safe_lock(|c| c.channel_nominal_hashrate) - .map_err(|_e| PoisonLock) - })??; - let user_identity = "ABC".to_string().try_into()?; - - // Get the min_extranonce_size from the instance - let min_extranonce_size = self_.safe_lock(|u| u.min_extranonce_size)?; - - let open_channel = Mining::OpenExtendedMiningChannel(OpenExtendedMiningChannel { - request_id: 0, // TODO - user_identity, // TODO - nominal_hash_rate, - max_target: u256_from_int(u64::MAX), // TODO - min_extranonce_size, - }); - - // reset channel hashrate so downstreams can manage from now on out - self_.safe_lock(|u| { - u.difficulty_config - .safe_lock(|d| d.channel_nominal_hashrate = 0.0) - .map_err(|_e| PoisonLock) - })??; - - let sv2_frame: StdFrame = Message::Mining(open_channel).try_into()?; - connection.send(sv2_frame).await?; - - Ok(()) - } - - /// Spawns tasks to handle incoming SV2 messages from the Upstream role. - /// - /// This method creates two main asynchronous tasks: - /// 1. A task to handle incoming SV2 frames, parsing them, routing them to the appropriate - /// message handlers (`handle_message_mining`), and forwarding translated messages to the - /// Bridge or responding directly to the upstream if necessary. - /// 2. A task to periodically check and update the nominal hashrate sent to the upstream based - /// on th - #[allow(clippy::result_large_err)] - pub fn parse_incoming(self_: Arc>) -> ProxyResult<'static, ()> { - let clone = self_.clone(); - let task_collector = self_.safe_lock(|s| s.task_collector.clone()).unwrap(); - let collector1 = task_collector.clone(); - let collector2 = task_collector.clone(); - let ( - tx_frame, - tx_sv2_extranonce, - tx_sv2_new_ext_mining_job, - tx_sv2_set_new_prev_hash, - recv, - tx_status, - ) = clone.safe_lock(|s| { - ( - s.connection.sender.clone(), - s.tx_sv2_extranonce.clone(), - s.tx_sv2_new_ext_mining_job.clone(), - s.tx_sv2_set_new_prev_hash.clone(), - s.connection.receiver.clone(), - s.tx_status.clone(), - ) - })?; - { - let self_ = self_.clone(); - let tx_status = tx_status.clone(); - let start_diff_management = tokio::task::spawn(async move { - // No need to start diff management immediatly - sleep(Duration::from_secs(10)).await; - loop { - handle_result!(tx_status, Self::try_update_hashrate(self_.clone()).await); - } - }); - let _ = collector1.safe_lock(|a| { - a.push(( - start_diff_management.abort_handle(), - "start_diff_management".to_string(), - )) - }); - } - - let parse_incoming = tokio::task::spawn(async move { - loop { - // Waiting to receive a message from the SV2 Upstream role - let incoming = handle_result!(tx_status, recv.recv().await); - let mut incoming: StdFrame = handle_result!(tx_status, incoming.try_into()); - // On message receive, get the message type from the message header and get the - // message payload - let message_type = - incoming - .get_header() - .ok_or(super::super::error::Error::FramingSv2( - framing_sv2::Error::ExpectedSv2Frame, - )); - - let message_type = handle_result!(tx_status, message_type).msg_type(); - - let payload = incoming.payload(); - - // Gets the response message for the received SV2 Upstream role message - // `handle_message_mining` takes care of the SetupConnection + - // SetupConnection.Success - let next_message_to_send = - Upstream::handle_message_mining(self_.clone(), message_type, payload); - - // Routes the incoming messages accordingly - match next_message_to_send { - // No translation required, simply respond to SV2 pool w a SV2 message - Ok(SendTo::Respond(message_for_upstream)) => { - let message = Message::Mining(message_for_upstream); - - let frame: StdFrame = handle_result!(tx_status, message.try_into()); - let frame: EitherFrame = frame.into(); - - // Relay the response message to the Upstream role - handle_result!(tx_status, tx_frame.send(frame).await); - } - // Does not send the messages anywhere, but instead handle them internally - Ok(SendTo::None(Some(m))) => { - match m { - Mining::OpenExtendedMiningChannelSuccess(m) => { - let prefix_len = m.extranonce_prefix.len(); - // update upstream_extranonce1_size for tracking - let miner_extranonce2_size = self_ - .safe_lock(|u| { - u.upstream_extranonce1_size = prefix_len; - u.min_extranonce_size as usize - }) - .map_err(|_e| PoisonLock); - let miner_extranonce2_size = - handle_result!(tx_status, miner_extranonce2_size); - let extranonce_prefix: Extranonce = m.extranonce_prefix.into(); - // Create the extended extranonce that will be saved in bridge and - // it will be used to open downstream (sv1) channels - // range 0 is the extranonce1 from upstream - // range 1 is the extranonce1 added by the tproxy - // range 2 is the extranonce2 used by the miner for rolling - // range 0 + range 1 is the extranonce1 sent to the miner - let tproxy_e1_len = super::super::utils::proxy_extranonce1_len( - m.extranonce_size as usize, - miner_extranonce2_size, - ); - let range_0 = 0..prefix_len; // upstream extranonce1 - let range_1 = prefix_len..prefix_len + tproxy_e1_len; // downstream extranonce1 - let range_2 = prefix_len + tproxy_e1_len - ..prefix_len + m.extranonce_size as usize; // extranonce2 - let extended = handle_result!(tx_status, ExtendedExtranonce::from_upstream_extranonce( - extranonce_prefix.clone(), range_0.clone(), range_1.clone(), range_2.clone(), - ).map_err(|err| InvalidExtranonce(format!("Impossible to create a valid extended extranonce from {:?} {:?} {:?} {:?}: {:?}", - extranonce_prefix, range_0, range_1, range_2, err)))); - handle_result!( - tx_status, - tx_sv2_extranonce.send((extended, m.channel_id)).await - ); - } - Mining::NewExtendedMiningJob(m) => { - let job_id = m.job_id; - let res = self_ - .safe_lock(|s| { - let _ = s.job_id.insert(job_id); - }) - .map_err(|_e| PoisonLock); - handle_result!(tx_status, res); - handle_result!(tx_status, tx_sv2_new_ext_mining_job.send(m).await); - } - Mining::SetNewPrevHash(m) => { - handle_result!(tx_status, tx_sv2_set_new_prev_hash.send(m).await); - } - Mining::CloseChannel(_m) => { - error!("Received Mining::CloseChannel msg from upstream!"); - handle_result!(tx_status, Err(NoUpstreamsConnected)); - } - Mining::OpenMiningChannelError(_) - | Mining::UpdateChannelError(_) - | Mining::SubmitSharesError(_) - | Mining::SetCustomMiningJobError(_) => { - error!("parse_incoming SV2 protocol error Message"); - handle_result!(tx_status, Err(m)); - } - // impossible state: handle_message_mining only returns - // the above 3 messages in the Ok(SendTo::None(Some(m))) case to be sent - // to the bridge for translation. - _ => panic!(), - } - } - Ok(SendTo::None(None)) => (), - // No need to handle impossible state just panic cause are impossible and we - // will never panic ;-) Verified: handle_message_mining only either panics, - // returns Ok(SendTo::None(None)) or Ok(SendTo::None(Some(m))), or returns Err - Ok(_) => panic!(), - Err(e) => { - let status = status::Status { - state: status::State::UpstreamShutdown(UpstreamIncoming(e)), - }; - error!( - "TERMINATING: Error handling pool role message: {:?}", - status - ); - if let Err(e) = tx_status.send(status).await { - error!("Status channel down: {:?}", e); - } - - break; - } - } - } - }); - let _ = collector2 - .safe_lock(|a| a.push((parse_incoming.abort_handle(), "parse_incoming".to_string()))); - - Ok(()) - } - - // Retrieves the current job ID. - // - // If work selection is enabled (which it is not for a Translator Proxy), - // it would return the last `SetCustomMiningJobSuccess` job ID. If - // work selection is disabled, it returns the job ID from the last - // `NewExtendedMiningJob` - #[allow(clippy::result_large_err)] - fn get_job_id( - self_: &Arc>, - ) -> Result>, super::super::error::Error<'static>> - { - self_ - .safe_lock(|s| { - if s.is_work_selection_enabled() { - s.last_job_id - .ok_or(super::super::error::Error::RolesSv2Logic( - RolesLogicError::NoValidTranslatorJob, - )) - } else { - s.job_id.ok_or(super::super::error::Error::RolesSv2Logic( - RolesLogicError::NoValidJob, - )) - } - }) - .map_err(|_e| PoisonLock) - } - - /// Spawns a task to handle outgoing `SubmitSharesExtended` messages. - /// - /// This task continuously receives `SubmitSharesExtended` messages from the - /// `rx_sv2_submit_shares_ext` channel (populated by the Bridge). It updates - /// the channel ID and job ID in the submit message (ensuring they match - /// the current upstream channel details), encodes the message into an SV2 frame, - /// and sends it to the upstream server. - #[allow(clippy::result_large_err)] - pub fn handle_submit(self_: Arc>) -> ProxyResult<'static, ()> { - let task_collector = self_.safe_lock(|s| s.task_collector.clone()).unwrap(); - let clone = self_.clone(); - let (tx_frame, receiver, tx_status) = clone.safe_lock(|s| { - ( - s.connection.sender.clone(), - s.rx_sv2_submit_shares_ext.clone(), - s.tx_status.clone(), - ) - })?; - - let handle_submit = tokio::task::spawn(async move { - loop { - let mut sv2_submit: SubmitSharesExtended = - handle_result!(tx_status, receiver.recv().await); - - let channel_id = self_ - .safe_lock(|s| { - s.channel_id - .ok_or(super::super::error::Error::RolesSv2Logic( - RolesLogicError::NotFoundChannelId, - )) - }) - .map_err(|_e| PoisonLock); - sv2_submit.channel_id = - handle_result!(tx_status, handle_result!(tx_status, channel_id)); - let job_id = Self::get_job_id(&self_); - sv2_submit.job_id = handle_result!(tx_status, handle_result!(tx_status, job_id)); - - let message = Message::Mining( - roles_logic_sv2::parsers::Mining::SubmitSharesExtended(sv2_submit), - ); - - let frame: StdFrame = handle_result!(tx_status, message.try_into()); - // Doesnt actually send because of Braiins Pool issue that needs to be fixed - - let frame: EitherFrame = frame.into(); - handle_result!(tx_status, tx_frame.send(frame).await); - } - }); - let _ = task_collector - .safe_lock(|a| a.push((handle_submit.abort_handle(), "handle_submit".to_string()))); - - Ok(()) - } - - // Unimplemented method to check if a submitted share is contained within the upstream target. - // - // This method is currently unimplemented (`todo!()`). Its purpose would be - // to validate a share against the target set by the upstream pool. - fn _is_contained_in_upstream_target(&self, _share: SubmitSharesExtended) -> bool { - todo!() - } - - // Creates the initial `SetupConnection` message for the SV2 handshake. - // - // This message contains information about the proxy acting as a mining device, - // including supported protocol versions, flags, and hardcoded endpoint details. - // - // TODO: The Mining Device information is currently hardcoded. It should ideally - // be configurable or derived from the downstream connections. - #[allow(clippy::result_large_err)] - fn get_setup_connection_message( - min_version: u16, - max_version: u16, - is_work_selection_enabled: bool, - ) -> ProxyResult<'static, SetupConnection<'static>> { - let endpoint_host = "0.0.0.0".to_string().into_bytes().try_into()?; - let vendor = String::new().try_into()?; - let hardware_version = String::new().try_into()?; - let firmware = String::new().try_into()?; - let device_id = String::new().try_into()?; - let flags = match is_work_selection_enabled { - false => 0b0000_0000_0000_0000_0000_0000_0000_0100, - true => 0b0000_0000_0000_0000_0000_0000_0000_0110, - }; - Ok(SetupConnection { - protocol: Protocol::MiningProtocol, - min_version, - max_version, - flags, - endpoint_host, - endpoint_port: 50, - vendor, - hardware_version, - firmware, - device_id, - }) - } -} - -// Can be removed? -impl IsUpstream for Upstream { - fn get_version(&self) -> u16 { - todo!() - } - - fn get_flags(&self) -> u32 { - todo!() - } - - fn get_supported_protocols(&self) -> Vec { - todo!() - } - - fn get_id(&self) -> u32 { - todo!() - } - - fn get_mapper(&mut self) -> Option<&mut roles_logic_sv2::common_properties::RequestIdMapper> { - todo!() - } -} - -// Can be removed? -impl IsMiningUpstream for Upstream { - fn total_hash_rate(&self) -> u64 { - todo!() - } - - fn add_hash_rate(&mut self, _to_add: u64) { - todo!() - } - - fn get_opened_channels( - &mut self, - ) -> &mut Vec { - todo!() - } - - fn update_channels(&mut self, _c: roles_logic_sv2::common_properties::UpstreamChannel) { - todo!() - } -} - -impl ParseCommonMessagesFromUpstream for Upstream { - // Handles the SV2 `SetupConnectionSuccess` message received from the upstream. - // - // Returns `Ok(SendToCommon::None(None))` as this message is handled internally - // and does not require a direct response or forwarding. - fn handle_setup_connection_success( - &mut self, - m: roles_logic_sv2::common_messages_sv2::SetupConnectionSuccess, - ) -> Result { - info!( - "Received `SetupConnectionSuccess`: version={}, flags={:b}", - m.used_version, m.flags - ); - Ok(SendToCommon::None(None)) - } - - fn handle_setup_connection_error( - &mut self, - _: roles_logic_sv2::common_messages_sv2::SetupConnectionError, - ) -> Result { - todo!() - } - - fn handle_channel_endpoint_changed( - &mut self, - _: roles_logic_sv2::common_messages_sv2::ChannelEndpointChanged, - ) -> Result { - todo!() - } - - fn handle_reconnect(&mut self, _m: Reconnect) -> Result { - todo!() - } -} - -/// Connection-wide SV2 Upstream role messages parser implemented by a downstream ("downstream" -/// here is relative to the SV2 Upstream role and is represented by this `Upstream` struct). -impl ParseMiningMessagesFromUpstream for Upstream { - /// Returns the type of channel used between this proxy and the SV2 Upstream. - /// For a Translator Proxy, this is always `Extended`. - fn get_channel_type(&self) -> SupportedChannelTypes { - SupportedChannelTypes::Extended - } - - /// Indicates whether work selection is enabled for this upstream connection. - /// For a Translator Proxy, work selection is handled by the upstream pool, - /// so this method always returns `false`. - fn is_work_selection_enabled(&self) -> bool { - false - } - - /// The SV2 `OpenStandardMiningChannelSuccess` message is NOT handled because it is NOT used - /// for the Translator Proxy as only `Extended` channels are used between the SV1/SV2 Translator - /// Proxy and the SV2 Upstream role. - fn handle_open_standard_mining_channel_success( - &mut self, - _m: roles_logic_sv2::mining_sv2::OpenStandardMiningChannelSuccess, - ) -> Result, RolesLogicError> { - panic!("Standard Mining Channels are not used in Translator Proxy") - } - - /// Handles the SV2 `OpenExtendedMiningChannelSuccess` message. - /// - /// This message is received after requesting to open an extended mining channel. - /// It provides the assigned `channel_id`, the extranonce prefix, the initial - /// mining `target`, and the expected `extranonce_size`. It stores the `channel_id` and - /// `extranonce_prefix`, updates the shared `target`, and prepares the extranonce - /// information (including calculating the size for the TProxy's added extranonce1) to be - /// sent to the Downstream handler for use with SV1 clients. - /// - /// Returns `Ok(SendTo::None(Some(Mining::OpenExtendedMiningChannelSuccess)))` - /// to indicate that the message has been handled internally and should be - /// forwarded to the Bridge. - fn handle_open_extended_mining_channel_success( - &mut self, - m: roles_logic_sv2::mining_sv2::OpenExtendedMiningChannelSuccess, - ) -> Result, RolesLogicError> { - info!( - "Received OpenExtendedMiningChannelSuccess with request id: {} and channel id: {}", - m.request_id, m.channel_id - ); - debug!("OpenStandardMiningChannelSuccess: {:?}", m); - let tproxy_e1_len = super::super::utils::proxy_extranonce1_len( - m.extranonce_size as usize, - self.min_extranonce_size.into(), - ) as u16; - if self.min_extranonce_size + tproxy_e1_len < m.extranonce_size { - return Err(RolesLogicError::InvalidExtranonceSize( - self.min_extranonce_size, - m.extranonce_size, - )); - } - self.target.safe_lock(|t| *t = m.target.to_vec())?; - - info!("Up: Successfully Opened Extended Mining Channel"); - self.channel_id = Some(m.channel_id); - self.extranonce_prefix = Some(m.extranonce_prefix.to_vec()); - let m = Mining::OpenExtendedMiningChannelSuccess(m.into_static()); - Ok(SendTo::None(Some(m))) - } - - /// Handles the SV2 `OpenExtendedMiningChannelError` message (TODO). - fn handle_open_mining_channel_error( - &mut self, - m: roles_logic_sv2::mining_sv2::OpenMiningChannelError, - ) -> Result, RolesLogicError> { - error!( - "Received OpenExtendedMiningChannelError with error code {}", - std::str::from_utf8(m.error_code.as_ref()).unwrap_or("unknown error code") - ); - Ok(SendTo::None(Some(Mining::OpenMiningChannelError( - m.as_static(), - )))) - } - - /// Handles the SV2 `UpdateChannelError` message (TODO). - fn handle_update_channel_error( - &mut self, - m: roles_logic_sv2::mining_sv2::UpdateChannelError, - ) -> Result, RolesLogicError> { - error!( - "Received UpdateChannelError with error code {}", - std::str::from_utf8(m.error_code.as_ref()).unwrap_or("unknown error code") - ); - Ok(SendTo::None(Some(Mining::UpdateChannelError( - m.as_static(), - )))) - } - - /// Handles the SV2 `CloseChannel` message (TODO). - fn handle_close_channel( - &mut self, - m: roles_logic_sv2::mining_sv2::CloseChannel, - ) -> Result, RolesLogicError> { - info!("Received CloseChannel for channel id: {}", m.channel_id); - Ok(SendTo::None(Some(Mining::CloseChannel(m.as_static())))) - } - - /// Handles the SV2 `SetExtranoncePrefix` message (TODO). - fn handle_set_extranonce_prefix( - &mut self, - _: roles_logic_sv2::mining_sv2::SetExtranoncePrefix, - ) -> Result, RolesLogicError> { - todo!() - } - - /// Handles the SV2 `SubmitSharesSuccess` message. - fn handle_submit_shares_success( - &mut self, - m: roles_logic_sv2::mining_sv2::SubmitSharesSuccess, - ) -> Result, RolesLogicError> { - info!("Received SubmitSharesSuccess"); - debug!("SubmitSharesSuccess: {:?}", m); - Ok(SendTo::None(None)) - } - - /// Handles the SV2 `SubmitSharesError` message. - fn handle_submit_shares_error( - &mut self, - m: roles_logic_sv2::mining_sv2::SubmitSharesError, - ) -> Result, RolesLogicError> { - error!( - "Received SubmitSharesError with error code {}", - std::str::from_utf8(m.error_code.as_ref()).unwrap_or("unknown error code") - ); - Ok(SendTo::None(None)) - } - - /// The SV2 `NewMiningJob` message is NOT handled because it is NOT used for the Translator - /// Proxy as only `Extended` channels are used between the SV1/SV2 Translator Proxy and the SV2 - /// Upstream role. - fn handle_new_mining_job( - &mut self, - _m: roles_logic_sv2::mining_sv2::NewMiningJob, - ) -> Result, RolesLogicError> { - panic!("Standard Mining Channels are not used in Translator Proxy") - } - - /// Handles the SV2 `NewExtendedMiningJob` message which is used (along with the SV2 - /// `SetNewPrevHash` message) to later create a SV1 `mining.notify` for the Downstream - /// role. - fn handle_new_extended_mining_job( - &mut self, - m: NewExtendedMiningJob, - ) -> Result, RolesLogicError> { - info!( - "Received new extended mining job for channel id: {} with job id: {} is_future: {}", - m.channel_id, - m.job_id, - m.is_future() - ); - debug!("NewExtendedMiningJob: {:?}", m); - if self.is_work_selection_enabled() { - Ok(SendTo::None(None)) - } else { - IS_NEW_JOB_HANDLED.store(false, std::sync::atomic::Ordering::SeqCst); - if !m.version_rolling_allowed { - warn!("VERSION ROLLING NOT ALLOWED IS A TODO"); - // todo!() - } - - let message = Mining::NewExtendedMiningJob(m.into_static()); - - Ok(SendTo::None(Some(message))) - } - } - - /// Handles the SV2 `SetNewPrevHash` message which is used (along with the SV2 - /// `NewExtendedMiningJob` message) to later create a SV1 `mining.notify` for the Downstream - /// role. - fn handle_set_new_prev_hash( - &mut self, - m: SetNewPrevHash, - ) -> Result, RolesLogicError> { - info!( - "Received SetNewPrevHash channel id: {}, job id: {}", - m.channel_id, m.job_id - ); - debug!("SetNewPrevHash: {:?}", m); - if self.is_work_selection_enabled() { - Ok(SendTo::None(None)) - } else { - let message = Mining::SetNewPrevHash(m.into_static()); - Ok(SendTo::None(Some(message))) - } - } - - /// Handles the SV2 `SetCustomMiningJobSuccess` message (TODO). - fn handle_set_custom_mining_job_success( - &mut self, - m: roles_logic_sv2::mining_sv2::SetCustomMiningJobSuccess, - ) -> Result, RolesLogicError> { - info!( - "Received SetCustomMiningJobSuccess for channel id: {} for job id: {}", - m.channel_id, m.job_id - ); - debug!("SetCustomMiningJobSuccess: {:?}", m); - self.last_job_id = Some(m.job_id); - Ok(SendTo::None(None)) - } - - /// Handles the SV2 `SetCustomMiningJobError` message (TODO). - fn handle_set_custom_mining_job_error( - &mut self, - _m: roles_logic_sv2::mining_sv2::SetCustomMiningJobError, - ) -> Result, RolesLogicError> { - unimplemented!() - } - - /// Handles the SV2 `SetTarget` message which updates the Downstream role(s) target - /// difficulty via the SV1 `mining.set_difficulty` message. - fn handle_set_target( - &mut self, - m: roles_logic_sv2::mining_sv2::SetTarget, - ) -> Result, RolesLogicError> { - info!("Received SetTarget for channel id: {}", m.channel_id); - debug!("SetTarget: {:?}", m); - let m = m.into_static(); - self.target.safe_lock(|t| *t = m.maximum_target.to_vec())?; - Ok(SendTo::None(None)) - } - - fn handle_set_group_channel( - &mut self, - _m: SetGroupChannel, - ) -> Result, RolesLogicError> { - todo!() - } -} diff --git a/roles/translator/src/lib/upstream_sv2/upstream_connection.rs b/roles/translator/src/lib/upstream_sv2/upstream_connection.rs deleted file mode 100644 index ef4d6a0a5a..0000000000 --- a/roles/translator/src/lib/upstream_sv2/upstream_connection.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! ## Upstream SV2 Connection Module -//! -//! Defines [`UpstreamConnection`], the structure responsible for managing the -//! communication channels with an upstream. - -use super::{super::error::ProxyResult, EitherFrame, StdFrame}; -use async_channel::{Receiver, Sender}; - -/// Handles the sending and receiving of messages to and from an SV2 Upstream role (most typically -/// a SV2 Pool server). -/// On upstream, we have a sv2connection, so we use the connection from network helpers -/// use network_helpers::Connection; -/// this does the dirty work of reading byte by byte in the socket and puts them in a complete -/// Sv2Messages frame and when the message is ready then sends to our Upstream -/// sender_incoming + receiver_outgoing are in network_helpers::Connection -#[derive(Debug, Clone)] -pub struct UpstreamConnection { - /// Receives messages from the SV2 Upstream role - pub receiver: Receiver, - /// Sends messages to the SV2 Upstream role - pub sender: Sender, -} - -impl UpstreamConnection { - /// Send a SV2 message to the Upstream role - pub async fn send(&mut self, sv2_frame: StdFrame) -> ProxyResult<'static, ()> { - let either_frame = sv2_frame.into(); - self.sender.send(either_frame).await?; - Ok(()) - } -} diff --git a/roles/translator/src/lib/utils.rs b/roles/translator/src/lib/utils.rs index 9668db0384..3a397a000b 100644 --- a/roles/translator/src/lib/utils.rs +++ b/roles/translator/src/lib/utils.rs @@ -1,15 +1,302 @@ -/// Calculates the required length of the proxy's extranonce1. -/// -/// The proxy needs to calculate an extranonce1 value to send to the -/// upstream server. This function determines the length of that -/// extranonce1 value -/// FIXME: The pool only supported 16 bytes exactly for its -/// `extranonce1` field is no longer the case and the -/// code needs to be changed to support variable `extranonce1` lengths. -pub fn proxy_extranonce1_len( - channel_extranonce2_size: usize, - downstream_extranonce2_len: usize, +use buffer_sv2::Slice; +use stratum_common::roles_logic_sv2::{ + bitcoin::{ + block::{Header, Version}, + hashes::Hash, + CompactTarget, TxMerkleNode, + }, + channels_sv2::{ + merkle_root::merkle_root_from_path, + target::{bytes_to_hex, u256_to_block_hash}, + }, + codec_sv2::{ + binary_sv2::{Sv2DataType, U256}, + Frame, + }, + mining_sv2::Target, + parsers_sv2::{AnyMessage, CommonMessages}, + utils::Mutex, +}; +use tracing::{debug, error}; +use v1::{client_to_server, utils::HexU32Be}; + +use crate::error::TproxyError; + +/// Validates an SV1 share against the target difficulty and job parameters. +/// +/// This function performs complete share validation by: +/// 1. Finding the corresponding job from the valid jobs storage +/// 2. Constructing the full extranonce from extranonce1 and extranonce2 +/// 3. Calculating the merkle root from the coinbase transaction and merkle path +/// 4. Building the block header with the share's nonce and timestamp +/// 5. Hashing the header and comparing against the target difficulty +/// +/// # Arguments +/// * `share` - The SV1 submit message containing the share data +/// * `target` - The target difficulty for this share +/// * `extranonce1` - The first part of the extranonce (from server) +/// * `version_rolling_mask` - Optional mask for version rolling +/// * `sv1_server_data` - Reference to shared SV1 server data for accessing valid jobs +/// * `channel_id` - Channel ID for job lookup +/// +/// # Returns +/// * `Ok(true)` if the share is valid and meets the target +/// * `Ok(false)` if the share is valid but doesn't meet the target +/// * `Err(TproxyError)` if validation fails due to missing job or invalid data +pub fn validate_sv1_share( + share: &client_to_server::Submit<'static>, + target: Target, + extranonce1: Vec, + version_rolling_mask: Option, + sv1_server_data: std::sync::Arc>, + channel_id: u32, +) -> Result { + let job_id = share.job_id.clone(); + + // Access valid jobs based on the configured mode + let job = sv1_server_data + .super_safe_lock(|server_data| { + if let Some(ref aggregated_jobs) = server_data.aggregated_valid_jobs { + // Aggregated mode: search in shared jobs + aggregated_jobs + .iter() + .find(|job| job.job_id == job_id) + .cloned() + } else if let Some(ref non_aggregated_jobs) = server_data.non_aggregated_valid_jobs { + // Non-aggregated mode: search in channel-specific jobs + non_aggregated_jobs + .get(&channel_id) + .and_then(|channel_jobs| channel_jobs.iter().find(|job| job.job_id == job_id)) + .cloned() + } else { + None + } + }) + .ok_or(TproxyError::JobNotFound)?; + + let mut full_extranonce = vec![]; + full_extranonce.extend_from_slice(extranonce1.as_slice()); + full_extranonce.extend_from_slice(share.extra_nonce2.0.as_ref()); + + let share_version = share + .version_bits + .clone() + .map(|vb| vb.0) + .unwrap_or(job.version.0); + let mask = version_rolling_mask.unwrap_or(HexU32Be(0x1FFFE000_u32)).0; + let version = (job.version.0 & !mask) | (share_version & mask); + + let prev_hash_vec: Vec = job.prev_hash.clone().into(); + let prev_hash = U256::from_vec_(prev_hash_vec).map_err(TproxyError::BinarySv2)?; + + // calculate the merkle root from: + // - job coinbase_tx_prefix + // - full extranonce + // - job coinbase_tx_suffix + // - job merkle_path + let merkle_root: [u8; 32] = merkle_root_from_path( + job.coin_base1.as_ref(), + job.coin_base2.as_ref(), + full_extranonce.as_ref(), + job.merkle_branch.as_ref(), + ) + .ok_or(TproxyError::InvalidMerkleRoot)? + .try_into() + .map_err(|_| TproxyError::InvalidMerkleRoot)?; + + // create the header for validation + let header = Header { + version: Version::from_consensus(version as i32), + prev_blockhash: u256_to_block_hash(prev_hash), + merkle_root: TxMerkleNode::from_byte_array(merkle_root), + time: share.time.0, + bits: CompactTarget::from_consensus(job.bits.0), + nonce: share.nonce.0, + }; + + // convert the header hash to a target type for easy comparison + let hash = header.block_hash(); + let raw_hash: [u8; 32] = *hash.to_raw_hash().as_ref(); + let hash_as_target: Target = raw_hash.into(); + + // print hash_as_target and self.target as human readable hex + let hash_as_u256: U256 = hash_as_target.clone().into(); + let mut hash_bytes = hash_as_u256.to_vec(); + hash_bytes.reverse(); // Convert to big-endian for display + let target_u256: U256 = target.clone().into(); + let mut target_bytes = target_u256.to_vec(); + target_bytes.reverse(); // Convert to big-endian for display + + debug!( + "share validation \nshare:\t\t{}\ndownstream target:\t{}\n", + bytes_to_hex(&hash_bytes), + bytes_to_hex(&target_bytes), + ); + // check if the share hash meets the downstream target + if hash_as_target < target { + /*if self.share_accounting.is_share_seen(hash.to_raw_hash()) { + return Err(ShareValidationError::DuplicateShare); + }*/ + + return Ok(true); + } + + Ok(false) +} + +/// Calculates the required length of the proxy's extranonce prefix. +/// +/// This function determines how many bytes the proxy needs to reserve for its own +/// extranonce prefix, based on the difference between the channel's rollable extranonce +/// size and the downstream miner's rollable extranonce size. +/// +/// # Arguments +/// * `channel_rollable_extranonce_size` - Size of the rollable extranonce from the channel +/// * `downstream_rollable_extranonce_size` - Size of the rollable extranonce for downstream +/// +/// # Returns +/// The number of bytes needed for the proxy's extranonce prefix +pub fn proxy_extranonce_prefix_len( + channel_rollable_extranonce_size: usize, + downstream_rollable_extranonce_size: usize, ) -> usize { - // full_extranonce_len - pool_extranonce1_len - miner_extranonce2 = tproxy_extranonce1_len - channel_extranonce2_size - downstream_extranonce2_len + channel_rollable_extranonce_size - downstream_rollable_extranonce_size +} + +/// Extracts message type, payload, and parsed message from an SV2 frame. +/// +/// This function processes an SV2 frame and extracts the essential components: +/// - Message type identifier +/// - Raw payload bytes +/// - Parsed message structure +/// +/// # Arguments +/// * `frame` - The SV2 frame to process +/// +/// # Returns +/// A tuple containing (message_type, payload, parsed_message) on success, +/// or a TproxyError if the frame is invalid or cannot be parsed +pub fn message_from_frame( + frame: &mut Frame, Slice>, +) -> Result<(u8, Vec, AnyMessage<'static>), TproxyError> { + match frame { + Frame::Sv2(frame) => { + let header = frame + .get_header() + .ok_or(TproxyError::UnexpectedMessage(0))?; + let message_type = header.msg_type(); + let mut payload = frame.payload().to_vec(); + let message: Result, _> = + (message_type, payload.as_mut_slice()).try_into(); + match message { + Ok(message) => { + let message = into_static(message)?; + Ok((message_type, payload.to_vec(), message)) + } + Err(_) => { + error!("Received frame with invalid payload or message type: {frame:?}"); + Err(TproxyError::UnexpectedMessage(message_type)) + } + } + } + Frame::HandShake(f) => { + error!("Received unexpected handshake frame: {f:?}"); + Err(TproxyError::UnexpectedMessage(0)) + } + } +} + +/// Converts a borrowed AnyMessage to a static lifetime version. +/// +/// This function takes an AnyMessage with a borrowed lifetime and converts it to +/// a static lifetime version, which is necessary for storing messages across +/// async boundaries and in data structures. +/// +/// # Arguments +/// * `m` - The AnyMessage to convert to static lifetime +/// +/// # Returns +/// A static lifetime version of the message, or TproxyError if the message +/// type is not supported for static conversion +pub fn into_static(m: AnyMessage<'_>) -> Result, TproxyError> { + match m { + AnyMessage::Mining(m) => Ok(AnyMessage::Mining(m.into_static())), + AnyMessage::Common(m) => match m { + CommonMessages::ChannelEndpointChanged(m) => Ok(AnyMessage::Common( + CommonMessages::ChannelEndpointChanged(m.into_static()), + )), + CommonMessages::SetupConnection(m) => Ok(AnyMessage::Common( + CommonMessages::SetupConnection(m.into_static()), + )), + CommonMessages::SetupConnectionError(m) => Ok(AnyMessage::Common( + CommonMessages::SetupConnectionError(m.into_static()), + )), + CommonMessages::SetupConnectionSuccess(m) => Ok(AnyMessage::Common( + CommonMessages::SetupConnectionSuccess(m.into_static()), + )), + CommonMessages::Reconnect(m) => Ok(AnyMessage::Common(CommonMessages::Reconnect( + m.into_static(), + ))), + }, + _ => Err(TproxyError::UnexpectedMessage(0)), + } +} + +/// Messages used for coordinating shutdown across different components. +/// +/// This enum defines the different types of shutdown signals that can be sent +/// through the broadcast channel to coordinate graceful shutdown of the translator. +#[derive(Debug, Clone)] +pub enum ShutdownMessage { + /// Shutdown all components immediately + ShutdownAll, + /// Shutdown all downstream connections + DownstreamShutdownAll, + /// Shutdown a specific downstream connection by ID + DownstreamShutdown(u32), + /// Reset channel manager state and shutdown downstreams due to upstream reconnection + UpstreamReconnectedResetAndShutdownDownstreams, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_proxy_extranonce_prefix_len() { + assert_eq!(proxy_extranonce_prefix_len(8, 4), 4); + assert_eq!(proxy_extranonce_prefix_len(10, 6), 4); + assert_eq!(proxy_extranonce_prefix_len(4, 4), 0); + } + + #[test] + fn test_shutdown_message_debug() { + let msg1 = ShutdownMessage::ShutdownAll; + let msg2 = ShutdownMessage::DownstreamShutdown(123); + let msg3 = ShutdownMessage::DownstreamShutdownAll; + let msg4 = ShutdownMessage::UpstreamReconnectedResetAndShutdownDownstreams; + + // Test Debug implementation + assert!(format!("{:?}", msg1).contains("ShutdownAll")); + assert!(format!("{:?}", msg2).contains("DownstreamShutdown")); + assert!(format!("{:?}", msg2).contains("123")); + assert!(format!("{:?}", msg3).contains("DownstreamShutdownAll")); + assert!(format!("{:?}", msg4).contains("UpstreamReconnected")); + } + + #[test] + fn test_shutdown_message_clone() { + let msg = ShutdownMessage::DownstreamShutdown(456); + let cloned = msg.clone(); + + match (msg, cloned) { + ( + ShutdownMessage::DownstreamShutdown(id1), + ShutdownMessage::DownstreamShutdown(id2), + ) => { + assert_eq!(id1, id2); + } + _ => panic!("Clone failed"), + } + } } diff --git a/roles/translator/src/main.rs b/roles/translator/src/main.rs index 7fe418c09a..aef52cc1e0 100644 --- a/roles/translator/src/main.rs +++ b/roles/translator/src/main.rs @@ -1,9 +1,8 @@ mod args; -pub use translator_sv2::{ - config, downstream_sv1, error, proxy, status, upstream_sv2, TranslatorSv2, -}; +use std::process; -use tracing::info; +use config_helpers_sv2::logging::init_logging; +pub use translator_sv2::{config, error, status, sv1, sv2, TranslatorSv2}; use crate::args::process_cli_args; @@ -13,13 +12,14 @@ use crate::args::process_cli_args; /// defined in `translator_sv2::TranslatorSv2`. Errors during startup are logged. #[tokio::main] async fn main() { - tracing_subscriber::fmt::init(); + let proxy_config = process_cli_args().unwrap_or_else(|e| { + eprintln!("Translator proxy config error: {e}"); + std::process::exit(1); + }); - let proxy_config = match process_cli_args() { - Ok(p) => p, - Err(e) => panic!("failed to load config: {}", e), - }; - info!("Proxy Config: {:?}", &proxy_config); + init_logging(proxy_config.log_dir()); TranslatorSv2::new(proxy_config).start().await; + + process::exit(1); } diff --git a/scripts/build_header.sh b/scripts/build_header.sh deleted file mode 100755 index 6f0b4957de..0000000000 --- a/scripts/build_header.sh +++ /dev/null @@ -1,16 +0,0 @@ -#! /bin/sh -cargo install --version 0.21.0 cbindgen --force - -rm -f ./sv2.h -touch ./sv2.h - -dir=${1:-../protocols} - -cd "$dir" - cbindgen --crate const_sv2 >> ../scripts/sv2.h - cbindgen --crate binary_codec_sv2 >> ../scripts/sv2.h - cbindgen --crate common_messages_sv2 >> ../scripts/sv2.h - cbindgen --crate template_distribution_sv2 >> ../scripts/sv2.h - cbindgen --crate codec_sv2 >> ../scripts/sv2.h - cbindgen --crate sv2_ffi >> ../scripts/sv2.h -cd .. diff --git a/scripts/coverage-protocols.sh b/scripts/coverage-protocols.sh index d53aa982c9..a0bf33dcd4 100755 --- a/scripts/coverage-protocols.sh +++ b/scripts/coverage-protocols.sh @@ -14,6 +14,7 @@ crates=( "v2/binary-sv2/codec" "v2/binary-sv2/derive_codec" "v2/binary-sv2" + "v2/channels-sv2" "v2/noise-sv2" "v2/framing-sv2" "v2/codec-sv2" @@ -21,8 +22,8 @@ crates=( "v2/subprotocols/template-distribution" "v2/subprotocols/mining" "v2/subprotocols/job-declaration" - "v2/sv2-ffi" "v2/roles-logic-sv2" + "v2/parsers-sv2" ) for crate in "${crates[@]}"; do diff --git a/scripts/coverage-roles.sh b/scripts/coverage-roles.sh index 6ba4df60c6..4106c608e0 100755 --- a/scripts/coverage-roles.sh +++ b/scripts/coverage-roles.sh @@ -10,10 +10,8 @@ cd roles tarpaulin crates=( - "mining-proxy" "pool" "test-utils/mining-device" - "test-utils/mining-device-sv1" "translator" "jd-client" "jd-server" diff --git a/scripts/sv2-publish.sh b/scripts/sv2-publish.sh index af40af763a..2493dcf778 100755 --- a/scripts/sv2-publish.sh +++ b/scripts/sv2-publish.sh @@ -22,7 +22,6 @@ output=$(cargo smart-release \ job_declaration_sv2 \ mining_sv2 \ template_distribution_sv2 \ - sv2_ffi \ buffer_sv2 \ error_handling \ network_helpers \ diff --git a/test/integration-tests/.config/nextest.toml b/test/integration-tests/.config/nextest.toml new file mode 100644 index 0000000000..5f5d26a09c --- /dev/null +++ b/test/integration-tests/.config/nextest.toml @@ -0,0 +1,17 @@ +[profile.default] + +# SRI has flaky integration tests, which we are ok to live with for now +# but if a test fails more than 3 times, it's safe to assume it's failing deterministically +# and that's a reliable indication that we shouldn't merge this PR +retries = { backoff = "fixed", count = 3, delay = "2s" } + +# only run one test at a time, which allows a human-friendly experience for inspecting logs +test-threads = 1 + +# label as slow if a test runs for more than 60s +# kill it after 120s +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 diff --git a/test/integration-tests/Cargo.lock b/test/integration-tests/Cargo.lock index a59069cd4a..5962b180b6 100644 --- a/test/integration-tests/Cargo.lock +++ b/test/integration-tests/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aead" @@ -58,21 +58,30 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "once_cell", "version_check", ] [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", "once_cell", "version_check", - "zerocopy 0.7.35", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", ] [[package]] @@ -83,9 +92,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", @@ -98,44 +107,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", - "once_cell", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.95" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arraydeque" @@ -179,18 +188,18 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] name = "async-trait" -version = "0.1.86" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -201,15 +210,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -217,7 +226,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -242,6 +251,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bech32" version = "0.11.0" @@ -250,14 +265,14 @@ checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" [[package]] name = "binary_codec_sv2" -version = "2.0.0" +version = "3.0.0" dependencies = [ "buffer_sv2", ] [[package]] name = "binary_sv2" -version = "3.0.0" +version = "4.0.0" dependencies = [ "binary_codec_sv2", "derive_codec_sv2", @@ -265,9 +280,9 @@ dependencies = [ [[package]] name = "bitcoin" -version = "0.32.5" +version = "0.32.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026" +checksum = "0fda569d741b895131a88ee5589a467e73e9c4718e958ac9308e4f7dc44b6945" dependencies = [ "base58ck", "base64 0.21.7", @@ -345,9 +360,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.8.0" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" dependencies = [ "serde", ] @@ -396,13 +411,14 @@ name = "buffer_sv2" version = "2.0.0" dependencies = [ "aes-gcm", + "generic-array", ] [[package]] name = "byte-slice-cast" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" [[package]] name = "byteorder" @@ -412,24 +428,25 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.12" +version = "1.2.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755717a7de9ec452bf7f3f1a3099085deabd7f2962b861dae91ecd7a365903d2" +checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9" dependencies = [ + "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "chacha20" @@ -455,6 +472,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "channels_sv2" +version = "2.0.0" +dependencies = [ + "binary_sv2", + "bitcoin", + "common_messages_sv2", + "job_declaration_sv2", + "mining_sv2", + "primitive-types", + "template_distribution_sv2", + "tracing", +] + [[package]] name = "cipher" version = "0.4.4" @@ -468,9 +499,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.39" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" dependencies = [ "clap_builder", "clap_derive", @@ -478,9 +509,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.39" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" +checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" dependencies = [ "anstream", "anstyle", @@ -490,47 +521,45 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "codec_sv2" -version = "2.1.0" +version = "3.0.1" dependencies = [ "binary_sv2", "buffer_sv2", "framing_sv2", "noise_sv2", "rand 0.8.5", - "stratum-common", "tracing", ] [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "common_messages_sv2" -version = "5.1.0" +version = "6.0.1" dependencies = [ "binary_sv2", - "stratum-common", ] [[package]] @@ -562,12 +591,13 @@ dependencies = [ ] [[package]] -name = "config-helpers" +name = "config_helpers_sv2" version = "0.1.0" dependencies = [ "miniscript", - "roles_logic_sv2", "serde", + "tracing", + "tracing-subscriber", ] [[package]] @@ -585,7 +615,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "once_cell", "tiny-keccak", ] @@ -619,6 +649,22 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "corepc-client" version = "0.7.0" @@ -635,9 +681,9 @@ dependencies = [ [[package]] name = "corepc-node" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cb0b5b9e99b8290eeac6cdccfa4f86821fb49011480d86111d85e26287d128" +checksum = "b2bcc6e09458f052024ec36e4728bd5619e248643da6175876eb3b10ca6d4d86" dependencies = [ "anyhow", "corepc-client", @@ -669,9 +715,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -684,9 +730,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" @@ -754,18 +800,18 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -786,16 +832,22 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "filetime" -version = "0.2.25" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" + [[package]] name = "fixed-hash" version = "0.8.0" @@ -810,9 +862,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", "miniz_oxide", @@ -826,12 +878,11 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "framing_sv2" -version = "5.1.0" +version = "5.0.1" dependencies = [ "binary_sv2", "buffer_sv2", "noise_sv2", - "stratum-common", ] [[package]] @@ -896,7 +947,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] @@ -941,25 +992,25 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] name = "getrandom" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", - "wasi 0.13.3+wasi-0.2.2", - "windows-targets", + "r-efi", + "wasi 0.14.7+wasi-0.2.4", ] [[package]] @@ -980,9 +1031,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "h2" -version = "0.4.7" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -997,6 +1048,19 @@ dependencies = [ "tracing", ] +[[package]] +name = "handlers_sv2" +version = "0.2.0" +dependencies = [ + "binary_sv2", + "common_messages_sv2", + "job_declaration_sv2", + "mining_sv2", + "parsers_sv2", + "template_distribution_sv2", + "trait-variant", +] + [[package]] name = "hashbrown" version = "0.11.2" @@ -1013,15 +1077,15 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash 0.8.11", + "ahash 0.8.12", "allocator-api2", ] [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" [[package]] name = "hashlink" @@ -1076,9 +1140,9 @@ checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" [[package]] name = "http" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", @@ -1097,12 +1161,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "futures-util", + "futures-core", "http", "http-body", "pin-project-lite", @@ -1110,9 +1174,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -1122,13 +1186,14 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http", "http-body", @@ -1136,6 +1201,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -1143,21 +1209,28 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", + "futures-core", "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -1177,24 +1250,24 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] name = "indexmap" -version = "2.7.1" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.16.0", ] [[package]] name = "inout" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ "generic-array", ] @@ -1204,32 +1277,44 @@ name = "integration_tests_sv2" version = "0.1.0" dependencies = [ "async-channel", - "binary_sv2", - "codec_sv2", - "config-helpers", + "config_helpers_sv2", "corepc-node", "flate2", - "jd_client", + "jd_client_sv2", "jd_server", "key-utils", "mining_device", - "mining_device_sv1", - "mining_proxy_sv2", "minreq", - "network_helpers_sv2", "once_cell", "pool_sv2", - "rand 0.9.0", - "roles_logic_sv2", + "rand 0.9.2", "stratum-common", "sv1_api", "tar", "tokio", + "tokio-util", "tracing", "tracing-subscriber", "translator_sv2", ] +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1238,35 +1323,24 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] -name = "jd_client" -version = "0.1.4" +name = "jd_client_sv2" +version = "0.1.0" dependencies = [ "async-channel", - "async-recursion 0.3.2", - "binary_sv2", "buffer_sv2", "clap", - "codec_sv2", "config", - "config-helpers", - "error_handling", - "framing_sv2", - "futures", + "config_helpers_sv2", "key-utils", - "network_helpers_sv2", - "nohash-hasher", - "primitive-types", - "roles_logic_sv2", "serde", "stratum-common", "tokio", "tracing", - "tracing-subscriber", ] [[package]] @@ -1274,36 +1348,29 @@ name = "jd_server" version = "0.1.3" dependencies = [ "async-channel", - "binary_sv2", "buffer_sv2", "clap", - "codec_sv2", "config", - "config-helpers", + "config_helpers_sv2", "error_handling", "hashbrown 0.11.2", "hex", "key-utils", - "network_helpers_sv2", "nohash-hasher", - "noise_sv2", "rand 0.8.5", - "roles_logic_sv2", "rpc_sv2", "serde", "serde_json", "stratum-common", "tokio", "tracing", - "tracing-subscriber", ] [[package]] name = "job_declaration_sv2" -version = "4.0.0" +version = "5.0.1" dependencies = [ "binary_sv2", - "stratum-common", ] [[package]] @@ -1334,6 +1401,7 @@ name = "key-utils" version = "1.2.0" dependencies = [ "bs58", + "generic-array", "rand 0.8.5", "rustversion", "secp256k1 0.28.2", @@ -1348,15 +1416,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.169" +version = "0.2.176" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags", "libc", @@ -1365,15 +1433,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.15" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -1381,15 +1449,24 @@ dependencies = [ [[package]] name = "log" -version = "0.4.25" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "minimal-lexical" @@ -1403,59 +1480,14 @@ version = "0.1.3" dependencies = [ "async-channel", "async-recursion 0.3.2", - "binary_sv2", "buffer_sv2", "clap", - "codec_sv2", "futures", "key-utils", - "network_helpers_sv2", + "num-format", "primitive-types", "rand 0.8.5", - "roles_logic_sv2", - "sha2 0.10.8", - "stratum-common", - "tokio", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "mining_device_sv1" -version = "0.1.0" -dependencies = [ - "async-channel", - "num-bigint", - "num-traits", - "primitive-types", - "roles_logic_sv2", - "serde", - "serde_json", - "stratum-common", - "sv1_api", - "tokio", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "mining_proxy_sv2" -version = "0.1.3" -dependencies = [ - "async-channel", - "async-recursion 0.3.2", - "binary_sv2", - "buffer_sv2", - "clap", - "codec_sv2", - "config", - "futures", - "key-utils", - "network_helpers_sv2", - "nohash-hasher", - "once_cell", - "roles_logic_sv2", - "serde", + "sha2 0.10.9", "stratum-common", "tokio", "tracing", @@ -1464,17 +1496,16 @@ dependencies = [ [[package]] name = "mining_sv2" -version = "4.0.0" +version = "5.0.1" dependencies = [ "binary_sv2", - "stratum-common", ] [[package]] name = "miniscript" -version = "12.3.2" +version = "12.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0760e92feaf4ee26bd2e616f557de64712bf1e75f3b1b218dfb475c0a84c7943" +checksum = "487906208f38448e186e3deb02f2b8ef046a9078b0de00bdb28bf4fb9b76951c" dependencies = [ "bech32", "bitcoin", @@ -1482,9 +1513,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.8.5" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] @@ -1506,25 +1537,23 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", ] [[package]] name = "network_helpers_sv2" -version = "3.1.0" +version = "4.0.1" dependencies = [ "async-channel", - "binary_sv2", "codec_sv2", "futures", "serde_json", - "stratum-common", "sv1_api", "tokio", "tokio-util", @@ -1543,10 +1572,10 @@ version = "1.4.0" dependencies = [ "aes-gcm", "chacha20poly1305", + "generic-array", "rand 0.8.5", "rand_chacha 0.3.1", "secp256k1 0.28.2", - "stratum-common", ] [[package]] @@ -1561,40 +1590,21 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "overload", - "winapi", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", + "windows-sys 0.52.0", ] [[package]] -name = "num-traits" -version = "0.2.19" +name = "num-format" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" dependencies = [ - "autocfg", + "arrayvec", + "itoa", ] [[package]] @@ -1608,9 +1618,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.2" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "opaque-debug" @@ -1628,17 +1644,11 @@ dependencies = [ "hashbrown 0.14.5", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "parity-scale-codec" -version = "3.7.4" +version = "3.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9fde3d0718baf5bc92f577d652001da0f8d54cd03a7974e118d04fc888dc23d" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" dependencies = [ "arrayvec", "bitvec", @@ -1652,21 +1662,21 @@ dependencies = [ [[package]] name = "parity-scale-codec-derive" -version = "3.7.4" +version = "3.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581c837bb6b9541ce7faa9377c20616e4fb7650f6b0f68bc93c827ee504fb7b3" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -1674,15 +1684,27 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "parsers_sv2" +version = "0.1.1" +dependencies = [ + "binary_sv2", + "common_messages_sv2", + "framing_sv2", + "job_declaration_sv2", + "mining_sv2", + "template_distribution_sv2", ] [[package]] @@ -1691,11 +1713,17 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pest" -version = "2.7.15" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" +checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" dependencies = [ "memchr", "thiserror", @@ -1704,9 +1732,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.15" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e" +checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" dependencies = [ "pest", "pest_generator", @@ -1714,26 +1742,26 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.15" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b" +checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] name = "pest_meta" -version = "2.7.15" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" +checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" dependencies = [ "once_cell", "pest", - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] @@ -1777,33 +1805,28 @@ version = "0.1.3" dependencies = [ "async-channel", "async-recursion 1.1.1", - "binary_sv2", "buffer_sv2", "clap", - "codec_sv2", "config", - "config-helpers", + "config_helpers_sv2", "error_handling", "key-utils", - "network_helpers_sv2", "nohash-hasher", - "noise_sv2", "rand 0.8.5", - "roles_logic_sv2", + "secp256k1 0.28.2", "serde", "stratum-common", "tokio", "tracing", - "tracing-subscriber", ] [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.7.35", + "zerocopy", ] [[package]] @@ -1819,31 +1842,37 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.2.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit", + "toml_edit 0.23.6", ] [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.38" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "radium" version = "0.7.0" @@ -1863,13 +1892,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", - "zerocopy 0.8.22", ] [[package]] @@ -1898,7 +1926,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] @@ -1907,47 +1935,65 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.1", + "getrandom 0.3.3", ] [[package]] name = "redox_syscall" -version = "0.5.8" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ "bitflags", ] +[[package]] +name = "regex-automata" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + [[package]] name = "ring" -version = "0.17.8" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "roles_logic_sv2" -version = "3.0.0" +version = "5.0.0" dependencies = [ - "binary_sv2", + "bitcoin", "chacha20poly1305", + "channels_sv2", + "codec_sv2", "common_messages_sv2", - "framing_sv2", + "handlers_sv2", "hex-conservative 0.3.0", "job_declaration_sv2", "mining_sv2", "nohash-hasher", + "parsers_sv2", "primitive-types", - "stratum-common", "template_distribution_sv2", "tracing", ] @@ -1966,7 +2012,7 @@ dependencies = [ [[package]] name = "rpc_sv2" -version = "1.0.0" +version = "1.1.1" dependencies = [ "base64 0.21.7", "hex", @@ -1990,9 +2036,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hex" @@ -2002,15 +2048,15 @@ checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" [[package]] name = "rustix" -version = "0.38.44" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -2037,15 +2083,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.19" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "scopeguard" @@ -2105,41 +2151,52 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.217" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.226" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] name = "serde_json" -version = "1.0.138" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] @@ -2159,13 +2216,23 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", "digest 0.10.7", + "sha2-asm", +] + +[[package]] +name = "sha2-asm" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b845214d6175804686b2bd482bcffe96651bb2d1200742b712003504a2dac1ab" +dependencies = [ + "cc", ] [[package]] @@ -2185,44 +2252,35 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] [[package]] name = "slab" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.8" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "static_assertions" version = "1.1.0" @@ -2231,10 +2289,21 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "stratum-common" -version = "2.0.0" +version = "4.0.1" dependencies = [ - "bitcoin", - "secp256k1 0.28.2", + "network_helpers_sv2", + "roles_logic_sv2", +] + +[[package]] +name = "stratum_translation" +version = "0.1.0" +dependencies = [ + "binary_sv2", + "channels_sv2", + "mining_sv2", + "sv1_api", + "tracing", ] [[package]] @@ -2251,7 +2320,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sv1_api" -version = "1.0.1" +version = "2.1.1" dependencies = [ "binary_sv2", "bitcoin_hashes 0.3.2", @@ -2275,15 +2344,36 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.98" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tap" version = "1.0.1" @@ -2292,9 +2382,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c65998313f8e17d0d553d28f91a0df93e4dbbbf770279c7bc21ca0f09ea1a1f6" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" dependencies = [ "filetime", "libc", @@ -2302,53 +2392,50 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.16.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ - "cfg-if", "fastrand", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] name = "template_distribution_sv2" -version = "3.1.0" +version = "4.0.1" dependencies = [ "binary_sv2", - "stratum-common", ] [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] @@ -2362,21 +2449,23 @@ dependencies = [ [[package]] name = "tokio" -version = "1.44.1" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", + "slab", "socket2", "tokio-macros", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2387,14 +2476,14 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] name = "tokio-util" -version = "0.7.13" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", @@ -2405,38 +2494,75 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.20" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" -version = "0.22.23" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.11", + "toml_write", "winnow", ] +[[package]] +name = "toml_edit" +version = "0.23.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +dependencies = [ + "indexmap", + "toml_datetime 0.7.2", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tower-service" version = "0.3.3" @@ -2456,20 +2582,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", @@ -2488,46 +2614,51 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex-automata", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] +[[package]] +name = "trait-variant" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "translator_sv2" -version = "1.0.0" +version = "2.0.0" dependencies = [ "async-channel", - "async-recursion 0.3.2", - "binary_sv2", "buffer_sv2", "clap", - "codec_sv2", "config", - "error_handling", - "framing_sv2", - "futures", + "config_helpers_sv2", "key-utils", "network_helpers_sv2", - "once_cell", - "primitive-types", - "rand 0.8.5", - "roles_logic_sv2", "serde", "serde_json", "stratum-common", + "stratum_translation", "sv1_api", "tokio", - "tokio-util", "tracing", - "tracing-subscriber", ] [[package]] @@ -2538,9 +2669,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "ucd-trie" @@ -2562,9 +2693,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.16" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-segmentation" @@ -2623,17 +2754,26 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.13.3+wasi-0.2.2" +version = "0.14.7+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" dependencies = [ - "wit-bindgen-rt", + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", ] [[package]] @@ -2652,26 +2792,45 @@ dependencies = [ ] [[package]] -name = "winapi" -version = "0.3.9" +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "windows-link 0.1.3", + "windows-result", + "windows-strings", ] [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "windows-result" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "windows-strings" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] [[package]] name = "windows-sys" @@ -2679,7 +2838,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -2688,7 +2847,25 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-sys" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +dependencies = [ + "windows-link 0.2.0", ] [[package]] @@ -2697,14 +2874,31 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link 0.1.3", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", ] [[package]] @@ -2713,65 +2907,110 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" -version = "0.7.1" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] [[package]] -name = "wit-bindgen-rt" -version = "0.33.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" -dependencies = [ - "bitflags", -] +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "wyz" @@ -2795,43 +3034,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ - "byteorder", - "zerocopy-derive 0.7.35", -] - -[[package]] -name = "zerocopy" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09612fda0b63f7cb9e0af7e5916fe5a1f8cdcb066829f10f36883207628a4872" -dependencies = [ - "zerocopy-derive 0.8.22", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", + "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.22" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79f81d38d7a2ed52d8f034e62c568e111df9bf8aba2f7cf19ddc5bf7bd89d520" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.106", ] [[package]] diff --git a/test/integration-tests/Cargo.toml b/test/integration-tests/Cargo.toml index e2e7ce470f..6e210e4ffc 100644 --- a/test/integration-tests/Cargo.toml +++ b/test/integration-tests/Cargo.toml @@ -2,7 +2,7 @@ name = "integration_tests_sv2" version = "0.1.0" authors = ["The Stratum V2 Developers"] -edition = "2018" +edition = "2021" documentation = "https://github.com/stratum-mining/stratum" readme = "README.md" homepage = "https://stratumprotocol.org" @@ -19,22 +19,17 @@ once_cell = { version = "1.19.0", default-features = false } rand = { version = "0.9.0", default-features = false, features = ["thread_rng"] } tar = { version = "0.4.41", default-features = false } tokio = { version="1.44.1", default-features = false, features = ["tracing"] } +tokio-util = { version = "0.7", default-features = false } tracing = { version = "0.1.41", default-features = false } tracing-subscriber = { version = "0.3.19", default-features = false } -binary_sv2 = { path = "../../protocols/v2/binary-sv2" } -codec_sv2 = { path = "../../protocols/v2/codec-sv2", features = ["noise_sv2"] } -jd_client = { path = "../../roles/jd-client" } +jd_client_sv2 = { path = "../../roles/jd-client" } jd_server = { path = "../../roles/jd-server" } key-utils = { path = "../../utils/key-utils" } mining_device = { path = "../../roles/test-utils/mining-device" } -mining_device_sv1 = { path = "../../roles/test-utils/mining-device-sv1" } -mining_proxy_sv2 = { path = "../../roles/mining-proxy" } -network_helpers_sv2 = { path = "../../roles/roles-utils/network-helpers", features = ["with_buffer_pool"] } pool_sv2 = { path = "../../roles/pool" } -roles_logic_sv2 = { path = "../../protocols/v2/roles-logic-sv2" } -stratum-common = { path = "../../common" } -config-helpers = { path = "../../roles/roles-utils/config-helpers" } +config_helpers_sv2 = { path = "../../roles/roles-utils/config-helpers" } +stratum-common = { path = "../../common" , features = ["with_network_helpers", "sv1"]} translator_sv2 = { path = "../../roles/translator" } sv1_api = { path = "../../protocols/v1", optional = true } @@ -43,4 +38,4 @@ path = "lib/mod.rs" [features] default = [] -sv1 = ["sv1_api", "network_helpers_sv2/sv1"] +sv1 = ["sv1_api", "stratum-common/sv1"] diff --git a/test/integration-tests/high_diff_chain/blocks/.lock b/test/integration-tests/high_diff_chain/blocks/.lock new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/integration-tests/high_diff_chain/blocks/blk00000.dat b/test/integration-tests/high_diff_chain/blocks/blk00000.dat new file mode 100644 index 0000000000..63f072fe0e Binary files /dev/null and b/test/integration-tests/high_diff_chain/blocks/blk00000.dat differ diff --git a/test/integration-tests/high_diff_chain/blocks/index/000005.ldb b/test/integration-tests/high_diff_chain/blocks/index/000005.ldb new file mode 100644 index 0000000000..9f1ad6e971 Binary files /dev/null and b/test/integration-tests/high_diff_chain/blocks/index/000005.ldb differ diff --git a/test/integration-tests/high_diff_chain/blocks/index/000255.ldb b/test/integration-tests/high_diff_chain/blocks/index/000255.ldb new file mode 100644 index 0000000000..c2bd3c61fb Binary files /dev/null and b/test/integration-tests/high_diff_chain/blocks/index/000255.ldb differ diff --git a/test/integration-tests/high_diff_chain/blocks/index/000257.ldb b/test/integration-tests/high_diff_chain/blocks/index/000257.ldb new file mode 100644 index 0000000000..40bea491ac Binary files /dev/null and b/test/integration-tests/high_diff_chain/blocks/index/000257.ldb differ diff --git a/test/integration-tests/high_diff_chain/blocks/index/000260.ldb b/test/integration-tests/high_diff_chain/blocks/index/000260.ldb new file mode 100644 index 0000000000..691ab9ea0e Binary files /dev/null and b/test/integration-tests/high_diff_chain/blocks/index/000260.ldb differ diff --git a/test/integration-tests/high_diff_chain/blocks/index/000261.log b/test/integration-tests/high_diff_chain/blocks/index/000261.log new file mode 100644 index 0000000000..b8a88c3cdf Binary files /dev/null and b/test/integration-tests/high_diff_chain/blocks/index/000261.log differ diff --git a/test/integration-tests/high_diff_chain/blocks/index/CURRENT b/test/integration-tests/high_diff_chain/blocks/index/CURRENT new file mode 100644 index 0000000000..4ee1a673cd --- /dev/null +++ b/test/integration-tests/high_diff_chain/blocks/index/CURRENT @@ -0,0 +1 @@ +MANIFEST-000259 diff --git a/test/integration-tests/high_diff_chain/blocks/index/LOCK b/test/integration-tests/high_diff_chain/blocks/index/LOCK new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/integration-tests/high_diff_chain/blocks/index/MANIFEST-000259 b/test/integration-tests/high_diff_chain/blocks/index/MANIFEST-000259 new file mode 100644 index 0000000000..512f6c489c Binary files /dev/null and b/test/integration-tests/high_diff_chain/blocks/index/MANIFEST-000259 differ diff --git a/test/integration-tests/high_diff_chain/blocks/rev00000.dat b/test/integration-tests/high_diff_chain/blocks/rev00000.dat new file mode 100644 index 0000000000..fe1a71b6c4 Binary files /dev/null and b/test/integration-tests/high_diff_chain/blocks/rev00000.dat differ diff --git a/test/integration-tests/high_diff_chain/blocks/xor.dat b/test/integration-tests/high_diff_chain/blocks/xor.dat new file mode 100644 index 0000000000..1aff61dd9f --- /dev/null +++ b/test/integration-tests/high_diff_chain/blocks/xor.dat @@ -0,0 +1 @@ +ˆTl3®Õ" \ No newline at end of file diff --git a/test/integration-tests/high_diff_chain/chainstate/000289.log b/test/integration-tests/high_diff_chain/chainstate/000289.log new file mode 100644 index 0000000000..4e33a9fd12 Binary files /dev/null and b/test/integration-tests/high_diff_chain/chainstate/000289.log differ diff --git a/test/integration-tests/high_diff_chain/chainstate/000290.ldb b/test/integration-tests/high_diff_chain/chainstate/000290.ldb new file mode 100644 index 0000000000..c8e87db2e1 Binary files /dev/null and b/test/integration-tests/high_diff_chain/chainstate/000290.ldb differ diff --git a/test/integration-tests/high_diff_chain/chainstate/CURRENT b/test/integration-tests/high_diff_chain/chainstate/CURRENT new file mode 100644 index 0000000000..3b48cae827 --- /dev/null +++ b/test/integration-tests/high_diff_chain/chainstate/CURRENT @@ -0,0 +1 @@ +MANIFEST-000287 diff --git a/test/integration-tests/high_diff_chain/chainstate/LOCK b/test/integration-tests/high_diff_chain/chainstate/LOCK new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/integration-tests/high_diff_chain/chainstate/MANIFEST-000287 b/test/integration-tests/high_diff_chain/chainstate/MANIFEST-000287 new file mode 100644 index 0000000000..be1d2c41ca Binary files /dev/null and b/test/integration-tests/high_diff_chain/chainstate/MANIFEST-000287 differ diff --git a/test/integration-tests/lib/interceptor.rs b/test/integration-tests/lib/interceptor.rs index bdf93ddab4..32454fa2f8 100644 --- a/test/integration-tests/lib/interceptor.rs +++ b/test/integration-tests/lib/interceptor.rs @@ -1,5 +1,7 @@ +use std::fmt; + use crate::types::MsgType; -use roles_logic_sv2::parsers::AnyMessage; +use stratum_common::roles_logic_sv2::parsers_sv2::AnyMessage; #[derive(Debug, Clone, PartialEq, Eq)] pub enum MessageDirection { @@ -7,6 +9,15 @@ pub enum MessageDirection { ToUpstream, } +impl fmt::Display for MessageDirection { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MessageDirection::ToDownstream => write!(f, "downstream"), + MessageDirection::ToUpstream => write!(f, "upstream"), + } + } +} + /// Represents an action that [`Sniffer`] can take on intercepted messages. #[derive(Debug, Clone)] pub enum InterceptAction { diff --git a/test/integration-tests/lib/message_aggregator.rs b/test/integration-tests/lib/message_aggregator.rs index 1bcd9d2f91..e33a2fc792 100644 --- a/test/integration-tests/lib/message_aggregator.rs +++ b/test/integration-tests/lib/message_aggregator.rs @@ -1,5 +1,5 @@ -use roles_logic_sv2::{parsers::AnyMessage, utils::Mutex}; use std::{collections::VecDeque, sync::Arc}; +use stratum_common::roles_logic_sv2::{parsers_sv2::AnyMessage, utils::Mutex}; use crate::types::MsgType; diff --git a/test/integration-tests/lib/mock_roles.rs b/test/integration-tests/lib/mock_roles.rs index 495e39a45b..58c01fc9ff 100644 --- a/test/integration-tests/lib/mock_roles.rs +++ b/test/integration-tests/lib/mock_roles.rs @@ -4,9 +4,11 @@ use crate::{ utils::{create_downstream, create_upstream, message_from_frame, wait_for_client}, }; use async_channel::Sender; -use codec_sv2::{StandardEitherFrame, Sv2Frame}; -use roles_logic_sv2::parsers::AnyMessage; use std::net::SocketAddr; +use stratum_common::roles_logic_sv2::{ + codec_sv2::{StandardEitherFrame, Sv2Frame}, + parsers_sv2::AnyMessage, +}; use tokio::net::TcpStream; pub struct MockDownstream { @@ -106,17 +108,17 @@ impl MockUpstream { #[cfg(test)] mod tests { use super::*; - use crate::start_template_provider; - use codec_sv2::{StandardEitherFrame, Sv2Frame}; - use roles_logic_sv2::{ + use crate::{start_template_provider, template_provider::DifficultyLevel}; + use std::{convert::TryInto, net::TcpListener}; + use stratum_common::roles_logic_sv2::{ + codec_sv2::{StandardEitherFrame, Sv2Frame}, common_messages_sv2::{Protocol, SetupConnection, SetupConnectionSuccess, *}, - parsers::CommonMessages, + parsers_sv2::CommonMessages, }; - use std::{convert::TryInto, net::TcpListener}; #[tokio::test] async fn test_mock_downstream() { - let (_tp, socket) = start_template_provider(None); + let (_tp, socket) = start_template_provider(None, DifficultyLevel::Low); let mock_downstream = MockDownstream::new(socket); let send_to_upstream = mock_downstream.start().await; let setup_connection = diff --git a/test/integration-tests/lib/mod.rs b/test/integration-tests/lib/mod.rs index 0ab57bb9f1..157b5c23aa 100644 --- a/test/integration-tests/lib/mod.rs +++ b/test/integration-tests/lib/mod.rs @@ -1,18 +1,18 @@ -use crate::{sniffer::*, template_provider::*}; -use config_helpers::CoinbaseOutput; +use crate::{sniffer::*, sv1_minerd::MinerdProcess, template_provider::*}; +use config_helpers_sv2::CoinbaseRewardScript; use corepc_node::{ConnectParams, CookieValues}; use interceptor::InterceptAction; -use jd_client::JobDeclaratorClient; +use jd_client_sv2::JobDeclaratorClient; use jd_server::JobDeclaratorServer; use key_utils::{Secp256k1PublicKey, Secp256k1SecretKey}; +use once_cell::sync::OnceCell; use pool_sv2::PoolSv2; -use rand::{rng, Rng}; use std::{ - convert::{TryFrom, TryInto}, - net::SocketAddr, - str::FromStr, - sync::Once, + convert::TryFrom, + net::{Ipv4Addr, SocketAddr}, }; +use tracing::Level; +use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; use translator_sv2::TranslatorSv2; use utils::get_available_address; @@ -21,6 +21,7 @@ pub mod message_aggregator; pub mod mock_roles; pub mod sniffer; pub mod sniffer_error; +pub mod sv1_minerd; #[cfg(feature = "sv1")] pub mod sv1_sniffer; pub mod template_provider; @@ -29,12 +30,18 @@ pub(crate) mod utils; const SHARES_PER_MINUTE: f32 = 120.0; -static LOGGER: Once = Once::new(); +static LOGGER: OnceCell<()> = OnceCell::new(); /// Each test function should call `start_tracing()` to enable logging. pub fn start_tracing() { - LOGGER.call_once(|| { - tracing_subscriber::fmt::init(); + LOGGER.get_or_init(|| { + let env_filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new(Level::INFO.to_string())); + + tracing_subscriber::registry() + .with(env_filter) + .with(fmt::layer()) + .init(); }); } @@ -43,7 +50,8 @@ pub fn start_sniffer( upstream: SocketAddr, check_on_drop: bool, action: Vec, -) -> (Sniffer, SocketAddr) { + timeout: Option, +) -> (Sniffer<'_>, SocketAddr) { let listening_address = get_available_address(); let sniffer = Sniffer::new( identifier, @@ -51,6 +59,7 @@ pub fn start_sniffer( upstream, check_on_drop, action, + timeout, ); sniffer.start(); (sniffer, listening_address) @@ -68,10 +77,10 @@ pub async fn start_pool(template_provider_address: Option) -> (PoolS ) .expect("failed"); let cert_validity_sec = 3600; - let coinbase_outputs = vec![CoinbaseOutput::new( - "P2WPKH".to_string(), - "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075".to_string(), - )]; + let coinbase_reward_script = CoinbaseRewardScript::from_descriptor( + "wpkh(036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075)", + ) + .unwrap(); let pool_signature = "Stratum V2 SRI Pool".to_string(); let tp_address = if let Some(tp_add) = template_provider_address { tp_add.to_string() @@ -91,19 +100,23 @@ pub async fn start_pool(template_provider_address: Option) -> (PoolS connection_config, template_provider_config, authority_config, - coinbase_outputs, + coinbase_reward_script, SHARES_PER_MINUTE, share_batch_size, + 1, ); let pool = PoolSv2::new(config); assert!(pool.start().await.is_ok()); (pool, listening_address) } -pub fn start_template_provider(sv2_interval: Option) -> (TemplateProvider, SocketAddr) { +pub fn start_template_provider( + sv2_interval: Option, + difficulty_level: DifficultyLevel, +) -> (TemplateProvider, SocketAddr) { let address = get_available_address(); let sv2_interval = sv2_interval.unwrap_or(20); - let template_provider = TemplateProvider::start(address.port(), sv2_interval); + let template_provider = TemplateProvider::start(address.port(), sv2_interval, difficulty_level); template_provider.generate_blocks(1); (template_provider, address) } @@ -112,13 +125,12 @@ pub fn start_jdc( pool: &[(SocketAddr, SocketAddr)], // (pool_address, jds_address) tp_address: SocketAddr, ) -> (JobDeclaratorClient, SocketAddr) { - use jd_client::config::{ + use jd_client_sv2::config::{ JobDeclaratorClientConfig, PoolConfig, ProtocolConfig, TPConfig, Upstream, }; let jdc_address = get_available_address(); let max_supported_version = 2; let min_supported_version = 2; - let withhold = false; let authority_public_key = Secp256k1PublicKey::try_from( "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72".to_string(), ) @@ -127,10 +139,10 @@ pub fn start_jdc( "mkDLTBBRxdBv998612qipDYoTK3YUrqLe8uWw7gu3iXbSrn2n".to_string(), ) .unwrap(); - let coinbase_outputs = vec![CoinbaseOutput::new( - "P2WPKH".to_string(), - "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075".to_string(), - )]; + let coinbase_reward_script = CoinbaseRewardScript::from_descriptor( + "wpkh(036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075)", + ) + .unwrap(); let authority_pubkey = Secp256k1PublicKey::try_from( "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72".to_string(), ) @@ -140,8 +152,10 @@ pub fn start_jdc( .map(|(pool_addr, jds_addr)| { Upstream::new( authority_pubkey, - pool_addr.to_string(), - jds_addr.to_string(), + pool_addr.ip().to_string(), + pool_addr.port(), + jds_addr.ip().to_string(), + jds_addr.port(), ) }) .collect(); @@ -150,20 +164,25 @@ pub fn start_jdc( let protocol_config = ProtocolConfig::new( max_supported_version, min_supported_version, - coinbase_outputs, + coinbase_reward_script, ); + let shares_per_minute = 10.0; + let shares_batch_size = 1; + let user_identity = "IT-test".to_string(); let jdc_signature = "JDC".to_string(); let jd_client_proxy = JobDeclaratorClientConfig::new( jdc_address, protocol_config, - withhold, + user_identity, + shares_per_minute, + shares_batch_size, pool_config, tp_config, upstreams, - std::time::Duration::from_secs(1), jdc_signature, + None, ); - let ret = jd_client::JobDeclaratorClient::new(jd_client_proxy); + let ret = jd_client_sv2::JobDeclaratorClient::new(jd_client_proxy); let ret_clone = ret.clone(); tokio::spawn(async move { ret_clone.start().await }); (ret, jdc_address) @@ -181,10 +200,10 @@ pub fn start_jds(tp_rpc_connection: &ConnectParams) -> (JobDeclaratorServer, Soc .unwrap(); let listen_jd_address = get_available_address(); let cert_validity_sec = 3600; - let coinbase_outputs = vec![CoinbaseOutput::new( - "P2WPKH".to_string(), - "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075".to_string(), - )]; + let coinbase_reward_script = CoinbaseRewardScript::from_descriptor( + "wpkh(036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075)", + ) + .unwrap(); if let Ok(Some(CookieValues { user, password })) = tp_rpc_connection.get_cookie_values() { let ip = tp_rpc_connection.rpc_socket.ip().to_string(); let url = jd_server::Uri::builder() @@ -204,7 +223,7 @@ pub fn start_jds(tp_rpc_connection: &ConnectParams) -> (JobDeclaratorServer, Soc authority_public_key, authority_secret_key, cert_validity_sec, - coinbase_outputs, + coinbase_reward_script, core_rpc, std::time::Duration::from_secs(1), ); @@ -219,7 +238,7 @@ pub fn start_jds(tp_rpc_connection: &ConnectParams) -> (JobDeclaratorServer, Soc } } -pub fn start_sv2_translator(upstream: SocketAddr) -> (TranslatorSv2, SocketAddr) { +pub async fn start_sv2_translator(upstream: SocketAddr) -> (TranslatorSv2, SocketAddr) { let upstream_address = upstream.ip().to_string(); let upstream_port = upstream.port(); let upstream_authority_pubkey = Secp256k1PublicKey::try_from( @@ -228,41 +247,34 @@ pub fn start_sv2_translator(upstream: SocketAddr) -> (TranslatorSv2, SocketAddr) .expect("failed"); let listening_address = get_available_address(); let listening_port = listening_address.port(); - let min_individual_miner_hashrate = measure_hashrate(1) as f32; - let channel_diff_update_interval = 60; - let channel_nominal_hashrate = min_individual_miner_hashrate; + + let minerd_process = MinerdProcess::new(SocketAddr::from((Ipv4Addr::LOCALHOST, 0)), false) + .await + .unwrap(); + let min_individual_miner_hashrate = minerd_process.measure_hashrate().await.unwrap() as f32; + let downstream_difficulty_config = translator_sv2::config::DownstreamDifficultyConfig::new( min_individual_miner_hashrate, SHARES_PER_MINUTE, - 0, - 0, - ); - let upstream_difficulty_config = translator_sv2::config::UpstreamDifficultyConfig::new( - channel_diff_update_interval, - channel_nominal_hashrate, - 0, - false, + true, ); - let upstream_conf = translator_sv2::config::UpstreamConfig::new( + let upstream_conf = translator_sv2::config::Upstream::new( upstream_address, upstream_port, upstream_authority_pubkey, - upstream_difficulty_config, ); - let downstream_conf = translator_sv2::config::DownstreamConfig::new( + let downstream_extranonce2_size = 4; + + let config = translator_sv2::config::TranslatorConfig::new( + vec![upstream_conf], listening_address.ip().to_string(), listening_port, downstream_difficulty_config, - ); - - let min_extranonce2_size = 4; - - let config = translator_sv2::config::TranslatorConfig::new( - upstream_conf, - downstream_conf, 2, 2, - min_extranonce2_size, + downstream_extranonce2_size, + "user_identity".to_string(), + false, ); let translator_v2 = translator_sv2::TranslatorSv2::new(config); let clone_translator_v2 = translator_v2.clone(); @@ -272,51 +284,17 @@ pub fn start_sv2_translator(upstream: SocketAddr) -> (TranslatorSv2, SocketAddr) (translator_v2, listening_address) } -pub fn measure_hashrate(duration_secs: u64) -> f64 { - use stratum_common::bitcoin::hashes::{sha256d, Hash, HashEngine}; - - let mut share = { - let mut rng = rng(); - let mut arr = [0u8; 80]; - rng.fill(&mut arr[..]); - arr - }; - let start_time = std::time::Instant::now(); - let mut hashes: u64 = 0; - let duration = std::time::Duration::from_secs(duration_secs); - - let hash = |share: &mut [u8; 80]| { - let nonce: [u8; 8] = share[0..8].try_into().unwrap(); - let mut nonce = u64::from_le_bytes(nonce); - nonce += 1; - share[0..8].copy_from_slice(&nonce.to_le_bytes()); - let mut engine = sha256d::Hash::engine(); - engine.input(share); - sha256d::Hash::from_engine(engine); - }; - - loop { - if start_time.elapsed() >= duration { - break; - } - hash(&mut share); - hashes += 1; - } - - let elapsed_secs = start_time.elapsed().as_secs_f64(); - - hashes as f64 / elapsed_secs -} - -pub fn start_mining_device_sv1( +pub async fn start_minerd( upstream_addr: SocketAddr, + username: Option, + password: Option, single_submit: bool, - custom_target: Option<[u8; 32]>, -) { - tokio::spawn(async move { - mining_device_sv1::client::Client::connect(80, upstream_addr, single_submit, custom_target) - .await; - }); +) -> (sv1_minerd::MinerdProcess, SocketAddr) { + let (process, local_addr) = + sv1_minerd::start_minerd(upstream_addr, username, password, single_submit) + .await + .expect("Failed to start minerd process"); + (process, local_addr) } pub fn start_mining_device_sv2( @@ -342,37 +320,6 @@ pub fn start_mining_device_sv2( }); } -pub fn start_mining_sv2_proxy(upstreams: &[SocketAddr]) -> SocketAddr { - use mining_proxy_sv2::{ChannelKind, UpstreamMiningValues}; - let upstreams = upstreams - .iter() - .map(|upstream| UpstreamMiningValues { - address: upstream.ip().to_string(), - port: upstream.port(), - pub_key: Secp256k1PublicKey::from_str( - "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72", - ) - .unwrap(), - channel_kind: ChannelKind::Extended, - }) - .collect(); - let mining_proxy_listening_address = get_available_address(); - let config = mining_proxy_sv2::MiningProxyConfig { - upstreams, - listen_address: mining_proxy_listening_address.ip().to_string(), - listen_mining_port: mining_proxy_listening_address.port(), - max_supported_version: 2, - min_supported_version: 2, - downstream_share_per_minute: 1.0, - expected_total_downstream_hr: 10_000.0, - reconnect: true, - }; - tokio::spawn(async move { - mining_proxy_sv2::start_mining_proxy(config).await; - }); - mining_proxy_listening_address -} - #[cfg(feature = "sv1")] pub fn start_sv1_sniffer(upstream_address: SocketAddr) -> (sv1_sniffer::SnifferSV1, SocketAddr) { let listening_address = get_available_address(); diff --git a/test/integration-tests/lib/sniffer.rs b/test/integration-tests/lib/sniffer.rs index 16b7b194e6..98511fa20b 100644 --- a/test/integration-tests/lib/sniffer.rs +++ b/test/integration-tests/lib/sniffer.rs @@ -7,10 +7,12 @@ use crate::{ wait_for_client, }, }; -use roles_logic_sv2::parsers::AnyMessage; use std::net::SocketAddr; +use stratum_common::roles_logic_sv2::parsers_sv2::{message_type_to_name, AnyMessage}; use tokio::{net::TcpStream, select}; +const DEFAULT_TIMEOUT: u64 = 60; + /// Allows to intercept messages sent between two roles. /// /// Can be useful for testing purposes, as it allows to assert that the roles have sent specific @@ -27,6 +29,9 @@ use tokio::{net::TcpStream, select}; /// queues via [`Sniffer::next_message_from_downstream`] and /// [`Sniffer::next_message_from_upstream`], respectively. /// +/// The `timeout` parameter can be used to configure the timeout for the sniffer. If not provided, +/// the default timeout is 1 minute. +/// /// In order to replace or ignore the messages sent between the roles, [`InterceptAction`] can be /// used in [`Sniffer::new`]. #[derive(Debug, Clone)] @@ -38,6 +43,7 @@ pub struct Sniffer<'a> { messages_from_upstream: MessagesAggregator, check_on_drop: bool, action: Vec, + timeout: Option, } impl<'a> Sniffer<'a> { @@ -49,6 +55,7 @@ impl<'a> Sniffer<'a> { upstream_address: SocketAddr, check_on_drop: bool, action: Vec, + timeout: Option, ) -> Self { Self { identifier, @@ -58,6 +65,7 @@ impl<'a> Sniffer<'a> { messages_from_upstream: MessagesAggregator::new(), check_on_drop, action, + timeout, } } @@ -137,10 +145,13 @@ impl<'a> Sniffer<'a> { return; } - // 1 min timeout - // only for worst case, ideally should never be triggered - if now.elapsed().as_secs() > 60 { - panic!("Timeout waiting for message type"); + // configurable timeout, 1 minute default + if now.elapsed().as_secs() > self.timeout.unwrap_or(DEFAULT_TIMEOUT) { + panic!( + "timeout while waiting for message {} to go {}", + message_type_to_name(message_type), + message_direction + ); } // sleep to reduce async lock contention @@ -190,10 +201,13 @@ impl<'a> Sniffer<'a> { return true; } - // 1 min timeout - // only for worst case, ideally should never be triggered - if now.elapsed().as_secs() > 60 { - panic!("Timeout waiting for message type"); + // configurable timeout, 1 minute default + if now.elapsed().as_secs() > self.timeout.unwrap_or(DEFAULT_TIMEOUT) { + panic!( + "timeout while waiting for message {} to go {}", + message_type_to_name(message_type), + message_direction + ); } // sleep to reduce async lock contention diff --git a/test/integration-tests/lib/sv1_minerd/error.rs b/test/integration-tests/lib/sv1_minerd/error.rs new file mode 100644 index 0000000000..e6a408428c --- /dev/null +++ b/test/integration-tests/lib/sv1_minerd/error.rs @@ -0,0 +1,70 @@ +use std::fmt; + +/// Errors that can occur when using MinerdWrapper +#[derive(Debug)] +pub enum MinerdError { + /// IO operation failed + Io(tokio::io::Error), + /// Process spawn failed + ProcessSpawn(tokio::io::Error), + /// Process is already running + ProcessAlreadyRunning, + /// Process is not running when expected + ProcessNotRunning, + /// Network connection failed + NetworkConnection(tokio::io::Error), + /// Proxy setup failed + ProxySetup(tokio::io::Error), + /// Invalid configuration + InvalidConfiguration(String), + /// Failed to parse hashrate from minerd benchmark output + HashrateParseError, + /// Mutex was poisoned + MutexPoisoned, + /// OS or Architecture not supported + OsArchNotSupported(String), +} + +impl fmt::Display for MinerdError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MinerdError::Io(e) => write!(f, "IO error: {}", e), + MinerdError::ProcessSpawn(e) => write!(f, "Failed to spawn minerd process: {}", e), + MinerdError::ProcessAlreadyRunning => write!(f, "Minerd process is already running"), + MinerdError::ProcessNotRunning => write!(f, "Minerd process is not running"), + MinerdError::NetworkConnection(e) => write!(f, "Network connection failed: {}", e), + MinerdError::ProxySetup(e) => write!(f, "Proxy setup failed: {}", e), + MinerdError::InvalidConfiguration(msg) => write!(f, "Invalid configuration: {}", msg), + MinerdError::HashrateParseError => { + write!(f, "Failed to parse hashrate from minerd benchmark output") + } + MinerdError::MutexPoisoned => write!(f, "Mutex was poisoned"), + MinerdError::OsArchNotSupported(msg) => { + write!(f, "OS or architecture not supported: {}", msg) + } + } + } +} + +impl std::error::Error for MinerdError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + MinerdError::Io(e) => Some(e), + MinerdError::ProcessSpawn(e) => Some(e), + MinerdError::NetworkConnection(e) => Some(e), + MinerdError::ProxySetup(e) => Some(e), + MinerdError::ProcessAlreadyRunning + | MinerdError::ProcessNotRunning + | MinerdError::InvalidConfiguration(_) + | MinerdError::HashrateParseError + | MinerdError::MutexPoisoned => None, + MinerdError::OsArchNotSupported(_) => None, + } + } +} + +impl From for MinerdError { + fn from(error: tokio::io::Error) -> Self { + MinerdError::Io(error) + } +} diff --git a/test/integration-tests/lib/sv1_minerd/mod.rs b/test/integration-tests/lib/sv1_minerd/mod.rs new file mode 100644 index 0000000000..8c015aa533 --- /dev/null +++ b/test/integration-tests/lib/sv1_minerd/mod.rs @@ -0,0 +1,5 @@ +pub mod error; +pub mod process; + +pub use error::MinerdError; +pub use process::{start_minerd, MinerdProcess}; diff --git a/test/integration-tests/lib/sv1_minerd/process.rs b/test/integration-tests/lib/sv1_minerd/process.rs new file mode 100644 index 0000000000..6a52f0eca8 --- /dev/null +++ b/test/integration-tests/lib/sv1_minerd/process.rs @@ -0,0 +1,593 @@ +use std::{ + fs, + net::SocketAddr, + path::PathBuf, + sync::{Arc, Mutex}, +}; +use tokio::{ + io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}, + net::{TcpListener, TcpStream}, + process::{Child as TokioChild, Command as TokioCommand}, +}; +use tokio_util::sync::CancellationToken; +use tracing::{debug, error, info}; + +use crate::utils::{http, tarball}; + +use super::error::MinerdError; + +const VERSION_MINERD: &str = "2.5.1"; + +fn get_minerd_filename(os: &str, arch: &str) -> Result { + match (os, arch) { + ("macos", "aarch64") => Ok(format!( + "pooler-cpuminer-{VERSION_MINERD}-arm64-apple-darwin.tar.gz" + )), + ("macos", "x86_64") => Ok(format!( + "pooler-cpuminer-{VERSION_MINERD}-x86_64-apple-darwin.tar.gz" + )), + ("linux", "x86_64") => Ok(format!( + "pooler-cpuminer-{VERSION_MINERD}-linux-x86_64.tar.gz" + )), + ("linux", "aarch64") => Ok(format!( + "pooler-cpuminer-{VERSION_MINERD}-linux-arm64.tar.gz" + )), + _ => Err(MinerdError::OsArchNotSupported(format!( + "OS or architecture not supported: {} {}", + os, arch + ))), + } +} + +/// A wrapper struct for the minerd process that provides: +/// - TCP proxy functionality to intercept communications +/// - Process management for spawning and killing minerd +#[derive(Debug)] +pub struct MinerdProcess { + /// Path to the minerd binary + minerd_binary: PathBuf, + /// Handle to the spawned minerd process + process: Arc>>, + /// Address where the wrapper listens for minerd connections + local_address: SocketAddr, + /// Address of the upstream mining server + upstream_address: SocketAddr, + /// Whether to kill the process after the first mining.submit + single_submit: bool, + /// Cancellation token to coordinate shutdown of all tasks + cancellation_token: CancellationToken, +} + +impl MinerdProcess { + /// Creates a new MinerdProcess with the given upstream address + pub async fn new( + upstream_address: SocketAddr, + single_submit: bool, + ) -> Result { + let current_dir: PathBuf = std::env::current_dir().expect("failed to read current dir"); + let minerd_dir = current_dir.join("minerd"); + + let os = std::env::consts::OS; + let arch = std::env::consts::ARCH; + let download_filename = get_minerd_filename(os, arch)?; + + if !minerd_dir.exists() { + fs::create_dir_all(minerd_dir.clone()).expect("failed to create minerd directory"); + let download_endpoint = format!( + "https://github.com/stratum-mining/cpuminer/releases/download/v{VERSION_MINERD}/" + ); + let url = format!("{download_endpoint}{download_filename}"); + let tarball_bytes = http::make_get_request(&url, 5); + println!("tarball_bytes: {:?}", tarball_bytes); + tarball::unpack(&tarball_bytes, &minerd_dir); + } + + let minerd_binary = minerd_dir.join("minerd"); + + if os == "macos" { + std::process::Command::new("codesign") + .arg("--sign") + .arg("-") + .arg(&minerd_binary) + .output() + .expect("failed to sign minerd binary"); + } + + // Bind to local address for the proxy + // use 0 to let the OS assign a randomly available port + let listener = TcpListener::bind(("127.0.0.1", 0)) + .await + .map_err(MinerdError::ProxySetup)?; + let local_address = listener.local_addr().map_err(MinerdError::ProxySetup)?; + + Ok(MinerdProcess { + minerd_binary, + process: Arc::new(Mutex::new(None)), + local_address, + upstream_address, + single_submit, + cancellation_token: CancellationToken::new(), + }) + } + + /// Returns the local address where the wrapper is listening + pub fn local_address(&self) -> SocketAddr { + self.local_address + } + + /// Returns the upstream address that minerd will connect to through the proxy + pub fn upstream_address(&self) -> SocketAddr { + self.upstream_address + } + + /// Spawns the minerd process with the given parameters + pub async fn spawn_minerd( + &mut self, + username: Option, + password: Option, + ) -> Result<(), MinerdError> { + let mut process_guard = self + .process + .lock() + .map_err(|_| MinerdError::MutexPoisoned)?; + if process_guard.is_some() { + return Err(MinerdError::ProcessAlreadyRunning); + } + + let mut cmd = TokioCommand::new(&self.minerd_binary); + + // Kill the process on drop + cmd.kill_on_drop(true); + + // Set the algorithm to sha256d + cmd.arg("-a").arg("sha256d"); + + // Set the number of threads to use for mining + cmd.arg("--threads").arg("1"); + + // Set the retry pause to 1 second + cmd.arg("--retry-pause").arg("1"); + + // Set the URL to connect to our local proxy instead of upstream directly + cmd.arg("--url").arg(format!( + "stratum+tcp://{}:{}", + self.local_address.ip(), + self.local_address.port() + )); + + // Add username and password if provided + if let Some(ref username) = username { + cmd.arg("--userpass").arg(format!( + "{}:{}", + username, + password.as_deref().unwrap_or("") + )); + } + + info!("Spawning minerd with command: {:?}", cmd); + + let child = cmd.spawn().map_err(MinerdError::ProcessSpawn)?; + *process_guard = Some(child); + info!("minerd process spawned successfully"); + Ok(()) + } + + /// Starts the TCP proxy to intercept communications between minerd and the upstream server + pub async fn start_tcp_proxy(&mut self) -> Result<(), MinerdError> { + let listener = TcpListener::bind(self.local_address) + .await + .map_err(MinerdError::ProxySetup)?; + let upstream_address = self.upstream_address; + let single_submit = self.single_submit; + let process = Arc::clone(&self.process); + let cancellation_token = self.cancellation_token.clone(); + + tokio::spawn(async move { + info!("Proxy server started, waiting for connections..."); + + loop { + tokio::select! { + accept_result = listener.accept() => { + match accept_result { + Ok((downstream_stream, _)) => { + info!("New connection from minerd"); + + // Connect to upstream server + match TcpStream::connect(upstream_address).await { + Ok(upstream_stream) => { + // Split streams for bidirectional communication + let (downstream_read, downstream_write) = downstream_stream.into_split(); + let (upstream_read, upstream_write) = upstream_stream.into_split(); + + let process_clone = Arc::clone(&process); + let token_clone1 = cancellation_token.clone(); + let token_clone2 = cancellation_token.clone(); + + // Task for downstream -> upstream (minerd -> pool) + if single_submit { + tokio::spawn(async move { + let _ = Self::proxy_tcp_data_single_submit( + downstream_read, + upstream_write, + process_clone, + token_clone1, + ).await; + }); + } else { + tokio::spawn(async move { + let _ = Self::proxy_tcp_data( + downstream_read, + upstream_write, + token_clone1, + ).await; + }); + } + + // Task for upstream -> downstream (pool -> minerd) + tokio::spawn(async move { + let _ = Self::proxy_tcp_data( + upstream_read, + downstream_write, + token_clone2, + ).await; + }); + } + Err(e) => { + error!("Failed to connect to upstream server {}: {}", upstream_address, e); + } + } + } + Err(e) => { + error!("Failed to accept connection: {}", e); + break; + } + } + } + _ = cancellation_token.cancelled() => { + info!("Proxy server shutting down due to cancellation"); + break; + } + } + } + }); + + Ok(()) + } + + /// Proxies data between two TCP streams with monitoring for mining.submit + /// This function will automatically kill the process and trigger shutdown when mining.submit is + /// detected + async fn proxy_tcp_data_single_submit( + mut from: tokio::net::tcp::OwnedReadHalf, + mut to: tokio::net::tcp::OwnedWriteHalf, + process: Arc>>, + cancellation_token: CancellationToken, + ) { + let mut buffer = [0; 4096]; + + loop { + tokio::select! { + read_result = from.read(&mut buffer) => { + match read_result { + Ok(0) => { + debug!("Connection closed"); + break; + } + Ok(n) => { + let data = &buffer[..n]; + + // Check for mining.submit and trigger shutdown + if let Ok(data_str) = std::str::from_utf8(data) { + if data_str.contains("\"mining.submit\"") { + info!("Detected mining.submit, killing minerd process and triggering shutdown"); + + // Forward the data first + if let Err(e) = to.write_all(data).await { + error!("Failed to write data: {}", e); + } + + // Kill the process + let child = { + match process.lock() { + Ok(mut process_guard) => process_guard.take(), + Err(_) => { + error!("Mutex poisoned while trying to kill process"); + None + } + } + }; // Lock is released here + + if let Some(mut child) = child { + if let Err(e) = child.kill().await { + error!("Failed to kill minerd process: {}", e); + } else { + info!("minerd process killed successfully after mining.submit"); + } + } + + // Trigger cancellation to stop all tasks + cancellation_token.cancel(); + break; + } + } + + // Forward the data + if let Err(e) = to.write_all(data).await { + error!("Failed to write data: {}", e); + break; + } + } + Err(e) => { + error!("Failed to read data: {}", e); + break; + } + } + } + _ = cancellation_token.cancelled() => { + info!("Proxy task (downstream->upstream) shutting down due to cancellation"); + break; + } + } + } + } + + /// Proxies data between two TCP streams + async fn proxy_tcp_data( + mut from: tokio::net::tcp::OwnedReadHalf, + mut to: tokio::net::tcp::OwnedWriteHalf, + cancellation_token: CancellationToken, + ) -> Result<(), MinerdError> { + let mut buffer = [0; 4096]; + + loop { + tokio::select! { + read_result = from.read(&mut buffer) => { + match read_result { + Ok(0) => { + debug!("Connection closed"); + return Ok(()); + } + Ok(n) => { + let data = &buffer[..n]; + + // Forward the data + if let Err(e) = to.write_all(data).await { + error!("Failed to write data: {}", e); + return Err(MinerdError::Io(e)); + } + } + Err(e) => { + error!("Failed to read data: {}", e); + return Err(MinerdError::Io(e)); + } + } + } + _ = cancellation_token.cancelled() => { + info!("TCP proxy shutting down due to cancellation"); + return Ok(()); + } + } + } + } + + /// Checks if the minerd process is still running + pub fn is_running(&self) -> Result { + let mut process_guard = self + .process + .lock() + .map_err(|_| MinerdError::MutexPoisoned)?; + if let Some(ref mut process) = *process_guard { + match process.try_wait() { + Ok(Some(_)) => Ok(false), // Process has exited + Ok(None) => Ok(true), // Process is still running + Err(_) => Ok(false), // Error checking process status + } + } else { + Ok(false) + } + } + + /// Measures the hashrate of the local minerd binary in benchmark mode + /// Returns the hashrate in hashes per second + pub async fn measure_hashrate(&self) -> Result { + info!("Starting hashrate measurement using minerd benchmark mode"); + + let mut cmd = TokioCommand::new(&self.minerd_binary); + + // Set benchmark mode with specific parameters for consistent measurement + cmd.arg("-a") + .arg("sha256d") // Use sha256d algorithm + .arg("-t") + .arg("1") // Use 1 thread for consistent measurement + .arg("--benchmark") // Enable benchmark mode (no network connection) + .arg("-q"); // Quiet mode to reduce output noise + + // Capture stderr to parse the hashrate output (minerd outputs to stderr) + cmd.stdout(std::process::Stdio::null()); + cmd.stderr(std::process::Stdio::piped()); + + info!("Running minerd benchmark: {:?}", cmd); + + let mut child = cmd.spawn().map_err(MinerdError::ProcessSpawn)?; + + let stderr = child.stderr.take().ok_or_else(|| { + MinerdError::ProcessSpawn(std::io::Error::other("Failed to get stderr")) + })?; + + // Give minerd some time to run and produce hashrate output + tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; + + // Kill the benchmark process + if let Err(e) = child.kill().await { + error!("Failed to kill benchmark process: {}", e); + } + + // Read and parse the output from stderr + let mut reader = BufReader::new(stderr); + let mut line = String::new(); + let mut hashrate_hashes_per_sec = None; + + // Read output lines to find hashrate information + let mut all_output = Vec::new(); + while let Ok(bytes_read) = reader.read_line(&mut line).await { + if bytes_read == 0 { + break; + } + + let line_trimmed = line.trim(); + all_output.push(line_trimmed.to_string()); + debug!("Benchmark output: {}", line_trimmed); + + // Parse hashrate from lines like: + // "[2025-08-29 20:10:39] thread 0: 2097152 hashes, 1441 khash/s" + // "[2025-08-29 20:10:39] Total: 1441 khash/s" + if let Some(hashrate_khash) = parse_hashrate_from_benchmark_line(line_trimmed) { + info!("Detected benchmark hashrate: {} khash/s", hashrate_khash); + // Convert khash/s to hashes/s (multiply by 1000) + hashrate_hashes_per_sec = Some(hashrate_khash * 1000.0); + // We can break after finding the first hashrate measurement + break; + } + + line.clear(); + } + + // If we couldn't parse hashrate, log all output for debugging + if hashrate_hashes_per_sec.is_none() { + error!("Failed to parse hashrate from minerd benchmark output. Full output:"); + for (i, line) in all_output.iter().enumerate() { + error!(" Line {}: {}", i + 1, line); + } + } + + hashrate_hashes_per_sec.ok_or(MinerdError::HashrateParseError) + } +} + +impl Drop for MinerdProcess { + fn drop(&mut self) { + // Trigger cancellation to signal all tasks to stop + self.cancellation_token.cancel(); + + match self.process.lock() { + Ok(mut process_guard) => { + if let Some(mut process) = process_guard.take() { + if let Err(e) = process.start_kill() { + error!("Error killing minerd process on drop: {}", e); + } else { + info!("minerd process killed on drop"); + } + } + } + Err(_) => { + error!("Mutex poisoned in Drop implementation, cannot kill process cleanly"); + } + } + } +} + +/// Parses hashrate from a minerd benchmark output line +/// Examples: +/// - "[2025-08-29 20:10:39] thread 0: 2097152 hashes, 1441 khash/s" +/// - "[2025-08-29 20:10:39] Total: 1441 khash/s" +fn parse_hashrate_from_benchmark_line(line: &str) -> Option { + // Look for pattern: "X khash/s" or "X.Y khash/s" + if let Some(pos) = line.find(" khash/s") { + // Find the number before " khash/s" + let before_khash = &line[..pos]; + if let Some(space_pos) = before_khash.rfind(' ') { + let number_str = &before_khash[space_pos + 1..]; + if let Ok(hashrate) = number_str.parse::() { + return Some(hashrate); + } + } + } + + // Also try to parse "hash/s" patterns (without the 'k' prefix) - convert to khash/s + if let Some(pos) = line.find(" hash/s") { + let before_hash = &line[..pos]; + if let Some(space_pos) = before_hash.rfind(' ') { + let number_str = &before_hash[space_pos + 1..]; + if let Ok(hashrate) = number_str.parse::() { + // Convert hash/s to khash/s + return Some(hashrate / 1000.0); + } + } + } + + None +} + +pub async fn start_minerd( + upstream_address: SocketAddr, + username: Option, + password: Option, + single_submit: bool, +) -> Result<(MinerdProcess, SocketAddr), MinerdError> { + if username.is_none() && password.is_some() || username.is_some() && password.is_none() { + return Err(MinerdError::InvalidConfiguration( + "Username and password must be provided together".to_string(), + )); + } + + let mut minerd_process = MinerdProcess::new(upstream_address, single_submit).await?; + let local_address = minerd_process.local_address(); + + minerd_process.start_tcp_proxy().await?; + minerd_process.spawn_minerd(username, password).await?; + + Ok((minerd_process, local_address)) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::net::{Ipv4Addr, SocketAddr}; + + #[tokio::test] + async fn test_measure_hashrate() { + let minerd_process = MinerdProcess::new(SocketAddr::from((Ipv4Addr::LOCALHOST, 0)), false) + .await + .unwrap(); + let hashrate = minerd_process.measure_hashrate().await.unwrap(); + println!("Hashrate: {} hashes/s", hashrate); + assert!(hashrate > 0.0); + } + + #[test] + fn test_parse_hashrate_from_benchmark_line() { + // Test the parsing logic with known good inputs + assert_eq!( + parse_hashrate_from_benchmark_line( + "[2025-08-29 20:10:39] thread 0: 2097152 hashes, 1441 khash/s" + ), + Some(1441.0) + ); + + assert_eq!( + parse_hashrate_from_benchmark_line("[2025-08-29 20:10:39] Total: 1441 khash/s"), + Some(1441.0) + ); + + assert_eq!( + parse_hashrate_from_benchmark_line( + "[2025-08-29 20:10:39] thread 0: 2097152 hashes, 1441.5 khash/s" + ), + Some(1441.5) + ); + + // Test hash/s conversion + assert_eq!( + parse_hashrate_from_benchmark_line( + "[2025-08-29 20:10:39] thread 0: 2097152 hashes, 1441000 hash/s" + ), + Some(1441.0) + ); + + // Test invalid lines + assert_eq!( + parse_hashrate_from_benchmark_line("random text without hashrate"), + None + ); + } +} diff --git a/test/integration-tests/lib/sv1_sniffer.rs b/test/integration-tests/lib/sv1_sniffer.rs index 002ed4ddf6..c3be140d25 100644 --- a/test/integration-tests/lib/sv1_sniffer.rs +++ b/test/integration-tests/lib/sv1_sniffer.rs @@ -1,8 +1,8 @@ #![cfg(feature = "sv1")] use crate::interceptor::MessageDirection; use async_channel::{Receiver, Sender}; -use network_helpers_sv2::sv1_connection::ConnectionSV1; use std::{collections::VecDeque, net::SocketAddr, sync::Arc}; +use stratum_common::network_helpers_sv2::sv1_connection::ConnectionSV1; use tokio::{ net::{TcpListener, TcpStream}, select, @@ -127,7 +127,7 @@ impl SnifferSV1 { .await .map_err(|_| SnifferError::DownstreamClosed)?; upstream_messages.add_message(msg.clone()).await; - tracing::info!("🔠Sv1 Sniffer | Forwarded: {} | Direction: ⬇", msg); + tracing::info!("🔠Sv1 Sniffer | Direction: ⬇ | Forwarded: {}", msg); } Err(SnifferError::UpstreamClosed) } @@ -142,7 +142,7 @@ impl SnifferSV1 { .await .map_err(|_| SnifferError::UpstreamClosed)?; downstream_messages.add_message(msg.clone()).await; - tracing::info!("🔠Sv1 Sniffer | Forwarded: {} | Direction: ⬆", msg); + tracing::info!("🔠Sv1 Sniffer | Direction: ⬆ | Forwarded: {}", msg); } Err(SnifferError::DownstreamClosed) } diff --git a/test/integration-tests/lib/template_provider.rs b/test/integration-tests/lib/template_provider.rs index 901cf4f769..9f05d94552 100644 --- a/test/integration-tests/lib/template_provider.rs +++ b/test/integration-tests/lib/template_provider.rs @@ -1,27 +1,23 @@ -use corepc_node::{Conf, ConnectParams, Node}; +use corepc_node::{types::GetBlockchainInfo, Conf, ConnectParams, Node}; use std::{env, fs::create_dir_all, path::PathBuf}; -use stratum_common::bitcoin::{Address, Amount, Txid}; +use stratum_common::roles_logic_sv2::bitcoin::{Address, Amount, Txid}; +use tracing::warn; -use crate::utils::{http, tarball}; +use crate::utils::{fs_utils, http, tarball}; const VERSION_TP: &str = "0.1.15"; fn get_bitcoind_filename(os: &str, arch: &str) -> String { match (os, arch) { - ("macos", "aarch64") => format!( - "bitcoin-sv2-tp-{}-arm64-apple-darwin-unsigned.tar.gz", - VERSION_TP - ), - ("macos", "x86_64") => format!( - "bitcoin-sv2-tp-{}-x86_64-apple-darwin-unsigned.tar.gz", - VERSION_TP - ), - ("linux", "x86_64") => format!("bitcoin-sv2-tp-{}-x86_64-linux-gnu.tar.gz", VERSION_TP), - ("linux", "aarch64") => format!("bitcoin-sv2-tp-{}-aarch64-linux-gnu.tar.gz", VERSION_TP), - _ => format!( - "bitcoin-sv2-tp-{}-x86_64-apple-darwin-unsigned.zip", - VERSION_TP - ), + ("macos", "aarch64") => { + format!("bitcoin-sv2-tp-{VERSION_TP}-arm64-apple-darwin-unsigned.tar.gz") + } + ("macos", "x86_64") => { + format!("bitcoin-sv2-tp-{VERSION_TP}-x86_64-apple-darwin-unsigned.tar.gz") + } + ("linux", "x86_64") => format!("bitcoin-sv2-tp-{VERSION_TP}-x86_64-linux-gnu.tar.gz"), + ("linux", "aarch64") => format!("bitcoin-sv2-tp-{VERSION_TP}-aarch64-linux-gnu.tar.gz"), + _ => format!("bitcoin-sv2-tp-{VERSION_TP}-x86_64-apple-darwin-unsigned.zip"), } } @@ -33,17 +29,66 @@ pub struct TemplateProvider { bitcoind: Node, } +/// Represents the consensus difficulty level of the network. +/// +/// Low: regtest mode (every share is a block) +/// +/// Mid: signet mode with genesis difficulty +/// (most of the time, a CPU should find a block in a minute or less) +/// +/// High: signet mode with premined blocks raising difficulty to 77761.11 +/// (most of the time, a CPU should take a REALLY long time to find a block) +/// +/// Note: signet mode has signetchallenge=51, which means no signature is needed on the coinbase. +pub enum DifficultyLevel { + Low, + Mid, + High, +} + impl TemplateProvider { /// Start a new [`TemplateProvider`] instance. - pub fn start(port: u16, sv2_interval: u32) -> Self { + pub fn start(port: u16, sv2_interval: u32, difficulty_level: DifficultyLevel) -> Self { let current_dir: PathBuf = std::env::current_dir().expect("failed to read current dir"); let tp_dir = current_dir.join("template-provider"); let mut conf = Conf::default(); conf.wallet = Some(port.to_string()); - let staticdir = format!(".bitcoin-{}", port); - conf.staticdir = Some(tp_dir.join(staticdir)); - let port_arg = format!("-sv2port={}", port); - let sv2_interval_arg = format!("-sv2interval={}", sv2_interval); + + let staticdir = format!(".bitcoin-{port}"); + conf.staticdir = Some(tp_dir.join(staticdir.clone())); + let port_arg = format!("-sv2port={port}"); + let sv2_interval_arg = format!("-sv2interval={sv2_interval}"); + + match difficulty_level { + DifficultyLevel::Low => { + // use default corepc-node settings, which means regtest mode + // where every share is a block + } + DifficultyLevel::Mid => { + // use signet mode with genesis difficulty + // (signetchallenge=51, no signature needed on the coinbase) + // most of the time, a CPU should find a block in a minute or less + conf.args = vec!["-signet", "-fallbackfee=0.0001", "-signetchallenge=51"]; + conf.network = "signet"; + } + DifficultyLevel::High => { + // use signet mode with premined blocks raising difficulty to 77761.11 + // (signetchallenge=51, no signature needed on the coinbase) + // most of the time, a CPU should take a REALLY long time to find a block + conf.args = vec!["-signet", "-fallbackfee=0.0001", "-signetchallenge=51"]; + conf.network = "signet"; + + // Create signet datadir + let signet_datadir = tp_dir.join(staticdir.clone()).join("signet"); + create_dir_all(signet_datadir.clone()).expect("Failed to create signet directory"); + + // Copy high difficulty signet data into signet datadir + let high_diff_chain_dir = current_dir.join("high_diff_chain"); + fs_utils::copy_dir_contents(&high_diff_chain_dir, &signet_datadir) + .expect("Failed to copy high difficulty chain data"); + } + } + conf.args.extend(vec![ "-txindex=1", "-sv2", @@ -59,21 +104,20 @@ impl TemplateProvider { let arch = env::consts::ARCH; let download_filename = get_bitcoind_filename(os, arch); let bitcoin_exe_home = tp_dir - .join(format!("bitcoin-sv2-tp-{}", VERSION_TP)) + .join(format!("bitcoin-sv2-tp-{VERSION_TP}")) .join("bin"); if !bitcoin_exe_home.exists() { let tarball_bytes = match env::var("BITCOIND_TARBALL_FILE") { Ok(path) => tarball::read_from_file(&path), Err(_) => { + warn!("Downloading template provider for the testing session. This could take a while..."); let download_endpoint = env::var("BITCOIND_DOWNLOAD_ENDPOINT").unwrap_or_else(|_| { "https://github.com/Sjors/bitcoin/releases/download".to_owned() }); - let url = format!( - "{}/sv2-tp-{}/{}", - download_endpoint, VERSION_TP, download_filename - ); + let url = + format!("{download_endpoint}/sv2-tp-{VERSION_TP}/{download_filename}"); http::make_get_request(&url, 5) } }; @@ -110,9 +154,9 @@ impl TemplateProvider { } Err(e) => { if current_time.elapsed() > timeout { - panic!("Failed to start bitcoind: {}", e); + panic!("Failed to start bitcoind: {e}"); } - println!("Failed to start bitcoind due to {}", e); + println!("Failed to start bitcoind due to {e}"); } } } @@ -136,6 +180,13 @@ impl TemplateProvider { &self.bitcoind.params } + /// Return the result of `getblockchaininfo` RPC call. + pub fn get_blockchain_info(&self) -> Result { + let client = &self.bitcoind.client; + let blockchain_info = client.get_blockchain_info()?; + Ok(blockchain_info) + } + /// Create and broadcast a transaction to the mempool. /// /// It is recommended to use [`TemplateProvider::fund_wallet`] before calling this method to @@ -171,14 +222,14 @@ impl TemplateProvider { #[cfg(test)] mod tests { - use super::TemplateProvider; + 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); + let tp = TemplateProvider::start(port, 1, DifficultyLevel::Low); assert!(tp.fund_wallet().is_ok()); assert!(tp.create_mempool_transaction().is_ok()); } diff --git a/test/integration-tests/lib/types.rs b/test/integration-tests/lib/types.rs index a69a22a4fc..2f2459b0cc 100644 --- a/test/integration-tests/lib/types.rs +++ b/test/integration-tests/lib/types.rs @@ -1,5 +1,4 @@ -use codec_sv2::StandardEitherFrame; -use roles_logic_sv2::parsers::AnyMessage; +use stratum_common::roles_logic_sv2::{codec_sv2::StandardEitherFrame, parsers_sv2::AnyMessage}; pub type MessageFrame = StandardEitherFrame>; pub type MsgType = u8; diff --git a/test/integration-tests/lib/utils.rs b/test/integration-tests/lib/utils.rs index 4f77afe808..307d23a0f5 100644 --- a/test/integration-tests/lib/utils.rs +++ b/test/integration-tests/lib/utils.rs @@ -5,28 +5,33 @@ use crate::{ types::{MessageFrame, MsgType}, }; use async_channel::{Receiver, Sender}; -use codec_sv2::{ - framing_sv2::framing::Frame, HandshakeRole, Initiator, Responder, StandardEitherFrame, Sv2Frame, -}; use key_utils::{Secp256k1PublicKey, Secp256k1SecretKey}; -use network_helpers_sv2::noise_connection::Connection; use once_cell::sync::Lazy; -use roles_logic_sv2::parsers::{ - message_type_to_name, AnyMessage, CommonMessages, IsSv2Message, - JobDeclaration::{ - AllocateMiningJobToken, AllocateMiningJobTokenSuccess, DeclareMiningJob, - DeclareMiningJobError, DeclareMiningJobSuccess, ProvideMissingTransactions, - ProvideMissingTransactionsSuccess, PushSolution, - }, - TemplateDistribution, - TemplateDistribution::CoinbaseOutputConstraints, -}; use std::{ collections::HashSet, convert::TryInto, net::{SocketAddr, TcpListener}, sync::Mutex, }; +use stratum_common::{ + network_helpers_sv2::noise_connection::Connection, + roles_logic_sv2::{ + codec_sv2::{ + framing_sv2::framing::Frame, HandshakeRole, Initiator, Responder, StandardEitherFrame, + Sv2Frame, + }, + parsers_sv2::{ + message_type_to_name, AnyMessage, CommonMessages, IsSv2Message, + JobDeclaration::{ + AllocateMiningJobToken, AllocateMiningJobTokenSuccess, DeclareMiningJob, + DeclareMiningJobError, DeclareMiningJobSuccess, ProvideMissingTransactions, + ProvideMissingTransactionsSuccess, PushSolution, + }, + TemplateDistribution, + TemplateDistribution::CoinbaseOutputConstraints, + }, + }, +}; // prevents get_available_port from ever returning the same port twice static UNIQUE_PORTS: Lazy>> = Lazy::new(|| Mutex::new(HashSet::new())); @@ -79,8 +84,7 @@ pub async fn create_downstream( Responder::from_authority_kp(&pub_key, &prv_key, std::time::Duration::from_secs(10000)) .unwrap(); if let Ok((receiver_from_client, sender_to_client)) = - Connection::new::<'static, AnyMessage<'static>>(stream, HandshakeRole::Responder(responder)) - .await + Connection::new::>(stream, HandshakeRole::Responder(responder)).await { Some((receiver_from_client, sender_to_client)) } else { @@ -93,8 +97,7 @@ pub async fn create_upstream( ) -> Option<(Receiver, Sender)> { let initiator = Initiator::without_pk().expect("This fn call can not fail"); if let Ok((receiver_from_server, sender_to_server)) = - Connection::new::<'static, AnyMessage<'static>>(stream, HandshakeRole::Initiator(initiator)) - .await + Connection::new::>(stream, HandshakeRole::Initiator(initiator)).await { Some((receiver_from_server, sender_to_server)) } else { @@ -152,14 +155,15 @@ pub async fn recv_from_down_send_to_up( } } } else { - downstream_messages.add_message(msg_type, msg); + downstream_messages.add_message(msg_type, msg.clone()); send.send(frame) .await .map_err(|_| SnifferError::UpstreamClosed)?; tracing::info!( - "🔠Sv2 Sniffer {} | Forwarded: {} | Direction: ⬆", + "🔠Sv2 Sniffer {} | Forwarded: {} | Direction: ⬆ | Data: {}", identifier, - message_type_to_name(msg_type) + message_type_to_name(msg_type), + msg ); } } @@ -217,14 +221,15 @@ pub async fn recv_from_up_send_to_down( } } } else { - upstream_messages.add_message(msg_type, msg); + upstream_messages.add_message(msg_type, msg.clone()); send.send(frame) .await .map_err(|_| SnifferError::DownstreamClosed)?; tracing::info!( - "🔠Sv2 Sniffer {} | Forwarded: {} | Direction: ⬇", + "🔠Sv2 Sniffer {} | Forwarded: {} | Direction: ⬇ | Data: {}", identifier, - message_type_to_name(msg_type) + message_type_to_name(msg_type), + msg ); } } @@ -344,13 +349,11 @@ pub mod http { return res.as_bytes().to_vec(); } else if (500..600).contains(&status_code) { eprintln!( - "Attempt {}: URL {} returned a server error code {}", - attempt, download_url, status_code + "Attempt {attempt}: URL {download_url} returned a server error code {status_code}" ); } else { panic!( - "URL {} returned unexpected status code {}. Aborting.", - download_url, status_code + "URL {download_url} returned unexpected status code {status_code}. Aborting." ); } } @@ -366,33 +369,25 @@ pub mod http { if attempt < retries { let delay = 1u64 << (attempt - 1); - eprintln!("Retrying in {} seconds (exponential backoff)...", delay); + eprintln!("Retrying in {delay} seconds (exponential backoff)..."); std::thread::sleep(std::time::Duration::from_secs(delay)); } } // If all retries fail, panic with an error message - panic!( - "Cannot reach URL {} after {} attempts", - download_url, retries - ); + panic!("Cannot reach URL {download_url} after {retries} attempts"); } } pub mod tarball { - use flate2::read::GzDecoder; use std::{ fs::File, io::{BufReader, Read}, path::Path, }; - use tar::Archive; pub fn read_from_file(path: &str) -> Vec { let file = File::open(path).unwrap_or_else(|_| { - panic!( - "Cannot find {:?} specified with env var BITCOIND_TARBALL_FILE", - path - ) + panic!("Cannot find {path:?} specified with env var BITCOIND_TARBALL_FILE") }); let mut reader = BufReader::new(file); let mut buffer = Vec::new(); @@ -401,14 +396,54 @@ pub mod tarball { } pub fn unpack(tarball_bytes: &[u8], destination: &Path) { - let decoder = GzDecoder::new(tarball_bytes); - let mut archive = Archive::new(decoder); - for mut entry in archive.entries().unwrap().flatten() { - if let Ok(file) = entry.path() { - if file.ends_with("bitcoind") { - entry.unpack_in(destination).unwrap(); - } + use std::{io::Write as IoWrite, process::Command}; + + // Write tarball bytes to a temp file + let temp_tarball = destination.join("temp.tar.gz"); + let mut temp_file = File::create(&temp_tarball).unwrap(); + temp_file.write_all(tarball_bytes).unwrap(); + drop(temp_file); + + // Use system tar command to extract, which properly handles GNU sparse files + let output = Command::new("tar") + .arg("-xzf") + .arg(&temp_tarball) + .arg("-C") + .arg(destination) + .arg("--strip-components=0") + .output() + .expect("Failed to execute tar command"); + + if !output.status.success() { + eprintln!("tar stderr: {}", String::from_utf8_lossy(&output.stderr)); + panic!("tar extraction failed"); + } + + // Clean up temp tarball + std::fs::remove_file(&temp_tarball).ok(); + } +} + +pub mod fs_utils { + use std::{fs, path::Path}; + + /// Recursively copy all contents from source directory to destination directory + pub fn copy_dir_contents(src: &Path, dst: &Path) -> std::io::Result<()> { + if !dst.exists() { + fs::create_dir_all(dst)?; + } + + for entry in fs::read_dir(src)? { + let entry = entry?; + let src_path = entry.path(); + let dst_path = dst.join(entry.file_name()); + + if src_path.is_dir() { + copy_dir_contents(&src_path, &dst_path)?; + } else { + fs::copy(&src_path, &dst_path)?; } } + Ok(()) } } diff --git a/test/integration-tests/tests/jd_integration.rs b/test/integration-tests/tests/jd_integration.rs index ca0e3ff4cc..e9febca190 100644 --- a/test/integration-tests/tests/jd_integration.rs +++ b/test/integration-tests/tests/jd_integration.rs @@ -1,13 +1,15 @@ // This file contains integration tests for the `JDC/S` module. -use binary_sv2::{Seq064K, B032, U256}; use integration_tests_sv2::{ - interceptor::{IgnoreMessage, MessageDirection, ReplaceMessage}, + interceptor::{MessageDirection, ReplaceMessage}, + template_provider::DifficultyLevel, *, }; -use roles_logic_sv2::{ +use stratum_common::roles_logic_sv2::{ + self, + codec_sv2::binary_sv2::{Seq064K, B032, U256}, common_messages_sv2::*, job_declaration_sv2::{ProvideMissingTransactionsSuccess, PushSolution, *}, - parsers::AnyMessage, + parsers_sv2::AnyMessage, }; // This test verifies that jd-server does not exit when a connected jd-client shuts down. @@ -17,10 +19,10 @@ use roles_logic_sv2::{ #[tokio::test] async fn jds_should_not_panic_if_jdc_shutsdown() { start_tracing(); - let (tp, tp_addr) = start_template_provider(None); + let (tp, tp_addr) = start_template_provider(None, DifficultyLevel::Low); let (_pool, pool_addr) = start_pool(Some(tp_addr)).await; let (_jds, jds_addr) = start_jds(tp.rpc_info()); - let (sniffer_a, sniffer_addr_a) = start_sniffer("0", jds_addr, false, vec![]); + let (sniffer_a, sniffer_addr_a) = start_sniffer("0", jds_addr, false, vec![], None); let (jdc, jdc_addr) = start_jdc(&[(pool_addr, sniffer_addr_a)], tp_addr); sniffer_a .wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_SETUP_CONNECTION) @@ -31,10 +33,10 @@ async fn jds_should_not_panic_if_jdc_shutsdown() { MESSAGE_TYPE_SETUP_CONNECTION_SUCCESS, ) .await; - jdc.shutdown(); - tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + drop(jdc); + tokio::time::sleep(tokio::time::Duration::from_millis(2000)).await; assert!(tokio::net::TcpListener::bind(jdc_addr).await.is_ok()); - let (sniffer, sniffer_addr) = start_sniffer("0", jds_addr, false, vec![]); + let (sniffer, sniffer_addr) = start_sniffer("0", jds_addr, false, vec![], None); let (_jdc_1, _jdc_addr_1) = start_jdc(&[(pool_addr, sniffer_addr)], tp_addr); sniffer .wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_SETUP_CONNECTION) @@ -48,14 +50,14 @@ async fn jds_should_not_panic_if_jdc_shutsdown() { #[tokio::test] async fn jdc_tp_success_setup() { start_tracing(); - let (tp, tp_addr) = start_template_provider(None); + let (tp, tp_addr) = start_template_provider(None, DifficultyLevel::Low); let (_pool, pool_addr) = start_pool(Some(tp_addr)).await; let (_jds, jds_addr) = start_jds(tp.rpc_info()); - let (tp_jdc_sniffer, tp_jdc_sniffer_addr) = start_sniffer("0", tp_addr, false, vec![]); + let (tp_jdc_sniffer, tp_jdc_sniffer_addr) = start_sniffer("0", tp_addr, false, vec![], None); let (_jdc, jdc_addr) = start_jdc(&[(pool_addr, jds_addr)], tp_jdc_sniffer_addr); // This is needed because jd-client waits for a downstream connection before it starts // exchanging messages with the Template Provider. - start_sv2_translator(jdc_addr); + start_sv2_translator(jdc_addr).await; tp_jdc_sniffer .wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_SETUP_CONNECTION) .await; @@ -67,51 +69,6 @@ async fn jdc_tp_success_setup() { .await; } -// This test ensures that `jd-client` does not panic even if `jd-server` leaves the connection open -// after receiving the request for token. -#[tokio::test] -async fn jdc_does_not_stackoverflow_when_no_token() { - start_tracing(); - let (tp, tp_addr) = start_template_provider(None); - let (_pool, pool_addr) = start_pool(Some(tp_addr)).await; - let (_jds, jds_addr) = start_jds(tp.rpc_info()); - let block_from_message = IgnoreMessage::new( - MessageDirection::ToDownstream, - MESSAGE_TYPE_ALLOCATE_MINING_JOB_TOKEN_SUCCESS, - ); - let (jds_jdc_sniffer, jds_jdc_sniffer_addr) = start_sniffer( - "JDS-JDC-sniffer", - jds_addr, - false, - vec![block_from_message.into()], - ); - let (_jdc, jdc_addr) = start_jdc(&[(pool_addr, jds_jdc_sniffer_addr)], tp_addr); - let _ = start_sv2_translator(jdc_addr); - jds_jdc_sniffer - .wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_SETUP_CONNECTION) - .await; - jds_jdc_sniffer - .wait_for_message_type( - MessageDirection::ToDownstream, - MESSAGE_TYPE_SETUP_CONNECTION_SUCCESS, - ) - .await; - jds_jdc_sniffer - .wait_for_message_type( - MessageDirection::ToUpstream, - MESSAGE_TYPE_ALLOCATE_MINING_JOB_TOKEN, - ) - .await; - - // The 3-second delay simulates a scenario where JDC does not receive an - // `AllocateMiningJobTokenSuccess` response from JDS, leaving `self.allocated_tokens` empty. - // Without the fix introduced in [PR](https://github.com/stratum-mining/stratum/pull/720), - // JDC would recursively call `Self::get_last_token`, eventually causing a stack overflow. - // This test verifies that JDC now blocks/yields correctly instead of infinitely recursing. - tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; - assert!(tokio::net::TcpListener::bind(jdc_addr).await.is_err()); -} - // This test verifies that JDS does not exit when it receives a `SubmitSolution` // while still expecting a `ProvideMissingTransactionsSuccess`. // @@ -120,8 +77,8 @@ async fn jdc_does_not_stackoverflow_when_no_token() { #[tokio::test] async fn jds_receive_solution_while_processing_declared_job_test() { start_tracing(); - let (tp_1, tp_addr_1) = start_template_provider(None); - let (tp_2, tp_addr_2) = start_template_provider(None); + let (tp_1, tp_addr_1) = start_template_provider(None, DifficultyLevel::Low); + let (tp_2, tp_addr_2) = start_template_provider(None, DifficultyLevel::Low); let (_pool, pool_addr) = start_pool(Some(tp_addr_1)).await; let (_jds, jds_addr) = start_jds(tp_1.rpc_info()); @@ -136,7 +93,7 @@ async fn jds_receive_solution_while_processing_declared_job_test() { let submit_solution_replace = ReplaceMessage::new( MessageDirection::ToUpstream, MESSAGE_TYPE_PROVIDE_MISSING_TRANSACTIONS_SUCCESS, - AnyMessage::JobDeclaration(roles_logic_sv2::parsers::JobDeclaration::PushSolution( + AnyMessage::JobDeclaration(roles_logic_sv2::parsers_sv2::JobDeclaration::PushSolution( PushSolution { ntime: 0, nbits: 0, @@ -150,10 +107,16 @@ async fn jds_receive_solution_while_processing_declared_job_test() { // This sniffer sits between `jds` and `jdc`, replacing `ProvideMissingTransactionSuccess` // with `SubmitSolution`. - let (sniffer_a, sniffer_a_addr) = - start_sniffer("A", jds_addr, false, vec![submit_solution_replace.into()]); + let (sniffer_a, sniffer_a_addr) = start_sniffer( + "A", + jds_addr, + false, + vec![submit_solution_replace.into()], + None, + ); let (_jdc, jdc_addr) = start_jdc(&[(pool_addr, sniffer_a_addr)], tp_addr_2); - start_sv2_translator(jdc_addr); + let (_translator, tproxy_addr) = start_sv2_translator(jdc_addr).await; + let (_minerd_process, _minerd_addr) = start_minerd(tproxy_addr, None, None, false).await; assert!(tp_2.fund_wallet().is_ok()); assert!(tp_2.create_mempool_transaction().is_ok()); sniffer_a @@ -204,8 +167,8 @@ async fn jds_receive_solution_while_processing_declared_job_test() { #[tokio::test] async fn jds_wont_exit_upon_receiving_unexpected_txids_in_provide_missing_transaction_success() { start_tracing(); - let (tp_1, tp_addr_1) = start_template_provider(None); - let (tp_2, tp_addr_2) = start_template_provider(None); + let (tp_1, tp_addr_1) = start_template_provider(None, DifficultyLevel::Low); + let (tp_2, tp_addr_2) = start_template_provider(None, DifficultyLevel::Low); assert!(tp_2.fund_wallet().is_ok()); assert!(tp_2.create_mempool_transaction().is_ok()); @@ -217,7 +180,7 @@ async fn jds_wont_exit_upon_receiving_unexpected_txids_in_provide_missing_transa MessageDirection::ToUpstream, MESSAGE_TYPE_PROVIDE_MISSING_TRANSACTIONS_SUCCESS, AnyMessage::JobDeclaration( - roles_logic_sv2::parsers::JobDeclaration::ProvideMissingTransactionsSuccess( + roles_logic_sv2::parsers_sv2::JobDeclaration::ProvideMissingTransactionsSuccess( ProvideMissingTransactionsSuccess { request_id: 1, transaction_list: Seq064K::new(Vec::new()).unwrap(), @@ -233,10 +196,12 @@ async fn jds_wont_exit_upon_receiving_unexpected_txids_in_provide_missing_transa jds_addr, false, vec![provide_missing_transaction_success_replace.into()], + None, ); let (_, jdc_addr_1) = start_jdc(&[(pool_addr, sniffer_addr)], tp_addr_2); - start_sv2_translator(jdc_addr_1); + let (_translator, tproxy_addr) = start_sv2_translator(jdc_addr_1).await; + let (_minerd_process, _minerd_addr) = start_minerd(tproxy_addr, None, None, false).await; sniffer .wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_SETUP_CONNECTION) diff --git a/test/integration-tests/tests/jd_provide_missing_transaction.rs b/test/integration-tests/tests/jd_provide_missing_transaction.rs index f59f8b19aa..62a25260c6 100644 --- a/test/integration-tests/tests/jd_provide_missing_transaction.rs +++ b/test/integration-tests/tests/jd_provide_missing_transaction.rs @@ -1,16 +1,17 @@ -use integration_tests_sv2::{interceptor::MessageDirection, *}; -use roles_logic_sv2::job_declaration_sv2::*; +use integration_tests_sv2::{interceptor::MessageDirection, template_provider::DifficultyLevel, *}; +use stratum_common::roles_logic_sv2::job_declaration_sv2::*; #[tokio::test] async fn jds_ask_for_missing_transactions() { start_tracing(); - let (tp_1, tp_addr_1) = start_template_provider(None); - let (tp_2, tp_addr_2) = start_template_provider(None); + let (tp_1, tp_addr_1) = start_template_provider(None, DifficultyLevel::Low); + let (tp_2, tp_addr_2) = start_template_provider(None, DifficultyLevel::Low); let (_pool, pool_addr) = start_pool(Some(tp_addr_1)).await; let (_jds, jds_addr) = start_jds(tp_1.rpc_info()); - let (sniffer, sniffer_addr) = start_sniffer("A", jds_addr, false, vec![]); + let (sniffer, sniffer_addr) = start_sniffer("A", jds_addr, false, vec![], None); let (_jdc, jdc_addr) = start_jdc(&[(pool_addr, sniffer_addr)], tp_addr_2); - start_sv2_translator(jdc_addr); + let (_translator, tproxy_addr) = start_sv2_translator(jdc_addr).await; + let (_minerd_process, _minerd_addr) = start_minerd(tproxy_addr, None, None, false).await; assert!(tp_2.fund_wallet().is_ok()); assert!(tp_2.create_mempool_transaction().is_ok()); sniffer diff --git a/test/integration-tests/tests/jd_tproxy_integration.rs b/test/integration-tests/tests/jd_tproxy_integration.rs index 77cdf5111c..4920d41d5b 100644 --- a/test/integration-tests/tests/jd_tproxy_integration.rs +++ b/test/integration-tests/tests/jd_tproxy_integration.rs @@ -1,16 +1,17 @@ -use integration_tests_sv2::{interceptor::MessageDirection, *}; -use roles_logic_sv2::{common_messages_sv2::*, mining_sv2::*}; +use integration_tests_sv2::{interceptor::MessageDirection, template_provider::DifficultyLevel, *}; +use stratum_common::roles_logic_sv2::{common_messages_sv2::*, mining_sv2::*}; #[tokio::test] async fn jd_tproxy_integration() { start_tracing(); - let (tp, tp_addr) = start_template_provider(None); + let (tp, tp_addr) = start_template_provider(None, DifficultyLevel::Low); let (_pool, pool_addr) = start_pool(Some(tp_addr)).await; - let (jdc_pool_sniffer, jdc_pool_sniffer_addr) = start_sniffer("0", pool_addr, false, vec![]); + let (jdc_pool_sniffer, jdc_pool_sniffer_addr) = + start_sniffer("0", pool_addr, false, vec![], None); let (_jds, jds_addr) = start_jds(tp.rpc_info()); let (_jdc, jdc_addr) = start_jdc(&[(jdc_pool_sniffer_addr, jds_addr)], tp_addr); - let (_translator, tproxy_addr) = start_sv2_translator(jdc_addr); - start_mining_device_sv1(tproxy_addr, false, None); + let (_translator, tproxy_addr) = start_sv2_translator(jdc_addr).await; + let (_minerd_process, _minerd_addr) = start_minerd(tproxy_addr, None, None, false).await; jdc_pool_sniffer .wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_SETUP_CONNECTION) .await; @@ -32,12 +33,6 @@ async fn jd_tproxy_integration() { MESSAGE_TYPE_OPEN_EXTENDED_MINING_CHANNEL_SUCCESS, ) .await; - jdc_pool_sniffer - .wait_for_message_type( - MessageDirection::ToDownstream, - MESSAGE_TYPE_NEW_EXTENDED_MINING_JOB, - ) - .await; jdc_pool_sniffer .wait_for_message_type( MessageDirection::ToUpstream, diff --git a/test/integration-tests/tests/jdc_block_propogation.rs b/test/integration-tests/tests/jdc_block_propagation.rs similarity index 60% rename from test/integration-tests/tests/jdc_block_propogation.rs rename to test/integration-tests/tests/jdc_block_propagation.rs index 3bc9ff66e9..28a1837a59 100644 --- a/test/integration-tests/tests/jdc_block_propogation.rs +++ b/test/integration-tests/tests/jdc_block_propagation.rs @@ -1,25 +1,31 @@ use integration_tests_sv2::{ interceptor::{IgnoreMessage, MessageDirection}, + template_provider::DifficultyLevel, *, }; -use roles_logic_sv2::{job_declaration_sv2::*, template_distribution_sv2::*}; +use stratum_common::roles_logic_sv2::{job_declaration_sv2::*, template_distribution_sv2::*}; -// Block propogated from JDC to TP +// Block propagated from JDC to TP #[tokio::test] -async fn propogated_from_jdc_to_tp() { +async fn propagated_from_jdc_to_tp() { start_tracing(); - let (tp, tp_addr) = start_template_provider(None); + let (tp, tp_addr) = start_template_provider(None, DifficultyLevel::Low); let current_block_hash = tp.get_best_block_hash().unwrap(); let (_pool, pool_addr) = start_pool(Some(tp_addr)).await; let (_jds, jds_addr) = start_jds(tp.rpc_info()); let ignore_push_solution = IgnoreMessage::new(MessageDirection::ToUpstream, MESSAGE_TYPE_PUSH_SOLUTION); - let (jdc_jds_sniffer, jdc_jds_sniffer_addr) = - start_sniffer("0", jds_addr, false, vec![ignore_push_solution.into()]); - let (jdc_tp_sniffer, jdc_tp_sniffer_addr) = start_sniffer("1", tp_addr, false, vec![]); + let (jdc_jds_sniffer, jdc_jds_sniffer_addr) = start_sniffer( + "0", + jds_addr, + false, + vec![ignore_push_solution.into()], + None, + ); + let (jdc_tp_sniffer, jdc_tp_sniffer_addr) = start_sniffer("1", tp_addr, false, vec![], None); let (_jdc, jdc_addr) = start_jdc(&[(pool_addr, jdc_jds_sniffer_addr)], jdc_tp_sniffer_addr); - let (_translator, tproxy_addr) = start_sv2_translator(jdc_addr); - start_mining_device_sv1(tproxy_addr, false, None); + let (_translator, tproxy_addr) = start_sv2_translator(jdc_addr).await; + let (_minerd_process, _minerd_addr) = start_minerd(tproxy_addr, None, None, false).await; jdc_tp_sniffer .wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_SUBMIT_SOLUTION) .await; @@ -27,5 +33,6 @@ async fn propogated_from_jdc_to_tp() { .assert_message_not_present(MessageDirection::ToUpstream, MESSAGE_TYPE_PUSH_SOLUTION) .await; let new_block_hash = tp.get_best_block_hash().unwrap(); + tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; assert_ne!(current_block_hash, new_block_hash); } diff --git a/test/integration-tests/tests/jdc_fallback.rs b/test/integration-tests/tests/jdc_fallback.rs deleted file mode 100644 index c748b4cb51..0000000000 --- a/test/integration-tests/tests/jdc_fallback.rs +++ /dev/null @@ -1,73 +0,0 @@ -use integration_tests_sv2::{ - interceptor::{MessageDirection, ReplaceMessage}, - *, -}; -use roles_logic_sv2::{ - common_messages_sv2::*, - mining_sv2::{SubmitSharesError, *}, - parsers::{AnyMessage, Mining}, -}; -use std::convert::TryInto; - -// Tests whether JDC will switch to a new pool after receiving a `SubmitSharesError` message from -// the currently connected pool. -// -// This ignore directive can be removed once this issue is resolved: https://github.com/stratum-mining/stratum/issues/1574. -#[ignore] -#[tokio::test] -async fn test_jdc_pool_fallback_after_submit_rejection() { - start_tracing(); - let (tp, tp_addr) = start_template_provider(None); - let (_pool_1, pool_addr_1) = start_pool(Some(tp_addr)).await; - // Sniffer between JDC and first pool - let (sniffer_1, sniffer_addr_1) = start_sniffer( - "0", - pool_addr_1, - false, - vec![ - // Should trigger Fallback process in JDC - ReplaceMessage::new( - MessageDirection::ToDownstream, - MESSAGE_TYPE_SUBMIT_SHARES_SUCCESS, - AnyMessage::Mining(Mining::SubmitSharesError(SubmitSharesError { - channel_id: 0, - sequence_number: 0, - error_code: "invalid-nonce".to_string().into_bytes().try_into().unwrap(), - })), - ) - .into(), - ], - ); - let (_pool_2, pool_addr_2) = start_pool(Some(tp_addr)).await; - // Sniffer between JDC and second pool - let (sniffer_2, sniffer_addr_2) = start_sniffer("1", pool_addr_2, false, vec![]); - let (_jds_1, jds_addr_1) = start_jds(tp.rpc_info()); - // Sniffer between JDC and first JDS - let (sniffer_3, sniffer_addr_3) = start_sniffer("2", jds_addr_1, false, vec![]); - let (_jds_2, jds_addr_2) = start_jds(tp.rpc_info()); - // Sniffer between JDC and second JDS - let (sniffer_4, sniffer_addr_4) = start_sniffer("3", jds_addr_2, false, vec![]); - let (_jdc, jdc_addr) = start_jdc( - &[ - (sniffer_addr_1, sniffer_addr_3), - (sniffer_addr_2, sniffer_addr_4), - ], - tp_addr, - ); - // Assert that JDC has connected to the first (Pool,JDS) pair - sniffer_1 - .wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_SETUP_CONNECTION) - .await; - sniffer_3 - .wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_SETUP_CONNECTION) - .await; - let (_translator, sv2_translator_addr) = start_sv2_translator(jdc_addr); - start_mining_device_sv1(sv2_translator_addr, false, None); - // Assert that JDC switched to the second (Pool,JDS) pair - sniffer_2 - .wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_SETUP_CONNECTION) - .await; - sniffer_4 - .wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_SETUP_CONNECTION) - .await; -} diff --git a/test/integration-tests/tests/jdc_receives_submit_shares_success.rs b/test/integration-tests/tests/jdc_receives_submit_shares_success.rs new file mode 100644 index 0000000000..c120578289 --- /dev/null +++ b/test/integration-tests/tests/jdc_receives_submit_shares_success.rs @@ -0,0 +1,22 @@ +use integration_tests_sv2::{interceptor::MessageDirection, template_provider::DifficultyLevel, *}; +use stratum_common::roles_logic_sv2::mining_sv2::*; + +#[tokio::test] +async fn jdc_submit_shares_success() { + start_tracing(); + let (tp, tp_addr) = start_template_provider(None, DifficultyLevel::Low); + let (_pool, pool_addr) = start_pool(Some(tp_addr)).await; + let (sniffer, sniffer_addr) = start_sniffer("0", pool_addr, false, vec![], None); + let (_jds, jds_addr) = start_jds(tp.rpc_info()); + let (_jdc, jdc_addr) = start_jdc(&[(sniffer_addr, jds_addr)], tp_addr); + let (_translator, tproxy_addr) = start_sv2_translator(jdc_addr).await; + let (_minerd_process, _minerd_addr) = start_minerd(tproxy_addr, None, None, false).await; + + // make sure sure JDC gets a share acknowledgement + sniffer + .wait_for_message_type( + MessageDirection::ToDownstream, + MESSAGE_TYPE_SUBMIT_SHARES_SUCCESS, + ) + .await; +} diff --git a/test/integration-tests/tests/jds_block_propogation.rs b/test/integration-tests/tests/jds_block_propagation.rs similarity index 63% rename from test/integration-tests/tests/jds_block_propogation.rs rename to test/integration-tests/tests/jds_block_propagation.rs index 31158060b9..b315dd57d8 100644 --- a/test/integration-tests/tests/jds_block_propogation.rs +++ b/test/integration-tests/tests/jds_block_propagation.rs @@ -1,25 +1,31 @@ use integration_tests_sv2::{ interceptor::{IgnoreMessage, MessageDirection}, + template_provider::DifficultyLevel, *, }; -use roles_logic_sv2::{job_declaration_sv2::*, template_distribution_sv2::*}; +use stratum_common::roles_logic_sv2::{job_declaration_sv2::*, template_distribution_sv2::*}; -// Block propogated from JDS to TP +// Block propagated from JDS to TP #[tokio::test] -async fn propogated_from_jds_to_tp() { +async fn propagated_from_jds_to_tp() { start_tracing(); - let (tp, tp_addr) = start_template_provider(None); + let (tp, tp_addr) = start_template_provider(None, DifficultyLevel::Low); let current_block_hash = tp.get_best_block_hash().unwrap(); let (_pool, pool_addr) = start_pool(Some(tp_addr)).await; let (_jds, jds_addr) = start_jds(tp.rpc_info()); - let (jdc_jds_sniffer, jdc_jds_sniffer_addr) = start_sniffer("0", jds_addr, false, vec![]); + let (jdc_jds_sniffer, jdc_jds_sniffer_addr) = start_sniffer("0", jds_addr, false, vec![], None); let ignore_submit_solution = IgnoreMessage::new(MessageDirection::ToUpstream, MESSAGE_TYPE_SUBMIT_SOLUTION); - let (jdc_tp_sniffer, jdc_tp_sniffer_addr) = - start_sniffer("1", tp_addr, false, vec![ignore_submit_solution.into()]); + let (jdc_tp_sniffer, jdc_tp_sniffer_addr) = start_sniffer( + "1", + tp_addr, + false, + vec![ignore_submit_solution.into()], + None, + ); let (_jdc, jdc_addr) = start_jdc(&[(pool_addr, jdc_jds_sniffer_addr)], jdc_tp_sniffer_addr); - let (_translator, tproxy_addr) = start_sv2_translator(jdc_addr); - start_mining_device_sv1(tproxy_addr, false, None); + let (_translator, tproxy_addr) = start_sv2_translator(jdc_addr).await; + let (_minerd_process, _minerd_addr) = start_minerd(tproxy_addr, None, None, false).await; jdc_jds_sniffer .wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_PUSH_SOLUTION) .await; diff --git a/test/integration-tests/tests/pool_integration.rs b/test/integration-tests/tests/pool_integration.rs index 0d000065ce..62114f007e 100644 --- a/test/integration-tests/tests/pool_integration.rs +++ b/test/integration-tests/tests/pool_integration.rs @@ -1,11 +1,11 @@ // This file contains integration tests for the `PoolSv2` module. // // `PoolSv2` is a module that implements the Pool role in the Stratum V2 protocol. -use integration_tests_sv2::{interceptor::MessageDirection, *}; -use roles_logic_sv2::{ +use integration_tests_sv2::{interceptor::MessageDirection, template_provider::DifficultyLevel, *}; +use stratum_common::roles_logic_sv2::{ common_messages_sv2::{Protocol, SetupConnection, *}, mining_sv2::*, - parsers::{AnyMessage, CommonMessages, Mining, TemplateDistribution}, + parsers_sv2::{AnyMessage, CommonMessages, Mining, TemplateDistribution}, template_distribution_sv2::*, }; @@ -16,8 +16,8 @@ use roles_logic_sv2::{ #[tokio::test] async fn success_pool_template_provider_connection() { start_tracing(); - let (_tp, tp_addr) = start_template_provider(None); - let (sniffer, sniffer_addr) = start_sniffer("", tp_addr, true, vec![]); + let (_tp, tp_addr) = start_template_provider(None, DifficultyLevel::Low); + let (sniffer, sniffer_addr) = start_sniffer("", tp_addr, true, vec![], None); let _ = start_pool(Some(sniffer_addr)).await; // here we assert that the downstream(pool in this case) have sent `SetupConnection` message // with the correct parameters, protocol, flags, min_version and max_version. Note that the @@ -89,17 +89,31 @@ async fn success_pool_template_provider_connection() { async fn header_timestamp_value_assertion_in_new_extended_mining_job() { start_tracing(); let sv2_interval = Some(5); - let (_tp, tp_addr) = start_template_provider(sv2_interval); + let (_tp, tp_addr) = start_template_provider(sv2_interval, DifficultyLevel::High); let tp_pool_sniffer_identifier = "header_timestamp_value_assertion_in_new_extended_mining_job tp_pool sniffer"; let (tp_pool_sniffer, tp_pool_sniffer_addr) = - start_sniffer(tp_pool_sniffer_identifier, tp_addr, false, vec![]); + start_sniffer(tp_pool_sniffer_identifier, tp_addr, false, vec![], None); let (_, pool_addr) = start_pool(Some(tp_pool_sniffer_addr)).await; let pool_translator_sniffer_identifier = "header_timestamp_value_assertion_in_new_extended_mining_job pool_translator sniffer"; - let (pool_translator_sniffer, pool_translator_sniffer_addr) = - start_sniffer(pool_translator_sniffer_identifier, pool_addr, false, vec![]); - let _tproxy_addr = start_sv2_translator(pool_translator_sniffer_addr); + let (pool_translator_sniffer, pool_translator_sniffer_addr) = start_sniffer( + pool_translator_sniffer_identifier, + pool_addr, + false, + vec![ + // Block SubmitSharesSuccess messages to prevent them from interfering + // with the test's expectation to receive NewExtendedMiningJob messages + integration_tests_sv2::interceptor::IgnoreMessage::new( + integration_tests_sv2::interceptor::MessageDirection::ToDownstream, + MESSAGE_TYPE_SUBMIT_SHARES_SUCCESS, + ) + .into(), + ], + None, + ); + let (_tproxy, tproxy_addr) = start_sv2_translator(pool_translator_sniffer_addr).await; + let (_minerd_process, _minerd_addr) = start_minerd(tproxy_addr, None, None, false).await; tp_pool_sniffer .wait_for_message_type( @@ -157,9 +171,9 @@ async fn header_timestamp_value_assertion_in_new_extended_mining_job() { #[tokio::test] async fn pool_standard_channel_receives_share() { start_tracing(); - let (_tp, tp_addr) = start_template_provider(None); + let (_tp, tp_addr) = start_template_provider(None, DifficultyLevel::Low); let (_pool, pool_addr) = start_pool(Some(tp_addr)).await; - let (sniffer, sniffer_addr) = start_sniffer("A", pool_addr, false, vec![]); + let (sniffer, sniffer_addr) = start_sniffer("A", pool_addr, false, vec![], None); start_mining_device_sv2(sniffer_addr, None, None, None, 1, None, true); sniffer .wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_SETUP_CONNECTION) diff --git a/test/integration-tests/tests/sniffer_integration.rs b/test/integration-tests/tests/sniffer_integration.rs index 335fa31bbd..8dc4bd6c70 100644 --- a/test/integration-tests/tests/sniffer_integration.rs +++ b/test/integration-tests/tests/sniffer_integration.rs @@ -1,21 +1,22 @@ // This file contains integration tests for the `Sniffer` module. use integration_tests_sv2::{ interceptor::{IgnoreMessage, MessageDirection, ReplaceMessage}, + template_provider::DifficultyLevel, *, }; -use roles_logic_sv2::{ +use std::convert::TryInto; +use stratum_common::roles_logic_sv2::{ common_messages_sv2::{Protocol, SetupConnection, SetupConnectionSuccess, *}, - parsers::{AnyMessage, CommonMessages}, + parsers_sv2::{AnyMessage, CommonMessages}, template_distribution_sv2::*, }; -use std::convert::TryInto; // This test aims to assert that Sniffer is able to intercept and replace/ignore messages. // TP -> sniffer_a -> sniffer_b -> Pool #[tokio::test] async fn test_sniffer_interception() { start_tracing(); - let (_tp, tp_addr) = start_template_provider(None); + let (_tp, tp_addr) = start_template_provider(None, DifficultyLevel::Low); let ignore_message = IgnoreMessage::new(MessageDirection::ToDownstream, MESSAGE_TYPE_NEW_TEMPLATE); let setup_connection_message = @@ -55,12 +56,14 @@ async fn test_sniffer_interception() { setup_connection_success_replacement.into(), ignore_message.into(), ], + None, ); let (sniffer_b, sniffer_b_addr) = start_sniffer( "B", sniffer_a_addr, false, vec![setup_connection_replacement.into()], + None, ); let _ = start_pool(Some(sniffer_b_addr)).await; sniffer_a @@ -107,8 +110,8 @@ async fn test_sniffer_interception() { #[tokio::test] async fn test_sniffer_wait_for_message_type_with_remove() { start_tracing(); - let (_tp, tp_addr) = start_template_provider(None); - let (sniffer, sniffer_addr) = start_sniffer("", tp_addr, false, vec![]); + let (_tp, tp_addr) = start_template_provider(None, DifficultyLevel::Low); + let (sniffer, sniffer_addr) = start_sniffer("", tp_addr, false, vec![], None); let _ = start_pool(Some(sniffer_addr)).await; assert!( sniffer diff --git a/test/integration-tests/tests/sv1.rs b/test/integration-tests/tests/sv1.rs index 85e0236932..4529934fd0 100644 --- a/test/integration-tests/tests/sv1.rs +++ b/test/integration-tests/tests/sv1.rs @@ -1,34 +1,23 @@ #![cfg(feature = "sv1")] -use integration_tests_sv2::*; +use integration_tests_sv2::{template_provider::DifficultyLevel, *}; use interceptor::MessageDirection; #[tokio::test] async fn test_basic_sv1() { start_tracing(); - let (_tp, tp_addr) = start_template_provider(None); + let (_tp, tp_addr) = start_template_provider(None, DifficultyLevel::Low); let (_pool, pool_addr) = start_pool(Some(tp_addr)).await; - let (_, tproxy_addr) = start_sv2_translator(pool_addr); + let (_, tproxy_addr) = start_sv2_translator(pool_addr).await; let (sniffer_sv1, sniffer_sv1_addr) = start_sv1_sniffer(tproxy_addr); - let _mining_device = start_mining_device_sv1(sniffer_sv1_addr, false, None); + let (_minerd_process, _minerd_addr) = start_minerd(sniffer_sv1_addr, None, None, false).await; sniffer_sv1 - .wait_for_message(&["mining.configure"], MessageDirection::ToUpstream) + .wait_for_message(&["mining.subscribe"], MessageDirection::ToUpstream) .await; sniffer_sv1 .wait_for_message(&["mining.authorize"], MessageDirection::ToUpstream) .await; sniffer_sv1 - .wait_for_message( - &[ - "minimum-difficulty", - "version-rolling", - "version-rolling.mask", - "version-rolling.min-bit-count", - ], - MessageDirection::ToDownstream, - ) - .await; - sniffer_sv1 - .wait_for_message(&["mining.subscribe"], MessageDirection::ToUpstream) + .wait_for_message(&["mining.set_difficulty"], MessageDirection::ToDownstream) .await; sniffer_sv1 .wait_for_message(&["mining.notify"], MessageDirection::ToDownstream) diff --git a/test/integration-tests/tests/sv2_mining_device.rs b/test/integration-tests/tests/sv2_mining_device.rs index 79073f6db9..d022a173a0 100644 --- a/test/integration-tests/tests/sv2_mining_device.rs +++ b/test/integration-tests/tests/sv2_mining_device.rs @@ -1,12 +1,12 @@ -use integration_tests_sv2::{interceptor::MessageDirection, *}; -use roles_logic_sv2::common_messages_sv2::*; +use integration_tests_sv2::{interceptor::MessageDirection, template_provider::DifficultyLevel, *}; +use stratum_common::roles_logic_sv2::common_messages_sv2::*; #[tokio::test] async fn sv2_mining_device_and_pool_success() { start_tracing(); - let (_tp, tp_addr) = start_template_provider(None); + let (_tp, tp_addr) = start_template_provider(None, DifficultyLevel::Low); let (_pool, pool_addr) = start_pool(Some(tp_addr)).await; - let (sniffer, sniffer_addr) = start_sniffer("A", pool_addr, false, vec![]); + let (sniffer, sniffer_addr) = start_sniffer("A", pool_addr, false, vec![], None); start_mining_device_sv2(sniffer_addr, None, None, None, 1, None, true); sniffer .wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_SETUP_CONNECTION) diff --git a/test/integration-tests/tests/template_provider_integration.rs b/test/integration-tests/tests/template_provider_integration.rs new file mode 100644 index 0000000000..78615bd2ee --- /dev/null +++ b/test/integration-tests/tests/template_provider_integration.rs @@ -0,0 +1,25 @@ +use integration_tests_sv2::{template_provider::DifficultyLevel, *}; + +#[tokio::test] +async fn tp_low_diff() { + let (tp, _) = start_template_provider(None, DifficultyLevel::Low); + let blockchain_info = tp.get_blockchain_info().unwrap(); + assert_eq!(blockchain_info.difficulty, 4.6565423739069247e-10); + assert_eq!(blockchain_info.chain, "regtest"); +} + +#[tokio::test] +async fn tp_mid_diff() { + let (tp, _) = start_template_provider(None, DifficultyLevel::Mid); + let blockchain_info = tp.get_blockchain_info().unwrap(); + assert_eq!(blockchain_info.difficulty, 0.001126515290698186); + assert_eq!(blockchain_info.chain, "signet"); +} + +#[tokio::test] +async fn tp_high_diff() { + let (tp, _) = start_template_provider(None, DifficultyLevel::High); + let blockchain_info = tp.get_blockchain_info().unwrap(); + assert_eq!(blockchain_info.difficulty, 77761.1123986095); + assert_eq!(blockchain_info.chain, "signet"); +} diff --git a/test/integration-tests/tests/translator_integration.rs b/test/integration-tests/tests/translator_integration.rs index d08ab3b1a9..4b068e1508 100644 --- a/test/integration-tests/tests/translator_integration.rs +++ b/test/integration-tests/tests/translator_integration.rs @@ -1,10 +1,6 @@ // This file contains integration tests for the `TranslatorSv2` module. -use integration_tests_sv2::{interceptor::MessageDirection, *}; -use roles_logic_sv2::{ - common_messages_sv2::*, - mining_sv2::*, - parsers::{AnyMessage, CommonMessages, Mining}, -}; +use integration_tests_sv2::{interceptor::MessageDirection, template_provider::DifficultyLevel, *}; +use stratum_common::roles_logic_sv2::{common_messages_sv2::*, mining_sv2::*}; // This test runs an sv2 translator between an sv1 mining device and a pool. the connection between // the translator and the pool is intercepted by a sniffer. The test checks if the translator and @@ -13,35 +9,39 @@ use roles_logic_sv2::{ #[tokio::test] async fn translate_sv1_to_sv2_successfully() { start_tracing(); - let (_tp, tp_addr) = start_template_provider(None); + let (_tp, tp_addr) = start_template_provider(None, DifficultyLevel::Low); let (_pool, pool_addr) = start_pool(Some(tp_addr)).await; let (pool_translator_sniffer, pool_translator_sniffer_addr) = - start_sniffer("0", pool_addr, false, vec![]); - let (_, tproxy_addr) = start_sv2_translator(pool_translator_sniffer_addr); - start_mining_device_sv1(tproxy_addr, false, None); + start_sniffer("0", pool_addr, false, vec![], None); + let (_, tproxy_addr) = start_sv2_translator(pool_translator_sniffer_addr).await; + let (_minerd_process, _minerd_addr) = start_minerd(tproxy_addr, None, None, false).await; pool_translator_sniffer .wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_SETUP_CONNECTION) .await; - assert_common_message!( - &pool_translator_sniffer.next_message_from_downstream(), - SetupConnection - ); - assert_common_message!( - &pool_translator_sniffer.next_message_from_upstream(), - SetupConnectionSuccess - ); - assert_mining_message!( - &pool_translator_sniffer.next_message_from_downstream(), - OpenExtendedMiningChannel - ); - assert_mining_message!( - &pool_translator_sniffer.next_message_from_upstream(), - OpenExtendedMiningChannelSuccess - ); - assert_mining_message!( - &pool_translator_sniffer.next_message_from_upstream(), - NewExtendedMiningJob - ); + pool_translator_sniffer + .wait_for_message_type( + MessageDirection::ToDownstream, + MESSAGE_TYPE_SETUP_CONNECTION_SUCCESS, + ) + .await; + pool_translator_sniffer + .wait_for_message_type( + MessageDirection::ToUpstream, + MESSAGE_TYPE_OPEN_EXTENDED_MINING_CHANNEL, + ) + .await; + pool_translator_sniffer + .wait_for_message_type( + MessageDirection::ToDownstream, + MESSAGE_TYPE_OPEN_EXTENDED_MINING_CHANNEL_SUCCESS, + ) + .await; + pool_translator_sniffer + .wait_for_message_type( + MessageDirection::ToDownstream, + MESSAGE_TYPE_NEW_EXTENDED_MINING_JOB, + ) + .await; pool_translator_sniffer .wait_for_message_type( MessageDirection::ToUpstream, diff --git a/test/scale/Cargo.toml b/test/scale/Cargo.toml deleted file mode 100644 index efc33dbda1..0000000000 --- a/test/scale/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "scale" -version = "1.0.0" -edition = "2021" - -[profile.release] -lto = true - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -clap = "2.33.3" -async-channel = "1.5.1" -async-std="1.8.0" -bytes = "1.0.1" -binary_sv2 = { path = "../../protocols/v2/binary-sv2" } -codec_sv2 = { path = "../../protocols/v2/codec-sv2", features=["noise_sv2"] } -network_helpers_sv2 = { version = "0.1", path = "../../roles/roles-utils/network-helpers" } -roles_logic_sv2 = { path = "../../protocols/v2/roles-logic-sv2" } -tokio = { version = "1.44.1", features = ["full"] } -key-utils = { version = "^1.0.0", path = "../../utils/key-utils" } - - diff --git a/test/scale/README.md b/test/scale/README.md deleted file mode 100644 index 36404b4947..0000000000 --- a/test/scale/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# Scale Test - -This test simply outputs the time spent sending 1,000,000 SubmitSharesStandard -through the system. When you start the test you specify -h -e (for encryption). -The test spawns "proxies" (ports 19000->19000+) which simply decrypt/encrypt each -SubmitSharesStandard message coming in (if encryption is on). Then it sends -1,000,000 share messages to the first proxy and then times the whole system to see -how long it takes for the last proxy to receive all 1M messages. It uses the same -network_helpers that the pool, and proxies use so it should be a good approximation -of the work they do. - -The test is run with the following command: -NOTE: running without `--release` dramatically slows down the test. - -```cargo run --release -- -h 4 -e``` -This runs the test with 4 hops and encryption on. - -```cargo run --release -- -h 4``` -This runs the test with 4 hops and encryption off. - - - diff --git a/test/scale/src/main.rs b/test/scale/src/main.rs deleted file mode 100644 index 0e9872c6b7..0000000000 --- a/test/scale/src/main.rs +++ /dev/null @@ -1,237 +0,0 @@ -use std::thread; -use tokio::{ - net::{TcpListener, TcpStream}, - task, -}; - -use async_channel::{bounded, Receiver, Sender}; - -use clap::{App, Arg}; -use codec_sv2::{HandshakeRole, Initiator, Responder, StandardEitherFrame, StandardSv2Frame}; -use std::time::Duration; - -use network_helpers::{ - noise_connection_tokio::Connection, plain_connection_tokio::PlainConnection, -}; - -use key_utils::{Secp256k1PublicKey, Secp256k1SecretKey}; -use roles_logic_sv2::{ - mining_sv2::*, - parsers::{Mining, MiningDeviceMessages}, -}; - -pub type EitherFrame = StandardEitherFrame; -pub const AUTHORITY_PUBLIC_K: &str = "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72"; - -pub const AUTHORITY_PRIVATE_K: &str = "mkDLTBBRxdBv998612qipDYoTK3YUrqLe8uWw7gu3iXbSrn2n"; - -static HOST: &str = "127.0.0.1"; - -#[tokio::main] -async fn main() { - let matches = App::new("ScaleTest") - .arg(Arg::with_name("encrypt").short("e").help("Use encryption")) - .arg( - Arg::with_name("hops") - .short("h") - .takes_value(true) - .help("Number of hops"), - ) - .get_matches(); - - let total_messages = 1_000_000; - let encrypt = matches.is_present("encrypt"); - let hops: u16 = matches.value_of("hops").unwrap_or("0").parse().unwrap_or(0); - let mut orig_port: u16 = 19000; - - // create channel to tell final server number of messages - let (tx, rx) = bounded(1); - - if hops > 0 { - orig_port = spawn_proxies(encrypt, hops, tx, total_messages).await; - } else { - println!("Usage: ./program -h -e"); - } - println!("Connecting to localhost:{}", orig_port); - setup_driver(orig_port, encrypt, rx, total_messages, hops).await; -} - -async fn setup_driver( - server_port: u16, - encrypt: bool, - rx: Receiver, - total_messages: i32, - hops: u16, -) { - let server_stream = TcpStream::connect(format!("{}:{}", HOST, server_port)) - .await - .unwrap(); - let (_server_receiver, server_sender): (Receiver, Sender); - - if encrypt { - let k: Secp256k1PublicKey = AUTHORITY_PUBLIC_K.to_string().try_into().unwrap(); - let initiator = Initiator::from_raw_k(k.into_bytes()).unwrap(); - - (_, server_sender, _, _) = - Connection::new(server_stream, HandshakeRole::Initiator(initiator)) - .await - .unwrap(); - } else { - (_server_receiver, server_sender) = PlainConnection::new(server_stream).await; - } - // Create timer to see how long this method takes - let start = std::time::Instant::now(); - - send_messages(server_sender, total_messages).await; - - //listen for message on rx - let msg = rx.recv().await.unwrap(); - - let end = std::time::Instant::now(); - - println!( - "client: {} - Took {}s hops: {} encryption: {}", - msg, - (end - start).as_secs(), - hops, - encrypt - ); -} - -pub type Message = MiningDeviceMessages<'static>; -pub type StdFrame = StandardSv2Frame; - -async fn send_messages(stream: Sender, total_messages: i32) { - let mut number: i32 = 0; - println!("Creating share"); - let share = MiningDeviceMessages::Mining(Mining::SubmitSharesStandard(SubmitSharesStandard { - channel_id: 1, - sequence_number: number as u32, - job_id: 2, - nonce: 3, - ntime: 4, - version: 5, - })); - - while number <= total_messages { - //println!("client: sending msg-{}", number); - let frame: StdFrame = share.clone().try_into().unwrap(); - let binary: EitherFrame = frame.into(); - - stream.send(binary).await.unwrap(); - number += 1; - } -} - -async fn handle_messages( - _name: String, - client: Receiver, - server: Option>, - total_messages: i32, - tx: Sender, -) { - let mut messages_received = 0; - - while messages_received <= total_messages { - let frame: StdFrame = client.recv().await.unwrap().try_into().unwrap(); - - let binary: EitherFrame = frame.into(); - - if server.is_some() { - server.as_ref().unwrap().send(binary).await.unwrap(); - } else { - messages_received += 1; - //println!("last server: {} got msg {}", name, messages_received); - } - } - tx.send("got all messages".to_string()).await.unwrap(); -} - -async fn create_proxy( - name: String, - listen_port: u16, - server_port: u16, - encrypt: bool, - total_messages: i32, - tx: Sender, -) { - println!( - "Creating proxy listener {}: {} connecting to: {}", - name, listen_port, server_port - ); - let listener = TcpListener::bind(format!("0.0.0.0:{}", listen_port)) - .await - .unwrap(); - println!("Bound - now waiting for connection..."); - let cli_stream = listener.accept().await.unwrap().0; - let (cli_receiver, _cli_sender): (Receiver, Sender); - - if encrypt { - let k_pub: Secp256k1PublicKey = AUTHORITY_PUBLIC_K.to_string().try_into().unwrap(); - let k_priv: Secp256k1SecretKey = AUTHORITY_PRIVATE_K.to_string().try_into().unwrap(); - let responder = Responder::from_authority_kp( - &k_pub.into_bytes(), - &k_priv.into_bytes(), - Duration::from_secs(3600), - ) - .unwrap(); - (cli_receiver, _, _, _) = Connection::new(cli_stream, HandshakeRole::Responder(responder)) - .await - .unwrap(); - } else { - (cli_receiver, _cli_sender) = PlainConnection::new(cli_stream).await; - } - - let mut server = None; - if server_port > 0 { - println!("Proxy {} Connecting to server: {}", name, server_port); - let server_stream = TcpStream::connect(format!("{}:{}", HOST, server_port)) - .await - .unwrap(); - let (_server_receiver, server_sender): (Receiver, Sender); - let k_pub: Secp256k1PublicKey = AUTHORITY_PUBLIC_K.to_string().try_into().unwrap(); - - if encrypt { - let initiator = Initiator::from_raw_k(k_pub.into_bytes()).unwrap(); - (_, server_sender, _, _) = - Connection::new(server_stream, HandshakeRole::Initiator(initiator)) - .await - .unwrap(); - } else { - (_server_receiver, server_sender) = PlainConnection::new(server_stream).await; - } - server = Some(server_sender); - } - - println!("Proxy {} has a client", name); - handle_messages(name, cli_receiver, server, total_messages, tx).await; -} - -async fn spawn_proxies(encrypt: bool, hops: u16, tx: Sender, total_messages: i32) -> u16 { - let orig_port: u16 = 19000; - let final_server_port = orig_port + (hops - 1); - let mut listen_port = final_server_port; - let mut server_port: u16 = 0; - - for name in (0..hops).rev() { - let tx_clone = tx.clone(); - let name_clone = name.to_string(); - - task::spawn(async move { - create_proxy( - name_clone, - listen_port, - server_port, - encrypt, - total_messages, - tx_clone, - ) - .await; - }); - - thread::sleep(std::time::Duration::from_secs(1)); - server_port = listen_port; - listen_port -= 1; - } - orig_port -} diff --git a/utils/Cargo.lock b/utils/Cargo.lock index 10f8528362..3e5618c269 100644 --- a/utils/Cargo.lock +++ b/utils/Cargo.lock @@ -81,8 +81,8 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" dependencies = [ - "bitcoin-internals", - "bitcoin_hashes", + "bitcoin-internals 0.3.0", + "bitcoin_hashes 0.14.0", ] [[package]] @@ -91,6 +91,21 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" +[[package]] +name = "binary_codec_sv2" +version = "3.0.0" +dependencies = [ + "buffer_sv2", +] + +[[package]] +name = "binary_sv2" +version = "4.0.0" +dependencies = [ + "binary_codec_sv2", + "derive_codec_sv2", +] + [[package]] name = "bip32_derivation" version = "1.0.0" @@ -107,15 +122,21 @@ checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026" dependencies = [ "base58ck", "bech32", - "bitcoin-internals", + "bitcoin-internals 0.3.0", "bitcoin-io", "bitcoin-units", - "bitcoin_hashes", - "hex-conservative", + "bitcoin_hashes 0.14.0", + "hex-conservative 0.2.1", "hex_lit", "secp256k1 0.29.1", ] +[[package]] +name = "bitcoin-internals" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" + [[package]] name = "bitcoin-internals" version = "0.3.0" @@ -134,7 +155,17 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" dependencies = [ - "bitcoin-internals", + "bitcoin-internals 0.3.0", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" +dependencies = [ + "bitcoin-internals 0.2.0", + "hex-conservative 0.1.2", ] [[package]] @@ -144,7 +175,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" dependencies = [ "bitcoin-io", - "hex-conservative", + "hex-conservative 0.2.1", ] [[package]] @@ -153,6 +184,18 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -177,6 +220,7 @@ version = "2.0.0" dependencies = [ "aes-gcm", "criterion", + "generic-array", "iai", "rand", ] @@ -187,6 +231,12 @@ version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + [[package]] name = "byteorder" version = "1.5.0" @@ -214,6 +264,44 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "channels_sv2" +version = "2.0.0" +dependencies = [ + "binary_sv2", + "bitcoin", + "common_messages_sv2", + "job_declaration_sv2", + "mining_sv2", + "primitive-types", + "template_distribution_sv2", + "tracing", +] + [[package]] name = "cipher" version = "0.4.4" @@ -222,6 +310,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", + "zeroize", ] [[package]] @@ -235,6 +324,45 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "codec_sv2" +version = "3.0.1" +dependencies = [ + "binary_sv2", + "buffer_sv2", + "framing_sv2", + "noise_sv2", + "rand", + "tracing", +] + +[[package]] +name = "common_messages_sv2" +version = "6.0.1" +dependencies = [ + "binary_sv2", +] + +[[package]] +name = "const_format" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -305,6 +433,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + [[package]] name = "crypto-common" version = "0.1.6" @@ -312,6 +446,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] @@ -345,6 +480,13 @@ dependencies = [ "cipher", ] +[[package]] +name = "derive_codec_sv2" +version = "1.1.1" +dependencies = [ + "binary_codec_sv2", +] + [[package]] name = "digest" version = "0.9.0" @@ -356,14 +498,47 @@ dependencies = [ [[package]] name = "either" -version = "1.14.0" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "error_handling" version = "1.0.0" +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "framing_sv2" +version = "5.0.1" +dependencies = [ + "binary_sv2", + "buffer_sv2", + "noise_sv2", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "generic-array" version = "0.14.7" @@ -401,6 +576,19 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" +[[package]] +name = "handlers_sv2" +version = "0.2.0" +dependencies = [ + "binary_sv2", + "common_messages_sv2", + "job_declaration_sv2", + "mining_sv2", + "parsers_sv2", + "template_distribution_sv2", + "trait-variant", +] + [[package]] name = "hashbrown" version = "0.7.2" @@ -411,6 +599,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -420,6 +614,18 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212ab92002354b4819390025006c897e8140934349e8635c9b077f47b4dcbd20" + [[package]] name = "hex-conservative" version = "0.2.1" @@ -429,6 +635,15 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "hex-conservative" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afe881d0527571892c4034822e59bb10c6c991cce6abe8199b6f5cf10766f55" +dependencies = [ + "arrayvec", +] + [[package]] name = "hex_lit" version = "0.1.1" @@ -441,6 +656,36 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71a816c97c42258aa5834d07590b718b4c9a598944cd39a52dc25b351185d678" +[[package]] +name = "impl-codec" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d40b9d5e17727407e55028eafc22b2dc68781786e6d7eb8a21103f5058e3a14" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown 0.15.5", +] + [[package]] name = "inout" version = "0.1.4" @@ -461,9 +706,16 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "job_declaration_sv2" +version = "5.0.1" +dependencies = [ + "binary_sv2", +] [[package]] name = "js-sys" @@ -480,6 +732,7 @@ name = "key-utils" version = "1.2.0" dependencies = [ "bs58", + "generic-array", "rand", "rustversion", "secp256k1 0.28.2", @@ -501,9 +754,9 @@ checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" [[package]] name = "log" -version = "0.4.26" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "memchr" @@ -511,6 +764,31 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "mining_sv2" +version = "5.0.1" +dependencies = [ + "binary_sv2", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "noise_sv2" +version = "1.4.0" +dependencies = [ + "aes-gcm", + "chacha20poly1305", + "generic-array", + "rand", + "rand_chacha", + "secp256k1 0.28.2", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -528,9 +806,9 @@ checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" [[package]] name = "oorandom" -version = "11.1.4" +version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "opaque-debug" @@ -538,6 +816,52 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "parity-scale-codec" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "const_format", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "rustversion", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "parsers_sv2" +version = "0.1.1" +dependencies = [ + "binary_sv2", + "common_messages_sv2", + "framing_sv2", + "job_declaration_sv2", + "mining_sv2", + "template_distribution_sv2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + [[package]] name = "plotters" version = "0.3.7" @@ -566,6 +890,17 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "polyval" version = "0.6.2" @@ -587,24 +922,50 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "primitive-types" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15600a7d856470b7d278b3fe0e311fe28c2526348549f8ef2ff7db3299c87f5" +dependencies = [ + "fixed-hash", + "impl-codec", + "uint", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.38" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -684,6 +1045,32 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "roles_logic_sv2" +version = "5.0.0" +dependencies = [ + "bitcoin", + "chacha20poly1305", + "channels_sv2", + "codec_sv2", + "common_messages_sv2", + "handlers_sv2", + "hex-conservative 0.3.0", + "job_declaration_sv2", + "mining_sv2", + "nohash-hasher", + "parsers_sv2", + "primitive-types", + "template_distribution_sv2", + "tracing", +] + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + [[package]] name = "rustversion" version = "1.0.19" @@ -692,9 +1079,9 @@ checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "ryu" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" @@ -711,6 +1098,7 @@ version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" dependencies = [ + "bitcoin_hashes 0.13.0", "rand", "secp256k1-sys 0.9.2", ] @@ -721,7 +1109,7 @@ version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ - "bitcoin_hashes", + "bitcoin_hashes 0.14.0", "secp256k1-sys 0.10.1", ] @@ -745,9 +1133,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.218" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] @@ -764,9 +1152,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.218" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -775,9 +1163,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.139" +version = "1.0.142" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" dependencies = [ "itoa", "memchr", @@ -804,12 +1192,17 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "stratum-common" -version = "2.0.0" +version = "4.0.1" dependencies = [ - "bitcoin", - "secp256k1 0.28.2", + "roles_logic_sv2", ] [[package]] @@ -829,6 +1222,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "template_distribution_sv2" +version = "4.0.1" +dependencies = [ + "binary_sv2", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -853,16 +1259,87 @@ name = "toml" version = "0.5.6" source = "git+https://github.com/diondokter/toml-rs?rev=c4161aa#c4161aa70202b3992dbec79b76e7a8659713b604" dependencies = [ - "hashbrown", + "hashbrown 0.7.2", "serde", ] +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "trait-variant" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "typenum" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "uint" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + [[package]] name = "unicode-ident" version = "1.0.17" @@ -875,6 +1352,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "universal-hash" version = "0.5.1" @@ -1079,6 +1562,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +dependencies = [ + "memchr", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -1099,3 +1600,9 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/utils/bip32-key-derivation/Cargo.toml b/utils/bip32-key-derivation/Cargo.toml index 47116d07e9..b4dbc6c093 100644 --- a/utils/bip32-key-derivation/Cargo.toml +++ b/utils/bip32-key-derivation/Cargo.toml @@ -20,7 +20,8 @@ name = "bip32_derivation-bin" path = "src/main.rs" [dependencies] -stratum-common = { path = "../../common", features=["bitcoin"], version = "^2.0.0" } +stratum-common = { path = "../../common", version = "4.0.0" } + [dev-dependencies] toml = { version = "0.5.6", git = "https://github.com/diondokter/toml-rs", default-features = false, rev = "c4161aa" } diff --git a/utils/bip32-key-derivation/src/lib.rs b/utils/bip32-key-derivation/src/lib.rs index a96e1d6045..bdcb37a515 100644 --- a/utils/bip32-key-derivation/src/lib.rs +++ b/utils/bip32-key-derivation/src/lib.rs @@ -1,5 +1,5 @@ use std::str::FromStr; -use stratum_common::bitcoin::{ +use stratum_common::roles_logic_sv2::bitcoin::{ bip32::{DerivationPath, Error, Xpub}, secp256k1::Secp256k1, }; diff --git a/utils/bip32-key-derivation/src/main.rs b/utils/bip32-key-derivation/src/main.rs index 52c97f0a65..9bcbae4107 100644 --- a/utils/bip32-key-derivation/src/main.rs +++ b/utils/bip32-key-derivation/src/main.rs @@ -1,6 +1,6 @@ use bip32_derivation::derive_child_public_key; use std::{env, str::FromStr}; -use stratum_common::bitcoin::bip32::Xpub; +use stratum_common::roles_logic_sv2::bitcoin::bip32::Xpub; fn main() { let args: Vec = env::args().collect(); diff --git a/utils/buffer/Cargo.toml b/utils/buffer/Cargo.toml index ed72d41583..1e9f69c96c 100644 --- a/utils/buffer/Cargo.toml +++ b/utils/buffer/Cargo.toml @@ -2,7 +2,7 @@ name = "buffer_sv2" version = "2.0.0" authors = ["The Stratum V2 Developers"] -edition = "2018" +edition = "2021" description = "buffer" documentation = "https://docs.rs/buffer_sv2" readme = "README.md" @@ -14,6 +14,7 @@ keywords = ["stratum", "mining", "bitcoin", "protocol"] [dependencies] criterion = {version = "0.3", optional = true} aes-gcm = { version = "0.10.2", features = ["alloc", "aes"], default-features = false } +generic-array = "=0.14.7" [dev-dependencies] rand = "0.8.3" diff --git a/utils/buffer/examples/variable_sized_messages.rs b/utils/buffer/examples/variable_sized_messages.rs index 922ec7fd39..df5875677e 100644 --- a/utils/buffer/examples/variable_sized_messages.rs +++ b/utils/buffer/examples/variable_sized_messages.rs @@ -23,7 +23,7 @@ fn main() { let data_slice = pool.get_data_owned(); slices.push_back(data_slice); println!("{:?}", &pool); - println!(""); + println!(); }; // Write a small message to the first slot diff --git a/utils/buffer/fuzz/Cargo.toml b/utils/buffer/fuzz/Cargo.toml index 0197502a1c..c3edbefaf9 100644 --- a/utils/buffer/fuzz/Cargo.toml +++ b/utils/buffer/fuzz/Cargo.toml @@ -3,7 +3,7 @@ name = "buffer-fuzz" version = "0.0.0" authors = ["Automatically generated"] publish = false -edition = "2018" +edition = "2021" [package.metadata] cargo-fuzz = true diff --git a/utils/key-utils/Cargo.toml b/utils/key-utils/Cargo.toml index bf9d08acb0..d468e5a87b 100644 --- a/utils/key-utils/Cargo.toml +++ b/utils/key-utils/Cargo.toml @@ -28,6 +28,7 @@ secp256k1 = { version = "0.28.2", default-features = false, features =["alloc"," serde = { version = "1.0.89", features = ["derive","alloc"], default-features = false } rand = {version = "0.8.5", default-features = false } rustversion = "1.0" +generic-array = "=0.14.7" [dev-dependencies] toml = { version = "0.5.6", git = "https://github.com/diondokter/toml-rs", default-features = false, rev = "c4161aa" } diff --git a/utils/key-utils/src/main.rs b/utils/key-utils/src/main.rs index c15fa21836..35a12ccce9 100644 --- a/utils/key-utils/src/main.rs +++ b/utils/key-utils/src/main.rs @@ -23,8 +23,8 @@ fn main() { let (secret, public) = generate_key(); let secret: String = secret.into(); let public: String = public.into(); - println!("Secret Key: {}", secret); - println!("Public Key: {}", public); + println!("Secret Key: {secret}"); + println!("Public Key: {public}"); } #[cfg(not(feature = "std"))]