Skip to Content

Wallet authority is not wallet ownership — the trust boundaries in Clearproof's wallet verification

2026-11-09 · approved · source revision 796b1597cbd9

A signature proves that a key signed a message at a moment. It does not prove who owns the wallet, who controls it today, or who is allowed to settle a transfer. Clearproof's wallet ownership extension (merged November 2026) is built around that gap: a bound SIWE challenge, separate handling for EOAs and smart accounts, explicit EIP-7702 delegation checks, a supervisor-readable evidence export, and a staged circuit that has no verifier. This explainer walks where the trust boundaries actually sit.

When a Travel Rule workflow needs to tie a wallet to a person or an entity, the tempting shortcut is to ask the wallet for a signature and treat a valid signature as proof of ownership. The wallet ownership extension that landed in PR #127 in November 2026 takes the opposite starting point: a signature establishes that the signing key controlled the wallet at that moment, and everything else — who benefits from the wallet, whether control still holds, whether that amount of authority is even the right question — has to come from somewhere else, and be labeled as coming from there. The reference documentation says it in the first paragraph: this verifies control of an EOA signing key at challenge completion. It does not establish legal ownership, complete regulatory compliance, or settlement authority.

The rest of this explainer walks the boundaries that follow from that framing.

The challenge is the record, not a formality

Enrollment happens first, through the existing pilot credential flow with its wallet-signed consent. Only then can the authenticated tenant request a wallet-ownership challenge — and the caller cannot pick an identity: the wallet and VASP identity come from the enrolled credential the tenant already holds.

With PILOT_SIWE_DOMAIN configured, the challenge is a clearproof-wallet-challenge-v2 record whose message is a deterministic EIP-4361 SIWE message. That choice is doing real work: the signed text itself becomes a supervisor-readable record, because the SIWE fields (domain, nonce, chain ID, issued-at, expiration-time, resources) say what was signed, when, and in what context. The subject address is rendered with its EIP-55 checksum inside the message; protocol records keep lowercase hex.

The signed content binds the wallet's full credential, issuer, tenant, actor, chain, registry, timestamp and a random 256-bit nonce. Nothing in it is reused from another context, and the challenge expires exactly 300 seconds after creation (CHALLENGE_TTL = 300 in src/protocol/wallet_ownership.py). Message preparation on the client tolerates 30 seconds of clock skew in either direction; server verification enforces the exact five-minute interval with no tolerance.

Two boundary rules keep the client honest:

  • The expected context (tenant, actor, credential, chain, registry) must be retained independently from enrollment — never copied from an untrusted challenge. The walletOwnershipSigningMessage helper in @clearproof/proof returns the exact bytes to sign and accepts the expected context as input; it never receives a private key and never submits a transaction.
  • The Python server and the TypeScript helper must agree byte for byte, which is pinned by shared test vectors (challenge-v2.json among them) rather than by convention.

EOAs, smart accounts, and delegation are different paths

The EOA path is deliberately narrow: only canonical 65-byte, low-s signatures with recovery values 27/28 are accepted. malleable encodings are rejected instead of normalized, because an attestation should record one signature, not a class of interchangeable ones.

Contract wallets — and EOAs that delegate through EIP-7702 — are verified through ERC-1271 isValidSignature over the EIP-191 prehash of the SIWE message, or ERC-6492 for counterfactual (not-yet-deployed) accounts. The attestation records which method was used: eoa, erc-1271 or erc-6492. Non-EOA attestations are not re-verified with ecrecover on later reads; the standing code/delegation re-check is the deliberate cheap check instead.

The EIP-7702 delegation check is the part most systems skip, and it changes the outcome rather than decorating it. When WALLET_OWNERSHIP_RPC_URL is configured, verification records the delegation status of the subject wallet at verification time via eth_getCode, with strict 0xef0100 indicator parsing in src/chain/wallet_account.py: code that starts with the indicator but does not match the exact form is malformed, not delegated — and a malformed indicator does not silently pass as either a plain EOA or a valid delegate. A delegated wallet keeps its evidence but eligibility (status and extension issuance) pauses with a review flag until the delegation is resolved. Status and extension evaluations re-check delegation at most once per WALLET_OWNERSHIP_DELEGATION_RECHECK_SECONDS (default 3600), and a dedicated POST /wallet/ownership/delegation-check endpoint forces an immediate check for external schedulers.

