Status: complete. 149 tests passing (48 new payload, 27 encoding, 56 reference vectors, plus M0's 18).
| Module | Purpose |
|---|---|
ribbit/payload.py |
All 26 in-scope message types: encode, decode, validation |
ribbit/encoding.py |
Class B (obfuscated multisig) and Class C (OP_RETURN) containers |
D-002 recorded that Ribbit has no reference implementation to check against. That
is true of state, but not of the wire format. Omni Core ships its own unit
tests with hard-coded expected hex, and those vectors are just as valid for us —
the marker differs (rbit vs omni) but the marker is not part of the payload.
tests/test_payload_vectors.py lifts them verbatim:
obfuscation_tests.cpp — all matchcreate_payload_tests.cpp — all match, in both
directions (encode produces the reference bytes; decoding the reference bytes
and re-encoding is a fixed point)So Ribbit's wire format is now demonstrably byte-identical to the implementation that has been running on Bitcoin since 2013. That is a much stronger position than M1 was expected to end in.
parsing.cpp:108-131: round 1 hashes the address; every later round hashes the
64-character uppercase hex of the previous digest. Using the raw digest
produces plausible garbage rather than an error.amount comes AFTER the five strings, not before
(createpayload.cpp:CreatePayload_IssuanceFixed). Pinned by its own test.distribution_property ==
property_id the version is 0 and the field is omitted from the wire
entirely; otherwise version 1 includes it. The payload is 4 bytes shorter in
the v0 case.createpayload.cpp:31-45) — not base58 text, not NUL-terminated.0x02 || obfuscated(31) || filler(1); only the last byte is free, so we vary
it until x³+7 is a quadratic residue mod p. An off-curve key makes the output
non-standard and unrelayable.encode_class_c allowed an over-sized script. I assumed a flat 2-byte push
overhead, but a direct push costs 1 byte up to 75 and OP_PUSHDATA1 costs 2
beyond that. The flat assumption let a 77-byte payload through, producing an
84-byte scriptPubKey that Pepecoin would refuse to relay (limit 83). Fixed with an
explicit class_c_script_size(), and pinned by a test at the 75/76 boundary.
decode_class_b stripped trailing NULs. Convenient, and wrong. See below.
Class B pads the final packet with NULs to a 30-byte boundary, and Omni does
not strip that padding — packet_size = mdata_count * (PACKET_SIZE - 1)
(omnicore.cpp:1263). So a decoded Class B payload is always a multiple of 30
bytes.
My first implementation stripped the trailing NULs. That is a silent data-corruption bug: a file chunk legitimately ending in NUL bytes would be quietly truncated. Not stripping is also imperfect — a type-200 payload gains up to 29 bytes the sender never wrote — but it matches Omni exactly, and matching is a consensus requirement rather than a preference.
The fix belongs one layer up. Ribbit's v2 inscription header already specifies an explicit content length; the reader uses it to find the true end of the data. Length-delimited message types (everything except 200) are unaffected, because they stop reading at their own final field.
Action for M5: the v2 header's length field is not optional and not merely a convenience — it is load-bearing. Any chunk whose content could end in a NUL is unrecoverable without it.
| Vehicle | Data per transaction | Test |
|---|---|---|
| Class C, type 200 | 72 bytes | test_class_c_capacity_for_anydata_is_72_bytes |
| Class B, type 200 | 7,646 bytes (128 multisig outputs) | test_class_b_anydata_capacity_is_7646_bytes |
The 7.5× advantage of Class B type-200 over the v1 text-field approach is confirmed, not estimated.
ChainFollower and this codec, and it lands in M2.