Beneficiary id derivation
All hashes are keccak256 over abi.encode (not abi.encodePacked).
Constants
| Name | Value |
|---|---|
ID_TAG | keccak256("zkpad.beneficiary.v1") = 0x681d809cd5c3fa518f3628c2d65d0849116ca6752686d72a4d022bd9abfaa3f9 |
KIND_STEALTH | 1 |
KIND_HANDLE | 2 |
MIN_FALLBACK_DELAY | 180 days = 15552000 seconds |
Id
id = keccak256(abi.encode(
bytes32 ID_TAG,
uint8 kind,
bytes32 keyHash,
address fallbackRecipient, // address(0) disables the fallback
uint64 fallbackDelay
));
The fallback is committed inside the id, so no third party can attach a different fallback to
someone else's id. On-chain the effective delay is max(fallbackDelay, MIN_FALLBACK_DELAY). The
SDK's assertFallback rejects a non-zero recipient with a delay below 180 days.
Stealth key hash (kind = 1)
keyHash = keccak256(abi.encode(address stealthOwner, bytes32 salt));
stealthOwner is a fresh key (EOA or ERC-1271 account) generated client-side; salt is 32
random bytes. FeeVault: stealthKeyHash(owner, salt).
Handle commitment (kind = 2)
keyHash = handleCommitment = keccak256(abi.encode(
uint8 platformId, // 1 X, 2 Telegram, 3 GitHub, 4 Farcaster, 5 Discord
bytes32 userIdHash, // keccak256(utf8(immutable numeric user id))
bytes32 attestorSalt, // HMAC-SHA256(pepper, platformId ":" userId), returned to the creator, carried in the encrypted hint
bytes32 nonce // fresh 32 random bytes per escrow: this is what hides the account
));
- It binds the platform's immutable user id, not the handle, so renamed or resold handles
cannot claim (BUILD_SPEC §3.1,
docs/research/social-beneficiaries.md). - The
IFeeVaultNatSpec still calls the second fieldhandleHash;docs/BUILD_SPEC.md(which wins) and the SDK use the user-id hash. - The commitment is built off-chain by the attestor's
/v1/commitmentendpoint, which returns the salt and the nonce. Both go into the encrypted hint (hintFromCommitment) so every attestor of the committee can recompute the commitment. The salt is not secret from the hint's readers; the randomnonceis what defeats dictionary attacks.
SDK
import {stealthId, stealthKeyHash, computeId, handleCommitment, userIdHash, handleId, KIND_STEALTH} from '@zk-pad/sdk';
const id1 = stealthId(owner, salt, fallbackRecipient, fallbackDelay);
const id2 = computeId(KIND_STEALTH, stealthKeyHash(owner, salt), fallbackRecipient, fallbackDelay);
const commitment = handleCommitment(1, userIdHash('783214'), attestorSalt, nonce);
const id3 = handleId(commitment, fallbackRecipient, fallbackDelay);
The vault exposes computeId(kind, keyHash, fallbackRecipient, fallbackDelay) as a pure
function for cross-checking.