Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Storage Trait Split Analysis

Deep analysis of every Storage trait method: callers, frequency, embedded relevance, and proposed sub-trait groupings.

Method Inventory: 71 methods across 15 data categories

Group 1: Packet Dedup (3 methods)

#MethodCallerFrequencyEmbedded?
1has_packet_hashTransport (process_incoming)hot – every inbound packetESSENTIAL
2add_packet_hashTransport (9 sites: send/receive/proof/data)hot – every packetESSENTIAL
3remove_packet_hashDEAD CODE – 0 production callsneverdead

Embedded impl: Fixed-size ring buffer, e.g. [[u8; 32]; 512] with write cursor. Has-check is linear scan (512 x 32B = 16KB). Cannot be no-op – without dedup, packets loop forever.


Group 2: Path Table (12 methods)

#MethodCallerFrequencyEmbedded?
4get_pathTransport (19 sites)hotESSENTIAL
5set_pathTransport (4 sites)frequentESSENTIAL
6remove_pathNodeCore (1), Transport (3), RPC (1)sometimesESSENTIAL
7path_countNodeCore (1), Transport (1), Driver (1)rarelynice-to-have
8expire_pathsTransport (clean_path_states)periodicESSENTIAL
9earliest_path_expiryTransport (next_deadline)periodicESSENTIAL
10has_pathNodeCore (3), Transport (4), Driver (1)frequentESSENTIAL
11path_entriesTransport (2: path_table_entries, drop_all_paths_via)rarelynice-to-have
12get_path_stateTransport (1: path_is_unresponsive)sometimesnice-to-have
13set_path_stateTransport (3: mark_path_unresponsive/responsive)sometimesnice-to-have
14clean_stale_path_metadataTransport (clean_path_states)periodicnice-to-have
15remove_paths_for_interfaceNodeCore (1), Transport (1)rarelyESSENTIAL

Embedded impl: Fixed-size array, e.g. [Option<PathEntry>; 32] with LRU eviction on set. PathEntry is ~50 bytes, total ~1.6KB. Cannot be no-op – node can’t route without paths.

Path state (methods 12-14) is separable – unresponsive tracking is a quality-of-life feature. An embedded node could skip it and just remove stale paths via expiry.


Group 3: Announce Processing (11 methods)

#MethodCallerFrequencyEmbedded?
16get_announceTransport (5 sites)sometimesESSENTIAL
17get_announce_mutNodeCore (1), Transport (2)sometimesESSENTIAL
18set_announceTransport (4 sites)sometimesESSENTIAL
19remove_announceTransport (2: check_announce_rebroadcasts)sometimesESSENTIAL
20announce_keysTransport (2: next_deadline, check_announce_rebroadcasts)periodicESSENTIAL
21get_announce_cacheNodeCore (1), Transport (3)sometimesESSENTIAL
22set_announce_cacheNodeCore (3), Transport (1)sometimesESSENTIAL
23clean_announce_cacheTransport (1: clean_path_states)periodicnice-to-have
24get_announce_rateTransport (1: check_announce_rate)sometimesOPTIONAL
25set_announce_rateTransport (3: check_announce_rate)sometimesOPTIONAL
26announce_rate_entriesTransport (1: rate_table_entries)rarelyOPTIONAL

Embedded impl: AnnounceEntry array, e.g. [Option<AnnounceEntry>; 16] (~2KB). Announce cache stores raw bytes – variable size, harder for embedded (up to ~500 bytes each, so 16 x 500 = ~8KB). Cannot be no-op for core announces (16-22). Rate limiting (24-26) CAN be no-op – node just doesn’t rate-limit.


Group 4: Path Requests (3 methods)

#MethodCallerFrequencyEmbedded?
27get_path_request_timeTransport (2: send_path_request rate limiting)sometimesESSENTIAL
28set_path_request_timeTransport (1)sometimesESSENTIAL
29check_path_request_tagTransport (1: handle_path_request dedup)sometimesESSENTIAL

Embedded impl: Small fixed array, e.g. [([u8; 16], u64); 16] for request times (~384B), ring buffer for tags. Cannot be no-op – without request dedup, path request storms occur.


Group 5: Receipts (5 methods)

#MethodCallerFrequencyEmbedded?
30get_receiptTransport (4: get_receipt, mark_delivered, proof handling)sometimesOPTIONAL
31set_receiptTransport (3: create_receipt, create_receipt_with_timeout, mark_delivered)sometimesOPTIONAL
32remove_receipt0 production calls (only via expire_receipts)neverdead (direct)
33expire_receiptsTransport (1: check_receipt_timeouts)periodicOPTIONAL
34earliest_receipt_deadlineTransport (1: next_deadline)periodicOPTIONAL

