pay: domain-separate tip and make legacy signature acceptance one switch #193

Merged
sirius merged 2 commits from onionwire/t_438331aa-close-the-legacy-signature-surface-and-d into main 2026-10-24 22:11:54 +00:00
Owner

tip was the one payment frame with no domain tag. This PR gives it one, and turns the scattered v1 fallbacks into a single switch.

Code

  • tip v2. sign_tip now signs "onionwire-tip-v1" || 0x00 || txid || 0x00 || amount_atomic || 0x00 || address || 0x00 || memo || 0x00 || ascii(ts), mirroring inv/rcp (the tag keeps the historical -v1 suffix; the function follows the existing *_v2 naming). verify_tip tries v2, then the LF layout. encode_tip/decode_tip are unchanged — only the signed bytes move.
  • One switch. pay::LEGACY_SIGS: bool = true plus pay::legacy_sigs(). Every listed fallback reads it: the onionwire:v1 invite string (both historical forms), loc, inv, rcp, and tip. With it off, only the tagged form verifies. It is not a wire field and is not negotiable.
  • prf is not gated here. src/profile.rs is outside this card's file scope, so its legacy fallback stays unconditional; both the code doc and the spec say so explicitly rather than implying the switch is total.

Tests

Legacy on (default): legacy_untagged_loc_still_verifies, legacy_untagged_prf_still_verifies, legacy_untagged_inv_still_verifies, legacy_untagged_rcp_still_verifies, new legacy_untagged_tip_still_verifies; invite side keeps well_formed_v1_concat_still_decodes (in src/qr.rs) and invite_v1_still_decodes_to_a_classical_friend_row green.

Legacy off (pay::with_legacy_sigs(false, ...), a scoped thread-local test hook — restored by RAII, no cross-test state):

  • legacy_off_rejects_untagged_loc
  • legacy_off_rejects_untagged_inv
  • legacy_off_rejects_untagged_rcp
  • legacy_off_rejects_untagged_tip
  • legacy_off_rejects_v1_invite_but_v2_still_decodes

Each asserts both halves: the untagged form is rejected and the tagged form still verifies; the loc/inv/rcp/tip tests also assert acceptance returns after the scope exits.

Tip round-trip: tip_frame_roundtrip_and_wrong_key (emit → encode → decode → verify) stays green, and new tip_emits_tagged_form_only pins that sign_tip's signature does not verify over the LF-joined layout (so encode is v2-only) and that the memo is covered.

Verification (this worktree, host target)

cargo test --locked --target x86_64-unknown-linux-gnu   -> rc=0, 38 test binaries, lib 161 passed / 0 failed
cargo clippy --locked --all-targets --target x86_64-unknown-linux-gnu -- -D warnings -> rc=0

One earlier run failed wallet_child::tests::a_start_that_fails_after_the_daemon_came_up_leaves_nothing_running with Address already in use (os error 98): another kanban worker's suite held 18080 at the same time. On a re-run with the port free it passes; it is fixed-port test contention, not this diff.

Spec

docs/PROTOCOL.md: the intro now names tip among tagged frames, adds the LEGACY_SIGS paragraph, the per-frame emitted / accepted inbound table, and the mixed-fleet consequence (an old peer cannot verify a new tagged tip until it updates; the reverse direction works while the switch is on; turning it off is a fleet-wide cutover). The per-frame Legacy accept: sentences and the tip signature block are updated in the same diff — leaving them would contradict the table. No other doc sections touched; docs/THREAT_MODEL.md untouched.

Notes

  • No assert_eq! on wire strings was weakened; the tip prefix and LF frame layout are unchanged, only the signed preimage.
  • Files: src/pay.rs, src/loc.rs, src/qr.rs, tests/app_sig.rs, tests/qr.rs, docs/PROTOCOL.md.
