From edd125d0e3fe5fe7c0edf30c429723f3b0120c68 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 17 May 2026 04:50:07 +0530 Subject: 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 --- main/cashu.c | 8 ++++++ main/cashu.h | 3 ++ main/config.c | 10 +++++++ main/config.h | 2 ++ main/session.c | 30 ++++++++++++++++++++ main/session.h | 7 +++++ main/tollgate_api.c | 36 +++++++++++++++++------ tests/unit/Makefile | 4 +-- tests/unit/test_framework.h | 11 ++++++++ tests/unit/test_session.c | 69 +++++++++++++++++++++++++++++++++++++++++++-- 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_ return (token_amount / price_per_step) * step_size_ms; } +uint64_t cashu_calculate_allotment(uint64_t token_amount, uint64_t price_per_step, + const char *metric, uint64_t step_size) +{ + if (price_per_step == 0) return 0; + (void)metric; + return (token_amount / price_per_step) * step_size; +} + bool cashu_is_mint_accepted(const char *mint_url) { 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 uint64_t cashu_calculate_allotment_ms(uint64_t token_amount, uint64_t price_per_step, uint64_t step_size_ms); +uint64_t cashu_calculate_allotment(uint64_t token_amount, uint64_t price_per_step, + const char *metric, uint64_t step_size); + bool cashu_is_mint_accepted(const char *mint_url); #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) g_config.ap_max_conn = 4; g_config.price_per_step = 21; g_config.step_size_ms = 60000; + g_config.step_size_bytes = 22020096; + strncpy(g_config.metric, "bytes", sizeof(g_config.metric) - 1); g_config.persist_threshold_sats = 1; g_config.nostr_publish_interval_s = 21600; g_config.client_enabled = false; @@ -136,6 +138,14 @@ esp_err_t tollgate_config_init(void) cJSON *step = cJSON_GetObjectItem(root, "step_size_ms"); if (step) g_config.step_size_ms = step->valueint; + cJSON *step_bytes = cJSON_GetObjectItem(root, "step_size_bytes"); + if (step_bytes) g_config.step_size_bytes = step_bytes->valueint; + + cJSON *metric = cJSON_GetObjectItem(root, "metric"); + if (metric && cJSON_IsString(metric)) { + strncpy(g_config.metric, metric->valuestring, sizeof(g_config.metric) - 1); + } + cJSON *persist = cJSON_GetObjectItem(root, "persist_threshold_sats"); if (persist) g_config.persist_threshold_sats = (uint64_t)persist->valuedouble; 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 { char lnurl_url[256]; int price_per_step; int step_size_ms; + int step_size_bytes; + char metric[16]; uint64_t persist_threshold_sats; 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 @@ #include "session.h" #include "firewall.h" #include "dns_server.h" +#include "config.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" @@ -103,6 +104,29 @@ session_t *session_create(uint32_t client_ip, uint64_t allotment_ms, return NULL; } +session_t *session_create_bytes(uint32_t client_ip, uint64_t allotment_bytes, + const char *spent_secrets[], int secret_count) +{ + session_t *s = session_create(client_ip, 0, spent_secrets, secret_count); + if (s) { + s->allotment_bytes = allotment_bytes; + s->bytes_consumed = 0; + s->allotment_ms = INT64_MAX; + esp_ip4_addr_t ip = { .addr = client_ip }; + ESP_LOGI(TAG, "Bytes session created: " IPSTR " allotment=%llu bytes", IP2STR(&ip), + (unsigned long long)allotment_bytes); + } + return s; +} + +void session_add_bytes(uint32_t client_ip, uint64_t bytes) +{ + session_t *s = session_find_by_ip(client_ip); + if (s && s->active) { + s->bytes_consumed += bytes; + } +} + session_t *session_find_by_ip(uint32_t client_ip) { for (int i = 0; i < SESSION_MAX_CLIENTS; i++) { @@ -136,6 +160,12 @@ void session_extend(session_t *session, uint64_t additional_ms) bool session_is_expired(const session_t *session) { if (!session || !session->active) return true; + + const tollgate_config_t *cfg = tollgate_config_get(); + if (cfg && strcmp(cfg->metric, "bytes") == 0) { + return session->bytes_consumed >= session->allotment_bytes; + } + int64_t elapsed = get_time_ms() - session->start_time_ms; return elapsed >= (int64_t)session->allotment_ms; } 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 { char mac[SESSION_MAX_MAC_LEN]; uint64_t allotment_ms; int64_t start_time_ms; + uint64_t allotment_bytes; + uint64_t bytes_consumed; bool active; char spent_secrets[5][65]; int spent_secret_count; @@ -23,6 +25,11 @@ esp_err_t session_manager_init(void); session_t *session_create(uint32_t client_ip, uint64_t allotment_ms, const char *spent_secrets[], int secret_count); +session_t *session_create_bytes(uint32_t client_ip, uint64_t allotment_bytes, + const char *spent_secrets[], int secret_count); + +void session_add_bytes(uint32_t client_ip, uint64_t bytes); + session_t *session_find_by_ip(uint32_t client_ip); session_t *session_find_by_mac(const char *mac); 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) cJSON *metric_tag = cJSON_CreateArray(); cJSON_AddItemToArray(metric_tag, cJSON_CreateString("metric")); - cJSON_AddItemToArray(metric_tag, cJSON_CreateString("milliseconds")); + const tollgate_config_t *mcfg = tollgate_config_get(); + cJSON_AddItemToArray(metric_tag, cJSON_CreateString(mcfg->metric[0] ? mcfg->metric : "milliseconds")); cJSON_AddItemToArray(tags, metric_tag); cJSON_AddItemToObject(root, "tags", tags); @@ -98,13 +99,14 @@ static esp_err_t api_get_discovery(httpd_req_t *req) cJSON *metric_tag = cJSON_CreateArray(); cJSON_AddItemToArray(metric_tag, cJSON_CreateString("metric")); - cJSON_AddItemToArray(metric_tag, cJSON_CreateString("milliseconds")); + cJSON_AddItemToArray(metric_tag, cJSON_CreateString(cfg->metric[0] ? cfg->metric : "milliseconds")); cJSON_AddItemToArray(tags, metric_tag); cJSON *step_tag = cJSON_CreateArray(); cJSON_AddItemToArray(step_tag, cJSON_CreateString("step_size")); char step_str[32]; - snprintf(step_str, sizeof(step_str), "%d", cfg->step_size_ms); + bool is_bytes = (strcmp(cfg->metric, "bytes") == 0); + snprintf(step_str, sizeof(step_str), "%d", is_bytes ? cfg->step_size_bytes : cfg->step_size_ms); cJSON_AddItemToArray(step_tag, cJSON_CreateString(step_str)); cJSON_AddItemToArray(tags, step_tag); @@ -280,7 +282,10 @@ static esp_err_t api_post_payment(httpd_req_t *req) } const tollgate_config_t *cfg = tollgate_config_get(); - uint64_t allotment = cashu_calculate_allotment_ms(token->total_amount, cfg->price_per_step, cfg->step_size_ms); + bool is_bytes = (strcmp(cfg->metric, "bytes") == 0); + uint64_t step_size = is_bytes ? (uint64_t)cfg->step_size_bytes : (uint64_t)cfg->step_size_ms; + uint64_t allotment = cashu_calculate_allotment(token->total_amount, cfg->price_per_step, + cfg->metric, step_size); if (allotment == 0) { free(states); free(token); @@ -299,7 +304,12 @@ static esp_err_t api_post_payment(httpd_req_t *req) for (int i = 0; i < secret_count; i++) { secrets[i] = token->proofs[i].secret; } - session_t *session = session_create(client_ip, allotment, secrets, secret_count); + session_t *session; + if (is_bytes) { + session = session_create_bytes(client_ip, allotment, secrets, secret_count); + } else { + session = session_create(client_ip, allotment, secrets, secret_count); + } if (!session) { free(states); free(token); @@ -339,12 +349,20 @@ static esp_err_t api_get_usage(httpd_req_t *req) return ESP_OK; } - int64_t elapsed = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS - session->start_time_ms; - int64_t remaining = session->allotment_ms - elapsed; - if (remaining < 0) remaining = 0; + const tollgate_config_t *cfg = tollgate_config_get(); + bool is_bytes = (strcmp(cfg->metric, "bytes") == 0); char resp[64]; - snprintf(resp, sizeof(resp), "%lld/%llu", (long long)remaining, (unsigned long long)session->allotment_ms); + if (is_bytes) { + int64_t remaining = (int64_t)session->allotment_bytes - (int64_t)session->bytes_consumed; + if (remaining < 0) remaining = 0; + snprintf(resp, sizeof(resp), "%lld/%llu", (long long)remaining, (unsigned long long)session->allotment_bytes); + } else { + int64_t elapsed = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS - session->start_time_ms; + int64_t remaining = session->allotment_ms - elapsed; + if (remaining < 0) remaining = 0; + snprintf(resp, sizeof(resp), "%lld/%llu", (long long)remaining, (unsigned long long)session->allotment_ms); + } httpd_resp_set_type(req, "text/plain"); httpd_resp_send(req, resp, strlen(resp)); 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 test_cashu: test_cashu.c $(REPO_ROOT)/main/cashu.c $(CC) $(CFLAGS) $< $(REPO_ROOT)/main/cashu.c -o $@ $(LDFLAGS) -test_session: test_session.c $(REPO_ROOT)/main/session.c - $(CC) $(CFLAGS) $< $(REPO_ROOT)/main/session.c -o $@ $(LDFLAGS) +test_session: test_session.c $(REPO_ROOT)/main/session.c $(REPO_ROOT)/main/cashu.c + $(CC) $(CFLAGS) $< $(REPO_ROOT)/main/session.c $(REPO_ROOT)/main/cashu.c -o $@ $(LDFLAGS) test_tollgate_client: test_tollgate_client.c $(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; } \ } while(0) +#define ASSERT_EQ_UINT64(expected, actual, msg) do { \ + unsigned long long _e = (unsigned long long)(expected), _a = (unsigned long long)(actual); \ + if (_e == _a) { \ + printf(" PASS: %s (got %llu)\n", msg, _a); \ + g_tests_passed++; \ + } else { \ + printf(" FAIL: %s (expected %llu, got %llu) at %s:%d\n", msg, _e, _a, __FILE__, __LINE__); \ + g_tests_failed++; \ + } \ +} while(0) + #define ASSERT_EQ_STR(expected, actual, msg) do { \ const char *_e = (expected), *_a = (actual); \ 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 @@ #include "test_framework.h" #include "../../main/session.h" #include "../../main/firewall.h" +#include "../../main/config.h" +#include "../../main/cashu.h" #include #include +static tollgate_config_t g_test_config; + +const tollgate_config_t *tollgate_config_get(void) { + return &g_test_config; +} + static uint32_t g_granted_ips[32]; static int g_granted_count = 0; static uint32_t g_revoked_ips[32]; @@ -23,9 +31,11 @@ void firewall_revoke_access(uint32_t ip) { if (g_revoked_count < 32) g_revoked_ips[g_revoked_count++] = ip; } -int main(void) +static void test_sessions(void) { printf("=== test_session ===\n"); + memset(&g_test_config, 0, sizeof(g_test_config)); + strncpy(g_test_config.metric, "milliseconds", sizeof(g_test_config.metric) - 1); g_granted_count = 0; g_revoked_count = 0; @@ -87,6 +97,61 @@ int main(void) session_create(0x0A000001, 60000, st, 1); session_tick(); ASSERT_EQ_INT(1, session_active_count(), "Session still active after tick (not expired)"); +} + +void test_bytes_sessions(void) +{ + printf("\n=== Bytes-based sessions ===\n"); + session_manager_init(); + memset(&g_test_config, 0, sizeof(g_test_config)); + strncpy(g_test_config.metric, "bytes", sizeof(g_test_config.metric) - 1); + + const char *sec[] = {"bytes_secret"}; + uint64_t allotment = 22020096; + session_t *s = session_create_bytes(0x0A010001, allotment, sec, 1); + ASSERT(s != NULL, "bytes session created"); + ASSERT_EQ_INT(1, session_active_count(), "1 active bytes session"); + + ASSERT(!session_is_expired(s), "not expired at 0 consumed"); - TEST_SUMMARY(); + session_add_bytes(0x0A010001, 10000000); + ASSERT(!session_is_expired(s), "not expired at 10MB of 21MB"); + ASSERT_EQ_UINT64(10000000, s->bytes_consumed, "consumed 10MB"); + + session_add_bytes(0x0A010001, 12200996); + ASSERT(session_is_expired(s), "expired after consuming all allotment"); + ASSERT_EQ_UINT64(22200996, s->bytes_consumed, "consumed 22.2MB"); + + session_add_bytes(0x0A010001, 1000); + ASSERT_EQ_UINT64(22201996, s->bytes_consumed, "consumption keeps growing past expiry"); + + printf("\n--- Bytes session for unknown IP does nothing ---\n"); + session_add_bytes(0x0B0B0B0B, 9999); + ASSERT_EQ_UINT64(22201996, s->bytes_consumed, "unknown IP no effect"); + + printf("\n--- Mixed metric: milliseconds still works ---\n"); + session_manager_init(); + memset(&g_test_config, 0, sizeof(g_test_config)); + strncpy(g_test_config.metric, "milliseconds", sizeof(g_test_config.metric) - 1); + const char *ms_sec[] = {"ms_secret"}; + session_t *ms = session_create(0x0A020001, 60000, ms_sec, 1); + ASSERT(ms != NULL, "ms session created"); + ASSERT(!session_is_expired(ms), "ms session not expired immediately"); + + printf("\n--- cashu_calculate_allotment dispatch ---\n"); + uint64_t a = cashu_calculate_allotment(21, 21, "milliseconds", 60000); + ASSERT_EQ_UINT64(60000, a, "21 sats / 21 per step * 60000ms = 60000ms"); + a = cashu_calculate_allotment(42, 21, "bytes", 22020096); + ASSERT_EQ_UINT64(44040192, a, "42 sats / 21 per step * 21MB = 42MB"); + a = cashu_calculate_allotment(10, 21, "bytes", 22020096); + ASSERT_EQ_UINT64(0, a, "10 sats < 21 per step = 0 allotment"); + + printf("\n=== ALL BYTES SESSION TESTS PASSED ===\n"); +} + +int main(void) +{ + test_sessions(); + test_bytes_sessions(); + return g_tests_failed > 0 ? 1 : 0; } -- cgit v1.2.3