Skip to content

fix: defer direct messages while learning peer keys - #11222

Draft
RCGV1 wants to merge 15 commits into
meshtastic:developfrom
RCGV1:codex/dm-hardening
Draft

fix: defer direct messages while learning peer keys#11222
RCGV1 wants to merge 15 commits into
meshtastic:developfrom
RCGV1:codex/dm-hardening

Conversation

@RCGV1

@RCGV1 RCGV1 commented Jul 26, 2026

Copy link
Copy Markdown
Member

Summary

  • Hold a DM briefly while requesting NodeInfo when the sender needs a recipient key or the recipient may need the sender key.
  • Concurrent DMs to the same peer share one directed NodeInfo request and resume with their original client message IDs.
  • Replay KEY_EXCHANGE for a duplicate client submission while the original message is still pending, without a second RF transmission.
  • Avoid a pointless key-exchange wait for licensed recipients, which cannot use PKI.
  • Do not replace a previously signed identity with an unsigned NodeInfo update, even when its stored key is malformed.
  • Allow a valid client-supplied recipient key to encrypt an explicit PKI DM after the radio has evicted that contact, without persisting or replacing NodeDB state. A conflicting stored key still fails closed.
  • Keep QueueStatus.state = KEY_EXCHANGE associated with the original message ID for every deferred state, including a re-deferred retry.
  • Emit the resumed send result against the original message ID.

Client behavior

QueueStatus.state = KEY_EXCHANGE means the radio is waiting for a short NodeInfo/key-refresh exchange, not that the DM was delivered. Clients should retain the existing pending row for mesh_packet_id and update its state rather than create a second message.

Dependency

Depends on meshtastic/protobufs#1020, which adds the additive QueueStatus lifecycle field.

Testing

  • native-macos test_packet_signing: 86/86 passed
  • native-macos test_mesh_module: 25/25 passed
  • native-macos test_stream_api: 13/13 passed
  • trunk fmt and git diff --check passed
  • T-Beam S3 Core flashed from this worktree and returned healthy serial metadata; firmware API accepted isolated test messages

Hardware scope

The attached nRF test nodes are USB-visible but currently do not complete app-protocol discovery or serial DFU handshakes. This PR does not claim a two-node PKI DM delivery test on the physical bench.

Summary by CodeRabbit

  • New Features

    • Direct messages can now wait for required public keys, automatically request key information, and retry delivery.
    • Queue-status updates now indicate when a message is undergoing key exchange, including duplicate phone submissions.
    • Node information requests can be sent on demand to support message recovery.
  • Bug Fixes

    • Improved handling of missing, invalid, or empty encryption keys.
    • More accurate delivery errors and status reporting for encrypted messages.
  • Tests

    • Expanded coverage for deferred messages, key exchange, retries, and queue-status behavior.

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d2b3141e-0c02-49fe-a8da-ac7b27b0c092

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR updates protobuf resources, extends queue-status reporting, rejects zero-filled public keys, and adds deferred direct-message recovery for PKI key exchange. Router, ReliableRouter, NodeInfoModule, and RoutingModule coordinate deferral, suppression, retries, timeouts, and terminal errors, with expanded test coverage.

Changes

Deferred direct-message PKI flow

