From e366ceb336550a72c76efea4c98a2a08cca27bce Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 19 May 2026 14:25:18 +0530 Subject: feat(mining): Bitcoin mining-for-bandwidth payment system New modules: - mining_payment.c/h: hashprice calc (nbits->difficulty->sat/GH/s/day), share validation, client stats, allotment conversion (ms + bytes) - stratum_client.c/h: SV1 upstream pool connection (subscribe/authorize/submit) - stratum_proxy.c/h: Local SV1 TCP server for downstream miners, job broadcast - sw_miner.c/h: Software SHA256d miner (ESP32 CPU fallback) - asic_miner.c/h: ASIC detection stub (BM1366/BM1368 SPI) Config: - config.h/c: mining_payout_mode_t enum (auto/pool/upstream/proxy_only), stratum pool settings, mining port, hashprice override, sandbox mint access - Defaults fill nostr_seed_relays (8/8) and nostr_relays (4/4) with fast relays Integration into existing modules: - session.h/c: payment_method_t enum (CASHU/MINING/BYTES) - firewall.h/c: firewall_set_mining_port(), firewall_set_sandbox_mint_access() - tollgate_api.c: GET /mining/job, POST /mining/share, GET /mining/stats - tollgate_client.h/c: TG_CLIENT_MINING state, mining discovery tag parsing - tollgate_main.c: mining init in start_services(), stratum_client_tick() in loop - captive_portal.c: tabbed Cashu/Mine UI with live hashrate polling Unit tests (69 new assertions across 4 suites): - test_mining_payment (23 tests): nbits->difficulty, hashprice, client stats, allotment - test_stratum_proxy (21 tests): job set/get, stats, type validation - test_session_payment_method (12 tests): PAYMENT_METHOD enum, bytes/cashu methods - test_tollgate_client_mining (20 tests): mining tag parsing, discovery struct - test_firewall_sandbox (16 tests): client grant/revoke, max clients, setters Enhanced test stubs: - BaseType_t/pdPASS in freertos/task.h - lwip: sockets.h, etharp.h, prot/ip.h, prot/ip4.h, prot/tcp.h, netif.h - dns_server.h, esp_wifi_ap_get_sta_list.h Build fixes: - cvm_server.c: replace esp_timer_get_time() with xTaskGetTickCount(), fix process_relay_message() 3-arg call to 2-arg, add WS keepalive ping - stratum_proxy.c: widen task_name buffer 16->20 - sw_miner.c: add missing #include esp_random.h - nucula_src: save_proofs() moved to public in wallet.hpp Nostr relay updates: - nostr_seed_relays: +relay.anzenkodo.workers.dev, +nostr.koning-degraaf.nl, +knostr.neutrine.com, +nostr.einundzwanzig.space (8/8 slots) - nostr_relays: +relay.anzenkodo.workers.dev, +nostr.koning-degraaf.nl (4/4 slots) Squash-merge of feature/mining-payment (5 commits: c75230e..9d98ba1) --- main/tollgate_api.c | 190 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 189 insertions(+), 1 deletion(-) (limited to 'main/tollgate_api.c') diff --git a/main/tollgate_api.c b/main/tollgate_api.c index 21bf9ef..b775f55 100644 --- a/main/tollgate_api.c +++ b/main/tollgate_api.c @@ -7,6 +7,9 @@ #include "nucula_wallet.h" #include "mint_health.h" #include "market.h" +#include "mining_payment.h" +#include "stratum_proxy.h" +#include "stratum_client.h" #include "esp_log.h" #include "esp_system.h" #include "cJSON.h" @@ -150,6 +153,18 @@ static esp_err_t api_get_discovery(httpd_req_t *req) cJSON_AddItemToArray(tips_tag, cJSON_CreateString("5")); cJSON_AddItemToArray(tags, tips_tag); + if (cfg->mining_enabled) { + cJSON *mining_tag = cJSON_CreateArray(); + cJSON_AddItemToArray(mining_tag, cJSON_CreateString("price_per_step")); + cJSON_AddItemToArray(mining_tag, cJSON_CreateString("mining")); + char mining_port_str[16]; + snprintf(mining_port_str, sizeof(mining_port_str), "%d", cfg->mining_port); + cJSON_AddItemToArray(mining_tag, cJSON_CreateString(mining_port_str)); + cJSON_AddItemToArray(mining_tag, cJSON_CreateString("GH/s")); + cJSON_AddItemToArray(mining_tag, cJSON_CreateString("sv1")); + cJSON_AddItemToArray(tags, mining_tag); + } + cJSON_AddItemToObject(root, "tags", tags); cJSON_AddStringToObject(root, "content", ""); @@ -504,6 +519,169 @@ static esp_err_t api_get_mints(httpd_req_t *req) return ESP_OK; } +static esp_err_t api_get_mining_job(httpd_req_t *req) +{ + const stratum_job_t *job = stratum_proxy_get_current_job(); + if (!job || !job->valid) { + httpd_resp_set_status(req, "503 Service Unavailable"); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, "{\"error\":\"no job\"}", 15); + return ESP_OK; + } + + cJSON *root = cJSON_CreateObject(); + cJSON_AddNumberToObject(root, "job_id", job->job_id); + + char prevhash_hex[65]; + for (int i = 0; i < 32; i++) snprintf(prevhash_hex + i * 2, 3, "%02x", job->prevhash[i]); + cJSON_AddStringToObject(root, "prevhash", prevhash_hex); + + char merkle_hex[65]; + for (int i = 0; i < 32; i++) snprintf(merkle_hex + i * 2, 3, "%02x", job->merkle_root[i]); + cJSON_AddStringToObject(root, "merkle_root", merkle_hex); + + cJSON_AddNumberToObject(root, "version", job->version); + cJSON_AddNumberToObject(root, "nbits", job->nbits); + cJSON_AddNumberToObject(root, "ntime", job->ntime); + cJSON_AddNumberToObject(root, "hashprice", mining_get_current_hashprice()); + + char *json = cJSON_PrintUnformatted(root); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, json, strlen(json)); + cJSON_free(json); + cJSON_Delete(root); + return ESP_OK; +} + +static esp_err_t api_post_mining_share(httpd_req_t *req) +{ + uint32_t client_ip = 0; + get_client_ip(req, &client_ip); + + int content_len = req->content_len; + if (content_len <= 0 || content_len > 512) { + httpd_resp_set_status(req, "400 Bad Request"); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, "{\"error\":\"invalid body\"}", 21); + return ESP_OK; + } + + char body[512]; + int total = 0; + while (total < content_len) { + int r = httpd_req_recv(req, body + total, content_len - total); + if (r <= 0) { + httpd_resp_set_status(req, "400 Bad Request"); + httpd_resp_set_type(req, "text/plain"); + httpd_resp_send(req, "bad request", 11); + return ESP_OK; + } + total += r; + } + body[total] = '\0'; + + cJSON *root = cJSON_Parse(body); + if (!root) { + httpd_resp_set_status(req, "400 Bad Request"); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, "{\"error\":\"invalid json\"}", 21); + return ESP_OK; + } + + cJSON *j_job_id = cJSON_GetObjectItem(root, "job_id"); + cJSON *j_nonce = cJSON_GetObjectItem(root, "nonce"); + cJSON *j_ntime = cJSON_GetObjectItem(root, "ntime"); + cJSON *j_version = cJSON_GetObjectItem(root, "version"); + if (!j_job_id || !j_nonce || !j_ntime || !j_version) { + cJSON_Delete(root); + httpd_resp_set_status(req, "400 Bad Request"); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, "{\"error\":\"missing fields\"}", 22); + return ESP_OK; + } + + uint32_t job_id = (uint32_t)j_job_id->valuedouble; + uint32_t nonce = (uint32_t)j_nonce->valuedouble; + uint32_t ntime = (uint32_t)j_ntime->valuedouble; + uint32_t version = (uint32_t)j_version->valuedouble; + cJSON_Delete(root); + + const stratum_job_t *job = stratum_proxy_get_current_job(); + if (!job || !job->valid || job->job_id != job_id) { + httpd_resp_set_status(req, "400 Bad Request"); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, "{\"error\":\"stale job\"}", 19); + return ESP_OK; + } + + esp_err_t share_err = stratum_client_submit_share(job_id, nonce, ntime, version); + bool accepted = (share_err == ESP_OK); + + mining_update_hashrate(client_ip, accepted); + mining_client_stats_t *stats = mining_get_or_create_client(client_ip); + + if (accepted) { + const tollgate_config_t *cfg = tollgate_config_get(); + double hashprice = mining_get_current_hashprice(); + uint64_t allotment_ms = mining_shares_to_allotment_ms( + stats->hashrate_ghs, hashprice, cfg->price_per_step, cfg->step_size_ms); + + session_t *session = session_find_by_ip(client_ip); + if (!session || !session->active || session->payment_method != PAYMENT_METHOD_MINING) { + session = session_create(client_ip, allotment_ms); + if (session) session->payment_method = PAYMENT_METHOD_MINING; + } else { + session_extend(session, allotment_ms); + } + } + + cJSON *resp = cJSON_CreateObject(); + cJSON_AddBoolToObject(resp, "accepted", accepted); + cJSON_AddNumberToObject(resp, "hashrate_ghs", stats ? stats->hashrate_ghs : 0.0); + char *json = cJSON_PrintUnformatted(resp); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, json, strlen(json)); + cJSON_free(json); + cJSON_Delete(resp); + return ESP_OK; +} + +static esp_err_t api_get_mining_stats(httpd_req_t *req) +{ + stratum_proxy_stats_t proxy_stats; + stratum_proxy_get_stats(&proxy_stats); + + const stratum_client_state_t *client_state = stratum_client_get_state(); + + cJSON *root = cJSON_CreateObject(); + + cJSON *proxy = cJSON_CreateObject(); + cJSON_AddNumberToObject(proxy, "hashrate_ghs", proxy_stats.hashrate_ghs); + cJSON_AddNumberToObject(proxy, "total_shares", (double)proxy_stats.total_shares); + cJSON_AddNumberToObject(proxy, "total_accepted", (double)proxy_stats.total_accepted); + cJSON_AddNumberToObject(proxy, "total_rejected", (double)proxy_stats.total_rejected); + cJSON_AddNumberToObject(proxy, "hashprice", proxy_stats.current_hashprice); + cJSON_AddNumberToObject(proxy, "active_miners", proxy_stats.active_miners); + cJSON_AddItemToObject(root, "proxy", proxy); + + cJSON *upstream = cJSON_CreateObject(); + cJSON_AddBoolToObject(upstream, "connected", client_state->connected); + cJSON_AddStringToObject(upstream, "pool_host", client_state->pool_host); + cJSON_AddNumberToObject(upstream, "pool_port", client_state->pool_port); + cJSON_AddNumberToObject(upstream, "difficulty", (double)client_state->difficulty); + cJSON_AddNumberToObject(upstream, "shares_accepted", (double)client_state->shares_accepted); + cJSON_AddNumberToObject(upstream, "shares_rejected", (double)client_state->shares_rejected); + cJSON_AddItemToObject(root, "upstream", upstream); + + char *json = cJSON_PrintUnformatted(root); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, json, strlen(json)); + cJSON_free(json); + cJSON_Delete(root); +>>>>>>> feature/mining-payment + return ESP_OK; +} + static const httpd_uri_t uri_discovery = { .uri = "/", .method = HTTP_GET, .handler = api_get_discovery }; static const httpd_uri_t uri_payment = { .uri = "/", .method = HTTP_POST, .handler = api_post_payment }; static const httpd_uri_t uri_mints = { .uri = "/mints", .method = HTTP_GET, .handler = api_get_mints }; @@ -512,6 +690,9 @@ static const httpd_uri_t uri_whoami = { .uri = "/whoami", .method = HTTP_GET, .h static const httpd_uri_t uri_wallet = { .uri = "/wallet", .method = HTTP_GET, .handler = api_get_wallet }; static const httpd_uri_t uri_wallet_swap = { .uri = "/wallet/swap", .method = HTTP_POST, .handler = api_post_wallet_swap }; static const httpd_uri_t uri_wallet_send = { .uri = "/wallet/send", .method = HTTP_POST, .handler = api_post_wallet_send }; +static const httpd_uri_t uri_mining_job = { .uri = "/mining/job", .method = HTTP_GET, .handler = api_get_mining_job }; +static const httpd_uri_t uri_mining_share = { .uri = "/mining/share", .method = HTTP_POST, .handler = api_post_mining_share }; +static const httpd_uri_t uri_mining_stats = { .uri = "/mining/stats", .method = HTTP_GET, .handler = api_get_mining_stats }; static esp_err_t api_get_market(httpd_req_t *req) { @@ -559,7 +740,7 @@ esp_err_t tollgate_api_start(void) httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.server_port = 2121; config.ctrl_port = 32769; - config.max_uri_handlers = 12; + config.max_uri_handlers = 16; config.stack_size = 16384; esp_err_t ret = httpd_start(&s_api_server, &config); @@ -579,6 +760,13 @@ esp_err_t tollgate_api_start(void) httpd_register_uri_handler(s_api_server, &uri_wallet_send); httpd_register_uri_handler(s_api_server, &uri_market); + const tollgate_config_t *cfg = tollgate_config_get(); + if (cfg->mining_enabled) { + httpd_register_uri_handler(s_api_server, &uri_mining_job); + httpd_register_uri_handler(s_api_server, &uri_mining_share); + httpd_register_uri_handler(s_api_server, &uri_mining_stats); + } + ESP_LOGI(TAG, "TollGate API started on port 2121"); return ESP_OK; } -- cgit v1.2.3