Skip to main content

Beneficiary ids, stealth keys and relayers

Beneficiary ids​

A beneficiary in zk-pad is a bytes32 beneficiaryId in the FeeVault. It is a hash of the information needed to claim:

id = keccak256(abi.encode(ID_TAG, kind, keyHash, fallbackRecipient, fallbackDelay))
KindkeyHashWho can claim
Stealth key (kind = 1)keccak256(abi.encode(stealthOwner, salt))Whoever holds the stealth private key
Social-account escrow (kind = 2)a salted handleCommitment to a platform accountThe account owner, after an attestor bind (not possible until attestors are registered)

Because the id is a hash with a random salt, nobody can work out the owner or the key from it. The fallback settings are inside the hash too, so nobody can attach a different fallback to someone else's id. Exact encodings are in beneficiary id derivation.

Registration is lazy. Fees can be credited to an id before anyone registers it. The id is registered (its preimage revealed to the vault) only when it is first used, usually in the same relayed transaction as the first claim.

A stealth key is an ordinary secp256k1 key pair generated in the browser by the SDK when the creator launches a coin. It is never funded and never sends a transaction.

The creator receives a claim kit: a claim link and a printable QR code.

https://zkpad.family/claim#v1.<base64url(json)>
  • The key material is only in the URL fragment (after #). Browsers do not send the fragment to servers, so the web server hosting the claim page never sees it.
  • Whoever has the link can claim. Treat it like cash. The creator passes it to the beneficiary through a private channel. A "myself" launch keeps it with the creator.
  • The beneficiary should rotate the key after receiving it, so the creator no longer holds a working key.
The creator knows the first key

For a stealth-key beneficiary, the creator generated the key and could have kept a copy. Until the beneficiary rotates, the creator can claim. Rotating moves control to a key only the beneficiary has.

Signed actions​

The stealth key (the account's current owner) controls the account only through EIP-712 signatures:

ActionWhat it does
ClaimWithdraw an amount of one asset (or the whole balance, amount = 2^256 − 1) to a destination (direct address or adapter), paying a relayer fee
RotateOwnerReplace the owner key. Also discards every unused shield template
PingProve the beneficiary is still active (resets the inactivity clock)
CancelBindCancel a pending rebind of a social-escrow account
ConsolidateSwap a balance into USDT with the beneficiary's own minOut
RegisterShieldTemplatesPre-register single-use Railgun destinations for creator-triggered shields
ClearShieldTemplatesDiscard every unused template

Every signature carries a per-account nonce and a deadline, and is bound to the vault's chain and address. The type strings are in EIP-712 typed data. The owner can also be a smart-contract account that validates signatures with ERC-1271.

Relayers​

A relayer is a service that receives a signed action, checks it, simulates it, and submits it on-chain, paying the gas.

  • The Claim names the relayer and its fee. The fee (at most the claimed amount) is paid in the claimed asset, to the relayer, in the same transaction.
  • Other owner actions (consolidate, register templates, rotate, ping) are paid with a fee-only claim: a second signed Claim that pays only the relayer's fee and runs in the same transaction, so the action and its payment succeed or fail together. Bind actions are free.
  • With relayer = address(0) anyone may submit, and the fee goes to whoever submits.
  • A relayer cannot change anything in a signed claim: not the amount, the destination, the adapter payload (signed by hash) or its own fee.
  • The FeeVault supports multicall, so a relayer can register an id and claim in one transaction.

What a relayer does learn: your IP address (unless you use Tor or a similar network), the timing of your request, and the claim contents, which become public on-chain anyway. The SDK fetches account data, including the nonce to sign with, from a fetch-everything endpoint, downloading every id's balance and filtering locally, so neither the relayer nor a public RPC learns which id you are looking at. Fee quotes carry no amount and transaction status is polled with a POST, so nothing account-specific lands in an access-logged URL.

Inactivity fallback​

A creator can give a beneficiary an optional fallback recipient, typically an NGO address, and a fallback delay of at least 180 days.

  • If the account shows no owner activity (any signed action: claim, consolidate, template registration, rotate, ping, cancel-bind; or a finalized bind) for max(fallbackDelay, 180 days), anyone can call sweepToFallback to send its balances to the fallback recipient. Creator-triggered consolidations are not owner activity.
  • The clock is set at the first deposit and restarted at registration. Since a sweep needs a registered id, in practice the delay runs from registration (or from the last owner action). Fee deposits from trading do not reset it; only owner actions do.
  • The id must be registered for a sweep to work, because the vault only learns the fallback recipient from the registration. Registration is permissionless but needs the id's preimage, so a creator who sets a fallback should register the id right after launch (the launch success page offers a relayed registration). Registering a stealth id reveals the stealth owner's address and salt, which is harmless because that key is never funded.
  • A sweep is blocked while a social-escrow bind is pending, so a beneficiary who shows up at the last moment cannot be raced. A veto of a pending bind keeps the sweep closed for at least 2 × bindDelay, so a vetoed beneficiary has time to be re-attested.
  • The fallback is committed inside the id, so it cannot be changed later.

With a fallback set and the id registered, donation money is never stranded if the beneficiary never claims. Without a fallback, an unclaimed balance stays in the vault indefinitely.