Skip to Content
ConceptsNote Delivery

Note Delivery

A note created for you is just a leaf in a Merkle tree. Nothing about it says “this belongs to that address.” So two problems need solving: how a sender finds out what to build the note against, and how you find out it exists.

OwnerKeyRegistry — finding a recipient

Building a note for someone requires their public ownerKey. A wallet address alone doesn’t reveal it, since deriving it needs that wallet’s own signature.

OwnerKeyRegistry is a small standalone contract where a wallet publishes its ownerKey once:

function register(uint256 ownerKey) external; function isRegistered(address account) external view returns (bool); mapping(address => uint256) public ownerKeyOf;

pay looks the recipient up here before building the output commitment. If a recipient hasn’t registered, the app refuses rather than inserting an unspendable leaf.

The contract is deliberately standalone — ShieldedVault never reads it. It is a lookup helper for callers building proof inputs off-chain. There is also no removal or rotation function: a wallet can always overwrite its own entry with a fresh ownerKey, and the contract never has to reason about which one is “current” beyond the latest value on file.

See the Receive guide for the user-facing flow.

StealthAnnouncer — discovering an incoming note

Once a note exists on-chain, its recipient still needs the private values — the blinding in particular — to ever spend it.

StealthAnnouncer is an on-chain announcement log following the EIP-5564 Announcement event shape:

event Announcement( uint256 indexed schemeId, address indexed stealthAddress, address indexed caller, bytes ephemeralPubKey, bytes metadata );

Anyone may announce. Umbra uses two scheme IDs of its own:

Scheme IDCarries
2A pay-created note: assetId, amount, blinding, commitment
3A partial fill’s residual order: assetIn, assetOut, amountIn, minAmountOut, blinding, commitment, originalAmountIn

Scheme ID 1 is left alone — it is reserved by the contract’s own documentation for real EIP-5564 secp256k1 announcements.

This is not full EIP-5564 stealth addressing. There is no ephemeral keypair ECDH here, and stealthAddress is the recipient’s ordinary wallet address, not a fresh one-time destination. Metadata is announced in the clear rather than ECDH-encrypted.

That follows directly from the reused-ownerKey note scheme (see Shielded Notes) rather than per-payment stealth addresses. It adds no new privacy loss beyond what pay already exposes on-chainassetId, amount, and commitment are already public inputs on the pay() call itself. The blinding is the only genuinely new value revealed, and on its own it doesn’t let anyone but the spendingKey holder spend the note.

Residual orders come back to you automatically

When a partial fill leaves a residual, it is announced on scheme ID 3 and — since the matcher already holds every detail needed to keep it tradeable — re-listed on the order book immediately. You don’t have to notice and resubmit it.

Scanning

Your wallet reconstructs its notes by scanning the chain’s full event history from the vault’s deploy block, matching announcements and commitments against your derived keys. Results are cached in IndexedDB.

That means clearing site data is recoverable — the scan rebuilds everything — but the first scan on a new device walks a lot of history and takes a moment.