Embedded impl: Fixed array, e.g. [Option<PacketReceipt>; 8] (~1KB). CAN be no-op – node works without delivery proofs. Links still establish; resources still transfer. You just don’t get explicit delivery confirmation for single packets.


Group 6: Known Identities (2 methods)

#MethodCallerFrequencyEmbedded?
35get_identityNodeCore (1: send_single_packet), Driver (1)sometimesESSENTIAL
36set_identityNodeCore (1: remember_identity)sometimesESSENTIAL

Embedded impl: Fixed array, e.g. [([u8; 16], Identity); 16]. Identity is ~128 bytes, total ~2.3KB. Borderline essential – without it, node can’t encrypt to a destination whose announce it already saw but isn’t currently cached in the announce table. Could be no-op if the node only talks to destinations it just heard announce.


Group 7: Transport Relay (14 methods)

#MethodCallerFrequencyEmbedded?
37get_link_entryTransport (3: forward_link_routed, process_data, is_for_local_client_link)sometimesRELAY ONLY
38get_link_entry_mutTransport (1: mark link validated on proof)rarelyRELAY ONLY
39set_link_entryTransport (1: handle_link_request – insert bidirectional route)rarelyRELAY ONLY
40remove_link_entry0 production calls (only via expire/cleanup)neverdead (direct)
41has_link_entryTransport (3: dedup exemptions, is_link_routed check)sometimesRELAY ONLY
42expire_link_entriesTransport (1: clean_link_table)periodicRELAY ONLY
43earliest_link_deadlineTransport (1: next_deadline)periodicRELAY ONLY
44remove_link_entries_for_interfaceNodeCore (1), Transport (1)rarelyRELAY ONLY
45get_reverseTransport (1: proof routing)sometimesRELAY ONLY
46set_reverseTransport (3: forward_packet, link-routed data, proof handling)sometimesRELAY ONLY
47remove_reverseTransport (1: proof routing)sometimesRELAY ONLY
48has_reverse0 production calls (default impl, test only)neverdead
49expire_reversesTransport (1: clean_reverse_table)periodicRELAY ONLY
50remove_reverse_entries_for_interfaceNodeCore (1), Transport (1)rarelyRELAY ONLY

Embedded impl: CAN be full no-op for leaf nodes (enable_transport=false). A leaf node never relays, never builds link/reverse tables. If an embedded node IS a relay, needs fixed arrays: [Option<LinkEntry>; 16] (~1KB), [Option<ReverseEntry>; 32] (~2KB).


Group 8: Discovery Path Requests (5 methods)

#MethodCallerFrequencyEmbedded?
51set_discovery_path_requestTransport (1: handle_path_request)sometimesRELAY ONLY
52get_discovery_path_requestTransport (3: handle/retry/send_discovery)sometimesRELAY ONLY
53remove_discovery_path_requestTransport (2: send_discovery_path_response)sometimesRELAY ONLY
54expire_discovery_path_requestsTransport (1: clean_path_states)periodicRELAY ONLY
55discovery_path_request_dest_hashesTransport (2: next_deadline, retry)periodicRELAY ONLY

Embedded impl: CAN be full no-op for leaf nodes. Only transport nodes forward path requests on behalf of others. Leaf nodes send their own path requests via send_path_request (Group 4), not this mechanism.


Group 9: Ratchets (7 methods)

#MethodCallerFrequencyEmbedded?
56get_known_ratchetTransport (1: set_local_client ratchet replay)rarelyOPTIONAL
57remember_known_ratchetTransport (1: handle_announce), NodeCore (2: announce_destination, check_mgmt_announces)rarelyOPTIONAL
58has_known_ratchet0 production callsneverdead
59known_ratchet_count0 production calls (test only)neverdead
60expire_known_ratchetsTransport (1: clean_path_states)periodicOPTIONAL
61store_dest_ratchet_keysNodeCore (2: announce_destination, check_mgmt_announces)rarelyOPTIONAL
62load_dest_ratchet_keysNodeCore (1: register_destination)rarelyOPTIONAL

Embedded impl: CAN be full no-op. Node works without forward secrecy – announces are still validated, links still established, data still encrypted. Ratchets add key rotation for post-compromise security. On a RAM-constrained device, this is the first thing to skip.


Group 10: Shared Instance (7 methods)

#MethodCallerFrequencyEmbedded?
63add_local_client_destTransport (1: handle_announce for local client)rarelySHARED ONLY
64remove_local_client_destsTransport (1: set_local_client cleanup)rarelySHARED ONLY
65has_local_client_dest0 production calls (test only)neverdead
66set_local_client_known_destTransport (1: handle_announce)rarelySHARED ONLY
67has_local_client_known_dest0 production calls (test only)neverdead
68local_client_known_dest_hashesTransport (2: set_local_client, clean_path_states)rarelySHARED ONLY
69expire_local_client_known_destsTransport (1: clean_path_states)periodicSHARED ONLY

