How it works
Winternitz signs with keys made of hash chains, uses each key exactly once, and lets the account rotate to the next key inside the same transaction.
1. Keys are hash chains#
A hash function turns any input into a fixed 32-byte output. It is easy to compute forwards and practically impossible to reverse. A hash chain repeats it: start with a random secret and hash it 15 times.
- The start of the chain (position 0) is secret.
- The end of the chain (position 15) is public.
- Anyone can walk forwards along a chain. Nobody can walk backwards.
2. Signing is revealing a point on each chain#
To sign, the wallet first hashes the message into 32 bytes and splits it into 64 digits between 0 and 15. For each digit it reveals the chain value at that position. The verifier hashes each revealed value forward until it reaches position 15 and checks that the result matches the public key.
Three extra checksum digits are added, bringing the total to 67 chains. The checksum stops an attacker from hashing revealed values forward to forge a different message: making one digit larger always makes the checksum smaller, and making a checksum digit smaller would mean reversing a hash.
| Parameter | Value |
|---|---|
| Hash function | keccak256 |
Winternitz parameter w | 16 (4 bits per digit) |
| Chains per key | 67 (64 message + 3 checksum) |
| Signature size | 67 × 32 bytes = 2,144 bytes |
| Hashes to verify | at most 67 × 15 = 1,005 (about half on average) |
3. A key signs only once#
Revealing points on a chain gives something away. After two different signatures with the same key, an attacker may be able to combine them into a signature for a third message. So a Winternitz key must never sign twice.
Winternitz solves this by giving your account an endless sequence of keys, all derived from one 32-byte seed, and using a new one for every transaction.
burned
signed tx 1
burned
signed tx 2
active
signs next tx
queued
committed next
queued
4. The account rotates automatically#
Your account contract stores just one value: the hash of the public key that must sign next. Every transaction carries the hash of the key that comes after it, and the signed message covers that next key too.
Your wallet reads the current key index from the chain
It never trusts local state for this, so two devices can't disagree about which key is next.
It signs the operation with key #n
The signed message is keccak256(userOpHash ‖ hash of key #n+1), so nobody can swap in a different next key.
The EntryPoint asks your account to validate
The account recomputes the public key from the signature and compares it with the stored hash.
The account moves on to key #n+1
If the signature is valid, the account replaces the stored hash with key #n+1's hash and emits a KeyRotated event. Key #n can never sign again.
Your address never changes. Only the key behind it does.
5. Replays are rejected#
A signature cannot be reused in any form:
| Attempt | Result |
|---|---|
| Re-submit an operation that already went through | Rejected by the EntryPoint nonce (AA25) |
| Attach an old signature to a new operation | Rejected: the signed message no longer matches (AA24) |
| Sign with a key that was already used | Rejected: the account has moved on to the next key (AA24) |
| Change even one bit of a signature | Rejected (fuzz-tested over all 2,176 bytes) |
6. A recovery key unsticks stuck transactions#
If a transaction can never confirm (say, fees spiked), your key can't simply sign it again. Your seed also derives a separate chain of recovery keys. A recovery key signs one replacement with the same nonce and higher fees; the account then skips the stuck key and moves on to the next recovery key, emitting KeyRecovered. Main and recovery signatures have different formats, so neither can pass for the other.
7. Your first transaction deploys the account#
Your address is computed in advance with CREATE2, from the factory and your first main and recovery key hashes. You can receive funds before anything is deployed. The first transaction you send deploys the account contract and burns key #0 in the same step.
Want the exact specification?
The byte-level scheme (chain function, key derivation, digit encoding) is in Developers, and in full in docs/design.md in the repository.