Skip to content

Add v2 roundtrip fuzzer - #1887

Open
caarloshenriq wants to merge 1 commit into
payjoin:masterfrom
caarloshenriq:roundtrip_v2
Open

caarloshenriq wants to merge 1 commit into
payjoin:masterfrom
caarloshenriq:roundtrip_v2

Conversation

@caarloshenriq

Copy link
Copy Markdown
Contributor

Adds a libFuzzer target covering the full v2 payjoin roundtrip: sender posts an encrypted message to the receiver's mailbox, the receiver walks the typestate chain and posts a proposal back, the sender decrypts and validates it. Unlike v1, every message the state machine sees is wrapped in two layers of crypto (HPKE messages A/B, OHTTP request/response), so random mutation alone cannot pass an AEAD tag and never reaches the protocol logic.

The harness was built together with @benalleng, who did the initial implementation; I reviewed it and polished a few details (test correctness, comments on two harness-side invariants).

Design

Both crypto layers are constructed harness-side so the fuzzer only ever mutates plaintext:

  • The OHTTP key config is derived once per process from a fixed seed and shared by the receiver session, the harness sender, and an in-process mock directory gateway. LazyLock statics make this setup cost vanish across the millions of execs in one fuzz process.
  • The receiver's HPKE secret key never leaves the receiver; the harness sender reads the public key from the session's own pj_uri and encrypts the fuzz-controlled plaintext (base64(psbt)\nquery) to it.
  • The mock directory is a pure function from request bytes to response bytes: decapsulate, route by method and mailbox path, store-and-forward, encapsulate. No network, no threads.

As in roundtrip_v1, a custom mutator keeps a known-good original PSBT (the BIP-78 test vector) intact and spends the mutation budget on the query, where the BIP-77 parameters live. 25% of mutations pass straight to fuzzer_mutate.

Reproducibility

The random values (reply keypair, HPKE and OHTTP ephemerals) only key the transport layer. Downstream execution is a function of the plaintext alone, so a crash replays under fresh keys: the harness re-encrypts the same input and the state machine walks the same path. The seed_psbt_completes_the_roundtrip test guards this property.

What is covered

The full chain on both sides: ReceiverBuilder::build, create_poll_request/process_response, assume_interactive_receiver, check_inputs_not_owned, check_no_inputs_seen_before, identify_receiver_outputs, commit_outputs, commit_inputs, apply_fee_range, finalize_proposal, create_post_request, PayjoinProposal::process_response, plus the sender-side HPKE encrypt/decrypt and the v1 sender context used to validate the returned proposal.

Fuzzing results

From an empty corpus on x86_64 Linux, 6 forks, 61 seconds:

Metric Value
Coverage counters reached 7012
Feature edges 12715
Corpus size 418 inputs
Crashes / OOM / Timeouts 0 / 0 / 0

Coverage was still growing when the run ended. Throughput is lower than the other targets (~300-500 exec/s) since each execution runs two full HPKE handshakes and two OHTTP round trips end to end.

Addresses part of #1267.

Disclosure: co-authored by Claude

Pull Request Checklist

Please confirm the following before requesting review:

Co-authored-by: Carlos Santos <rickhenriquetab@gmail.com>
@coveralls

Copy link
Copy Markdown
Collaborator

Coverage Report for CI Build 35135586389

Coverage remained the same at 86.709%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 16710
Covered Lines: 14489
Line Coverage: 86.71%
Coverage Strength: 338.51 hits per line

💛 - Coveralls

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants