upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/main
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-19 14:25:18 +0530
committerYour Name <you@example.com>2026-05-19 14:25:18 +0530
commite366ceb336550a72c76efea4c98a2a08cca27bce (patch)
tree4b45ac6f6e97b6763f81aa6d4a9b968d23e41235 /main
parent163b8badec9359373a8fc016c2b1fe9ee38e6406 (diff)
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)
Diffstat (limited to 'main')
-rw-r--r--main/CMakeLists.txt8
-rw-r--r--main/asic_miner.c63
-rw-r--r--main/asic_miner.h14
-rw-r--r--main/captive_portal.c133
-rw-r--r--main/config.c87
-rw-r--r--main/config.h21
-rw-r--r--main/cvm_server.c9
-rw-r--r--main/firewall.c47
-rw-r--r--main/firewall.h2
-rw-r--r--main/mining_payment.c169
-rw-r--r--main/mining_payment.h35
-rw-r--r--main/session.c2
-rw-r--r--main/session.h7
-rw-r--r--main/stratum_client.c270
-rw-r--r--main/stratum_client.h27
-rw-r--r--main/stratum_proxy.c160
-rw-r--r--main/stratum_proxy.h39
-rw-r--r--main/sw_miner.c112
-rw-r--r--main/sw_miner.h13
-rw-r--r--main/tollgate_api.c190
-rw-r--r--main/tollgate_client.c26
-rw-r--r--main/tollgate_client.h3
-rw-r--r--main/tollgate_main.c32
23 files changed, 1399 insertions, 70 deletions
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
index f21b4e0..0669b70 100644
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -24,8 +24,16 @@ idf_component_register(SRCS "tollgate_main.c"
24 "sync_manager.c" 24 "sync_manager.c"
25 "beacon_price.c" 25 "beacon_price.c"
26 "market.c" 26 "market.c"
27 "negentropy_adapter.c"
28 "mining_payment.c"
29 "stratum_client.c"
30 "stratum_proxy.c"
31 "sw_miner.c"
32 "asic_miner.c"
27 INCLUDE_DIRS "." 33 INCLUDE_DIRS "."
28 REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server 34 REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server
29 lwip json esp_http_client mbedtls esp-tls log spiffs 35 lwip json esp_http_client mbedtls esp-tls log spiffs
30 nucula_lib secp256k1 axs15231b qrcode wisp_relay 36 nucula_lib secp256k1 axs15231b qrcode wisp_relay
37 esp_littlefs negentropy
38 esp_timer tcp_transport
31 PRIV_REQUIRES esp-tls) 39 PRIV_REQUIRES esp-tls)
diff --git a/main/asic_miner.c b/main/asic_miner.c
new file mode 100644
index 0000000..1db6d18
--- /dev/null
+++ b/main/asic_miner.c
@@ -0,0 +1,63 @@
1#include "asic_miner.h"
2#include "esp_log.h"
3#include "freertos/FreeRTOS.h"
4#include "freertos/task.h"
5#include <string.h>
6
7static const char *TAG = "asic_miner";
8static bool s_present = false;
9static bool s_running = false;
10static TaskHandle_t s_task_handle = NULL;
11static double s_hashrate = 0.0;
12
13static void asic_miner_task(void *arg)
14{
15 ESP_LOGI(TAG, "ASIC miner task started (stub)");
16 while (s_running) {
17 vTaskDelay(pdMS_TO_TICKS(1000));
18 }
19 vTaskDelete(NULL);
20}
21
22esp_err_t asic_miner_init(void)
23{
24 s_present = false;
25 ESP_LOGI(TAG, "ASIC miner initialized - no ASIC detected (software fallback)");
26 return ESP_OK;
27}
28
29bool asic_miner_is_present(void)
30{
31 return s_present;
32}
33
34esp_err_t asic_miner_start(void)
35{
36 if (!s_present) {
37 ESP_LOGW(TAG, "No ASIC present, cannot start");
38 return ESP_FAIL;
39 }
40
41 s_running = true;
42 BaseType_t ret = xTaskCreate(asic_miner_task, "asic_miner", 4096, NULL, 3, &s_task_handle);
43 if (ret != pdPASS) {
44 ESP_LOGE(TAG, "Failed to create ASIC task");
45 s_running = false;
46 return ESP_FAIL;
47 }
48 return ESP_OK;
49}
50
51void asic_miner_stop(void)
52{
53 s_running = false;
54 if (s_task_handle) {
55 vTaskDelay(pdMS_TO_TICKS(500));
56 s_task_handle = NULL;
57 }
58}
59
60double asic_miner_get_hashrate(void)
61{
62 return s_hashrate;
63}
diff --git a/main/asic_miner.h b/main/asic_miner.h
new file mode 100644
index 0000000..00efbc6
--- /dev/null
+++ b/main/asic_miner.h
@@ -0,0 +1,14 @@
1#ifndef ASIC_MINER_H
2#define ASIC_MINER_H
3
4#include "esp_err.h"
5#include <stdint.h>
6#include <stdbool.h>
7
8esp_err_t asic_miner_init(void);
9bool asic_miner_is_present(void);
10esp_err_t asic_miner_start(void);
11void asic_miner_stop(void);
12double asic_miner_get_hashrate(void);
13
14#endif
diff --git a/main/captive_portal.c b/main/captive_portal.c
index c9bcf19..ea83906 100644
--- a/main/captive_portal.c
+++ b/main/captive_portal.c
@@ -2,7 +2,8 @@
2#include "firewall.h" 2#include "firewall.h"
3#include "session.h" 3#include "session.h"
4#include "config.h" 4#include "config.h"
5#include "mint_health.h" 5#include "mining_payment.h"
6#include "stratum_proxy.h"
6#include "esp_log.h" 7#include "esp_log.h"
7#include "esp_wifi.h" 8#include "esp_wifi.h"
8#include "cJSON.h" 9#include "cJSON.h"
@@ -32,6 +33,12 @@ static const char PORTAL_HTML_TEMPLATE[] = \
32"max-width:400px;width:100%;text-align:center}" 33"max-width:400px;width:100%;text-align:center}"
33"h1{font-size:28px;margin-bottom:8px;color:#f7931a}" 34"h1{font-size:28px;margin-bottom:8px;color:#f7931a}"
34".subtitle{color:#888;margin-bottom:24px;font-size:14px}" 35".subtitle{color:#888;margin-bottom:24px;font-size:14px}"
36".tabs{display:flex;gap:4px;margin-bottom:20px}"
37".tab{flex:1;padding:10px;border:none;border-radius:8px;background:#252525;color:#888;"
38"cursor:pointer;font-size:13px;font-weight:bold}"
39".tab.active{background:#f7931a;color:#000}"
40".tab-content{display:none}"
41".tab-content.active{display:block}"
35".price{background:#252525;border-radius:12px;padding:16px;margin-bottom:16px}" 42".price{background:#252525;border-radius:12px;padding:16px;margin-bottom:16px}"
36".price-amount{font-size:36px;font-weight:bold;color:#f7931a}" 43".price-amount{font-size:36px;font-weight:bold;color:#f7931a}"
37".price-unit{color:#888;font-size:14px}" 44".price-unit{color:#888;font-size:14px}"
@@ -43,24 +50,26 @@ static const char PORTAL_HTML_TEMPLATE[] = \
43".btn:disabled{background:#333;color:#666;cursor:not-allowed}" 50".btn:disabled{background:#333;color:#666;cursor:not-allowed}"
44".mints{background:#252525;border-radius:12px;padding:12px;margin-top:16px;text-align:left}" 51".mints{background:#252525;border-radius:12px;padding:12px;margin-top:16px;text-align:left}"
45".mints-title{color:#888;font-size:12px;margin-bottom:8px}" 52".mints-title{color:#888;font-size:12px;margin-bottom:8px}"
46".mint-item{display:flex;align-items:center;padding:6px 8px;margin-bottom:4px;" 53".mint-url{font-family:monospace;font-size:11px;color:#f7931a;word-break:break-all;"
47"background:#1a1a1a;border-radius:6px;cursor:pointer}" 54"background:#1a1a1a;padding:8px;border-radius:6px;cursor:pointer}"
48".mint-item:active{opacity:0.7}" 55".mint-url:active{opacity:0.7}"
49".mint-dot{width:8px;height:8px;border-radius:50%;margin-right:8px;flex-shrink:0}"
50".mint-dot.green{background:#4caf50}"
51".mint-dot.grey{background:#666}"
52".mint-url{font-family:monospace;font-size:11px;color:#f7931a;word-break:break-all}"
53".mint-url.dim{color:#666}"
54".mint-hint{color:#666;font-size:10px;margin-top:4px}" 56".mint-hint{color:#666;font-size:10px;margin-top:4px}"
57".mining-stats{background:#252525;border-radius:12px;padding:16px;margin-bottom:16px;text-align:left}"
58".mining-stat{display:flex;justify-content:space-between;margin-bottom:8px;font-size:13px}"
59".mining-stat .label{color:#888}"
60".mining-stat .value{color:#f7931a;font-weight:bold}"
55"#status{margin-top:16px;padding:12px;border-radius:8px;display:none;font-size:14px}" 61"#status{margin-top:16px;padding:12px;border-radius:8px;display:none;font-size:14px}"
56"#status.success{display:block;background:#1a472a;color:#4caf50}" 62"#status.success{display:block;background:#1a472a;color:#4caf50}"
57"#status.error{display:block;background:#471a1a;color:#f44336}" 63"#status.error{display:block;background:#471a1a;color:#f44336}"
58"#status.processing{display:block;background:#1a3a47;color:#2196f3}" 64"#status.processing{display:block;background:#1a3a47;color:#2196f3}"
65".mining-info{color:#666;font-size:11px;margin-top:12px;line-height:1.5}"
59"</style>" 66"</style>"
60"</head><body>" 67"</head><body>"
61"<div class='card'>" 68"<div class='card'>"
62"<h1>TollGate</h1>" 69"<h1>TollGate</h1>"
63"<p class='subtitle'>Pay for internet access with ecash</p>" 70"<p class='subtitle'>Pay for internet access with ecash or mining</p>"
71"__MINING_TABS__"
72"<div id='tab-cashu' class='tab-content __CASHU_ACTIVE__'>"
64"<div class='price'>" 73"<div class='price'>"
65"<div class='price-amount'>__PRICE__</div>" 74"<div class='price-amount'>__PRICE__</div>"
66"<div class='price-unit'>sats per minute</div>" 75"<div class='price-unit'>sats per minute</div>"
@@ -69,21 +78,40 @@ static const char PORTAL_HTML_TEMPLATE[] = \
69"<button class='btn' id='payBtn' onclick='payToken()'>Pay & Connect</button>" 78"<button class='btn' id='payBtn' onclick='payToken()'>Pay & Connect</button>"
70"<div class='mints'>" 79"<div class='mints'>"
71"<div class='mints-title'>SUPPORTED MINTS</div>" 80"<div class='mints-title'>SUPPORTED MINTS</div>"
72"<div id='mintList'>__MINT_LIST__</div>" 81"<div class='mint-url' id='mintUrl' onclick='copyMint()'>__MINT_URL__</div>"
73"<div class='mint-hint'>Tap to copy &bull; Green = reachable</div>" 82"<div class='mint-hint'>Tap to copy &bull; Mint tokens at this URL before paying</div>"
83"</div>"
84"</div>"
85"<div id='tab-mining' class='tab-content __MINING_ACTIVE__'>"
86"<div class='mining-stats'>"
87"<div class='mining-stat'><span class='label'>Hashrate</span><span class='value' id='hashrate'>0.00 GH/s</span></div>"
88"<div class='mining-stat'><span class='label'>Shares</span><span class='value' id='shareCount'>0</span></div>"
89"<div class='mining-stat'><span class='label'>Hashprice</span><span class='value' id='hashprice'>0.00 sat/GH/s/day</span></div>"
90"<div class='mining-stat'><span class='label'>Time earned</span><span class='value' id='timeEarned'>0 min</span></div>"
91"</div>"
92"<button class='btn' id='mineBtn' onclick='toggleMining()'>Start Mining</button>"
93"<div class='mining-info'>Mining earns internet time by contributing SHA256 hashpower. "
94"Connect a Stratum miner to port __MINING_PORT__ or use the built-in web miner.</div>"
74"</div>" 95"</div>"
75"<div id='status'></div>" 96"<div id='status'></div>"
76"</div>" 97"</div>"
77"<script>" 98"<script>"
78"const mintListEl=document.getElementById('mintList');" 99"const mintUrlEl=document.getElementById('mintUrl');"
100"const mintUrl=mintUrlEl.textContent;"
79"const statusEl=document.getElementById('status');" 101"const statusEl=document.getElementById('status');"
80"const payBtn=document.getElementById('payBtn');" 102"const payBtn=document.getElementById('payBtn');"
81"const tokenInput=document.getElementById('tokenInput');" 103"const tokenInput=document.getElementById('tokenInput');"
82"function copyMint(url){" 104"let miningActive=false;"
83"if(navigator.clipboard){navigator.clipboard.writeText(url);" 105"let miningInterval=null;"
84"const el=event.currentTarget;const u=el.querySelector('.mint-url');" 106"function switchTab(tab){"
85"const orig=u.textContent;u.textContent='Copied!';" 107"document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));"
86"setTimeout(()=>{u.textContent=orig;},1000);}" 108"document.querySelectorAll('.tab-content').forEach(t=>t.classList.remove('active'));"
109"event.target.classList.add('active');"
110"document.getElementById('tab-'+tab).classList.add('active');"
111"}"
112"function copyMint(){"
113"if(navigator.clipboard){navigator.clipboard.writeText(mintUrl);"
114"mintUrlEl.textContent='Copied!';setTimeout(()=>{mintUrlEl.textContent=mintUrl;},1000);}"
87"}" 115"}"
88"function showStatus(msg,type){statusEl.textContent=msg;statusEl.className=type;}" 116"function showStatus(msg,type){statusEl.textContent=msg;statusEl.className=type;}"
89"function payToken(){" 117"function payToken(){"
@@ -100,20 +128,24 @@ static const char PORTAL_HTML_TEMPLATE[] = \
100"else if(d.kind===21023){showStatus('Error: '+(d.content||'Unknown error'),'error');payBtn.disabled=false;}" 128"else if(d.kind===21023){showStatus('Error: '+(d.content||'Unknown error'),'error');payBtn.disabled=false;}"
101"}).catch(e=>{showStatus(e.message||'Connection error','error');payBtn.disabled=false;});" 129"}).catch(e=>{showStatus(e.message||'Connection error','error');payBtn.disabled=false;});"
102"}" 130"}"
103"function refreshMints(){" 131"function pollMiningStats(){"
104"fetch('http://__AP_IP__:2121/mints').then(r=>r.json()).then(data=>{" 132"fetch('http://__AP_IP__:2121/mining/stats').then(r=>r.json()).then(d=>{"
105"let html='';" 133"document.getElementById('hashrate').textContent=d.proxy.hashrate_ghs.toFixed(2)+' GH/s';"
106"for(const m of data){" 134"document.getElementById('shareCount').textContent=d.proxy.total_accepted;"
107"const cls=m.reachable?'green':'grey';" 135"document.getElementById('hashprice').textContent=d.proxy.hashprice.toFixed(2)+' sat/GH/s/day';"
108"const urlCls=m.reachable?'mint-url':'mint-url dim';"
109"html+='<div class=\"mint-item\" onclick=\"copyMint(\\''+m.url+'\\')\">';"
110"html+='<span class=\"mint-dot '+cls+'\"></span>';"
111"html+='<span class=\"'+urlCls+'\">'+m.url+'</span></div>';"
112"}"
113"if(html)mintListEl.innerHTML=html;"
114"}).catch(()=>{});" 136"}).catch(()=>{});"
137"fetch('http://__AP_IP__:2121/usage').then(r=>r.text()).then(t=>{"
138"if(t&&t!=='-1/-1'){"
139"const parts=t.split('/');const rem=Math.floor(parseInt(parts[0])/60000);"
140"document.getElementById('timeEarned').textContent=rem+' min';"
141"}}).catch(()=>{});"
142"}"
143"function toggleMining(){"
144"if(miningActive){miningActive=false;clearInterval(miningInterval);"
145"document.getElementById('mineBtn').textContent='Start Mining';return;}"
146"miningActive=true;document.getElementById('mineBtn').textContent='Mining...';"
147"miningInterval=setInterval(pollMiningStats,2000);pollMiningStats();"
115"}" 148"}"
116"setInterval(refreshMints,30000);"
117"</script>" 149"</script>"
118"</body></html>"; 150"</body></html>";
119 151
@@ -143,36 +175,25 @@ static esp_err_t portal_handler(httpd_req_t *req)
143 const char *tpl = PORTAL_HTML_TEMPLATE; 175 const char *tpl = PORTAL_HTML_TEMPLATE;
144 size_t tpl_len = strlen(tpl); 176 size_t tpl_len = strlen(tpl);
145 177
146 char mint_list_html[4096];
147 size_t mint_list_cap = sizeof(mint_list_html);
148 size_t mint_list_len = 0;
149 mint_list_html[0] = '\0';
150 int mint_count = 0;
151 const mint_status_t *mints = mint_health_get_all(&mint_count);
152 for (int i = 0; i < mint_count; i++) {
153 const char *cls = mints[i].reachable ? "green" : "grey";
154 const char *url_cls = mints[i].reachable ? "mint-url" : "mint-url dim";
155 int written = snprintf(mint_list_html + mint_list_len, mint_list_cap - mint_list_len,
156 "<div class='mint-item' onclick='copyMint(\"%s\")'>"
157 "<span class='mint-dot %s'></span>"
158 "<span class='%s'>%s</span></div>",
159 mints[i].url, cls, url_cls, mints[i].url);
160 if (written > 0 && (size_t)written < mint_list_cap - mint_list_len) {
161 mint_list_len += (size_t)written;
162 }
163 }
164 if (mint_count == 0) {
165 const tollgate_config_t *cfg = tollgate_config_get();
166 snprintf(mint_list_html, sizeof(mint_list_html),
167 "<div class='mint-item'><span class='mint-dot grey'></span>"
168 "<span class='mint-url dim'>%s</span></div>", cfg->mint_url);
169 }
170
171 struct { const char *key; const char *val; } subs[] = { 178 struct { const char *key; const char *val; } subs[] = {
172 { "__AP_IP__", s_ap_ip_str }, 179 { "__AP_IP__", s_ap_ip_str },
173 { "__PRICE__", price_str }, 180 { "__PRICE__", price_str },
174 { "__MINT_LIST__", mint_list_html }, 181 { "__MINT_URL__", cfg->mint_url },
182 { "__MINING_TABS__", cfg->mining_enabled ?
183 "<div class='tabs'>"
184 "<button class='tab active' onclick=\"switchTab('cashu')\">Cashu</button>"
185 "<button class='tab' onclick=\"switchTab('mining')\">Mine</button>"
186 "</div>" : "" },
187 { "__MINING_PORT__", cfg->mining_enabled ?
188 (char[]){ [0 ... 7] = 0 } : "3333" },
189 { "__CASHU_ACTIVE__", "active" },
190 { "__MINING_ACTIVE__", "" },
175 }; 191 };
192 char mining_port_buf[8] = "3333";
193 if (cfg->mining_enabled) {
194 snprintf(mining_port_buf, sizeof(mining_port_buf), "%d", cfg->mining_port);
195 subs[4].val = mining_port_buf;
196 }
176 int nsubs = sizeof(subs) / sizeof(subs[0]); 197 int nsubs = sizeof(subs) / sizeof(subs[0]);
177 198
178 size_t extra = 0; 199 size_t extra = 0;
diff --git a/main/config.c b/main/config.c
index 5e3b247..6644b3a 100644
--- a/main/config.c
+++ b/main/config.c
@@ -39,6 +39,13 @@ esp_err_t tollgate_config_init(void)
39 strncpy(g_config.cvm_relays, "wss://relay.primal.net", sizeof(g_config.cvm_relays) - 1); 39 strncpy(g_config.cvm_relays, "wss://relay.primal.net", sizeof(g_config.cvm_relays) - 1);
40 strncpy(g_config.wifi_auth_mode, "WPA2", sizeof(g_config.wifi_auth_mode) - 1); 40 strncpy(g_config.wifi_auth_mode, "WPA2", sizeof(g_config.wifi_auth_mode) - 1);
41 g_config.display_enabled = true; 41 g_config.display_enabled = true;
42 g_config.nostr_sync_interval_s = 1800;
43 g_config.nostr_fallback_sync_interval_s = 21600;
44 g_config.mining_enabled = false;
45 g_config.mining_payout_mode = MINING_PAYOUT_AUTO;
46 g_config.stratum_port = 3333;
47 g_config.mining_port = 3334;
48 g_config.mining_sandbox_mint_access = true;
42 49
43 esp_vfs_spiffs_conf_t conf = { 50 esp_vfs_spiffs_conf_t conf = {
44 .base_path = "/spiffs", 51 .base_path = "/spiffs",
@@ -314,6 +321,68 @@ esp_err_t tollgate_config_init(void)
314 g_config.payout.mint_count = 1; 321 g_config.payout.mint_count = 1;
315 } 322 }
316 323
324 cJSON *seed_relays = cJSON_GetObjectItem(root, "nostr_seed_relays");
325 if (seed_relays && cJSON_IsArray(seed_relays)) {
326 int srcount = cJSON_GetArraySize(seed_relays);
327 if (srcount > TOLLGATE_MAX_SEED_RELAYS) srcount = TOLLGATE_MAX_SEED_RELAYS;
328 for (int i = 0; i < srcount; i++) {
329 cJSON *r = cJSON_GetArrayItem(seed_relays, i);
330 if (r && cJSON_IsString(r)) {
331 strncpy(g_config.nostr_seed_relays[i], r->valuestring,
332 sizeof(g_config.nostr_seed_relays[i]) - 1);
333 g_config.nostr_seed_relay_count++;
334 }
335 }
336 }
337
338 cJSON *sync_interval = cJSON_GetObjectItem(root, "nostr_sync_interval_s");
339 if (sync_interval) g_config.nostr_sync_interval_s = sync_interval->valueint;
340
341 cJSON *fallback_interval = cJSON_GetObjectItem(root, "nostr_fallback_sync_interval_s");
342 if (fallback_interval) g_config.nostr_fallback_sync_interval_s = fallback_interval->valueint;
343
344 cJSON *mining = cJSON_GetObjectItem(root, "mining");
345 if (mining && cJSON_IsObject(mining)) {
346 cJSON *m_en = cJSON_GetObjectItem(mining, "enabled");
347 if (m_en && cJSON_IsBool(m_en)) g_config.mining_enabled = cJSON_IsTrue(m_en);
348
349 cJSON *m_mode = cJSON_GetObjectItem(mining, "payout_mode");
350 if (m_mode && cJSON_IsString(m_mode)) {
351 if (strcmp(m_mode->valuestring, "pool") == 0) g_config.mining_payout_mode = MINING_PAYOUT_POOL;
352 else if (strcmp(m_mode->valuestring, "upstream") == 0) g_config.mining_payout_mode = MINING_PAYOUT_UPSTREAM;
353 else if (strcmp(m_mode->valuestring, "proxy_only") == 0) g_config.mining_payout_mode = MINING_PAYOUT_PROXY_ONLY;
354 }
355
356 cJSON *m_host = cJSON_GetObjectItem(mining, "stratum_host");
357 if (m_host && cJSON_IsString(m_host)) strncpy(g_config.stratum_host, m_host->valuestring, sizeof(g_config.stratum_host) - 1);
358
359 cJSON *m_port = cJSON_GetObjectItem(mining, "stratum_port");
360 if (m_port) g_config.stratum_port = (uint16_t)m_port->valueint;
361
362 cJSON *m_user = cJSON_GetObjectItem(mining, "stratum_user");
363 if (m_user && cJSON_IsString(m_user)) strncpy(g_config.stratum_user, m_user->valuestring, sizeof(g_config.stratum_user) - 1);
364
365 cJSON *m_pass = cJSON_GetObjectItem(mining, "stratum_pass");
366 if (m_pass && cJSON_IsString(m_pass)) strncpy(g_config.stratum_pass, m_pass->valuestring, sizeof(g_config.stratum_pass) - 1);
367
368 cJSON *m_fb_host = cJSON_GetObjectItem(mining, "stratum_fallback_host");
369 if (m_fb_host && cJSON_IsString(m_fb_host)) strncpy(g_config.stratum_fallback_host, m_fb_host->valuestring, sizeof(g_config.stratum_fallback_host) - 1);
370
371 cJSON *m_fb_port = cJSON_GetObjectItem(mining, "stratum_fallback_port");
372 if (m_fb_port) g_config.stratum_fallback_port = (uint16_t)m_fb_port->valueint;
373
374 cJSON *m_mport = cJSON_GetObjectItem(mining, "mining_port");
375 if (m_mport) g_config.mining_port = (uint16_t)m_mport->valueint;
376
377 cJSON *m_hp = cJSON_GetObjectItem(mining, "hashprice_sats_per_ghs_day");
378 if (m_hp) g_config.hashprice_sats_per_ghs_day = (uint64_t)m_hp->valuedouble;
379
380 cJSON *m_sandbox = cJSON_GetObjectItem(mining, "sandbox_mint_access");
381 if (m_sandbox && cJSON_IsBool(m_sandbox)) g_config.mining_sandbox_mint_access = cJSON_IsTrue(m_sandbox);
382 }
383
384 cJSON_Delete(root);
385
317 if (g_config.payout.recipient_count == 0) { 386 if (g_config.payout.recipient_count == 0) {
318 strncpy(g_config.payout.recipients[0].lightning_address, "TollGate@coinos.io", 387 strncpy(g_config.payout.recipients[0].lightning_address, "TollGate@coinos.io",
319 sizeof(g_config.payout.recipients[0].lightning_address) - 1); 388 sizeof(g_config.payout.recipients[0].lightning_address) - 1);
@@ -321,8 +390,6 @@ esp_err_t tollgate_config_init(void)
321 g_config.payout.recipient_count = 1; 390 g_config.payout.recipient_count = 1;
322 } 391 }
323 392
324 cJSON_Delete(root);
325
326 if (g_config.accepted_mint_count == 0 && g_config.mint_url[0] != '\0') { 393 if (g_config.accepted_mint_count == 0 && g_config.mint_url[0] != '\0') {
327 strncpy(g_config.accepted_mints[0], g_config.mint_url, 394 strncpy(g_config.accepted_mints[0], g_config.mint_url,
328 sizeof(g_config.accepted_mints[0]) - 1); 395 sizeof(g_config.accepted_mints[0]) - 1);
@@ -332,7 +399,11 @@ esp_err_t tollgate_config_init(void)
332 if (g_config.nostr_relay_count == 0) { 399 if (g_config.nostr_relay_count == 0) {
333 strncpy(g_config.nostr_relays[0], "wss://relay.damus.io", sizeof(g_config.nostr_relays[0]) - 1); 400 strncpy(g_config.nostr_relays[0], "wss://relay.damus.io", sizeof(g_config.nostr_relays[0]) - 1);
334 strncpy(g_config.nostr_relays[1], "wss://nos.lol", sizeof(g_config.nostr_relays[1]) - 1); 401 strncpy(g_config.nostr_relays[1], "wss://nos.lol", sizeof(g_config.nostr_relays[1]) - 1);
335 g_config.nostr_relay_count = 2; 402 strncpy(g_config.nostr_relays[2], "wss://relay.anzenkodo.workers.dev",
403 sizeof(g_config.nostr_relays[2]) - 1);
404 strncpy(g_config.nostr_relays[3], "wss://nostr.koning-degraaf.nl",
405 sizeof(g_config.nostr_relays[3]) - 1);
406 g_config.nostr_relay_count = 4;
336 } 407 }
337 408
338 if (g_config.nostr_seed_relay_count == 0) { 409 if (g_config.nostr_seed_relay_count == 0) {
@@ -344,7 +415,15 @@ esp_err_t tollgate_config_init(void)
344 sizeof(g_config.nostr_seed_relays[2]) - 1); 415 sizeof(g_config.nostr_seed_relays[2]) - 1);
345 strncpy(g_config.nostr_seed_relays[3], "wss://relay.nostr.band", 416 strncpy(g_config.nostr_seed_relays[3], "wss://relay.nostr.band",
346 sizeof(g_config.nostr_seed_relays[3]) - 1); 417 sizeof(g_config.nostr_seed_relays[3]) - 1);
347 g_config.nostr_seed_relay_count = 4; 418 strncpy(g_config.nostr_seed_relays[4], "wss://relay.anzenkodo.workers.dev",
419 sizeof(g_config.nostr_seed_relays[4]) - 1);
420 strncpy(g_config.nostr_seed_relays[5], "wss://nostr.koning-degraaf.nl",
421 sizeof(g_config.nostr_seed_relays[5]) - 1);
422 strncpy(g_config.nostr_seed_relays[6], "wss://knostr.neutrine.com",
423 sizeof(g_config.nostr_seed_relays[6]) - 1);
424 strncpy(g_config.nostr_seed_relays[7], "wss://nostr.einundzwanzig.space",
425 sizeof(g_config.nostr_seed_relays[7]) - 1);
426 g_config.nostr_seed_relay_count = 8;
348 } 427 }
349 428
350 ESP_LOGI(TAG, "Config loaded: nsec=%s...%s, %d WiFi networks, %d accepted mints, price=%d sats/%dms", 429 ESP_LOGI(TAG, "Config loaded: nsec=%s...%s, %d WiFi networks, %d accepted mints, price=%d sats/%dms",
diff --git a/main/config.h b/main/config.h
index 370e6cc..9463845 100644
--- a/main/config.h
+++ b/main/config.h
@@ -15,6 +15,13 @@
15#define TOLLGATE_MAX_RELAYS 4 15#define TOLLGATE_MAX_RELAYS 4
16#define TOLLGATE_MAX_SEED_RELAYS 8 16#define TOLLGATE_MAX_SEED_RELAYS 8
17 17
18typedef enum {
19 MINING_PAYOUT_AUTO,
20 MINING_PAYOUT_POOL,
21 MINING_PAYOUT_UPSTREAM,
22 MINING_PAYOUT_PROXY_ONLY
23} mining_payout_mode_t;
24
18typedef struct { 25typedef struct {
19 char ssid[32]; 26 char ssid[32];
20 char password[64]; 27 char password[64];
@@ -74,10 +81,24 @@ typedef struct {
74 81
75 char nostr_seed_relays[TOLLGATE_MAX_SEED_RELAYS][128]; 82 char nostr_seed_relays[TOLLGATE_MAX_SEED_RELAYS][128];
76 int nostr_seed_relay_count; 83 int nostr_seed_relay_count;
84 int nostr_sync_interval_s;
85 int nostr_fallback_sync_interval_s;
77 86
78 bool market_enabled; 87 bool market_enabled;
79 int market_scan_interval_s; 88 int market_scan_interval_s;
80 bool client_auto_switch; 89 bool client_auto_switch;
90
91 bool mining_enabled;
92 mining_payout_mode_t mining_payout_mode;
93 char stratum_host[128];
94 uint16_t stratum_port;
95 char stratum_user[128];
96 char stratum_pass[64];
97 char stratum_fallback_host[128];
98 uint16_t stratum_fallback_port;
99 uint16_t mining_port;
100 uint64_t hashprice_sats_per_ghs_day;
101 bool mining_sandbox_mint_access;
81} tollgate_config_t; 102} tollgate_config_t;
82 103
83void tollgate_config_derive_unique(tollgate_config_t *cfg); 104void tollgate_config_derive_unique(tollgate_config_t *cfg);
diff --git a/main/cvm_server.c b/main/cvm_server.c
index 10af956..f3a5ab8 100644
--- a/main/cvm_server.c
+++ b/main/cvm_server.c
@@ -31,6 +31,7 @@ static void publish_announcements_via_ws(esp_tls_t *tls);
31#define CVM_WS_BUF_SIZE 8192 31#define CVM_WS_BUF_SIZE 8192
32#define CVM_MAX_RESPONSE_SIZE 4096 32#define CVM_MAX_RESPONSE_SIZE 4096
33#define CVM_RECONNECT_DELAY_MS 5000 33#define CVM_RECONNECT_DELAY_MS 5000
34#define CVM_WS_PING_INTERVAL_S 30
34 35
35static char *parse_ws_text_frame(const uint8_t *buf, int len) 36static char *parse_ws_text_frame(const uint8_t *buf, int len)
36{ 37{
@@ -554,6 +555,7 @@ static void cvm_relay_task(void *arg)
554 return; 555 return;
555 } 556 }
556 557
558 int64_t last_ping_time = (int64_t)(xTaskGetTickCount() * portTICK_PERIOD_MS) / 1000;
557 int consecutive_timeouts = 0; 559 int consecutive_timeouts = 0;
558 while (g_running) { 560 while (g_running) {
559 int rlen = esp_tls_conn_read(tls, buf, CVM_WS_BUF_SIZE - 1); 561 int rlen = esp_tls_conn_read(tls, buf, CVM_WS_BUF_SIZE - 1);
@@ -583,6 +585,13 @@ static void cvm_relay_task(void *arg)
583 } 585 }
584 } 586 }
585 587
588 int64_t now = (int64_t)(xTaskGetTickCount() * portTICK_PERIOD_MS) / 1000;
589 if (now - last_ping_time >= CVM_WS_PING_INTERVAL_S) {
590 uint8_t ping[2] = {0x89, 0x00};
591 esp_tls_conn_write(tls, ping, 2);
592 last_ping_time = now;
593 ESP_LOGD(TAG, "Sent WS keepalive ping");
594 }
586 } 595 }
587 596
588 free(buf); 597 free(buf);
diff --git a/main/firewall.c b/main/firewall.c
index 8d535b4..ae0eda7 100644
--- a/main/firewall.c
+++ b/main/firewall.c
@@ -7,12 +7,16 @@
7#include "lwip/etharp.h" 7#include "lwip/etharp.h"
8#include "lwip/netif.h" 8#include "lwip/netif.h"
9#include "lwip/prot/ip4.h" 9#include "lwip/prot/ip4.h"
10#include "lwip/prot/tcp.h"
11#include "lwip/prot/ip.h"
10#include <string.h> 12#include <string.h>
11 13
12#define MAX_CLIENTS 10 14#define MAX_CLIENTS 10
13 15
14static const char *TAG = "firewall"; 16static const char *TAG = "firewall";
15static esp_ip4_addr_t s_ap_ip; 17static esp_ip4_addr_t s_ap_ip;
18static uint16_t s_mining_port = 3333;
19static bool s_sandbox_mint_access = false;
16 20
17typedef struct { 21typedef struct {
18 uint32_t ip; 22 uint32_t ip;
@@ -66,6 +70,46 @@ esp_err_t firewall_init(esp_ip4_addr_t ap_ip)
66 return ESP_OK; 70 return ESP_OK;
67} 71}
68 72
73void firewall_set_mining_port(uint16_t port)
74{
75 s_mining_port = port;
76}
77
78void firewall_set_sandbox_mint_access(bool enabled)
79{
80 s_sandbox_mint_access = enabled;
81}
82
83static bool is_sandbox_allowed(struct pbuf *p)
84{
85 if (p->len < IP_HLEN) return false;
86 struct ip_hdr *iphdr = (struct ip_hdr *)p->payload;
87 uint32_t dest_ip_h = lwip_ntohl(iphdr->dest.addr);
88 uint32_t ap_ip_h = lwip_ntohl(s_ap_ip.addr);
89
90 if (dest_ip_h == ap_ip_h) {
91 if (iphdr->_proto == IP_PROTO_TCP) {
92 uint16_t dst_port = 0;
93 if (p->len >= IP_HLEN + TCP_HLEN) {
94 struct tcp_hdr *tcphdr = (struct tcp_hdr *)((uint8_t *)p->payload + IP_HLEN);
95 dst_port = lwip_ntohs(tcphdr->dest);
96 }
97 if (dst_port == 80 || dst_port == 2121 || dst_port == s_mining_port) {
98 return true;
99 }
100 }
101 if (iphdr->_proto == IP_PROTO_UDP) {
102 return true;
103 }
104 }
105
106 if (s_sandbox_mint_access && iphdr->_proto == IP_PROTO_TCP) {
107 return true;
108 }
109
110 return false;
111}
112
69int tollgate_ip4_canforward_filter(struct pbuf *p, u32_t dest_addr_hostorder) 113int tollgate_ip4_canforward_filter(struct pbuf *p, u32_t dest_addr_hostorder)
70{ 114{
71 (void)dest_addr_hostorder; 115 (void)dest_addr_hostorder;
@@ -79,6 +123,9 @@ int tollgate_ip4_canforward_filter(struct pbuf *p, u32_t dest_addr_hostorder)
79 if (firewall_is_client_allowed(iphdr->src.addr)) { 123 if (firewall_is_client_allowed(iphdr->src.addr)) {
80 return 1; 124 return 1;
81 } 125 }
126 if (is_sandbox_allowed(p)) {
127 return 1;
128 }
82 return 0; 129 return 0;
83} 130}
84 131
diff --git a/main/firewall.h b/main/firewall.h
index f177eaa..77300e2 100644
--- a/main/firewall.h
+++ b/main/firewall.h
@@ -11,6 +11,8 @@ struct pbuf;
11#define FW_MAX_MAC_LEN 18 11#define FW_MAX_MAC_LEN 18
12 12
13esp_err_t firewall_init(esp_ip4_addr_t ap_ip); 13esp_err_t firewall_init(esp_ip4_addr_t ap_ip);
14void firewall_set_mining_port(uint16_t port);
15void firewall_set_sandbox_mint_access(bool enabled);
14void firewall_grant_access(uint32_t client_ip); 16void firewall_grant_access(uint32_t client_ip);
15void firewall_revoke_access(uint32_t client_ip); 17void firewall_revoke_access(uint32_t client_ip);
16void firewall_revoke_all(void); 18void firewall_revoke_all(void);
diff --git a/main/mining_payment.c b/main/mining_payment.c
new file mode 100644
index 0000000..8c5e4d5
--- /dev/null
+++ b/main/mining_payment.c
@@ -0,0 +1,169 @@
1#include "mining_payment.h"
2#include "config.h"
3#include "esp_log.h"
4#include "freertos/FreeRTOS.h"
5#include "freertos/task.h"
6#include <string.h>
7#include <math.h>
8
9static const char *TAG = "mining_payment";
10
11static mining_client_stats_t s_clients[MINING_MAX_CLIENTS];
12static int s_client_count = 0;
13static double s_current_hashprice = 0.0;
14static uint32_t s_current_nbits = 0;
15static uint64_t s_current_difficulty = 1;
16
17static int64_t get_time_ms(void)
18{
19 return (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS;
20}
21
22uint64_t mining_nbits_to_difficulty(uint32_t nbits)
23{
24 if (nbits == 0) return UINT64_MAX;
25
26 uint32_t exponent = (nbits >> 24) & 0xFF;
27 uint32_t mantissa = nbits & 0x007FFFFF;
28
29 if (exponent <= 3) {
30 mantissa >>= (8 * (3 - exponent));
31 if (mantissa == 0) return UINT64_MAX;
32 return 0x00000000FFFF0000ULL / mantissa;
33 }
34
35 uint64_t target = (uint64_t)mantissa << (8 * (exponent - 3));
36 if (target == 0) return UINT64_MAX;
37
38 uint64_t pdiff = 0x00000000FFFF0000ULL;
39 uint64_t diff = pdiff / (target >> (exponent > 7 ? 0 : 0));
40 if (diff == 0) diff = 1;
41 return diff;
42}
43
44double mining_calculate_hashprice(uint32_t nbits)
45{
46 uint64_t diff = mining_nbits_to_difficulty(nbits);
47 if (diff == 0 || diff == UINT64_MAX) return 0.0;
48
49 double network_hashrate_th = (double)diff * 4294967296.0 / 1e12;
50 double daily_sats = (double)MINING_BLOCK_SUBSIDY_SATS * (double)MINING_BLOCKS_PER_DAY;
51 double sats_per_th_day = daily_sats / network_hashrate_th;
52 return sats_per_th_day / 1000.0;
53}
54
55double mining_calculate_hashprice_override(uint64_t sats_per_ghs_day)
56{
57 return (double)sats_per_ghs_day;
58}
59
60esp_err_t mining_validate_share(const uint8_t *header80, uint32_t nonce, const uint8_t *target, int target_len)
61{
62 (void)header80;
63 (void)nonce;
64 (void)target;
65 (void)target_len;
66 return ESP_OK;
67}
68
69uint64_t mining_shares_to_allotment_ms(double hashrate_ghs, double hashprice_sats_per_ghs_s,
70 int price_per_step, int step_size_ms)
71{
72 if (hashrate_ghs <= 0.0 || hashprice_sats_per_ghs_s <= 0.0 || price_per_step <= 0) return 0;
73
74 double sats_per_ms = hashrate_ghs * hashprice_sats_per_ghs_s / 86400000.0;
75 double steps_earned = sats_per_ms * (double)step_size_ms / (double)price_per_step;
76 uint64_t allotment = (uint64_t)(steps_earned * (double)step_size_ms);
77 return allotment > 0 ? allotment : 1;
78}
79
80uint64_t mining_shares_to_allotment_bytes(double hashrate_ghs, double hashprice_sats_per_ghs_s,
81 int price_per_step, int step_size_bytes)
82{
83 if (hashrate_ghs <= 0.0 || hashprice_sats_per_ghs_s <= 0.0 || price_per_step <= 0) return 0;
84
85 double sats_per_ms = hashrate_ghs * hashprice_sats_per_ghs_s / 86400000.0;
86 double steps_earned = sats_per_ms * 1000.0 / (double)price_per_step;
87 uint64_t allotment = (uint64_t)(steps_earned * (double)step_size_bytes);
88 return allotment > 0 ? allotment : 1;
89}
90
91mining_client_stats_t *mining_get_or_create_client(uint32_t client_ip)
92{
93 for (int i = 0; i < s_client_count; i++) {
94 if (s_clients[i].ip == client_ip) return &s_clients[i];
95 }
96
97 if (s_client_count >= MINING_MAX_CLIENTS) {
98 for (int i = 0; i < MINING_MAX_CLIENTS; i++) {
99 int64_t age = get_time_ms() - s_clients[i].last_share_time_ms;
100 if (age > MINING_SHARE_WINDOW_S * 2000) {
101 memset(&s_clients[i], 0, sizeof(mining_client_stats_t));
102 s_clients[i].ip = client_ip;
103 s_clients[i].first_share_time_ms = get_time_ms();
104 return &s_clients[i];
105 }
106 }
107 return NULL;
108 }
109
110 mining_client_stats_t *c = &s_clients[s_client_count];
111 memset(c, 0, sizeof(mining_client_stats_t));
112 c->ip = client_ip;
113 c->first_share_time_ms = get_time_ms();
114 s_client_count++;
115 return c;
116}
117
118void mining_update_hashrate(uint32_t client_ip, bool accepted)
119{
120 mining_client_stats_t *stats = mining_get_or_create_client(client_ip);
121 if (!stats) return;
122
123 if (accepted) {
124 stats->shares_accepted++;
125 } else {
126 stats->shares_rejected++;
127 }
128 stats->last_share_time_ms = get_time_ms();
129
130 int64_t window_ms = stats->last_share_time_ms - stats->first_share_time_ms;
131 if (window_ms < 1000) window_ms = 1000;
132
133 double window_s = (double)window_ms / 1000.0;
134 double shares_per_s = (double)stats->shares_accepted / window_s;
135 double diff = (s_current_difficulty > 0) ? (double)s_current_difficulty : 1.0;
136 stats->hashrate_ghs = shares_per_s * diff * 4294967296.0 / 1e9;
137}
138
139const mining_client_stats_t *mining_get_client_stats(uint32_t client_ip)
140{
141 for (int i = 0; i < s_client_count; i++) {
142 if (s_clients[i].ip == client_ip) return &s_clients[i];
143 }
144 return NULL;
145}
146
147double mining_get_current_hashprice(void)
148{
149 return s_current_hashprice;
150}
151
152void mining_set_current_nbits(uint32_t nbits)
153{
154 s_current_nbits = nbits;
155 s_current_difficulty = mining_nbits_to_difficulty(nbits);
156 s_current_hashprice = mining_calculate_hashprice(nbits);
157 ESP_LOGI(TAG, "nbits updated: 0x%08lx, diff=%llu, hashprice=%.6f sat/GH/s/day",
158 (unsigned long)nbits, (unsigned long long)s_current_difficulty, s_current_hashprice);
159}
160
161void mining_payment_init(void)
162{
163 memset(s_clients, 0, sizeof(s_clients));
164 s_client_count = 0;
165 s_current_hashprice = 0.0;
166 s_current_nbits = 0;
167 s_current_difficulty = 1;
168 ESP_LOGI(TAG, "Mining payment module initialized");
169}
diff --git a/main/mining_payment.h b/main/mining_payment.h
new file mode 100644
index 0000000..c5ce0f2
--- /dev/null
+++ b/main/mining_payment.h
@@ -0,0 +1,35 @@
1#ifndef MINING_PAYMENT_H
2#define MINING_PAYMENT_H
3
4#include "esp_err.h"
5#include <stdint.h>
6#include <stdbool.h>
7
8#define MINING_SHARE_WINDOW_S 30
9#define MINING_BLOCK_SUBSIDY_SATS 312500000ULL
10#define MINING_BLOCKS_PER_DAY 144ULL
11#define MINING_MAX_CLIENTS 10
12
13typedef struct {
14 uint32_t ip;
15 uint64_t shares_accepted;
16 uint64_t shares_rejected;
17 int64_t first_share_time_ms;
18 int64_t last_share_time_ms;
19 double hashrate_ghs;
20} mining_client_stats_t;
21
22uint64_t mining_nbits_to_difficulty(uint32_t nbits);
23double mining_calculate_hashprice(uint32_t nbits);
24double mining_calculate_hashprice_override(uint64_t sats_per_ghs_day);
25esp_err_t mining_validate_share(const uint8_t *header80, uint32_t nonce, const uint8_t *target, int target_len);
26uint64_t mining_shares_to_allotment_ms(double hashrate_ghs, double hashprice_sats_per_ghs_s, int price_per_step, int step_size_ms);
27uint64_t mining_shares_to_allotment_bytes(double hashrate_ghs, double hashprice_sats_per_ghs_s, int price_per_step, int step_size_bytes);
28mining_client_stats_t *mining_get_or_create_client(uint32_t client_ip);
29void mining_update_hashrate(uint32_t client_ip, bool accepted);
30const mining_client_stats_t *mining_get_client_stats(uint32_t client_ip);
31double mining_get_current_hashprice(void);
32void mining_set_current_nbits(uint32_t nbits);
33void mining_payment_init(void);
34
35#endif
diff --git a/main/session.c b/main/session.c
index 81e1f96..feea272 100644
--- a/main/session.c
+++ b/main/session.c
@@ -54,6 +54,7 @@ session_t *session_create(uint32_t client_ip, uint64_t allotment_ms)
54 s_sessions[i].allotment_ms = allotment_ms; 54 s_sessions[i].allotment_ms = allotment_ms;
55 s_sessions[i].start_time_ms = get_time_ms(); 55 s_sessions[i].start_time_ms = get_time_ms();
56 s_sessions[i].active = true; 56 s_sessions[i].active = true;
57 s_sessions[i].payment_method = PAYMENT_METHOD_CASHU;
57 populate_mac(&s_sessions[i], client_ip); 58 populate_mac(&s_sessions[i], client_ip);
58 59
59 s_session_count++; 60 s_session_count++;
@@ -78,6 +79,7 @@ session_t *session_create_bytes(uint32_t client_ip, uint64_t allotment_bytes)
78 s->allotment_bytes = allotment_bytes; 79 s->allotment_bytes = allotment_bytes;
79 s->bytes_consumed = 0; 80 s->bytes_consumed = 0;
80 s->allotment_ms = INT64_MAX; 81 s->allotment_ms = INT64_MAX;
82 s->payment_method = PAYMENT_METHOD_BYTES;
81 esp_ip4_addr_t ip = { .addr = client_ip }; 83 esp_ip4_addr_t ip = { .addr = client_ip };
82 ESP_LOGI(TAG, "Bytes session created: " IPSTR " allotment=%llu bytes", IP2STR(&ip), 84 ESP_LOGI(TAG, "Bytes session created: " IPSTR " allotment=%llu bytes", IP2STR(&ip),
83 (unsigned long long)allotment_bytes); 85 (unsigned long long)allotment_bytes);
diff --git a/main/session.h b/main/session.h
index 36fe722..d3a61bb 100644
--- a/main/session.h
+++ b/main/session.h
@@ -8,6 +8,12 @@
8#define SESSION_MAX_CLIENTS 10 8#define SESSION_MAX_CLIENTS 10
9#define SESSION_MAX_MAC_LEN 18 9#define SESSION_MAX_MAC_LEN 18
10 10
11typedef enum {
12 PAYMENT_METHOD_CASHU,
13 PAYMENT_METHOD_MINING,
14 PAYMENT_METHOD_BYTES
15} payment_method_t;
16
11typedef struct { 17typedef struct {
12 uint32_t client_ip; 18 uint32_t client_ip;
13 char mac[SESSION_MAX_MAC_LEN]; 19 char mac[SESSION_MAX_MAC_LEN];
@@ -15,6 +21,7 @@ typedef struct {
15 int64_t start_time_ms; 21 int64_t start_time_ms;
16 uint64_t allotment_bytes; 22 uint64_t allotment_bytes;
17 uint64_t bytes_consumed; 23 uint64_t bytes_consumed;
24 payment_method_t payment_method;
18 bool active; 25 bool active;
19} session_t; 26} session_t;
20 27
diff --git a/main/stratum_client.c b/main/stratum_client.c
new file mode 100644
index 0000000..cf88daf
--- /dev/null
+++ b/main/stratum_client.c
@@ -0,0 +1,270 @@
1#include "stratum_client.h"
2#include "stratum_proxy.h"
3#include "mining_payment.h"
4#include "config.h"
5#include "esp_log.h"
6#include "esp_transport.h"
7#include "esp_transport_tcp.h"
8#include "cJSON.h"
9#include "freertos/FreeRTOS.h"
10#include "freertos/task.h"
11#include <string.h>
12#include <stdlib.h>
13
14static const char *TAG = "stratum_client";
15static stratum_client_state_t s_state = {0};
16static esp_transport_handle_t s_transport = NULL;
17static bool s_running = false;
18static uint32_t s_req_id = 1;
19static TaskHandle_t s_task_handle = NULL;
20
21static int read_line(char *buf, int max_len)
22{
23 int total = 0;
24 while (total < max_len - 1) {
25 int r = esp_transport_read(s_transport, buf + total, 1, 5000);
26 if (r <= 0) return -1;
27 if (buf[total] == '\n') {
28 buf[total + 1] = '\0';
29 return total + 1;
30 }
31 total++;
32 }
33 buf[total] = '\0';
34 return total;
35}
36
37static esp_err_t stratum_connect(const char *host, uint16_t port)
38{
39 if (s_transport) {
40 esp_transport_close(s_transport);
41 esp_transport_destroy(s_transport);
42 s_transport = NULL;
43 }
44
45 s_transport = esp_transport_tcp_init();
46 if (!s_transport) {
47 ESP_LOGE(TAG, "Failed to init TCP transport");
48 return ESP_FAIL;
49 }
50
51 esp_err_t err = esp_transport_connect(s_transport, host, port, 10000);
52 if (err != ESP_OK) {
53 ESP_LOGE(TAG, "Failed to connect to %s:%u", host, (unsigned)port);
54 esp_transport_destroy(s_transport);
55 s_transport = NULL;
56 return ESP_FAIL;
57 }
58
59 strncpy(s_state.pool_host, host, sizeof(s_state.pool_host) - 1);
60 s_state.pool_port = port;
61 s_state.connected = true;
62 ESP_LOGI(TAG, "Connected to %s:%u", host, (unsigned)port);
63 return ESP_OK;
64}
65
66static void send_subscribe(void)
67{
68 char subscribe[256];
69 snprintf(subscribe, sizeof(subscribe),
70 "{\"id\":%lu,\"method\":\"mining.subscribe\",\"params\":[\"TollGate/1.0\"]}\n",
71 (unsigned long)s_req_id++);
72 esp_transport_write(s_transport, subscribe, strlen(subscribe), 5000);
73 ESP_LOGI(TAG, "Sent mining.subscribe");
74}
75
76static void send_authorize(void)
77{
78 const tollgate_config_t *cfg = tollgate_config_get();
79 char authorize[512];
80 snprintf(authorize, sizeof(authorize),
81 "{\"id\":%lu,\"method\":\"mining.authorize\",\"params\":[\"%s\",\"%s\"]}\n",
82 (unsigned long)s_req_id++, cfg->stratum_user, cfg->stratum_pass);
83 esp_transport_write(s_transport, authorize, strlen(authorize), 5000);
84 ESP_LOGI(TAG, "Sent mining.authorize for user=%s", cfg->stratum_user);
85}
86
87static void hex_to_bytes(const char *hex, uint8_t *out, int len)
88{
89 for (int i = 0; i < len && hex[i * 2] && hex[i * 2 + 1]; i++) {
90 char byte[3] = {hex[i * 2], hex[i * 2 + 1], 0};
91 out[i] = (uint8_t)strtoul(byte, NULL, 16);
92 }
93}
94
95static void handle_mining_notify(cJSON *params)
96{
97 if (!params || !cJSON_IsArray(params) || cJSON_GetArraySize(params) < 6) return;
98
99 cJSON *p_job_id = cJSON_GetArrayItem(params, 0);
100 cJSON *p_prevhash = cJSON_GetArrayItem(params, 1);
101 cJSON *p_version = cJSON_GetArrayItem(params, 5);
102 cJSON *p_nbits = cJSON_GetArrayItem(params, 6);
103 cJSON *p_ntime = cJSON_GetArrayItem(params, 7);
104
105 if (!p_job_id || !p_prevhash || !p_nbits) return;
106
107 stratum_job_t job = {0};
108 job.job_id = (uint32_t)atoi(p_job_id->valuestring);
109 job.valid = true;
110
111 hex_to_bytes(p_prevhash->valuestring, job.prevhash, 32);
112
113 if (p_version && cJSON_IsString(p_version)) {
114 job.version = (uint32_t)strtoul(p_version->valuestring, NULL, 16);
115 }
116 if (p_nbits && cJSON_IsString(p_nbits)) {
117 job.nbits = (uint32_t)strtoul(p_nbits->valuestring, NULL, 16);
118 s_state.nbits = job.nbits;
119 }
120 if (p_ntime && cJSON_IsString(p_ntime)) {
121 job.ntime = (uint32_t)strtoul(p_ntime->valuestring, NULL, 16);
122 }
123
124 memset(job.target, 0xFF, 32);
125 job.target_len = 32;
126
127 mining_set_current_nbits(job.nbits);
128 stratum_proxy_set_job(&job);
129
130 ESP_LOGI(TAG, "New mining job: id=%lu, nbits=0x%08lx", (unsigned long)job.job_id, (unsigned long)job.nbits);
131}
132
133static void handle_mining_set_difficulty(cJSON *params)
134{
135 if (!params || !cJSON_IsArray(params) || cJSON_GetArraySize(params) < 1) return;
136 cJSON *diff = cJSON_GetArrayItem(params, 0);
137 if (diff && cJSON_IsNumber(diff)) {
138 s_state.difficulty = (uint64_t)diff->valuedouble;
139 ESP_LOGI(TAG, "Pool set difficulty: %llu", (unsigned long long)s_state.difficulty);
140 }
141}
142
143static void stratum_client_task(void *arg)
144{
145 const tollgate_config_t *cfg = tollgate_config_get();
146
147 while (s_running) {
148 if (!s_state.connected) {
149 esp_err_t err = stratum_connect(cfg->stratum_host, cfg->stratum_port);
150 if (err != ESP_OK) {
151 ESP_LOGW(TAG, "Connection failed, retrying in 10s...");
152 vTaskDelay(pdMS_TO_TICKS(10000));
153 continue;
154 }
155 send_subscribe();
156 send_authorize();
157 }
158
159 char recv_buf[2048];
160 int len = read_line(recv_buf, sizeof(recv_buf));
161 if (len <= 0) {
162 ESP_LOGW(TAG, "Connection lost");
163 s_state.connected = false;
164 if (s_transport) {
165 esp_transport_close(s_transport);
166 esp_transport_destroy(s_transport);
167 s_transport = NULL;
168 }
169 vTaskDelay(pdMS_TO_TICKS(5000));
170 continue;
171 }
172
173 cJSON *root = cJSON_Parse(recv_buf);
174 if (!root) continue;
175
176 cJSON *method = cJSON_GetObjectItemCaseSensitive(root, "method");
177 if (method && cJSON_IsString(method)) {
178 cJSON *params = cJSON_GetObjectItemCaseSensitive(root, "params");
179
180 if (strcmp(method->valuestring, "mining.notify") == 0) {
181 handle_mining_notify(params);
182 } else if (strcmp(method->valuestring, "mining.set_difficulty") == 0) {
183 handle_mining_set_difficulty(params);
184 }
185 }
186
187 cJSON *id = cJSON_GetObjectItemCaseSensitive(root, "id");
188 cJSON *result = cJSON_GetObjectItemCaseSensitive(root, "result");
189 cJSON *error = cJSON_GetObjectItemCaseSensitive(root, "error");
190
191 if (id && result) {
192 if (cJSON_IsFalse(result) || (error && !cJSON_IsNull(error))) {
193 ESP_LOGW(TAG, "Request %d rejected", id->valueint);
194 }
195 }
196
197 cJSON_Delete(root);
198 }
199
200 if (s_transport) {
201 esp_transport_close(s_transport);
202 esp_transport_destroy(s_transport);
203 s_transport = NULL;
204 }
205 s_state.connected = false;
206 vTaskDelete(NULL);
207}
208
209esp_err_t stratum_client_init(void)
210{
211 memset(&s_state, 0, sizeof(s_state));
212 s_req_id = 1;
213 return ESP_OK;
214}
215
216esp_err_t stratum_client_start(void)
217{
218 if (s_running) return ESP_OK;
219 s_running = true;
220 BaseType_t ret = xTaskCreate(stratum_client_task, "stratum_cli", 8192, NULL, 4, &s_task_handle);
221 if (ret != pdPASS) {
222 ESP_LOGE(TAG, "Failed to create stratum client task");
223 s_running = false;
224 return ESP_FAIL;
225 }
226 ESP_LOGI(TAG, "Stratum client started");
227 return ESP_OK;
228}
229
230void stratum_client_stop(void)
231{
232 s_running = false;
233 if (s_task_handle) {
234 vTaskDelay(pdMS_TO_TICKS(1000));
235 s_task_handle = NULL;
236 }
237}
238
239esp_err_t stratum_client_submit_share(uint32_t job_id, uint32_t nonce, uint32_t ntime, uint32_t version)
240{
241 if (!s_state.connected || !s_transport) return ESP_FAIL;
242
243 const tollgate_config_t *cfg = tollgate_config_get();
244
245 char submit[512];
246 snprintf(submit, sizeof(submit),
247 "{\"id\":%lu,\"method\":\"mining.submit\",\"params\":[\"%s\",\"%lu\",\"%08lx\",\"%08lx\",\"%08lx\"]}\n",
248 (unsigned long)s_req_id++, cfg->stratum_user,
249 (unsigned long)job_id, (unsigned long)ntime, (unsigned long)nonce, (unsigned long)version);
250
251 int written = esp_transport_write(s_transport, submit, strlen(submit), 5000);
252 if (written < 0) {
253 ESP_LOGW(TAG, "Failed to submit share");
254 s_state.shares_rejected++;
255 return ESP_FAIL;
256 }
257
258 s_state.shares_accepted++;
259 ESP_LOGI(TAG, "Share submitted: job=%lu nonce=%08lx", (unsigned long)job_id, (unsigned long)nonce);
260 return ESP_OK;
261}
262
263const stratum_client_state_t *stratum_client_get_state(void)
264{
265 return &s_state;
266}
267
268void stratum_client_tick(void)
269{
270}
diff --git a/main/stratum_client.h b/main/stratum_client.h
new file mode 100644
index 0000000..e143439
--- /dev/null
+++ b/main/stratum_client.h
@@ -0,0 +1,27 @@
1#ifndef STRATUM_CLIENT_H
2#define STRATUM_CLIENT_H
3
4#include "esp_err.h"
5#include "stratum_proxy.h"
6#include <stdint.h>
7#include <stdbool.h>
8
9typedef struct {
10 bool connected;
11 char pool_host[128];
12 uint16_t pool_port;
13 uint32_t nbits;
14 uint64_t difficulty;
15 uint64_t shares_accepted;
16 uint64_t shares_rejected;
17 bool sv2_active;
18} stratum_client_state_t;
19
20esp_err_t stratum_client_init(void);
21esp_err_t stratum_client_start(void);
22void stratum_client_stop(void);
23esp_err_t stratum_client_submit_share(uint32_t job_id, uint32_t nonce, uint32_t ntime, uint32_t version);
24const stratum_client_state_t *stratum_client_get_state(void);
25void stratum_client_tick(void);
26
27#endif
diff --git a/main/stratum_proxy.c b/main/stratum_proxy.c
new file mode 100644
index 0000000..288c633
--- /dev/null
+++ b/main/stratum_proxy.c
@@ -0,0 +1,160 @@
1#include "stratum_proxy.h"
2#include "mining_payment.h"
3#include "esp_log.h"
4#include "lwip/sockets.h"
5#include "freertos/FreeRTOS.h"
6#include "freertos/task.h"
7#include <string.h>
8
9static const char *TAG = "stratum_proxy";
10static uint16_t s_port = 3333;
11static bool s_running = false;
12static TaskHandle_t s_task_handle = NULL;
13static int s_server_fd = -1;
14
15static stratum_job_t s_current_job = {0};
16static stratum_proxy_stats_t s_stats = {0};
17
18static void proxy_client_handler(void *arg)
19{
20 int client_fd = (int)(intptr_t)arg;
21 struct sockaddr_in client_addr;
22 socklen_t addr_len = sizeof(client_addr);
23 getpeername(client_fd, (struct sockaddr *)&client_addr, &addr_len);
24 uint32_t client_ip = client_addr.sin_addr.s_addr;
25
26 ESP_LOGI(TAG, "Miner connected from 0x%08lx", (unsigned long)client_ip);
27
28 if (s_current_job.valid) {
29 char job_json[512];
30 snprintf(job_json, sizeof(job_json),
31 "{\"id\":1,\"method\":\"mining.notify\",\"params\":[\"%lu\",\"%08lx%08lx%08lx%08lx%08lx%08lx%08lx%08lx\",\"\",\"\",\"\",\"%08lx\",\"%08lx\",\"%08lx\",true]}\n",
32 (unsigned long)s_current_job.job_id,
33 (unsigned long)0, (unsigned long)0, (unsigned long)0, (unsigned long)0,
34 (unsigned long)0, (unsigned long)0, (unsigned long)0, (unsigned long)0,
35 (unsigned long)s_current_job.nbits, (unsigned long)s_current_job.ntime,
36 (unsigned long)s_current_job.version);
37 send(client_fd, job_json, strlen(job_json), 0);
38 }
39
40 char buf[1024];
41 while (s_running) {
42 int len = recv(client_fd, buf, sizeof(buf) - 1, 0);
43 if (len <= 0) break;
44 buf[len] = '\0';
45
46 ESP_LOGI(TAG, "Received from miner: %s", buf);
47 s_stats.total_shares++;
48 s_stats.total_accepted++;
49 }
50
51 ESP_LOGI(TAG, "Miner disconnected from 0x%08lx", (unsigned long)client_ip);
52 close(client_fd);
53 vTaskDelete(NULL);
54}
55
56static void proxy_server_task(void *arg)
57{
58 struct sockaddr_in server_addr;
59 memset(&server_addr, 0, sizeof(server_addr));
60 server_addr.sin_family = AF_INET;
61 server_addr.sin_addr.s_addr = INADDR_ANY;
62 server_addr.sin_port = htons(s_port);
63
64 s_server_fd = socket(AF_INET, SOCK_STREAM, 0);
65 if (s_server_fd < 0) {
66 ESP_LOGE(TAG, "Failed to create socket");
67 vTaskDelete(NULL);
68 return;
69 }
70
71 int opt = 1;
72 setsockopt(s_server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
73
74 if (bind(s_server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) != 0) {
75 ESP_LOGE(TAG, "Failed to bind to port %u", (unsigned)s_port);
76 close(s_server_fd);
77 s_server_fd = -1;
78 vTaskDelete(NULL);
79 return;
80 }
81
82 if (listen(s_server_fd, 5) != 0) {
83 ESP_LOGE(TAG, "Failed to listen");
84 close(s_server_fd);
85 s_server_fd = -1;
86 vTaskDelete(NULL);
87 return;
88 }
89
90 ESP_LOGI(TAG, "Stratum proxy listening on port %u", (unsigned)s_port);
91
92 while (s_running) {
93 struct sockaddr_in client_addr;
94 socklen_t client_len = sizeof(client_addr);
95 int client_fd = accept(s_server_fd, (struct sockaddr *)&client_addr, &client_len);
96 if (client_fd < 0) continue;
97
98 s_stats.active_miners++;
99 char task_name[20];
100 snprintf(task_name, sizeof(task_name), "miner_%d", client_fd);
101 xTaskCreate(proxy_client_handler, task_name, 4096, (void *)(intptr_t)client_fd, 3, NULL);
102 }
103
104 close(s_server_fd);
105 s_server_fd = -1;
106 vTaskDelete(NULL);
107}
108
109esp_err_t stratum_proxy_init(uint16_t port)
110{
111 s_port = port;
112 memset(&s_current_job, 0, sizeof(s_current_job));
113 memset(&s_stats, 0, sizeof(s_stats));
114 s_running = true;
115
116 BaseType_t ret = xTaskCreate(proxy_server_task, "stratum_proxy", 4096, NULL, 4, &s_task_handle);
117 if (ret != pdPASS) {
118 ESP_LOGE(TAG, "Failed to create proxy task");
119 s_running = false;
120 return ESP_FAIL;
121 }
122
123 ESP_LOGI(TAG, "Stratum proxy initialized on port %u", (unsigned)port);
124 return ESP_OK;
125}
126
127void stratum_proxy_set_job(const stratum_job_t *job)
128{
129 if (job) {
130 memcpy(&s_current_job, job, sizeof(stratum_job_t));
131 s_stats.nbits = job->nbits;
132 s_stats.current_hashprice = mining_get_current_hashprice();
133 }
134}
135
136const stratum_job_t *stratum_proxy_get_current_job(void)
137{
138 return &s_current_job;
139}
140
141void stratum_proxy_get_stats(stratum_proxy_stats_t *stats)
142{
143 if (stats) {
144 *stats = s_stats;
145 stats->current_hashprice = mining_get_current_hashprice();
146 }
147}
148
149void stratum_proxy_stop(void)
150{
151 s_running = false;
152 if (s_server_fd >= 0) {
153 close(s_server_fd);
154 s_server_fd = -1;
155 }
156 if (s_task_handle) {
157 vTaskDelay(pdMS_TO_TICKS(500));
158 s_task_handle = NULL;
159 }
160}
diff --git a/main/stratum_proxy.h b/main/stratum_proxy.h
new file mode 100644
index 0000000..b940640
--- /dev/null
+++ b/main/stratum_proxy.h
@@ -0,0 +1,39 @@
1#ifndef STRATUM_PROXY_H
2#define STRATUM_PROXY_H
3
4#include "esp_err.h"
5#include <stdint.h>
6#include <stdbool.h>
7
8#define STRATUM_MAX_JOB_ID_LEN 32
9#define STRATUM_MAX_JOBS 4
10
11typedef struct {
12 uint32_t job_id;
13 uint8_t prevhash[32];
14 uint8_t merkle_root[32];
15 uint32_t ntime;
16 uint32_t nbits;
17 uint32_t version;
18 uint8_t target[32];
19 int target_len;
20 bool valid;
21} stratum_job_t;
22
23typedef struct {
24 double hashrate_ghs;
25 uint32_t nbits;
26 uint64_t total_shares;
27 uint64_t total_accepted;
28 uint64_t total_rejected;
29 double current_hashprice;
30 int active_miners;
31} stratum_proxy_stats_t;
32
33esp_err_t stratum_proxy_init(uint16_t port);
34void stratum_proxy_set_job(const stratum_job_t *job);
35const stratum_job_t *stratum_proxy_get_current_job(void);
36void stratum_proxy_get_stats(stratum_proxy_stats_t *stats);
37void stratum_proxy_stop(void);
38
39#endif
diff --git a/main/sw_miner.c b/main/sw_miner.c
new file mode 100644
index 0000000..cdd98a0
--- /dev/null
+++ b/main/sw_miner.c
@@ -0,0 +1,112 @@
1#include "sw_miner.h"
2#include "stratum_proxy.h"
3#include "stratum_client.h"
4#include "mining_payment.h"
5#include "config.h"
6#include "esp_log.h"
7#include "esp_random.h"
8#include "mbedtls/sha256.h"
9#include "freertos/FreeRTOS.h"
10#include "freertos/task.h"
11#include <string.h>
12
13static const char *TAG = "sw_miner";
14static bool s_running = false;
15static TaskHandle_t s_task_handle = NULL;
16static double s_hashrate = 0.0;
17
18static void sha256d(const uint8_t *data, size_t len, uint8_t *hash)
19{
20 uint8_t tmp[32];
21 mbedtls_sha256(data, len, tmp, 0);
22 mbedtls_sha256(tmp, 32, hash, 0);
23}
24
25static void sw_miner_task(void *arg)
26{
27 ESP_LOGI(TAG, "Software miner started");
28
29 uint64_t hashes = 0;
30 int64_t start_time = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS;
31
32 uint8_t header[80];
33 uint8_t hash[32];
34
35 while (s_running) {
36 const stratum_job_t *job = stratum_proxy_get_current_job();
37 if (!job || !job->valid) {
38 vTaskDelay(pdMS_TO_TICKS(1000));
39 continue;
40 }
41
42 stratum_job_t local_job;
43 memcpy(&local_job, job, sizeof(stratum_job_t));
44
45 memcpy(header, local_job.prevhash, 32);
46 memcpy(header + 32, local_job.merkle_root, 32);
47
48 uint32_t start_nonce = esp_random();
49 uint32_t end_nonce = start_nonce + 1000;
50
51 for (uint32_t nonce = start_nonce; nonce < end_nonce && s_running; nonce++) {
52 header[76] = (nonce >> 0) & 0xFF;
53 header[77] = (nonce >> 8) & 0xFF;
54 header[78] = (nonce >> 16) & 0xFF;
55 header[79] = (nonce >> 24) & 0xFF;
56
57 sha256d(header, 80, hash);
58 hashes++;
59
60 if (memcmp(hash, local_job.target, local_job.target_len) <= 0) {
61 ESP_LOGI(TAG, "Valid share found! nonce=%08lx", (unsigned long)nonce);
62 stratum_client_submit_share(local_job.job_id, nonce, local_job.ntime, local_job.version);
63 mining_update_hashrate(0, true);
64 break;
65 }
66 }
67
68 int64_t now = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS;
69 int64_t elapsed_s = (now - start_time) / 1000;
70 if (elapsed_s > 0) {
71 s_hashrate = (double)hashes / (double)elapsed_s / 1e6;
72 }
73
74 taskYIELD();
75 }
76
77 vTaskDelete(NULL);
78}
79
80esp_err_t sw_miner_start(void)
81{
82 if (s_running) return ESP_OK;
83 s_running = true;
84 s_hashrate = 0.0;
85
86 BaseType_t ret = xTaskCreate(sw_miner_task, "sw_miner", 8192, NULL, 2, &s_task_handle);
87 if (ret != pdPASS) {
88 ESP_LOGE(TAG, "Failed to create sw_miner task");
89 s_running = false;
90 return ESP_FAIL;
91 }
92 return ESP_OK;
93}
94
95void sw_miner_stop(void)
96{
97 s_running = false;
98 if (s_task_handle) {
99 vTaskDelay(pdMS_TO_TICKS(500));
100 s_task_handle = NULL;
101 }
102}
103
104bool sw_miner_is_running(void)
105{
106 return s_running;
107}
108
109double sw_miner_get_hashrate(void)
110{
111 return s_hashrate;
112}
diff --git a/main/sw_miner.h b/main/sw_miner.h
new file mode 100644
index 0000000..d0c2f06
--- /dev/null
+++ b/main/sw_miner.h
@@ -0,0 +1,13 @@
1#ifndef SW_MINER_H
2#define SW_MINER_H
3
4#include "esp_err.h"
5#include <stdint.h>
6#include <stdbool.h>
7
8esp_err_t sw_miner_start(void);
9void sw_miner_stop(void);
10bool sw_miner_is_running(void);
11double sw_miner_get_hashrate(void);
12
13#endif
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 @@
7#include "nucula_wallet.h" 7#include "nucula_wallet.h"
8#include "mint_health.h" 8#include "mint_health.h"
9#include "market.h" 9#include "market.h"
10#include "mining_payment.h"
11#include "stratum_proxy.h"
12#include "stratum_client.h"
10#include "esp_log.h" 13#include "esp_log.h"
11#include "esp_system.h" 14#include "esp_system.h"
12#include "cJSON.h" 15#include "cJSON.h"
@@ -150,6 +153,18 @@ static esp_err_t api_get_discovery(httpd_req_t *req)
150 cJSON_AddItemToArray(tips_tag, cJSON_CreateString("5")); 153 cJSON_AddItemToArray(tips_tag, cJSON_CreateString("5"));
151 cJSON_AddItemToArray(tags, tips_tag); 154 cJSON_AddItemToArray(tags, tips_tag);
152 155
156 if (cfg->mining_enabled) {
157 cJSON *mining_tag = cJSON_CreateArray();
158 cJSON_AddItemToArray(mining_tag, cJSON_CreateString("price_per_step"));
159 cJSON_AddItemToArray(mining_tag, cJSON_CreateString("mining"));
160 char mining_port_str[16];
161 snprintf(mining_port_str, sizeof(mining_port_str), "%d", cfg->mining_port);
162 cJSON_AddItemToArray(mining_tag, cJSON_CreateString(mining_port_str));
163 cJSON_AddItemToArray(mining_tag, cJSON_CreateString("GH/s"));
164 cJSON_AddItemToArray(mining_tag, cJSON_CreateString("sv1"));
165 cJSON_AddItemToArray(tags, mining_tag);
166 }
167
153 cJSON_AddItemToObject(root, "tags", tags); 168 cJSON_AddItemToObject(root, "tags", tags);
154 cJSON_AddStringToObject(root, "content", ""); 169 cJSON_AddStringToObject(root, "content", "");
155 170
@@ -504,6 +519,169 @@ static esp_err_t api_get_mints(httpd_req_t *req)
504 return ESP_OK; 519 return ESP_OK;
505} 520}
506 521
522static esp_err_t api_get_mining_job(httpd_req_t *req)
523{
524 const stratum_job_t *job = stratum_proxy_get_current_job();
525 if (!job || !job->valid) {
526 httpd_resp_set_status(req, "503 Service Unavailable");
527 httpd_resp_set_type(req, "application/json");
528 httpd_resp_send(req, "{\"error\":\"no job\"}", 15);
529 return ESP_OK;
530 }
531
532 cJSON *root = cJSON_CreateObject();
533 cJSON_AddNumberToObject(root, "job_id", job->job_id);
534
535 char prevhash_hex[65];
536 for (int i = 0; i < 32; i++) snprintf(prevhash_hex + i * 2, 3, "%02x", job->prevhash[i]);
537 cJSON_AddStringToObject(root, "prevhash", prevhash_hex);
538
539 char merkle_hex[65];
540 for (int i = 0; i < 32; i++) snprintf(merkle_hex + i * 2, 3, "%02x", job->merkle_root[i]);
541 cJSON_AddStringToObject(root, "merkle_root", merkle_hex);
542
543 cJSON_AddNumberToObject(root, "version", job->version);
544 cJSON_AddNumberToObject(root, "nbits", job->nbits);
545 cJSON_AddNumberToObject(root, "ntime", job->ntime);
546 cJSON_AddNumberToObject(root, "hashprice", mining_get_current_hashprice());
547
548 char *json = cJSON_PrintUnformatted(root);
549 httpd_resp_set_type(req, "application/json");
550 httpd_resp_send(req, json, strlen(json));
551 cJSON_free(json);
552 cJSON_Delete(root);
553 return ESP_OK;
554}
555
556static esp_err_t api_post_mining_share(httpd_req_t *req)
557{
558 uint32_t client_ip = 0;
559 get_client_ip(req, &client_ip);
560
561 int content_len = req->content_len;
562 if (content_len <= 0 || content_len > 512) {
563 httpd_resp_set_status(req, "400 Bad Request");
564 httpd_resp_set_type(req, "application/json");
565 httpd_resp_send(req, "{\"error\":\"invalid body\"}", 21);
566 return ESP_OK;
567 }
568
569 char body[512];
570 int total = 0;
571 while (total < content_len) {
572 int r = httpd_req_recv(req, body + total, content_len - total);
573 if (r <= 0) {
574 httpd_resp_set_status(req, "400 Bad Request");
575 httpd_resp_set_type(req, "text/plain");
576 httpd_resp_send(req, "bad request", 11);
577 return ESP_OK;
578 }
579 total += r;
580 }
581 body[total] = '\0';
582
583 cJSON *root = cJSON_Parse(body);
584 if (!root) {
585 httpd_resp_set_status(req, "400 Bad Request");
586 httpd_resp_set_type(req, "application/json");
587 httpd_resp_send(req, "{\"error\":\"invalid json\"}", 21);
588 return ESP_OK;
589 }
590
591 cJSON *j_job_id = cJSON_GetObjectItem(root, "job_id");
592 cJSON *j_nonce = cJSON_GetObjectItem(root, "nonce");
593 cJSON *j_ntime = cJSON_GetObjectItem(root, "ntime");
594 cJSON *j_version = cJSON_GetObjectItem(root, "version");
595 if (!j_job_id || !j_nonce || !j_ntime || !j_version) {
596 cJSON_Delete(root);
597 httpd_resp_set_status(req, "400 Bad Request");
598 httpd_resp_set_type(req, "application/json");
599 httpd_resp_send(req, "{\"error\":\"missing fields\"}", 22);
600 return ESP_OK;
601 }
602
603 uint32_t job_id = (uint32_t)j_job_id->valuedouble;
604 uint32_t nonce = (uint32_t)j_nonce->valuedouble;
605 uint32_t ntime = (uint32_t)j_ntime->valuedouble;
606 uint32_t version = (uint32_t)j_version->valuedouble;
607 cJSON_Delete(root);
608
609 const stratum_job_t *job = stratum_proxy_get_current_job();
610 if (!job || !job->valid || job->job_id != job_id) {
611 httpd_resp_set_status(req, "400 Bad Request");
612 httpd_resp_set_type(req, "application/json");
613 httpd_resp_send(req, "{\"error\":\"stale job\"}", 19);
614 return ESP_OK;
615 }
616
617 esp_err_t share_err = stratum_client_submit_share(job_id, nonce, ntime, version);
618 bool accepted = (share_err == ESP_OK);
619
620 mining_update_hashrate(client_ip, accepted);
621 mining_client_stats_t *stats = mining_get_or_create_client(client_ip);
622
623 if (accepted) {
624 const tollgate_config_t *cfg = tollgate_config_get();
625 double hashprice = mining_get_current_hashprice();
626 uint64_t allotment_ms = mining_shares_to_allotment_ms(
627 stats->hashrate_ghs, hashprice, cfg->price_per_step, cfg->step_size_ms);
628
629 session_t *session = session_find_by_ip(client_ip);
630 if (!session || !session->active || session->payment_method != PAYMENT_METHOD_MINING) {
631 session = session_create(client_ip, allotment_ms);
632 if (session) session->payment_method = PAYMENT_METHOD_MINING;
633 } else {
634 session_extend(session, allotment_ms);
635 }
636 }
637
638 cJSON *resp = cJSON_CreateObject();
639 cJSON_AddBoolToObject(resp, "accepted", accepted);
640 cJSON_AddNumberToObject(resp, "hashrate_ghs", stats ? stats->hashrate_ghs : 0.0);
641 char *json = cJSON_PrintUnformatted(resp);
642 httpd_resp_set_type(req, "application/json");
643 httpd_resp_send(req, json, strlen(json));
644 cJSON_free(json);
645 cJSON_Delete(resp);
646 return ESP_OK;
647}
648
649static esp_err_t api_get_mining_stats(httpd_req_t *req)
650{
651 stratum_proxy_stats_t proxy_stats;
652 stratum_proxy_get_stats(&proxy_stats);
653
654 const stratum_client_state_t *client_state = stratum_client_get_state();
655
656 cJSON *root = cJSON_CreateObject();
657
658 cJSON *proxy = cJSON_CreateObject();
659 cJSON_AddNumberToObject(proxy, "hashrate_ghs", proxy_stats.hashrate_ghs);
660 cJSON_AddNumberToObject(proxy, "total_shares", (double)proxy_stats.total_shares);
661 cJSON_AddNumberToObject(proxy, "total_accepted", (double)proxy_stats.total_accepted);
662 cJSON_AddNumberToObject(proxy, "total_rejected", (double)proxy_stats.total_rejected);
663 cJSON_AddNumberToObject(proxy, "hashprice", proxy_stats.current_hashprice);
664 cJSON_AddNumberToObject(proxy, "active_miners", proxy_stats.active_miners);
665 cJSON_AddItemToObject(root, "proxy", proxy);
666
667 cJSON *upstream = cJSON_CreateObject();
668 cJSON_AddBoolToObject(upstream, "connected", client_state->connected);
669 cJSON_AddStringToObject(upstream, "pool_host", client_state->pool_host);
670 cJSON_AddNumberToObject(upstream, "pool_port", client_state->pool_port);
671 cJSON_AddNumberToObject(upstream, "difficulty", (double)client_state->difficulty);
672 cJSON_AddNumberToObject(upstream, "shares_accepted", (double)client_state->shares_accepted);
673 cJSON_AddNumberToObject(upstream, "shares_rejected", (double)client_state->shares_rejected);
674 cJSON_AddItemToObject(root, "upstream", upstream);
675
676 char *json = cJSON_PrintUnformatted(root);
677 httpd_resp_set_type(req, "application/json");
678 httpd_resp_send(req, json, strlen(json));
679 cJSON_free(json);
680 cJSON_Delete(root);
681>>>>>>> feature/mining-payment
682 return ESP_OK;
683}
684
507static const httpd_uri_t uri_discovery = { .uri = "/", .method = HTTP_GET, .handler = api_get_discovery }; 685static const httpd_uri_t uri_discovery = { .uri = "/", .method = HTTP_GET, .handler = api_get_discovery };
508static const httpd_uri_t uri_payment = { .uri = "/", .method = HTTP_POST, .handler = api_post_payment }; 686static const httpd_uri_t uri_payment = { .uri = "/", .method = HTTP_POST, .handler = api_post_payment };
509static const httpd_uri_t uri_mints = { .uri = "/mints", .method = HTTP_GET, .handler = api_get_mints }; 687static 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
512static const httpd_uri_t uri_wallet = { .uri = "/wallet", .method = HTTP_GET, .handler = api_get_wallet }; 690static const httpd_uri_t uri_wallet = { .uri = "/wallet", .method = HTTP_GET, .handler = api_get_wallet };
513static const httpd_uri_t uri_wallet_swap = { .uri = "/wallet/swap", .method = HTTP_POST, .handler = api_post_wallet_swap }; 691static const httpd_uri_t uri_wallet_swap = { .uri = "/wallet/swap", .method = HTTP_POST, .handler = api_post_wallet_swap };
514static const httpd_uri_t uri_wallet_send = { .uri = "/wallet/send", .method = HTTP_POST, .handler = api_post_wallet_send }; 692static const httpd_uri_t uri_wallet_send = { .uri = "/wallet/send", .method = HTTP_POST, .handler = api_post_wallet_send };
693static const httpd_uri_t uri_mining_job = { .uri = "/mining/job", .method = HTTP_GET, .handler = api_get_mining_job };
694static const httpd_uri_t uri_mining_share = { .uri = "/mining/share", .method = HTTP_POST, .handler = api_post_mining_share };
695static const httpd_uri_t uri_mining_stats = { .uri = "/mining/stats", .method = HTTP_GET, .handler = api_get_mining_stats };
515 696
516static esp_err_t api_get_market(httpd_req_t *req) 697static esp_err_t api_get_market(httpd_req_t *req)
517{ 698{
@@ -559,7 +740,7 @@ esp_err_t tollgate_api_start(void)
559 httpd_config_t config = HTTPD_DEFAULT_CONFIG(); 740 httpd_config_t config = HTTPD_DEFAULT_CONFIG();
560 config.server_port = 2121; 741 config.server_port = 2121;
561 config.ctrl_port = 32769; 742 config.ctrl_port = 32769;
562 config.max_uri_handlers = 12; 743 config.max_uri_handlers = 16;
563 config.stack_size = 16384; 744 config.stack_size = 16384;
564 745
565 esp_err_t ret = httpd_start(&s_api_server, &config); 746 esp_err_t ret = httpd_start(&s_api_server, &config);
@@ -579,6 +760,13 @@ esp_err_t tollgate_api_start(void)
579 httpd_register_uri_handler(s_api_server, &uri_wallet_send); 760 httpd_register_uri_handler(s_api_server, &uri_wallet_send);
580 httpd_register_uri_handler(s_api_server, &uri_market); 761 httpd_register_uri_handler(s_api_server, &uri_market);
581 762
763 const tollgate_config_t *cfg = tollgate_config_get();
764 if (cfg->mining_enabled) {
765 httpd_register_uri_handler(s_api_server, &uri_mining_job);
766 httpd_register_uri_handler(s_api_server, &uri_mining_share);
767 httpd_register_uri_handler(s_api_server, &uri_mining_stats);
768 }
769
582 ESP_LOGI(TAG, "TollGate API started on port 2121"); 770 ESP_LOGI(TAG, "TollGate API started on port 2121");
583 return ESP_OK; 771 return ESP_OK;
584} 772}
diff --git a/main/tollgate_client.c b/main/tollgate_client.c
index a81d16f..73c8370 100644
--- a/main/tollgate_client.c
+++ b/main/tollgate_client.c
@@ -127,15 +127,25 @@ static bool parse_discovery_response(const char *json_str, tollgate_discovery_t
127 if (val && cJSON_IsString(val)) { 127 if (val && cJSON_IsString(val)) {
128 out->step_size_ms = atoi(val->valuestring); 128 out->step_size_ms = atoi(val->valuestring);
129 } 129 }
130 } else if (strcmp(tag_name->valuestring, "price_per_step") == 0 && tag_len >= 6) { 130 } else if (strcmp(tag_name->valuestring, "price_per_step") == 0 && tag_len >= 4) {
131 cJSON *amount = cJSON_GetArrayItem(tag, 2); 131 cJSON *payment_type = cJSON_GetArrayItem(tag, 2);
132 cJSON *mint = cJSON_GetArrayItem(tag, 4); 132
133 if (cJSON_IsString(payment_type) && strcmp(payment_type->valuestring, "mining") == 0 && tag_len >= 5) {
134 out->mining_available = true;
135 cJSON *port_val = cJSON_GetArrayItem(tag, 3);
136 if (port_val && cJSON_IsString(port_val)) {
137 out->mining_port = (uint16_t)atoi(port_val->valuestring);
138 }
139 } else {
140 cJSON *amount = cJSON_GetArrayItem(tag, 2);
141 cJSON *mint = cJSON_GetArrayItem(tag, 4);
133 142
134 if (amount && cJSON_IsString(amount)) { 143 if (amount && cJSON_IsString(amount)) {
135 out->price_per_step = atoi(amount->valuestring); 144 out->price_per_step = atoi(amount->valuestring);
136 } 145 }
137 if (mint && cJSON_IsString(mint)) { 146 if (mint && cJSON_IsString(mint)) {
138 strncpy(out->mint_url, mint->valuestring, sizeof(out->mint_url) - 1); 147 strncpy(out->mint_url, mint->valuestring, sizeof(out->mint_url) - 1);
148 }
139 } 149 }
140 } 150 }
141 } 151 }
diff --git a/main/tollgate_client.h b/main/tollgate_client.h
index 2055e52..ccee624 100644
--- a/main/tollgate_client.h
+++ b/main/tollgate_client.h
@@ -17,6 +17,7 @@ typedef enum {
17 TG_CLIENT_PAYING, 17 TG_CLIENT_PAYING,
18 TG_CLIENT_PAID, 18 TG_CLIENT_PAID,
19 TG_CLIENT_RENEWING, 19 TG_CLIENT_RENEWING,
20 TG_CLIENT_MINING,
20 TG_CLIENT_ERROR 21 TG_CLIENT_ERROR
21} tollgate_client_state_t; 22} tollgate_client_state_t;
22 23
@@ -26,6 +27,8 @@ typedef struct {
26 int step_size_ms; 27 int step_size_ms;
27 char mint_url[TG_CLIENT_MAX_MINT_URL]; 28 char mint_url[TG_CLIENT_MAX_MINT_URL];
28 char metric[TG_CLIENT_MAX_METRIC]; 29 char metric[TG_CLIENT_MAX_METRIC];
30 bool mining_available;
31 uint16_t mining_port;
29} tollgate_discovery_t; 32} tollgate_discovery_t;
30 33
31esp_err_t tollgate_client_init(void); 34esp_err_t tollgate_client_init(void);
diff --git a/main/tollgate_main.c b/main/tollgate_main.c
index 33e5b90..561fc3f 100644
--- a/main/tollgate_main.c
+++ b/main/tollgate_main.c
@@ -31,6 +31,11 @@
31#include "sync_manager.h" 31#include "sync_manager.h"
32#include "beacon_price.h" 32#include "beacon_price.h"
33#include "market.h" 33#include "market.h"
34#include "stratum_client.h"
35#include "stratum_proxy.h"
36#include "sw_miner.h"
37#include "asic_miner.h"
38#include "mining_payment.h"
34 39
35#define MAX_STA_RETRY 5 40#define MAX_STA_RETRY 5
36static const char *TAG = "tollgate_main"; 41static const char *TAG = "tollgate_main";
@@ -190,7 +195,6 @@ static void start_services(void)
190 session_manager_init(); 195 session_manager_init();
191 196
192 const tollgate_config_t *cfg = tollgate_config_get(); 197 const tollgate_config_t *cfg = tollgate_config_get();
193
194 mint_health_init(cfg->accepted_mints, cfg->accepted_mint_count); 198 mint_health_init(cfg->accepted_mints, cfg->accepted_mint_count);
195 mint_health_start(); 199 mint_health_start();
196 200
@@ -199,6 +203,11 @@ static void start_services(void)
199 } else { 203 } else {
200 nucula_wallet_init(cfg->mint_url); 204 nucula_wallet_init(cfg->mint_url);
201 } 205 }
206
207 if (cfg->mining_enabled) {
208 firewall_set_mining_port(cfg->mining_port);
209 firewall_set_sandbox_mint_access(cfg->mining_sandbox_mint_access);
210 }
202 lightning_payout_init(&cfg->payout); 211 lightning_payout_init(&cfg->payout);
203 212
204 dns_server_start(ap_ip_info.ip, upstream_dns); 213 dns_server_start(ap_ip_info.ip, upstream_dns);
@@ -223,6 +232,26 @@ static void start_services(void)
223 cvm_server_start(); 232 cvm_server_start();
224 } 233 }
225 234
235 if (cfg2->mining_enabled) {
236 ESP_LOGI(TAG, "Mining subsystem enabled, initializing...");
237 mining_payment_init();
238 stratum_client_init();
239 stratum_proxy_init(cfg2->mining_port);
240
241 if (cfg2->mining_payout_mode != MINING_PAYOUT_UPSTREAM) {
242 stratum_client_start();
243 }
244
245 asic_miner_init();
246 if (asic_miner_is_present()) {
247 asic_miner_start();
248 ESP_LOGI(TAG, "ASIC miner started");
249 } else {
250 sw_miner_start();
251 ESP_LOGI(TAG, "Software miner started (no ASIC)");
252 }
253 }
254
226 s_services_running = true; 255 s_services_running = true;
227 if (s_services_mutex) xSemaphoreGive(s_services_mutex); 256 if (s_services_mutex) xSemaphoreGive(s_services_mutex);
228 ESP_LOGI(TAG, "=== TollGate services started ==="); 257 ESP_LOGI(TAG, "=== TollGate services started ===");
@@ -392,5 +421,6 @@ void app_main(void)
392 tollgate_client_tick(); 421 tollgate_client_tick();
393 lightning_payout_tick(); 422 lightning_payout_tick();
394 market_tick(); 423 market_tick();
424 stratum_client_tick();
395 } 425 }
396} 426}