Wire format¶
Canonical, simulator-agnostic on-wire layout of an ant packet. This is the
single source of truth referenced by AGENTS.md golden rule #4
and by docs/porting-notes.md. The encoder/decoder lives in
core/src/ant_message_codec.cpp; the NS-2 header (ns2/src/ant_packet_ns2) and
the NS-3 AntHeader (ns3/model/anthocnet-packet) both delegate to it, so a
byte produced on one simulator is interpreted identically on the other.
All multi-byte integers and doubles are little-endian. Variable arrays are
length-prefixed with a uint16 count. There is exactly one layout — see
ADR-0006 for why we version it and why
we do not negotiate alternative layouts.
Status. The table below is the layout the codec emits today —
kWireVersion = 0x03. All decided wire changes have landed: the version byte + bounds/enum enforcement (item 12, 0x01),broadcastBudget+AntType::LinkFail(item 05, 0x02), and the per-hop-deltaAntHop.timesemantic + the backward-ant byte slim (item 02/ADR-0009, 0x03). Keep this section in sync with the code as the format evolves.
Frame layout (current code)¶
The fixed 37-byte head, then three length-prefixed variable arrays:
packet-beta
0-7: "version (u8)"
8-15: "type (u8)"
16-23: "direction (u8)"
24-55: "src (i32)"
56-87: "dst (i32)"
88-119: "seqNum (u32)"
120-183: "timeStart (f64)"
184-247: "lifeAnt (f64)"
248-279: "broadcastBudget (i32)"
280-295: "nVisited (u16)"
296-359: "visited[] — 12 bytes per AntHop, then nHistory / history[] / nHello / helloDests[]"
Everything after nVisited is variable-length and repeats the same shape:
a u16 count followed by that many 12-byte records ({i32 node; f64 value}).
Each count is rejected on decode if it exceeds its cap — kMaxVisitedOnWire,
kMaxHistoryOnWire, kMaxHelloOnWire — so a malformed or hostile frame cannot
allocate unboundedly. Byte offsets and semantics per field:
| Offset | Field | Type | Bytes | Meaning |
|---|---|---|---|---|
| 0 | version |
u8 |
1 | Wire-format version (kWireVersion, currently 0x03). Checked first; a mismatch is rejected in O(1). |
| 1 | type |
u8 |
1 | Ant role (AntType): 0x01 Hello, 0x02 Reactive, 0x04 Proactive, 0x08 Repair, 0x10 LinkFail. Validated on decode. |
| 2 | direction |
u8 |
1 | AntDirection: 0x11 Up (forward), 0x12 Down (backward). Validated on decode. |
| 3 | src |
i32 |
4 | Originating node. |
| 7 | dst |
i32 |
4 | Final destination. |
| 11 | seqNum |
u32 |
4 | Per-source ant sequence number (32-bit; see vs. original). |
| 15 | timeStart |
f64 |
8 | Generation time, for trip-time accounting. |
| 23 | lifeAnt |
f64 |
8 | Repair-ant lifetime budget (seconds). |
| 31 | broadcastBudget |
i32 |
4 | Remaining (re)broadcasts allowed (-1 = untracked); bounds repair/LinkFail propagation (item 05). |
| 35 | nVisited |
u16 |
2 | Count of visited entries (rejected if > kMaxVisitedOnWire). |
| 37 | visited[] |
AntHop |
12·n | Forward stack: { i32 node; f64 time } per hop. time is a per-hop delta (seconds, item 02). |
| … | nHistory |
u16 |
2 | Count of history entries (rejected if > kMaxHistoryOnWire). |
| … | history[] |
AntHop |
12·n | Back-ant retraced path: { i32 node; f64 time }. The receiver reconstructs the deposit state (prevHop/hops/pathTime/pheromone) from this (ADR-0009). |
| … | nHello |
u16 |
2 | Count of helloDests entries (rejected if > kMaxHelloOnWire). Also carries the LinkFail payload of (dest, newBestPheromone). |
| … | helloDests[] |
HelloDest |
12·n | Hello adverts / LinkFail payload: { i32 node; f64 pheromone }. |
prevHop/hops/pathTime (was prevSINR)/pheromone are no longer on the
wire (ADR-0009): they are transient deposit state, recomputed at each receiver
from history. Fixed prefix (version through broadcastBudget) is 35 bytes;
the three empty arrays add 6 bytes of counts, so the minimum frame is 41 bytes.
Worst-case size is bounded because nVisited/nHistory/nHello are capped on
decode (kMaxVisitedOnWire/kMaxHistoryOnWire/kMaxHelloOnWire, enforcement
added by item 12).
Planned wire evolution¶
kWireVersion is a monotonic counter, not a fixed value — 0x01 (item 12),
0x02 (item 05), 0x03 (item 02 slim); each subsequent layout/semantic change
increments it. Do not hard-code a number; use current + 1.
- ✅ Version byte (landed, 0x01) — item 12,
ADR-0006.
version : u8at offset 0;deserializechecks it first and drops unknown values in O(1), then validates thetype/directionenums and clamps the element counts to their caps. - ✅ Repair / link-failure (landed, 0x02) —
item 05. Added
broadcastBudget : i32and theAntType::LinkFailrole (the notification reuses thehelloDests{node, pheromone}shape). - ✅ Backward-ant slim (landed, 0x03) —
item 02,
ADR-0009. Removed
prevHop,hops,prevSINR→pathTime,pheromonefrom the serialized image (−24 bytes); they are recomputed at each receiver fromhistory.
Versioning¶
- Offset 0 is a
u8version constant (kWireVersion), introduced by item 12. deserializechecks it first and returnsfalseon any unknown value — before reading any length field — so foreign, corrupt, or stale-version frames are rejected in O(1) at the trust boundary.- The constant is defined once in the codec and asserted identical by the NS-2
and NS-3 headers and the round-trip test. Any change to field order,
width, units, or semantics bumps
kWireVersion(see the maintenance rule in ADR-0006). - We do not negotiate or translate between versions. A mismatched version is a dropped packet, not a fallback — within a single simulation every node runs the same build, and across builds we want cross-version traces to fail loudly rather than silently misparse. This is the deliberate trade-off recorded in the ADR.
This makes the "one canonical wire format" invariant machine-checkable: the field layout and its version move together, in lockstep, across the codec and both adapters.
vs. the original implementation¶
The original NS-2 module (vendored inside ns-allinone-2.34) had no codec and
no portable wire format at all — the ant lived as a C++ object whose
visited/history/hello lists were AntTimeEntry** heap arrays referenced
directly from the NS-2 packet header. That carried three defects (see
ADR-0004 and
porting-notes):
| Aspect | Original | This implementation |
|---|---|---|
| Path / history / hello storage | Header-resident AntTimeEntry** heap pointers |
Length-prefixed value arrays in a byte buffer |
| Broadcast semantics | Byte-copying the header duplicated a raw pointer → double-free | POD copy is safe; ants are trivially copyable |
| Serialized size | size() returned sizeof(pointer) → wrong airtime/MAC modelling |
serializedSize() returns the true byte count |
seqNum width |
u_int8_t → wrapped after 256 ants, aliasing (src,seq) dedup |
u32 |
| Path / dedup growth | Unbounded | Capped by Config::maxPathLength / maxHistory (also enforced on decode) |
| Endianness / portability | Native struct layout (compiler/arch dependent) | Explicit little-endian, compiler/arch independent |
| Version marker | None | None today; 1-byte version at offset 0 planned (item 12, ADR-0006) |
| Transient compute state on the wire | N/A (live object) | prevHop/hops/pathTime/pheromone are off the wire (item 02, ADR-0009): the wire carries only identity + path, and each receiver recomputes them from history |
So there is no "original wire format" to be byte-compatible with; this codec is the first portable format, and the planned version byte exists so the next change is detectable in a way the original never was.
vs. the protocol specification¶
AntHocNet is defined by its papers ([1], [2], [3]), not by any byte-level wire standard. The papers specify the contents and semantics of ants, never an octet layout, so this format is one faithful encoding of the paper's fields — plus a few implementation-only fields. There is no newer or competing wire specification to track.
| Wire field | Status vs. spec | Notes |
|---|---|---|
type, direction |
Spec concept | Reactive/proactive forward ants, backward ants, hello messages, repair ants ([1] §3). |
src, dst |
Spec concept | Session endpoints an ant is routing between. |
visited, history (node + time) |
Spec concept | Forward ant records the path and per-hop timing; backward ant retraces it to deposit pheromone ([1] §3.1–3.2). |
helloDests (node + pheromone) |
Spec concept | Pheromone diffusion: hellos advertise virtual-pheromone goodness for active destinations ([2], diffusion section). The value is real, not constant — see item 03. |
hops |
Spec-aligned | Hop count in the path metric. Computed locally, removed from the wire by item 02 (ADR-0009). |
pathTime (was prevSINR) |
Implementation | Accumulated time/cost term; the old prevSINR name was a misnomer (the metric is delay/quality, not SINR). Renamed and removed from the wire by item 02; recomputed locally from history. |
pheromone |
Implementation | Running estimate on the backward ant; the spec computes pheromone per node. Removed from the wire by item 02 — each node recomputes. |
lifeAnt |
Implementation | Repair-ant TTL budget — a concrete realisation of the spec's bounded local repair. |
timeStart, seqNum |
Implementation | Trip-time accounting and (src,seq) duplicate suppression. The spec assumes ant identification; these are the mechanisms. |
prevHop |
Implementation | Retrace bookkeeping. Computed locally, removed from the wire by item 02. |
version |
Implementation | Frame envelope; no spec analogue. Planned, item 12. |
Where a field's behaviour deviates from the paper, the deviation is tracked in the design ADRs and GitHub issues (items 02, 03 in particular), not here — this document describes only the bytes.
Maintenance checklist¶
When you change AntMessage fields, update in the same field order and bump
kWireVersion:
core/include/anthocnet/core/ant_message.h(the struct).core/src/ant_message_codec.cpp(serialize + deserialize + size constants).ns2/src/ant_packet_ns2(NS-2 POD header +wireSize()).ns3/model/anthocnet-packet(AntHeaderSerialize/Deserialize/GetSerializedSize).core/tests/test_codec.cpp(round-trip + the version-mismatch rejection test).- This table and the offsets above.