01What a bunker is
A bunker is a Solana account owned by the Bunker Cash program, at an address derived from a 32-byte id: seeds ["bunker", bunker_id]. It holds SOL as plain lamports. It has no private key: it is a program-derived address, so no ed25519 key exists for it, and the program will only move its lamports when shown a valid one-time signature.
The bunker stores a 28-byte commitment to its current one-time public key and a key index. Opening a bunker writes the commitment for key 0. Each verified release replaces the commitment with the one named inside the signature, so the key that just signed can never sign again.
02The signature
Bunker Cash uses a Winternitz one-time signature (WOTS) built only from SHA-256. A signer holds 30 secret values. Hashing each secret 255 times gives 30 chain ends; hashing the chain ends together gives the public key commitment stored on chain.
Hash
SHA-256, truncated to 224 bits (28 bytes)
Winternitz w
256 (one byte per chain)
Message chains
28
Checksum chains
2
Chain length
255 steps
Signature
30 x 28 = 840 bytes
Key commitment
28 bytes, H(end_0 || ... || end_29)
Domain tag
bunker.cash/release/v1
Verification cost
up to 7,650 SHA-256 syscalls
| Parameter | Value |
|---|---|
| Hash | SHA-256, truncated to 224 bits (28 bytes) |
| Winternitz w | 256 (one byte per chain) |
| Message chains | 28 |
| Checksum chains | 2 |
| Chain length | 255 steps |
| Signature | 30 x 28 = 840 bytes |
| Key commitment | 28 bytes, H(end_0 || ... || end_29) |
| Domain tag | bunker.cash/release/v1 |
| Verification cost | up to 7,650 SHA-256 syscalls |
To sign a 28-byte digest, each digest byte picks how far along its chain to reveal: byte value b reveals the secret hashed b times. Two more chains encode a checksum, the sum of (255 - b) over all bytes. The verifier finishes every chain to step 255 and checks the result hashes to the stored commitment.
positions b[0..28] = digest bytes
checksum = sum(255 - b[i]) // 0..7140
b[28] = checksum >> 8
b[29] = checksum & 0xff
sign sig[i] = H^b[i](secret[i])
verify end[i] = H^(255 - b[i])(sig[i])
H(end[0] || ... || end[29]) == bunker.current_key_hash03What is signed
The digest binds everything that matters about a release. Change any field by one bit and the signature is worthless.
domain
bytes22
encodingascii bunker.cash/release/v1
whyno cross-protocol replay
bunker
bytes32
encodingpubkey
whyuseless on any other bunker
key_index
bytes8
encodingu64 little endian
whyuseless after rotation
amount
bytes8
encodingu64 little endian, lamports
whyexact amount
destination
bytes32
encodingpubkey
whyexact recipient
next_key_hash
bytes28
encodingcommitment to key k+1
whythe owner names the successor
| Field | Bytes | Encoding | Why |
|---|---|---|---|
| domain | 22 | ascii bunker.cash/release/v1 | no cross-protocol replay |
| bunker | 32 | pubkey | useless on any other bunker |
| key_index | 8 | u64 little endian | useless after rotation |
| amount | 8 | u64 little endian, lamports | exact amount |
| destination | 32 | pubkey | exact recipient |
| next_key_hash | 28 | commitment to key k+1 | the owner names the successor |
digest = SHA-256(domain || bunker || u64le(key_index) || u64le(amount)
|| destination || next_key_hash)[0..28]04Three steps
An 840-byte signature plus its terms leaves almost no room in a 1,232-byte Solana packet, and checking it takes about a million compute units. So a release is split into three instructions. You sign once, in your browser; the relay sends all three.
authorize_release
senderanyone, 1 signer
doesStores the terms and signature in a Release account seeded by ["release", bunker, next_key_hash]. No hashing. 1-byte discriminator, legacy transaction, 1,223 of 1,232 bytes with a priority fee.
verify_release
senderanyone
doesWalks all 30 chains (compute limit 1,400,000), checks the key index is still current and the bunker can cover the amount after rent and reserves, reserves it, and rotates the key to next_key_hash.
execute_release
senderanyone
doesMoves the reserved lamports to the destination and closes the record, refunding its rent to whoever paid for authorize.
cancel_release
senderoriginal payer
doesReclaims rent on a record that never verified. A verified release cannot be cancelled: the key already rotated.
| Step | Sender | Does |
|---|---|---|
| authorize_release | anyone, 1 signer | Stores the terms and signature in a Release account seeded by ["release", bunker, next_key_hash]. No hashing. 1-byte discriminator, legacy transaction, 1,223 of 1,232 bytes with a priority fee. |
| verify_release | anyone | Walks all 30 chains (compute limit 1,400,000), checks the key index is still current and the bunker can cover the amount after rent and reserves, reserves it, and rotates the key to next_key_hash. |
| execute_release | anyone | Moves the reserved lamports to the destination and closes the record, refunding its rent to whoever paid for authorize. |
| cancel_release | original payer | Reclaims rent on a record that never verified. A verified release cannot be cancelled: the key already rotated. |
The key rotates only in verify, only after the signature checks out. Nobody can rotate a bunker with garbage. Release records are seeded by the next key hash rather than the key index, so a stranger cannot block the owner by squatting the slot: only someone who knows key k+1 can occupy it.
05Keys from 24 words
Your 24 words are a standard BIP39 phrase (256 bits of entropy). Everything else is derived from them in the browser with Web Crypto, and the derivation is fixed, so the same words always find the same bunker.
seed = BIP39 mnemonicToSeed(words)[0..32] bunker_id = SHA-256(seed || "bunker-id") address = PDA(["bunker", bunker_id]) secret(k, i) = H(seed || "sk" || u64le(k) || [i]) // i = 0..29 key_hash(k) = H(H^255(secret(k,0)) || ... || H^255(secret(k,29)))
Key k signs the release that rotates the bunker to key k+1. There are 2^64 keys per bunker; you will not run out. The browser signer is tested byte for byte against the reference implementation the program's integration suite uses.
06Why it holds
Forward only. A chain can be walked forward by anyone, never backward. Someone holding a signature can raise a message byte by hashing further along its chain, but raising any message byte lowers the checksum, and lowering a checksum position means inverting SHA-256. That is the whole argument.
Hash-only. Forging requires a preimage of truncated SHA-256: about 2^224 work classically and about 2^112 for an attacker with a large quantum computer running Grover's search. No elliptic-curve or factoring assumption sits between the funds and the owner.
One use. A WOTS key is safe for exactly one signature. The program enforces this by rotating on every verified release and binding the key index into the digest, so an observed signature opens nothing afterwards.
No double claims. The bunker tracks lamports reserved by verified-but-unexecuted releases. Verification fails unless the free balance covers the new amount, so two releases can never claim the same lamports.
07The relay
You never need a wallet. The platform runs a relay that builds each transaction, pays the fee and rent from its own account, and submits it. It receives only public values: the bunker id, key commitments, amount, destination, and the finished signature. Before spending rent on an authorize, it checks the signature off chain against the bunker's current key.
The relay cannot steal or redirect funds: the signature only opens the exact terms you signed. It can refuse service. If it does, every step can be sent by anyone with any funded wallet, straight to the open program.
08Coins
A coin launched here gets a Meteora bonding-curve pool whose creator seat is a bunker. Creator trading fees (1% or 2% per trade, chosen at launch) are cranked into that bunker and split with the platform bunker. The platform share defaults to 20% and is hard-capped at 30% on chain, so a compromised admin key cannot redirect creator revenue wholesale. Withdrawing fees is an ordinary release.
The pool module is being built. The launch form is ready for it.
09What it does not do
- SOL only.v1 bunkers hold lamports. Tokens sent to a bunker address cannot be released by this program.
- No recovery.Lose the 24 words and the funds are gone. Nobody, including the platform, can reset a bunker.
- Upgradeable until frozen.The program has an upgrade authority until it is explicitly revoked. Until then, whoever holds that authority could change the rules. This is the largest trust assumption in the system.
- One signature per key.Signing two different releases under the same key leaks more of it. This app never does: an interrupted release is resumed with its original signature, not re-signed.
- Not a shield for Solana.Validators, fee payers, and every other account still use ed25519. A bunker keeps an attacker out of your funds; it does not keep the network itself running.
- Pausable entries.An admin pause blocks opening bunkers and program deposits. It never blocks authorize, verify, or execute. Exits are never trapped.
- Unaudited.This is new code. Size your deposits accordingly.
10Numbers
framework
Anchor 0.31.1
signature
840 bytes
authorize tx
1,223 / 1,232 bytes with a priority fee
verify compute
limit 1,400,000 CU, about 1M used
bunker account
175 bytes
release account
998 bytes, rent refunded on execute
platform share
20% default, 30% cap
| Item | Value |
|---|---|
| program | BuNKTmvVfThYYh4f1aJjjqbogthf5t5cpzQ1Wjjk5MZ4 |
| framework | Anchor 0.31.1 |
| signature | 840 bytes |
| authorize tx | 1,223 / 1,232 bytes with a priority fee |
| verify compute | limit 1,400,000 CU, about 1M used |
| bunker account | 175 bytes |
| release account | 998 bytes, rent refunded on execute |
| platform share | 20% default, 30% cap |