`tip` was the one payment frame with no domain tag. This PR gives it one, and turns the scattered v1 fallbacks into a single switch. ## Code - **`tip ` v2.** `sign_tip` now signs `"onionwire-tip-v1" || 0x00 || txid || 0x00 || amount_atomic || 0x00 || address || 0x00 || memo || 0x00 || ascii(ts)`, mirroring `inv`/`rcp` (the tag keeps the historical `-v1` suffix; the function follows the existing `*_v2` naming). `verify_tip` tries v2, then the LF layout. `encode_tip`/`decode_tip` are unchanged — only the signed bytes move. - **One switch.** `pay::LEGACY_SIGS: bool = true` plus `pay::legacy_sigs()`. Every listed fallback reads it: the `onionwire:v1` invite string (both historical forms), `loc`, `inv`, `rcp`, and `tip`. With it off, only the tagged form verifies. It is not a wire field and is not negotiable. - **`prf` is not gated here.** `src/profile.rs` is outside this card's file scope, so its legacy fallback stays unconditional; both the code doc and the spec say so explicitly rather than implying the switch is total. ## Tests Legacy **on** (default): `legacy_untagged_loc_still_verifies`, `legacy_untagged_prf_still_verifies`, `legacy_untagged_inv_still_verifies`, `legacy_untagged_rcp_still_verifies`, **new** `legacy_untagged_tip_still_verifies`; invite side keeps `well_formed_v1_concat_still_decodes` (in `src/qr.rs`) and `invite_v1_still_decodes_to_a_classical_friend_row` green. Legacy **off** (`pay::with_legacy_sigs(false, ...)`, a scoped thread-local test hook — restored by RAII, no cross-test state): - `legacy_off_rejects_untagged_loc` - `legacy_off_rejects_untagged_inv` - `legacy_off_rejects_untagged_rcp` - `legacy_off_rejects_untagged_tip` - `legacy_off_rejects_v1_invite_but_v2_still_decodes` Each asserts both halves: the untagged form is rejected **and** the tagged form still verifies; the `loc`/`inv`/`rcp`/`tip` tests also assert acceptance returns after the scope exits. Tip round-trip: `tip_frame_roundtrip_and_wrong_key` (emit → encode → decode → verify) stays green, and new `tip_emits_tagged_form_only` pins that `sign_tip`'s signature does **not** verify over the LF-joined layout (so encode is v2-only) and that the memo is covered. ## Verification (this worktree, host target) ``` cargo test --locked --target x86_64-unknown-linux-gnu -> rc=0, 38 test binaries, lib 161 passed / 0 failed cargo clippy --locked --all-targets --target x86_64-unknown-linux-gnu -- -D warnings -> rc=0 ``` One earlier run failed `wallet_child::tests::a_start_that_fails_after_the_daemon_came_up_leaves_nothing_running` with `Address already in use (os error 98)`: another kanban worker's suite held 18080 at the same time. On a re-run with the port free it passes; it is fixed-port test contention, not this diff. ## Spec `docs/PROTOCOL.md`: the intro now names `tip` among tagged frames, adds the `LEGACY_SIGS` paragraph, the per-frame **emitted / accepted inbound** table, and the mixed-fleet consequence (an old peer cannot verify a new tagged `tip` until it updates; the reverse direction works while the switch is on; turning it off is a fleet-wide cutover). The per-frame `Legacy accept:` sentences and the `tip` signature block are updated in the same diff — leaving them would contradict the table. No other doc sections touched; `docs/THREAT_MODEL.md` untouched. ## Notes - No `assert_eq!` on wire strings was weakened; the `tip ` prefix and LF frame layout are unchanged, only the signed preimage. - Files: `src/pay.rs`, `src/loc.rs`, `src/qr.rs`, `tests/app_sig.rs`, `tests/qr.rs`, `docs/PROTOCOL.md`.
pay: domain-separate tip and gate legacy signature acceptance
All checks were successful
ci / test (pull_request) Successful in 19m52s
ci / sdk (pull_request) Successful in 2m16s
b8c52ca97d
`tip` was the one payment frame with no domain tag: sign_tip signed the
LF-joined fields, so its signature could be confused with any other
newline-joined preimage. Mirror inv/rcp: sign_tip now emits
"onionwire-tip-v1" || 0x00-separated txid/amount/address/memo/ts, and
verify_tip tries that form then the historical layout.

Legacy acceptance is now one switch instead of nine call sites:
`pay::LEGACY_SIGS` (default true) plus `pay::legacy_sigs()`. Every v1 /
concat fallback reads it — the `onionwire:v1` invite string and its two
historical signature forms, `loc`, `inv`, `rcp`, and `tip`. `prf` is not
gated here because src/profile.rs is outside this card's file scope; the
spec says so.

Tests: legacy-on keeps every untagged form verifying (incl. a new tip
case); legacy-off rejects each untagged form while the tagged form still
verifies (loc, inv, rcp, tip, invite); sign_tip is pinned to the tagged
layout.

docs/PROTOCOL.md gains the emitted/accepted table per signed frame and
the mixed-fleet consequence.
Merge branch 'main' into onionwire/t_438331aa-close-the-legacy-signature-surface-and-d
Some checks failed
ci / sdk (pull_request) Has been cancelled
ci / test (pull_request) Has been cancelled
android-ci / android (pull_request) Has been cancelled
d3f1a44ef6
sirius merged commit 713dbd9006 into main 2026-10-24 22:11:54 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
sirius/onionwire!193
No description provided.