OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer commits / cb5ff99

compact_encoding: forward to libcompact instead of writing the bytes again
hyper-games #73 NEXT item 3. src/compact_encoding.h hand-wrote compact-encoding in C++ while swarmd's generated wire linked libcompact — holepunch's own C port of the same format — three directories away. Two codecs for one wire in one repo is one too many, and this repo has already paid for that shape once: the announce signable in docs/PARITY-hyperswarm.md §7 was written twice and diverged, so every unannounce we sent failed verification in silence. Every primitive now forwards: uint, uint8/16/32/64, fixed32, fixed64, and the length-prefixed buffer. 327 lines of code become 224. What stays is what libcompact does not have and hyperswarm does — the ipv4Address pair from compact-encoding-net, the BLAKE2b node id from dht-rpc/lib/peer.js, and the Kademlia XOR helpers — plus two contracts libcompact does not state: decoders return a value rather than an error code, because every call site is written that way; and a truncated decode yields 0/null AND leaves start == end, so a loop over a malformed frame terminates rather than spins. That second contract is why the previous commit went in first. It now holds everywhere: uint64_decode was two uint32 decodes and returned the low word of a number that was never on the wire. 76/76. ⛔ It is not enough that the tests pass — they would pass against a codec that agreed with itself and not with JS. test_verify_vectors is the one that matters: 65/65 against vectors generated by the JavaScript compact-encoding, regenerated for this run. Full suite green, including three_peer_swarm and session_channel on a live testnet, and the GDExtension links libcompact into the .so. libcompact and libutf are symlinked into thirdparty/ exactly as libudx is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cb5ff9969e0a6945340f972bc7a777e5a81f4373
by plankatron <fleeky@gmail.com> · 2026-09-10T18:59:58.000Z

Changed files (6) +124 -152

