Status: Draft v1 · License: CC0-1.0 · Layer: naming / UX (non-core)
ONS gives a stealth meta-address a human-readable name that resolves on both Ethereum and Solana:
alice.opq.ethreturns the same CSAP meta-address to a MetaMask sender (on-chain ENS wildcard resolution) and to a Phantom sender (on-chain mirror PDA, no Ethereum RPC). One canonical record, one claim, no central server. ONS is a convenience layer over CSAP.md; it adds no privacy properties and no new trust beyond the Wormhole guardian set already used by the UAB.
A CSAP meta-address is 66 bytes (132 hex chars) — too long to share by hand. ENS resolves
.eth only from Ethereum; SNS resolves .sol only from Solana. ONS bridges the gap with a
canonical registry on Ethereum (an ENSIP-10 wildcard resolver serving *.opq.eth
subnames, sourcing meta-addresses from the deployed ERC-6538 registry) and a read-only
mirror on Solana (PDAs written exclusively by Wormhole VAA from the canonical registry).
Names can be claimed from either chain; Ethereum is always authoritative.
Consistency model (normative): eventually consistent, canonical-chain-wins. There is no atomic cross-chain claim — cross-chain messaging is asynchronous and best-effort, and the Ethereum leg finalizes in ~19 minutes. A claim made on Ethereum is immediately authoritative. A claim made from Solana is provisional until the canonical registry confirms it, and loses to any concurrent direct Ethereum registration. Implementations MUST NOT present a provisional claim as owned (§6).
The production namespace is *.opq.eth (an ENS subname tree — a native .opq TLD is not
mintable on ENS). The parent name is a deployment parameter: testnet deployments develop
against a Sepolia-registered test name, and the parent in force is published in
@opaquecash/deployments. Everything below writes opq.eth for readability.
A v1 label (the alice in alice.opq.eth) MUST match [a-z0-9-]{1,63} and MUST NOT start
or end with -. Resolvers MUST lowercase input before hashing. Unicode normalization
(ENSIP-15) is out of scope for v1; non-LDH labels are rejected at registration.
Two hashes of the same name are used, for the two chains' native keying:
| Hash | Definition | Used by |
|---|---|---|
node |
ENS namehash: keccak256(parentNode ‖ keccak256(label)) |
Ethereum registry storage; ENSIP-10 resolution |
name_hash |
keccak256(utf8(fullName)), e.g. keccak256("alice.opq.eth") |
Solana mirror PDA seeds |
fullName is the lowercase dot-joined full name including the parent.
OpaqueNameRegistry.sol is simultaneously the subname registrar and the ENSIP-10 wildcard
resolver for the parent name. The parent name's ENS resolver record is set to this contract.
struct Record {
address registrant; // ETH controller; for Solana-originated claims, a
// deterministic surrogate (§4.2)
bytes32 solAuthority; // claimer's Solana pubkey (zero if none)
bytes spendPubKey; // 33-byte compressed secp256k1 (empty => source ERC-6538)
bytes viewPubKey; // 33-byte compressed secp256k1
uint64 updatedAt;
}
Registration is first-come-first-served, free at the contract level (gas + Wormhole fee
only). Only the registrant may update or transfer. If spendPubKey is empty, the resolver
sources the meta-address live from the ERC-6538 StealthMetaAddressRegistry
(stealthMetaAddressOf(registrant, schemeId=1)) — names track the registrant's registry
entry with no second write. An explicit record overrides the registry-sourced one.
The contract implements resolve(bytes name, bytes data) (interface id 0x9061b923) over
the DNS-encoded name, answering for any depth-1 subname of the parent:
text(node, "com.opaque.meta")→ the CSAP §2.9 record value:st:opq:+ the0x-prefixed 132-hex-charV‖Sserialisation;addr(node)→ the registrant address (zero for Solana-surrogate registrants).
Resolvers MUST validate both 33-byte halves are valid compressed secp256k1 points before use (CSAP §2.9). Unregistered names revert / return empty.
Every state change (register, update, transfer, revoke) publishes an ONS mirror payload
(§5.1) through the Wormhole Core Contract at consistencyLevel = 200 (finalized), exactly
as the UAB sender does. The registry contract address (left-padded to 32 bytes) is the ONS
Ethereum emitter.
OpaqueNameMirror (Anchor) holds one PDA per name:
seeds = ["ons_mirror", name_hash] // name_hash per §1.3
pub struct OnsRecord {
pub name_hash: [u8; 32],
pub spend_pubkey: [u8; 33],
pub view_pubkey: [u8; 33],
pub eth_owner: [u8; 20],
pub sol_authority: Pubkey, // zero if none
pub wormhole_sequence: u64, // last applied sequence
pub updated_at: i64,
}
Mirror PDAs are written only by receive_record(posted_vaa), which MUST verify the
VAA emitter is the configured (chainId = 2, OpaqueNameRegistry) pair and MUST reject a
sequence ≤ the stored wormhole_sequence (stale/replayed update). There is no
direct-write path; the mirror is read-only from Solana's perspective. A revoke action
closes the PDA.
Resolution from Solana: the SDK derives the PDA from name_hash client-side and reads
one account — no Ethereum RPC, no getProgramAccounts scan, no gateway.
Staleness: a mirror record lags the canonical record by Wormhole end-to-end latency (Ethereum finality ~19 min + guardian signing + relay). Clients that can reach an Ethereum RPC MAY prefer canonical resolution; mirror-only clients accept the lag (§7).
register(label, spendPubKey, viewPubKey) on the registry. The record is live for ENS
resolution in the same block; the mirror PDA follows after relay. One transaction.
OpaqueNameRegistration (Anchor) lets a Solana-only user claim without touching Ethereum:
claim(label, spend_pubkey, view_pubkey)creates a provisional PDA (seeds = ["ons_claim", name_hash], payer = claimer) and CPIs the Wormhole Corepost_messagewith an ONS claim payload (§5.2). Duplicate provisional claims for the samename_hashare rejected while one is open.- The relay delivers the VAA to
OpaqueNameRegistry.registerFromVAA(vaa), which verifies the emitter is the registration program's emitter PDA (seeds = ["emitter"], chainId = 1), validates the label, and — if the name is still free — registers it. The registrant is the deterministic surrogateaddress(uint160(uint256(keccak256(sol_authority))));solAuthorityis stored, and subsequent updates for that name are accepted only via VAA from the same authority. If the name was taken in the interim, the VAA is consumed with no effect (the canonical chain wins) — the claim is lost. - Registration (like any registration) emits the mirror payload. The mirror PDA is the
confirmation signal: a mirror record for
name_hashwithsol_authorityequal to the claimer upgrades the claim to confirmed; one with a different owner proves the claim lost.reconcile(name_hash)on the registration program closes the provisional PDA against the mirror state (or unconditionally for the claimer after the pending window, §6) and refunds rent to the claimer.
ONS payloads travel on dedicated emitters (the registry contract and the registration program), so they are disambiguated from the 96-byte UAB payment payload by emitter, never by sniffing. Both carry a leading version byte. Multi-byte integers are big-endian.
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 1 | version |
1 |
| 1 | 1 | action |
1 = upsert, 2 = revoke |
| 2 | 32 | name_hash |
§1.3 |
| 34 | 33 | spend_pubkey |
compressed secp256k1 (zeroed on revoke) |
| 67 | 33 | view_pubkey |
compressed secp256k1 (zeroed on revoke) |
| 100 | 32 | eth_owner |
registrant, left-padded 20-byte address |
| 132 | 32 | sol_authority |
claimer Solana pubkey; zero if none |
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 1 | version |
1 |
| 1 | 1 | action |
1 = claim |
| 2 | 32 | sol_authority |
claimer Solana pubkey (must equal the claim signer) |
| 34 | 33 | spend_pubkey |
compressed secp256k1 |
| 67 | 33 | view_pubkey |
compressed secp256k1 |
| 100 | 1 | label_len |
L, 1–63 |
| 101 | L | label |
UTF-8, §1.2 charset |
Receivers MUST enforce emitter allowlists and (emitterChain, emitterAddress, sequence)
replay protection exactly as specified in [UAB.md].
A Solana-originated claim moves through four states; clients MUST track and surface them:
| State | On-chain condition | Client behaviour |
|---|---|---|
pending |
provisional PDA exists; no mirror PDA for name_hash |
Show "pending confirmation (~20–40 min)". The name MUST NOT resolve and MUST NOT be shown as owned. |
confirmed |
mirror PDA exists, sol_authority = claimer |
Show owned. Offer reconcile to reclaim provisional rent. |
lost |
mirror PDA exists, owner ≠ claimer — a direct Ethereum registration won the race | Show "name taken on the canonical chain". Offer reconcile (rent refund) and a fresh-name retry. |
expired |
no mirror PDA after the pending window of 24 h | Treat as delivery failure (relay outage / VAA never submitted). Offer reconcile; the user may retry the claim. |
Resolution NEVER serves provisional claims: senders only ever see canonical (ENS) or
mirrored (PDA) records. The pending window exists purely so the claimer's rent is not
locked forever behind a dead relay; closing an expired claim does not release the name on
Ethereum if the VAA later arrives (the registry registers it regardless — the claimer can
re-create the provisional PDA and reconcile to confirmed).
resolveOpaqueMetaAddress(name) in @opaquecash/opaque tries, in order of input shape:
| Input | Path |
|---|---|
alice.opq.eth (parent in force) |
Mirror PDA first (cheap, no ETH RPC); ENS wildcard resolution as fallback / freshness override |
any other *.eth |
ENS text(node, "com.opaque.meta") per CSAP §2.9 |
*.sol |
SNS record com.opaque.meta (Records V2 / TXT) |
| raw meta-address | accepted directly |
All paths MUST point-validate both 33-byte halves (CSAP §2.9). Where canonical and mirror disagree (staleness window), the canonical record wins; mirror-only clients accept the lag bounded by Wormhole end-to-end finality.
- Parent name custody:
opq.ethis held by the protocol multisig; registration and resolution logic is in the registry contract, not the key. (Testnet: deployer-held test name.) - Guardian trust: mirror integrity rests on Wormhole's 13/19 threshold — the same assumption the UAB already makes. If Wormhole halts, each chain's existing records keep resolving; only sync is delayed.
- No gateway: both resolution paths are pure on-chain reads. CCIP Read is not used.
- Squatting: first-come-first-served, canonical-chain-wins. No atomicity is claimed; see §4.2/§6 for the race window and its honest handling.
- The registry MUST restrict updates to the registrant (or, for surrogate registrants,
to VAAs from the recorded
sol_authority). - The mirror MUST reject non-allowlisted emitters and non-increasing sequences; the registry MUST consume each claim VAA at most once.
- ONS adds no privacy surface: a name maps to a meta-address, which is already public in the ERC-6538 registry; payments to it remain unlinkable per CSAP.
- A malicious relay can only delay (liveness), never forge or alter (guardian-signed payloads) — identical to the UAB trust model.
Copyright and related rights waived via CC0-1.0.