Three nodes, and a forwarder that stores nothing (L4 routing)

A chain is the one-hop rule applied again. A holds a child called b, B holds a child called c, C holds the vertex; a client writes /b/c/sensor/temp. There is no route discovery, no flooding, no forwarding information base and no per-flow entry anywhere — each node applies the same test, and the route consumes itself as it travels (ADR-0040).

The check that matters is the negative one. After the write has crossed B, B’s routing plane holds exactly what it held before: zero label bindings, zero link shells, the same receiver-context count. That bounds a forwarder’s memory by its topology (how many links it has) rather than by its traffic (how many flows cross it) — the reason a 16 KB node can be a forwarder at all.

What to notice

  • The “before” values are read, not assumed. The example snapshots B’s counters and compares against the snapshot. Asserting == 0 against a hard-coded zero would keep passing if the baseline ever moved.

  • Each hop’s wire bytes are asserted. dst=/c/sensor/temp, src=/cli on the A→B wire, dst=/sensor/temp, src=/a/cli on the B→C wire. The return route is visibly under construction, one hop at a time.

  • Hops are driven explicitly. The frame a node emits is handed to the next node’s on_frame. A loopback_channel_t or a socket does the same thing with threads in between — see two nodes over a wire — but driving it by hand keeps the example synchronous and puts the intermediate bytes where they can be asserted on.

  • The names are private. b means something only to A and c means something only to B. /b/c/sensor/temp is the composition, spelled by whoever holds both mounts.

  • Statelessness is a choice with a price, and the price is on the wire. Every frame re-carries its route. The label plane is what buys that back for flows that repeat — deliberately, per flow, and only when someone asks.

  • This target needs the FWD net plane. It is built only when LIBTRACER_NET_PLANE is on (the default). Nothing in it is conditional at run time.

Source

  1/*
  2 * SPDX-License-Identifier: Apache-2.0
  3 * SPDX-FileCopyrightText: Copyright 2026 avatarsd LLC
  4 */
  5
  6/**
  7 * @file
  8 * @brief ONE CONCEPT — a chain is just the one-hop rule applied again, and the MIDDLE hop
  9 *        stores nothing at all.
 10 *
 11 * Three nodes: A holds a child called `b`, B holds a child called `c`, C holds the vertex.
 12 * A client writes `/b/c/sensor/temp`. There is no route discovery, no flooding, no forwarding
 13 * information base and no per-flow entry anywhere: each node applies the same test the
 14 * one-hop examples showed, and the route consumes itself as it travels
 15 * (RFC-0004 §B, ADR-0040).
 16 *
 17 * The check that matters is the negative one. After the write has crossed B, B's routing plane
 18 * holds exactly what it held before: zero label bindings, zero link shells, and the same
 19 * receiver-context count. That is the slice-3 stateless-forwarder property, and it is what
 20 * bounds a forwarder's memory by its TOPOLOGY (how many links it has) rather than by its
 21 * TRAFFIC (how many flows cross it) — the reason a 16 KB node can be a forwarder at all.
 22 *
 23 * Each hop is driven explicitly: the frame a node emits is handed to the next node's
 24 * `on_frame`. A `loopback_channel_t` or a socket would do the same thing with threads in
 25 * between (`two_node_fwd` shows that shape); doing it by hand keeps the example synchronous
 26 * and puts the intermediate bytes where they can be asserted on.
 27 *
 28 * Runs under ctest as `example_route_multi_hop`; returns non-zero on any failed check.
 29 */
 30
 31#include <cstddef>
 32#include <cstdint>
 33#include <cstdio>
 34#include <initializer_list>
 35#include <span>
 36#include <string_view>
 37#include <vector>
 38
 39#include "libtracer/fwd_router.hpp"
 40#include "libtracer/tlv_emit.hpp"
 41#include "libtracer/tracer.hpp"
 42
 43namespace {
 44
 45using tr::graph::fwd_op_t;
 46using tr::graph::graph_t;
 47using tr::graph::path_t;
 48using tr::graph::role_t;
 49using tr::wire::opt_t;
 50using tr::wire::type_t;
 51
 52/** @brief Report expectation @p what and record a failure on @p ok. */
 53void check(bool& ok, bool cond, const char* what) {
 54    std::printf("  [%s] %s\n", cond ? "ok" : "FAIL", what);
 55    ok = ok && cond;
 56}
 57
 58/** @brief A `transport_t` that keeps every frame handed to it — the "wire", made inspectable. */
 59struct recording_link_t : tr::net::transport_t {
 60    std::vector<std::vector<std::byte>> sent; /**< @brief Frames emitted on this link, in order. */
 61    void send(std::span<const std::byte> frame) override {
 62        sent.emplace_back(frame.begin(), frame.end());
 63    }
 64    void send(std::span<const std::span<const std::byte>> iov) override {
 65        std::vector<std::byte> flat;
 66        for (const auto part : iov) flat.insert(flat.end(), part.begin(), part.end());
 67        sent.push_back(std::move(flat));
 68    }
 69};
 70
 71/** @brief A `PATH` TLV over @p segs — RFC-0018 packed segment records, `opt.PL` clear. */
 72std::vector<std::byte> path_tlv(std::initializer_list<std::string_view> segs) {
 73    std::vector<std::byte> body;
 74    for (std::string_view s : segs) (void)tr::wire::emit_path_segment(body, s);
 75    std::vector<std::byte> out;
 76    tr::wire::emit_tlv(out, type_t::PATH, opt_t{}, body);
 77    return out;
 78}
 79
 80/** @brief A one-byte `VALUE` TLV carrying @p v. */
 81std::vector<std::byte> value_tlv(std::uint8_t v) {
 82    const std::byte b{v};
 83    std::vector<std::byte> out;
 84    tr::wire::emit_tlv(out, type_t::VALUE, opt_t{}, std::span<const std::byte>(&b, 1));
 85    return out;
 86}
 87
 88/** @brief `FWD[.pl]{ VALUE op, PATH dst, PATH src, VALUE payload }` (RFC-0004 §B child order). */
 89std::vector<std::byte> fwd_write(std::initializer_list<std::string_view> dst,
 90                                 std::initializer_list<std::string_view> src) {
 91    std::vector<std::byte> body = value_tlv(static_cast<std::uint8_t>(fwd_op_t::WRITE));
 92    for (const auto& part : {path_tlv(dst), path_tlv(src), value_tlv(0x2A)}) {
 93        body.insert(body.end(), part.begin(), part.end());
 94    }
 95    std::vector<std::byte> out;
 96    tr::wire::emit_tlv(out, type_t::FWD, opt_t{.pl = true}, body);
 97    return out;
 98}
 99
100}  // namespace
101
102int main() {
103    bool ok = true;
104    graph_t graph_a, graph_b, graph_c;
105    tr::net::fwd_router_t router_a(graph_a), router_b(graph_b), router_c(graph_c);
106    recording_link_t a_to_b, b_to_c;
107
108    // A knows B as "b"; B knows C as "c". Neither name means anything to any other node —
109    // the client's address /b/c/... is the composition of the two, spelled by whoever holds
110    // both mounts, which is what makes the route explicit and loop-free by construction.
111    if (!router_a.add_child("b", a_to_b) || !router_b.add_child("c", b_to_c)) {
112        std::fprintf(stderr, "route_multi_hop: add_child failed — nothing registered\n");
113        return 1;
114    }
115    (void)graph_c.register_vertex(path_t("/sensor/temp"), role_t::STORED_VALUE);
116
117    // Whatever B held before the flow — nothing, but read it rather than assume it.
118    const std::size_t b_ingress_before = router_b.handles().ingress_count();
119    const std::size_t b_links_before = router_b.handles().link_count();
120    const std::size_t b_ctx_before = router_b.receiver_ctx_count();
121
122    // Hop 1: A strips "b", grows src by "cli".
123    router_a.on_frame("cli", fwd_write({"b", "c", "sensor", "temp"}, {}));
124    check(ok, a_to_b.sent.size() == 1, "A forwarded toward B");
125    if (a_to_b.sent.size() != 1) return 1;
126    check(ok, a_to_b.sent[0] == fwd_write({"c", "sensor", "temp"}, {"cli"}),
127          "the frame on the A→B wire: dst=/c/sensor/temp, src=/cli");
128
129    // Hop 2: B applies the SAME rule to the SAME frame — nothing about it is hop-aware.
130    router_b.on_frame("a", a_to_b.sent[0]);
131    check(ok, b_to_c.sent.size() == 1, "B forwarded toward C");
132    if (b_to_c.sent.size() != 1) return 1;
133    check(ok, b_to_c.sent[0] == fwd_write({"sensor", "temp"}, {"a", "cli"}),
134          "the frame on the B→C wire: dst=/sensor/temp, src=/a/cli — the return route, growing");
135
136    // Hop 3: at C the leading route segment names no child, so C is the terminus and writes.
137    router_c.on_frame("b", b_to_c.sent[0]);
138    check(ok, graph_c.read(path_t("/sensor/temp")).has_value(),
139          "C was the terminus and the write landed three hops from the client");
140
141    // The point of the example: B is exactly as it was.
142    check(ok,
143          router_b.handles().ingress_count() == b_ingress_before &&
144              router_b.handles().egress_count() == 0,
145          "the middle hop bound NO label state for the flow it just carried");
146    check(ok, router_b.handles().link_count() == b_links_before,
147          "and created no per-link shell — its memory is a function of topology, not traffic");
148    check(ok, router_b.receiver_ctx_count() == b_ctx_before,
149          "and its receiver-context chain did not grow");
150
151    std::printf("3 hops, 1 rule, %zu per-flow binding(s) on the forwarder\n",
152                router_b.handles().ingress_count() + router_b.handles().egress_count());
153    return ok ? 0 : 1;
154}

See also: fwd-router module · communication flows · composition over the network · the source route · the reply home.