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 srcdst 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 <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::reply_kind_t;
 49using tr::graph::role_t;
 50using tr::wire::opt_t;
 51using tr::wire::type_t;
 52
 53/** @brief Report expectation @p what and record a failure on @p ok. */
 54void check(bool& ok, bool cond, const char* what) {
 55    std::printf("  [%s] %s\n", cond ? "ok" : "FAIL", what);
 56    ok = ok && cond;
 57}
 58
 59/** @brief A `transport_t` that keeps every frame handed to it — the "wire", made inspectable. */
 60struct recording_link_t : tr::net::transport_t {
 61    std::vector<std::vector<std::byte>> sent; /**< @brief Frames emitted on this link, in order. */
 62    void send(std::span<const std::byte> frame) override {
 63        sent.emplace_back(frame.begin(), frame.end());
 64    }
 65    void send(std::span<const std::span<const std::byte>> iov) override {
 66        std::vector<std::byte> flat;
 67        for (const auto part : iov) flat.insert(flat.end(), part.begin(), part.end());
 68        sent.push_back(std::move(flat));
 69    }
 70};
 71
 72/** @brief What the origin's reply sink saw — the terminal `FWD{REPLY}`, flattened once. */
 73struct reply_sink_t {
 74    std::size_t count = 0;             /**< @brief Replies that terminated here. */
 75    std::vector<std::byte> last_frame; /**< @brief The last one's complete bytes. */
 76};
 77
 78/** @brief A `PATH` TLV over @p segs — RFC-0018 packed segment records, `opt.PL` clear. */
 79std::vector<std::byte> path_tlv(std::initializer_list<std::string_view> segs) {
 80    std::vector<std::byte> body;
 81    for (std::string_view s : segs) (void)tr::wire::emit_path_segment(body, s);
 82    std::vector<std::byte> out;
 83    tr::wire::emit_tlv(out, type_t::PATH, opt_t{}, body);
 84    return out;
 85}
 86
 87/** @brief A one-byte `VALUE` TLV carrying @p v. */
 88std::vector<std::byte> value_tlv(std::uint8_t v) {
 89    const std::byte b{v};
 90    std::vector<std::byte> out;
 91    tr::wire::emit_tlv(out, type_t::VALUE, opt_t{}, std::span<const std::byte>(&b, 1));
 92    return out;
 93}
 94
 95/** @brief `FWD[.pl]{ VALUE op, PATH dst, PATH src }` — a READ carries no payload. */
 96std::vector<std::byte> fwd_read(std::initializer_list<std::string_view> dst,
 97                                std::initializer_list<std::string_view> src) {
 98    std::vector<std::byte> body = value_tlv(static_cast<std::uint8_t>(fwd_op_t::READ));
 99    for (const auto& part : {path_tlv(dst), path_tlv(src)}) {
100        body.insert(body.end(), part.begin(), part.end());
101    }
102    std::vector<std::byte> out;
103    tr::wire::emit_tlv(out, type_t::FWD, opt_t{.pl = true}, body);
104    return out;
105}
106
107/** @brief The `PATH` child at index @p i of @p frame, re-encoded — or empty if absent. */
108std::vector<std::byte> path_child(std::span<const std::byte> frame, std::size_t i) {
109    const auto tlv = tr::wire::decode(frame);
110    if (!tlv || tlv->children.size() <= i || tlv->children[i].type != type_t::PATH) return {};
111    return tr::wire::encode(tlv->children[i]);
112}
113
114}  // namespace
115
116int main() {
117    bool ok = true;
118
119    // The origin: it holds the link to A and a sink for replies that get all the way back.
120    graph_t graph_o;
121    tr::net::fwd_router_t router_o(graph_o);
122    recording_link_t o_to_a;
123
124    // The terminus: it holds the vertex and its link back toward the origin.
125    graph_t graph_b;
126    tr::net::fwd_router_t router_b(graph_b);
127    recording_link_t b_to_o;
128
129    if (!router_o.add_child("a", o_to_a) || !router_b.add_child("o", b_to_o)) {
130        std::fprintf(stderr, "route_reply_home: add_child failed — nothing registered\n");
131        return 1;
132    }
133    const auto v = graph_b.register_vertex(path_t("/sensor/temp"), role_t::STORED_VALUE);
134    (void)graph_b.write(v, *tr::view::over_bytes(value_tlv(0x2A)));
135
136    reply_sink_t sink;
137    router_o.on_reply(
138        [](void* ctx, const tr::view::rope_t& reply) {
139            auto* s = static_cast<reply_sink_t*>(ctx);
140            ++s->count;
141            // The router hands the reply over rope-native; a sink that wants contiguous bytes
142            // materializes once and keeps the view alive while it reads (ADR-0052/ADR-0055).
143            const tr::view::view_t flat = reply.materialize();
144            const auto b = flat.bytes();
145            s->last_frame.assign(b.begin(), b.end());
146        },
147        &sink);
148
149    // The app hands its READ to its own router over a link the router calls "app". The frame
150    // carries an EMPTY src: the origin has nothing to prepend yet.
151    router_o.on_frame("app", fwd_read({"a", "sensor", "temp"}, {}));
152    check(ok, o_to_a.sent.size() == 1, "the origin forwarded the READ toward A");
153    if (o_to_a.sent.size() != 1) return 1;
154    check(ok, path_child(o_to_a.sent[0], 2) == path_tlv({"app"}),
155          "and grew src to /app — the one hop the request has taken so far");
156
157    // The terminus resolves /sensor/temp locally and answers. It sends the reply on the link
158    // the request ARRIVED on, addressed to the src it was handed.
159    router_b.on_frame("o", o_to_a.sent[0]);
160    check(ok, b_to_o.sent.size() == 1, "the terminus answered on the link the request came in on");
161    if (b_to_o.sent.size() != 1) return 1;
162    check(ok, path_child(b_to_o.sent[0], 1) == path_tlv({"app"}),
163          "the REPLY's dst IS the request's accumulated src — no correlation table was consulted");
164    check(ok, path_child(b_to_o.sent[0], 2) == path_tlv({"sensor", "temp"}),
165          "and its src names the responder, which is how the answer carries its provenance");
166
167    // Back at the origin, "app" names no child, so the route is spent and the reply terminates.
168    router_o.on_frame("a", b_to_o.sent[0]);
169    check(ok, sink.count == 1, "the reply reached the origin's on_reply sink");
170    check(ok, !sink.last_frame.empty() && sink.last_frame == b_to_o.sent[0],
171          "byte-for-byte the frame the terminus emitted — the origin was one hop away");
172
173    const auto reply = tr::wire::decode(sink.last_frame);
174    const bool is_result =
175        reply && reply->children.size() >= 4 && reply->children[3].payload.size() == 1 &&
176        static_cast<reply_kind_t>(reply->children[3].payload[0]) == reply_kind_t::RESULT;
177    check(ok, is_result, "and it is kind=RESULT, carrying the value the terminus read");
178
179    check(ok, router_o.handles().ingress_count() == 0 && router_b.handles().ingress_count() == 0,
180          "neither node stored anything to make the round trip work");
181
182    std::printf("request src /app -> reply dst /app; %zu correlation entries anywhere\n",
183                router_o.handles().ingress_count() + router_b.handles().ingress_count());
184    return ok ? 0 : 1;
185}

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