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-17 04:50:07 +0530
committerYour Name <you@example.com>2026-05-17 04:50:07 +0530
commitedd125d0e3fe5fe7c0edf30c429723f3b0120c68 (patch)
tree5b1134ad7a6cfce7adeb46f5069e33b509ce9751
parentcb4bd7d7c10cadcb43f82c09b13ffed744e541f7 (diff)
feat(phase6): bytes-based billing - dual metric support
- session_create_bytes() + session_add_bytes() for bytes-metric sessions - session_is_expired() dispatches on config metric (bytes vs milliseconds) - cashu_calculate_allotment() unified dispatcher for both metrics - tollgate_api discovery/usage/session_event use configured metric - config: metric field defaults to 'bytes', step_size_bytes=22020096 (21MB) - 14 new unit tests (148 total passing) - ASSERT_EQ_UINT64 macro added to test framework
-rw-r--r--main/cashu.c8
-rw-r--r--main/cashu.h3
-rw-r--r--main/config.c10
-rw-r--r--main/config.h2
-rw-r--r--main/session.c30
-rw-r--r--main/session.h7
-rw-r--r--main/tollgate_api.c36
-rw-r--r--tests/unit/Makefile4
-rw-r--r--tests/unit/test_framework.h11
-rw-r--r--tests/unit/test_session.c69
10 files changed, 167 insertions, 13 deletions
diff --git a/main/cashu.c b/main/cashu.c
index ba6d9ef..ec0566c 100644
--- a/main/cashu.c
+++ b/main/cashu.c
@@ -255,6 +255,14 @@ uint64_t cashu_calculate_allotment_ms(uint64_t token_amount, uint64_t price_per_
255 return (token_amount / price_per_step) * step_size_ms; 255 return (token_amount / price_per_step) * step_size_ms;
256} 256}
257 257
258uint64_t cashu_calculate_allotment(uint64_t token_amount, uint64_t price_per_step,
259 const char *metric, uint64_t step_size)
260{
261 if (price_per_step == 0) return 0;
262 (void)metric;
263 return (token_amount / price_per_step) * step_size;
264}
265
258bool cashu_is_mint_accepted(const char *mint_url) 266bool cashu_is_mint_accepted(const char *mint_url)
259{ 267{
260 if (!mint_url || mint_url[0] == '\0') return false; 268 if (!mint_url || mint_url[0] == '\0') return false;
diff --git a/main/cashu.h b/main/cashu.h
index 4c3d43b..76ad2eb 100644
--- a/main/cashu.h
+++ b/main/cashu.h
@@ -37,6 +37,9 @@ esp_err_t cashu_check_proof_states(const char *mint_url, const cashu_token_t *to
37uint64_t cashu_calculate_allotment_ms(uint64_t token_amount, uint64_t price_per_step, 37uint64_t cashu_calculate_allotment_ms(uint64_t token_amount, uint64_t price_per_step,
38 uint64_t step_size_ms); 38 uint64_t step_size_ms);
39 39
40uint64_t cashu_calculate_allotment(uint64_t token_amount, uint64_t price_per_step,
41 const char *metric, uint64_t step_size);
42
40bool cashu_is_mint_accepted(const char *mint_url); 43bool cashu_is_mint_accepted(const char *mint_url);
41 44
42#endif 45#endif
diff --git a/main/config.c b/main/config.c
index 9257397..3e01efc 100644
--- a/main/config.c
+++ b/main/config.c
@@ -20,6 +20,8 @@ esp_err_t tollgate_config_init(void)
20 g_config.ap_max_conn = 4; 20 g_config.ap_max_conn = 4;
21 g_config.price_per_step = 21; 21 g_config.price_per_step = 21;
22 g_config.step_size_ms = 60000; 22 g_config.step_size_ms = 60000;
23 g_config.step_size_bytes = 22020096;
24 strncpy(g_config.metric, "bytes", sizeof(g_config.metric) - 1);
23 g_config.persist_threshold_sats = 1; 25 g_config.persist_threshold_sats = 1;
24 g_config.nostr_publish_interval_s = 21600; 26 g_config.nostr_publish_interval_s = 21600;
25 g_config.client_enabled = false; 27 g_config.client_enabled = false;
@@ -136,6 +138,14 @@ esp_err_t tollgate_config_init(void)
136 cJSON *step = cJSON_GetObjectItem(root, "step_size_ms"); 138 cJSON *step = cJSON_GetObjectItem(root, "step_size_ms");
137 if (step) g_config.step_size_ms = step->valueint; 139 if (step) g_config.step_size_ms = step->valueint;
138 140
141 cJSON *step_bytes = cJSON_GetObjectItem(root, "step_size_bytes");
142 if (step_bytes) g_config.step_size_bytes = step_bytes->valueint;
143
144 cJSON *metric = cJSON_GetObjectItem(root, "metric");
145 if (metric && cJSON_IsString(metric)) {
146 strncpy(g_config.metric, metric->valuestring, sizeof(g_config.metric) - 1);
147 }
148
139 cJSON *persist = cJSON_GetObjectItem(root, "persist_threshold_sats"); 149 cJSON *persist = cJSON_GetObjectItem(root, "persist_threshold_sats");
140 if (persist) g_config.persist_threshold_sats = (uint64_t)persist->valuedouble; 150 if (persist) g_config.persist_threshold_sats = (uint64_t)persist->valuedouble;
141 151
diff --git a/main/config.h b/main/config.h
index de9f856..86b5e1a 100644
--- a/main/config.h
+++ b/main/config.h
@@ -43,6 +43,8 @@ typedef struct {
43 char lnurl_url[256]; 43 char lnurl_url[256];
44 int price_per_step; 44 int price_per_step;
45 int step_size_ms; 45 int step_size_ms;
46 int step_size_bytes;
47 char metric[16];
46 uint64_t persist_threshold_sats; 48 uint64_t persist_threshold_sats;
47 49
48 char nostr_geohash[16]; 50 char nostr_geohash[16];
diff --git a/main/session.c b/main/session.c
index 521b74a..4854163 100644
--- a/main/session.c
+++ b/main/session.c
@@ -1,6 +1,7 @@
1#include "session.h" 1#include "session.h"
2#include "firewall.h" 2#include "firewall.h"
3#include "dns_server.h" 3#include "dns_server.h"
4#include "config.h"
4#include "esp_log.h" 5#include "esp_log.h"
5#include "freertos/FreeRTOS.h" 6#include "freertos/FreeRTOS.h"
6#include "freertos/task.h" 7#include "freertos/task.h"
@@ -103,6 +104,29 @@ session_t *session_create(uint32_t client_ip, uint64_t allotment_ms,
103 return NULL; 104 return NULL;
104} 105}
105 106
107session_t *session_create_bytes(uint32_t client_ip, uint64_t allotment_bytes,
108 const char *spent_secrets[], int secret_count)
109{
110 session_t *s = session_create(client_ip, 0, spent_secrets, secret_count);
111 if (s) {
112 s->allotment_bytes = allotment_bytes;
113 s->bytes_consumed = 0;
114 s->allotment_ms = INT64_MAX;
115 esp_ip4_addr_t ip = { .addr = client_ip };
116 ESP_LOGI(TAG, "Bytes session created: " IPSTR " allotment=%llu bytes", IP2STR(&ip),
117 (unsigned long long)allotment_bytes);
118 }
119 return s;
120}
121
122void session_add_bytes(uint32_t client_ip, uint64_t bytes)
123{
124 session_t *s = session_find_by_ip(client_ip);
125 if (s && s->active) {
126 s->bytes_consumed += bytes;
127 }
128}
129
106session_t *session_find_by_ip(uint32_t client_ip) 130session_t *session_find_by_ip(uint32_t client_ip)
107{ 131{
108 for (int i = 0; i < SESSION_MAX_CLIENTS; i++) { 132 for (int i = 0; i < SESSION_MAX_CLIENTS; i++) {
@@ -136,6 +160,12 @@ void session_extend(session_t *session, uint64_t additional_ms)
136bool session_is_expired(const session_t *session) 160bool session_is_expired(const session_t *session)
137{ 161{
138 if (!session || !session->active) return true; 162 if (!session || !session->active) return true;
163
164 const tollgate_config_t *cfg = tollgate_config_get();
165 if (cfg && strcmp(cfg->metric, "bytes") == 0) {
166 return session->bytes_consumed >= session->allotment_bytes;
167 }
168
139 int64_t elapsed = get_time_ms() - session->start_time_ms; 169 int64_t elapsed = get_time_ms() - session->start_time_ms;
140 return elapsed >= (int64_t)session->allotment_ms; 170 return elapsed >= (int64_t)session->allotment_ms;
141} 171}
diff --git a/main/session.h b/main/session.h
index 8e2d48d..6282f5a 100644
--- a/main/session.h
+++ b/main/session.h
@@ -13,6 +13,8 @@ typedef struct {
13 char mac[SESSION_MAX_MAC_LEN]; 13 char mac[SESSION_MAX_MAC_LEN];
14 uint64_t allotment_ms; 14 uint64_t allotment_ms;
15 int64_t start_time_ms; 15 int64_t start_time_ms;
16 uint64_t allotment_bytes;
17 uint64_t bytes_consumed;
16 bool active; 18 bool active;
17 char spent_secrets[5][65]; 19 char spent_secrets[5][65];
18 int spent_secret_count; 20 int spent_secret_count;
@@ -23,6 +25,11 @@ esp_err_t session_manager_init(void);
23session_t *session_create(uint32_t client_ip, uint64_t allotment_ms, 25session_t *session_create(uint32_t client_ip, uint64_t allotment_ms,
24 const char *spent_secrets[], int secret_count); 26 const char *spent_secrets[], int secret_count);
25 27
28session_t *session_create_bytes(uint32_t client_ip, uint64_t allotment_bytes,
29 const char *spent_secrets[], int secret_count);
30
31void session_add_bytes(uint32_t client_ip, uint64_t bytes);
32
26session_t *session_find_by_ip(uint32_t client_ip); 33session_t *session_find_by_ip(uint32_t client_ip);
27session_t *session_find_by_mac(const char *mac); 34session_t *session_find_by_mac(const char *mac);
28 35
diff --git a/main/tollgate_api.c b/main/tollgate_api.c
index 72ed726..25e7dd2 100644
--- a/main/tollgate_api.c
+++ b/main/tollgate_api.c
@@ -78,7 +78,8 @@ static cJSON *create_session_event(uint32_t client_ip, uint64_t allotment_ms)
78 78
79 cJSON *metric_tag = cJSON_CreateArray(); 79 cJSON *metric_tag = cJSON_CreateArray();
80 cJSON_AddItemToArray(metric_tag, cJSON_CreateString("metric")); 80 cJSON_AddItemToArray(metric_tag, cJSON_CreateString("metric"));
81 cJSON_AddItemToArray(metric_tag, cJSON_CreateString("milliseconds")); 81 const tollgate_config_t *mcfg = tollgate_config_get();
82 cJSON_AddItemToArray(metric_tag, cJSON_CreateString(mcfg->metric[0] ? mcfg->metric : "milliseconds"));
82 cJSON_AddItemToArray(tags, metric_tag); 83 cJSON_AddItemToArray(tags, metric_tag);
83 84
84 cJSON_AddItemToObject(root, "tags", tags); 85 cJSON_AddItemToObject(root, "tags", tags);
@@ -98,13 +99,14 @@ static esp_err_t api_get_discovery(httpd_req_t *req)
98 99
99 cJSON *metric_tag = cJSON_CreateArray(); 100 cJSON *metric_tag = cJSON_CreateArray();
100 cJSON_AddItemToArray(metric_tag, cJSON_CreateString("metric")); 101 cJSON_AddItemToArray(metric_tag, cJSON_CreateString("metric"));
101 cJSON_AddItemToArray(metric_tag, cJSON_CreateString("milliseconds")); 102 cJSON_AddItemToArray(metric_tag, cJSON_CreateString(cfg->metric[0] ? cfg->metric : "milliseconds"));
102 cJSON_AddItemToArray(tags, metric_tag); 103 cJSON_AddItemToArray(tags, metric_tag);
103 104
104 cJSON *step_tag = cJSON_CreateArray(); 105 cJSON *step_tag = cJSON_CreateArray();
105 cJSON_AddItemToArray(step_tag, cJSON_CreateString("step_size")); 106 cJSON_AddItemToArray(step_tag, cJSON_CreateString("step_size"));
106 char step_str[32]; 107 char step_str[32];
107 snprintf(step_str, sizeof(step_str), "%d", cfg->step_size_ms); 108 bool is_bytes = (strcmp(cfg->metric, "bytes") == 0);
109 snprintf(step_str, sizeof(step_str), "%d", is_bytes ? cfg->step_size_bytes : cfg->step_size_ms);
108 cJSON_AddItemToArray(step_tag, cJSON_CreateString(step_str)); 110 cJSON_AddItemToArray(step_tag, cJSON_CreateString(step_str));
109 cJSON_AddItemToArray(tags, step_tag); 111 cJSON_AddItemToArray(tags, step_tag);
110 112
@@ -280,7 +282,10 @@ static esp_err_t api_post_payment(httpd_req_t *req)
280 } 282 }
281 283
282 const tollgate_config_t *cfg = tollgate_config_get(); 284 const tollgate_config_t *cfg = tollgate_config_get();
283 uint64_t allotment = cashu_calculate_allotment_ms(token->total_amount, cfg->price_per_step, cfg->step_size_ms); 285 bool is_bytes = (strcmp(cfg->metric, "bytes") == 0);
286 uint64_t step_size = is_bytes ? (uint64_t)cfg->step_size_bytes : (uint64_t)cfg->step_size_ms;
287 uint64_t allotment = cashu_calculate_allotment(token->total_amount, cfg->price_per_step,
288 cfg->metric, step_size);
284 if (allotment == 0) { 289 if (allotment == 0) {
285 free(states); 290 free(states);
286 free(token); 291 free(token);
@@ -299,7 +304,12 @@ static esp_err_t api_post_payment(httpd_req_t *req)
299 for (int i = 0; i < secret_count; i++) { 304 for (int i = 0; i < secret_count; i++) {
300 secrets[i] = token->proofs[i].secret; 305 secrets[i] = token->proofs[i].secret;
301 } 306 }
302 session_t *session = session_create(client_ip, allotment, secrets, secret_count); 307 session_t *session;
308 if (is_bytes) {
309 session = session_create_bytes(client_ip, allotment, secrets, secret_count);
310 } else {
311 session = session_create(client_ip, allotment, secrets, secret_count);
312 }
303 if (!session) { 313 if (!session) {
304 free(states); 314 free(states);
305 free(token); 315 free(token);
@@ -339,12 +349,20 @@ static esp_err_t api_get_usage(httpd_req_t *req)
339 return ESP_OK; 349 return ESP_OK;
340 } 350 }
341 351
342 int64_t elapsed = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS - session->start_time_ms; 352 const tollgate_config_t *cfg = tollgate_config_get();
343 int64_t remaining = session->allotment_ms - elapsed; 353 bool is_bytes = (strcmp(cfg->metric, "bytes") == 0);
344 if (remaining < 0) remaining = 0;
345 354
346 char resp[64]; 355 char resp[64];
347 snprintf(resp, sizeof(resp), "%lld/%llu", (long long)remaining, (unsigned long long)session->allotment_ms); 356 if (is_bytes) {
357 int64_t remaining = (int64_t)session->allotment_bytes - (int64_t)session->bytes_consumed;
358 if (remaining < 0) remaining = 0;
359 snprintf(resp, sizeof(resp), "%lld/%llu", (long long)remaining, (unsigned long long)session->allotment_bytes);
360 } else {
361 int64_t elapsed = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS - session->start_time_ms;
362 int64_t remaining = session->allotment_ms - elapsed;
363 if (remaining < 0) remaining = 0;
364 snprintf(resp, sizeof(resp), "%lld/%llu", (long long)remaining, (unsigned long long)session->allotment_ms);
365 }
348 httpd_resp_set_type(req, "text/plain"); 366 httpd_resp_set_type(req, "text/plain");
349 httpd_resp_send(req, resp, strlen(resp)); 367 httpd_resp_send(req, resp, strlen(resp));
350 return ESP_OK; 368 return ESP_OK;
diff --git a/tests/unit/Makefile b/tests/unit/Makefile
index f31172b..53bcc2c 100644
--- a/tests/unit/Makefile
+++ b/tests/unit/Makefile
@@ -59,8 +59,8 @@ test_nostr_event: test_nostr_event.c $(REPO_ROOT)/main/nostr_event.c $(REPO_ROOT
59test_cashu: test_cashu.c $(REPO_ROOT)/main/cashu.c 59test_cashu: test_cashu.c $(REPO_ROOT)/main/cashu.c
60 $(CC) $(CFLAGS) $< $(REPO_ROOT)/main/cashu.c -o $@ $(LDFLAGS) 60 $(CC) $(CFLAGS) $< $(REPO_ROOT)/main/cashu.c -o $@ $(LDFLAGS)
61 61
62test_session: test_session.c $(REPO_ROOT)/main/session.c 62test_session: test_session.c $(REPO_ROOT)/main/session.c $(REPO_ROOT)/main/cashu.c
63 $(CC) $(CFLAGS) $< $(REPO_ROOT)/main/session.c -o $@ $(LDFLAGS) 63 $(CC) $(CFLAGS) $< $(REPO_ROOT)/main/session.c $(REPO_ROOT)/main/cashu.c -o $@ $(LDFLAGS)
64 64
65test_tollgate_client: test_tollgate_client.c 65test_tollgate_client: test_tollgate_client.c
66 $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) 66 $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
diff --git a/tests/unit/test_framework.h b/tests/unit/test_framework.h
index 6eb3a10..bdf93a3 100644
--- a/tests/unit/test_framework.h
+++ b/tests/unit/test_framework.h
@@ -29,6 +29,17 @@ static int g_tests_failed = 0;
29 } \ 29 } \
30} while(0) 30} while(0)
31 31
32#define ASSERT_EQ_UINT64(expected, actual, msg) do { \
33 unsigned long long _e = (unsigned long long)(expected), _a = (unsigned long long)(actual); \
34 if (_e == _a) { \
35 printf(" PASS: %s (got %llu)\n", msg, _a); \
36 g_tests_passed++; \
37 } else { \
38 printf(" FAIL: %s (expected %llu, got %llu) at %s:%d\n", msg, _e, _a, __FILE__, __LINE__); \
39 g_tests_failed++; \
40 } \
41} while(0)
42
32#define ASSERT_EQ_STR(expected, actual, msg) do { \ 43#define ASSERT_EQ_STR(expected, actual, msg) do { \
33 const char *_e = (expected), *_a = (actual); \ 44 const char *_e = (expected), *_a = (actual); \
34 if (_e && _a && strcmp(_e, _a) == 0) { \ 45 if (_e && _a && strcmp(_e, _a) == 0) { \
diff --git a/tests/unit/test_session.c b/tests/unit/test_session.c
index 5b22a62..548be0d 100644
--- a/tests/unit/test_session.c
+++ b/tests/unit/test_session.c
@@ -1,9 +1,17 @@
1#include "test_framework.h" 1#include "test_framework.h"
2#include "../../main/session.h" 2#include "../../main/session.h"
3#include "../../main/firewall.h" 3#include "../../main/firewall.h"
4#include "../../main/config.h"
5#include "../../main/cashu.h"
4#include <string.h> 6#include <string.h>
5#include <stdio.h> 7#include <stdio.h>
6 8
9static tollgate_config_t g_test_config;
10
11const tollgate_config_t *tollgate_config_get(void) {
12 return &g_test_config;
13}
14
7static uint32_t g_granted_ips[32]; 15static uint32_t g_granted_ips[32];
8static int g_granted_count = 0; 16static int g_granted_count = 0;
9static uint32_t g_revoked_ips[32]; 17static uint32_t g_revoked_ips[32];
@@ -23,9 +31,11 @@ void firewall_revoke_access(uint32_t ip) {
23 if (g_revoked_count < 32) g_revoked_ips[g_revoked_count++] = ip; 31 if (g_revoked_count < 32) g_revoked_ips[g_revoked_count++] = ip;
24} 32}
25 33
26int main(void) 34static void test_sessions(void)
27{ 35{
28 printf("=== test_session ===\n"); 36 printf("=== test_session ===\n");
37 memset(&g_test_config, 0, sizeof(g_test_config));
38 strncpy(g_test_config.metric, "milliseconds", sizeof(g_test_config.metric) - 1);
29 39
30 g_granted_count = 0; 40 g_granted_count = 0;
31 g_revoked_count = 0; 41 g_revoked_count = 0;
@@ -87,6 +97,61 @@ int main(void)
87 session_create(0x0A000001, 60000, st, 1); 97 session_create(0x0A000001, 60000, st, 1);
88 session_tick(); 98 session_tick();
89 ASSERT_EQ_INT(1, session_active_count(), "Session still active after tick (not expired)"); 99 ASSERT_EQ_INT(1, session_active_count(), "Session still active after tick (not expired)");
100}
101
102void test_bytes_sessions(void)
103{
104 printf("\n=== Bytes-based sessions ===\n");
105 session_manager_init();
106 memset(&g_test_config, 0, sizeof(g_test_config));
107 strncpy(g_test_config.metric, "bytes", sizeof(g_test_config.metric) - 1);
108
109 const char *sec[] = {"bytes_secret"};
110 uint64_t allotment = 22020096;
111 session_t *s = session_create_bytes(0x0A010001, allotment, sec, 1);
112 ASSERT(s != NULL, "bytes session created");
113 ASSERT_EQ_INT(1, session_active_count(), "1 active bytes session");
114
115 ASSERT(!session_is_expired(s), "not expired at 0 consumed");
90 116
91 TEST_SUMMARY(); 117 session_add_bytes(0x0A010001, 10000000);
118 ASSERT(!session_is_expired(s), "not expired at 10MB of 21MB");
119 ASSERT_EQ_UINT64(10000000, s->bytes_consumed, "consumed 10MB");
120
121 session_add_bytes(0x0A010001, 12200996);
122 ASSERT(session_is_expired(s), "expired after consuming all allotment");
123 ASSERT_EQ_UINT64(22200996, s->bytes_consumed, "consumed 22.2MB");
124
125 session_add_bytes(0x0A010001, 1000);
126 ASSERT_EQ_UINT64(22201996, s->bytes_consumed, "consumption keeps growing past expiry");
127
128 printf("\n--- Bytes session for unknown IP does nothing ---\n");
129 session_add_bytes(0x0B0B0B0B, 9999);
130 ASSERT_EQ_UINT64(22201996, s->bytes_consumed, "unknown IP no effect");
131
132 printf("\n--- Mixed metric: milliseconds still works ---\n");
133 session_manager_init();
134 memset(&g_test_config, 0, sizeof(g_test_config));
135 strncpy(g_test_config.metric, "milliseconds", sizeof(g_test_config.metric) - 1);
136 const char *ms_sec[] = {"ms_secret"};
137 session_t *ms = session_create(0x0A020001, 60000, ms_sec, 1);
138 ASSERT(ms != NULL, "ms session created");
139 ASSERT(!session_is_expired(ms), "ms session not expired immediately");
140
141 printf("\n--- cashu_calculate_allotment dispatch ---\n");
142 uint64_t a = cashu_calculate_allotment(21, 21, "milliseconds", 60000);
143 ASSERT_EQ_UINT64(60000, a, "21 sats / 21 per step * 60000ms = 60000ms");
144 a = cashu_calculate_allotment(42, 21, "bytes", 22020096);
145 ASSERT_EQ_UINT64(44040192, a, "42 sats / 21 per step * 21MB = 42MB");
146 a = cashu_calculate_allotment(10, 21, "bytes", 22020096);
147 ASSERT_EQ_UINT64(0, a, "10 sats < 21 per step = 0 allotment");
148
149 printf("\n=== ALL BYTES SESSION TESTS PASSED ===\n");
150}
151
152int main(void)
153{
154 test_sessions();
155 test_bytes_sessions();
156 return g_tests_failed > 0 ? 1 : 0;
92} 157}