Layer / File(s) Summary
Queue-status contract and initialization
protobufs, src/mesh/MeshService.*, src/mesh/PhoneAPI.cpp, src/mesh/*Radio*, src/platform/portduino/SimRadio.cpp, test/test_mesh_module/test_main.cpp
Queue-status messages now preserve packet IDs and carry key-exchange state; MeshService returns send errors, and queue-status structures use zero initialization.
PKI key validation and encoding
src/mesh/NodeDB.cpp, src/mesh/Router.cpp
Zero-filled keys are rejected, embedded packet keys can supply PKI destinations, routing packets may remain PKI-encrypted, and Curve25519 failures return a PKI error.
Deferred DM state machine
src/mesh/Router.*, src/mesh/ReliableRouter.cpp
Router and ReliableRouter add deferred-message queues, key-exchange retry tracking, routing suppression, timeout handling, and missing-key send behavior.
NodeInfo and routing integration
src/modules/NodeInfoModule.*, src/modules/RoutingModule.cpp
NodeInfo requests can be forced and return packet IDs; received NodeInfo triggers deferred retries, while routing delivery is suppressed for matching recovery packets.
Deferred DM and queue-status tests
test/test_packet_signing/test_main.cpp
Tests cover deferred queueing, retries, timeouts, NAK handling, suppression, key rotation, duplicate packets, phone-origin packets, and multiple peers.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested labels: bugfix, needs-review, mesh, module

Suggested reviewers: thebentern, jp-bennett, caveman99

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: deferring direct messages while learning peer keys.
Description check ✅ Passed The PR description covers the required summary, behavior, dependency, testing, and hardware scope; only the template's attestation checklist is missing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

⚡ Try this PR in the Web Flasher

Flash this PR in the Web Flasher

firmware commit boards expires

Warning

This is an automated, unreviewed CI test build. Back up your device configuration
before flashing, and only flash devices you are able to recover.

Supported boards built by this PR (31)
Device Board Platform
Crowpanel Adv 3.5 TFT elecrow-adv-35-tft esp32-s3
Heltec HT62 heltec-ht62-esp32c3-sx1262 esp32-c3
Heltec Mesh Node 096 heltec-mesh-node-t096 nrf52840
Heltec Mesh Node T1 heltec-mesh-node-t1 nrf52840
Heltec Mesh Node T114 heltec-mesh-node-t114 nrf52840
Heltec V3 heltec-v3 esp32-s3
Heltec V4 heltec-v4 esp32-s3
Meshnology W10 meshnology_w10 esp32-s3
Meshnology W12 meshnology_w12 esp32-s3
Raspberry Pi Pico pico rp2040
Raspberry Pi Pico W picow rp2040
RAK WisMesh Pocket V3 rak_wismesh_pocket nrf52840
RAK WisMesh Pod rak_wismesh_pod nrf52840
RAK WisMesh Repeater Mini V2 rak_wismesh_repeater_mini nrf52840
RAK WisMesh Tag rak_wismeshtag nrf52840
RAK WisBlock 11200 rak11200 esp32
RAK WisBlock 11310 rak11310 rp2040
RAK3312 rak3312 esp32-s3
RAK WisBlock 4631 rak4631 nrf52840
Seeed SenseCAP Mesh-Tracker-X1 seeed_mesh_tracker_X1 nrf52840
Seeed Wio Tracker L1 seeed_wio_tracker_L1 nrf52840
Seeed Xiao NRF52840 Kit seeed_xiao_nrf52840_kit nrf52840
Seeed Xiao ESP32-S3 seeed-xiao-s3 esp32-s3
Station G2 station-g2 esp32-s3
Station G3 station-g3 esp32-s3
LILYGO T-Deck t-deck-tft esp32-s3
LILYGO T-Echo t-echo nrf52840
LILYGO T-Echo Plus t-echo-plus nrf52840
LILYGO T-Impulse Plus t-impulse-plus nrf52840
LilyGo T3-C6 tlora-c6 esp32-c6
Seeed SenseCAP T1000-E tracker-t1000-e nrf52840

Build artifacts expire on 2026-08-25. Updated for ae7592b.

@RCGV1

RCGV1 commented Jul 26, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/mesh/NodeDB.cpp (1)

3389-3399: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject zero-filled incoming public keys before persisting them.

This guard only treats an existing all-zero key as absent. An incoming p.public_key with size == 32 and all-zero bytes still reaches CopyUserToNodeInfoLite(info, p) and is stored. Later lookup helpers reject it, but downstream code can still interpret size == 32 as key presence, causing PKI failures and allowing malformed NodeInfo to poison the peer record.

Validate the incoming key before the existing-key comparison and reject or normalize it as absent.

Proposed fix
 `#if` !(MESHTASTIC_EXCLUDE_PKI)
     if (p.public_key.size == 32 && nodeId != nodeDB->getNodeNum()) {
+        if (memfll(p.public_key.bytes, 0, sizeof(p.public_key.bytes))) {
+            LOG_WARN("Rejecting zero-filled public key for node 0x%08x", nodeId);
+            return false;
+        }
         printBytes("Incoming Pubkey: ", p.public_key.bytes, 32);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mesh/NodeDB.cpp` around lines 3389 - 3399, Validate p.public_key before
the existing-key comparison in the surrounding NodeInfo update flow, treating a
32-byte all-zero incoming key as absent and preventing it from reaching
CopyUserToNodeInfoLite(info, p). Preserve the current mismatch rejection for
valid incoming keys and existing usable keys, while ensuring malformed
zero-filled keys are rejected or normalized before persistence.
🧹 Nitpick comments (3)
test/test_packet_signing/test_main.cpp (1)

243-261: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Group M leaves process globals mutated for later groups.

nodeInfoModule, airTime, and portduino_config.force_simradio are set by enableNodeInfoForDmKeyWait()/enablePkiForLocalNode() but only restored ad-hoc inside test_M2_... (Lines 1525-1527). Everything after Group M (Groups N/D/E) runs with the DM shims installed and force_simradio forced false. Restoring these in setUp (or a tearDown) would make the ordering irrelevant.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/test_packet_signing/test_main.cpp` around lines 243 - 261, Restore the
process-wide DM test state in the common test lifecycle rather than only inside
test_M2_..., including nodeInfoModule, airTime, the installed DM shims, and
ARCH_PORTDUINO’s portduino_config.force_simradio. Update setUp or tearDown to
reset these values after each test, reusing the existing saved-state variables
and preserving the original configuration for subsequent groups.
src/mesh/Router.h (1)

246-264: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Name the attempt-table bound and align constant casing.

peerKeyExchangeAttempts[8] is the only bound not expressed as a named constant. Also, project convention is UPPER_SNAKE_CASE for constants, while these are camelCase.

♻️ Suggested tidy-up
-    static constexpr uint8_t deferredDmCapacity = 2;
+    static constexpr uint8_t DEFERRED_DM_CAPACITY = 2;
+    static constexpr uint8_t PEER_KEY_ATTEMPT_CAPACITY = 8;
@@
-    } peerKeyExchangeAttempts[8];
+    } peerKeyExchangeAttempts[PEER_KEY_ATTEMPT_CAPACITY];

As per coding guidelines: "Use PascalCase for classes, camelCase for functions and member variables, and UPPER_SNAKE_CASE for constants and defines."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mesh/Router.h` around lines 246 - 264, Update the constants near
DeferredDm to use UPPER_SNAKE_CASE, and add a named constant for the peer-key
exchange attempt capacity currently hardcoded as 8. Replace the literal array
bound in peerKeyExchangeAttempts with that new capacity constant, and update all
references to the renamed constants consistently.

Source: Coding guidelines

src/mesh/ReliableRouter.cpp (1)

161-168: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the NodeDB copy-out accessor for the sender key, and format the reason code as hex.

Router.cpp reads peer keys via nodeDB->copyPublicKey(...); reading sender->public_key off the returned NodeInfoLite pointer here diverges from that pattern (and from the NodeDB accessor guidance). Also the error reason is logged with %d instead of 0x%x.

♻️ Proposed change
-                    const meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(p->from);
-                    const bool hasSenderKey = sender && sender->public_key.size == 32 &&
-                                              !memfll(sender->public_key.bytes, 0, sizeof(sender->public_key.bytes));
+                    meshtastic_NodeInfoLite_public_key_t senderKey = {0, {0}};
+                    const bool hasSenderKey = nodeDB->copyPublicKey(p->from, senderKey) && senderKey.size == 32 &&
+                                              !memfll(senderKey.bytes, 0, sizeof(senderKey.bytes));
                     const auto error =
                         hasSenderKey ? meshtastic_Routing_Error_PKI_FAILED : meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY;
-                    LOG_INFO("Undecryptable PKI packet from 0x%08x, send error %d", p->from, error);
+                    LOG_INFO("Undecryptable PKI packet from 0x%08x, send error 0x%x", p->from, error);

As per coding guidelines: "Format 32-bit node and packet IDs as 0x%08x; format one-byte values, flags, addresses, and reason codes as 0x%x" and "use satellite copy-out accessors and flat NodeInfoLite fields".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mesh/ReliableRouter.cpp` around lines 161 - 168, Update the sender-key
check in the PKI handling branch to obtain the key through
nodeDB->copyPublicKey(...) instead of reading sender->public_key from
NodeInfoLite, while preserving the existing validity checks. Change the
send-error LOG_INFO reason-code format from %d to 0x%x.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/mesh/Router.cpp`:
- Around line 1503-1524: The PEER_KEY retry in Router::processDeferredDms can
re-enter deferPeerKeyDm and indefinitely refresh deferred waits when another DM
for the same peer remains queued. Add a per-packet “already preflighted” marker
for the DM released by processDeferredDms, have deferPeerKeyDm recognize and
consume that marker to bypass the key-exchange deferral once, and preserve
normal deferral behavior for newly submitted packets.

In `@src/modules/NodeInfoModule.cpp`:
- Around line 177-181: The forceSend path in NodeInfo request handling bypasses
all per-destination rate limiting, allowing repeated recovery requests. Update
requestNodeInfo and its destination-key recovery caller to apply the existing
per-peer key-exchange attempt throttle, or enforce a shorter minimum interval
when forceSend is true, while preserving normal forced-send behavior outside
this recovery path.

In `@test/test_packet_signing/test_main.cpp`:
- Around line 2275-2279: Remove the post-send `dm->id` assertion from the test
after `pipelineRouter->sendLocal(dm, ...)`, since ownership has transferred and
`originalDmId` came from the same object. Keep the existing deferred-state and
radio-send assertions, or validate the packet ID through an owned record such as
`pipelineRadio->sentPackets` if that behavior must be checked.

---

Outside diff comments:
In `@src/mesh/NodeDB.cpp`:
- Around line 3389-3399: Validate p.public_key before the existing-key
comparison in the surrounding NodeInfo update flow, treating a 32-byte all-zero
incoming key as absent and preventing it from reaching
CopyUserToNodeInfoLite(info, p). Preserve the current mismatch rejection for
valid incoming keys and existing usable keys, while ensuring malformed
zero-filled keys are rejected or normalized before persistence.

---

Nitpick comments:
In `@src/mesh/ReliableRouter.cpp`:
- Around line 161-168: Update the sender-key check in the PKI handling branch to
obtain the key through nodeDB->copyPublicKey(...) instead of reading
sender->public_key from NodeInfoLite, while preserving the existing validity
checks. Change the send-error LOG_INFO reason-code format from %d to 0x%x.

In `@src/mesh/Router.h`:
- Around line 246-264: Update the constants near DeferredDm to use
UPPER_SNAKE_CASE, and add a named constant for the peer-key exchange attempt
capacity currently hardcoded as 8. Replace the literal array bound in
peerKeyExchangeAttempts with that new capacity constant, and update all
references to the renamed constants consistently.

In `@test/test_packet_signing/test_main.cpp`:
- Around line 243-261: Restore the process-wide DM test state in the common test
lifecycle rather than only inside test_M2_..., including nodeInfoModule,
airTime, the installed DM shims, and ARCH_PORTDUINO’s
portduino_config.force_simradio. Update setUp or tearDown to reset these values
after each test, reusing the existing saved-state variables and preserving the
original configuration for subsequent groups.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 45351631-fa5b-48b7-8942-6dda2165da52

📥 Commits

Reviewing files that changed from the base of the PR and between 8e104a9 and e8a1ac4.

⛔ Files ignored due to path filters (2)
  • src/mesh/generated/meshtastic/mesh.pb.cpp is excluded by !**/generated/**, !src/mesh/generated/**
  • src/mesh/generated/meshtastic/mesh.pb.h is excluded by !**/generated/**, !src/mesh/generated/**
📒 Files selected for processing (16)
  • protobufs
  • src/mesh/MeshService.cpp
  • src/mesh/MeshService.h
  • src/mesh/NodeDB.cpp
  • src/mesh/PhoneAPI.cpp
  • src/mesh/RadioInterface.h
  • src/mesh/RadioLibInterface.cpp
  • src/mesh/ReliableRouter.cpp
  • src/mesh/Router.cpp
  • src/mesh/Router.h
  • src/modules/NodeInfoModule.cpp
  • src/modules/NodeInfoModule.h
  • src/modules/RoutingModule.cpp
  • src/platform/portduino/SimRadio.cpp
  • test/test_mesh_module/test_main.cpp
  • test/test_packet_signing/test_main.cpp

Comment thread src/mesh/Router.cpp
Comment on lines +1503 to +1524
void Router::processDeferredDms()
{
for (auto &deferred : deferredDms) {
meshtastic_MeshPacket *p = deferred.p;
if (!p)
continue;

if (deferred.reason == DeferredDm::Reason::PEER_KEY) {
if (!Throttle::isWithinTimespanMs(deferred.queuedAtMs, deferredDmPeerKeyWaitMs)) {
deferred.p = nullptr;
deferred.queuedAtMs = 0;
deferred.keyExchangeId = 0;
LOG_INFO("Retrying deferred DM id=0x%08x after NodeInfo response wait for 0x%08x", p->id, p->to);
rememberPeerKeyExchangeAttempt(p->to);
const PacketId dmId = p->id;
const ErrorCode result = send(p);
const auto state = isDeferredDm(dmId) ? meshtastic_QueueStatus_State_KEY_EXCHANGE
: meshtastic_QueueStatus_State_STATE_UNSPECIFIED;
service->sendQueueStatusToPhone(getQueueStatus(), result, dmId, state);
}
continue;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Two DMs to the same peer can be deferred forever.

When the PEER_KEY wait expires, this clears the slot and re-enters send()deferPeerKeyDm(). If a second DM to the same peer is still sitting in deferredDms, the lookup at Lines 1390-1395 inherits its keyExchangeId, which bypasses the hasPeerKeyExchangeAttempt() throttle at Line 1396, so the packet is re-deferred with a fresh queuedAtMs. Each DM keeps re-deferring the other and neither is ever transmitted, while runOnce() polls every second indefinitely.

Consider marking a DM that was just released from the deferred queue (e.g. a "already preflighted" flag consulted by deferPeerKeyDm) so the retry path always falls through to the actual send.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mesh/Router.cpp` around lines 1503 - 1524, The PEER_KEY retry in
Router::processDeferredDms can re-enter deferPeerKeyDm and indefinitely refresh
deferred waits when another DM for the same peer remains queued. Add a
per-packet “already preflighted” marker for the DM released by
processDeferredDms, have deferPeerKeyDm recognize and consume that marker to
bypass the key-exchange deferral once, and preserve normal deferral behavior for
newly submitted packets.

Comment on lines +177 to +181
if (!forceSend && !shorterTimeout && lastNodeInfo && Throttle::isWithinTimespanMs(lastNodeInfo, timeoutMs)) {
LOG_DEBUG("Skip send NodeInfo since we sent it <%us ago", timeoutMs / 1000);
ignoreRequest = true; // Mark it as ignored for MeshModule
return NULL;
} else if (shorterTimeout && lastNodeInfo && Throttle::isWithinTimespanMs(lastNodeInfo, 60 * 1000)) {
} else if (!forceSend && shorterTimeout && lastNodeInfo && Throttle::isWithinTimespanMs(lastNodeInfo, 60 * 1000)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

forceSend removes every NodeInfo rate limit on the DESTINATION_KEY recovery path.

Router::deferMissingKeyDm() calls requestNodeInfo() (force=true) with no equivalent of the hasPeerKeyExchangeAttempt() throttle used by the peer-key path, so each DM attempt to a keyless node emits an unthrottled directed NodeInfo. Only isTxAllowedChannelUtil() remains between a chatty client and repeated airtime use.

Suggest gating the destination-key request with the same per-peer attempt window, or keeping a shorter floor (rather than none) when forceSend is set.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/NodeInfoModule.cpp` around lines 177 - 181, The forceSend path in
NodeInfo request handling bypasses all per-destination rate limiting, allowing
repeated recovery requests. Update requestNodeInfo and its destination-key
recovery caller to apply the existing per-peer key-exchange attempt throttle, or
enforce a shorter minimum interval when forceSend is true, while preserving
normal forced-send behavior outside this recovery path.

Source: Coding guidelines

Comment on lines +2275 to +2279
TEST_ASSERT_EQUAL(1, pipelineRouter->deferredDmPending());
TEST_ASSERT_EQUAL(1, pipelineRadio->sendCalls);
TEST_ASSERT_FALSE(nodeDB->copyPublicKey(REMOTE_NODE, storedKey));
TEST_ASSERT_EQUAL_HEX32(originalDmId, dm->id);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Don't dereference dm after sendLocal() took ownership.

pipelineRouter->sendLocal(dm, ...) transfers ownership of the packet (deferred store or release to pool), so dm->id on Line 2278 reads memory the test no longer owns; it only happens to be alive because the DM is still deferred. The assertion is also tautological — originalDmId was copied from that same object. Drop it, or assert against pipelineRadio->sentPackets/deferred state instead.

🧹 Proposed cleanup
     TEST_ASSERT_EQUAL(1, pipelineRouter->deferredDmPending());
     TEST_ASSERT_EQUAL(1, pipelineRadio->sendCalls);
     TEST_ASSERT_FALSE(nodeDB->copyPublicKey(REMOTE_NODE, storedKey));
-    TEST_ASSERT_EQUAL_HEX32(originalDmId, dm->id);
+    TEST_ASSERT_TRUE(pipelineRouter->isDeferredDm(originalDmId));
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/test_packet_signing/test_main.cpp` around lines 2275 - 2279, Remove the
post-send `dm->id` assertion from the test after `pipelineRouter->sendLocal(dm,
...)`, since ownership has transferred and `originalDmId` came from the same
object. Keep the existing deferred-state and radio-send assertions, or validate
the packet ID through an owned record such as `pipelineRadio->sentPackets` if
that behavior must be checked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant