Reference
TCC-0004 Address ownership proof
How the owner of a Tenrai address proves control to a verifier. The request and proof documents, the signed message and the checks in order.
| Number | TCC-0004 |
|---|---|
| Status | Final |
| Category | Wallet |
| Identifier | Document version 1, context TRAI/ADDRESS/PROOF/1 |
This convention specifies how the owner of a Tenrai address proves it to a verifier, version 1. The verifier sends a request with a random challenge; the owner's wallet signs it with the ML-DSA-65 key behind the address. The proof is never part of a transaction and moves no funds.
Request
{
"type": "tenrai-address-proof-request",
"version": 1,
"address": "trai1...",
"challenge": "<64 lower-case hex>",
"expires_at": 1900000000
}| Field | Rule |
|---|---|
type | Exactly tenrai-address-proof-request |
version | 1 |
address | Canonical lower-case Bech32m address of a single-key lock (profile 1) |
challenge | 32 bytes from a cryptographic random source, 64 lower-case hex characters |
expires_at | Unix time in seconds after which the proof no longer counts, at most 2^53 − 1 so JavaScript reads it exactly |
The verifier keeps each challenge until it expires and accepts it once.
Proof
{
"type": "tenrai-address-proof",
"version": 1,
"address": "trai1...",
"challenge": "<64 lower-case hex>",
"expires_at": 1900000000,
"public_key": "<3904 lower-case hex>",
"signature": "<6618 lower-case hex>"
}address, challenge and expires_at repeat the request. public_key is the 1,952-byte ML-DSA-65 key of the address, signature the 3,309-byte signature. Both documents are JSON objects without further fields; parsers refuse unknown fields, upper-case hex and other lengths.
Signed message
message = sha384_domain_hash("TRAI_ADDRESS_PROOF_V1",
[ hrp, address_version, lock,
challenge, u64_be(expires_at) ])
signature = ML-DSA-65.Sign(secret_key, message, context = "TRAI/ADDRESS/PROOF/1")hrp, address_version, lock and sha384_domain_hash are those of TCC-0003. The message is the 48-byte digest. The context belongs to no other signature, so a spend or staking signature is never a proof, and a proof never authorizes a spend.
Verification
A verifier checks, in this order:
- The address is a single-key address (lock profile
1). Otherwisenot-single-key. SHAKE256("TRAI_LOCK_V1" || 0x01 || public_key)read to 40 bytes equals the lock commitment. Otherwisekey-mismatch.- The current time in seconds is at most
expires_at. Otherwiseexpired. - The signature verifies under the message and context above. Otherwise
bad-signature.
With the request at hand, the verifier also checks that address, challenge and expires_at of the proof equal those of the request.
Threshold addresses have no single key and cannot be proven this way.
Wallets
A wallet answers a request only before expires_at, shows the address, its verification words and the time left, and signs once the user agrees. It looks the address up among its tracked addresses; the command-line wallet also searches the first 10,000 receive and change addresses of an account.
Vectors
crates/wallet/tests/vectors/address-proof-v1.json in the node repository holds requests and proofs with fixed keys and signing randomness, the 48-byte message, a time and the expected result: valid, expired, key-mismatch, bad-signature or not-single-key.
