Skip to main content

Beneficiary id derivation

All hashes are keccak256 over abi.encode (not abi.encodePacked).

Constants​

NameValue
ID_TAGkeccak256("zkpad.beneficiary.v1") = 0x681d809cd5c3fa518f3628c2d65d0849116ca6752686d72a4d022bd9abfaa3f9
KIND_STEALTH1
KIND_HANDLE2
MIN_FALLBACK_DELAY180 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 IFeeVault NatSpec still calls the second field handleHash; 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/commitment endpoint, 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 random nonce is 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.