Skip to content

Commit dba7788

Browse files
committed
Merge stratum-extensions: ASICBoost, NiceHash, address separators, code separation
2 parents 2e27c2b + 64d1ab3 commit dba7788

13 files changed

Lines changed: 2159 additions & 1100 deletions

.github/workflows/build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ jobs:
8181
cmake --build build_ci \
8282
--target test_hardening test_share_messages test_coin_broadcaster \
8383
test_redistribute_address test_redistribute test_auto_ratchet \
84+
test_stratum_extensions \
8485
core_test sharechain_test share_test \
8586
test_threading test_weights \
8687
test_header_chain test_mempool test_template_builder \

docs/API_REFERENCE.md

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,10 @@ Connect via `stratum+tcp://host:worker_port` (default 9327).
239239
|--------|--------|---------|-------------|
240240
| `mining.subscribe` | `[user_agent]` | `[[subscriptions], extranonce1, extranonce2_size]` | Subscribe to work |
241241
| `mining.authorize` | `[username, password]` | `true/false` | Authorize worker. Username format: `LTC_ADDR[,DOGE_ADDR][.worker][+difficulty]` |
242-
| `mining.submit` | `[worker, job_id, extranonce2, ntime, nonce]` | `true/false` | Submit share |
242+
| `mining.configure` | `[extensions[], params{}]` | `{ext: result}` | BIP 310 extension negotiation (version-rolling, subscribe-extranonce) |
243+
| `mining.submit` | `[worker, job_id, extranonce2, ntime, nonce, version_bits?]` | `true/false` | Submit share (6th param = ASICBoost version bits) |
244+
| `mining.suggest_difficulty` | `[difficulty]` | `true` | Suggest initial difficulty |
245+
| `mining.extranonce.subscribe` | `[]` | `true` | NiceHash extranonce subscription |
243246
| `mining.set_merged_addresses` | `[{chain_id: address}]` | `true/false` | Set merged chain addresses (c2pool extension) |
244247

245248
### Server-to-Client (Notifications)
@@ -248,25 +251,56 @@ Connect via `stratum+tcp://host:worker_port` (default 9327).
248251
|--------|--------|-------------|
249252
| `mining.notify` | `[job_id, prevhash, coinb1, coinb2, merkle_branches, version, nbits, ntime, clean]` | New work |
250253
| `mining.set_difficulty` | `[difficulty]` | Difficulty update |
254+
| `mining.set_extranonce` | `[extranonce1, extranonce2_size]` | Extranonce change (requires subscription) |
255+
256+
### BIP 310: mining.configure
257+
258+
Negotiate extensions before subscribing. Pool mask: `0x1fffe000` (bits 13-28).
259+
260+
```json
261+
// Request
262+
{"method": "mining.configure", "params": [
263+
["version-rolling", "subscribe-extranonce"],
264+
{"version-rolling.mask": "1fffe000", "version-rolling.min-bit-count": 2}
265+
]}
266+
// Response
267+
{"result": {
268+
"version-rolling": true, "version-rolling.mask": "1fffe000",
269+
"subscribe-extranonce": true
270+
}}
271+
```
272+
273+
After version-rolling is negotiated, miners pass rolled version bits as the 6th param in `mining.submit`:
274+
```json
275+
{"method": "mining.submit", "params": ["worker", "job_1", "deadbeef", "5f123456", "00000001", "1fffe000"]}
276+
```
277+
278+
### NiceHash/MiningRigRentals Compatibility
279+
280+
Subscribe to extranonce changes via either:
281+
- BIP 310: `mining.configure(["subscribe-extranonce"], {})`, or
282+
- NiceHash: `mining.extranonce.subscribe([])`
283+
284+
After subscription, the server may send `mining.set_extranonce` followed by `mining.notify` with `clean_jobs=true`.
251285

252286
### Address Format in `mining.authorize`
253287

288+
Multiple separator styles are supported for maximum firmware compatibility:
289+
254290
| Format | Example | Behavior |
255291
|--------|---------|----------|
256292
| `LTC_ADDR` | `tltc1q...` | LTC payout, auto-derive DOGE from same pubkey_hash |
257-
| `LTC_ADDR,DOGE_ADDR` | `tltc1q...,nXyz...` | Explicit addresses for both chains |
293+
| `LTC_ADDR,DOGE_ADDR` | `tltc1q...,nXyz...` | Comma separator (standard Stratum) |
294+
| `LTC_ADDR\|DOGE_ADDR` | `tltc1q...\|nXyz...` | Pipe separator (Vnish firmware) |
295+
| `LTC_ADDR;DOGE_ADDR` | `tltc1q...;nXyz...` | Semicolon separator (alternative) |
296+
| `LTC_ADDR DOGE_ADDR` | `tltc1q... nXyz...` | Space separator (some web UIs) |
258297
| `LTC_ADDR/98:DOGE_ADDR` | `tltc1q.../98:nXyz...` | Slash format with explicit chain_id |
259298
| `DOGE_ADDR,LTC_ADDR` | `nXyz...,tltc1q...` | Auto-detected swap, corrected to LTC,DOGE |
260299
| `LTC_ADDR.worker` | `tltc1q....rig1` | Worker name (stripped, used for stats) |
261300
| `LTC_ADDR_worker` | `tltc1q..._rig1` | Underscore worker separator |
262301
| `LTC_ADDR+1024` | `tltc1q...+1024` | Fixed difficulty override |
263302

264-
### Not Implemented (BIP 310 Extensions)
265-
266-
| Method | Description | Impact |
267-
|--------|-------------|--------|
268-
| `mining.configure` | Version rolling extension negotiation | ASICs requiring version rolling will fall back to non-rolling mode |
269-
| `mining.set_version_mask` | Dynamic version mask updates | Same as above |
303+
Separator priority: slash (explicit chain IDs) > comma > pipe > semicolon > space.
270304

271305
## CORS
272306

src/c2pool/hashrate/tracker.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,15 @@ void HashrateTracker::enable_vardiff(bool enabled) {
120120
vardiff_enabled_ = enabled;
121121
}
122122

123+
void HashrateTracker::set_difficulty_hint(double hint) {
124+
std::lock_guard<std::mutex> lock(shares_mutex_);
125+
double clamped = std::max(min_difficulty_, std::min(max_difficulty_, hint));
126+
if (clamped != current_difficulty_) {
127+
LOG_INFO << "[Stratum] Applying difficulty hint: " << current_difficulty_ << " -> " << clamped;
128+
current_difficulty_ = clamped;
129+
}
130+
}
131+
123132
// Python-style aggressive vardiff. Called after every share submission.
124133
// Three triggers:
125134
// 1) Normal: after VARDIFF_TRIGGER shares, scale by actual_rate / target_rate.

src/c2pool/hashrate/tracker.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ class HashrateTracker {
8585
set_target_time_per_mining_share(target_seconds);
8686
}
8787

88+
/// Apply a difficulty hint from the miner (mining.suggest_difficulty).
89+
/// Clamps to [min, max] bounds and updates current difficulty.
90+
void set_difficulty_hint(double hint);
91+
8892
/// Check if difficulty changed (caller should resend set_difficulty).
8993
/// Returns true if current_difficulty_ was updated.
9094
bool difficulty_changed_since(double old_difficulty) const;

src/core/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ set(source
2929
random.hpp random.cpp
3030
addr_store.hpp addr_store.cpp
3131
web_server.hpp web_server.cpp
32+
stratum_server.hpp stratum_server.cpp
33+
address_utils.hpp address_utils.cpp
3234
leveldb_store.hpp leveldb_store.cpp
3335
address_validator.hpp address_validator.cpp
3436
)

