upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-19 04:07:16 +0530
committerYour Name <you@example.com>2026-05-19 04:07:16 +0530
commitc75230e551a778408b2e370b208aff76b74c6560 (patch)
tree0cf51ecae1c469b7e10272d44b37f69ff4cb611f
parentabee221b0f0e5a4513ab126afbdfddc2728df6be (diff)
feat(mining): add new mining source files and unit tests
- mining_payment.c/h: hashprice calc, share validation, per-client hashrate - stratum_client.c/h: SV1 upstream pool connection - stratum_proxy.c/h: local SV1 TCP server for downstream miners - sw_miner.c/h: software SHA256d miner using mbedtls - asic_miner.c/h: ASIC detection stub (software fallback) - test_mining_payment.c: 23 unit tests for mining payment module
-rw-r--r--MINING_PLAN.md357
-rw-r--r--main/asic_miner.c63
-rw-r--r--main/asic_miner.h14
-rw-r--r--main/mining_payment.c169
-rw-r--r--main/mining_payment.h35
-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.c111
-rw-r--r--main/sw_miner.h13
-rw-r--r--tests/unit/Makefile5
-rw-r--r--tests/unit/test_mining_payment.c92
13 files changed, 1354 insertions, 1 deletions
diff --git a/MINING_PLAN.md b/MINING_PLAN.md
new file mode 100644
index 0000000..bb72d3c
--- /dev/null
+++ b/MINING_PLAN.md
@@ -0,0 +1,357 @@
1# Mining-for-Bandwidth Implementation Plan
2
3## Overview
4
5Add Bitcoin mining (Stratum v1 + v2) to the TollGate firmware so that devices earn internet access by mining real Bitcoin blocks. A BitAxe (ESP32-S3 + BM1366 ASIC) running TollGate firmware becomes a plug-and-play mesh node — no e-cash, Nostr identity, or prior setup required.
6
7## Design Decisions
8
9### Why real Bitcoin mining instead of arbitrary proof-of-work?
10
11The work must be **useful** — mining against a Stratum v2 block template means every share contributes to Bitcoin's security. Even at negligible hashrate (ESP32 software mining at ~10-50 kH/s), the work is real. With a BM1366 ASIC (~500 GH/s), the device produces meaningful hashrate.
12
13### Why Stratum v2 upstream + Stratum v1 local?
14
15- **SV2 upstream (to pool)**: Binary framing is bandwidth-efficient, Noise encryption prevents hash hijacking, and the encrypted tunnel uses minimal megabytes on the paid internet link
16- **SV1 local (to downstream miners)**: JSON-RPC is trivial to implement, no handshake overhead, works over local WiFi with negligible latency
17- BitAxe already has both implementations — we reuse them
18
19### Why Braiins Pool as default SV2 pool?
20
21- Native SV2 support with published authority pubkey
22- 0% PPLNS fee option
23- Lightning Network payouts (useful for converting mining revenue to e-cash)
24- The authority pubkey is known: `024e031a0b63c7885b19e48f76d49ddbcda9bf3d7f1d6b05df8b71569e2c2f7ff0`
25
26### Why dual payout mode (Lightning sats vs e-cash)?
27
28A TollGate's position in the mesh determines what it earns:
29
30| Position | Hashrate goes to | Earns |
31|----------|-----------------|-------|
32| Standalone (has direct internet) | Braiins pool | Lightning sats for operator |
33| Mesh node (upstream TollGate detected) | Upstream TollGate's proxy | e-cash / megabytes / minutes |
34| Relay (no ASIC, no internet) | Nothing locally | Just proxies for downstream miners |
35
36The `mining_payout_mode` config field controls this: `auto` (default) detects upstream TollGate and chooses accordingly.
37
38### Why mine with CPU too?
39
40UX. We don't care if CPU mining is profitable. A plain ESP32-S3 without an ASIC can still produce non-zero hashrate (~10-50 kH/s via hardware SHA256 accelerator). This means ANY ESP32 running TollGate firmware can bootstrap itself — even one byte per hour is better than zero.
41
42### Why sandbox mint access?
43
44Miners who earn bandwidth via hashrate should also reach the Cashu mint URLs the TollGate accepts. This way they can receive e-cash from mobile wallets without first having internet access. The firewall whitelists mint URLs for unauthenticated clients.
45
46### Why hashprice from block template?
47
48The conversion from hashrate → bandwidth needs a price signal:
49
50```
51hashprice = (block_subsidy * blocks_per_day) / (difficulty * 2^32) [sat/GH/day]
52allotment = (hashrate_ghs * hashprice_per_s * duration_s) / price_per_step * step_size
53```
54
55Calculating hashprice from the `nbits` field in the SV2 block template is automatic, requires no external API, and is always accurate. A config override (`hashprice_sats_per_ghs_day`) is available as fallback. A ContextVM MCP tool (`get_hashprice`) is planned for future dynamic pricing.
56
57### Why BitAxe as git submodule?
58
59The BitAxe ESP-Miner firmware (GPL-3.0) runs on the same ESP-IDF v5.x on ESP32-S3. It contains production-quality implementations of:
60- `components/stratum_v2/` — SV2 protocol + Noise handshake
61- `components/stratum/` — SV1 protocol
62- `components/asic/` — BM1366/BM1368 serial drivers
63
64Rather than copying code that will diverge, we reference it as a submodule and compile selected components.
65
66## Architecture
67
68```
69[Bitcoin SV2 Pool (Braiins)]
70 ↑ SV2 + Noise (encrypted, binary)
71[TollGate Gateway] (ESP32-S3, has internet via STA)
72 ├── stratum_client.c — SV2 upstream to Braiins (Noise handshake, job reception, share forwarding)
73 ├── stratum_proxy.c — Local SV1 TCP :3333 (distribute jobs, collect shares, per-IP hashrate meter)
74 ├── mining_payment.c — Share validation, hashprice calc, session_create() calls
75 ├── sw_miner.c — ESP32-S3 hardware SHA256 accelerator (~10-50 kH/s, always runs)
76 ├── asic_miner.c — BM1366/BM1368 via SPI (~500 GH/s, if detected)
77 ├── Existing: captive portal, Cashu, firewall, sessions, wifistr, CVM
78
79[TollGate Miner] (BitAxe ESP32-S3 + BM1366, no internet)
80 ├── SV1 client → connects to gateway's :3333 via local WiFi
81 ├── ASIC driver → BM1366 via SPI
82 ├── tollgate_client.c → mining mode (instead of Cashu payment)
83 └── Also runs its own AP for downstream devices
84
85[Plain ESP32 TollGate]
86 ├── SV1 client → connects to gateway's :3333
87 ├── sw_miner.c only (~10-50 kH/s)
88 └── Also runs its own AP for downstream devices
89```
90
91## Config Fields (config.json)
92
93```json
94{
95 "mining_enabled": true,
96 "mining_payout_mode": "auto",
97 "stratum_host": "v2.pool.braiins.com",
98 "stratum_port": 3333,
99 "stratum_user": "bc1q...TollGate",
100 "stratum_pass": "x",
101 "stratum_sv2_authority_pubkey": "024e031a0b63c7885b19e48f76d49ddbcda9bf3d7f1d6b05df8b71569e2c2f7ff0",
102 "stratum_fallback_host": "public-pool.io",
103 "stratum_fallback_port": 21496,
104 "mining_port": 3333,
105 "hashprice_sats_per_ghs_day": 0,
106 "mining_sandbox_mint_access": true
107}
108```
109
110| Field | Default | Description |
111|-------|---------|-------------|
112| `mining_enabled` | `false` | Enable mining subsystem |
113| `mining_payout_mode` | `"auto"` | `"auto"`, `"pool"`, `"upstream"`, `"proxy_only"` |
114| `stratum_host` | `"v2.pool.braiins.com"` | SV2 pool hostname |
115| `stratum_port` | `3333` | SV2 pool port |
116| `stratum_user` | `""` | Bitcoin/Lightning address for pool payout |
117| `stratum_pass` | `"x"` | Pool password |
118| `stratum_sv2_authority_pubkey` | Braiins key | Pool authority pubkey for Noise verification |
119| `stratum_fallback_host` | `"public-pool.io"` | SV1 fallback pool hostname |
120| `stratum_fallback_port` | `21496` | SV1 fallback pool port |
121| `mining_port` | `3333` | Local mining proxy listen port |
122| `hashprice_sats_per_ghs_day` | `0` | Manual hashprice override (0 = auto from nbits) |
123| `mining_sandbox_mint_access` | `true` | Allow unauthenticated clients to reach mint URLs |
124
125## Mining Payout Modes
126
127### `auto` (default)
128```
129TollGate boots → connects to WiFi (STA)
130 → tollgate_client_detect(gw_ip)
131 → GET http://gw_ip:2121/
132 → If upstream TollGate detected → mine to upstream proxy → earn e-cash/bytes
133 → If regular router → mine to Braiins pool → earn Lightning sats
134```
135
136### `pool`
137Always mine to Braiins/public-pool via SV2. Never mine to upstream TollGate. Gateway's own hashrate earns Lightning sats for the operator.
138
139### `upstream`
140Always mine to upstream TollGate's proxy. Fail if no upstream TollGate detected. Gateway's own hashrate earns e-cash/bytes/minutes.
141
142### `proxy_only`
143Don't mine locally at all. Only run the local proxy for downstream miners. Useful for plain ESP32 relay nodes without ASICs that don't want to waste CPU on mining.
144
145## Sandbox / Firewall Changes
146
147Unauthenticated clients (no Cashu, no session) get access to:
148- `TCP :3333` — mining proxy (get jobs, submit shares)
149- `TCP :2121` — tollgate API (`GET /mining/job`, `POST /mining/share`)
150- `TCP :80` — captive portal
151- `TCP/443` to `mint_url` — so miners can receive e-cash from mobile wallets
152
153## Hashrate-to-Bandwidth Conversion
154
155```
156difficulty = nbits_to_difficulty(job.nbits)
157hashprice_sats_per_ghs_day = (312500000 * 144) / (difficulty * 2^32)
158hashprice_sats_per_ghs_s = hashprice_sats_per_ghs_day / 86400
159
160allotment_ms = (client_hashrate_ghs * hashprice_sats_per_ghs_s * measurement_window_s)
161 / price_per_step * step_size_ms
162```
163
164The measurement window is a sliding interval (e.g., 30 seconds). Shares submitted during the window are counted, hashrate is estimated, and allotment is calculated and granted via `session_create()` or `session_extend()`.
165
166## New Files
167
168| File | Purpose |
169|------|---------|
170| `main/stratum_client.c/h` | SV2 upstream client (connect to Braiins, Noise handshake, receive jobs, submit shares) |
171| `main/stratum_proxy.c/h` | Local SV1 TCP server on :3333 (distribute jobs, collect shares, per-IP hashrate meter) |
172| `main/mining_payment.c/h` | Share validation (SHA256d check), hashprice calculation, session creation |
173| `main/sw_miner.c/h` | Software SHA256 miner using ESP32-S3 hardware SHA256 accelerator |
174| `main/asic_miner.c/h` | BM1366/BM1368 ASIC detection + driver wrapper |
175| `tests/unit/test_mining_payment.c` | Hashprice calculation tests, share validation tests |
176| `tests/unit/test_stratum_proxy.c` | SV1/SV2 frame parsing tests |
177
178## Modified Files
179
180| File | Changes |
181|------|---------|
182| `main/tollgate_api.c` | Add `GET /mining/job`, `POST /mining/share`; add mining tag to `GET /` discovery |
183| `main/tollgate_client.c` | Add mining mode (detect upstream mining support, mine instead of Cashu) |
184| `main/tollgate_client.h` | New states: `TG_CLIENT_MINING`, `TG_CLIENT_MINING_ACTIVE` |
185| `main/firewall.c/h` | Quarantine allowlist: mining port + mint URLs for unauthenticated clients |
186| `main/dns_server.c/h` | Resolve mint URLs for unauthenticated clients |
187| `main/session.c/h` | Add `payment_method` field (Cashu vs mining) |
188| `main/config.c/h` | Parse all new mining config fields |
189| `main/captive_portal.c` | "Mine for Access" tab in portal HTML |
190| `main/tollgate_main.c` | Start mining tasks, init ASIC detection |
191| `CMakeLists.txt` | Add new source files, reference BitAxe submodule |
192| `.gitmodules` | Add `bitaxeorg/ESP-Miner` submodule |
193
194## Implementation Phases
195
196### Phase 1: Foundation (config + submodule + build)
197- [ ] Add `bitaxeorg/ESP-Miner` as git submodule at `components/esp-miner/`
198- [ ] Add mining config fields to `config.c/h`
199- [ ] Update `CMakeLists.txt` to compile new sources
200- [ ] Verify build compiles cleanly
201
202### Phase 2: Stratum client (SV2 upstream)
203- [ ] Create `main/stratum_client.c/h`
204- [ ] SV2 connection lifecycle: TCP connect → Noise handshake → SetupConnection → OpenChannel → receive jobs
205- [ ] Job reception: parse NewMiningJob, SetNewPrevHash, SetTarget
206- [ ] Share submission: forward shares from local proxy to upstream pool
207- [ ] Fallback: if SV2 fails, try SV1 to public-pool.io
208
209### Phase 3: Stratum proxy (local SV1 server)
210- [ ] Create `main/stratum_proxy.c/h`
211- [ ] TCP listener on `:3333`
212- [ ] SV1 JSON-RPC: `mining.subscribe`, `mining.authorize`, `mining.notify`, `mining.submit`
213- [ ] Distribute current job to connected miners
214- [ ] Collect shares, forward to stratum_client for upstream submission
215- [ ] Per-client IP hashrate meter (shares / time window)
216
217### Phase 4: Mining payment
218- [ ] Create `main/mining_payment.c/h`
219- [ ] `nbits_to_difficulty()` conversion
220- [ ] `calculate_hashprice()` from difficulty + block subsidy
221- [ ] `validate_share()` — SHA256d(header) < target check
222- [ ] `shares_to_allotment()` — hashrate → bandwidth conversion
223- [ ] Integration with `session_create()` / `session_extend()`
224
225### Phase 5: API endpoints
226- [ ] `GET /mining/job` — return current block template as JSON
227- [ ] `POST /mining/share` — accept share, validate, create/extend session
228- [ ] Add mining tag to `GET /` discovery response
229- [ ] `GET /mining/stats` — current hashrate, total shares, hashprice
230
231### Phase 6: Firewall sandbox
232- [ ] `firewall.c` — quarantine allowlist for `:3333`, `:2121`, `:80`
233- [ ] `firewall.c` — conditional allow mint URL hostnames if `mining_sandbox_mint_access`
234- [ ] `dns_server.c` — resolve mint URLs for unauthenticated clients
235
236### Phase 7: Client mining mode
237- [ ] `tollgate_client.c` — detect upstream mining support via discovery tag
238- [ ] New state machine: `TG_CLIENT_MINING` → `TG_CLIENT_MINING_ACTIVE`
239- [ ] Connect to upstream `:3333` mining proxy
240- [ ] Submit shares to earn bandwidth
241
242### Phase 8: Software miner
243- [ ] Create `main/sw_miner.c/h`
244- [ ] ESP32-S3 hardware SHA256 accelerator via `esp_sha.h` / mbedtls
245- [ ] Get job from local stratum proxy, iterate nonces, check against target
246- [ ] Low-priority FreeRTOS task (don't starve WiFi/routing)
247- [ ] Expected: ~10-50 kH/s
248
249### Phase 9: ASIC miner
250- [ ] Create `main/asic_miner.c/h`
251- [ ] Probe SPI bus at boot for BM1366/BM1368
252- [ ] If ASIC found: use BitAxe driver (`BM1366_send_work`, `BM1366_process_work`)
253- [ ] If no ASIC: fall back to software miner
254- [ ] Expected: ~500 GH/s (BM1366) or ~120 GH/s (BM1368)
255
256### Phase 10: Portal UI
257- [ ] Add "Mine for Access" tab to captive portal HTML
258- [ ] Show current hashrate, shares submitted, time earned
259- [ ] Auto-start mining when tab is opened (JavaScript Web Crypto SHA256 in browser)
260- [ ] Show progress bar / earnings counter
261
262### Phase 11: CVM integration
263- [ ] Add `get_hashprice` MCP tool to `mcp_handler.c/h`
264- [ ] Returns current hashprice, difficulty, estimated earnings
265- [ ] `set_mining_config` tool for remote configuration
266
267### Phase 12: Main integration
268- [ ] `tollgate_main.c` — start stratum_client task on boot
269- [ ] `tollgate_main.c` — start stratum_proxy task on boot
270- [ ] `tollgate_main.c` — start sw_miner task on boot
271- [ ] `tollgate_main.c` — start asic_miner task if ASIC detected
272- [ ] Mining task lifecycle: start/stop with services
273
274### Phase 13: Tests
275- [ ] Unit test: `test_mining_payment.c` — hashprice calc, nbits→difficulty, share validation
276- [ ] Unit test: `test_stratum_proxy.c` — SV1 frame parsing, SV2 frame encode/decode
277- [ ] Integration test: `mining.mjs` — submit share, verify session, check bandwidth
278- [ ] E2E test: `mining.spec.mjs` — portal mining tab, hashrate display
279
280## Checklist — Implementation Progress
281
282### Phase 1: Foundation
283- [ ] Add BitAxe git submodule
284- [ ] Mining config fields in config.c/h
285- [ ] CMakeLists.txt updated
286- [ ] Clean build verified
287
288### Phase 2: Stratum Client
289- [ ] stratum_client.c/h created
290- [ ] SV2 Noise handshake
291- [ ] Job reception
292- [ ] Share submission
293- [ ] SV1 fallback
294
295### Phase 3: Stratum Proxy
296- [ ] stratum_proxy.c/h created
297- [ ] SV1 JSON-RPC server
298- [ ] Job distribution
299- [ ] Share collection
300- [ ] Per-IP hashrate meter
301
302### Phase 4: Mining Payment
303- [ ] mining_payment.c/h created
304- [ ] nbits_to_difficulty
305- [ ] hashprice calculation
306- [ ] share validation (SHA256d)
307- [ ] shares_to_allotment conversion
308- [ ] session_create integration
309
310### Phase 5: API Endpoints
311- [ ] GET /mining/job
312- [ ] POST /mining/share
313- [ ] Mining discovery tag
314- [ ] GET /mining/stats
315
316### Phase 6: Firewall Sandbox
317- [ ] Quarantine allowlist for mining ports
318- [ ] Mint URL access for unauthenticated clients
319- [ ] DNS resolution in sandbox
320
321### Phase 7: Client Mining Mode
322- [ ] Mining support detection in tollgate_client.c
323- [ ] TG_CLIENT_MINING states
324- [ ] Upstream proxy connection
325- [ ] Share submission for bandwidth
326
327### Phase 8: Software Miner
328- [ ] sw_miner.c/h created
329- [ ] ESP32-S3 HW SHA256
330- [ ] Job dequeue → nonce iteration → target check
331- [ ] Low-priority task
332
333### Phase 9: ASIC Miner
334- [ ] asic_miner.c/h created
335- [ ] BM1366/BM1368 SPI detection
336- [ ] ASIC mining loop
337- [ ] Software fallback
338
339### Phase 10: Portal UI
340- [ ] "Mine for Access" tab
341- [ ] Hashrate display
342- [ ] Earnings counter
343
344### Phase 11: CVM Integration
345- [ ] get_hashprice MCP tool
346- [ ] set_mining_config MCP tool
347
348### Phase 12: Main Integration
349- [ ] Mining tasks started on boot
350- [ ] ASIC detection at boot
351- [ ] Task lifecycle management
352
353### Phase 13: Tests
354- [ ] test_mining_payment.c
355- [ ] test_stratum_proxy.c
356- [ ] integration/mining.mjs
357- [ ] e2e/mining.spec.mjs
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/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/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..278f8f3
--- /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[16];
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..b45e7c5
--- /dev/null
+++ b/main/sw_miner.c
@@ -0,0 +1,111 @@
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 "mbedtls/sha256.h"
8#include "freertos/FreeRTOS.h"
9#include "freertos/task.h"
10#include <string.h>
11
12static const char *TAG = "sw_miner";
13static bool s_running = false;
14static TaskHandle_t s_task_handle = NULL;
15static double s_hashrate = 0.0;
16
17static void sha256d(const uint8_t *data, size_t len, uint8_t *hash)
18{
19 uint8_t tmp[32];
20 mbedtls_sha256(data, len, tmp, 0);
21 mbedtls_sha256(tmp, 32, hash, 0);
22}
23
24static void sw_miner_task(void *arg)
25{
26 ESP_LOGI(TAG, "Software miner started");
27
28 uint64_t hashes = 0;
29 int64_t start_time = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS;
30
31 uint8_t header[80];
32 uint8_t hash[32];
33
34 while (s_running) {
35 const stratum_job_t *job = stratum_proxy_get_current_job();
36 if (!job || !job->valid) {
37 vTaskDelay(pdMS_TO_TICKS(1000));
38 continue;
39 }
40
41 stratum_job_t local_job;
42 memcpy(&local_job, job, sizeof(stratum_job_t));
43
44 memcpy(header, local_job.prevhash, 32);
45 memcpy(header + 32, local_job.merkle_root, 32);
46
47 uint32_t start_nonce = esp_random();
48 uint32_t end_nonce = start_nonce + 1000;
49
50 for (uint32_t nonce = start_nonce; nonce < end_nonce && s_running; nonce++) {
51 header[76] = (nonce >> 0) & 0xFF;
52 header[77] = (nonce >> 8) & 0xFF;
53 header[78] = (nonce >> 16) & 0xFF;
54 header[79] = (nonce >> 24) & 0xFF;
55
56 sha256d(header, 80, hash);
57 hashes++;
58
59 if (memcmp(hash, local_job.target, local_job.target_len) <= 0) {
60 ESP_LOGI(TAG, "Valid share found! nonce=%08lx", (unsigned long)nonce);
61 stratum_client_submit_share(local_job.job_id, nonce, local_job.ntime, local_job.version);
62 mining_update_hashrate(0, true);
63 break;
64 }
65 }
66
67 int64_t now = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS;
68 int64_t elapsed_s = (now - start_time) / 1000;
69 if (elapsed_s > 0) {
70 s_hashrate = (double)hashes / (double)elapsed_s / 1e6;
71 }
72
73 taskYIELD();
74 }
75
76 vTaskDelete(NULL);
77}
78
79esp_err_t sw_miner_start(void)
80{
81 if (s_running) return ESP_OK;
82 s_running = true;
83 s_hashrate = 0.0;
84
85 BaseType_t ret = xTaskCreate(sw_miner_task, "sw_miner", 8192, NULL, 2, &s_task_handle);
86 if (ret != pdPASS) {
87 ESP_LOGE(TAG, "Failed to create sw_miner task");
88 s_running = false;
89 return ESP_FAIL;
90 }
91 return ESP_OK;
92}
93
94void sw_miner_stop(void)
95{
96 s_running = false;
97 if (s_task_handle) {
98 vTaskDelay(pdMS_TO_TICKS(500));
99 s_task_handle = NULL;
100 }
101}
102
103bool sw_miner_is_running(void)
104{
105 return s_running;
106}
107
108double sw_miner_get_hashrate(void)
109{
110 return s_hashrate;
111}
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/tests/unit/Makefile b/tests/unit/Makefile
index 7ebc3b2..0a726f6 100644
--- a/tests/unit/Makefile
+++ b/tests/unit/Makefile
@@ -22,7 +22,7 @@ LDFLAGS := -lmbedcrypto -lcjson -lm
22 22
23SECP256K1_OBJ := secp256k1.o precomputed_ecmult.o precomputed_ecmult_gen.o 23SECP256K1_OBJ := secp256k1.o precomputed_ecmult.o precomputed_ecmult_gen.o
24 24
25TESTS := test_geohash test_identity test_nostr_event test_cashu test_session test_tollgate_client test_lnurl_pay test_lightning_payout test_mcp_handler test_nip04 test_cvm_server 25TESTS := test_geohash test_identity test_nostr_event test_cashu test_session test_tollgate_client test_lnurl_pay test_lightning_payout test_mcp_handler test_nip04 test_cvm_server test_mining_payment
26 26
27.PHONY: all test clean $(TESTS) 27.PHONY: all test clean $(TESTS)
28 28
@@ -81,5 +81,8 @@ test_nip04: test_nip04.c $(REPO_ROOT)/main/nip04.c $(SECP256K1_OBJ)
81test_cvm_server: test_cvm_server.c 81test_cvm_server: test_cvm_server.c
82 $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) 82 $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
83 83
84test_mining_payment: test_mining_payment.c $(REPO_ROOT)/main/mining_payment.c
85 $(CC) $(CFLAGS) $< $(REPO_ROOT)/main/mining_payment.c -o $@ $(LDFLAGS)
86
84clean: 87clean:
85 rm -f $(TESTS) $(SECP256K1_OBJ) 88 rm -f $(TESTS) $(SECP256K1_OBJ)
diff --git a/tests/unit/test_mining_payment.c b/tests/unit/test_mining_payment.c
new file mode 100644
index 0000000..c3834fd
--- /dev/null
+++ b/tests/unit/test_mining_payment.c
@@ -0,0 +1,92 @@
1#include "test_framework.h"
2#include "../../main/mining_payment.h"
3#include <stdio.h>
4#include <string.h>
5#include <math.h>
6
7int main(void)
8{
9 printf("=== test_mining_payment ===\n");
10
11 mining_payment_init();
12
13 printf("\n--- mining_nbits_to_difficulty ---\n");
14 uint64_t d1 = mining_nbits_to_difficulty(0);
15 ASSERT(d1 == UINT64_MAX, "nbits=0 returns max");
16
17 uint64_t d2 = mining_nbits_to_difficulty(0x170309E2);
18 ASSERT(d2 > 0, "mainnet nbits gives positive difficulty");
19 printf(" INFO: difficulty for 0x170309E2 = %llu\n", (unsigned long long)d2);
20
21 uint64_t d3 = mining_nbits_to_difficulty(0x1d00ffff);
22 ASSERT(d3 == 1, "pseudotransaction nbits = difficulty 1");
23
24 printf("\n--- mining_calculate_hashprice ---\n");
25 double hp1 = mining_calculate_hashprice(0);
26 ASSERT(hp1 == 0.0, "nbits=0 gives hashprice 0");
27
28 double hp2 = mining_calculate_hashprice(0x170309E2);
29 ASSERT(hp2 > 0.0, "mainnet nbits gives positive hashprice");
30 printf(" INFO: hashprice for 0x170309E2 = %.6f sat/GH/s/day\n", hp2);
31
32 printf("\n--- mining_calculate_hashprice_override ---\n");
33 double hp3 = mining_calculate_hashprice_override(1000);
34 ASSERT(fabs(hp3 - 1000.0) < 0.001, "override returns exact value");
35
36 printf("\n--- mining_set_current_nbits ---\n");
37 mining_set_current_nbits(0x170309E2);
38 double hp4 = mining_get_current_hashprice();
39 ASSERT(fabs(hp4 - hp2) < 0.0001, "stored hashprice matches calculated");
40
41 printf("\n--- mining_get_or_create_client ---\n");
42 mining_client_stats_t *c1 = mining_get_or_create_client(0x0A010203);
43 ASSERT(c1 != NULL, "created client 1");
44 ASSERT(c1->ip == 0x0A010203, "client 1 IP matches");
45 ASSERT(c1->shares_accepted == 0, "client 1 starts with 0 shares");
46
47 mining_client_stats_t *c2 = mining_get_or_create_client(0x0A010204);
48 ASSERT(c2 != NULL, "created client 2");
49 ASSERT(c2->ip == 0x0A010204, "client 2 IP matches");
50
51 mining_client_stats_t *c1_again = mining_get_or_create_client(0x0A010203);
52 ASSERT(c1_again == c1, "same IP returns same client");
53
54 printf("\n--- mining_update_hashrate ---\n");
55 mining_update_hashrate(0x0A010203, true);
56 mining_update_hashrate(0x0A010203, true);
57 mining_update_hashrate(0x0A010203, false);
58
59 const mining_client_stats_t *c1_stats = mining_get_client_stats(0x0A010203);
60 ASSERT(c1_stats != NULL, "client 1 stats found");
61 ASSERT(c1_stats->shares_accepted == 2, "client 1 has 2 accepted");
62 ASSERT(c1_stats->shares_rejected == 1, "client 1 has 1 rejected");
63
64 printf("\n--- mining_get_client_stats ---\n");
65 const mining_client_stats_t *notfound = mining_get_client_stats(0xFFFFFFFF);
66 ASSERT(notfound == NULL, "nonexistent client returns NULL");
67
68 printf("\n--- mining_shares_to_allotment_ms ---\n");
69 uint64_t allot1 = mining_shares_to_allotment_ms(0.0, 100.0, 21, 60000);
70 ASSERT(allot1 == 0, "zero hashrate = zero allotment");
71
72 uint64_t allot2 = mining_shares_to_allotment_ms(1.0, 100.0, 21, 60000);
73 ASSERT(allot2 > 0, "positive hashrate gives positive allotment");
74 printf(" INFO: 1 GH/s at 100 sat/GH/s/day, 21 sats/60s => %llu ms\n", (unsigned long long)allot2);
75
76 uint64_t allot3 = mining_shares_to_allotment_ms(10.0, 50.0, 10, 30000);
77 ASSERT(allot3 > 0, "10 GH/s at 50 sat gives positive allotment");
78 printf(" INFO: 10 GH/s at 50 sat/GH/s/day, 10 sats/30s => %llu ms\n", (unsigned long long)allot3);
79
80 printf("\n--- mining_shares_to_allotment_bytes ---\n");
81 uint64_t ab1 = mining_shares_to_allotment_bytes(0.0, 100.0, 21, 1048576);
82 ASSERT(ab1 == 0, "zero hashrate = zero bytes");
83
84 uint64_t ab2 = mining_shares_to_allotment_bytes(1.0, 100.0, 21, 1048576);
85 ASSERT(ab2 > 0, "positive hashrate gives positive bytes");
86
87 printf("\n--- mining_validate_share (stub) ---\n");
88 esp_err_t vr = mining_validate_share(NULL, 0, NULL, 0);
89 ASSERT(vr == ESP_OK, "validate share stub returns OK");
90
91 TEST_SUMMARY();
92}