Enhancement: Add Recipient-Signed AsyncHashPool Proofs for Trustless Lightning Address Async Payments
Background
The current RLN async payment design uses a stable Lightning Address as the public payment surface.
Today, the invoice-host / LSP owns the AsyncHashPool after async_order.new, reserves a hash_index, creates a fresh bolt11_hodl_invoice, and returns it to the payer through the Lightning Address / LNURL callback.
The intended noncustodial property is:
The invoice-host can only claim the inbound HODL payment after the recipient reveals the matching preimage through the outbound leg.
However, the current design still requires the payer to trust that the payment_hash inside the returned HODL invoice was actually generated by the recipient.
Problem
When a payer resolves a Lightning Address, the Lightning Address server / invoice-host returns a BOLT11 HODL invoice.
That invoice is signed by the LSP node, not by the final recipient.
Because of that, the payer cannot independently verify that the invoice's payment_hash belongs to a preimage controlled by the recipient.
A malicious or compromised invoice-host could return:
- A regular invoice controlled by the LSP.
- A HODL invoice whose preimage is already known by the LSP.
- A HODL invoice using a
payment_hash that is not part of the recipient-provided AsyncHashPool.
In all cases, the payer may believe they are paying the recipient, while the LSP may be able to claim the inbound payment without recipient participation.
This creates a trust gap in the current async Lightning Address design.
Goal
Add a cryptographic proof that allows the payer to verify that the returned HODL invoice is bound to:
- The intended recipient identity.
- A recipient-generated
payment_hash.
- A recipient-signed batch commitment.
- The specific
hash_index selected by the invoice-host.
- The returned BOLT11 HODL invoice.
After this enhancement, the payer should be able to verify that the invoice-host cannot claim the payment unless the recipient eventually reveals the matching preimage.
Proposed Solution
Introduce Recipient-Signed AsyncHashPool Commitments.
Instead of trusting the payment_hash inside the LSP-signed BOLT11 invoice, the Lightning Address / LNURL callback should return:
- The BOLT11 HODL invoice.
- A payer-verifiable proof object.
Example response:
{
"pr": "<bolt11_hodl_invoice>",
"routes": [],
"verify": {
"version": 1,
"receiver_pubkey": "<recipient_node_pubkey_or_account_pubkey>",
"order_id": "<order_id_or_public_order_commitment>",
"batch_id": "<batch_id>",
"hash_index": 42,
"payment_hash": "<32-byte-payment-hash>",
"batch_root": "<merkle_root>",
"merkle_proof": [
"<sibling_hash_1>",
"<sibling_hash_2>"
],
"signature": "<recipient_signature_over_batch_commitment>",
"signature_scheme": "schnorr/secp256k1"
}
}
The payer client must verify this proof before paying.
Recipient Identity
The sender must know which recipient public key they are paying.
Option A: Use the recipient public key in the Lightning Address
Example:
<receiver_pubkey>@utexo.com
This gives the simplest trust model because the payer directly sees the recipient identity in the payment address.
Option B: Use a short alias that resolves to the full recipient public key
Example:
In this case, the LNURL metadata or verification object must return the full receiver_pubkey.
The payer must verify that the alias is bound to that recipient public key.
Important:
A short alias or shortened public key should not be treated as the cryptographic identity unless it has enough collision resistance.
For UX, we can use aliases. For security, verification must use the full recipient public key or a sufficiently long collision-resistant fingerprint / commitment.
Batch Commitment Design
When the recipient creates or refreshes an async order, the recipient generates a batch of future preimages:
preimage_i
payment_hash_i = SHA256(preimage_i)
For each hash entry, define a canonical Merkle leaf:
leaf_i = SHA256(
"UTEXO_ASYNC_HASH_V1" ||
receiver_pubkey ||
order_id ||
batch_id ||
hash_index_i ||
payment_hash_i
)
Then build a Merkle tree:
batch_root = merkle_root(
leaf_0,
leaf_1,
...,
leaf_n
)
The recipient signs the batch commitment once:
signed_message = SHA256(
"UTEXO_ASYNC_HASH_BATCH_V1" ||
receiver_pubkey ||
order_id ||
batch_id ||
batch_root ||
batch_size ||
created_at ||
optional_expiry
)
signature = sign(receiver_private_key, signed_message)
The recipient sends the following to the invoice-host during async_order.new or async_order.sync_hashes:
{
"order_id": "...",
"batch_id": "...",
"receiver_pubkey": "...",
"batch_root": "...",
"batch_size": 200,
"signature": "...",
"signature_scheme": "schnorr/secp256k1",
"created_at": "...",
"optional_expiry": "...",
"hash_entries": [
{
"hash_index": 0,
"payment_hash": "..."
},
{
"hash_index": 1,
"payment_hash": "..."
}
]
}
The invoice-host stores:
- The hash entries.
- The batch root.
- The recipient signature.
- The batch metadata.
Payment Flow With Verification
- Recipient-client calls
async_order.new.
- Recipient-client uploads
AsyncHashPool entries plus one signed batch commitment.
- Invoice-host stores the batch.
- Payer resolves the recipient's Lightning Address.
- Invoice-host reserves the next
hash_index.
- Invoice-host creates a BOLT11 HODL invoice using the corresponding
payment_hash.
- Invoice-host returns the BOLT11 invoice plus the verification proof object.
- Payer parses the BOLT11 invoice and extracts the invoice
payment_hash.
- Payer verifies:
- The BOLT11 invoice
payment_hash equals verify.payment_hash.
- The Merkle proof proves that
payment_hash is included in batch_root.
- The batch signature is valid under
receiver_pubkey.
- The
receiver_pubkey matches the intended recipient identity.
- The
order_id, batch_id, and hash_index are domain-bound and canonical.
- Only if verification succeeds, the payer pays the invoice.
Payer Verification Algorithm
invoice_hash = parse_payment_hash(bolt11_hodl_invoice)
assert invoice_hash == verify.payment_hash
leaf = SHA256(
"UTEXO_ASYNC_HASH_V1" ||
verify.receiver_pubkey ||
verify.order_id ||
verify.batch_id ||
verify.hash_index ||
verify.payment_hash
)
computed_root = verify_merkle_proof(
leaf,
verify.merkle_proof
)
assert computed_root == verify.batch_root
signed_message = SHA256(
"UTEXO_ASYNC_HASH_BATCH_V1" ||
verify.receiver_pubkey ||
verify.order_id ||
verify.batch_id ||
verify.batch_root ||
verify.batch_size ||
verify.created_at ||
verify.optional_expiry
)
assert verify_signature(
verify.receiver_pubkey,
signed_message,
verify.signature
)
assert verify.receiver_pubkey == expected_receiver_pubkey
If any check fails, the payer must reject the invoice and must not pay.
Why Merkle Proof Instead of Individual Signatures?
The naive design would require the recipient to sign every payment_hash individually:
signature_i = sign(receiver_private_key, payment_hash_i)
This works, but it does not scale well for large batches.
The Merkle design gives us:
- One recipient signature per batch.
- One compact inclusion proof per invoice.
- Efficient payer-side verification.
- No need for the payer to download the full batch.
- Cryptographic binding between the recipient identity and each payment hash.
For example, with a batch of 200 entries, the proof requires only around log2(200) sibling hashes instead of 200 individual signatures.
Required Changes
Recipient-client
- Generate async payment preimages.
- Derive
payment_hash = SHA256(preimage).
- Build canonical Merkle leaves.
- Build
batch_root.
- Sign the batch commitment.
- Send
batch_root, signature, and hash entries to invoice-host in async_order.new.
- Later support the same format in
async_order.sync_hashes.
Invoice-host / LSP
- Store batch metadata:
receiver_pubkey
batch_id
batch_root
batch_size
batch_signature
hash_index
payment_hash
- When creating a HODL invoice, include the selected
payment_hash.
- Return the BOLT11 invoice together with the proof object.
- Ensure the proof corresponds exactly to the selected
hash_index.
- Never return an invoice without a valid proof for this async payment path.
Payer-client
- Parse the BOLT11 invoice.
- Extract
payment_hash.
- Verify the proof object.
- Reject invoices without a valid proof when paying a verified async Lightning Address.
- Only pay if the HODL invoice is cryptographically bound to the intended recipient.
Data Model Additions
New table: AsyncHashBatch
AsyncHashBatch
- order_id
- batch_id
- receiver_pubkey
- batch_root
- batch_size
- signature
- signature_scheme
- created_at
- expires_at optional
Extend AsyncHashPool
AsyncHashPool
- order_id
- batch_id
- hash_index
- payment_hash
- merkle_proof optional/cached
- status
Extend Lightning Address callback response
AsyncInvoiceProof
- version
- receiver_pubkey
- order_id or public_order_commitment
- batch_id
- hash_index
- payment_hash
- batch_root
- batch_size
- merkle_proof
- signature
- signature_scheme
- created_at
- optional_expiry
Security Properties
After this enhancement:
- The LSP cannot replace the payment hash with one it controls without invalidating the proof.
- The LSP cannot return a regular invoice controlled by itself and make it pass recipient verification.
- The payer can verify that the invoice's payment hash belongs to a recipient-signed batch.
- The recipient does not need to be online at invoice-fetch time.
- The invoice-host still performs the always-online Lightning Address role.
- The invoice-host no longer needs to be trusted for payment-hash correctness.
Open Questions
- Should the public Lightning Address localpart be the full
receiver_pubkey, or should we support aliases that resolve to a full recipient public key?
- If we use shortened identifiers, what minimum collision-resistant size is acceptable?
- Should
order_id be exposed directly in the proof, or should we use a public_order_commitment to avoid leaking internal IDs?
- Should batch commitments have explicit expiry?
- Should the signature key be the RLN node pubkey, a wallet account key, or a dedicated async-payment identity key?
- Should the Merkle proof be generated dynamically by the invoice-host, or precomputed and stored per
hash_index?
- Should the payer require this proof only for async Lightning Address payments, or for all LSP-hosted HODL invoices?
- Should the LNURL metadata also commit to
receiver_pubkey, so the payer can bind the human-readable Lightning Address to the cryptographic recipient identity?
Acceptance Criteria
Suggested Priority
High.
This closes a major trust gap in the current async Lightning Address design and makes the payer-side security model match the intended noncustodial settlement model.
Enhancement: Add Recipient-Signed AsyncHashPool Proofs for Trustless Lightning Address Async Payments
Background
The current RLN async payment design uses a stable Lightning Address as the public payment surface.
Today, the invoice-host / LSP owns the
AsyncHashPoolafterasync_order.new, reserves ahash_index, creates a freshbolt11_hodl_invoice, and returns it to the payer through the Lightning Address / LNURL callback.The intended noncustodial property is:
However, the current design still requires the payer to trust that the
payment_hashinside the returned HODL invoice was actually generated by the recipient.Problem
When a payer resolves a Lightning Address, the Lightning Address server / invoice-host returns a BOLT11 HODL invoice.
That invoice is signed by the LSP node, not by the final recipient.
Because of that, the payer cannot independently verify that the invoice's
payment_hashbelongs to a preimage controlled by the recipient.A malicious or compromised invoice-host could return:
payment_hashthat is not part of the recipient-providedAsyncHashPool.In all cases, the payer may believe they are paying the recipient, while the LSP may be able to claim the inbound payment without recipient participation.
This creates a trust gap in the current async Lightning Address design.
Goal
Add a cryptographic proof that allows the payer to verify that the returned HODL invoice is bound to:
payment_hash.hash_indexselected by the invoice-host.After this enhancement, the payer should be able to verify that the invoice-host cannot claim the payment unless the recipient eventually reveals the matching preimage.
Proposed Solution
Introduce Recipient-Signed AsyncHashPool Commitments.
Instead of trusting the
payment_hashinside the LSP-signed BOLT11 invoice, the Lightning Address / LNURL callback should return:Example response:
{ "pr": "<bolt11_hodl_invoice>", "routes": [], "verify": { "version": 1, "receiver_pubkey": "<recipient_node_pubkey_or_account_pubkey>", "order_id": "<order_id_or_public_order_commitment>", "batch_id": "<batch_id>", "hash_index": 42, "payment_hash": "<32-byte-payment-hash>", "batch_root": "<merkle_root>", "merkle_proof": [ "<sibling_hash_1>", "<sibling_hash_2>" ], "signature": "<recipient_signature_over_batch_commitment>", "signature_scheme": "schnorr/secp256k1" } }The payer client must verify this proof before paying.
Recipient Identity
The sender must know which recipient public key they are paying.
Option A: Use the recipient public key in the Lightning Address
Example:
This gives the simplest trust model because the payer directly sees the recipient identity in the payment address.
Option B: Use a short alias that resolves to the full recipient public key
Example:
In this case, the LNURL metadata or verification object must return the full
receiver_pubkey.The payer must verify that the alias is bound to that recipient public key.
Important:
A short alias or shortened public key should not be treated as the cryptographic identity unless it has enough collision resistance.
For UX, we can use aliases. For security, verification must use the full recipient public key or a sufficiently long collision-resistant fingerprint / commitment.
Batch Commitment Design
When the recipient creates or refreshes an async order, the recipient generates a batch of future preimages:
For each hash entry, define a canonical Merkle leaf:
Then build a Merkle tree:
The recipient signs the batch commitment once:
The recipient sends the following to the invoice-host during
async_order.neworasync_order.sync_hashes:{ "order_id": "...", "batch_id": "...", "receiver_pubkey": "...", "batch_root": "...", "batch_size": 200, "signature": "...", "signature_scheme": "schnorr/secp256k1", "created_at": "...", "optional_expiry": "...", "hash_entries": [ { "hash_index": 0, "payment_hash": "..." }, { "hash_index": 1, "payment_hash": "..." } ] }The invoice-host stores:
Payment Flow With Verification
async_order.new.AsyncHashPoolentries plus one signed batch commitment.hash_index.payment_hash.payment_hash.payment_hashequalsverify.payment_hash.payment_hashis included inbatch_root.receiver_pubkey.receiver_pubkeymatches the intended recipient identity.order_id,batch_id, andhash_indexare domain-bound and canonical.Payer Verification Algorithm
If any check fails, the payer must reject the invoice and must not pay.
Why Merkle Proof Instead of Individual Signatures?
The naive design would require the recipient to sign every
payment_hashindividually:This works, but it does not scale well for large batches.
The Merkle design gives us:
For example, with a batch of 200 entries, the proof requires only around
log2(200)sibling hashes instead of 200 individual signatures.Required Changes
Recipient-client
payment_hash = SHA256(preimage).batch_root.batch_root,signature, and hash entries to invoice-host inasync_order.new.async_order.sync_hashes.Invoice-host / LSP
receiver_pubkeybatch_idbatch_rootbatch_sizebatch_signaturehash_indexpayment_hashpayment_hash.hash_index.Payer-client
payment_hash.Data Model Additions
New table:
AsyncHashBatchExtend
AsyncHashPoolExtend Lightning Address callback response
Security Properties
After this enhancement:
Open Questions
receiver_pubkey, or should we support aliases that resolve to a full recipient public key?order_idbe exposed directly in the proof, or should we use apublic_order_commitmentto avoid leaking internal IDs?hash_index?receiver_pubkey, so the payer can bind the human-readable Lightning Address to the cryptographic recipient identity?Acceptance Criteria
AsyncHashPoolbatch.bolt11_hodl_invoiceplus proof object in the Lightning Address callback response.payment_hashdoes not match the provenpayment_hash.receiver_pubkeydoes not match the intended recipient.hash_index, wrongbatch_id, wrongreceiver_pubkey, and wrong invoice hash.Suggested Priority
High.
This closes a major trust gap in the current async Lightning Address design and makes the payer-side security model match the intended noncustodial settlement model.