src/core/address_utils.cpp

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
#include "address_utils.hpp"
2+
3+
#include <cstring>
4+
#include <sstream>
5+
#include <iomanip>
6+
7+
#include <btclibs/crypto/sha256.h>
8+
#include <btclibs/bech32.h>
9+
10+
namespace core {
11+
12+
std::string base58check_to_hash160(const std::string& address)
13+
{
14+
static constexpr const char* B58 =
15+
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
16+
17+
// decoded[0..24]: 1 version byte + 20 hash160 bytes + 4 checksum bytes
18+
uint8_t decoded[25] = {};
19+
for (unsigned char ch : address) {
20+
const char* p = std::strchr(B58, static_cast<char>(ch));
21+
if (!p) return "";
22+
int carry = static_cast<int>(p - B58);
23+
for (int i = 24; i >= 0; --i) {
24+
carry += 58 * static_cast<int>(decoded[i]);
25+
decoded[i] = static_cast<uint8_t>(carry & 0xFF);
26+
carry >>= 8;
27+
}
28+
if (carry) return "";
29+
}
30+
31+
// Verify checksum: SHA256d(decoded[0..20])[0..4] == decoded[21..24]
32+
uint8_t tmp[32], chk[32];
33+
CSHA256().Write(decoded, 21).Finalize(tmp);
34+
CSHA256().Write(tmp, 32).Finalize(chk);
35+
for (int i = 0; i < 4; ++i)
36+
if (chk[i] != decoded[21 + i]) return "";
37+
38+
static const char* HEX = "0123456789abcdef";
39+
std::string hex;
40+
hex.reserve(40);
41+
for (int i = 1; i <= 20; ++i) {
42+
hex += HEX[decoded[i] >> 4];
43+
hex += HEX[decoded[i] & 0x0f];
44+
}
45+
return hex;
46+
}
47+
48+
std::string address_to_hash160(const std::string& address, std::string& addr_type)
49+
{
50+
addr_type.clear();
51+
52+
// Try Bech32 first
53+
static const std::vector<std::string> bech32_hrps = {"tltc", "ltc", "bc", "tb"};
54+
for (const auto& hrp : bech32_hrps) {
55+
std::string prefix = hrp + "1";
56+
if (address.size() > prefix.size() &&
57+
address.substr(0, prefix.size()) == prefix)
58+
{
59+
int witver = -1;
60+
std::vector<uint8_t> prog;
61+
if (bech32::decode_segwit(hrp, address, witver, prog)) {
62+
if (witver == 0 && prog.size() == 20) {
63+
addr_type = "p2wpkh";
64+
static const char* HEX = "0123456789abcdef";
65+
std::string hex;
66+
hex.reserve(40);
67+
for (uint8_t b : prog) { hex += HEX[b >> 4]; hex += HEX[b & 0x0f]; }
68+
return hex;
69+
}
70+
}
71+
return "";
72+
}
73+
}
74+
75+
// Base58Check
76+
auto h160 = base58check_to_hash160(address);
77+
if (h160.size() == 40) {
78+
static constexpr const char* B58 =
79+
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
80+
uint8_t decoded[25] = {};
81+
for (unsigned char ch : address) {
82+
const char* p = std::strchr(B58, static_cast<char>(ch));
83+
if (!p) return "";
84+
int carry = static_cast<int>(p - B58);
85+
for (int i = 24; i >= 0; --i) {
86+
carry += 58 * static_cast<int>(decoded[i]);
87+
decoded[i] = static_cast<uint8_t>(carry & 0xFF);
88+
carry >>= 8;
89+
}
90+
}
91+
uint8_t version = decoded[0];
92+
if (version == 0x32 || version == 0x05 || version == 0x3a ||
93+
version == 0xc4 || version == 0x16) {
94+
addr_type = "p2sh";
95+
} else {
96+
addr_type = "p2pkh";
97+
}
98+
return h160;
99+
}
100+
101+
return "";
102+
}
103+
104+
std::vector<unsigned char> hash160_to_merged_script(
105+
const std::string& h160_hex, const std::string& addr_type)
106+
{
107+
if (h160_hex.size() != 40) return {};
108+
std::vector<unsigned char> hash_bytes;
109+
hash_bytes.reserve(20);
110+
for (size_t i = 0; i < h160_hex.size(); i += 2)
111+
hash_bytes.push_back(static_cast<unsigned char>(
112+
std::stoul(h160_hex.substr(i, 2), nullptr, 16)));
113+
114+
std::vector<unsigned char> script;
115+
if (addr_type == "p2sh") {
116+
script.reserve(23);
117+
script.push_back(0xa9);
118+
script.push_back(0x14);
119+
script.insert(script.end(), hash_bytes.begin(), hash_bytes.end());
120+
script.push_back(0x87);
121+
} else {
122+
script.reserve(25);
123+
script.push_back(0x76);
124+
script.push_back(0xa9);
125+
script.push_back(0x14);
126+
script.insert(script.end(), hash_bytes.begin(), hash_bytes.end());
127+
script.push_back(0x88);
128+
script.push_back(0xac);
129+
}
130+
return script;
131+
}
132+
133+
bool is_address_for_chain(const std::string& address,
134+
const std::vector<std::string>& chain_hrps,
135+
const std::vector<uint8_t>& chain_versions)
136+
{
137+
// Check bech32
138+
for (const auto& hrp : chain_hrps) {
139+
std::string prefix = hrp + "1";
140+
if (address.size() > prefix.size() &&
141+
address.substr(0, prefix.size()) == prefix)
142+
return true;
143+
}
144+
// Check base58 version byte
145+
if (address.size() >= 25) {
146+
static constexpr const char* B58 =
147+
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
148+
uint8_t decoded[25] = {};
149+
bool valid = true;
150+
for (unsigned char ch : address) {
151+
const char* p = std::strchr(B58, static_cast<char>(ch));
152+
if (!p) { valid = false; break; }
153+
int carry = static_cast<int>(p - B58);
154+
for (int i = 24; i >= 0; --i) {
155+
carry += 58 * static_cast<int>(decoded[i]);
156+
decoded[i] = static_cast<uint8_t>(carry & 0xFF);
157+
carry >>= 8;
158+
}
159+
}
160+
if (valid) {
161+
for (uint8_t ver : chain_versions) {
162+
if (decoded[0] == ver) return true;
163+
}
164+
}
165+
}
166+
return false;
167+
}
168+
169+
std::vector<unsigned char> address_to_script(const std::string& address)
170+
{
171+
// Try Bech32 first
172+
static const std::vector<std::string> bech32_hrps = {"tltc", "ltc", "bc", "tb"};
173+
for (const auto& hrp : bech32_hrps) {
174+
std::string prefix = hrp + "1";
175+
if (address.size() > prefix.size() &&
176+
address.substr(0, prefix.size()) == prefix)
177+
{
178+
int witver = -1;
179+
std::vector<uint8_t> prog;
180+
if (bech32::decode_segwit(hrp, address, witver, prog)) {
181+
std::vector<unsigned char> script;
182+
script.push_back(static_cast<unsigned char>(witver == 0 ? 0x00 : (0x50 + witver)));
183+
script.push_back(static_cast<unsigned char>(prog.size()));
184+
script.insert(script.end(), prog.begin(), prog.end());
185+
return script;
186+
}
187+
break;
188+
}
189+
}
190+
191+
// Try Base58Check (P2PKH)
192+
auto h160 = base58check_to_hash160(address);
193+
if (h160.size() == 40) {
194+
std::vector<unsigned char> script = {0x76, 0xa9, 0x14};
195+
for (size_t i = 0; i < h160.size(); i += 2)
196+
script.push_back(static_cast<unsigned char>(
197+
std::stoul(h160.substr(i, 2), nullptr, 16)));
198+
script.push_back(0x88);
199+
script.push_back(0xac);
200+
return script;
201+
}
202+
203+
return {};
204+
}
205+
206+
} // namespace core

