Make stratum module testable and introduce first unit test

This commit is contained in:
johnny9 2023-04-29 21:23:57 -04:00
parent ec28d0a74d
commit 3066edb7f1
12 changed files with 242 additions and 49 deletions

View File

@ -0,0 +1,3 @@
idf_component_register(SRCS "stratum_api.c"
INCLUDE_DIRS "include"
REQUIRES json)

View File

@ -0,0 +1,54 @@
#ifndef STRATUM_API_H
#define STRATUM_API_H
#include "cJSON.h"
#include <stdint.h>
#define MAX_MERKLE_BRANCHES 32
#define PREV_BLOCK_HASH_SIZE 32
#define COINBASE_SIZE 100
#define COINBASE2_SIZE 128
typedef struct {
uint32_t job_id;
uint8_t prev_block_hash[PREV_BLOCK_HASH_SIZE];
uint8_t coinbase_1[COINBASE_SIZE];
size_t coinbase_1_len;
uint8_t coinbase_2[COINBASE2_SIZE];
size_t coinbase_2_len;
uint8_t merkle_branches[MAX_MERKLE_BRANCHES][PREV_BLOCK_HASH_SIZE];
size_t n_merkle_branches;
uint32_t version;
uint32_t curtime;
uint32_t bits;
uint32_t target;
uint32_t nonce;
} work;
typedef enum {
STRATUM_UNKNOWN,
MINING_NOTIFY,
MINING_SET_DIFFICULTY
} stratum_method;
typedef struct {
int id;
stratum_method method;
char * method_str;
union {
work notify_work;
uint32_t notify_difficulty;
};
} stratum_message;
void initialize_stratum_buffer();
char * receive_jsonrpc_line(int sockfd);
int subscribe_to_stratum(int socket);
stratum_message parse_stratum_notify_message(const char * stratum_json);
int auth_to_stratum(int socket, const char * username);
#endif // STRATUM_API_H

View File

