Selective disclosure
Every output on NULL is encrypted, so by default nobody but the sender and the recipient can see what a payment carried. Selective disclosure lets the owner reveal exactly one fact to someone else, off-chain and on their own initiative: what one output paid, or that they control an address. Neither touches consensus.
There are two mechanisms, each with one wallet method and one node method:
| Question | Prover (wallet daemon) | Verifier (node) |
|---|---|---|
| Did withdrawal X pay this address this amount? | getpaymentdisclosure | verifypaymentdisclosure |
| Do you control this address? | answerchallenge | createchallenge |
The wallet methods are served by null-wallet-rpc (examples on 127.0.0.1:18447 with WALLET_TOKEN), and the node methods by nulld (examples on 127.0.0.1:18446 with TOKEN). See the overview for transport and errors.
Payment disclosure
For one output of one transaction, the sender's wallet reveals the recipient's transmission key and the ephemeral secret used to encrypt that output. These two values decrypt that output and nothing else: not the other outputs of the transaction, and nothing else the wallet sent or received. A verifier with any node checks them against the output's ciphertext and note commitment. A disclosure cannot claim an amount, address or memo the transaction does not carry.
A typical use: an exchange proves to a customer, or to support staff, that a withdrawal paid the customer's address.
Address ownership
The checker encrypts a secret message of their choosing to the address, the way a payment output would be encrypted. Only the holder of that address's incoming viewing key can read it back. The checker keeps the message secret, hands over the challenge, and compares the answer. No signature scheme is involved. A watch-only wallet can answer, because it holds the incoming viewing key.
getpaymentdisclosure
Wallet daemon. Disclosures for the outputs this wallet sent in a transaction, change included. Only transactions that spent this wallet's notes qualify, and the wallet must have recorded the spending txid, which requires full-block scanning (not --light). Zero-value padding outputs are skipped.
Parameters
- Name
txid- Type
- hex
- Description
Required. A transaction this wallet sent.
- Name
index- Type
- number
- Description
Optional. Only the output at this action index.
Result
An array, one entry per output:
- Name
index- Type
- number
- Description
Action index of the output in the transaction.
- Name
disclosure- Type
- hex
- Description
The disclosure to hand to the verifier: txid, action index, transmission key and ephemeral secret (100 bytes).
- Name
address- Type
- string
- Description
The recipient address.
- Name
amount- Type
- string
- Description
The amount paid, in units.
- Name
memo- Type
- string | object | null
- Description
The memo as text;
{"hex": "…"}if not UTF-8;nullif empty.
Errors
-1 if the wallet did not send this transaction, or it has no output this wallet can open at that index.
Request
curl -s http://127.0.0.1:18447/ \
-H "Authorization: Bearer $WALLET_TOKEN" \
-d '{"jsonrpc":"2.0","id":1,"method":"getpaymentdisclosure","params":["5a9e3c1f7b2d8e4a6c0f9b3d5e7a1c8f2b4d6e0a9c3f5b7d1e8a2c4f6b0d9e3a", 0]}'
Response
{
"jsonrpc": "2.0",
"result": [
{
"index": 0,
"disclosure": "5a9e3c1f7b2d8e4a6c0f9b3d5e7a1c8f2b4d6e0a9c3f5b7d1e8a2c4f6b0d9e3a00000000730108852af1d39a37e931703c560014246c4f039ff6bd5c974e6a2f4a7a9a60b9a8194dd983f7f352b87bb2226a33c52499dd2d460090299ca94057b894d8cc",
"address": "null19crg8qy05x8gj7mz6tuq0r9c9j48njnny34ycueljywcrm7uquvyjhfkzknarmngc0yegdyfvh8",
"amount": "150000000",
"memo": "withdrawal 5521"
}
],
"id": 1
}
verifypaymentdisclosure
Node. Opens the output a disclosure names and reports what it proves. Any node can verify; it needs no keys. A disclosure that proves nothing is a normal result with valid: false, not an error.
Parameters
- Name
disclosure- Type
- hex
- Description
Required. From
getpaymentdisclosure.
Result
- Name
valid- Type
- boolean
- Description
Whether the disclosure opens the output.
- Name
txid- Type
- hex
- Description
The transaction. Only when valid.
- Name
index- Type
- number
- Description
The action index. Only when valid.
- Name
address- Type
- string
- Description
The address the output paid. Only when valid.
- Name
amount- Type
- string
- Description
The amount in units. Only when valid.
- Name
memo- Type
- string | null
- Description
The memo as text;
""if empty;nullif not UTF-8. Only when valid.
- Name
height- Type
- number | null
- Description
Block height;
nullwhile the transaction is in the mempool. Only when valid.
- Name
confirmations- Type
- number
- Description
0while in the mempool. Only when valid.
- Name
reason- Type
- string
- Description
Why it proves nothing, for example
transaction not foundorno such action. Only when not valid.
Errors
-32602 if the hex does not decode as a disclosure.
Request
curl -s http://127.0.0.1:18446/ \
-H "Authorization: Bearer $TOKEN" \
-d '{"jsonrpc":"2.0","id":1,"method":"verifypaymentdisclosure","params":["5a9e3c1f7b2d8e4a6c0f9b3d5e7a1c8f2b4d6e0a9c3f5b7d1e8a2c4f6b0d9e3a00000000730108852af1d39a37e931703c560014246c4f039ff6bd5c974e6a2f4a7a9a60b9a8194dd983f7f352b87bb2226a33c52499dd2d460090299ca94057b894d8cc"]}'
Valid
{
"jsonrpc": "2.0",
"result": {
"valid": true,
"txid": "5a9e3c1f7b2d8e4a6c0f9b3d5e7a1c8f2b4d6e0a9c3f5b7d1e8a2c4f6b0d9e3a",
"index": 0,
"address": "null19crg8qy05x8gj7mz6tuq0r9c9j48njnny34ycueljywcrm7uquvyjhfkzknarmngc0yegdyfvh8",
"amount": "150000000",
"memo": "withdrawal 5521",
"height": 6203,
"confirmations": 4
},
"id": 1
}
Not valid
{
"jsonrpc": "2.0",
"result": { "valid": false, "reason": "transaction not found" },
"id": 1
}
createchallenge
Node. Encrypts a message to an address and returns the challenge. Run it on your own node; it needs no keys. Choose an unpredictable message and keep it secret until the other party answers.
Parameters
- Name
address- Type
- string
- Description
Required. The address whose owner should answer.
- Name
message- Type
- string
- Description
Required. The secret message, UTF-8, at most 512 bytes.
Result
- Name
challenge- Type
- hex
- Description
The encrypted challenge to send to the address owner.
Errors
-32602 if the address does not decode for this network or the message is too long.
Request
curl -s http://127.0.0.1:18446/ \
-H "Authorization: Bearer $TOKEN" \
-d '{"jsonrpc":"2.0","id":1,"method":"createchallenge","params":{"address":"null1c32nxrlucj0u4vepa9swy97v2p3gwxyzgxsxya6nj26jf7nxl2tqdm2xr7pyurdaez4nj4g3cw7","message":"proof 7f3a91c2"}}'
Response
{
"jsonrpc": "2.0",
"result": {
"challenge": "ef64d5a7d9c66ec8f2137a815e86db7bb41208e0bc8081a915cf09d0be9b9580422a9ab5c1aff438…"
},
"id": 1
}
answerchallenge
Wallet daemon. Decrypts a challenge made for any of this wallet's addresses and returns the message. Send the message back to the checker. Watch-only wallets can answer.
Parameters
- Name
challenge- Type
- hex
- Description
Required. From
createchallenge.
Result
- Name
message- Type
- string
- Description
The decrypted message.
Errors
-2 with challenge is not for this wallet. -32602 if the hex does not decode as a challenge.
Request
curl -s http://127.0.0.1:18447/ \
-H "Authorization: Bearer $WALLET_TOKEN" \
-d '{"jsonrpc":"2.0","id":1,"method":"answerchallenge","params":["ef64d5a7d9c66ec8f2137a815e86db7bb41208e0bc8081a915cf09d0be9b9580422a9ab5c1aff438…"]}'
Response
{ "jsonrpc": "2.0", "result": { "message": "proof 7f3a91c2" }, "id": 1 }
What is not disclosed
Block explorers, including explorer.nullnet.sh, show blocks, txids, action counts and nullifiers only. No public service can show the balance of an address or the payments it received. Proving anything beyond one output or one address takes the owner's cooperation through the methods above. Multisig and hardware wallets are not supported.