Oblivious Signing

Infrastructure, not intermediaries

OP_NEXT 2026

Sigbash

Oblivious Signing

Named by analogy with Oblivious DNS; the resolver answers queries without learning what you looked up. Two phases: Key Creation and Transaction Signing

1

User defines and commits to rules

What transactions are allowed, under what conditions, with what limits

2

User generates keys

Random user key aggregated with randomly-derived child of server key

3

User generates zero-knowledge proofs

Locally, in WASM, without revealing the transaction to anyone

4

Verifier signs, obliviously

Sees only a proof bundle and original opaque commitment; never amounts or addresses

Key Creation
Signing

Takeaway: the verifier can't sign transactions by itself; it can only provide a signature share in exchange for a valid proof bundle.

Why do we need this?

BATTAC
(Best Alternative To Activating Covenants)

An oblivious signer can emulate covenant restrictions right now. No consensus changes needed, works with any wallet that supports Taproot today.

The happy path waiting for when covenants arrive

When (if?) covenants activate, the cosigner can serve the happy path - key path spend that looks like a plain multisig (or even just a single key!) - and the covenant can serve as the fallback path. Happy path stays private, fallback guarantees good behavior.

Either way, we need signing privacy yesterday

Right now every cosigner sees and stores users keys, balance and full transaction graph. Either we acknowledge the problem and fix it, or we all dump our coins and invest in companies that sell $5 wrenches.

Related academic work

Fuchsbauer & Wolf (2024), Concurrently Secure Blind Schnorr Signatures

What they achieve

  • Introduces concept of predicate blind signatures
  • Server validates policy conditions without seeing the full transaction

Their key limitations

  • Public predicates; policy structure is visible to the signer; leaks information
  • Not expressive enough; real policies tend to have multiple conditions, nesting, exceptions
  • No statefulness; cannot enforce "this path may only be used once"
  • Heavy proving times; impractical for complex real-world predicates

Four components of Oblivious Signing

1. POET policies

POET: Policy Operand Expression Trees. Boolean abstract syntax trees condensed to a compact 32-byte commitment. Real policies: nested boolean logic, roles, temporal conditions, rate limits, recovery paths.

2. Nullifier statefulness

Clients register nullifiers at policy creation time - either n-use (path can be used N times) and wallclock (time-bounded windows). Client submits proof of non-membership; server records when nullifiers are burned.

3. MuSig2 binding

2-of-2 MuSig2: one randomly generated client-side key, one derived from server master key. Blind aggregation; verifier never sees transaction or policy. Produces standard Taproot Schnorr signature.

4. SHA256-in-ZK

Proof circuit embeds the sighash derivation. Links the policy proof to the signing session cryptographically, preventing substitution attacks.

System overview

End-to-end oblivious signing pipeline: Client/WASM, Policy Compiler, Proof Generation, Verifier, Bitcoin
  • All heavy computation; key aggregation, proof generation; runs in WASM in the browser
  • The verifier stores a 32-byte PolicyRoot at registration; at signing, receives a proof bundle only
  • The on-chain result is a standard Taproot Schnorr signature;indistinguishable from a key-path spend
  • User receives a standard BIP-328 xpub, works with any wallet; signer accepts standard PSBTs;MuSig2 ceremony is invisible

POETs: beyond simple predicates

POET operators: basic logic gates (AND, OR, NOT, XOR, NAND, NOR, IFF, IMPLIES, VETO) and advanced threshold (THRESHOLD, WEIGHTED_THRESHOLD, MAJORITY, EXACTLY, AT_MOST)

14 operators across two groups

Basic logic gates (9):

  • AND · OR · NOT · XOR
  • NAND · NOR · IFF
  • IMPLIES · VETO

Advanced threshold (5):

  • THRESHOLD · MAJORITY · EXACTLY
  • AT_MOST · WEIGHTED_THRESHOLD

Constrained parameters:

  • amounts · addresses · sequence (RBF)
  • op_return values · input/output indices
  • locktime · and more