@ -2,9 +2,12 @@
#include "cJSON.h"
#include <string.h>
#include <stdio.h>
#include "esp_log.h"
#include "lwip/sockets.h"
#define BUFFER_SIZE 1024
static const char *TAG = "stratum client";
static char *json_rpc_buffer = NULL;
static size_t json_rpc_buffer_size = 0;
@ -78,6 +81,7 @@ char * receive_jsonrpc_line(int sockfd)
realloc_json_buffer(nbytes);
strncat(json_rpc_buffer, recv_buffer, nbytes);
ESP_LOGI(TAG, "Current buffer %s", json_rpc_buffer);
} while (!strstr(json_rpc_buffer, "\n"));
}
buflen = strlen(json_rpc_buffer);
@ -91,6 +95,62 @@ char * receive_jsonrpc_line(int sockfd)
return line;
}
static uint8_t hex2val(char c)
{
if (c >= '0' && c <= '9') {
return c - '0';
} else if (c >= 'a' && c <= 'f') {
return c - 'a' + 10;
} else if (c >= 'A' && c <= 'F') {
return c - 'A' + 10;
} else {
return 0;
}
}
static size_t hex2bin(const char *hex, uint8_t *bin, size_t bin_len)
{
size_t len = 0;
while (*hex && len < bin_len) {
bin[len] = hex2val(*hex++) << 4;
if (!*hex) {
len++;
break;
}
bin[len++] |= hex2val(*hex++);
}
return len;
}
work parse_notify_work(cJSON * params) {
work new_work;
new_work.job_id = (uint32_t) strtoul(cJSON_GetArrayItem(params, 0)->valuestring, NULL, 16);
hex2bin(cJSON_GetArrayItem(params, 1)->valuestring, new_work.prev_block_hash, PREV_BLOCK_HASH_SIZE * 2);
char *coinb1 = cJSON_GetArrayItem(params, 2)->valuestring;
char *coinb2 = cJSON_GetArrayItem(params, 3)->valuestring;
hex2bin(coinb1, new_work.coinbase_1, strlen(coinb1) / 2);
new_work.coinbase_1_len = strlen(coinb1) / 2;
hex2bin(coinb2, new_work.coinbase_2, strlen(coinb2) / 2);
new_work.coinbase_2_len = strlen(coinb2) / 2;
cJSON *merkle_branch = cJSON_GetArrayItem(params, 4);
new_work.n_merkle_branches = cJSON_GetArraySize(merkle_branch);
if (new_work.n_merkle_branches > MAX_MERKLE_BRANCHES) {
printf("Too many Merkle branches.\n");
abort();
}
for (size_t i = 0; i < new_work.n_merkle_branches; i++) {
hex2bin(cJSON_GetArrayItem(merkle_branch, i)->valuestring, new_work.merkle_branches[i], PREV_BLOCK_HASH_SIZE * 2);
}
return new_work;
}
stratum_message parse_stratum_notify_message(const char * stratum_json)
{
stratum_message output;
@ -108,6 +168,7 @@ stratum_message parse_stratum_notify_message(const char * stratum_json)
output.method_str = strdup(method->valuestring);
if (strcmp("mining.notify", method->valuestring) == 0) {
output.method = MINING_NOTIFY;
output.notify_work = parse_notify_work(cJSON_GetObjectItem(json, "params"));
} else if (strcmp("mining.set_difficulty", method->valuestring) == 0) {
output.method = MINING_SET_DIFFICULTY;
} else {
@ -123,10 +184,34 @@ int subscribe_to_stratum(int socket)
{
// Subscribe
char subscribe_msg[BUFFER_SIZE];
sprintf(subscribe_msg, "{\"id\": %d, \"method\": \"mining.subscribe\", \"params\": []}", send_uid++);
sprintf(subscribe_msg, "{\"id\": %d, \"method\": \"mining.subscribe\", \"params\": []}\n", send_uid++);
ESP_LOGI(TAG, "Subscribe: %s", subscribe_msg);
write(socket, subscribe_msg, strlen(subscribe_msg));
char * line;
line = receive_jsonrpc_line(socket);
ESP_LOGI(TAG, "Received result %s", line);
free(line);
return 1;
}
int auth_to_stratum(int socket, const char * username)
{
char authorize_msg[BUFFER_SIZE];
sprintf(authorize_msg, "{\"id\": %d, \"method\": \"mining.authorize\", \"params\": [\"%s\", \"x\"]}\n",
send_uid++, username);
ESP_LOGI(TAG, "Authorize: %s", authorize_msg);
write(socket, authorize_msg, strlen(authorize_msg));
char * line;
line = receive_jsonrpc_line(socket);
ESP_LOGI(TAG, "Received result %s", line);
// TODO: Parse authorize results
free(line);
return 1;

View File

@ -0,0 +1,3 @@
idf_component_register(SRC_DIRS "."
INCLUDE_DIRS "."
REQUIRES cmock stratum)

View File

@ -0,0 +1,15 @@
#include "unity.h"
#include "stratum_api.h"
TEST_CASE("Check can parse json", "[mining.notify]")
{
const char * json_string = "{\"id\":null,\"method\":\"mining.notify\",\"params\":"
"[\"1b4c3d9041\","
"\"ef4b9a48c7986466de4adc002f7337a6e121bc43000376ea0000000000000000\","
"\"01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4b03a5020cfabe6d6d379ae882651f6469f2ed6b8b40a4f9a4b41fd838a3ad6de8cba775f4e8f1d3080100000000000000\","
"\"41903d4c1b2f736c7573682f0000000003ca890d27000000001976a9147c154ed1dc59609e3d26abb2df2ea3d587cd8c4188ac00000000000000002c6a4c2952534b424c4f434b3a4cb4cb2ddfc37c41baf5ef6b6b4899e3253a8f1dfc7e5dd68a5b5b27005014ef0000000000000000266a24aa21a9ed5caa249f1af9fbf71c986fea8e076ca34ae3514fb2f86400561b28c7b15949bf00000000\","
"[\"ae23055e00f0f697cc3640124812d96d4fe8bdfa03484c1c638ce5a1c0e9aa81\",\"980fb87cb61021dd7afd314fcb0dabd096f3d56a7377f6f320684652e7410a21\",\"a52e9868343c55ce405be8971ff340f562ae9ab6353f07140d01666180e19b52\",\"7435bdfa004e603953b2ed39f118803934d9cf17b06d979ceb682f2251bafac2\",\"2a91f061a22d27cb8f44eea79938fb241ebeb359891aa907f05ffde7ed44e52e\",\"302401f80eb5e958155135e25200bb8ea181ad2d05e804a531c7314d86403cdc\",\"318ecb6161eb9b4cfd802bd730e2d36c167ddf102e70aa7b4158e2870dd47392\",\"1114332a9858e0cf84b2425bb1e59eaabf91dd102d114aa443d57fc1b3beb0c9\",\"f43f38095c810613ed795a44d9fab02ff25269706f454885db9be05cdf9c06e1\",\"3e2fc26b27fddc39668b59099cd9635761bb72ed92404204e12bdff08b16fb75\",\"463c19427286342120039a83218fa87ce45448e246895abac11fff0036076758\",\"03d287f655813e540ddb9c4e7aeb922478662b0f5d8e9d0cbd564b20146bab76\"],"
"\"20000004\",\"1705c739\",\"64495522\",false]}";
stratum_message message = parse_stratum_notify_message(json_string);
TEST_ASSERT_EQUAL(MINING_NOTIFY, message.method);
}

View File

@ -20,6 +20,5 @@ idf_component_register(SRCS
"crc.c"
"pretty.c"
"tcp_client.c"
"stratum_api.c"
"work_queue.c"
INCLUDE_DIRS ".")

View File

@ -1,28 +0,0 @@
#ifndef STRATUM_API_H
#define STRATUM_API_H
#include "cJSON.h"
typedef enum {
STRATUM_UNKNOWN,
MINING_NOTIFY,
MINING_SET_DIFFICULTY
} stratum_method;
typedef struct {
int id;
stratum_method method;
char * method_str;
} stratum_message;
void initialize_stratum_buffer();
char * receive_jsonrpc_line(int sockfd);
int subscribe_to_stratum(int socket);
stratum_message parse_stratum_notify_message(const char * stratum_json);
int auth_to_stratum(int socket, const char * username, const char * passwork);
#endif // STRATUM_API_H

View File

@ -22,6 +22,7 @@
#include "lwip/err.h"
#include "lwip/sockets.h"
#include "stratum_api.h"
#include "work_queue.h"
#include "system.h"
#include "serial.h"
@ -41,6 +42,32 @@ static const char *TAG = "stratum client";
TaskHandle_t sysTaskHandle = NULL;
TaskHandle_t serialTaskHandle = NULL;
static work_queue g_queue;
static void stratum_worker_task(void *pvParameters)
{
int termination_flag = 0;
while(true) {
work next_work = queue_dequeue(&g_queue, &termination_flag);
ESP_LOGI(TAG, "New Work Dequeued");
// TODO: dequeue work from work_queue
// TODO: Construct the coinbase transaction and compute the merkle root
// TODO: Prepare the block header and start mining
// TODO: Increment the nonce
// TODO: Update the block header with the new nonce
// TODO: Hash the block header using SHA256 twice (double SHA256)
// TODO: Check if the hash meets the target specified by nbits
// TODO: If hash meets target, submit shares
// snprintf(submit_msg, BUFFER_SIZE,
// "{\"id\": 4, \"method\": \"mining.submit\", \"params\": [\"%s\", \"%s\", \"%s\", \"%s\", \"%08x\"]}\n",
// user, job_id, ntime, extranonce2, nonce);
}
}
static void tcp_client_task(void *pvParameters)
{
initialize_stratum_buffer();
@ -83,12 +110,10 @@ static void tcp_client_task(void *pvParameters)
ESP_LOGE(TAG, "Socket unable to connect: errno %d", errno);
break;
}
ESP_LOGI(TAG, "Successfully connected");
auth_to_stratum(sock, "johnny9.esp");
// Authorize
char authorize_msg[] = "{\"id\": 2, \"method\": \"mining.authorize\", \"params\": [\"johnny9.esp\", \"x\"]}\n";
write(sock, authorize_msg, strlen(authorize_msg));
subscribe_to_stratum(sock);
while (1)
{
@ -100,6 +125,9 @@ static void tcp_client_task(void *pvParameters)
ESP_LOGI(TAG, "UNKNOWN MESSAGE");
} else {
ESP_LOGI(TAG, "method: %s", parsed_message.method_str);
if (parsed_message.method == MINING_NOTIFY) {
queue_enqueue(&g_queue, parsed_message.notify_work);
}
}
free(line);
}
@ -129,5 +157,9 @@ void app_main(void)
xTaskCreate(SysTask, "System_Task", 4096, NULL, 10, &sysTaskHandle);
xTaskCreate(SerialTask, "serial_test", 4096, NULL, 10, &serialTaskHandle);
xTaskCreate(tcp_client_task, "tcp_client", 4096, NULL, 5, NULL);
queue_init(&g_queue);
xTaskCreate(tcp_client_task, "tcp_client", 8192, NULL, 5, NULL);
xTaskCreate(stratum_worker_task, "miner", 8192, NULL, 5, NULL);
}

View File

@ -2,6 +2,8 @@
#define WORK_QUEUE_H
#include <pthread.h>
#include "stratum_api.h"
#define QUEUE_SIZE 10
@ -9,20 +11,6 @@
#define PREV_BLOCK_HASH_SIZE 32
#define COINBASE_SIZE 100
typedef struct {
uint32_t job_id;
uint8_t prev_block_hash[PREV_BLOCK_HASH_SIZE];
uint8_t coinbase[COINBASE_SIZE];
size_t coinbase_len;
uint8_t merkle_branches[MAX_MERKLE_BRANCHES][PREV_BLOCK_HASH_SIZE];
size_t n_merkle_branches;
uint32_t version;
uint32_t curtime;
uint32_t bits;
uint32_t target;
uint32_t nonce;
} work;
typedef struct {
work buffer[QUEUE_SIZE];
int head;

16
test/CMakeLists.txt Normal file
View File

@ -0,0 +1,16 @@
# This is the project CMakeLists.txt file for the test subproject
cmake_minimum_required(VERSION 3.16)
# Include the components directory of the main application:
#
set(EXTRA_COMPONENT_DIRS "../components")
# Set the components to include the tests for.
# This can be overriden from CMake cache:
# - when invoking CMake directly: cmake -D TEST_COMPONENTS="xxxxx" ..
# - when using idf.py: idf.py -T xxxxx build
#
set(TEST_COMPONENTS "stratum" CACHE STRING "List of components to test")
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(unit_test_stratum)

2
test/main/CMakeLists.txt Normal file
View File

@ -0,0 +1,2 @@
idf_component_register(SRCS "unit_test_stratum.c"
INCLUDE_DIRS ".")

View File

@ -0,0 +1,24 @@
#include <stdio.h>
#include <string.h>
#include "unity.h"
static void print_banner(const char* text);
void app_main(void)
{
print_banner("Running all the registered tests");
UNITY_BEGIN();
unity_run_all_tests();
UNITY_END();
print_banner("Starting interactive test menu");
/* This function will not return, and will be busy waiting for UART input.
* Make sure that task watchdog is disabled if you use this function.
*/
unity_run_menu();
}
static void print_banner(const char* text)
{
printf("\n#### %s #####\n\n", text);
}