What this is

A Liquid wallet that signs covenant spends with a hash-based, quantum-resistant signature. Plus a small new crate inside LWK that wires the on-chain part end-to-end.

Why I built it

Blockstream had already published the SHRINCS Simplicity verifier a covenant that accepts hash-based, quantum-resistant signatures on chain. The on-chain half was done. But no wallet could actually use it: no Rust signer that produced a SHRINCS-compatible signature, no LWK glue to inject the witness, no demo of the full loop.

That gap bothered me. The whole point of having a post-quantum verifier on chain is that wallets can lock funds with it. Without a wallet, the verifier just sits there. So we built the wallet side.

How it works (in one paragraph)

You generate a Lamport keypair, derive a Liquid P2TR address that commits to a tiny wrapper covenant (the SHRINCS verifier plus two security bindings we added), fund that address with tL-BTC from the faucet, then spend it by producing a Lamport signature over the spending transaction's sighash. The wallet builds the 4-tuple Simplicity witness [witness, program, cmr, control_block], injects it into the PSET input, signs the fee input with a normal Schnorr key, and broadcasts.

The two security bindings We added

The upstream SHRINCS verifier accepts any valid Lamport signature, full stop. That means two real problems:

  1. Every UTXO would share the same address (the verifier doesn't know who you are). Our wrapper adds param::EXPECTED_PK_HASH, which binds the address to a specific public key.
  2. A sniffed signature could be replayed against any other transaction spending the same scriptPubKey. Our wrapper adds jet::sig_all_hash(), which makes the signed message be the spending transaction itself.

These two changes are ~10 lines of SimplicityHL. Without them, the covenant is on-chain anyone-can-spend.

How it's organised

Three pieces:

hackaton/
├── pq-liquid-wallet/   ← the wallet (this submission)
├── lwk/                 ← Blockstream's LWK + one new crate We added
│   └── lwk_simplicity_pq/   (PqSigner trait + Lamport + spend helper)
└── shrincs-simplicity-verifier/   ← Blockstream's verifier (read-only)

The wallet uses a just command surface: just setup builds everything once, just demo runs the full offline pipeline in 30 seconds, just option-b prints the testnet recipe.

What works today

  • Offline end-to-end: keygen → wrapper compile → Lamport sign → on-chain verifier accepts in-process. 26 tests green, byte-for-byte conformance against Blockstream's shipped .wit fixture passes.
  • A real funding tx is on Liquid Testnet: 43972e1970e4a4adcb036823270879a0a22c20d596ec3a4b64ef2fa5cad39a5f — 5 000 sats locked under the covenant, unspent.

What doesn't (honestly)

The on-chain spend assembles correctly (PSET built, Lamport signed, witness injected) but is rejected by Liquid's standard relay policy. The Lamport verifier's static worst-case cost is 256 iterations of SHA-256; the budget Liquid grants from the witness weight isn't enough. The standard fix (Taproot annex padding) is itself non-standard.

This isn't a bug in our code, it's a real consensus-vs-policy edge case.

Repo and full user guide: https://github.com/smeneguz/pq-liquid-wallet

Built With

  • electrum
  • elements
  • just
  • liquid
  • lwk
  • mcpp
  • rust
  • shrincs
  • simplicityhl
Share this project:

Updates