Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion components/stratum_v2/include/sv2_protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
#define SV2_MSG_SET_TARGET 0x21

#define SV2_MAX_MERKLE_BRANCHES 20
#define SV2_MIN_EXTRANONCE_SIZE 2U
#define SV2_MAX_EXTRANONCE_SIZE 32U

// Extension type flag for channel messages
#define SV2_CHANNEL_MSG_FLAG 0x8000
Expand Down Expand Up @@ -109,7 +111,7 @@ typedef struct sv2_conn {
sv2_channel_type_t channel_type;
uint8_t extranonce_prefix[32];
uint8_t extranonce_prefix_len;
uint8_t extranonce_size; // total extranonce bytes assigned by pool
uint8_t extranonce_size; // locally rollable extranonce bytes
sv2_ext_job_t *ext_pending_jobs[SV2_PENDING_JOBS_SIZE];
} sv2_conn_t;

Expand Down
21 changes: 19 additions & 2 deletions components/stratum_v2/sv2_protocol.c
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,11 @@ int sv2_build_open_extended_mining_channel(uint8_t *buf, size_t buf_len,
uint32_t request_id, const char *user_identity,
float nominal_hash_rate, uint16_t min_extranonce_size)
{
if (min_extranonce_size < SV2_MIN_EXTRANONCE_SIZE ||
min_extranonce_size > SV2_MAX_EXTRANONCE_SIZE) {
return -1;
}

uint8_t payload[512];
int pos = 0;

Expand Down Expand Up @@ -417,6 +422,13 @@ int sv2_parse_open_extended_channel_success(const uint8_t *payload, uint32_t len
uint8_t *extranonce_prefix_len,
uint32_t *group_channel_id)
{
if (payload == NULL || request_id == NULL || channel_id == NULL ||
target == NULL || extranonce_size == NULL ||
extranonce_prefix == NULL || extranonce_prefix_len == NULL ||
group_channel_id == NULL) {
return -1;
}

// request_id(4) + channel_id(4) + target(32) + extranonce_size(2) + B0_32(1+N) + group_channel_id(4) = min 47 bytes
if (len < 47) return -1;

Expand All @@ -425,12 +437,17 @@ int sv2_parse_open_extended_channel_success(const uint8_t *payload, uint32_t len
*channel_id = read_u32_le(payload + pos); pos += 4;
memcpy(target, payload + pos, 32); pos += 32;

*extranonce_size = read_u16_le(payload + pos); pos += 2;
uint16_t parsed_extranonce_size = read_u16_le(payload + pos); pos += 2;
if (parsed_extranonce_size < SV2_MIN_EXTRANONCE_SIZE ||
parsed_extranonce_size > SV2_MAX_EXTRANONCE_SIZE) {
return -1;
}

// extranonce_prefix: B0_32 (1 byte length + data)
uint8_t prefix_len = payload[pos++];
if (prefix_len > 32) return -1;
if ((uint32_t)pos + prefix_len + 4 > len) return -1;
if ((uint32_t)pos + prefix_len + 4 != len) return -1;
*extranonce_size = parsed_extranonce_size;
*extranonce_prefix_len = prefix_len;
if (prefix_len > 0) {
memcpy(extranonce_prefix, payload + pos, prefix_len);
Expand Down
3 changes: 3 additions & 0 deletions components/stratum_v2/test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
idf_component_register(SRC_DIRS "."
INCLUDE_DIRS "."
REQUIRES cmock stratum_v2)
95 changes: 95 additions & 0 deletions components/stratum_v2/test/test_sv2_protocol.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#include <string.h>

#include "sv2_protocol.h"
#include "unity.h"

static size_t build_open_extended_success(uint8_t *payload,
uint16_t extranonce_size,
uint8_t prefix_len)
{
const size_t payload_len = 47U + prefix_len;
memset(payload, 0, payload_len);

payload[40] = (uint8_t)(extranonce_size & 0xffU);
payload[41] = (uint8_t)(extranonce_size >> 8U);
payload[42] = prefix_len;
for (uint8_t i = 0; i < prefix_len; i++) {
payload[43U + i] = i;
}

return payload_len;
}

static int parse_open_extended_success(const uint8_t *payload, size_t payload_len)
{
uint32_t request_id = 0;
uint32_t channel_id = 0;
uint8_t target[32] = {0};
uint16_t extranonce_size = 0;
uint8_t extranonce_prefix[32] = {0};
uint8_t extranonce_prefix_len = 0;
uint32_t group_channel_id = 0;

return sv2_parse_open_extended_channel_success(
payload, (uint32_t)payload_len, &request_id, &channel_id, target,
&extranonce_size, extranonce_prefix, &extranonce_prefix_len,
&group_channel_id);
}

TEST_CASE("SV2 extended channel accepts supported extranonce sizes", "[sv2]")
{
uint8_t payload[47 + 32];

size_t len = build_open_extended_success(payload, SV2_MIN_EXTRANONCE_SIZE, 0);
TEST_ASSERT_EQUAL_INT(0, parse_open_extended_success(payload, len));

len = build_open_extended_success(payload, SV2_MAX_EXTRANONCE_SIZE, 32);
TEST_ASSERT_EQUAL_INT(0, parse_open_extended_success(payload, len));
}

TEST_CASE("SV2 extended channel rejects unsafe extranonce sizes", "[sv2]")
{
static const uint16_t unsafe_sizes[] = {
0, 1, 33, 255, 256, UINT16_MAX,
};
uint8_t payload[47];

for (size_t i = 0; i < sizeof(unsafe_sizes) / sizeof(unsafe_sizes[0]); i++) {
size_t len = build_open_extended_success(payload, unsafe_sizes[i], 0);
TEST_ASSERT_EQUAL_INT(-1, parse_open_extended_success(payload, len));
}
}

TEST_CASE("SV2 extended channel rejects invalid prefix framing", "[sv2]")
{
uint8_t payload[47 + 33 + 1];
size_t len = build_open_extended_success(payload, SV2_MIN_EXTRANONCE_SIZE, 33);
TEST_ASSERT_EQUAL_INT(-1, parse_open_extended_success(payload, len));

len = build_open_extended_success(payload, SV2_MIN_EXTRANONCE_SIZE, 0);
TEST_ASSERT_EQUAL_INT(-1, parse_open_extended_success(payload, len - 1U));

payload[len] = 0;
TEST_ASSERT_EQUAL_INT(-1, parse_open_extended_success(payload, len + 1U));
}

TEST_CASE("SV2 extended channel request enforces local extranonce limits", "[sv2]")
{
uint8_t frame[128];

TEST_ASSERT_GREATER_THAN(0, sv2_build_open_extended_mining_channel(
frame, sizeof(frame), 1, "miner", 1e12f,
SV2_MIN_EXTRANONCE_SIZE));
TEST_ASSERT_GREATER_THAN(0, sv2_build_open_extended_mining_channel(
frame, sizeof(frame), 1, "miner", 1e12f,
SV2_MAX_EXTRANONCE_SIZE));
TEST_ASSERT_EQUAL_INT(-1, sv2_build_open_extended_mining_channel(
frame, sizeof(frame), 1, "miner", 1e12f, 0));
TEST_ASSERT_EQUAL_INT(-1, sv2_build_open_extended_mining_channel(
frame, sizeof(frame), 1, "miner", 1e12f, 1));
TEST_ASSERT_EQUAL_INT(-1, sv2_build_open_extended_mining_channel(
frame, sizeof(frame), 1, "miner", 1e12f, 33));
TEST_ASSERT_EQUAL_INT(-1, sv2_build_open_extended_mining_channel(
frame, sizeof(frame), 1, "miner", 1e12f,
UINT16_MAX));
}
24 changes: 16 additions & 8 deletions main/tasks/asic_result_task.c
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,22 @@ void ASIC_result_task(void *pvParameters)
sv2_conn_t *conn = GLOBAL_STATE->sv2_conn;
// SV2 spec: extranonce_size is the miner's rollable portion.
// The pool prepends its extranonce_prefix separately.
uint8_t en2_len = conn->extranonce_size;
uint8_t extranonce_2[32];
hex2bin(active_job->extranonce2, extranonce_2, en2_len);
ret = stratum_v2_submit_share_extended(GLOBAL_STATE, sv2_job_id,
asic_result->nonce,
active_job->ntime,
asic_result->rolled_version,
extranonce_2, en2_len);
uint8_t en2_len = conn != NULL ? conn->extranonce_size : 0;
uint8_t extranonce_2[SV2_MAX_EXTRANONCE_SIZE];
if (en2_len < SV2_MIN_EXTRANONCE_SIZE ||
en2_len > sizeof(extranonce_2) ||
active_job->extranonce2 == NULL ||
strlen(active_job->extranonce2) != (size_t)en2_len * 2U ||
hex2bin(active_job->extranonce2, extranonce_2, en2_len) != en2_len) {
ESP_LOGW(TAG, "Dropping SV2 share with invalid extranonce data");
ret = -1;
} else {
ret = stratum_v2_submit_share_extended(GLOBAL_STATE, sv2_job_id,
asic_result->nonce,
active_job->ntime,
asic_result->rolled_version,
extranonce_2, en2_len);
}
} else {
ret = stratum_v2_submit_share(GLOBAL_STATE, sv2_job_id,
asic_result->nonce,
Expand Down
10 changes: 8 additions & 2 deletions main/tasks/create_jobs_task.c
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,13 @@ static void generate_work_sv2_ext(GlobalState *GLOBAL_STATE, sv2_ext_job_t *ext_
sv2_conn_t *conn = GLOBAL_STATE->sv2_conn;
if (!conn) return;

uint8_t extranonce_2_len = conn->extranonce_size;
if (extranonce_2_len < SV2_MIN_EXTRANONCE_SIZE ||
extranonce_2_len > SV2_MAX_EXTRANONCE_SIZE) {
ESP_LOGE(TAG, "Invalid SV2 extranonce size: %u", extranonce_2_len);
return;
}

bm_job *next_job = malloc(sizeof(bm_job));
if (!next_job) {
ESP_LOGE(TAG, "Failed to allocate memory for SV2 ext job");
Expand All @@ -322,8 +329,7 @@ static void generate_work_sv2_ext(GlobalState *GLOBAL_STATE, sv2_ext_job_t *ext_

// Derive extranonce_2 from counter
// SV2 spec: extranonce_size is the miner's rollable portion (not total)
uint8_t extranonce_2_len = conn->extranonce_size;
uint8_t extranonce_2[32];
uint8_t extranonce_2[SV2_MAX_EXTRANONCE_SIZE];
memset(extranonce_2, 0, sizeof(extranonce_2));
// Encode counter as big-endian bytes
for (int i = extranonce_2_len - 1; i >= 0 && extranonce_2_counter > 0; i--) {
Expand Down
2 changes: 1 addition & 1 deletion test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ set(EXTRA_COMPONENT_DIRS "../components")
# - when invoking CMake directly: cmake -D TEST_COMPONENTS="xxxxx" ..
# - when using idf.py: idf.py -T xxxxx build
#
set(TEST_COMPONENTS "stratum asic" CACHE STRING "List of components to test")
set(TEST_COMPONENTS "stratum stratum_v2 asic" CACHE STRING "List of components to test")

include($ENV{IDF_PATH}/tools/cmake/project.cmake)

Expand Down
Loading