# M1 — payload codec

**Status: complete.** 149 tests passing (48 new payload, 27 encoding, 56 reference vectors, plus M0's 18).

## What was built

| 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 |

## We found an oracle after all

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:

- **4 obfuscation vectors** from `obfuscation_tests.cpp` — **all match**
- **26 payload vectors** from `create_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.

## Details that would have been easy to get wrong

1. **Obfuscation hashes the uppercase hex STRING, not the digest bytes.**
   `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.
2. **In type 50, `amount` comes AFTER the five strings**, not before
   (`createpayload.cpp:CreatePayload_IssuanceFixed`). Pinned by its own test.
3. **Type 3's version is derived from the data.** If `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.
4. **Types 185/186 carry 21 raw address bytes**, base58-decoded with the checksum
   stripped (`createpayload.cpp:31-45`) — not base58 text, not NUL-terminated.
5. **Type 200's data has no terminator** and is byte-transparent, which is exactly
   what makes it usable for file chunks.
6. **Class B keys must be real curve points.** A fabricated key is
   `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.

## Two bugs my own tests caught

**`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.

## ⚠️ A constraint that M5 must design around

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.

## Capacity, now verified by test rather than arithmetic

| 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.

## Carried into M2

- Sender determination is not implemented. Class B decoding **needs** the sender
  address as its obfuscation seed, so M2 must implement Omni's sender rules
  (first-in vs by-contribution) before real transactions can be decoded.
- Nothing yet extracts multisig outputs from a real transaction — that is the
  bridge between `ChainFollower` and this codec, and it lands in M2.
