Skip to content

Credit Routing — retiring TTL/hops for an airtime-denominated budget

Status: DESIGN (forward-looking, not normative). Wire changes here are an LPP v2 event: run /lrc-wire-change, update docs/PROTOCOL.md, refresh golden bytes deliberately. Companion docs: CONSOLIDATED_ROUTING_PLAN.md (edge-mesh/hub split, contention window, Return Descriptor), ASYNC_ROUTING_DESIGN.md, and the scenario testbed that validates the constants (../SCENARIO_TESTBED.md).

LPP v1 byte 2 packs TTL (4 bits, default 7) and hops-taken (4 bits), capping any path at 15 hops. Three defects, in increasing order of severity:

  1. It’s too small. 15 hops is an arbitrary ceiling; a healthy fast-lane corridor (SF7/250, ~45 ms per frame) could usefully carry traffic across dozens of relays, and the asymmetric-return design explicitly allows the return leg to take more hops than the uplink.
  2. It counts the wrong thing. A hop is not a unit of cost. One SF12/125 relay burns ~2.5 s of shared spectrum; one SF7/500 relay burns ~25 ms — a 100× difference the hop counter cannot see. TTL=7 permits a catastrophic 17+ seconds of cumulative SF12 airtime per path while forbidding a cheap 20-hop SF7 corridor.
  3. It conflates three jobs. TTL is asked to be (a) the loop guard, (b) the flood-radius bound, and (c) the resource bound, and does all three badly. chIRpChat already has better mechanisms for (a) and (b); only (c) needs a wire field.

2. The replacement: three orthogonal bounds

Section titled “2. The replacement: three orthogonal bounds”

Delete TH. Loop-freedom, flood width, and resource consumption become three separate mechanisms, each already partially built:

Job Mechanism Status
Loop guard (per-node revisits) (SRC, MSGID) dedup ring — a node relays a given frame at most once built (v1)
Uplink direction gradient descent: relay only if own rdist < frame rprog (ERB), rewrite rprog on forward — strictly decreasing potential, revisits impossible ERB carriage built; enforcement is this design
Flood width (copies) tiered suppression Rule A + contention window (routing_policy.h) host policy built
Resource bound AC — airtime credit, the new byte 2 this design

The dedup ring’s bounded false-negative rate (documented trade in dedup.h) is why AC still matters even where dedup terminates floods: a false negative re-relays a frame, and AC caps the damage. Defense in depth, not redundancy.

AC (u8) is the frame’s remaining network airtime budget, unit 50 ms, so the ceiling is 12.75 s of cumulative time-on-air along any single path.

Debit rule. Before retransmitting, a relay computes the time-on-air of the frame as it will actually send it (its preset — the relay knows SF/BW/CR and the byte count before keying up) and debits max(1, ceil(toa_ms / 50)). If the debit exceeds the remaining credit, the frame is dropped and fwd.credit_drop bumps. The minimum debit of 1 is the hard floor that bounds even sub-50 ms presets to ≤255 relays per path.

What that buys at each rung (≈55-byte frame):

Preset TOA debit max hops on a 120-credit chat frame
P0 ANCHOR (SF12/125) ~2.5 s 50 2
P2 (SF10/250) ~450 ms 9 13
P3 default client (SF9/250) ~150 ms 3 40
P5 (SF7/250) ~45 ms 1 120
P7 (SF5/500) ~5 ms 1 120

This is the honest shape: hops are abundant where they’re cheap and scarce where they’re ruinous. Deep SF12 chains — the thing v1’s TTL=7 nominally allowed — are exactly what the lane ladder, client promotion, and routers exist to prevent; credit routing prices them correctly instead of pretending seven of them cost the same as seven SF5 hops.

Origin budgets are class constants (tuned in the scenario testbed, then frozen in PROTOCOL.md):

