Every step
is a keccak.
prelode is a vault whose only authorisation primitive is keccak256. It uses the same ingredients lean Ethereum is converging on for hash-based signatures: Winternitz one-time signatures and a Merkle tree, verified in a contract that has no owner, no upgrade path and no ECDSA fallback.
Open a vaultFrom a seed
to a spend.
- 01
Derive one-time keys.
skleaf,i = keccak256("preimage/sk" ‖ seed ‖ leaf ‖ i) for 67 chains per leaf. Each chain is walked 15 steps with a position tweak, f(x,i,j) = keccak256(x ‖ i ‖ j), so chains can never be swapped or replayed across positions. The chain ends are hashed together into the leaf.
pk_i = f^15(sk_i) leaf = keccak256(pk_0 ‖ … ‖ pk_66) root = merkle(leaf_0 … leaf_{2^h−1}) - 02
Commit the root.
A vault is created with up to 32 roots, a threshold and the tree height. The address is CREATE2-derived from exactly those values plus a salt, so it is known before deployment and cannot be deployed with a different config. Anyone can deploy it; the deployer gets nothing.
salt = keccak256(abi.encode(roots, threshold, height, userSalt)) vault = CREATE2(factory, salt, minimal proxy → implementation) - 03
Sign the exact calls.
The digest covers the chain id, the vault address, the vault nonce and the full list of calls (target, value, calldata). It is split into 64 nibbles plus a 3-nibble checksum. For each chunk mi the signer reveals fmi(ski). Each leaf signs once: the key file advances its counter before the signature is written out, and the contract keeps a spent bitmap.
d = keccak256(abi.encode(DOMAIN, chainid, vault, nonce, calls)) σ_i = f^{m_i}(sk_i) (67 words, 2144 bytes) bundle = { calls, auths: [{signer, leaf, σ, merkle proof}] } - 04
Anyone relays.
The contract walks each chain the remaining 15 − mi steps, hashes the 67 ends into a leaf, verifies the Merkle path against the signer's root, marks the leaf spent, and only then runs the calls. A relayer cannot alter a byte: any change moves the digest and the recovered leaf. Around 165k gas for a 1-of-1 ETH transfer.
execute(calls, auths): d = digest(calls, nonce++) for auth: leaf = leafFromSignature(d, σ) require merkle(leaf, proof) == roots[signer] require !used[signer][leaf]; used = true for call: call.to.call{value}(data) - 05
Rotate before you run out.
Rotation is just another call, from the vault to itself. It installs new roots, a new threshold and height, bumps the generation and starts a fresh spent bitmap. Do it when a key file leaks, when a co-signer changes, or when a tree is nearly spent.
calls = [{ to: vault, data: rotate(newRoots, threshold, height) }]
Each signer signs the same digest with their own one-time key and passes the bundle along. The last signer, or anyone, relays. There is no ECDSA signer anywhere in the path, so there is nothing to "gracefully degrade" to.
Because the address commits to the config, you can send ETH or tokens to a vault that does not exist yet. Deploy it from a burner when you need to spend. Botched migrations are the real risk; this makes the move a two-step you can rehearse with dust.
Reusing a Winternitz key leaks parts of the secret. The tooling takes it seriously: the counter is advanced and written before a signature exists, and the on-chain bitmap is consulted so a stale file still cannot reuse a leaf.
It is a small contract, a signer library mirrored in Solidity and JavaScript, and a relay. Read the code before trusting it with more than you would lose. The factory and vault have no admin and cannot be paused or upgraded by anyone, including us.
