Skip to content

Commit b4d256a

Browse files
committed
Add testnet/regtest network detection to coinbase address decoder
Detect network from user address prefix (bc1/tb1/bcrt1/m/n/2) and use the correct bech32 HRP and base58 version bytes when decoding coinbase output addresses. Previously hardcoded to mainnet ("bc", 0x00/0x05). - P2PKH: 0x6F for testnet (m/n prefix) instead of 0x00 - P2SH: 0xC4 for testnet (2 prefix) instead of 0x05 - Bech32: "tb" for testnet, "bcrt" for regtest instead of "bc" - Add test cases for testnet P2PKH, P2SH, P2WPKH, P2TR and regtest P2WPKH - Remove duplicate function declarations in coinbase_decoder.h
1 parent 037be4b commit b4d256a

3 files changed

Lines changed: 136 additions & 48 deletions

File tree

components/stratum/coinbase_decoder.c

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -45,67 +45,69 @@ uint64_t coinbase_decode_varint(const uint8_t *data, int *offset) {
4545
}
4646
}
4747

48-
void coinbase_decode_address_from_scriptpubkey(const uint8_t *script, size_t script_len,
49-
char *output, size_t output_len) {
48+
void coinbase_decode_address_from_scriptpubkey(const uint8_t *script, size_t script_len,
49+
char *output, size_t output_len,
50+
const char *bech32_hrp, bool is_testnet) {
5051
if (script_len == 0 || output_len < 65) {
5152
snprintf(output, output_len, "unknown");
5253
return;
5354
}
54-
55+
5556
ensure_base58_init();
56-
57+
58+
uint8_t p2pkh_version = is_testnet ? 0x6F : 0x00;
59+
uint8_t p2sh_version = is_testnet ? 0xC4 : 0x05;
60+
5761
// P2PKH: OP_DUP OP_HASH160 <20 bytes> OP_EQUALVERIFY OP_CHECKSIG
58-
if (script_len == 25 && script[0] == OP_DUP && script[1] == OP_HASH160 &&
62+
if (script_len == 25 && script[0] == OP_DUP && script[1] == OP_HASH160 &&
5963
script[2] == OP_PUSHDATA_20 && script[23] == OP_EQUALVERIFY && script[24] == OP_CHECKSIG) {
6064
size_t b58sz = output_len;
61-
// 0x00 is version for Mainnet P2PKH
62-
if (b58check_enc(output, &b58sz, 0x00, script + 3, 20)) {
65+
if (b58check_enc(output, &b58sz, p2pkh_version, script + 3, 20)) {
6366
return;
6467
}
6568
// Fallback
6669
snprintf(output, output_len, "P2PKH:");
6770
bin2hex(script + 3, 20, output + 6, output_len - 6);
6871
return;
6972
}
70-
73+
7174
// P2SH: OP_HASH160 <20 bytes> OP_EQUAL
7275
if (script_len == 23 && script[0] == OP_HASH160 && script[1] == OP_PUSHDATA_20 && script[22] == OP_EQUAL) {
7376
size_t b58sz = output_len;
74-
// 0x05 is version for Mainnet P2SH
75-
if (b58check_enc(output, &b58sz, 0x05, script + 2, 20)) {
77+
if (b58check_enc(output, &b58sz, p2sh_version, script + 2, 20)) {
7678
return;
7779
}
7880
// Fallback
7981
snprintf(output, output_len, "P2SH:");
8082
bin2hex(script + 2, 20, output + 5, output_len - 5);
8183
return;
8284
}
83-
85+
8486
// P2WPKH: OP_0 <20 bytes>
8587
if (script_len == 22 && script[0] == OP_0 && script[1] == OP_PUSHDATA_20) {
86-
if (segwit_addr_encode(output, "bc", 0, script + 2, 20)) {
88+
if (segwit_addr_encode(output, bech32_hrp, 0, script + 2, 20)) {
8789
return;
8890
}
8991
// Fallback to hex if encoding fails
9092
snprintf(output, output_len, "P2WPKH:");
9193
bin2hex(script + 2, 20, output + 7, output_len - 7);
9294
return;
9395
}
94-
96+
9597
// P2WSH: OP_0 <32 bytes>
9698
if (script_len == 34 && script[0] == OP_0 && script[1] == OP_PUSHDATA_32) {
97-
if (segwit_addr_encode(output, "bc", 0, script + 2, 32)) {
99+
if (segwit_addr_encode(output, bech32_hrp, 0, script + 2, 32)) {
98100
return;
99101
}
100102
// Fallback to hex if encoding fails
101103
snprintf(output, output_len, "P2WSH:");
102104
bin2hex(script + 2, 32, output + 6, output_len - 6);
103105
return;
104106
}
105-
107+
106108
// P2TR: OP_1 <32 bytes>
107109
if (script_len == 34 && script[0] == OP_1 && script[1] == OP_PUSHDATA_32) {
108-
if (segwit_addr_encode(output, "bc", 1, script + 2, 32)) {
110+
if (segwit_addr_encode(output, bech32_hrp, 1, script + 2, 32)) {
109111
return;
110112
}
111113
// Fallback to hex if encoding fails
@@ -153,6 +155,22 @@ esp_err_t coinbase_process_notification(const mining_notify *notification,
153155
result->user_value_satoshis = 0;
154156
result->decoding_enabled = decode_outputs;
155157

158+
// Detect network from user address prefix for correct address encoding
159+
const char *bech32_hrp = "bc";
160+
bool is_testnet = false;
161+
if (user_address) {
162+
if (strncmp(user_address, "bcrt1", 4) == 0) {
163+
bech32_hrp = "bcrt";
164+
is_testnet = true;
165+
} else if (strncmp(user_address, "tb1", 3) == 0) {
166+
bech32_hrp = "tb";
167+
is_testnet = true;
168+
} else if (user_address[0] == 'm' || user_address[0] == 'n' || user_address[0] == '2') {
169+
bech32_hrp = "tb";
170+
is_testnet = true;
171+
}
172+
}
173+
156174
// 1. Calculate difficulty
157175
result->network_difficulty = networkDifficulty(notification->target);
158176

@@ -284,7 +302,7 @@ esp_err_t coinbase_process_notification(const mining_notify *notification,
284302
if (decode_outputs) {
285303
if (value_satoshis > 0) {
286304
char output_address[MAX_ADDRESS_STRING_LEN];
287-
coinbase_decode_address_from_scriptpubkey(coinbase_2_bin + offset, script_len, output_address, MAX_ADDRESS_STRING_LEN);
305+
coinbase_decode_address_from_scriptpubkey(coinbase_2_bin + offset, script_len, output_address, MAX_ADDRESS_STRING_LEN, bech32_hrp, is_testnet);
288306
bool is_user_address = strncmp(user_address, output_address, strlen(output_address)) == 0;
289307

290308
if (is_user_address) result->user_value_satoshis += value_satoshis;
@@ -297,7 +315,7 @@ esp_err_t coinbase_process_notification(const mining_notify *notification,
297315
}
298316
} else {
299317
if (i < MAX_COINBASE_TX_OUTPUTS) {
300-
coinbase_decode_address_from_scriptpubkey(coinbase_2_bin + offset, script_len, result->outputs[i].address, MAX_ADDRESS_STRING_LEN);
318+
coinbase_decode_address_from_scriptpubkey(coinbase_2_bin + offset, script_len, result->outputs[i].address, MAX_ADDRESS_STRING_LEN, bech32_hrp, is_testnet);
301319
result->outputs[i].value_satoshis = 0;
302320
result->outputs[i].is_user_output = false;
303321
result->output_count++;

components/stratum/include/coinbase_decoder.h

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,20 @@ uint64_t coinbase_decode_varint(const uint8_t *data, int *offset);
3333

3434
/**
3535
* @brief Decode Bitcoin address from scriptPubKey
36-
*
36+
*
3737
* Supports P2PKH, P2SH, P2WPKH, P2WSH, and P2TR address types.
38-
* Output format: "<TYPE>:<HEX_HASH>"
39-
*
38+
* Detects network from user_address prefix (bc1/tb1/bcrt1/1/3/m/n/2).
39+
*
4040
* @param script ScriptPubKey binary data
4141
* @param script_len Length of scriptPubKey
4242
* @param output Output buffer for address string
4343
* @param output_len Size of output buffer (should be at least MAX_ADDRESS_STRING_LEN)
44+
* @param bech32_hrp Bech32 human-readable part ("bc" for mainnet, "tb" for testnet, "bcrt" for regtest)
45+
* @param is_testnet true for testnet/regtest (affects base58 version bytes)
4446
*/
45-
void coinbase_decode_address_from_scriptpubkey(const uint8_t *script, size_t script_len,
46-
char *output, size_t output_len);
47+
void coinbase_decode_address_from_scriptpubkey(const uint8_t *script, size_t script_len,
48+
char *output, size_t output_len,
49+
const char *bech32_hrp, bool is_testnet);
4750

4851
/**
4952
* @brief Structure representing a decoded coinbase transaction output
@@ -54,26 +57,6 @@ typedef struct {
5457
bool is_user_output;
5558
} coinbase_output_t;
5659

57-
/**
58-
* @brief Decode a variable-length integer from binary data
59-
*
60-
* @param data Binary data containing the varint
61-
* @param offset Pointer to current offset (will be updated)
62-
* @return Decoded integer value
63-
*/
64-
uint64_t coinbase_decode_varint(const uint8_t *data, int *offset);
65-
66-
/**
67-
* @brief Decode a Bitcoin address from a scriptPubKey
68-
*
69-
* @param script Binary scriptPubKey data
70-
* @param script_len Length of scriptPubKey
71-
* @param output Buffer to store the decoded address string
72-
* @param output_len Size of output buffer
73-
*/
74-
void coinbase_decode_address_from_scriptpubkey(const uint8_t *script, size_t script_len,
75-
char *output, size_t output_len);
76-
7760
/**
7861
* @brief Result structure for full mining notification processing
7962
*/

components/stratum/test/test_coinbase_decoder.c

Lines changed: 92 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ TEST_CASE("Decode P2PKH address", "[coinbase_decoder]")
5050
};
5151
char output[MAX_ADDRESS_STRING_LEN];
5252

53-
coinbase_decode_address_from_scriptpubkey(script, sizeof(script), output, sizeof(output));
53+
coinbase_decode_address_from_scriptpubkey(script, sizeof(script), output, sizeof(output), "bc", false);
5454

5555
TEST_ASSERT_EQUAL_STRING("1DYwPTnC4NgEmoqbLbcRqoSzVeH3ehmGbV", output);
5656
}
@@ -66,7 +66,7 @@ TEST_CASE("Decode P2SH address", "[coinbase_decoder]")
6666
};
6767
char output[MAX_ADDRESS_STRING_LEN];
6868

69-
coinbase_decode_address_from_scriptpubkey(script, sizeof(script), output, sizeof(output));
69+
coinbase_decode_address_from_scriptpubkey(script, sizeof(script), output, sizeof(output), "bc", false);
7070

7171
TEST_ASSERT_EQUAL_STRING("33MGnVL6rnKqt6Jjt3HbRqWJrhwy65dMhS", output);
7272
}
@@ -81,7 +81,7 @@ TEST_CASE("Decode P2WPKH address", "[coinbase_decoder]")
8181
};
8282
char output[MAX_ADDRESS_STRING_LEN];
8383

84-
coinbase_decode_address_from_scriptpubkey(script, sizeof(script), output, sizeof(output));
84+
coinbase_decode_address_from_scriptpubkey(script, sizeof(script), output, sizeof(output), "bc", false);
8585

8686
TEST_ASSERT_EQUAL_STRING("bc1q42aueh0wluqpzg3ng32kvaugnx4thnxa7y625x", output);
8787
}
@@ -98,7 +98,7 @@ TEST_CASE("Decode P2WSH address", "[coinbase_decoder]")
9898
};
9999
char output[MAX_ADDRESS_STRING_LEN];
100100

101-
coinbase_decode_address_from_scriptpubkey(script, sizeof(script), output, sizeof(output));
101+
coinbase_decode_address_from_scriptpubkey(script, sizeof(script), output, sizeof(output), "bc", false);
102102

103103
TEST_ASSERT_EQUAL_STRING("bc1qqypqxpq9qcrsszg2pvxq6rs0zqg3yyc5z5tpwxqergd3c8g7rusqyp0mu0", output);
104104
}
@@ -115,7 +115,94 @@ TEST_CASE("Decode P2TR address", "[coinbase_decoder]")
115115
};
116116
char output[MAX_ADDRESS_STRING_LEN];
117117

118-
coinbase_decode_address_from_scriptpubkey(script, sizeof(script), output, sizeof(output));
118+
coinbase_decode_address_from_scriptpubkey(script, sizeof(script), output, sizeof(output), "bc", false);
119119

120120
TEST_ASSERT_EQUAL_STRING("bc1pllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqqc0cgpt", output);
121121
}
122+
123+
// Testnet address tests
124+
125+
TEST_CASE("Decode testnet P2PKH address", "[coinbase_decoder]")
126+
{
127+
// Same hash as mainnet P2PKH test, but with testnet version byte (0x6F)
128+
uint8_t script[] = {
129+
0x76, 0xa9, 0x14,
130+
0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab,
131+
0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
132+
0x88, 0xac
133+
};
134+
char output[MAX_ADDRESS_STRING_LEN];
135+
136+
coinbase_decode_address_from_scriptpubkey(script, sizeof(script), output, sizeof(output), "tb", true);
137+
138+
// Testnet P2PKH addresses start with 'm' or 'n'
139+
TEST_ASSERT_TRUE(output[0] == 'm' || output[0] == 'n');
140+
}
141+
142+
TEST_CASE("Decode testnet P2SH address", "[coinbase_decoder]")
143+
{
144+
// Same hash as mainnet P2SH test, but with testnet version byte (0xC4)
145+
uint8_t script[] = {
146+
0xa9, 0x14,
147+
0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34,
148+
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78,
149+
0x87
150+
};
151+
char output[MAX_ADDRESS_STRING_LEN];
152+
153+
coinbase_decode_address_from_scriptpubkey(script, sizeof(script), output, sizeof(output), "tb", true);
154+
155+
// Testnet P2SH addresses start with '2'
156+
TEST_ASSERT_EQUAL_CHAR('2', output[0]);
157+
}
158+
159+
TEST_CASE("Decode testnet P2WPKH address", "[coinbase_decoder]")
160+
{
161+
// Same hash as mainnet P2WPKH test, but with "tb" HRP
162+
uint8_t script[] = {
163+
0x00, 0x14,
164+
0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33,
165+
0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd
166+
};
167+
char output[MAX_ADDRESS_STRING_LEN];
168+
169+
coinbase_decode_address_from_scriptpubkey(script, sizeof(script), output, sizeof(output), "tb", true);
170+
171+
TEST_ASSERT_TRUE(strncmp(output, "tb1q", 4) == 0);
172+
}
173+
174+
TEST_CASE("Decode testnet P2TR address", "[coinbase_decoder]")
175+
{
176+
// Same hash as mainnet P2TR test, but with "tb" HRP
177+
uint8_t script[] = {
178+
0x51, 0x20,
179+
0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88,
180+
0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00,
181+
0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88,
182+
0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00
183+
};
184+
char output[MAX_ADDRESS_STRING_LEN];
185+
186+
coinbase_decode_address_from_scriptpubkey(script, sizeof(script), output, sizeof(output), "tb", true);
187+
188+
TEST_ASSERT_TRUE(strncmp(output, "tb1p", 4) == 0);
189+
}
190+
191+
TEST_CASE("Decode regtest P2WPKH address", "[coinbase_decoder]")
192+
{
193+
// Same hash as mainnet P2WPKH test, but with "bcrt" HRP
194+
uint8_t script[] = {
195+
0x00, 0x14,
196+
0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33,
197+
0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd
198+
};
199+
char output[MAX_ADDRESS_STRING_LEN];
200+
201+
coinbase_decode_address_from_scriptpubkey(script, sizeof(script), output, sizeof(output), "bcrt", true);
202+
203+
TEST_ASSERT_TRUE(strncmp(output, "bcrt1q", 6) == 0);
204+
}
205+
206+
// Network auto-detection tests via coinbase_process_notification are
207+
// integration-level — the detection logic is tested implicitly through
208+
// the address prefix matching in the full processing pipeline.

0 commit comments

Comments
 (0)