Class initial AC rationale
DISCOVERY (HELLO, empty-path probes) 100 (5 s) must cross an unknown cell, never the whole network
CHAT (CHANMSG/DM) 120 (6 s) the workhorse
CONTROL (ACK/RACK/LANEGRANT) 60 (3 s) short, targeted
BULK (DCCDATA) n/a burst lanes are scheduled, not flooded; the lease’s airtime_budget_ms governs

Router re-origination. A router forwarding traffic across a scope boundary (edge→backbone or backbone→edge) stamps a fresh budget: the router is the path authority (CONSOLIDATED §router-as-authority), and each cell’s spectrum is a separate resource domain. End-to-end reach across a federation is therefore routers × per-cell budget — effectively unbounded network diameter with strictly bounded per-cell cost.

Region coupling (deliberate over-engineering that pays rent). The router’s re-origination budget scales with the local regulatory duty budget (../REGULATORY_SURVEY.md): an EU868 1%-subband cell hands out leaner credit than a US915 cell. One knob makes routing pressure track the actual legal spectrum budget — regulation becomes an input to the routing plane instead of a separately-tuned constant.

4. What bounds what (the invariant statement)

Section titled “4. What bounds what (the invariant statement)”

For any single injected frame:

  • Depth (cumulative airtime along any path) ≤ AC₀ × 50 ms — enforced hop-by-hop by the debit rule.
  • Width (redundant copies per hop neighborhood) — bounded by the contention window + suppression Rule A; suppression can only delay, never cancel, an honest relay (threat R1), so width control cannot black-hole.
  • Revisits — dedup exactly-once per node (false negatives absorbed by depth bound).
  • Local ceiling — regardless of what a frame claims, every relay’s own AirtimeLedger duty-cycle token bucket is the hard local cap. A forged AC=255 cannot make any node exceed its regional budget.

AC lives in the mutable region (like TH before it, excluded from the canonical image and Tag8) because relays must rewrite it and keyless foreign relays can hold no MAC key — same trust class, same accepted residuals as the ERB (THREAT R-3, R-4):

  • Inflation (attacker rewrites AC upward): burns spectrum in the attacker’s own radio neighborhood only; per-relay AirtimeLedger and suppression still cap actual TX. Equivalent to v1 TTL inflation; contained, not prevented.
  • Deflation / strip: indistinguishable from the relay simply not forwarding — the grayhole case already accepted and bounded by R1.
  • No trust decision reads AC. Like the ERB, it can steer efficiency, never identity, access, or delivery attestation.
  • Byte 2 THAC in the v2 layout. The hops-taken nibble is deleted; observability moves to ERB.TRAIL/trail_len (diagnostics-only, already specced) and counters (fwd.credit_drop, fwd.relayed).
  • AC joins the Tag8 mutable-mask exclusions exactly as TH did.
  • Golden bytes: new v2 vectors in test_packet.cpp beside the v1 set; v1 vectors stay as the blind-relay compatibility record.
  • Unknown-version frames are still blind-relayed — v2 relays debit their own TOA against the byte-2 field of known versions only; unknown versions ride the bounded store-and-forward rules unchanged.

Scenario-testbed runs that must pass before v2 golden bytes freeze (../SCENARIO_TESTBED.md):

  1. credit_depth — delivery ratio vs. network diameter at each preset; confirm the table in §3 empirically (sim TOA, not datasheet TOA).
  2. hilltop_asym — 200-hear/5-heard asymmetry: uplink gradient descent + asymmetric ERB return must deliver with AC to spare; measure credit consumption on the long ascending leg.
  3. eu868_duty_starve — region-coupled re-origination keeps every node’s ledger legal under saturation load.
  4. Sweep AC unit (25/50/100 ms) and class budgets; pick the knee where delivery ratio plateaus. Constants land in PROTOCOL.md with the sweep traces committed under docs/research/traces/.

Open questions for the sweep: whether DISCOVERY needs a cell-size hint from BEACON to size its budget; whether the min-debit floor should be 2 on BW500 presets to damp 255-relay pathologies; whether routers should decay re-origination budgets under measured congestion (ties into the beacon cadence controller’s inputs).