0..105 are the message the secure element signs.
The layout
Signed message: 105 bytes. Total packet: 202 bytes.
Why 202 is the budget
202 bytes fits an NDEF record for NFC and a QR code at version 10, error-correction level M — 213 bytes of capacity in byte mode, on a 57×57 grid that scans cleanly off a phone screen.
The layout is a contract
The TypeScript in@nelo/voucher and the Rust in programs/nelo_vault must produce byte-identical signed messages, or every signature fails verification.
That is not left to careful reading. packages/voucher/vectors/voucher-v1.json is a frozen set of golden vectors:
- Generated by the Rust side, because the chain is authoritative.
- Asserted by both sides —
tests/vectors.rsandtest/voucher.test.ts. - TypeScript also verifies signatures that Rust produced, so the check covers the crypto and not only the byte layout.
Encoding
remaining_after is a claim, not a fact
This is the field most likely to be misread.
remaining_after is what the payer’s device asserts its balance will be once this voucher settles. It exists so a merchant with no network can sanity-check a voucher against the last vault balance they saw cached.
The program does not verify it. It cannot — it has no idea what other vouchers are in flight. What the program checks is the real constraint: amount <= vault.balance at redemption, and the replay window.
Low-S, and why it bites
P-256 signatures have two valid forms for every signature:(r, s) and (r, n − s). Android returns whichever it computes. The Solana precompile accepts only the low-S form.
The failure is nasty because it is intermittent and silent-looking: roughly half your signatures verify fine on the phone and are rejected on chain with no obvious pattern.