From 215077d0ec8e6dacc0d963210c7463d9b883b35f Mon Sep 17 00:00:00 2001 From: Distortions81 Date: Fri, 7 Aug 2026 12:09:27 -0600 Subject: [PATCH] fix: bound SV2 extranonce negotiation --- components/stratum_v2/include/sv2_protocol.h | 4 +- components/stratum_v2/sv2_protocol.c | 21 +++- components/stratum_v2/test/CMakeLists.txt | 3 + .../stratum_v2/test/test_sv2_protocol.c | 95 +++++++++++++++++++ main/tasks/asic_result_task.c | 24 +++-- main/tasks/create_jobs_task.c | 10 +- test/CMakeLists.txt | 2 +- 7 files changed, 145 insertions(+), 14 deletions(-) create mode 100644 components/stratum_v2/test/CMakeLists.txt create mode 100644 components/stratum_v2/test/test_sv2_protocol.c diff --git a/components/stratum_v2/include/sv2_protocol.h b/components/stratum_v2/include/sv2_protocol.h index 6f34744021..548c86a417 100644 --- a/components/stratum_v2/include/sv2_protocol.h +++ b/components/stratum_v2/include/sv2_protocol.h @@ -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 @@ -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; diff --git a/components/stratum_v2/sv2_protocol.c b/components/stratum_v2/sv2_protocol.c index eb0de20fb6..ac4d549677 100644 --- a/components/stratum_v2/sv2_protocol.c +++ b/components/stratum_v2/sv2_protocol.c @@ -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; @@ -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; @@ -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); diff --git a/components/stratum_v2/test/CMakeLists.txt b/components/stratum_v2/test/CMakeLists.txt new file mode 100644 index 0000000000..19f38ec7d7 --- /dev/null +++ b/components/stratum_v2/test/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRC_DIRS "." + INCLUDE_DIRS "." + REQUIRES cmock stratum_v2) diff --git a/components/stratum_v2/test/test_sv2_protocol.c b/components/stratum_v2/test/test_sv2_protocol.c new file mode 100644 index 0000000000..a58bab3769 --- /dev/null +++ b/components/stratum_v2/test/test_sv2_protocol.c @@ -0,0 +1,95 @@ +#include + +#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)); +} diff --git a/main/tasks/asic_result_task.c b/main/tasks/asic_result_task.c index 9d096fc2a6..46c42cfe21 100644 --- a/main/tasks/asic_result_task.c +++ b/main/tasks/asic_result_task.c @@ -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, diff --git a/main/tasks/create_jobs_task.c b/main/tasks/create_jobs_task.c index 57e9668cbd..f754f781c3 100644 --- a/main/tasks/create_jobs_task.c +++ b/main/tasks/create_jobs_task.c @@ -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"); @@ -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--) { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e98ced1503..23aa13fefc 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -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)