protocol v1 / account layout version 2
How Hashlock works
A sealed coin's creator fees and dev bag sit on an address no wallet key controls. The only thing that moves them is a hash-based one-time signature that a Solana program checks with SHA-256. This is the specification: every formula below is what the program and the browser run, and every claim can be checked on chain.
- vault program
- reading
- who can change it
- reading
- creator fees
- reading
- platform share
- reading
These four lines are read when the page loads. The rest of the page is constant for protocol v1.
Contents0%
1Overview
Hashlock is a launchpad for pump.fun coins. On Solana an ordinary address is an Ed25519 public key, so anything a wallet holds is only as safe as that curve. A sealed launch moves the two things a creator controls, the creator fees and the dev buy, onto an address that has no private key at all. It belongs to a vault, and a vault is opened by hash-based keys.
A vault's public key is one 32-byte Merkle root over 1,024 one-time signing keys. The keys come from a 24-word phrase that your browser generates and never sends anywhere. When a tree is nearly used up the vault rotates to a fresh one at the same address, from the same 24 words. To move anything out, you sign the exact action with the next unused one-time key of the vault's current tree, and the vault program recomputes the root from your signature on chain. If it matches the root stored in the vault, the move happens. No Ed25519 signature can stand in for that check.
Solana itself signs every transaction with Ed25519, and pump.fun's programs are classical. The threat model lists what each of those controls.
- Verified on chain. The signature is checked by the program that holds the funds, not written into a file for someone to check later.
- Independent of your wallet. By default the vault key is fresh randomness, so breaking your wallet's key says nothing about the vault's.
- Derived from the chain. "Sealed" is computed from three on-chain accounts. You can recompute it at /verify with any RPC and without our server.
2Threat model
The adversary can forge an Ed25519 signature for any public key it has seen. That could be a large quantum computer running Shor's algorithm, or a classical break of the curve. It cannot find second preimages of SHA-256. Grover's algorithm leaves about 2128 work for a 256-bit second preimage.
Because a Solana address is the public key itself, there is no hash in front of it to hide behind. Every ordinary wallet is exposed from the day such an adversary exists.
| Asset | Before a curve break | After a curve break |
|---|---|---|
| SOL and tokens in an ordinary wallet | Safe while the key stays secret. | Exposed. The address is the public key. |
| Creator fees of a sealed coin, once paid to the vault | Only a vault signature moves them. | Unchanged. No Ed25519 key can authorize the move. |
| Creator fees still inside pump.fun | Held by pump.fun's programs. The locked split names the vault, and anyone can trigger the payout. | The split cannot be edited by a forged creator signature, because its admin is revoked. pump.fun's own keys are classical: see the row below. |
| The dev bag | In the vault's token account. Only a vault signature moves it. | Unchanged. |
| Anything else deposited in a vault made from a 24-word phrase | Only a vault signature moves it. | Unchanged. |
| A vault derived from a wallet signature | Safe while the wallet key stays secret. | Exposed. Whoever can forge that wallet's signature rebuilds the vault key. Worth exactly as much as the wallet. |
| The fee payer of a vault transaction | Pays network fees and stages the signature bytes. It can decline to send. It cannot change what the signature authorizes. | The same powers in an attacker's hands: pay, or refuse. Recipient, amount and mint are all inside the signed message. |
| pump.fun's own programs | Upgradeable and administered by pump.fun. | Their keys are Ed25519. An upgrade or admin action there could redirect creator fees before they reach a vault. This holds for every pump.fun coin on any launchpad. |
| The vault program | Its upgrade status is read from chain: see Program upgrades. While an upgrade authority exists it is an Ed25519 path around every vault. | |
| Hashlock's own share of fees | Paid to one ordinary platform wallet and spent by the flywheel. | Exposed like any wallet. It holds fees on their way to a burn, never a creator's or holder's funds. |
| Solana consensus | Validators sign with Ed25519. | Outside any program's control. A chain that cannot agree on blocks protects nothing. |
3Primitives
One hash function is used everywhere: SHA-256, with n = 32 bytes. The one-time signature and the tree follow RFC 8391 (XMSS), SHA2_256 parameter set, as a single tree (layer 0, tree 0). toByte(x, 32) is x as 32 big-endian bytes; it separates the four functions.
F(KEY, M) = SHA256(toByte(0,32) || KEY || M)
M is 32 bytes
H(KEY, M) = SHA256(toByte(1,32) || KEY || M)
M is 64 bytes
H_msg(KEY, M) = SHA256(toByte(2,32) || KEY || M)
KEY = r || root || toByte(leaf,32)
PRF(KEY, M) = SHA256(toByte(3,32) || KEY || M)
KEY = pub_seed, M = 32-byte ADRS
The address, ADRS
Every hash call is tied to one position in the structure by a 32-byte address: eight big-endian 32-bit words.
- layerword 0, always 0
- treewords 1 and 2, always 0
- typeword 3: 0, 1 or 2
- word 4leaf index, or 0
- word 5chain index or tree height
- word 6hash index or tree index
- keyAndMaskword 7: 0, 1 or 2
| type | word 4 | word 5 | word 6 |
|---|---|---|---|
| 0 OTS | leaf index | chain index | hash index |
| 1 L-tree | leaf index | tree height | tree index |
| 2 hash tree | 0 | tree height | tree index |
Chains and node hashes
chain(X, i, s):
for j in i .. i+s-1:
KEY = PRF(pub_seed, ADRS{hash=j, km=0})
BM = PRF(pub_seed, ADRS{hash=j, km=1})
X = F(KEY, X xor BM)
RAND_HASH(L, R):
KEY = PRF(pub_seed, ADRS{km=0})
BM0 = PRF(pub_seed, ADRS{km=1})
BM1 = PRF(pub_seed, ADRS{km=2})
return H(KEY, (L xor BM0) || (R xor BM1))
Every call is keyed and masked by the pair (pub_seed, ADRS). An attacker cannot reuse one preimage search across many chains, many leaves or many vaults, because each position hashes differently. The message digest is randomized as well (section 7). That is why security rests on second-preimage resistance and not on collision resistance.
4One-time signatures (WOTS+)
A one-time key is 67 hash chains. With Winternitz parameter w = 16, each chain has 16 positions, 0 to 15, and each base-16 digit of the signed digest picks a position on one chain.
| symbol | value | meaning |
|---|---|---|
| n | 32 | hash output, bytes |
| w | 16 | positions per chain |
| len1 | 64 | digest digits (32 bytes, 4 bits each) |
| len2 | 3 | checksum digits |
| len | 67 | chains per one-time key |
| signature | 2,144 bytes | 67 values of 32 bytes |
Message digits and checksum
What gets signed is a 32-byte digest of the message (section 7 defines it). The digest is read as 64 base-16 digits, high nibble first. A checksum is appended so that nobody can turn one signature into another by hashing forward.
d[0..63] = base-16 digits of digest,
high nibble first
C = sum(15 - d[i]), i in 0..63
at most 960
C = C << 4
written as 2 big-endian bytes
d[64..66] = first 3 base-16 digits of C
Raising any message digit lowers the checksum, and lowering a checksum digit means walking a chain backwards, which is a SHA-256 preimage.
Sign and verify
ADRS = {type 0, word4 = leaf, word5 = i}
sig_i = chain(sk_i, 0, d[i])
the signer walks d[i] steps
pk_i = chain(sig_i, d[i], 15 - d[i])
the verifier walks the rest
The verifier runs sum(15 - d[i]) chain steps, so the cost depends on the digest. The signer runs the complement, and the two always add up to 67 x 15 = 1,005 steps. Each step is two PRF calls and one F.
A signature reveals a position on every chain. Two signatures from the same key, over different digests, reveal enough to forge a third. Each key signs one message, once. The program enforces that on chain: see one-time key enforcement.
5The key tree
A vault needs more than one signature, so it holds a tree of one-time keys, in the style of XMSS. The site uses height 10, which is 1,024 keys per tree. The program also accepts heights 8 and 12. The last leaf of every tree is reserved: it can only sign a rotation to the next tree (section 7).
From a one-time key to a leaf
The 67 public chain ends of a one-time key are compressed into one 32-byte leaf by an L-tree: pair them with RAND_HASH (ADRS type 1, word 4 = the key's index), level by level, promoting an odd node unchanged. 67 values take 66 hashes.
From leaves to the root
node(0, i) = leaf i
node(k+1, j) = RAND_HASH(node(k, 2j),
node(k, 2j+1))
ADRS{type 2, height = k,
index = j}
root = node(10, 0)
A tree's whole public key is the root, the public seed and the height: 65 bytes. The vault's address is derived from all three values of its first tree (section 7).
The authentication path
A signature carries the 32-byte randomizer r, the 67 chain values, and the 10 sibling hashes on the way from its leaf to the root, bottom up. At level k the verifier hashes the node it has with the sibling it was given. Bit k of the leaf index says which goes on the left.
signature blob = 32 + 2,144 + 32 x height bytes = 2,496 bytes at height 10 = r, then 67 chain values, then a0 .. a9
The signature is valid when the recomputed root equals the active root stored in the vault account, byte for byte.
6Key derivation
Keys are made in the browser. The program never sees a secret, and neither does our server.
Default: a 24-word phrase
entropy = crypto.getRandomValues(32 bytes)
mnemonic = BIP39 English, 24 words
256 bits + 8-bit SHA-256 checksum
bip39 = PBKDF2-HMAC-SHA512(
NFKD(mnemonic),
"mnemonic" || NFKD(passphrase),
2048 rounds, 64 bytes)
seed = SHA256(
"entangle/pqvault/v1/bip39" || bip39)
per epoch (epoch 0 is the first tree):
eseed = SHA256(
"entangle/pqvault/v1/epoch" ||
seed || epoch u32 LE)
sk_seed = SHA256(
"entangle/pqvault/v1/sk_seed" || eseed)
pub_seed = SHA256(
"entangle/pqvault/v1/pub_seed" || eseed)
sk_prf = SHA256(
"entangle/pqvault/v1/sk_prf" || eseed)
sk[k][i] = SHA256(
toByte(4,32) || sk_seed ||
ADRS{type 0, word4 = k, word5 = i})
chain i of one-time key k
The phrase is standard BIP39, checked against the reference test vectors, so you can validate your words with any BIP39 tool. An optional passphrase acts as a 25th word. Every step is a hash, so the key keeps about 128 bits of security against Grover.
One phrase, every epoch. Each rotation moves the vault to the next epoch, and every epoch's tree is derived from the same seed and the epoch number. Epoch 0 is the genesis key: it fixes the vault's address for good. To reopen a vault from the words alone, the client builds the epoch 0 tree, derives the vault address from it, reads the current epoch and height from the vault account, builds that epoch's tree and checks its root against the active root on chain.
Why the default does not use your wallet. A key derived from a wallet signature can be rebuilt by anyone who can produce that signature. After a curve break, that is anyone. A phrase made from fresh randomness has no link to any Ed25519 key, so nothing an attacker learns about your wallet helps with your vault.
| stays in page memory | public, on chain |
|---|---|
| entropy, the 24 words, the passphrase | root |
| bip39, seed, eseed, sk_seed, sk_prf | pub_seed |
| every sk[k][i] | height, epoch, and the vault address the epoch 0 key derives |
Building a height-10 tree takes about one second in a browser (947 ms measured in a Worker, so the page does not freeze). A rotation builds one more tree. The words are shown once and never stored. Our server receives the roots, the public seeds, the heights and the vault address, all of which are public on chain anyway.
Fingerprint. The first 8 characters of base58(genesis root). It does not change when the vault rotates. The site shows it when you type a phrase, so you can see which vault the words open. It is a display aid of about 47 bits, enough to tell vaults apart, not proof against someone grinding a look-alike.
The strings read entangle/pqvault/v1 because Entangle was this product's name when the protocol was written. They are hashed into every root, address and signature. Changing one byte would give every phrase a different vault, so they stay.
Optional: derive from a wallet signature
seed = SHA256(signature)
the wallet's Ed25519 signature
over this exact text:
"Hashlock PQ key v1\nThis signature derives
your vault key. Only sign on Hashlock."
One string, set on two lines here. Its only
real line break is the \n; the other is a space.
This mode is not post-quantum. The vault is exactly as strong as the wallet that signed. The app labels it "Wallet-derived, not post-quantum" and such a vault should not be described as sealed against a curve break.
7The vault program
pqvault is a native Solana program, written without a framework. Program id: not published on this network yet. All of its accounts are program derived addresses with fixed little-endian layouts. Byte 0 is the account kind and byte 1 is the layout version, which is 2. The program and the client decoders refuse any other version.
Vault, 160 bytes
Seeds ["vault", genesis_root, genesis_pub_seed, [genesis_height]].
| offset | field | type |
|---|---|---|
| 0 | kind = 1 | u8 |
| 1 | version = 2 | u8 |
| 2 | genesis_root | [32] |
| 34 | genesis_pub_seed | [32] |
| 66 | genesis_height | u8 |
| 67 | root, the active key | [32] |
| 99 | pub_seed, active | [32] |
| 131 | height, active | u8 |
| 132 | epoch | u32 |
| 136 | next_leaf | u32 |
| 140 | created_slot | u64 |
| 148 | rotated_slot | u64 |
| 156 | successor_count | u16 |
| 158 | bump, owner_bump | u8, u8 |
A vault stores two keys. The genesis fields never change and are the address seeds. The active fields are the key signatures are checked against; a rotation replaces them, adds one to epoch, resets next_leaf to 0 and records the slot. At creation the two are equal.
The address commits to the full genesis public key. With only the root in the seeds, someone could create your vault first with the right root and a wrong public seed, leaving an address nobody can sign for. With all three in the seeds, a stranger can still pay to create your vault, but only the correct one. The address never changes afterwards.
Owner PDA
Seeds ["owner", vault]. A plain system account with no data and no private key. It holds the vault's SOL and owns the vault's token accounts. This is the address pump.fun pays a sealed coin's creator fees to. Deposits are ordinary transfers and need no instruction. Only the program can sign for it, and it does so only after a valid vault signature. A rotation does not touch it.
Signature buffer, 87 bytes plus the signature
Seeds ["sig", vault, payer, nonce u64]. A temporary account that holds one signature blob while it is uploaded.
| offset | field | type |
|---|---|---|
| 0 | kind = 2, version = 2 | u8, u8 |
| 2 | vault | [32] |
| 34 | payer | [32] |
| 66 | nonce | u64 |
| 74 | epoch when opened | u32 |
| 78 | leaf_index | u32 |
| 82 | total_len | u32 |
| 86 | bump | u8 |
| 87 | data: r, chain values, path | [total_len] |
total_len must equal 32 + 2,144 + 32 x height.
Launch record, 139 bytes
Seeds ["launch", mint]. One per coin, written once.
| offset | field | type |
|---|---|---|
| 0 | kind = 3, version = 2 | u8, u8 |
| 2 | mint | [32] |
| 34 | vault | [32] |
| 66 | fee_config | [32] |
| 98 | launched_slot | u64 |
| 106 | successor_mint | [32] |
| 138 | bump | u8 |
Instructions
| instruction | data | accounts |
|---|---|---|
| 0 init_vault | root[32] pub_seed[32] height u8 | payer (signer), vault, system program |
| 1 open_buffer | nonce u64, leaf_index u32, total_len u32 | payer (signer), vault, buffer, system program |
| 2 write_buffer | offset u32, bytes | payer (signer), buffer |
| 3 close_buffer | none | payer (signer), buffer |
| 4 execute | leaf_index u32, action_bytes | payer (signer), vault, buffer, owner PDA, system program, then the action's accounts |
open_buffer records the vault's current epoch in the buffer and requires an unspent leaf of the active tree. Only execute touches value, and it accepts five actions. Each is a tag byte followed by little-endian fields, and each ends with an expiry slot.
| action | fields | what the program checks |
|---|---|---|
| 0 WithdrawSol | lamports u64, to[32], expiry u64 | The destination account must be the signed to. A system transfer signed by the owner PDA. |
| 1 WithdrawToken | amount u64, mint[32], to_token_account[32], token_program[32], expiry u64 | The token program must be the signed one, and SPL Token or Token-2022. Mint and destination must match the signed values. Mint and source must be owned by that token program. Transfer-hook mints are not supported. |
| 2 RegisterLaunch | mint[32], fee_config[32], expiry u64 | The bonding curve must be pump.fun's own account for the mint, and its creator must be the fee payer or the owner PDA. That stops anyone else from claiming a coin's Launch record. |
| 3 SetSuccessor | mint[32], successor_mint[32], expiry u64 | The Launch record must belong to this vault. A successor can be set once and never changed. |
| 4 Rotate | new_root[32], new_pub_seed[32], new_height u8, expiry u64 | The new height must be 8, 10 or 12. Signed by the current tree. It needs no other accounts. |
The signed message and its digest
The message M is exactly this, 131 bytes in order:
- "entangle/pqvault/v1"offset 0, 19 bytes, ASCII
- program_idoffset 19, 32 bytes
- vaultoffset 51, 32 bytes
- epochoffset 83, u32 LE
- leaf_indexoffset 87, u32 LE
- SHA256(action_bytes)offset 91, 32 bytes
- expiry_slotoffset 123, u64 LE
r = SHA256(toByte(3,32) || sk_prf ||
toByte(leaf_index,32) || M)
made by the signer, sent in the signature
digest = SHA256(toByte(2,32) || r || root ||
toByte(leaf_index,32) || M)
root is the vault's active root
The one-time key signs digest. This is the randomized hashing of RFC 8391: r travels in the signature and the program recomputes the digest from it. Someone who can influence M, for example by supplying a destination address, cannot predict the digest before r is fixed, so a forgery needs a second preimage and not a collision. r is deterministic for a given key, leaf and message, so signing the identical message twice gives the identical signature.
M ties a signature to one deployment, one vault, one epoch, one leaf and one action. The expiry slot is in the message and in the action, and the program requires the current slot to be at most the expiry. The action bytes travel in the instruction itself, so what is signed is exactly what runs.
What execute does, in order
- Checks the payer is a writable signer, the vault is a program-owned version 2 Vault whose address re-derives from its genesis fields, and the owner PDA re-derives.
- Requires
leaf_index < 2^heightandleaf_index >= next_leaf. - If the action is not Rotate and the leaf is the last one,
2^height - 1, fails withLeafReserved. - Requires
slot <= expiry_slot. - Checks the buffer is program-owned and bound to this vault, this payer, the vault's current epoch and this leaf, with the length the active height implies.
- Reads
r, rebuilds M and the digest, then recomputes the 67 chain ends, the leaf and the root. If the root is not the vault's active root, it fails withBadSignature. - Sets
next_leaf = leaf_index + 1and writes it before calling any other program. - Performs the action.
- Closes the buffer and returns its rent to the payer.
One-time key enforcement
The vault account stores next_leaf. A signature for any leaf below it is refused with LeafAlreadyUsed, and every accepted signature moves it past the leaf just spent. Within an epoch the counter only goes up. Skipping leaves is allowed, going back is not. Replay is closed by the rising counter, the epoch, the program id and vault inside the message, the active root inside the digest, and the expiry slot.
The chain cannot see a signature that was uploaded and never executed. Those bytes are public from the moment they land, even if the buffer is later closed. So the client never reuses a leaf after any failed or abandoned attempt: it takes the highest of the vault's next_leaf, every live buffer's leaf plus one for the current epoch, and a mark the site keeps on the server per vault and epoch. That mark is a leaf number, not a secret.
Rotation and the reserved leaf
A sealed coin pays the same owner PDA for as long as it trades, and one tree signs a fixed number of times. Rotate gives the vault a fresh tree without moving anything: the active root, public seed and height are replaced, next_leaf becomes 0, epoch goes up by one and rotated_slot is set. The genesis fields, the vault address, the owner PDA, every balance, every locked fee split and every Launch record stay exactly as they are.
The last leaf of every tree can only sign a Rotate. Any other action on that leaf fails, so a vault cannot spend its way out of the ability to rotate. A height-10 tree therefore has 1,023 keys for ordinary moves and one for rotation. The client suggests rotating when 16 or fewer usable keys remain.
After a rotation the old tree authorizes nothing. Its signatures carry the old epoch in M and the old root in the digest, and its buffers carry the old epoch, so all three checks fail.
The program cannot know whether anyone holds the secrets behind a new root. A rotation to a root nobody can sign for would lock the vault for good. The client only rotates to the next epoch's key derived from the same seed, and refuses a next key that lacks its secrets or has the wrong epoch number. The rotation message for a given leaf is always the same bytes, with no expiry by default, so retrying it on the reserved leaf is safe.
Three transactions, and why
A Solana transaction is at most 1,232 bytes and the signature blob alone is 2,496. So every execute is three transactions, sent in order, each confirmed before the next.
open_buffer
Creates the buffer for this vault, payer and leaf, and writes the first chunk of the signature.
write_buffer
Writes the middle of the signature.
execute
Sets the compute budget, writes the last chunk, verifies, acts, and closes the buffer.
The verification itself is not split. It fits one transaction with room to spare, so there is no half-verified state on chain. At height 10 every action fits three transactions, including a token withdrawal that also creates the destination token account.
Measured cost
Real transactions on a local validator at height 10, layout version 2, measured 2026-10-09. The rows differ mostly because each signature has a different digest.
| execute | CU used | CU requested |
|---|---|---|
| WithdrawSol | 384,080 | 442,693 |
| WithdrawToken, SPL Token | 347,635 | 412,563 |
| WithdrawToken, Token-2022 | 357,761 | 431,020 |
| RegisterLaunch | 351,925 | 419,549 |
| SetSuccessor | 391,679 | 451,922 |
| Rotate | 350,134 | 404,628 |
CU = 62,500 + 750 x height
+ 535 x steps + action allowance
steps = sum(15 - d[i]) over the 67 digits
requested = 1.15 x CU
action allowance:
withdrawSol 10,000 withdrawToken 48,000
registerLaunch 30,000 setSuccessor 10,000
rotate 9,000
The client computes the step count from the signature's digest and requests exactly that. The worst possible digest needs 990 steps: about 607,647 units measured for a SOL withdrawal at height 10, and 746,523 requested in the model's worst case at height 12, under Solana's 1.4 million limit.
| account | bytes | rent, 2026-10-09 |
|---|---|---|
| Vault | 160 | 0.00146 SOL, once |
| Launch record | 139 | 0.00136 SOL, once per coin |
| Signature buffer, height 10 | 2,583 | 0.0138 SOL, returned when execute closes it |
| Program | 134,720 | about 0.685 SOL, paid at deploy |
8Sealed launch
A sealed launch is two wallet prompts after the vault exists. Every transaction is signed by your own wallet. Our server builds them and holds no key.
- stepyour walletvault programpump.fun
Create the vault, once
The browser makes the 24 words and builds the tree.
your walletvault programpump.funinit_vaultstores the root, public seed and height in a new Vault account. One vault serves every coin you launch.Create the coin and make the dev buy
pump.fun's create and buy, with your wallet as the coin's creator. The dev buy lands in your wallet for now.
prompt 1 your walletvault programpump.funRegister the launch with a vault signature
Signed in the same prompt, sent once the coin exists. Your next one-time key signs
prompt 1, three transactions your walletvault programpump.fun, read onlyRegisterLaunch(mint, fee_config). The program verifies it against your vault's root, checks that pump.fun's bonding curve names the fee payer as creator, writes the Launch record and advancesnext_leaf.Lock the fee split
Any creator fees already earned are swept first. Then pump.fun's fee-sharing config is created with the creator share to the vault's owner PDA and the platform share to Hashlock's platform wallet, and its admin is revoked. From here nobody can edit the split, including you and us.
prompt 2 your walletvault programpump.funMove the dev bag into the vault
The same prompt transfers your whole dev-buy balance to the owner PDA's token account. It is an ordinary token transfer in. Getting it out again takes a vault signature.
prompt 2 your walletowner PDA receivespump.funThe chain now says sealed
The site reads the Launch record and the fee-sharing config and shows the Sealed badge. Nothing is switched on by hand.
your walletLaunch recordlocked split
What sealed means, exactly
A coin is sealed when all three hold on chain:
- the Launch record at
["launch", mint]exists, is owned by the vault program and names the mint; - pump.fun's fee-sharing config at
["sharing-config", mint]has its admin revoked; - in that config, the vault's owner PDA holds at least 5,000 of the 10,000 basis points.
A Launch record alone is not enough. A creator could register a launch and then lock the fees to an ordinary wallet, so the split is checked directly against the vault the record names. The program stores the fee_config address it was given but does not inspect it, which is why the rule reads pump.fun's account itself.
The coin page and the fee pages read those accounts from chain. The board lists many coins at once, so it uses a cache of what the chain said, refreshed in the background, and a coin is never listed as sealed on the strength of our database alone. The dev bag is reported separately: it is sealed while it sits in the vault's token account, and the vault page shows the balance.
Between steps 2 and 5 the coin is not sealed. Fees earned in that window go to your wallet's creator balance until the split locks, and the dev buy is in your wallet until it is moved. If you stop halfway, every page reminds you to finish, and the coin carries no Sealed badge until the chain shows all three conditions.
The successor record
A vault can name a successor mint for a coin it launched, once, with SetSuccessor. If Solana one day moves to post-quantum accounts, holders can see in advance, on chain, where the creator says the coin continues. Claiming a successor is not built yet. The record can be made today so that it predates any break.
9Fees and the flywheel
Reading the live split from this site's configuration.
The split lives in pump.fun's fee-sharing config for each coin and is locked at launch. A coin keeps the split it was launched with: changing the site's setting later affects new launches only. pump.fun's own trading fees are separate and are set by pump.fun.
The platform wallet is an ordinary Solana wallet, signed through Privy's server wallets. No private key for it exists in our code, environment or database. It is not sealed. It holds only Hashlock's own share on its way to a burn.
Paying out is permissionless. Anyone can trigger pump.fun's sweep and distribute for a coin, and it pays every recipient in the config at once, never the caller. The site does it on a timer, and a creator can do it from the vault page for about 0.00001 SOL in network fees.
10Pairing
A coin can be quoted in anything pump.fun accepts and nothing else: SOL, USDC, the tokens on pump.fun's quote list such as tokenized stocks and large memecoins, and pump coins paired with one of those. A mint pump.fun refuses would fail on chain, so the launcher checks first.
The dev buy is always paid in SOL. For a pair that is not SOL, the launcher adds a swap from SOL into the quote token and your wallet signs it in the same prompt as the launch. Before your wallet sees it, the server simulates the swap and refuses it unless the only thing your wallet can lose is the dev buy amount plus fees.
Sealing works the same for every pair. Creator fees in a token quote arrive in the owner PDA's token account for that token and leave by WithdrawToken. Both SPL Token and Token-2022 quotes are supported. A token with a transfer hook is not, because the program forwards no extra accounts, so the site will not let you seal one.
11Program upgrades
Whoever can upgrade the vault program can replace the code that checks signatures. That is a classical path around every vault, so it is the most important thing to know about a deployment. The site does not state an intention. It reads the program's ProgramData account and reports what the chain says.
No upgrade authority
The program can never change. Your vault depends only on SHA-256 and on Solana running. A bug could not be fixed either.
Several signers and a delay
An upgrade needs a threshold of separate keys and waits a public delay on chain. You can see a pending change and withdraw before it runs. The signers are still Ed25519 keys.
One address
One key can replace the program with no notice. Treat a vault under such a program as protected by that key, not by hashes.
A multisig counts as a multisig only when it can be proven: the upgrade authority must be a vault address of a Squads multisig, and the threshold and delay are read from that multisig's account. A multisig that one signer can pass, or whose settings one authority can rewrite, is reported as a single key.
The multisig arrangement has been exercised on devnet with throwaway keys: a Squads multisig of 2 of 3 signers with a 14,400 second (4 hour) time lock holds the devnet program's upgrade authority, and an upgrade sent by the previous single key was refused by the loader. A no-op upgrade was then proposed and approved by two of the three members, and executing it one second after approval was refused by Squads because the time lock had not been released. It has not been executed after the time lock. That run shows the mechanism on devnet. It says nothing about any other network: the status of this deployment is the live line above, read from chain.
/verify reads the same ProgramData account from your own RPC, so you do not have to take this section's word for it.
12Recovery without Hashlock
A vault does not depend on this site. Everything needed to move its funds is the 24 words, the program on chain, a Solana RPC and any wallet to pay the network fee. The program id is not published on this network yet; keep it with your words. The offline file asks for it if it was saved before the program was published.
- /recover is a stand-alone page. It has no sign-in, calls no Hashlock API, and talks only to the RPC you type. It rebuilds your keys from the phrase, picks an unused one-time key and sends a withdrawal.
- The offline file is the same page as one HTML file with all of its code inside. Save it now. It opens from disk and works if this domain is gone.
Without the site there is no server-side leaf mark, so the recovery page reads the epoch and next_leaf and scans for live signature buffers itself. If your RPC does not allow that scan, it tells you.
13Security analysis
Forgery
To move value without the phrase, an attacker has to make execute accept a signature the owner never made, for a leaf that is not yet spent. There are three ways to try, and each is a SHA-256 problem.
- Walk a chain backwards. For an unused leaf nothing has been revealed, so every chain needs a preimage from its public end. For a leaf with one known signature, the checksum forces at least one chain to go backwards.
- Reach the root another way. A different leaf or a different path that hashes to the same root is a second preimage of a keyed, masked hash.
- Reuse a signature for another action. The signed digest fixes the program, vault, epoch, leaf, action and expiry, and it is randomized by
r. A different action with the same digest is a second preimage, even for someone who chose part of the action you signed.
A second preimage of SHA-256 costs about 2256 classically and about 2128 with Grover.
Key reuse
This is the main operational risk of any one-time scheme. The program refuses a spent leaf, but a signature is public as soon as its bytes are uploaded, before execute runs. The randomized digest does not change that. If an upload is abandoned and the same leaf of the same epoch later signs a different message, both signatures are on chain and an attacker can combine them. The client's rule is simple: a retry with the identical action and expiry is safe because signing is deterministic, and anything else moves to a new leaf. A skipped leaf costs one key of the current tree. A reused one can cost the vault.
Losing or leaking the phrase
The 24 words are the key for every epoch. Anyone who reads them, or reads page memory while a vault is open, controls the vault. If you lose them, nothing can move the funds: there is no reset, no support override and no second key, by design. A locked fee split keeps paying a vault whose phrase is lost, and those fees stay there.
If our server is compromised
| it cannot | it can |
|---|---|
| Sign for a vault. It never has the phrase, the seed or any one-time key. | Serve altered page code that copies the phrase when you type it. This is the serious one. The offline recovery file, saved in advance, does not load code from us. |
| Move anything a vault holds, or change a Launch record. | Show a wrong Sealed badge. /verify and the code in section 15 read the chain without us. |
| Edit a locked fee split. | Build a launch that does not do what the page says. Your wallet shows each transaction, and the result is checkable on chain before you promote the coin. |
| Make the program accept a spent leaf. | Report too low a leaf mark. The client also reads next_leaf, the epoch and live buffers from chain, so this only matters for a signature that was uploaded, abandoned and closed, on a browser that lost its own record. |
| Stop you withdrawing. The recovery page needs only an RPC. | Go offline, or refuse to relay your transactions. |
What is still Ed25519
- The fee payer. Any wallet can pay for a vault transaction. It can refuse to send or try to send first. Someone who copies an uploaded signature and executes it gets the identical result and pays the fee.
- pump.fun. The bonding curve, the pool and the fee-sharing program are pump.fun's, upgradeable by pump.fun. Fees are theirs to route until they land in the owner PDA.
- The vault program's upgrade authority, while one exists. See section 11.
- Solana's validators and transaction signatures.
- Your wallet, for everything outside the vault, including the SOL that pays fees.
Capacity and rotation
A height-10 tree signs 1,023 ordinary moves: every withdrawal, launch registration, successor record and skipped leaf spends one. The last key is reserved for a rotation, so the count is per epoch and not for the life of the vault. A vault that rotates gets a fresh tree from the same 24 words and keeps its address, its balances, its locked fee splits and its Launch records.
A locked fee split cannot be pointed at a new vault, and with rotation it does not need to be: the owner PDA a coin pays never changes. The vault page shows the keys left in the current epoch, and /verify shows the epoch, the keys used and the slot of the last rotation.
Two things stay with the owner. A rotation is a vault signature like any other, so someone holding the phrase can rotate, and someone who could forge a signature could rotate the vault to their own key, though they could equally withdraw. And a rotation to a root nobody holds the secrets for would lock the vault; the client only rotates to the next epoch's key from the same seed.
Scope
Sealing determines who can move a coin's creator fees and dev bag. A creator with the phrase can withdraw and sell with a valid vault signature. Tokens that holders keep in ordinary wallets are outside the vault; any holder can open a vault and deposit.
14Parameters
| parameter | value |
|---|---|
| Hash function | SHA-256, n = 32 bytes |
| Scheme | WOTS+ and an XMSS-style tree, RFC 8391, SHA2_256 |
| Winternitz w | 16 |
| Chains per key (len1 + len2) | 67 (64 + 3) |
| Tree height | 10 on the site; 8, 10 or 12 accepted |
| One-time keys per epoch | 1,024: 1,023 usable, 1 reserved for Rotate |
| Epochs per vault | u32 counter, one per rotation |
| Message randomizer r | 32 bytes |
| One-time signature | 2,144 bytes |
| Authentication path | 320 bytes |
| Signature blob | 2,496 bytes |
| Public key (root, public seed, height) | 65 bytes |
| Phrase | 24 words, 256 bits, BIP39 English |
| Phrase stretching | PBKDF2-HMAC-SHA512, 2048 rounds |
| Message domain | entangle/pqvault/v1 |
| Signed message M | 131 bytes |
| Account layout version | 2 |
| Vault / Launch / buffer header | 160 / 139 / 87 bytes |
| Transactions per vault move | 3 |
| Verifier chain steps, worst case | 990 |
| Second preimage work, classical / Grover | 2^256 / about 2^128 |
| Sealed: owner PDA share, minimum | 5,000 of 10,000 bps, locked |
| Platform share of creator fees | read at load |
| Part of the platform share that burns | read at load |
| Vault program id | not published on this network yet |
15Verify it yourself
/verify runs six checks on any mint, straight from an RPC you choose: the Launch record, the vault, the fee split with every recipient, the dev bag, the program's upgrade authority, and the epoch with the keys used and left in it. It shows each raw address with a link to Solscan.
Live figures
/proof is a board of the site's live figures: vaults, sealed coins, what the vaults hold, one-time keys spent, fees collected and $HASHLOCK burned, each linked to the account or transaction behind it. To check one coin without relying on any page of ours, use the verifier above or the function below.
In code
Or skip our pages. This is the whole sealed rule in one function, using the program's client modules and nothing from our API. It throws if a record is missing.
import { launchPda, ownerPda, decodeLaunch, decodeVault }
from '/pq/client/ix.js';
import { Connection, PublicKey } from '/pq/client/web3.js';
// the page needs the import map at /pq/client/importmap.json
const PROGRAM = new PublicKey('PASTE_THE_VAULT_PROGRAM_ID');
const PUMP_FEES = new PublicKey(
'pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ');
export async function checkCoin(mint, rpcUrl) {
const rpc = new Connection(rpcUrl, 'confirmed');
const read = async (address, program) => {
const a = await rpc.getAccountInfo(address);
if (!a || !a.owner.equals(program))
throw new Error('missing: ' + address.toBase58());
return a.data;
};
// 1. the Launch record names the vault
const launch = decodeLaunch(
await read(launchPda(PROGRAM, mint), PROGRAM));
const vault = decodeVault(await read(launch.vault, PROGRAM));
const owner = ownerPda(PROGRAM, launch.vault);
// 2. pump.fun's split: locked, and paying the owner PDA
const [config] = PublicKey.findProgramAddressSync(
[new TextEncoder().encode('sharing-config'),
new PublicKey(mint).toBytes()], PUMP_FEES);
const d = await read(config, PUMP_FEES);
const view = new DataView(d.buffer, d.byteOffset, d.length);
const locked = d[75] === 1;
let ownerBps = 0;
for (let i = 0, o = 80; i < view.getUint32(76, true); i++, o += 34)
if (owner.equals(new PublicKey(d.subarray(o, o + 32))))
ownerBps += view.getUint16(o + 32, true);
return {
sealed: locked && ownerBps >= 5000,
vault: launch.vault.toBase58(),
owner: owner.toBase58(),
ownerBps,
epoch: vault.epoch,
keysLeft: 2 ** vault.height - 1 - vault.nextLeaf,
};
}
The client modules are served from this site at /pq/client and are the same files the app and the recovery page use. To check a signature rather than an account, verify.js in the same folder re-implements the on-chain check in about twenty lines.