Embedded impl: Full no-op. Embedded nodes don’t share instances. Zero correctness impact. Shared instance is a desktop/server feature (multiple programs sharing one daemon via Unix sockets). An embedded node IS the daemon.


Group 11: Persistence & Diagnostics (2 methods)

#MethodCallerFrequencyEmbedded?
70flushDriver (2: save_persistent_state, auto_interface)rarelyOPTIONAL
71diagnostic_dumpDriver (1)rarelyOPTIONAL

Already have empty default implementations. No action needed.


Dead Code Summary

7 methods with zero production callers:

MethodNotes
remove_packet_hashDefined but never called anywhere
has_reverseOnly the default impl delegates to get_reverse; no external callers
remove_link_entryOnly called indirectly via expire_link_entries
remove_receiptOnly called indirectly via expire_receipts
has_known_ratchetTest-only
known_ratchet_countTest-only
has_local_client_known_destTest-only (one NodeCore test)

has_local_client_dest is also test-only in Transport tests.


Proposed Sub-Trait Split

Tier 1: CoreStorage – 28 methods

Every node needs these. Without them the protocol doesn’t function.

Packet dedup:    has_packet_hash, add_packet_hash          (2)
Path table:      get_path, set_path, remove_path,
                 path_count, expire_paths,
                 earliest_path_expiry, has_path,
                 path_entries, remove_paths_for_interface   (9)
Path state:      get_path_state, set_path_state,
                 clean_stale_path_metadata                  (3)
Announces:       get_announce, get_announce_mut,
                 set_announce, remove_announce,
                 announce_keys, get_announce_cache,
                 set_announce_cache, clean_announce_cache    (8)
Path requests:   get_path_request_time,
                 set_path_request_time,
                 check_path_request_tag                      (3)
Identities:      get_identity, set_identity                  (2)
Persistence:     flush                                       (1)

Embedded minimum: ~30KB RAM total

CollectionLayoutSize
Packet ring[[u8; 32]; 512]16KB
Path table[Option<PathEntry>; 32]~2KB
Announce table[Option<AnnounceEntry>; 16]~1KB
Announce cache[Option<([u8;16], Vec<u8>)>; 16]~8KB (variable, biggest concern)
Path requests[([u8;16], u64); 16]~0.5KB
Path states[Option<([u8;16], PathState)>; 32]~1KB
Identities[Option<([u8;16], Identity)>; 16]~2KB

Cannot be no-op.

Tier 2: ReceiptStorage – 5 methods

get_receipt, set_receipt, remove_receipt,
expire_receipts, earliest_receipt_deadline

Embedded: [Option<([u8;16], PacketReceipt)>; 8] ~1KB. Can be no-op. Node works; single-packet delivery proofs are lost. Links, resources, and channels all function – they have their own proof mechanisms.

Why separate from CoreStorage: An nRF52840 sensor that only sends data and doesn’t care about delivery confirmation saves 1KB RAM and 5 method implementations.

Tier 3: TransportRelayStorage – 19 methods

Link table:      get_link_entry, get_link_entry_mut,
                 set_link_entry, remove_link_entry,
                 has_link_entry, expire_link_entries,
                 earliest_link_deadline,
                 remove_link_entries_for_interface           (8)
Reverse table:   get_reverse, set_reverse, remove_reverse,
                 has_reverse, expire_reverses,
                 remove_reverse_entries_for_interface         (6)
Discovery:       set_discovery_path_request,
                 get_discovery_path_request,
                 remove_discovery_path_request,
                 expire_discovery_path_requests,
                 discovery_path_request_dest_hashes           (5)

Embedded: Full no-op for leaf nodes. Only matters if enable_transport=true. Can be no-op. Leaf node can’t relay, but communicates fine as an endpoint.

Why one trait instead of three: Link table, reverse table, and discovery requests are always used together – they’re all transport-mode infrastructure. A node is either a relay or it isn’t. There’s no use case for “relay with link table but no reverse table.”

Tier 4: RatchetStorage – 7 methods

get_known_ratchet, remember_known_ratchet,
has_known_ratchet, known_ratchet_count,
expire_known_ratchets,
store_dest_ratchet_keys, load_dest_ratchet_keys

Embedded: [Option<([u8;16], [u8;32], u64)>; 8] ~0.5KB if implemented. Can be full no-op. Forward secrecy is a security enhancement. Without ratchets, announce encryption still works via the destination’s static keys. An embedded sensor node may not need post-compromise key rotation.

Why separate: Security feature with storage cost. Embedded devices with extreme RAM constraints can skip it. Also the only group that spans both Transport and NodeCore callers in a way that’s cleanly separable.

Tier 5: SharedInstanceStorage – 7 methods

