The src you accumulated is the way home (L4 routing)

Every hop that stripped a dst mount run prepended its own name for the inbound link to src. By the time the request reaches the terminus, src spells the whole way back — in the frame, in the hands of the node that has to answer. So the terminus builds FWD{REPLY} with dst = the request's src and sends it back over the bidirectional link the request arrived on; each hop retraces its own link the same way, consuming one mount run of that dst as it goes. No reply address and no correlation id are needed anywhere (RFC-0004 §D, CONTEXT.md §Path-as-route).

That is the other half of the statelessness on the multi-hop page. A forwarder with a request table would need an entry per in-flight request, a timeout to reap it, and a policy for what happens when it overflows.

What to notice

  • The src→dst handoff is asserted directly. The example re-encodes the PATH child of each frame and compares: the request’s src is /app, and the reply’s dst is /app.

  • The reply’s own src names the responder. /sensor/temp, not the terminus node — which is how an answer carries its provenance without a separate field.

  • The reply is handed over rope-native. on_reply receives a rope_t; the router performs no decode and no flatten (ADR-0055). A sink that wants contiguous bytes calls materialize() once and keeps the view alive while it reads — a single-link reply, the common case, costs no copy at all.

  • The route terminates by running out. At the origin, app names no child, so the reply is terminal and the sink fires. Nothing marks a frame as “the last hop”; the address does.

  • Neither node stored anything. Both routers report zero bindings after the round trip.

  • 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 — nothing remembers the request: the `src` a frame accumulated on the way
  9 *        out IS the route its `FWD{REPLY}` comes home along.
 10 *
 11 * Every hop that stripped a `dst` mount run prepended its own name for the inbound link to `src`
 12 * (route_dst_is_source_route). By the time the request reaches the terminus, `src` spells the
 13 * whole way back — in the frame, in the hands of the node that has to answer. So the terminus
 14 * builds `FWD{REPLY}` with `dst = the request's src` and sends it back over the bidirectional
 15 * link the request arrived on; each hop retraces its own link the same way, consuming one mount
 16 * run of that `dst` as it goes. No reply address and no correlation id are needed anywhere
 17 * (RFC-0004 §D, `CONTEXT.md` §Path-as-route).
 18 *
 19 * That is the other half of the statelessness in route_multi_hop. A forwarder with a request
 20 * table would need an entry per in-flight request, a timeout to reap it, and a policy for what
 21 * happens when it overflows. Carrying the return route instead costs bytes on the wire and
 22 * nothing on the hop — and it is exactly the cost the route-handle label plane buys back for
 23 * flows that repeat (route_label_compact).
 24 *
 25 * The reply terminates where `dst` runs out of route segments that name a child: at the origin,
 26 * whose `on_reply` sink fires (ADR-0055 — rope-native, no decode and no flatten in the router).
 27 *
 28 * Runs under ctest as `example_route_reply_home`; returns non-zero on any failed check.
 29 */
 30
 31#include <cstddef>
 32#include <cstdint>
 33#include <cstdio>
 34#include <initializer_list>
 35#include <iterator>
 36#include <span>
 37#include <string_view>
 38#include <vector>
 39
 40#include "libtracer/fwd_router.hpp"
 41#include "libtracer/tlv_emit.hpp"
 42#include "libtracer/tracer.hpp"
 43
 44namespace {
 45
 46using tr::graph::fwd_op_t;
 47using tr::graph::graph_t;
 48using tr::graph::path_t;
 49using tr::graph::reply_kind_t;
 50using tr::graph::role_t;
 51using tr::wire::opt_t;
 52using tr::wire::type_t;
 53
 54/** @brief Report expectation @p what and record a failure on @p ok. */
 55void check(bool& ok, bool cond, const char* what) {
 56    std::printf("  [%s] %s\n", cond ? "ok" : "FAIL", what);
 57    ok = ok && cond;
 58}
 59
 60/** @brief A `transport_t` that keeps every frame handed to it — the "wire", made inspectable. */
 61struct recording_link_t : tr::net::transport_t {
 62    std::vector<std::vector<std::byte>> sent; /**< @brief Frames emitted on this link, in order. */
 63    void send(std::span<const std::byte> frame) override {
 64        sent.emplace_back(frame.begin(), frame.end());
 65    }
 66    void send(std::span<const std::span<const std::byte>> iov) override {
 67        std::vector<std::byte> flat;
 68        for (const auto part : iov) flat.insert(flat.end(), part.begin(), part.end());
 69        sent.push_back(std::move(flat));
 70    }
 71};
 72
 73/** @brief What the origin's reply sink saw — the terminal `FWD{REPLY}`, flattened once. */
 74struct reply_sink_t {
 75    std::size_t count = 0;             /**< @brief Replies that terminated here. */
 76    std::vector<std::byte> last_frame; /**< @brief The last one's complete bytes. */
 77};
 78
 79/** @brief A `PATH` TLV over @p segs — RFC-0018 packed segment records, `opt.PL` clear. */
 80std::vector<std::byte> path_tlv(std::initializer_list<std::string_view> segs) {
 81    std::vector<std::byte> body;
 82    for (std::string_view s : segs) (void)tr::wire::emit_path_segment(body, s);
 83    std::vector<std::byte> out;
 84    tr::wire::emit_tlv(out, type_t::PATH, opt_t{}, body);
 85    return out;
 86}
 87
 88/** @brief A one-byte `VALUE` TLV carrying @p v. */
 89std::vector<std::byte> value_tlv(std::uint8_t v) {
 90    const std::byte b{v};
 91    std::vector<std::byte> out;
 92    tr::wire::emit_tlv(out, type_t::VALUE, opt_t{}, std::span<const std::byte>(&b, 1));
 93    return out;
 94}
 95
 96/** @brief `FWD[.pl]{ VALUE op, PATH dst, PATH src }` — a READ carries no payload. */
 97std::vector<std::byte> fwd_read(std::initializer_list<std::string_view> dst,
 98                                std::initializer_list<std::string_view> src) {
 99    std::vector<std::byte> body = value_tlv(static_cast<std::uint8_t>(fwd_op_t::READ));
100    for (const auto& part : {path_tlv(dst), path_tlv(src)}) {
101        body.insert(body.end(), part.begin(), part.end());
102    }
103    std::vector<std::byte> out;
104    tr::wire::emit_tlv(out, type_t::FWD, opt_t{.pl = true}, body);
105    return out;
106}
107
108/** @brief The `PATH` child at index @p i of @p frame, re-encoded — or empty if absent. */
109std::vector<std::byte> path_child(std::span<const std::byte> frame, std::size_t i) {
110    const auto tlv = tr::wire::tlv_node_t::over(frame);
111    if (!tlv) return {};
112    const tr::wire::tlv_children_t kids = tlv->children();
113    const auto at = std::ranges::next(kids.begin(), static_cast<std::ptrdiff_t>(i), kids.end());
114    if (at == kids.end() || (*at).type() != type_t::PATH) return {};
115    const std::span<const std::byte> whole = (*at).bytes();  // the child's own encoding
116    return {whole.begin(), whole.end()};
117}
118
119}  // namespace
120
121int main() {
122    bool ok = true;
123
124    // The origin: it holds the link to A and a sink for replies that get all the way back.
125    graph_t graph_o;
126    tr::net::fwd_router_t router_o(graph_o);
127    recording_link_t o_to_a;
128
129    // The terminus: it holds the vertex and its link back toward the origin.
130    graph_t graph_b;
131    tr::net::fwd_router_t router_b(graph_b);
132    recording_link_t b_to_o;
133
134    if (!router_o.add_child("a", o_to_a) || !router_b.add_child("o", b_to_o)) {
135        std::fprintf(stderr, "route_reply_home: add_child failed — nothing registered\n");
136        return 1;
137    }
138    const auto v = graph_b.register_vertex(path_t("/sensor/temp"), role_t::STORED_VALUE);
139    (void)graph_b.write(v, *tr::view::over_bytes(value_tlv(0x2A)));
140
141    reply_sink_t sink;
142    router_o.on_reply(
143        [](void* ctx, const tr::view::rope_t& reply) {
144            auto* s = static_cast<reply_sink_t*>(ctx);
145            ++s->count;
146            // The router hands the reply over rope-native; a sink that wants contiguous bytes
147            // materializes once and keeps the view alive while it reads (ADR-0052/ADR-0055).
148            const tr::view::view_t flat = reply.materialize();
149            const auto b = flat.bytes();
150            s->last_frame.assign(b.begin(), b.end());
151        },
152        &sink);
153
154    // The app hands its READ to its own router over a link the router calls "app". The frame
155    // carries an EMPTY src: the origin has nothing to prepend yet.
156    router_o.on_frame("app", fwd_read({"a", "sensor", "temp"}, {}));
157    check(ok, o_to_a.sent.size() == 1, "the origin forwarded the READ toward A");
158    if (o_to_a.sent.size() != 1) return 1;
159    check(ok, path_child(o_to_a.sent[0], 2) == path_tlv({"app"}),
160          "and grew src to /app — the one hop the request has taken so far");
161
162    // The terminus resolves /sensor/temp locally and answers. It sends the reply on the link
163    // the request ARRIVED on, addressed to the src it was handed.
164    router_b.on_frame("o", o_to_a.sent[0]);
165    check(ok, b_to_o.sent.size() == 1, "the terminus answered on the link the request came in on");
166    if (b_to_o.sent.size() != 1) return 1;
167    check(ok, path_child(b_to_o.sent[0], 1) == path_tlv({"app"}),
168          "the REPLY's dst IS the request's accumulated src — no correlation table was consulted");
169    check(ok, path_child(b_to_o.sent[0], 2) == path_tlv({"sensor", "temp"}),
170          "and its src names the responder, which is how the answer carries its provenance");
171
172    // Back at the origin, "app" names no child, so the route is spent and the reply terminates.
173    router_o.on_frame("a", b_to_o.sent[0]);
174    check(ok, sink.count == 1, "the reply reached the origin's on_reply sink");
175    check(ok, !sink.last_frame.empty() && sink.last_frame == b_to_o.sent[0],
176          "byte-for-byte the frame the terminus emitted — the origin was one hop away");
177
178    const auto reply = tr::wire::tlv_node_t::over(sink.last_frame);
179    bool is_result = false;
180    if (reply) {
181        const tr::wire::tlv_children_t kids = reply->children();
182        const auto kind = std::ranges::next(kids.begin(), 3, kids.end());  // VALUE kind
183        is_result = kind != kids.end() && (*kind).payload().size() == 1 &&
184                    static_cast<reply_kind_t>((*kind).payload()[0]) == reply_kind_t::RESULT;
185    }
186    check(ok, is_result, "and it is kind=RESULT, carrying the value the terminus read");
187
188    check(ok, router_o.handles().ingress_count() == 0 && router_b.handles().ingress_count() == 0,
189          "neither node stored anything to make the round trip work");
190
191    std::printf("request src /app -> reply dst /app; %zu correlation entries anywhere\n",
192                router_o.handles().ingress_count() + router_b.handles().ingress_count());
193    return ok ? 0 : 1;
194}

See also: fwd-router module · communication flows · views and ownership · the source route · terminus or forward.