diff --git a/README.md b/README.md
index 69aac419b..06615d0b5 100644
--- a/README.md
+++ b/README.md
@@ -32,6 +32,8 @@ If you're looking for the low-level protocol libraries, check out the [`stratum`
- `stratum-apps/` - Shared application utilities
- Configuration helpers (TOML, coinbase outputs, logging)
- Network connection utilities (Noise protocol, plain TCP, SV1 connections)
+ - Payout identity parsing and coinbase output construction/verification helpers
+ - Runtime fallback coordination helpers
- RPC client implementation
- Key management utilities
- Custom synchronization primitives
@@ -102,4 +104,4 @@ Minimum Supported Rust Version: 1.85.0
> Website [stratumprotocol.org](https://www.stratumprotocol.org) ·
> Discord [SV2 Discord](https://discord.gg/fsEW23wFYs) ·
-> Twitter [@Stratumv2](https://twitter.com/StratumV2) ·
\ No newline at end of file
+> Twitter [@Stratumv2](https://twitter.com/StratumV2) ·
diff --git a/docker/README.md b/docker/README.md
index 722e88778..c4e1ab0e6 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -131,6 +131,8 @@ If something behaves weirdly, 99% of the time your `docker_env` is the culprit.
* Port **34255**
* Upstream target (JDC or pool) is fully controlled via `docker_env` variables
+* `TPROXY_VERIFY_PAYOUT=false` is the standard pool-mining default; set it to `true` only for
+ solo/donation identities where `TPROXY_USER_IDENTITY` encodes the expected on-chain payout
---
diff --git a/docker/config/translator-proxy-config.toml.template b/docker/config/translator-proxy-config.toml.template
index c6c50c188..443ed2c8d 100644
--- a/docker/config/translator-proxy-config.toml.template
+++ b/docker/config/translator-proxy-config.toml.template
@@ -16,6 +16,12 @@ downstream_extranonce2_size = 4
# This will be appended with a counter for each mining client (e.g., username.miner1, username.miner2)
user_identity = "${TPROXY_USER_IDENTITY}"
+# If true, tProxy verifies upstream coinbase outputs against the payout encoded by user_identity.
+# Keep false for standard pool mining, including pools that use a Bitcoin address as the username.
+# Enable for solo/donation identities: sri/solo/
/,
+# sri/donate///, or legacy [.worker].
+verify_payout = ${TPROXY_VERIFY_PAYOUT}
+
# Aggregate channels: if true, all miners share one upstream channel; if false, each miner gets its own channel
aggregate_channels = ${TPROXY_AGGREGATE_CHANNELS}
diff --git a/docker/docker_env.example b/docker/docker_env.example
index 7106e08cf..1064d693e 100644
--- a/docker/docker_env.example
+++ b/docker/docker_env.example
@@ -24,6 +24,7 @@ JDC_UPSTREAM_JDS_PORT=3334
#Tproxy Settings
TPROXY_USER_IDENTITY=your_username_here
+TPROXY_VERIFY_PAYOUT=false
TPROXY_AGGREGATE_CHANNELS=false
TPROXY_MIN_INDIVIDUAL_MINER_HASHRATE=10_000_000_000_000.0
TPROXY_SHARES_PER_MINUTE=6.0
@@ -31,4 +32,3 @@ TPROXY_ENABLE_VARDIFF=true
TPROXY_UPSTREAM_ADDRESS=jd_client_sv2 # Use jd_client_sv2 for pool_and_miner_apps, pool_sv2 for pool_and_miner_apps_no_jd
TPROXY_UPSTREAM_PORT=34265 # Use 34265 for pool_and_miner_apps, 3333 for pool_and_miner_apps_no_jd
TPROXY_UPSTREAM_AUTHORITY_PUBKEY=9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72
-
diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml
index 380d08883..fc867c448 100644
--- a/integration-tests/Cargo.toml
+++ b/integration-tests/Cargo.toml
@@ -14,7 +14,7 @@ keywords = ["stratum", "mining", "bitcoin", "protocol"]
exclude = ["resources/high_diff_chain.tar.gz"]
[dependencies]
-stratum-apps = { version = "0.5.0", path = "../stratum-apps", features = ["network", "config"] }
+stratum-apps = { version = "0.5.0", path = "../stratum-apps", features = ["network", "config", "payout"] }
jd_client_sv2 = { version = "0.3.0", path = "../miner-apps/jd-client" }
pool_sv2 = { version = "0.4.0", path = "../pool-apps/pool" }
translator_sv2 = { version = "0.3.0", path = "../miner-apps/translator" }
diff --git a/integration-tests/lib/mod.rs b/integration-tests/lib/mod.rs
index 360250be9..f272a3629 100644
--- a/integration-tests/lib/mod.rs
+++ b/integration-tests/lib/mod.rs
@@ -348,6 +348,30 @@ pub async fn start_sv2_translator(
required_extensions: Vec,
job_keepalive_interval_secs: Option,
enable_monitoring: bool,
+) -> (TranslatorSv2, SocketAddr, Option) {
+ start_sv2_translator_with_user_identity(
+ upstreams,
+ aggregate_channels,
+ supported_extensions,
+ required_extensions,
+ job_keepalive_interval_secs,
+ "user_identity".to_string(),
+ false,
+ enable_monitoring,
+ )
+ .await
+}
+
+#[allow(clippy::too_many_arguments)]
+pub async fn start_sv2_translator_with_user_identity(
+ upstreams: &[SocketAddr],
+ aggregate_channels: bool,
+ supported_extensions: Vec,
+ required_extensions: Vec,
+ job_keepalive_interval_secs: Option,
+ user_identity: String,
+ verify_payout: bool,
+ enable_monitoring: bool,
) -> (TranslatorSv2, SocketAddr, Option) {
let job_keepalive_interval_secs = job_keepalive_interval_secs.unwrap_or(60);
let upstreams = upstreams
@@ -399,7 +423,8 @@ pub async fn start_sv2_translator(
2,
2,
downstream_extranonce2_size,
- "user_identity".to_string(),
+ user_identity,
+ verify_payout,
aggregate_channels,
supported_extensions,
required_extensions,
diff --git a/integration-tests/tests/translator_integration.rs b/integration-tests/tests/translator_integration.rs
index e6112ce31..d03eeea37 100644
--- a/integration-tests/tests/translator_integration.rs
+++ b/integration-tests/tests/translator_integration.rs
@@ -7,7 +7,13 @@ use integration_tests_sv2::{
utils::get_available_address,
*,
};
-use stratum_apps::stratum_core::mining_sv2::*;
+use stratum_apps::{
+ config_helpers::CoinbaseRewardScript,
+ stratum_core::{
+ bitcoin::{consensus::serialize, Amount, TxOut},
+ mining_sv2::*,
+ },
+};
use tokio::net::{TcpListener, TcpStream};
use std::{
@@ -30,6 +36,8 @@ use stratum_apps::stratum_core::{
template_distribution_sv2::MESSAGE_TYPE_SUBMIT_SOLUTION,
};
+const PAYOUT_VERIFICATION_MINER_ADDRESS: &str = "tb1qpusf5256yxv50qt0pm0tue8k952fsu5lzsphft";
+
// 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
// the pool exchange the correct messages upon connection. And that the miner is able to submit
@@ -87,6 +95,302 @@ async fn translate_sv1_to_sv2_successfully() {
shutdown_all!(translator, pool);
}
+/// Checks that tProxy mines when payout verification passes for address and donation identities.
+#[tokio::test]
+async fn translator_mines_when_payout_matches_address_or_donation_identity() {
+ start_tracing();
+
+ let miner_script_pubkey = CoinbaseRewardScript::from_descriptor(&format!(
+ "addr({PAYOUT_VERIFICATION_MINER_ADDRESS})"
+ ))
+ .unwrap()
+ .script_pubkey();
+ let pool_script_pubkey =
+ CoinbaseRewardScript::from_descriptor(&format!("addr({POOL_COINBASE_REWARD_ADDRESS})"))
+ .unwrap()
+ .script_pubkey();
+
+ let mut solo_coinbase_tx_suffix = hex::decode("feffffff").unwrap();
+ solo_coinbase_tx_suffix.extend(serialize(&vec![TxOut {
+ value: Amount::from_sat(5_000_000_000),
+ script_pubkey: miner_script_pubkey.clone(),
+ }]));
+ solo_coinbase_tx_suffix.extend([0, 0, 0, 0]);
+
+ let mut partial_donation_coinbase_tx_suffix = hex::decode("feffffff").unwrap();
+ partial_donation_coinbase_tx_suffix.extend(serialize(&vec![
+ TxOut {
+ value: Amount::from_sat(500_000_000),
+ script_pubkey: pool_script_pubkey,
+ },
+ TxOut {
+ value: Amount::from_sat(4_500_000_000),
+ script_pubkey: miner_script_pubkey,
+ },
+ ]));
+ partial_donation_coinbase_tx_suffix.extend([0, 0, 0, 0]);
+
+ for (identifier, user_identity, coinbase_tx_suffix) in [
+ (
+ "payout-address",
+ PAYOUT_VERIFICATION_MINER_ADDRESS.to_string(),
+ solo_coinbase_tx_suffix,
+ ),
+ (
+ "payout-donation",
+ format!("sri/donate/10/{PAYOUT_VERIFICATION_MINER_ADDRESS}/worker"),
+ partial_donation_coinbase_tx_suffix,
+ ),
+ ] {
+ let mock_upstream_addr = get_available_address();
+ let send_to_tproxy = MockUpstream::new(
+ mock_upstream_addr,
+ WithSetup::yes_with_defaults(Protocol::MiningProtocol, 0),
+ )
+ .start()
+ .await;
+ let (sniffer, sniffer_addr) =
+ start_sniffer(identifier, mock_upstream_addr, false, vec![], None);
+
+ let (translator, tproxy_addr, _) = start_sv2_translator_with_user_identity(
+ &[sniffer_addr],
+ false,
+ vec![],
+ vec![],
+ None,
+ user_identity,
+ true,
+ false,
+ )
+ .await;
+ let (_minerd_process, _minerd_addr) = start_minerd(tproxy_addr, None, None, false).await;
+
+ sniffer
+ .wait_for_message_type(
+ MessageDirection::ToUpstream,
+ MESSAGE_TYPE_OPEN_EXTENDED_MINING_CHANNEL,
+ )
+ .await;
+ let open_extended_mining_channel: OpenExtendedMiningChannel = loop {
+ match sniffer.next_message_from_downstream() {
+ Some((
+ _,
+ AnyMessage::Mining(parsers_sv2::Mining::OpenExtendedMiningChannel(msg)),
+ )) => break msg,
+ _ => continue,
+ };
+ };
+
+ send_to_tproxy
+ .send(AnyMessage::Mining(
+ parsers_sv2::Mining::OpenExtendedMiningChannelSuccess(
+ OpenExtendedMiningChannelSuccess {
+ request_id: open_extended_mining_channel.request_id,
+ channel_id: 0,
+ target: hex::decode(
+ "0000137c578190689425e3ecf8449a1af39db0aed305d9206f45ac32fe8330fc",
+ )
+ .unwrap()
+ .try_into()
+ .unwrap(),
+ extranonce_size: 4,
+ extranonce_prefix: vec![0x00, 0x01, 0x00, 0x00].try_into().unwrap(),
+ group_channel_id: 100,
+ },
+ ),
+ ))
+ .await
+ .unwrap();
+
+ send_to_tproxy
+ .send(AnyMessage::Mining(parsers_sv2::Mining::NewExtendedMiningJob(NewExtendedMiningJob {
+ channel_id: 0,
+ job_id: 1,
+ min_ntime: Sv2Option::new(None),
+ version: 0x20000000,
+ version_rolling_allowed: true,
+ merkle_path: Seq0255::new(vec![]).unwrap(),
+ coinbase_tx_prefix: hex::decode("02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff225200162f5374726174756d2056322053524920506f6f6c2f2f08").unwrap().try_into().unwrap(),
+ coinbase_tx_suffix: coinbase_tx_suffix.try_into().unwrap(),
+ })))
+ .await
+ .unwrap();
+ sniffer
+ .wait_for_message_type(
+ MessageDirection::ToDownstream,
+ MESSAGE_TYPE_NEW_EXTENDED_MINING_JOB,
+ )
+ .await;
+
+ send_to_tproxy
+ .send(AnyMessage::Mining(parsers_sv2::Mining::SetNewPrevHash(
+ SetNewPrevHash {
+ channel_id: 0,
+ job_id: 1,
+ prev_hash: hex::decode(
+ "3ab7089cd2cd30f133552cfde82c4cb239cd3c2310306f9d825e088a1772cc39",
+ )
+ .unwrap()
+ .try_into()
+ .unwrap(),
+ min_ntime: 1766782170,
+ nbits: 0x207fffff,
+ },
+ )))
+ .await
+ .unwrap();
+ sniffer
+ .wait_for_message_type(
+ MessageDirection::ToDownstream,
+ MESSAGE_TYPE_MINING_SET_NEW_PREV_HASH,
+ )
+ .await;
+
+ sniffer
+ .wait_for_message_type(
+ MessageDirection::ToUpstream,
+ MESSAGE_TYPE_SUBMIT_SHARES_EXTENDED,
+ )
+ .await;
+
+ translator.shutdown().await;
+ }
+}
+
+/// Checks that tProxy falls back when the upstream job pays the wrong address.
+#[tokio::test]
+async fn translator_falls_back_when_payout_does_not_match_user_identity() {
+ start_tracing();
+
+ let mut wrong_coinbase_tx_suffix = hex::decode("feffffff").unwrap();
+ wrong_coinbase_tx_suffix.extend(serialize(&vec![TxOut {
+ value: Amount::from_sat(5_000_000_000),
+ script_pubkey: CoinbaseRewardScript::from_descriptor(&format!(
+ "addr({POOL_COINBASE_REWARD_ADDRESS})"
+ ))
+ .unwrap()
+ .script_pubkey(),
+ }]));
+ wrong_coinbase_tx_suffix.extend([0, 0, 0, 0]);
+
+ let primary_upstream_addr = get_available_address();
+ let primary_sender = MockUpstream::new(
+ primary_upstream_addr,
+ WithSetup::yes_with_defaults(Protocol::MiningProtocol, 0),
+ )
+ .start()
+ .await;
+ let (primary_sniffer, primary_sniffer_addr) = start_sniffer(
+ "payout-bad-primary",
+ primary_upstream_addr,
+ false,
+ vec![],
+ None,
+ );
+
+ let fallback_upstream_addr = get_available_address();
+ let _fallback_sender = MockUpstream::new(
+ fallback_upstream_addr,
+ WithSetup::yes_with_defaults(Protocol::MiningProtocol, 0),
+ )
+ .start()
+ .await;
+ let (fallback_sniffer, fallback_sniffer_addr) = start_sniffer(
+ "payout-fallback",
+ fallback_upstream_addr,
+ false,
+ vec![],
+ None,
+ );
+
+ let (translator, tproxy_addr, _) = start_sv2_translator_with_user_identity(
+ &[primary_sniffer_addr, fallback_sniffer_addr],
+ false,
+ vec![],
+ vec![],
+ None,
+ PAYOUT_VERIFICATION_MINER_ADDRESS.to_string(),
+ true,
+ false,
+ )
+ .await;
+ let (minerd_process, _minerd_addr) = start_minerd(tproxy_addr, None, None, false).await;
+
+ primary_sniffer
+ .wait_for_message_type(
+ MessageDirection::ToUpstream,
+ MESSAGE_TYPE_OPEN_EXTENDED_MINING_CHANNEL,
+ )
+ .await;
+ let open_extended_mining_channel: OpenExtendedMiningChannel = loop {
+ match primary_sniffer.next_message_from_downstream() {
+ Some((_, AnyMessage::Mining(parsers_sv2::Mining::OpenExtendedMiningChannel(msg)))) => {
+ break msg
+ }
+ _ => continue,
+ };
+ };
+
+ primary_sender
+ .send(AnyMessage::Mining(
+ parsers_sv2::Mining::OpenExtendedMiningChannelSuccess(
+ OpenExtendedMiningChannelSuccess {
+ request_id: open_extended_mining_channel.request_id,
+ channel_id: 0,
+ target: hex::decode(
+ "0000137c578190689425e3ecf8449a1af39db0aed305d9206f45ac32fe8330fc",
+ )
+ .unwrap()
+ .try_into()
+ .unwrap(),
+ extranonce_size: 4,
+ extranonce_prefix: vec![0x00, 0x01, 0x00, 0x00].try_into().unwrap(),
+ group_channel_id: 100,
+ },
+ ),
+ ))
+ .await
+ .unwrap();
+
+ primary_sender
+ .send(AnyMessage::Mining(parsers_sv2::Mining::NewExtendedMiningJob(NewExtendedMiningJob {
+ channel_id: 0,
+ job_id: 1,
+ min_ntime: Sv2Option::new(None),
+ version: 0x20000000,
+ version_rolling_allowed: true,
+ merkle_path: Seq0255::new(vec![]).unwrap(),
+ coinbase_tx_prefix: hex::decode("02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff225200162f5374726174756d2056322053524920506f6f6c2f2f08").unwrap().try_into().unwrap(),
+ coinbase_tx_suffix: wrong_coinbase_tx_suffix.try_into().unwrap(),
+ })))
+ .await
+ .unwrap();
+ primary_sniffer
+ .wait_for_message_type(
+ MessageDirection::ToDownstream,
+ MESSAGE_TYPE_NEW_EXTENDED_MINING_JOB,
+ )
+ .await;
+
+ assert!(
+ primary_sniffer
+ .assert_message_not_present(
+ MessageDirection::ToUpstream,
+ MESSAGE_TYPE_SUBMIT_SHARES_EXTENDED,
+ Duration::from_secs(2),
+ )
+ .await,
+ "tProxy should not submit shares to the upstream that failed payout verification"
+ );
+
+ fallback_sniffer
+ .wait_for_message_type(MessageDirection::ToUpstream, MESSAGE_TYPE_SETUP_CONNECTION)
+ .await;
+
+ drop(minerd_process);
+ translator.shutdown().await;
+}
+
// Demonstrates the scenario where TProxy falls back to the secondary pool
// after the primary pool returns a `SetupConnection.Error`.
#[tokio::test]
diff --git a/miner-apps/translator/README.md b/miner-apps/translator/README.md
index c1c7ce56a..71996f982 100644
--- a/miner-apps/translator/README.md
+++ b/miner-apps/translator/README.md
@@ -37,9 +37,13 @@ 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 (appended with counter for each miner unless it starts with `sri/`)
user_identity = "your_username_here"
+# Payout verification is opt-in. Keep false for standard pool mining,
+# including pools that use a Bitcoin address as the username.
+verify_payout = false
+
# Channel Configuration
aggregate_channels = true # true: shared channel, false: individual channels
@@ -78,6 +82,20 @@ Make sure the machine running the Translator Proxy has its clock synced with an
- `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)
+- `verify_payout`: When `true`, verify upstream coinbase payouts against a payout address encoded
+ by `user_identity`. Keep `false` for standard pool mining, including pools that use a Bitcoin
+ address as the username.
+
+#### **Solo/Donation Payout Verification**
+Payout verification is disabled by default. Set `verify_payout = true` for solo mining or
+donation configurations where `user_identity` intentionally encodes an on-chain payout address:
+
+- `sri/solo//`: tProxy verifies every upstream extended job pays 100% of spendable coinbase outputs to ``
+- `[.worker]`: legacy solo mode, verified as 100% miner payout when `verify_payout = true`
+- `sri/donate///`: tProxy verifies the miner address receives the remaining percentage
+- `sri/donate/`: full donation mode; keep `verify_payout = false` because no miner payout address is present
+
+If verification fails, tProxy triggers upstream fallback instead of forwarding the job to SV1 miners.
#### **Difficulty Configuration**
- `min_individual_miner_hashrate`: Expected hashrate of weakest miner (in H/s)
@@ -144,6 +162,7 @@ For connecting to a local SV2 pool server:
downstream_address = "0.0.0.0"
downstream_port = 34255
user_identity = "miner_farm_1"
+verify_payout = false
aggregate_channels = true
[downstream_difficulty_config]
@@ -164,6 +183,7 @@ For production environments with failover:
downstream_address = "0.0.0.0"
downstream_port = 34255
user_identity = "production_farm"
+verify_payout = false
aggregate_channels = true
[downstream_difficulty_config]
diff --git a/miner-apps/translator/config-examples/mainnet/tproxy-config-hosted-pool-example.toml b/miner-apps/translator/config-examples/mainnet/tproxy-config-hosted-pool-example.toml
index dfac8a5cc..92195992f 100644
--- a/miner-apps/translator/config-examples/mainnet/tproxy-config-hosted-pool-example.toml
+++ b/miner-apps/translator/config-examples/mainnet/tproxy-config-hosted-pool-example.toml
@@ -16,6 +16,12 @@ downstream_extranonce2_size = 4
# This will be appended with a counter for each mining client (e.g., username.miner1, username.miner2)
user_identity = "your_username_here"
+# If true, tProxy verifies upstream coinbase outputs against the payout encoded by user_identity.
+# Keep false for standard pool mining, including pools that use a Bitcoin address as the username.
+# Enable for solo/donation identities: sri/solo//,
+# sri/donate///, or legacy [.worker].
+verify_payout = false
+
# Aggregate channels: if true, all miners share one upstream channel; if false, each miner gets its own channel
aggregate_channels = true
diff --git a/miner-apps/translator/config-examples/mainnet/tproxy-config-local-jdc-example.toml b/miner-apps/translator/config-examples/mainnet/tproxy-config-local-jdc-example.toml
index ae9565ad6..0a0359cd7 100644
--- a/miner-apps/translator/config-examples/mainnet/tproxy-config-local-jdc-example.toml
+++ b/miner-apps/translator/config-examples/mainnet/tproxy-config-local-jdc-example.toml
@@ -16,6 +16,12 @@ downstream_extranonce2_size = 4
# This will be appended with a counter for each mining client (e.g., username.miner1, username.miner2)
user_identity = "your_username_here"
+# If true, tProxy verifies upstream coinbase outputs against the payout encoded by user_identity.
+# Keep false for standard pool mining, including pools that use a Bitcoin address as the username.
+# Enable for solo/donation identities: sri/solo//,
+# sri/donate///, or legacy [.worker].
+verify_payout = false
+
# Aggregate channels: if true, all miners share one upstream channel; if false, each miner gets its own channel
aggregate_channels = false
diff --git a/miner-apps/translator/config-examples/mainnet/tproxy-config-local-pool-example.toml b/miner-apps/translator/config-examples/mainnet/tproxy-config-local-pool-example.toml
index 5a1e919b0..848872650 100644
--- a/miner-apps/translator/config-examples/mainnet/tproxy-config-local-pool-example.toml
+++ b/miner-apps/translator/config-examples/mainnet/tproxy-config-local-pool-example.toml
@@ -16,6 +16,12 @@ downstream_extranonce2_size = 4
# This will be appended with a counter for each mining client (e.g., username.miner1, username.miner2)
user_identity = "your_username_here"
+# If true, tProxy verifies upstream coinbase outputs against the payout encoded by user_identity.
+# Keep false for standard pool mining, including pools that use a Bitcoin address as the username.
+# Enable for solo/donation identities: sri/solo//,
+# sri/donate///, or legacy [.worker].
+verify_payout = false
+
# Aggregate channels: if true, all miners share one upstream channel; if false, each miner gets its own channel
aggregate_channels = true
diff --git a/miner-apps/translator/config-examples/signet/tproxy-config-local-jdc-example.toml b/miner-apps/translator/config-examples/signet/tproxy-config-local-jdc-example.toml
index ae9565ad6..0a0359cd7 100644
--- a/miner-apps/translator/config-examples/signet/tproxy-config-local-jdc-example.toml
+++ b/miner-apps/translator/config-examples/signet/tproxy-config-local-jdc-example.toml
@@ -16,6 +16,12 @@ downstream_extranonce2_size = 4
# This will be appended with a counter for each mining client (e.g., username.miner1, username.miner2)
user_identity = "your_username_here"
+# If true, tProxy verifies upstream coinbase outputs against the payout encoded by user_identity.
+# Keep false for standard pool mining, including pools that use a Bitcoin address as the username.
+# Enable for solo/donation identities: sri/solo//,
+# sri/donate///, or legacy [.worker].
+verify_payout = false
+
# Aggregate channels: if true, all miners share one upstream channel; if false, each miner gets its own channel
aggregate_channels = false
diff --git a/miner-apps/translator/config-examples/signet/tproxy-config-local-pool-example.toml b/miner-apps/translator/config-examples/signet/tproxy-config-local-pool-example.toml
index fc9a632d5..2f6713cc8 100644
--- a/miner-apps/translator/config-examples/signet/tproxy-config-local-pool-example.toml
+++ b/miner-apps/translator/config-examples/signet/tproxy-config-local-pool-example.toml
@@ -16,6 +16,12 @@ downstream_extranonce2_size = 4
# This will be appended with a counter for each mining client (e.g., username.miner1, username.miner2)
user_identity = "your_username_here"
+# If true, tProxy verifies upstream coinbase outputs against the payout encoded by user_identity.
+# Keep false for standard pool mining, including pools that use a Bitcoin address as the username.
+# Enable for solo/donation identities: sri/solo//,
+# sri/donate///, or legacy [.worker].
+verify_payout = false
+
# Aggregate channels: if true, all miners share one upstream channel; if false, each miner gets its own channel
aggregate_channels = true
diff --git a/miner-apps/translator/config-examples/testnet4/tproxy-config-hosted-pool-example.toml b/miner-apps/translator/config-examples/testnet4/tproxy-config-hosted-pool-example.toml
index ce10d1918..2da43398d 100644
--- a/miner-apps/translator/config-examples/testnet4/tproxy-config-hosted-pool-example.toml
+++ b/miner-apps/translator/config-examples/testnet4/tproxy-config-hosted-pool-example.toml
@@ -16,6 +16,12 @@ downstream_extranonce2_size = 4
# This will be appended with a counter for each mining client (e.g., username.miner1, username.miner2)
user_identity = "your_username_here"
+# If true, tProxy verifies upstream coinbase outputs against the payout encoded by user_identity.
+# Keep false for standard pool mining, including pools that use a Bitcoin address as the username.
+# Enable for solo/donation identities: sri/solo//,
+# sri/donate///, or legacy [.worker].
+verify_payout = false
+
# Aggregate channels: if true, all miners share one upstream channel; if false, each miner gets its own channel
aggregate_channels = true
diff --git a/miner-apps/translator/config-examples/testnet4/tproxy-config-local-jdc-example.toml b/miner-apps/translator/config-examples/testnet4/tproxy-config-local-jdc-example.toml
index ae9565ad6..0a0359cd7 100644
--- a/miner-apps/translator/config-examples/testnet4/tproxy-config-local-jdc-example.toml
+++ b/miner-apps/translator/config-examples/testnet4/tproxy-config-local-jdc-example.toml
@@ -16,6 +16,12 @@ downstream_extranonce2_size = 4
# This will be appended with a counter for each mining client (e.g., username.miner1, username.miner2)
user_identity = "your_username_here"
+# If true, tProxy verifies upstream coinbase outputs against the payout encoded by user_identity.
+# Keep false for standard pool mining, including pools that use a Bitcoin address as the username.
+# Enable for solo/donation identities: sri/solo//,
+# sri/donate///, or legacy [.worker].
+verify_payout = false
+
# Aggregate channels: if true, all miners share one upstream channel; if false, each miner gets its own channel
aggregate_channels = false
diff --git a/miner-apps/translator/config-examples/testnet4/tproxy-config-local-pool-example.toml b/miner-apps/translator/config-examples/testnet4/tproxy-config-local-pool-example.toml
index b496fe38b..e0c039c4b 100644
--- a/miner-apps/translator/config-examples/testnet4/tproxy-config-local-pool-example.toml
+++ b/miner-apps/translator/config-examples/testnet4/tproxy-config-local-pool-example.toml
@@ -16,6 +16,12 @@ downstream_extranonce2_size = 4
# This will be appended with a counter for each mining client (e.g., username.miner1, username.miner2)
user_identity = "your_username_here"
+# If true, tProxy verifies upstream coinbase outputs against the payout encoded by user_identity.
+# Keep false for standard pool mining, including pools that use a Bitcoin address as the username.
+# Enable for solo/donation identities: sri/solo//,
+# sri/donate///, or legacy [.worker].
+verify_payout = false
+
# Aggregate channels: if true, all miners share one upstream channel; if false, each miner gets its own channel
aggregate_channels = true
diff --git a/miner-apps/translator/src/lib/config.rs b/miner-apps/translator/src/lib/config.rs
index 5cf04c790..6beb69d33 100644
--- a/miner-apps/translator/src/lib/config.rs
+++ b/miner-apps/translator/src/lib/config.rs
@@ -17,6 +17,7 @@ use std::net::SocketAddr;
use stratum_apps::{
config_helpers::opt_path_from_toml,
key_utils::Secp256k1PublicKey,
+ payout::{MissingMinerPayoutMode, PayoutMode, PayoutModeError},
utils::types::{Hashrate, SharesPerMinute},
};
@@ -38,6 +39,10 @@ pub struct TranslatorConfig {
/// This will be appended with a counter for each mining channel (e.g., username.miner1,
/// username.miner2).
pub user_identity: String,
+ /// Whether to verify upstream coinbase outputs against a payout address encoded in
+ /// `user_identity`.
+ #[serde(default)]
+ pub verify_payout: bool,
/// Configuration settings for managing difficulty on the downstream connection.
pub downstream_difficulty_config: DownstreamDifficultyConfig,
/// Whether to aggregate all downstream connections into a single upstream channel.
@@ -95,6 +100,7 @@ impl TranslatorConfig {
min_supported_version: u16,
downstream_extranonce2_size: u16,
user_identity: String,
+ verify_payout: bool,
aggregate_channels: bool,
supported_extensions: Vec,
required_extensions: Vec,
@@ -109,6 +115,7 @@ impl TranslatorConfig {
min_supported_version,
downstream_extranonce2_size,
user_identity,
+ verify_payout,
downstream_difficulty_config,
aggregate_channels,
supported_extensions,
@@ -129,6 +136,29 @@ impl TranslatorConfig {
self.monitoring_cache_refresh_secs
}
+ pub(crate) fn expected_payout_distribution(
+ &self,
+ ) -> Result