add_local_client_dest, remove_local_client_dests,
has_local_client_dest,
set_local_client_known_dest, has_local_client_known_dest,
local_client_known_dest_hashes,
expire_local_client_known_dests

Embedded: Full no-op. Zero correctness impact. Shared instance is a desktop/server feature (multiple programs sharing one daemon via Unix sockets). An embedded node IS the daemon.

Why separate: Entirely irrelevant to embedded. Also the most likely candidate for removal from the trait hierarchy entirely – it could be a compile-time feature flag instead.

Tier 6: AnnounceRateStorage – 3 methods

get_announce_rate, set_announce_rate, announce_rate_entries

Embedded: [Option<([u8;16], AnnounceRateEntry)>; 16] ~0.5KB if implemented. Can be no-op. Without rate limiting, node processes all announces. On a small network (typical for embedded LoRa), announce volume is low enough that rate limiting is unnecessary.

Why separate from CoreStorage: Rate limiting is operator policy, not protocol correctness. A network of 5 LoRa nodes doesn’t need it.


Composition Design

#![allow(unused)]
fn main() {
// Tier 1 -- every node
trait CoreStorage { /* 28 methods */ }

// Tier 2-6 -- optional capabilities
trait ReceiptStorage { /* 5 methods */ }
trait TransportRelayStorage { /* 19 methods */ }
trait RatchetStorage { /* 7 methods */ }
trait SharedInstanceStorage { /* 7 methods */ }
trait AnnounceRateStorage { /* 3 methods */ }

// Backward-compatible supertrait -- existing code unchanged
trait Storage: CoreStorage + ReceiptStorage
    + TransportRelayStorage + RatchetStorage
    + SharedInstanceStorage + AnnounceRateStorage {}

// Blanket impl
impl<T> Storage for T where T: CoreStorage + ReceiptStorage
    + TransportRelayStorage + RatchetStorage
    + SharedInstanceStorage + AnnounceRateStorage {}
}

Transport<C, S> and NodeCore<R, C, S> keep S: Storagezero changes to existing code. MemoryStorage and FileStorage implement all sub-traits and get Storage for free.

For embedded:

#![allow(unused)]
fn main() {
struct EmbeddedStorage {
    // Only CoreStorage collections
    // ~30KB RAM
}
impl CoreStorage for EmbeddedStorage { /* real impls */ }
impl ReceiptStorage for EmbeddedStorage { /* no-ops */ }
impl TransportRelayStorage for EmbeddedStorage { /* no-ops */ }
impl RatchetStorage for EmbeddedStorage { /* no-ops */ }
impl SharedInstanceStorage for EmbeddedStorage { /* no-ops */ }
impl AnnounceRateStorage for EmbeddedStorage { /* no-ops */ }
// Gets Storage automatically via blanket impl
}

Trade-offs & Uncertainties

Confident assessments

  • Groups 5 (SharedInstance) and 3 (TransportRelay) are cleanly separable – no leaf-node code path touches them in production.
  • Group 4 (Ratchets) is cleanly separable – all call sites have graceful None/no-op fallback.
  • The 7 dead-code methods should be removed regardless of whether the trait is split.

Uncertainties

1. CoreStorage is still 28 methods. That’s a lot for a “minimal” trait. I considered splitting Path and Announce into separate sub-traits, but they’re called from the same Transport methods (handle_announce touches both path table and announce table in the same function). Splitting would require where S: PathStorage + AnnounceStorage bounds scattered across Transport methods – high friction for zero embedded benefit since both are essential.

2. Announce cache is the RAM wildcard. Each cached announce is up to ~500 bytes of raw wire data. 16 entries = 8KB. On an nRF52840 with 256KB RAM this is manageable, but on a smaller MCU it could dominate. The cache is needed for path responses and link requests from remote nodes. An endpoint that only initiates (never responds to path requests) could skip it – but that’s a very narrow use case.

3. Whether the split is worth the complexity. Right now, NoStorage already serves as the “skip everything” option, and MemoryStorage with capacity limits would cover the “real embedded” case. The sub-trait split adds type-system guarantees but also adds 6 trait definitions, 6 impl blocks per storage type, and ongoing maintenance burden. If there’s only one embedded target (nRF52840), a capacity-limited MemoryStorage might be strictly better.

4. Conditional compilation is the pragmatic alternative. Instead of sub-traits, use #[cfg(feature = "transport")] to gate TransportRelay collections in MemoryStorage. Simpler, less generic-parameter noise, but loses per-instance flexibility (can’t mix endpoint and transport nodes in the same binary).


Recommendation

Remove the 7 dead methods now. Defer the sub-trait split until the first embedded target actually needs it. The current MemoryStorage with configurable capacity limits (already has packet_hash_cap, identity_cap) extended to all collections covers the nRF52840 case without any trait refactoring. The sub-trait design above is the right split IF the refactor becomes necessary – but it’s a premature abstraction today.