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→dsthandoff is asserted directly. The example re-encodes thePATHchild of each frame and compares: the request’ssrcis/app, and the reply’sdstis/app.The reply’s own
srcnames 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_replyreceives arope_t; the router performs no decode and no flatten (ADR-0055). A sink that wants contiguous bytes callsmaterialize()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,
appnames 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_PLANEis 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.