Signature scheme
Winternitz one-time keys, Merkle root ownership, and how QLP prevents key reuse and replay.
QLP's post-quantum signatures are built from two pieces: Winternitz one-time keys, and a Merkle tree that groups many of them under one public identity.
Winternitz one-time keys
Secret values are hashed repeatedly into chains. The end of each chain is part of the public key. A signature reveals one position per message digit, and the verifier hashes the rest of the way to the chain end.
One chain, step by step
QLP uses a Winternitz parameter of w = 16: each chain encodes one base-16 message digit, and each step along the chain is one SHA-256 hash. The chain starts at a secret value sk and runs through H¹(sk), H²(sk) … up to H¹⁵(sk), which is the public value.
To sign a digit d:
- The signer reveals
σ = Hᵈ(sk). Earlier values in the chain stay secret. - The verifier computes
H¹⁵⁻ᵈ(σ)and checks that it equalsH¹⁵(sk), the public chain end.
For example, for the digit 5 the signer reveals H⁵(sk) and the verifier hashes it 10 more times.
Why each key is one-time
Anyone could take a revealed value and hash it forward to forge a larger digit. A checksum blocks those forward-hashing forgeries. But signing twice with the same key leaks enough chain values to forge, so each key must be used exactly once.
Merkle root ownership
Many one-time keys form a Merkle tree. Its 32-byte root is your post-quantum identity.
A signature carries the leaf index (which one-time key was used) and the sibling hashes needed to rebuild the path up to the root. The verifier recomputes the root from the signature and compares it to the root you registered.
Key use and replay protection
The program stores the next unused key index (Position.next_q). It accepts only signatures at or above that index, then advances it, so an old signature can never be replayed.
Every message is domain-separated: it names the protocol, the cluster, the action, the program, the accounts involved and an expiry, so a signature for one purpose cannot be reused for another.
message (179 bytes, built by the program) =
"QLP/safe-vault/v1" ‖ cluster_tag ‖ action ‖ program_id ‖ vault_pda ‖
position_pda ‖ key_epoch ‖ sequence ‖ q ‖ amount ‖ destination ‖ expiry_slotThe trade-off: you must keep your key state
One-time keys are stateful. Losing your key state risks reusing a key; losing the keys themselves loses access to the position.