.gitignore +2 -0
……@@ -16,6 +16,8 @@ config.log
1616# Symlinked dependencies (not part of this repo)
1717godot-cpp
1818thirdparty/libudx
19thirdparty/libcompact
20thirdparty/libutf
1921
2022# Godot
2123.godot/
SConstruct +8 -1
……@@ -44,6 +44,13 @@ env.Append(CPPPATH=["src/"])
4444# still be lifted out of this repo by moving the directory.
4545env.Append(CPPPATH=["liboverlay/include"])
4646
47# libcompact — src/compact_encoding.h forwards its byte work here, so the
48# GDExtension carries it too. Symlinked into thirdparty/ the same way libudx is.
49compact_path = "thirdparty/libcompact"
50env.Append(CPPPATH=[os.path.join(compact_path, "include"),
51 os.path.join("thirdparty", "libutf", "include")])
52compact_sources = Glob(os.path.join(compact_path, "src", "*.c"))
53
4754# libudx — link against the holepunchto UDP transport
4855udx_path = "thirdparty/libudx"
4956env.Append(CPPPATH=[os.path.join(udx_path, "include")])
……@@ -119,7 +126,7 @@ if san:
119126 env.Append(CCFLAGS=["-fsanitize=" + san, "-fno-omit-frame-pointer", "-g"])
120127 env.Append(LINKFLAGS=["-fsanitize=" + san])
121128
122sources = Glob("src/*.cpp") + udx_sources
129sources = Glob("src/*.cpp") + udx_sources + compact_sources
123130
124131if env["target"] in ["editor", "template_debug"]:
125132 try:
daemon/build.sh +5 -1
……@@ -11,7 +11,10 @@ cd "$(dirname "$0")"
1111
1212SRCDIR=../src
1313UDX_INC=../thirdparty/libudx/include
14CXXFLAGS="-std=c++17 -O2 -D_GNU_SOURCE -I. -I$SRCDIR -I$UDX_INC"
14# compact_encoding.h forwards to libcompact; see ../test/build_tests.sh, which
15# compiles those objects into the same cache.
16COMPACT_INC="-I../thirdparty/libcompact/include -I../thirdparty/libutf/include"
17CXXFLAGS="-std=c++17 -O2 -D_GNU_SOURCE -I. -I$SRCDIR -I$UDX_INC $COMPACT_INC"
1518LDFLAGS="-luv -lsodium -lpthread"
1619
1720OBJ=/tmp/hsp-obj
……@@ -58,5 +61,6 @@ g++ -o session-daemon \
5861 $OBJ/session_main.o $OBJ/session_daemon.o \
5962 $(for o in $SHARED; do echo -n "$OBJ/$o.o "; done) \
6063 $(ls $OBJ/udx_*.o | grep -v "/udx_transport.o$") \
64 $(ls $OBJ/compact_*.o) \
6165 $LDFLAGS
6266echo " OK: session-daemon"
src/compact_encoding.h +84 -146
……@@ -2,7 +2,27 @@
22#define HYPERSWARM_COMPACT_ENCODING_H
33
44// Compact encoding codec — wire-compatible with holepunchto/compact-encoding.
5// Header-only. All multi-byte integers are little-endian.
5// All multi-byte integers are little-endian.
6//
7// ⛔ THE BYTES ARE NOT WRITTEN HERE ANY MORE. Every primitive below forwards to
8// libcompact, which is holepunch's own C port of compact-encoding and is what
9// swarmd's generated wire already links against. Two codecs for one format in
10// one repo is one codec too many: the unannounce signable in
11// docs/PARITY-hyperswarm.md §7 was written twice and diverged, and that is the
12// same failure at a level up.
13//
14// What remains here is what libcompact does NOT have and hyperswarm DOES: the
15// ipv4Address pair from compact-encoding-net, the BLAKE2b node id from
16// dht-rpc/lib/peer.js, and the Kademlia XOR helpers.
17//
18// The two contracts this wrapper keeps, and libcompact does not state:
19//
20// * decoders return a VALUE, not an error code, because every call site here
21// is written that way
22// * a truncated decode yields 0 / null AND leaves start == end, so a loop
23// over a malformed frame terminates. libcompact returns -1 and leaves
24// `start` wherever it stopped; a caller looping `while (start < end)` on
25// that would spin. test/test_compact_encoding.cpp pins all of it.
626//
727// Reference: holepunchto/compact-encoding/index.js
828// holepunchto/compact-encoding-net/index.js
……@@ -14,13 +34,15 @@
1434
1535#include <sodium.h>
1636
37extern "C" {
38#include <compact.h>
39}
40
1741// ---- Data structures ----
1842
19struct EncodeState {
20 size_t start;
21 size_t end;
22 uint8_t *buffer; // May be null during preencode pass.
23};
43// Layout-identical to what it replaced: { start, end, buffer }. Aggregate
44// initialisation at the call sites is unchanged.
45typedef compact_state_t EncodeState;
2446
2547struct IPv4Address {
2648 uint8_t ip[4]; // Network byte order (big-endian octets).
……@@ -42,210 +64,126 @@ namespace cenc {
4264
4365// ---- Variable-width unsigned integer (varint) ----
4466
45inline void uint_preencode(EncodeState &s, uint64_t n) {
46 if (n <= 0xfc) {
47 s.end += 1;
48 } else if (n <= 0xffff) {
49 s.end += 3;
50 } else if (n <= 0xffffffff) {
51 s.end += 5;
52 } else {
53 s.end += 9;
54 }
55}
67// A decode that ran off the end. libcompact tells us with -1; our callers read
68// a value, so give them the harmless one and make sure the frame is finished.
69inline void _truncated(EncodeState &s) { s.start = s.end; }
5670
57inline void uint_encode(EncodeState &s, uint64_t n) {
58 if (n <= 0xfc) {
59 s.buffer[s.start++] = static_cast<uint8_t>(n);
60 } else if (n <= 0xffff) {
61 s.buffer[s.start++] = 0xfd;
62 s.buffer[s.start++] = static_cast<uint8_t>(n & 0xff);
63 s.buffer[s.start++] = static_cast<uint8_t>((n >> 8) & 0xff);
64 } else if (n <= 0xffffffff) {
65 s.buffer[s.start++] = 0xfe;
66 s.buffer[s.start++] = static_cast<uint8_t>(n & 0xff);
67 s.buffer[s.start++] = static_cast<uint8_t>((n >> 8) & 0xff);
68 s.buffer[s.start++] = static_cast<uint8_t>((n >> 16) & 0xff);
69 s.buffer[s.start++] = static_cast<uint8_t>((n >> 24) & 0xff);
70 } else {
71 s.buffer[s.start++] = 0xff;
72 uint32_t lo = static_cast<uint32_t>(n & 0xffffffff);
73 uint32_t hi = static_cast<uint32_t>(n >> 32);
74 s.buffer[s.start++] = static_cast<uint8_t>(lo & 0xff);
75 s.buffer[s.start++] = static_cast<uint8_t>((lo >> 8) & 0xff);
76 s.buffer[s.start++] = static_cast<uint8_t>((lo >> 16) & 0xff);
77 s.buffer[s.start++] = static_cast<uint8_t>((lo >> 24) & 0xff);
78 s.buffer[s.start++] = static_cast<uint8_t>(hi & 0xff);
79 s.buffer[s.start++] = static_cast<uint8_t>((hi >> 8) & 0xff);
80 s.buffer[s.start++] = static_cast<uint8_t>((hi >> 16) & 0xff);
81 s.buffer[s.start++] = static_cast<uint8_t>((hi >> 24) & 0xff);
82 }
83}
71inline void uint_preencode(EncodeState &s, uint64_t n) { compact_preencode_uint(&s, n); }
72
73inline void uint_encode(EncodeState &s, uint64_t n) { compact_encode_uint(&s, n); }
8474
8575inline uint64_t uint_decode(EncodeState &s) {
86 if (s.start >= s.end) return 0;
87 uint8_t a = s.buffer[s.start++];
88 if (a <= 0xfc) return a;
89 if (a == 0xfd) {
90 if (s.start + 2 > s.end) { s.start = s.end; return 0; }
91 uint16_t v = static_cast<uint16_t>(s.buffer[s.start])
92 | (static_cast<uint16_t>(s.buffer[s.start + 1]) << 8);
93 s.start += 2;
94 return v;
95 }
96 if (a == 0xfe) {
97 if (s.start + 4 > s.end) { s.start = s.end; return 0; }
98 uint32_t v = static_cast<uint32_t>(s.buffer[s.start])
99 | (static_cast<uint32_t>(s.buffer[s.start + 1]) << 8)
100 | (static_cast<uint32_t>(s.buffer[s.start + 2]) << 16)
101 | (static_cast<uint32_t>(s.buffer[s.start + 3]) << 24);
102 s.start += 4;
103 return v;
104 }
105 // 0xff: uint64
106 if (s.start + 8 > s.end) { s.start = s.end; return 0; }
107 uint32_t lo = static_cast<uint32_t>(s.buffer[s.start])
108 | (static_cast<uint32_t>(s.buffer[s.start + 1]) << 8)
109 | (static_cast<uint32_t>(s.buffer[s.start + 2]) << 16)
110 | (static_cast<uint32_t>(s.buffer[s.start + 3]) << 24);
111 uint32_t hi = static_cast<uint32_t>(s.buffer[s.start + 4])
112 | (static_cast<uint32_t>(s.buffer[s.start + 5]) << 8)
113 | (static_cast<uint32_t>(s.buffer[s.start + 6]) << 16)
114 | (static_cast<uint32_t>(s.buffer[s.start + 7]) << 24);
115 s.start += 8;
116 return static_cast<uint64_t>(lo) | (static_cast<uint64_t>(hi) << 32);
76 uint64_t v = 0;
77 if (compact_decode_uint(&s, &v) < 0) { _truncated(s); return 0; }
78 return v;
11779}
11880
11981// ---- Fixed-width little-endian integers ----
82//
83// The preencode helpers take no value because these widths do not depend on
84// one; libcompact's take one and ignore it for exactly the same reason.
12085
121inline void uint8_encode(EncodeState &s, uint8_t n) {
122 s.buffer[s.start++] = n;
123}
86inline void uint8_encode(EncodeState &s, uint8_t n) { compact_encode_uint8(&s, n); }
12487
12588inline uint8_t uint8_decode(EncodeState &s) {
126 if (s.start >= s.end) return 0;
127 return s.buffer[s.start++];
89 uint8_t v = 0;
90 if (compact_decode_uint8(&s, &v) < 0) { _truncated(s); return 0; }
91 return v;
12892}
12993
130inline void uint16_preencode(EncodeState &s) {
131 s.end += 2;
132}
94inline void uint16_preencode(EncodeState &s) { compact_preencode_uint16(&s, 0); }
13395
134inline void uint16_encode(EncodeState &s, uint16_t n) {
135 s.buffer[s.start++] = static_cast<uint8_t>(n & 0xff);
136 s.buffer[s.start++] = static_cast<uint8_t>((n >> 8) & 0xff);
137}
96inline void uint16_encode(EncodeState &s, uint16_t n) { compact_encode_uint16(&s, n); }
13897
13998inline uint16_t uint16_decode(EncodeState &s) {
140 if (s.start + 2 > s.end) { s.start = s.end; return 0; }
141 uint16_t v = static_cast<uint16_t>(s.buffer[s.start])
142 | (static_cast<uint16_t>(s.buffer[s.start + 1]) << 8);
143 s.start += 2;
99 uint16_t v = 0;
100 if (compact_decode_uint16(&s, &v) < 0) { _truncated(s); return 0; }
144101 return v;
145102}
146103
147inline void uint32_preencode(EncodeState &s) {
148 s.end += 4;
149}
104inline void uint32_preencode(EncodeState &s) { compact_preencode_uint32(&s, 0); }
150105
151inline void uint32_encode(EncodeState &s, uint32_t n) {
152 s.buffer[s.start++] = static_cast<uint8_t>(n & 0xff);
153 s.buffer[s.start++] = static_cast<uint8_t>((n >> 8) & 0xff);
154 s.buffer[s.start++] = static_cast<uint8_t>((n >> 16) & 0xff);
155 s.buffer[s.start++] = static_cast<uint8_t>((n >> 24) & 0xff);
156}
106inline void uint32_encode(EncodeState &s, uint32_t n) { compact_encode_uint32(&s, n); }
157107
158108inline uint32_t uint32_decode(EncodeState &s) {
159 if (s.start + 4 > s.end) { s.start = s.end; return 0; }
160 uint32_t v = static_cast<uint32_t>(s.buffer[s.start])
161 | (static_cast<uint32_t>(s.buffer[s.start + 1]) << 8)
162 | (static_cast<uint32_t>(s.buffer[s.start + 2]) << 16)
163 | (static_cast<uint32_t>(s.buffer[s.start + 3]) << 24);
164 s.start += 4;
109 uint32_t v = 0;
110 if (compact_decode_uint32(&s, &v) < 0) { _truncated(s); return 0; }
165111 return v;
166112}
167113
168inline void uint64_preencode(EncodeState &s) {
169 s.end += 8;
170}
114inline void uint64_preencode(EncodeState &s) { compact_preencode_uint64(&s, 0); }
171115
172inline void uint64_encode(EncodeState &s, uint64_t n) {
173 uint32_t lo = static_cast<uint32_t>(n & 0xffffffff);
174 uint32_t hi = static_cast<uint32_t>(n >> 32);
175 uint32_encode(s, lo);
176 uint32_encode(s, hi);
177}
116inline void uint64_encode(EncodeState &s, uint64_t n) { compact_encode_uint64(&s, n); }
178117
118// ⛔ ONE BYTE-LEVEL BEHAVIOUR CHANGED HERE, deliberately. This used to be two
119// uint32 decodes, so four remaining bytes yielded the LOW WORD of a number that
120// was never on the wire. It is one decode now and a short read yields 0, which
121// is the contract the other decoders already kept.
179122inline uint64_t uint64_decode(EncodeState &s) {
180 uint32_t lo = uint32_decode(s);
181 uint32_t hi = uint32_decode(s);
182 return static_cast<uint64_t>(lo) | (static_cast<uint64_t>(hi) << 32);
123 uint64_t v = 0;
124 if (compact_decode_uint64(&s, &v) < 0) { _truncated(s); return 0; }
125 return v;
183126}
184127
185128// ---- Fixed-size raw buffers (no length prefix) ----
129//
130// These decode ZERO-COPY — the pointer is into the caller's frame, which is
131// what every call site expects. libcompact's fixed32/64 decoders copy into a
132// destination, so the pointer is taken here and libcompact is asked to do the
133// bounds check and the advance with a null destination.
186134
187inline void fixed32_preencode(EncodeState &s) {
188 s.end += 32;
189}
135inline void fixed32_preencode(EncodeState &s) { compact_preencode_fixed32(&s, nullptr); }
190136
191137inline void fixed32_encode(EncodeState &s, const uint8_t *data) {
192 memcpy(s.buffer + s.start, data, 32);
193 s.start += 32;
138 compact_encode_fixed32(&s, data);
194139}
195140
196141inline const uint8_t *fixed32_decode(EncodeState &s) {
197 if (s.start + 32 > s.end) { s.start = s.end; return nullptr; }
198142 const uint8_t *ptr = s.buffer + s.start;
199 s.start += 32;
143 if (compact_decode_fixed32(&s, nullptr) < 0) { _truncated(s); return nullptr; }
200144 return ptr;
201145}
202146
203inline void fixed64_preencode(EncodeState &s) {
204 s.end += 64;
205}
147inline void fixed64_preencode(EncodeState &s) { compact_preencode_fixed64(&s, nullptr); }
206148
207149inline void fixed64_encode(EncodeState &s, const uint8_t *data) {
208 memcpy(s.buffer + s.start, data, 64);
209 s.start += 64;
150 compact_encode_fixed64(&s, data);
210151}
211152
212153inline const uint8_t *fixed64_decode(EncodeState &s) {
213 if (s.start + 64 > s.end) { s.start = s.end; return nullptr; }
214154 const uint8_t *ptr = s.buffer + s.start;
215 s.start += 64;
155 if (compact_decode_fixed64(&s, nullptr) < 0) { _truncated(s); return nullptr; }
216156 return ptr;
217157}
218158
219159// ---- Length-prefixed buffer ----
160//
161// compact-encoding's `buffer` is uint8array with one extra rule this codebase
162// relies on: a null buffer is a single 0x00, and it decodes back to null rather
163// than to a zero-length view. libcompact has no null, so that rule stays here.
220164
221165inline void buffer_preencode(EncodeState &s, const uint8_t *data, size_t len) {
222166 if (data == nullptr) {
223 s.end += 1; // null encoded as single 0x00 byte
167 s.end += 1;
224168 } else {
225 uint_preencode(s, len);
226 s.end += len;
169 compact_preencode_uint8array(&s, data, len);
227170 }
228171}
229172
230173inline void buffer_encode(EncodeState &s, const uint8_t *data, size_t len) {
231174 if (data == nullptr) {
232 s.buffer[s.start++] = 0;
175 compact_encode_uint8(&s, 0);
233176 } else {
234 uint_encode(s, len);
235 if (len > 0) {
236 memcpy(s.buffer + s.start, data, len);
237 s.start += len;
238 }
177 compact_encode_uint8array(&s, data, len);
239178 }
240179}
241180
242181inline BufView buffer_decode(EncodeState &s) {
243 uint64_t len = uint_decode(s);
182 uint8_t *ptr = nullptr;
183 size_t len = 0;
184 if (compact_decode_uint8array(&s, &ptr, &len) < 0) { _truncated(s); return { nullptr, 0 }; }
244185 if (len == 0) return { nullptr, 0 };
245 if (s.start + len > s.end) { s.start = s.end; return { nullptr, 0 }; }
246 const uint8_t *ptr = s.buffer + s.start;
247 s.start += static_cast<size_t>(len);
248 return { ptr, static_cast<size_t>(len) };
186 return { ptr, len };
249187}
250188
251189// ---- IPv4 Address (6 bytes: 4 IP octets + 2 port LE) ----
test/build_tests.sh +22 -3
……@@ -8,9 +8,14 @@ cd "$(dirname "$0")"
88SRCDIR=../src
99UDX_INC=../thirdparty/libudx/include
1010UDX_SRC=../thirdparty/libudx/src
11
12CFLAGS="-std=c11 -D_GNU_SOURCE -I$SRCDIR -I$UDX_INC -c"
13CXXFLAGS="-std=c++17 -D_GNU_SOURCE -I$SRCDIR -I$UDX_INC -c"
11# src/compact_encoding.h forwards to libcompact, holepunch's own C port of
12# compact-encoding, so every binary that touches the wire links it. Symlinked
13# into thirdparty/ the same way libudx is.
14COMPACT_SRC=../thirdparty/libcompact/src
15COMPACT_INC="-I../thirdparty/libcompact/include -I../thirdparty/libutf/include"
16
17CFLAGS="-std=c11 -D_GNU_SOURCE -I$SRCDIR -I$UDX_INC $COMPACT_INC -c"
18CXXFLAGS="-std=c++17 -D_GNU_SOURCE -I$SRCDIR -I$UDX_INC $COMPACT_INC -c"
1419LDFLAGS="-luv -lsodium -lpthread"
1520
1621# ---- Optional sanitizer build: SAN=address ./build_tests.sh ----------------
……@@ -32,6 +37,20 @@ if [ -n "${SAN:-}" ]; then
3237 LDFLAGS="-fsanitize=$SAN $LDFLAGS"
3338fi
3439
40# Build libcompact objects (shared across all tests: every wire path uses them).
41COMPACT_OBJS=""
42for f in "$COMPACT_SRC"/*.c; do
43 b=$(basename "$f" .c)
44 obj="$OBJ/compact_$b.o"
45 if [ ! -f "$obj" ] || [ "$f" -nt "$obj" ]; then
46 gcc -std=c99 -O2 -D_GNU_SOURCE $COMPACT_INC -c -o "$obj" "$f"
47 fi
48 COMPACT_OBJS="$COMPACT_OBJS $obj"
49done
50LDFLAGS="$COMPACT_OBJS $LDFLAGS"
51
52echo "=== libcompact objects built ==="
53
3554# Build libudx object files (shared across all tests).
3655UDX_OBJS=""
3756for f in udx.c udx_bbr.c udx_rate.c cirbuf.c endian.c io_posix.c queue.c win_filter.c win_filter_f64.c; do
tools/build_swarm_probe.sh +3 -1
……@@ -9,7 +9,8 @@ cd "$(dirname "$0")"
99
1010SRCDIR=../src
1111UDX_INC=../thirdparty/libudx/include
12CXXFLAGS="-std=c++17 -O2 -D_GNU_SOURCE -I. -I$SRCDIR -I$UDX_INC"
12COMPACT_INC="-I../thirdparty/libcompact/include -I../thirdparty/libutf/include"
13CXXFLAGS="-std=c++17 -O2 -D_GNU_SOURCE -I. -I$SRCDIR -I$UDX_INC $COMPACT_INC"
1314LDFLAGS="-luv -lsodium -lpthread"
1415
1516OBJ=/tmp/hsp-obj
……@@ -44,5 +45,6 @@ g++ -o swarm-probe \
4445 $OBJ/swarm_probe.o \
4546 $(for o in $SHARED; do echo -n "$OBJ/$o.o "; done) \
4647 $(ls $OBJ/udx_*.o | grep -v "/udx_transport.o$") \
48 $(ls $OBJ/compact_*.o) \
4749 $LDFLAGS
4850echo " OK: swarm-probe"