Skip to content

Latest commit

 

History

History
170 lines (133 loc) · 7.33 KB

File metadata and controls

170 lines (133 loc) · 7.33 KB

Interop tests against the Python Reticulum reference

This directory holds tests that validate the C++ port's behaviour against the Python Reticulum reference implementation. Two flavours, in increasing scope:

Targets

Four C++ sender executables, one per interop scenario. Each is a CMake target of the same name and is configured at compile time with its own UDP port pair so the scenarios can coexist on a single host:

CMake target UDP local UDP remote Scenario
packet_interop_sender 14253 14252 DATA packets to a Destination, bidirectional
link_interop_sender 14255 14254 Link establish + two-pass teardown
request_interop_sender 14257 14256 Link::request("/echo", ...) single-packet RPC
resource_interop_sender 14243 14242 Full Resource transfer over a Link (1024-byte payload)

CMake

The four C++ interop senders are built by the top-level CMake project. Configure and build from the repository root:

Configure:

cmake -S . -B build

Build all interop senders:

cmake --build build -j --target packet_interop_sender link_interop_sender request_interop_sender resource_interop_sender

Build a single sender:

cmake --build build -j --target packet_interop_sender

After building, point the existing driver scripts at the binaries in build/test_interop/ (the scripts default to the PlatformIO output path — adjust PIO_BUILD_DIR or the binary path inside the script as needed):

bash test_interop/run_all.sh
bash test_interop/run_packet_exchange.sh

Skip building interop senders entirely when configuring the parent project:

cmake -S . -B build -DRNS_BUILD_INTEROP=OFF

Top-level driver

test_interop/run_all.sh builds each C++ test app and runs all four interop scenarios sequentially, reporting per-test PASS/FAIL and an aggregated exit status. Run from the repo root:

bash test_interop/run_all.sh

Each scenario also has its own driver script if you only want to run one:

Test Driver script What it covers
resource test_interop/run_resource_transfer.sh Full Resource transfer over a Link (1024-byte payload)
packet test_interop/run_packet_exchange.sh DATA packets to a Destination, bidirectional, 256-byte patterns
link test_interop/run_link_lifecycle.sh Link establish + two-pass teardown (initiator and destination)
request test_interop/run_request_response.sh Link::request("/echo", ...) single-packet RPC (200-byte payload)

All four currently PASS against Python RNS 1.2.9 at /Users/chad/python/Reticulum-latest.

1. Wire-format cross-validation (implemented)

The C++ unit test test/test_resource_advertisement includes a PYTHON_GOLDEN_HEX byte sequence captured from Python 1.2.9 RNS.Resource.ResourceAdvertisement.pack() for a known set of input fields. The test:

  • Unpacks Python's bytes via C++ ResourceAdvertisement::unpack and asserts every field round-trips correctly.
  • Packs the same input fields via C++ ResourceAdvertisement::pack, unpacks via C++ again, and compares byte-for-byte against the Python golden. If the bytes match exactly we have byte-equal wire interop; if they only match by field-equivalence we have spec-compliant but not byte-equal interop (msgpack permits multiple valid integer encodings of the same value).

encode_advertisement.py regenerates the golden bytes from Python so the constants in the C++ test can be updated whenever the Python reference's wire format changes.

2. Full Resource transfer over UDP — C++ initiator -> Python receiver

A driver that:

  1. Spawns python_resource_receiver.py on UDP port 14242 (avoids the common RNS desktop-app conflict on 4242).
  2. Spawns the C++ sender binary built from test_interop/resource_interop_sender/ on UDP port 14243, configured to send to 127.0.0.1:14242.
  3. Watches both stdouts and exits 0 iff both report SUCCESS.

Wire path on each side:

  • Python: listens on 127.0.0.1:14242, sends to 127.0.0.1:14243.
  • C++: listens on 127.0.0.1:14243, sends to 127.0.0.1:14242.

Point-to-point loopback is used instead of 255.255.255.255 broadcast because macOS doesn't reliably loop limited-broadcast traffic between two processes on the same host. The example's platformio.ini sets DEFAULT_UDP_LOCAL_HOST, DEFAULT_UDP_REMOTE_HOST, DEFAULT_UDP_LOCAL_PORT and DEFAULT_UDP_REMOTE_PORT build flags; examples/common/udp_interface/UDPInterface.h has been extended with matching #ifndef guards so these are overridable per-example without touching the library code.

Architectural caveats:

  • microReticulum uses a global static RNS::Transport. A two-instance C++ loopback inside the same process is not possible — this is why the test is structured as two OS processes communicating over UDP.

Running

# Build the C++ sender once
cd test_interop/resource_interop_sender
pio run -e native17
cd ../..

# Run the interop driver
bash test_interop/run_resource_transfer.sh

Expected last line: [driver] PASS with both processes exiting 0.

Bugs found and fixed during live interop

The interop test exposed several pre-existing bugs as well as ones in the new Resource implementation:

  • Link::ready_for_new_resource() — boolean returned the inverse of what it should (true when an outgoing resource already existed, blocking new ones). Fixed in src/Link.cpp.
  • Link::link_closed() — iterated _incoming_resources / _outgoing_resources while Resource::cancel() erases from those same sets, invalidating the iterator and crashing. Now snapshots first.
  • Link::receive() PROOF / RESOURCE_PRF dispatch — same iterator invalidation pattern when validate_proof() calls resource_concluded() which erases. Now snapshots first.
  • Resource hash & expected-proof input order — Python computes full_hash(data || random_hash) and full_hash(data || hash); the C++ port was computing full_hash(random_hash || data || ...). Receiver computed the right thing locally but the advertised hash didn't match, failing the integrity check. Fixed in src/Resource.cpp.
  • UDPInterface.h macrosDEFAULT_UDP_PORT etc. lacked #ifndef guards, so build-flag overrides were silently ignored.

Additional pre-existing bugs uncovered by the broader interop suite (test_interop/run_all.sh):

  • RequestReceiptData::_link declared as bare Link _link; instead of Link _link = {Type::NONE};. The default Link constructor with no destination and no owner generates _prv but leaves _sig_prv null, then dereferences _sig_prv->public_key() → SIGSEGV. Every call to Link::request() constructs a RequestReceipt containing a RequestReceiptData, so this crashed the RPC path entirely. Fixed in src/LinkData.h.
  • Embedded Link members in RequestReceiptData required by the fix above are now self-consistent across all four interop scenarios.