How UNLINKED works
Stealth addresses, shaped after ERC-5564 and ERC-6538. Nothing here is novel. The value is that it is small, checked, and running.
What it is
On Robinhood Chain a wallet is a public brokerage account. Every stock token and memecoin you hold is visible, and so is every payout that lands on you. Each one ties money to your name.
UNLINKED breaks that link. A payer derives a fresh, one-time address from keys you published once. The money lands there. Only you can spend it, and nothing on chain says the address is yours.
This is not anonymity and does not pretend to be. The payer still knows who they paid. Funds are never pooled and never mixed. What disappears is the public link between a payment and its owner.
The four steps
1 · Register
You sign one message. Its hash derives two private keys: a spending key and a viewing key. Their public halves form your meta-address, which you publish once.
2 · Pay
The payer picks a random number, combines it with your viewing public key, and derives a one-time address from your spending public key. Funds go there; the random number's public half becomes an announcement.
3 · Find
You scan announcements with your viewing key. A one-byte tag discards 255 of every 256 strangers' announcements before any curve maths runs.
4 · Spend
Your spending key plus the shared secret gives the private key to that one-time address. The viewing key alone cannot produce it — so a scanning service can find your payments without being able to take them.
Keys
spend = H("posit:spend:" ‖ first half of the signature) mod n
view = H("posit:view:" ‖ second half of the signature) mod n
metaAddress = (spend·G) ‖ (view·G) // 66 bytes
Deriving from a signature means the keys follow the wallet: the same wallet on another machine reproduces them, and nothing needs a separate backup. The cost is that whoever obtains that one signature obtains both keys, so the message is fixed, specific to UNLINKED, and grants nothing.
Paying
R = r·G // ephemeral key, announced
secret = H( (r · viewPub).compressed ) // shared secret
stealth = spendPub + H(secret)·G // one-time public key
address = last 20 bytes of H(stealth.uncompressed)
viewTag = secret[0] // one byte
Finding
secret = H( (view · R).compressed )
r·viewPub and view·R are the same point, so both
sides reach the same secret without ever talking. If the first byte differs
from the announced tag, the announcement is not yours and scanning stops
there.
Spending
stealthPrivate = (spend + H(secret)) mod n
Contract surface
function setMetaAddress(bytes calldata metaAddress) external;
mapping(address => bytes) public metaAddressOf;
function sendEth(address stealthAddress, bytes calldata ephemeralPub,
bytes1 viewTag) external payable;
function sendToken(address stealthAddress, address token, uint256 amount,
bytes calldata ephemeralPub, bytes1 viewTag) external payable;
event Announcement(
address indexed stealthAddress,
address indexed token,
address indexed sender,
uint256 amount,
bytes ephemeralPub,
bytes1 viewTag
);
Funds move straight from payer to the one-time address in the same transaction as the announcement. The contract never holds a balance — a fuzz test asserts that over 256 runs.
Threat model
Hidden from a chain observer: that a given payment belongs to you. Each payment lands on an address with no history and no link to your meta-address.
Not hidden:
- From the payer. They computed the address and know who they paid.
- Amounts and timing. Both public, as on any transparent chain.
- Your withdrawal. Moving funds to an address already yours re-links them. Spend from the stealth address directly, or move to a fresh destination.
- Gas funding. Paying the stealth address's gas from your main wallet links them immediately. This is the sharp edge until a relayer exists.
- Correlation. One payment of an unusual size, at an unusual time, on a quiet chain, can be linked by inference alone. Privacy here comes from the link being cut, not from a crowd.
Status
| Piece | State |
|---|---|
| Stealth address cryptography | done · 9 checks |
| Contract — registry + announcements | done · 12 tests incl. fuzz |
| End-to-end on a real EVM | passes · 7 steps |
| Web app — register, send, find | done |
| Deployed to Robinhood Chain | not yet |
| Relayer for gasless withdrawal | not yet |
Until the contract is deployed the app generates real addresses and real keys, but announcements travel out of band instead of on chain.
A stealth address receives tokens but holds no native coin, so it cannot pay for its own withdrawal, and funding it from a linked address undoes the privacy. The answer is a relayer that submits the transaction and takes its fee in the token being moved. That is required by the mechanics, not an add-on.
Run it yourself
# the protocol
node site/test/stealth.test.mjs
# the contract
cd contracts && forge test
# the whole loop against a local chain, seven steps end to end
cd contracts && ./e2e.sh
The cryptography ships with the page instead of loading from a third-party host, because a privacy tool that fetches its own maths from someone else's server is not one.