forge: add patch 16 — stabilize group request tokens

This commit is contained in:
cj-vana
2026-05-17 21:58:41 -06:00
parent 6123355ced
commit 3bbd23fd87
2 changed files with 353 additions and 15 deletions

View File

@@ -0,0 +1,331 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: cj-vana <cj@depth23.online>
Date: Sun, 17 May 2026 21:55:45 -0600
Subject: [PATCH] Stabilize group bot request tokens
---
examples/companion_radio/FirmwareBot.cpp | 51 ++++++++++--------
examples/companion_radio/MyMesh.cpp | 69 ++++++++++++++++++++----
2 files changed, 89 insertions(+), 31 deletions(-)
diff --git a/examples/companion_radio/FirmwareBot.cpp b/examples/companion_radio/FirmwareBot.cpp
index e228b7c8..3d3ad96f 100644
--- a/examples/companion_radio/FirmwareBot.cpp
+++ b/examples/companion_radio/FirmwareBot.cpp
@@ -319,10 +319,8 @@ BotWriteResult writeAckResponse(const BotMessage& message, const BotCommand& com
if (!output || output_len == 0) return BOT_WRITE_NO_SPACE;
const char* sender = message.sender_name[0] ? message.sender_name : "unknown";
- const char* connection = message.channel_kind == BOT_CHANNEL_DM
- ? "direct"
- : (message.channel_name[0] ? message.channel_name : "channel");
- int n = snprintf(output, output_len, "@[%s] %s", sender, connection);
+ int n;
+ n = snprintf(output, output_len, "@[%s]", sender);
if (n < 0) {
output[0] = 0;
return BOT_WRITE_NO_SPACE;
@@ -337,9 +335,9 @@ BotWriteResult writeAckResponse(const BotMessage& message, const BotCommand& com
char path_block[40];
int path_n;
if (message.path_hash_count == 0 || message.path_hash_size == 0) {
- path_n = snprintf(path_block, sizeof(path_block), " | 0h SNR %s%d.%02d", sign, value / 4, (value % 4) * 25);
+ path_n = snprintf(path_block, sizeof(path_block), " | 0 hops, SNR %s%d.%02d", sign, value / 4, (value % 4) * 25);
} else {
- path_n = snprintf(path_block, sizeof(path_block), " | %uh@%uB SNR %s%d.%02d",
+ path_n = snprintf(path_block, sizeof(path_block), " | %u hops, %u-byte hashes, SNR %s%d.%02d",
(unsigned)message.path_hash_count, (unsigned)message.path_hash_size, sign, value / 4,
(value % 4) * 25);
}
@@ -379,6 +377,24 @@ BotFingerprint fingerprintFor(const BotMessage& message) {
return fingerprint;
}
+static int hexNibble(char c);
+
+bool parseRequestTokenPrefix(const char* text, size_t text_len, uint16_t* token, size_t* prefix_len) {
+ if (token) *token = 0;
+ if (prefix_len) *prefix_len = 0;
+ if (!text || text_len < 7) return false;
+ if (text[0] != '[' || text[5] != ']' || text[6] != ' ') return false;
+ uint16_t value = 0;
+ for (int i = 0; i < 4; i++) {
+ int n = hexNibble(text[1 + i]);
+ if (n < 0) return false;
+ value = (uint16_t)((value << 4) | (uint16_t)n);
+ }
+ if (token) *token = value;
+ if (prefix_len) *prefix_len = 7;
+ return true;
+}
+
BotFingerprint responseFingerprintFor(const BotMessage& message, const char* response_text, size_t response_text_len) {
uint64_t hash = 1469598103934665603ULL;
hash = fnv1aUpdateChannel(hash, message);
@@ -387,6 +403,13 @@ BotFingerprint responseFingerprintFor(const BotMessage& message, const char* res
hash = fnv1aUpdateBytes(hash, message.sender_key_prefix, message.sender_key_prefix_len);
}
+ uint16_t token = 0;
+ size_t prefix_len = 0;
+ if (parseRequestTokenPrefix(response_text, response_text_len, &token, &prefix_len)) {
+ response_text += prefix_len;
+ response_text_len -= prefix_len;
+ }
+
char normalized[BOT_MAX_RESPONSE_LEN + 1];
size_t normalized_len = 0;
normalizeText(response_text, response_text_len, normalized, sizeof(normalized), &normalized_len);
@@ -421,22 +444,6 @@ static int hexNibble(char c) {
return -1;
}
-bool parseRequestTokenPrefix(const char* text, size_t text_len, uint16_t* token, size_t* prefix_len) {
- if (token) *token = 0;
- if (prefix_len) *prefix_len = 0;
- if (!text || text_len < 7) return false; // "[XXXX] " = 7 chars minimum
- if (text[0] != '[' || text[5] != ']' || text[6] != ' ') return false;
- uint16_t value = 0;
- for (int i = 0; i < 4; i++) {
- int n = hexNibble(text[1 + i]);
- if (n < 0) return false;
- value = (uint16_t)((value << 4) | (uint16_t)n);
- }
- if (token) *token = value;
- if (prefix_len) *prefix_len = 7;
- return true;
-}
-
BotWriteResult prependRequestToken(BotFingerprint request_fingerprint, char* text, size_t text_len, size_t buf_len,
size_t* new_len) {
if (new_len) *new_len = text_len;
diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp
index 9121d42c..9aad730d 100644
--- a/examples/companion_radio/MyMesh.cpp
+++ b/examples/companion_radio/MyMesh.cpp
@@ -121,6 +121,16 @@
#define LAZY_CONTACTS_WRITE_DELAY 5000
#if CMESH_BOT_ENABLED
+#ifndef CMESH_BOT_DEBUG
+#define CMESH_BOT_DEBUG 0
+#endif
+#if CMESH_BOT_DEBUG
+#define BOT_DEBUG_PRINTF(...) Serial.printf(__VA_ARGS__)
+#define BOT_DEBUG_PRINTLN(value) Serial.println(value)
+#else
+#define BOT_DEBUG_PRINTF(...)
+#define BOT_DEBUG_PRINTLN(value)
+#endif
#endif
#define PUBLIC_GROUP_PSK "izOH6cXN6mrJ5e26oRXNcg=="
@@ -1055,6 +1065,7 @@ bool MyMesh::handleBotCLI(const char *args) {
void MyMesh::observeBotDirectMessage(const ContactInfo &from, uint32_t sender_timestamp, const uint8_t *sender_prefix,
size_t sender_prefix_len, const char *text, const mesh::Packet *packet) {
+ BOT_DEBUG_PRINTF("CBOT-DBG: DM rx from=%s text=%.40s enabled=%d\r\n", from.name, text, bot_prefs.enabled);
BotMessage message;
memset(&message, 0, sizeof(message));
message.channel_kind = BotPolicy::classifyChannel(NULL, 0, true, bot_prefs);
@@ -1072,7 +1083,12 @@ void MyMesh::observeBotDirectMessage(const ContactInfo &from, uint32_t sender_ti
if (rssi < -32768.0f) rssi = -32768.0f;
recordBotNeighbor(from.id.pub_key, (int16_t)rssi, (int8_t)(packet->getSNR() * 4));
}
- if (from.out_path_len != OUT_PATH_UNKNOWN && mesh::Packet::isValidPathLen(from.out_path_len)) {
+ if (packet && packet->isRouteFlood() && packet->path_len <= 0xFF && mesh::Packet::isValidPathLen((uint8_t)packet->path_len)) {
+ message.path_len = (uint8_t)packet->path_len;
+ message.path_hash_size = packet->getPathHashSize();
+ message.path_hash_count = packet->getPathHashCount();
+ message.path = packet->path;
+ } else if (from.out_path_len != OUT_PATH_UNKNOWN && mesh::Packet::isValidPathLen(from.out_path_len)) {
message.path_len = from.out_path_len;
message.path_hash_size = (from.out_path_len >> 6) + 1;
message.path_hash_count = from.out_path_len & 63;
@@ -1090,6 +1106,7 @@ void MyMesh::observeBotDirectMessage(const ContactInfo &from, uint32_t sender_ti
void MyMesh::observeBotChannelMessage(uint8_t channel_idx, const char *channel_name, const char *text,
uint32_t sender_timestamp, const mesh::Packet *packet) {
+ BOT_DEBUG_PRINTF("CBOT-DBG: CH rx ch=%s text=%.40s enabled=%d\r\n", channel_name ? channel_name : "?", text, bot_prefs.enabled);
BotMessage message;
memset(&message, 0, sizeof(message));
size_t channel_len = botBoundedStrLen(channel_name, BOT_MAX_CHANNEL_NAME_LEN);
@@ -1429,7 +1446,7 @@ bool MyMesh::dispatchBotTraceDirectLink(const BotMessage &message, const Contact
return true;
}
- BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message);
+ BotFingerprint request_fingerprint = botRequestFingerprintFor(message, channel_idx);
BotFingerprint response_fingerprint =
FirmwareBot::responseFingerprintFor(message, final_response, final_response_len);
BotFingerprint fingerprint;
@@ -1503,7 +1520,7 @@ bool MyMesh::handleBotTraceCommand(const BotMessage &message, const ContactInfo
}
if (!have_path || !botTracePathShapeValid(path_len, flags)) return false;
- BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message);
+ BotFingerprint request_fingerprint = botRequestFingerprintFor(message, channel_idx);
char response[BOT_MAX_RESPONSE_LEN + 1];
size_t response_len = botFormatTraceSent(response, sizeof(response), (uint8_t)(path_len / hash_size));
char final_response[BOT_MAX_RESPONSE_LEN + 1];
@@ -1737,6 +1754,11 @@ bool MyMesh::observeKnownBotResponse(const BotMessage &message, bool authoritati
return false;
}
+BotFingerprint MyMesh::botRequestFingerprintFor(const BotMessage &message, uint8_t channel_idx) {
+ (void)channel_idx;
+ return FirmwareBot::fingerprintFor(message);
+}
+
bool MyMesh::observeBotGroupResponse(const BotMessage &message) {
if (!BotPolicy::isPrefixlessCommandAllowed(message.channel_kind)) return false;
@@ -1749,6 +1771,9 @@ bool MyMesh::observeBotGroupResponse(const BotMessage &message) {
if (FirmwareBot::parseRequestTokenPrefix(message.text, message.text_len, &token, &token_prefix_len)) {
if (ResponseCoordinator::suppressByRequestToken(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, token)) {
token_suppressed = true;
+ BOT_DEBUG_PRINTF("CBOT-DBG: token suppress %04x\r\n", (unsigned)token);
+ } else {
+ BOT_DEBUG_PRINTF("CBOT-DBG: token miss %04x\r\n", (unsigned)token);
}
}
@@ -1763,9 +1788,12 @@ bool MyMesh::observeBotGroupResponse(const BotMessage &message) {
}
void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx) {
+ BOT_DEBUG_PRINTF("CBOT-DBG: record kind=%d direct=%d enabled=%d\r\n", (int)message.channel_kind, direct_recipient != NULL, bot_prefs.enabled);
bot_stats.observed_messages++;
BotPolicyDecision decision = BotPolicy::decide(message.channel_kind);
+ BOT_DEBUG_PRINTF("CBOT-DBG: policy decision=%d\r\n", (int)decision);
if (decision == BOT_POLICY_IGNORE) {
+ BOT_DEBUG_PRINTLN("CBOT-DBG: IGNORE -> drop");
bot_stats.ignored_messages++;
return;
}
@@ -1776,24 +1804,28 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *
}
if (!bot_prefs.enabled) {
+ BOT_DEBUG_PRINTLN("CBOT-DBG: bot disabled -> drop");
bot_stats.ignored_messages++;
return;
}
- if (observeKnownBotResponse(message, direct_recipient != NULL)) return;
- if (!direct_recipient && observeBotGroupResponse(message)) return;
+ if (observeKnownBotResponse(message, direct_recipient != NULL)) { BOT_DEBUG_PRINTLN("CBOT-DBG: known-bot suppress"); return; }
+ if (!direct_recipient && observeBotGroupResponse(message)) { BOT_DEBUG_PRINTLN("CBOT-DBG: group-response suppress"); return; }
sendQueuedBotResponses();
BotCommand command;
if (!FirmwareBot::parseCommand(message.text, message.text_len, &command,
BotPolicy::isPrefixlessCommandAllowed(message.channel_kind))) {
+ BOT_DEBUG_PRINTF("CBOT-DBG: parseCommand FAILED text=%.40s prefixless=%d\r\n", message.text, BotPolicy::isPrefixlessCommandAllowed(message.channel_kind));
if (message.text_len > 0 && (message.text[0] == '!' || message.text[0] == '/')) bot_stats.parse_errors++;
return;
}
+ BOT_DEBUG_PRINTF("CBOT-DBG: parsed cmd.id=%d name=%s args=%.30s\r\n", (int)command.id, command.name, command.args);
if ((command.id != BOT_COMMAND_UNKNOWN && command.id != BOT_COMMAND_UNSUPPORTED &&
!BotPrefsCodec::commandEnabled(bot_prefs, command.id)) ||
FirmwareBot::isCommandOnCooldown(bot_command_cooldowns, BOT_COMMAND_COOLDOWN_SLOTS, command.id, _ms->getMillis())) {
+ BOT_DEBUG_PRINTLN("CBOT-DBG: cmd disabled or on cooldown -> drop");
bot_stats.ignored_messages++;
return;
}
@@ -1868,7 +1900,7 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *
return;
}
- BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message);
+ BotFingerprint request_fingerprint = botRequestFingerprintFor(message, channel_idx);
BotFingerprint response_fingerprint = FirmwareBot::responseFingerprintFor(message, final_response, final_response_len);
BotFingerprint fingerprint;
uint32_t due_at_millis = 0;
@@ -1884,7 +1916,11 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *
queue_depth, bot_prefs.normal_delay_ms,
bot_prefs.normal_jitter_ms, bot_prefs.hop_step_ms,
&fingerprint, &due_at_millis);
+ BOT_DEBUG_PRINTF("CBOT-DBG: schedule result=%d due=%lu now=%lu reqfp=%08lx%08lx\r\n",
+ (int)schedule, (unsigned long)due_at_millis, (unsigned long)_ms->getMillis(),
+ (unsigned long)(request_fingerprint.value >> 32), (unsigned long)(request_fingerprint.value & 0xFFFFFFFFul));
if (schedule == BOT_COORDINATOR_NO_SPACE || schedule == BOT_COORDINATOR_NOT_NORMAL) {
+ BOT_DEBUG_PRINTLN("CBOT-DBG: schedule FAILED -> drop");
bot_stats.send_failures++;
return;
}
@@ -1894,8 +1930,11 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *
FirmwareBot::recordCommandCooldown(bot_command_cooldowns, BOT_COMMAND_COOLDOWN_SLOTS, command.id, _ms->getMillis(),
BOT_COMMAND_COOLDOWN_MILLIS);
if (!enqueueBotResponse(message, direct_recipient, channel_idx, final_response, final_response_len, fingerprint, response_fingerprint)) {
+ BOT_DEBUG_PRINTLN("CBOT-DBG: enqueueBotResponse FAILED -> drop");
ResponseCoordinator::cancel(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, fingerprint);
bot_stats.send_failures++;
+ } else {
+ BOT_DEBUG_PRINTF("CBOT-DBG: queued response, text=%.40s\r\n", final_response);
}
}
@@ -1904,6 +1943,8 @@ void MyMesh::sendQueuedBotResponses() {
while (true) {
BotCoordinatorReady ready = ResponseCoordinator::poll(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, now);
if (ready.result == BOT_COORDINATOR_READY_NONE) return;
+ BOT_DEBUG_PRINTF("CBOT-DBG: poll result=%d reqfp=%08lx\r\n", (int)ready.result,
+ (unsigned long)(ready.request_fingerprint.value & 0xFFFFFFFFul));
if (ready.result == BOT_COORDINATOR_READY_SUPPRESSED) {
bot_stats.suppressed_responses++;
@@ -1945,12 +1986,14 @@ void MyMesh::sendQueuedBotResponses() {
bool success = false;
if (pending->direct) {
ContactInfo *recipient = lookupContactByPubKey(pending->recipient_pub_key, PUB_KEY_SIZE);
+ BOT_DEBUG_PRINTF("CBOT-DBG: send DM lookup=%s text=%.40s\r\n", recipient ? "FOUND" : "NULL", pending->text);
if (recipient) {
uint32_t expected_ack = 0;
uint32_t est_timeout = 0;
uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
int result = sendMessage(*recipient, timestamp, 0, pending->text, expected_ack, est_timeout);
success = result != MSG_SEND_FAILED;
+ BOT_DEBUG_PRINTF("CBOT-DBG: sendMessage result=%d success=%d\r\n", result, success);
if (success && expected_ack) {
expected_ack_table[next_ack_idx].msg_sent = _ms->getMillis();
expected_ack_table[next_ack_idx].ack = expected_ack;
@@ -1960,9 +2003,11 @@ void MyMesh::sendQueuedBotResponses() {
}
} else if (pending->channel_idx != 0xFF) {
ChannelDetails channel;
+ bool have_ch = getChannel(pending->channel_idx, channel);
uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
- success = getChannel(pending->channel_idx, channel) &&
- sendGroupMessage(timestamp, channel.channel, _prefs.node_name, pending->text, pending->text_len);
+ success = have_ch && sendGroupMessage(timestamp, channel.channel, _prefs.node_name, pending->text, pending->text_len);
+ BOT_DEBUG_PRINTF("CBOT-DBG: send CH idx=%u have=%d success=%d text=%.40s\r\n",
+ (unsigned)pending->channel_idx, have_ch, success, pending->text);
}
if (success) {
@@ -1970,6 +2015,7 @@ void MyMesh::sendQueuedBotResponses() {
ResponseCoordinator::recordRecent(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS,
pending->response_fingerprint, now);
} else {
+ BOT_DEBUG_PRINTLN("CBOT-DBG: send FAILED -> bot stays silent");
bot_stats.send_failures++;
}
pending->active = false;
@@ -2521,9 +2567,14 @@ void MyMesh::begin(bool has_display) {
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
#if CMESH_BOT_ENABLED
- if (!_store->loadBotPrefs(bot_prefs)) bot_prefs.prefs_load_failures++;
+ bool loaded = _store->loadBotPrefs(bot_prefs);
+ if (!loaded) bot_prefs.prefs_load_failures++;
BotPrefsCodec::validate(bot_prefs);
applyBotPrefs();
+ BOT_DEBUG_PRINTF("CBOT-DBG: boot: loaded=%d enabled=%d bot_ch=%s testing_ch=%s mask=0x%08lx delay=%u jitter=%u hop_step=%u\r\n",
+ loaded, bot_prefs.enabled, bot_prefs.bot_channel, bot_prefs.testing_channel,
+ (unsigned long)bot_prefs.command_mask, bot_prefs.normal_delay_ms, bot_prefs.normal_jitter_ms,
+ bot_prefs.hop_step_ms);
#endif
}

View File

@@ -1143,24 +1143,31 @@ static void test_fingerprint() {
assert(fa.value != FirmwareBot::fingerprintFor(b).value); assert(fa.value != FirmwareBot::fingerprintFor(b).value);
} }
static void test_channel_id_fingerprint() { static void test_group_request_token_uses_stable_channel_name() {
uint8_t channel_id_a[4] = { 0x10, 0x20, 0x30, 0x40 };
uint8_t channel_id_b[4] = { 0x10, 0x20, 0x30, 0x41 };
BotMessage a = make_message("#bot", "!PING"); BotMessage a = make_message("#bot", "!PING");
BotMessage b = make_message("#testing", "!ping"); BotMessage b = make_message("bot", "!ping");
BotMessage c = make_message("#ops", "!ping"); BotMessage c = a;
c.channel_kind = BOT_CHANNEL_BOT; a.path_hash_count = 3;
BotFingerprint fa = FirmwareBot::fingerprintFor(a, channel_id_a, sizeof(channel_id_a)); b.path_hash_count = 7;
BotFingerprint fb = FirmwareBot::fingerprintFor(b, channel_id_a, sizeof(channel_id_a)); c.channel_name[0] = '#';
BotFingerprint fc = FirmwareBot::fingerprintFor(c, channel_id_a, sizeof(channel_id_a)); c.channel_name[1] = 'B';
c.channel_name[2] = 'O';
c.channel_name[3] = 'T';
c.channel_name[4] = 0;
c.path_hash_count = 1;
BotFingerprint fa = FirmwareBot::fingerprintFor(a);
BotFingerprint fb = FirmwareBot::fingerprintFor(b);
BotFingerprint fc = FirmwareBot::fingerprintFor(c);
assert(fa.value == fb.value); assert(fa.value == fb.value);
assert(fa.value == fc.value); assert(fa.value == fc.value);
assert(fa.value != FirmwareBot::fingerprintFor(a, channel_id_b, sizeof(channel_id_b)).value); assert(FirmwareBot::requestToken(fa) == FirmwareBot::requestToken(fb));
assert(FirmwareBot::requestToken(fa) == FirmwareBot::requestToken(fc));
BotMessage dm = a; BotMessage other_channel = a;
dm.channel_kind = BOT_CHANNEL_DM; strncpy(other_channel.channel_name, "#testing", sizeof(other_channel.channel_name) - 1);
dm.channel_name[0] = 0; other_channel.channel_kind = BOT_CHANNEL_TESTING;
assert(FirmwareBot::fingerprintFor(dm, channel_id_a, sizeof(channel_id_a)).value == FirmwareBot::fingerprintFor(dm).value); assert(fa.value != FirmwareBot::fingerprintFor(other_channel).value);
} }
static BotFingerprint final_response_fingerprint_for(const BotMessage& message, const char* text) { static BotFingerprint final_response_fingerprint_for(const BotMessage& message, const char* text) {
@@ -1705,7 +1712,7 @@ int main() {
test_known_bot_registry(); test_known_bot_registry();
test_known_bot_registry_ambiguous_short_prefix(); test_known_bot_registry_ambiguous_short_prefix();
test_fingerprint(); test_fingerprint();
test_channel_id_fingerprint(); test_group_request_token_uses_stable_channel_name();
test_response_fingerprint(); test_response_fingerprint();
test_response_fingerprint_ignores_request_token(); test_response_fingerprint_ignores_request_token();
test_authoritative_suppression_flow(); test_authoritative_suppression_flow();