src/core/address_utils.hpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#pragma once
2+
3+
#include <string>
4+
#include <vector>
5+
#include <cstdint>
6+
7+
namespace core {
8+
9+
/// Decode a Base58Check-encoded address (P2PKH or P2SH) and return the
10+
/// 20-byte hash160 payload as a 40-char lowercase hex string.
11+
/// Returns "" on invalid address or checksum failure.
12+
std::string base58check_to_hash160(const std::string& address);
13+
14+
/// Decode any supported address format (bech32 or base58) to its hash160.
15+
/// Sets addr_type to "p2wpkh", "p2pkh", or "p2sh".
16+
/// Returns 40-char hex string, or "" on failure.
17+
std::string address_to_hash160(const std::string& address, std::string& addr_type);
18+
19+
/// Build a scriptPubKey from hash160 and address type.
20+
/// P2PKH/P2WPKH → OP_DUP OP_HASH160 <20> OP_EQUALVERIFY OP_CHECKSIG
21+
/// P2SH → OP_HASH160 <20> OP_EQUAL
22+
std::vector<unsigned char> hash160_to_merged_script(
23+
const std::string& h160_hex, const std::string& addr_type);
24+
25+
/// Check if an address belongs to a specific chain by testing HRP/version bytes.
26+
bool is_address_for_chain(const std::string& address,
27+
const std::vector<std::string>& chain_hrps,
28+
const std::vector<uint8_t>& chain_versions);
29+
30+
/// Build a scriptPubKey from either a Base58Check or Bech32 address.
31+
/// Returns empty vector on failure.
32+
std::vector<unsigned char> address_to_script(const std::string& address);
33+
34+
} // namespace core

0 commit comments

Comments
 (0)