Example: CEO always allowed · (CFO AND Friday AND payroll address) · recovery after 90 days · treasury requires (2-of-3 AND NOT frozen)

Expressiveness of POETs

Functional completeness

  • 9 basic logic gates (AND, OR, NOT, XOR, NAND, NOR, IFF, IMPLIES, VETO) are functionally complete
  • Any logical relationship can be built from these primitives

Policy completeness

  • All PSBT parameters (amounts, addresses, sequences, timelocks, op_return, signatures, etc.) + all operators theoretically enable any possible signing policy
  • If it can be observed in a transaction, it can be enforced

Three types of conditions

  • Direct observation: "What is this value?" (amount = 100 BTC; locktime = timestamp)
  • Derived observation: "Does this value pass a test?" (amount < limit; signature is valid; hash preimage matches)
  • Set inclusion: "Is this a member of a set?" (address in whitelist; op_return in approved values; input index in allowed range)

Set inclusion is the natural model for Bitcoin collections: addresses, outputs, signatures.

POETs: nullifiers and state

Why policies need memory

  • One-time approvals ("this specific payment, once")
  • Rate limits ("no more than 5 BTC per week on this path")
  • Sequence-sensitive rules ("recovery path only after standard path fails")
  • Invalidation (using a recovery path should burn the standard path)

Nullifiers as the mechanism

  • Policy can include stateful constraints: use limits, time windows, recovery paths
  • To use a path: client proves constraint not yet violated (via server-side Sparse Merkle Tree)
  • After signing: server records this proof as "used" (cannot be replayed)
  • Signer never learns the constraints or their values

Nullifiers enable stateful policies (the signer verifies compliance without learning the constraints).

POETs: privacy of policy

What verifier must NOT learn

  • The policy tree (org chart)
  • Which branches exist
  • Authorization logic
  • Transaction amounts or destinations

What verifier DOES learn

  • A 32-byte PolicyRoot
  • A valid proof exists
  • Proof bound to signing session
  • Nullifier is valid (stateful policies and stateless both submit nullifiers for obfuscation; stateless policies have vacuous nullifiers)

Unlike prior work: we hide the policy structure itself; verifier proves compliance without learning the rules.

POET walkthrough

POET compilation pipeline: policy tree (CEO OR (CFO AND Friday)) compiles via Tseitin CNF into two clauses C1 and C2, each hashed into a Merkle tree with a single PolicyRoot. At signing only one clause is revealed.

Why CNF and merkelized clauses

Why CNF (Conjunctive Normal Form)

  • Uniform structure; every satisfying path becomes a flat clause; no ambiguity about which branch was taken
  • Circuit-friendly; CNF clauses map directly to ZK constraint systems
  • Independently commitable; each clause can be hashed and Merkelized separately; paths don't bleed into each other

Why Merkle commitment

  • Compact root; entire policy reduces to 32 bytes stored by the verifier
  • Selective disclosure; the client opens one path leaf (a 32-byte clause hash) and a Merkle path to PolicyRoot; all other leaves stay hidden
  • Clause content is never revealed; the ZK proof proves the transaction satisfies the clause behind the leaf hash without exposing what that clause says; the verifier sees a hash, not conditions

What the verifier actually receives: a leaf hash · a Merkle path · an opaque ZK proof. It cannot read the clause, count the branches, or learn the policy structure.

ZK-gated MuSig2

Client-side key aggregation

  • Server provides its master key (one-time at setup)
  • WASM derives random child key from server master
  • Client generates its own random key
  • Aggregate: X̃ = [server_key + client_key] (MuSig2 aggregation)
  • Result: standard Taproot key (verifier sees only )

Proof bundle validation

  • Merkle inclusion: prove satisfied path leaf is in PolicyRoot
  • ZK proof: prove transaction satisfies that leaf's constraints (sighash bound privately inside circuit)
  • Comparison: proof bundle verified against 32-byte opaque PolicyRoot