If no RPC is configured, the check is recorded as not performed. That last part is a design position, not an omission: the attestation explicitly states what was verified and what was not, instead of degrading to a binary "verified/failed" that hides the difference.

One transaction, one winner

Replay protection is structural. Verification and nonce consumption share a single PostgreSQL transaction with a tenant lock, so exactly one concurrent verification can succeed; a retry receives a 409 and can use the nonce as the attestation identifier to inspect an uncertain-but-successful earlier attempt. A failed signature does not consume the challenge.

Creation is rate-limited — one challenge per actor/credential/five-minute bucket and 256 per tenant per UTC day, enforced atomically, surviving restarts. These bounds limit daily growth, not retention: challenge and attestation records are retained encrypted for evidence, and expiry does not delete them.

A verified attestation expires exactly 86,400 seconds after verification (ATTESTATION_TTL = 86400) — a lifetime deliberately separate from the original enrollment consent. Status and extension issuance recheck the retained signature, the authenticated context, expiry/revocation and the parent credential's current eligibility on every evaluation, so an extension retry cannot ride in on a previously successful result. A previously returned wallet_ownership_verified: true is a historical response, not perpetual validity; revocation is a current-state service check.

The export, and its honest label

GET /wallet/ownership/evidence/{attestation_id} returns a supervisor-readable export: the subject address, the verification method mapped to the verification methods in the EBA Travel Rule Guidelines (EBA/GL/2024/11) section 4.8.4 paragraph 83 — the ¶83(d) message-signing limb for EOA signatures, the ¶83(e) "other suitable technical means" catch-all for ERC-1271/6492 — the exact signed message, signature, timestamps, delegation status with the full re-check history, and credential linkage.

The mapping to ¶83(e) is Clearproof's interpretation, and wallet_evidence_export.py flags it as such in the export rather than presenting a regulator's conclusion. The export is evidence of a verification event, not a legal opinion.

The staged circuit says no on purpose

The extension also carries a versioned commitment — clearproof-wallet-credential-v1, a Poseidon(6) over six ordered field elements — and an isolated circuit, wallet_ownership_credential.circom, tested with real R1CS/witness generation. It is a staged circuit, and the boundaries around it are the point:

  • No new trusted setup or proving key is generated and no verifier is deployed for it.
  • Neither the legacy nor the pilot proving endpoints accept the extension as an authorization profile.
  • The legacy Poseidon preimage is unchanged — the existing pilot commitment and proof formats carry no wallet-ownership field.
  • The circuit constrains the recorded attestation statement (the flag, the domain, expected commitments, the evaluation interval). It does not verify the EIP-191 signature itself, and supplying self-selected public commitments does not prove authentic issuance. A future verifier must independently authenticate the expected commitments and current eligibility.

Revocation of a wallet-ownership attestation is a current-state service check, which is exactly why it lives outside the circuit: a proof can attest to what was recorded, not to what is still true.

The takeaway

"Signed by this wallet" is a fact about one moment. Ownership, ongoing control, delegation state, regulatory meaning and settlement authority are all separate facts with separate lifetimes, and each one needs its own named check. The wallet ownership extension is a worked example of drawing those lines explicitly — and of stating in every artifact, from the challenge record to the evidence export, which line a given check actually crossed.

Claim references checked against 796b1597cbd9: docs/internal/WALLET_OWNERSHIP.md, packages/proof/src/wallet-ownership.ts, src/chain/wallet_account.py, src/services/wallet_evidence_export.py, circuits/wallet_ownership_credential.circom, tests/vectors/wallet-ownership/challenge-v2.json

Wallet authority is not wallet ownership — the trust boundaries in Clearproof's wallet verification — clearproof | clearproof docs