End-to-end, one scripthash

Here's the complete path a single query takes, stitched together. The happy path has up to three PIR rounds — INDEX, CHUNK, MERKLE — each padded to fixed size. Every round is padded even when there's nothing to return.

1 · TAG & GROUPS tag = splitmix64(…, sh ⊕ tag_seed) · derive 3 PBC candidate groups · pick assigned group 2 · INDEX PIR · K=75 padded 1 real · 74 dummies 3 · XOR responses · match tag in bin found → (start_chunk_id, num_chunks) 4 · CHUNK PIR · K_CHUNK=80 per round (if found) N real · 80-N dummies, repeat rounds as needed 5 · MERKLE sibling PIR · one padded query per level ~log₂(leaves) levels ✓ verified UTXO set for scripthash X — server learned nothing
A single query's trip through the protocol. Grey = padded dummies, orange = real queries, green = verified result.
  1. Tag & groups. The client hashes the scripthash into a short 8-byte tag and derives its 3 candidate PBC groups; a deterministic rule picks one as the assigned group.
  2. INDEX round. The client generates 75 × 2 DPF keys — one real (keyed to the item's 2 in-group cuckoo bin positions) and 74 random dummies — and ships them to the two servers.
  3. Decode. Client XORs the two servers' responses per group, scans the returned bin for the matching tag, and reads (start_chunk_id, num_chunks).
  4. CHUNK round(s). If num_chunks > 0, the client issues CHUNK queries at K_CHUNK = 80 per round, potentially spreading many chunks across multiple rounds.
  5. MERKLE round(s). For each bin returned (found or empty), the client issues padded sibling-PIR queries — one per tree level — to verify the bin against the published Merkle root. For a "not found" answer, this must cover both INDEX_CUCKOO_NUM_HASHES = 2 in-group bins.
  6. Result. Only now does the client report UTXOs to the rest of the wallet. Every PIR-returned byte has been Merkle-verified; every round looked the same on the wire regardless of whether the address had one UTXO or zero.