Blinded 2-Party MuSig2 has no formal security proof but there are several well known "folklore" approaches. (https://gnusha.org/pi/bitcoindev/ca674cee-6fe9-f325-7e09-f3efda082b6b@gmail.com/). They all require SHA256-in-ZK - can we get around that?

Honest prover vs malicious prover

What we first assumed

  • Honest prover constructs proof and signing request coherently
  • A well-formed challenge in the FS transcript binding the proof to the session looked sufficient (yay no SHA256-in-ZK!)
  • MuSig2 challenge e is computed from the sighash; surely that's the binding?
  • If the proof verifies and the session is valid, what could go wrong?

What production demands

  • Malicious prover can split worlds: construct a valid proof for transaction A, then open a MuSig2 session for transaction B
  • The ZK proof has already been submitted; the signing session is a separate protocol step
  • Challenge e is computed after the proof; it's not inside the proof
  • Must assume adversarial client behavior

Honest prover can only be guaranteed with software attestation or firmware authenticity checks - WebAssembly must be assumed to be possibly malicious

The policy substitution attack

Policy substitution attack: prover proves compliance for Tx A, induces verifier to sign Tx B; gap exists without SHA256-in-ZK

Gap: proof and signing session are separate cryptographic objects.

The fix: SHA256-in-ZK chains policy root → sighash → challenge e, binding proof and session into one object.

Substitution attack: formal model

Adversary A controls client WASM execution and can construct arbitrary proofs and signing requests (either malware hijacking a user's machine or a purpose-built malicious client with valid credentials; e.g. a corp user trying to subvert organization spending policy).

Without SHA256-in-ZK

1

A generates compliant PSBT for TxA (satisfies PolicyRoot P)

2

A constructs ZK proof π: "Policy P satisfied"; witness contains sighashA

3

A submits π to verifier. Verifier accepts (proof is valid).

4

A opens MuSig2 session with sighashB (non-compliant TxB). Gets partial sig for TxB.

With SHA256-in-ZK

1

π must contain a valid in-circuit SHA256 derivation: prove SHA256(tx_data) = sighashX

2

Verifier checks: sighash in proof = sighash in signing session

3

If A substitutes TxB: sighashB ≠ sighashA in proof → signing session rejected

4

Substitution infeasible without breaking SHA256 pre-image resistance

Why it would be nice to avoid SHA256-in-ZK

SHA256 is hard to prove in ZK circuits. Bitcoin doesn't have the luxury of choosing a friendlier hash.

The technical challenge

  • SHA256 is designed for hardware, not arithmetic circuits
  • Requires ~25,000 constraints per hash operation (very expensive)
  • Each BIP341 sighash derivation involves multiple SHA256 blocks
  • Dominates constraint budget and proof generation time

Why altcoins have it easier

  • Altcoins can choose ZK-friendly hashes: Keccak, Poseidon, BLAKE3
  • Poseidon: ~10 constraints per operation (250× more efficient)
  • They can optimize their protocol for ZK from the ground up
  • Bitcoin is locked to SHA256 by protocol consensus — no choice

Different proof systems; pros and cons

System Prove Verify Notes
Groth16 39 s 400 ms Ceremony
Plonk 2m 29s 12 ms Ceremony
Jolt (WASM) 6.27 s 0.81 s RISC-V
Longfellow-zk 795 ms 466 ms Transparent

Data sources: First two from Fuchsbauer-Wolf paper; Jolt from live Jolt.rs demo; Longfellow-zk from their documentation. All numbers except Jolt are for native code.

Why Longfellow-zk

Longfellow-zk is Google's algorithm; a combination of the Ligero scheme (Lightweight Sublinear Arguments Without a Trusted Setup, 2022) and GKR Sum-Check (Delegating Computation: Interactive Proofs for Muggles, 2008). Independently audited by Trail of Bits and ISRG.

Better fit for SHA256 binding

  • Ligero is a linear-time IOP; efficient for SHA256's boolean gate structure without field-element bit decomposition
  • Sum-Check protocol evaluates multilinear extensions layer by layer; SHA256's 64 rounds map cleanly to this structure
  • Transparent setup; no trusted ceremony; proof system security rests on collision-resistant hashing alone

Why the audits matter

  • Trail of Bits; cryptographic security audit of the proof system and implementation
  • ISRG (Internet Security Research Group, operators of Let's Encrypt); independent review and production deployment experience
  • We are not the authors and did not design the proof system; we benefit from Google's engineering and external audit history

Sumcheck vs general zkVMs

General zkVM (e.g., Jolt, Cairo)

  • Flexible: run arbitrary code, loops, memory access, control flow
  • Cost: each instruction becomes multiple constraints; large proofs
  • When to use: you cannot express your computation algebraically, or don't want to

Sumcheck + algebraic encoding (Longfellow)

  • Restricted: must express valid states and transitions as polynomial relations
  • Payoff: tiny proofs, low constraint count, SHA256-friendly
  • When to use: your logic fits polynomial identities (and POET's certainly does)

POET is polynomial: policy nodes evaluate to bits; AND/OR are Boolean gates encoded as (z − xy = 0). Nullifiers, range checks, and threshold logic all become field equations. No bytecode interpreter needed.

See: Thaler, Sum-Check Is All You Need: An Opinionated Survey on Fast Provers in SNARK Design, a16z crypto research, 2025.

Longfellow WebAssembly performance part 1

Initial

45s proof

60s full

Unworkable

Threshold
~1s to notice
~10s to lose focus

WASM penalty: Roughly 1.5–2x slower than bare metal on average; potentially 25–50x slower for ZK operations due to missing SIMD and unimplemented instructions (swizzle, cmul).

The SHA256-in-ZK challenge: Jolt.rs state-of-the-art

Jolt.rs proves SHA256 in 6+ seconds for a single SHA256 string in WASM. We need to prove ~13 SHA256 blocks.

What we must prove in-circuit

  • BIP341 sighash: 3 SHA256 blocks
  • BIP341 challenge digest: 2 SHA256 blocks
  • Input/output script pubkeys: 5–6 blocks
  • Policy serialization: 3–4 blocks
  • Total: ~13 SHA256 blocks (832 bytes)

Why Jolt.rs is too slow

  • 6.27s per SHA256 block in WASM
  • 13 blocks × 6+ seconds = 78+ seconds minimum
  • RISC-V instruction interpretation overhead unavoidable
  • Architecturally incompatible with interactive signing UX

Longfellow WebAssembly performance part 2

Initial

45s proof

60s full

Unworkable

Threshold
~1s to notice
~10s to lose focus

Optimized

~3s full

PSBT → proof → signed

Key optimizations: Constraint batching via Web Workers; custom polynomial evaluation with circuit-specific unrolling; aggressive inlining of hash operations; commitment tree caching.

Trust model

Math + Implementation

  • ECDLP hardness (secp256k1) — no migration path yet; research ongoing on PQ aggregatable signatures
  • Reed-Solomon distance + Sumcheck (Ligero/GKR) — believed post-quantum secure
  • SHA256 pre-image resistance (nullifier + sighash binding)
  • Client WASM correctness; implementation bugs compromise both privacy and soundness

Server Judgment

  • Server cannot censor or inspect: sees only PolicyRoot (32-byte hash) + proof + signature. Never sees policy content, transaction sighash, amounts, or destinations. Cannot identify users or track spending patterns.
  • Adversarial client: explicitly in threat model. SHA256-in-ZK binding proves the sighash was evaluated in-circuit; client cannot prove one transaction and sign another.

Attack surface

MuSig2 / key layer

  • Rogue key attack; mitigated by BIP327 key aggregation hash (binds each key to full key set)
  • Wagner/ROS attack; concurrent sessions allow birthday attacks on nonces; mitigated by single-session enforcement

Policy layer

  • Vacuous policies; policies trivially satisfied by any transaction (e.g., empty AND clause); caught by policy compiler validation
  • Policy substitution; mitigated by SHA256-in-ZK sighash binding

Nullifier layer

  • Replay attacks; nullifier SMT prevents reuse of proof sessions; nullifier insertion is irreversible
  • Nullifier forging; requires breaking SHA256 pre-image resistance
  • Nullifier griefing; "burning" nullifiers with valid transactions but malicious intent

Vacuous policies: #1, Unconstrained outputs

Policy says:

Alice ∈ outputs AND amount ≥ 5 BTC

The gap: Only checks Alice is in the set. Does not limit other outputs. Client adds Eve receiving 100 BTC. Policy satisfied.

Vacuous policies: #2, Unconstrained change

Policy says:

outputs[0] = 1 BTC to Bob

The gap: Input is 10 BTC, Bob gets 1. The other 9 BTC unspecified. Client sends it to themselves as change. Policy satisfied.

Vacuous policies: #3, Unconstrained fees

Policy says:

outputs[0] = 2 BTC to Carol
outputs[1] = remainder to self

The gap: Input 3 BTC. Carol 2 BTC. Remainder 1 BTC, but fee unbounded. Client and miner collude: miner claims 0.99 BTC as fees instead of fair amount.

Vacuous policies: #4, Stateless limits

Policy says:

amount ≤ 0.5 BTC per transaction

The gap: No memory across signing sessions. Client spams 100 transactions of 0.5 BTC each = 50 BTC total, even if wallet holds 1 BTC. Nullifiers required.

So do we still need covenants?

Oblivious signing (Sigbash)

  • Conditions: off-chain, private
  • Participants: client + signing server
  • Binding: ZK proof between two parties
  • Fallback: none; policy enforced by server cooperation

Covenants (on-chain)

  • Conditions: on-chain, public
  • Participants: all quorum members + blockchain
  • Binding: opcode in output script
  • Fallback: covenant enforced regardless of cooperation

Perfect world (covenants exist): Happy path uses nested MuSig2 (or MuSig-in-FROST) subset between client and server, emulating the covenant privately. Fallback path: covenant opcode binds all participants, enforcing identical conditions if signers don't cooperate. Privacy maintained on happy path; fairness guaranteed on fallback.

Policy is source of truth, cosigner is just a notary.

Policy-enforced custody

Custodian enforces withdrawal policies but can't freeze specific addresses or selectively delay. Doesn't know which withdrawal belongs to which client.

Executor-proof inheritance

After a timelock, cosigner signs for any valid heir. Can't tell heirs apart, can't be bribed to accelerate. Executor can't collude with one beneficiary.

Corporate treasury, one cosigner

CEO+CFO together: unlimited. Each alone: capped. HR: payroll addresses only, Fridays only. Intern: per diem daily limit. Same cosigner enforces all four policies without knowing which role is signing.

Private peg-out federation

Federation validates peg-outs but learns nothing about who, how much, or where. Can't censor users or build a withdrawal graph.

Demo time!

Part 1: Key registration (webapp)

  • Generate a new key pair in the browser via WASM
  • Register the public key with the signing server
  • Server stores the key; never sees the private half

Part 2: Policy + signing (SDK)

  • Define a POET policy; compile it; register PolicyRoot with the server
  • Construct a compliant PSBT, generate ZK proof, submit proof bundle
  • Server verifies proof, returns partial signature; client assembles final transaction

Compliant PSBT: proof verifies, partial signature returned, transaction signed.

Non-compliant PSBT: proof generation fails at the constraint level; no proof bundle produced, no signature issued.

Thank you

Oblivious signing is live at

sigbash.com

SDK launching to integration partners in two weeks

Thanks for feedback & constructive criticism:

  • Rob Hamilton — Anchorwatch
  • Shinobi — Bitcoin Magazine
  • Salvatore Ingala — Ledger
  • Yuval Koenig — Spiral
  • Johan Halseth
  • James O'Beirne
  • Mainnet launch target: Pizza Day — May 22, 2026
  • Find me at Pubkey if you want more details — after-party starts at 6 PM