-
Notifications
You must be signed in to change notification settings - Fork 19
Add Silent Payments for the Liquid Network #37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
42Pupusas
wants to merge
6
commits into
ElementsProject:main
Choose a base branch
from
42Pupusas:elip-silent-payments-liquid
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+343
−0
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
d092ef7
Add Silent Payments for the Liquid Network draft
42Pupusas ab997b4
Condense Silent Payments for Liquid ELIP per reviewer feedback
42Pupusas ab0c3d6
Condense light-client receive section to a single paragraph
42Pupusas 5f2a62c
Make Taproot the output representation, matching BIP-352 exactly
42Pupusas 0124bbb
Fix SP address HRP collision with native Liquid; drop standalone Labe…
42Pupusas 8d634b1
Scope down Reference Implementation section
42Pupusas File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,343 @@ | ||
| <pre> | ||
| ELIP: ? | ||
| Layer: Applications | ||
| Title: Silent Payments for the Liquid Network | ||
| Author: 42pupusas | ||
| Comments-Summary: No comments yet. | ||
| Comments-URI: https://github.com/ElementsProject/elips/wiki/Comments:ELIP-???? | ||
| Status: Draft | ||
| Type: Standards Track | ||
| Created: 2026-06-01 | ||
| License: BSD-3-Clause | ||
| </pre> | ||
|
|
||
| ==Introduction== | ||
|
|
||
| ===Abstract=== | ||
|
|
||
| This document specifies Silent Payments for the Liquid Network, building on | ||
| [https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki BIP-352]. It assumes | ||
| familiarity with BIP-352 and describes only what Liquid requires to differ: the | ||
| BIP-352 key-derivation core is reused unchanged, while three things are adapted to | ||
| Confidential Transactions (CT) and Liquid's deployed output types — the output | ||
| representation, a per-output blinding key derived from the silent-payment shared | ||
| secret (so a confidential output can be discovered and unblinded non-interactively), | ||
| and a light-client receive flow following the "tweak server" model of the | ||
| [https://github.com/silent-payments/BIP0352-index-server-specification BIP-352 index server specification]. | ||
|
|
||
| ===Copyright=== | ||
|
|
||
| This document is licensed under the 3-clause BSD license. | ||
|
|
||
| ===Motivation=== | ||
|
|
||
| Confidential Transactions hide a Liquid output's asset and amount, but its script — | ||
| and so any reused address — is public. A receiver accepting many payments to one | ||
| published address must today either reuse an address, linking those payments, or run | ||
| an interactive protocol to hand out fresh ones. | ||
|
|
||
| Silent Payments remove this trade-off, and compose naturally with CT: the receiver | ||
| publishes one static address, each sender independently derives a distinct output | ||
| only the receiver can recognize, and the payment graph gains the unlinkability of | ||
| Silent Payments while amounts keep the confidentiality of CT. This document defines | ||
| how to construct and recognize such outputs on Liquid so that independent | ||
| implementations interoperate. | ||
|
|
||
| ==Conventions and Provenance of Each Rule== | ||
|
|
||
| Throughout this document, every normative rule is tagged to make its origin explicit: | ||
|
|
||
| * '''[BIP-352]''' — the rule is taken unchanged from BIP-352. Implementations SHOULD reuse existing, reviewed BIP-352 logic for these parts. | ||
| * '''[Liquid]''' — the rule is an adaptation made necessary by a structural difference between Liquid and Bitcoin (most importantly, Confidential Transactions and Liquid's deployed output types). These are the substantive technical contributions of this document. | ||
| * '''[Choice]''' — the rule reflects a design decision for which alternatives exist. Where this document states a value or behavior under a [Choice] tag, that value is the '''preferred''' option of this draft. These are the points on which reviewer input is most actively sought. | ||
|
|
||
| Notation follows BIP-352: | ||
|
|
||
| * <code>G</code> is the secp256k1 generator; <code>n</code> the curve order. | ||
| * Lowercase letters denote scalars (private keys); uppercase letters the corresponding points, e.g. <code>A = a·G</code>. | ||
| * <code>serP(P)</code> is the 33-byte compressed encoding of a point <code>P</code>; <code>ser32(i)</code> the 4-byte big-endian encoding of an integer <code>i</code>. | ||
| * <code>·</code> is scalar–point multiplication and <code>+</code> is point addition or scalar addition mod <code>n</code> as appropriate. | ||
| * <code>hash_tag(m)</code> is the BIP-340 tagged hash <code>SHA256(SHA256(tag) || SHA256(tag) || m)</code> with ASCII tag <code>tag</code>. | ||
|
|
||
| ==Design== | ||
|
|
||
| ===Overview=== | ||
|
|
||
| A receiver holds two key pairs: a '''scan''' key pair <code>(b_scan, B_scan)</code> and | ||
| a '''spend''' key pair <code>(b_spend, B_spend)</code>. The static silent-payment | ||
| address encodes the two public keys. | ||
|
|
||
| To pay the address, a sender: | ||
|
|
||
| # aggregates the private keys of its eligible transaction inputs into a single scalar <code>a</code> and forms <code>A = a·G</code> '''[BIP-352]'''; | ||
| # computes a transaction-bound <code>input_hash</code> and an ECDH shared secret <code>S</code> with the receiver's scan key '''[BIP-352]'''; | ||
| # derives, for output index <code>k</code>, a spend public key <code>P_k</code> that only the receiver can later re-derive '''[BIP-352]'''; | ||
| # places <code>P_k</code> in a Taproot (P2TR) output '''[BIP-352]''', and blinds that output's asset and amount to a blinding key that is itself derived from <code>S</code> '''[Liquid]'''. | ||
|
|
||
| To receive, the receiver re-derives the candidate spend keys and output scripts from | ||
| <code>input_hash·A</code> and its scan key, matches them against the transaction's | ||
| outputs, and on a match derives the blinding key to unblind and the spend key to spend. | ||
|
|
||
| ===Receiver keys and address=== | ||
|
|
||
| The scan and spend key pairs are independent secp256k1 key pairs as in BIP-352. '''[BIP-352]''' | ||
| Seed derivation is left to the wallet (a BIP-32 scheme analogous to BIP-352's is | ||
| RECOMMENDED), since only the public keys are transmitted. '''[Choice]''' | ||
|
|
||
| The address is the Bech32m '''[BIP-352]''' encoding of a version symbol followed by | ||
| the payload <code>serP(B_scan) || serP(B_spend)</code> (66 bytes), with a | ||
| network-specific human-readable part. '''[Liquid] [Choice]''' This draft uses: | ||
|
|
||
| {| class="wikitable" | ||
| ! Network !! HRP | ||
| |- | ||
| | Liquid (mainnet) || <code>lqsp</code> | ||
| |- | ||
| | Liquid testnet / regtest || <code>tlqsp</code> | ||
| |} | ||
|
|
||
| and version symbol <code>q</code> (the Bech32 character for value 0), denoting | ||
| version 0. As in BIP-352, the 90-character Bech32 length limit does '''not''' apply | ||
| to silent-payment addresses. '''[BIP-352]''' | ||
|
|
||
| A distinct, network-specific HRP is required: it must differ both from Bitcoin's | ||
| silent-payment HRP (<code>sp</code>/<code>tsp</code>) '''and''' from the HRPs of | ||
| ordinary Liquid addresses — in particular Liquid's '''confidential''' addresses use | ||
| <code>lq</code>/<code>tlq</code> (blech32), so a silent-payment HRP of <code>lq</code> | ||
| would collide with them. The <code>lqsp</code>/<code>tlqsp</code> HRPs are distinct | ||
| from every existing Liquid address HRP (<code>ex</code>/<code>tex</code> for | ||
| unconfidential, <code>lq</code>/<code>tlq</code> for confidential) as well as from | ||
| Bitcoin's, so a silent-payment address can never be confused with a Bitcoin one, with | ||
| a native Liquid one, nor a mainnet address with a testnet one. | ||
|
|
||
| ===Reused from BIP-352 unchanged '''[BIP-352]'''=== | ||
|
|
||
| Because silent-payment outputs are Taproot, the entire derivation and output path is | ||
| BIP-352 verbatim; the symbols below are restated only because the Liquid blinding key | ||
| (next section) is built from <code>S</code> and <code>k</code>: | ||
|
|
||
| <pre> | ||
| input aggregation: a = Σ a_i, A = a·G (eligible inputs per BIP-352) | ||
| input_hash = int(hashBIP0352/Inputs( outpoint_L || serP(A) )) mod n | ||
| S = input_hash · a · B_scan (sender) = input_hash · b_scan · A (receiver) | ||
| t_k = int(hashBIP0352/SharedSecret( serP(S) || ser32(k) )) mod n | ||
| P_k = B_spend + t_k·G | ||
| scriptPubKey = OP_1 <x_only(P_k)> (P_k used directly; no script tree, no taptweak) | ||
| </pre> | ||
|
|
||
| The eligible-input set (P2TR key-path, P2WPKH, P2SH-P2WPKH, P2PKH), the even-Y rule for | ||
| Taproot keys, gap-limited scanning, labels (including the change label <code>m = 0</code>), | ||
| and the x-only output key are all exactly as in BIP-352 — so a silent-payment output is | ||
| itself eligible as an input to a later one. | ||
| Two Liquid notes only: <code>outpoint_L</code> uses the Elements consensus outpoint | ||
| encoding (32-byte txid, internal order, then 4-byte little-endian vout) '''[Liquid]''', | ||
| and the asset and amount are blinded as in any CT output '''[Liquid]''' — see the next | ||
| section. The scriptPubKey itself is identical to a BIP-352 output. | ||
|
|
||
| ===Output blinding key '''[Liquid]'''=== | ||
|
|
||
| This is the central adaptation required by Confidential Transactions and has no | ||
| counterpart in BIP-352. | ||
|
|
||
| On Liquid, an output is blinded to a '''blinding key''': the sender places an ECDH | ||
| nonce in the output, and the holder of the corresponding blinding private key can | ||
| recover the asset, amount, and their blinding factors. For ordinary addresses the | ||
| blinding key is derived from the output script (e.g. SLIP-77 or | ||
| [https://github.com/ElementsProject/ELIPs/blob/main/elip-0151.mediawiki ELIP-151]). | ||
| A silent-payment output's script, however, is not known to the receiver in advance — | ||
| it is discovered by scanning — so a script-derived blinding key cannot be used. | ||
|
|
||
| This document specifies that the blinding key of a silent-payment output is derived | ||
| deterministically from the silent-payment shared secret, in a '''dedicated hash | ||
| domain''' disjoint from the spend-key derivation: | ||
|
|
||
| <pre> | ||
| bk_k = hashLiquidSilentPayments/Blind( serP(S) || ser32(k) ) (a 32-byte scalar) | ||
| BK_k = bk_k·G | ||
| </pre> | ||
|
|
||
| The sender blinds the output to <code>BK_k</code> (i.e. uses <code>BK_k</code> as the | ||
| receiver blinding public key when constructing the output's CT nonce, range proof, | ||
| and commitments, exactly as for any confidential output). The receiver, having | ||
| recomputed <code>S</code>, derives <code>bk_k</code> and unblinds the output. No | ||
| out-of-band exchange and no additional interaction are required: the same shared | ||
| secret that yields the spend key also yields the blinding key. | ||
|
|
||
| Because <code>bk_k</code> and <code>t_k</code> are outputs of a random oracle (a | ||
| tagged hash) evaluated on '''disjoint domains''' over the same secret <code>S</code>, | ||
| they are independent: knowledge of one does not assist in recovering the other or | ||
| <code>S</code>. The domain tag <code>LiquidSilentPayments/Blind</code> MUST differ | ||
| from the BIP-352 spend domain <code>BIP0352/SharedSecret</code>. The blinding key is | ||
| also unaffected by BIP-352 labels: it depends only on <code>S</code> and <code>k</code>, | ||
| not on the labeled spend key. | ||
|
|
||
| Two alternatives were rejected. Publishing a single fixed blinding key in the | ||
| address would link all of a receiver's outputs through a common blinding key, | ||
| negating the unlinkability Silent Payments provides. Exchanging a per-output blinding | ||
| key out of band would reintroduce the interaction Silent Payments is designed to | ||
| eliminate. | ||
|
|
||
| ==Light-client receive: the tweak server model== | ||
|
|
||
| The light-client receive flow follows the tweak-server model of the | ||
| [https://github.com/silent-payments/BIP0352-index-server-specification BIP-352 index server specification] | ||
| unchanged: the server publishes a per-transaction '''partial tweak''' | ||
| <code>T = input_hash · A</code> (no scan key needed), and the client completes | ||
| <code>S = b_scan · T</code> and runs the BIP-352 gap-limit match. '''[BIP-352]''' On a | ||
| match it derives <code>bk_k</code> to unblind and <code>b_spend + t_k</code> to spend. | ||
| The only divergence is that the BIP-158 compact-filter step is unnecessary on Liquid | ||
| '''[Liquid] [Choice]''': Confidential Transactions blind an output's asset, amount, | ||
| and nonce but '''not its scriptPubKey''', so a client matches its derived candidate | ||
| scripts directly against the public output scripts it already retrieves. Filters MAY | ||
| still be used as an optimization but are not part of the protocol. The protocol | ||
| therefore requires only that, per block height, a client can obtain that block's | ||
| partial tweaks; the concrete wire format is left to a companion specification or | ||
| existing Liquid indexing infrastructure. '''[Choice]''' | ||
|
|
||
| <pre> | ||
| tweaks(block_height) -> [ serP(T_1), serP(T_2), ... ] | ||
| </pre> | ||
|
|
||
| ==Spending a received output '''[BIP-352]'''== | ||
|
|
||
| Spending is an ordinary BIP-340 Taproot key-path spend with | ||
| <code>d = b_spend + t_k (+ label_tweak_m)</code> (even-Y normalized), exactly as in | ||
| BIP-352. Since <code>d</code> is not a BIP-32-derivable key, the only signer-side | ||
| requirement is a signer that accepts a base key plus an additive tweak rather than only | ||
| keys at a derivation path — a software-signer capability, sufficient to send, receive, | ||
| and spend silent-payment outputs. | ||
|
|
||
| Hardware-signer support is a separate matter. Common hardware-signer protocols | ||
| identify the signing key by a registered descriptor or derivation path and expose no | ||
| channel for an additive per-input tweak; signing a silent-payment output on such | ||
| devices therefore requires firmware-level support for key tweaks, which is out of | ||
| scope for this version and is noted as future work. Hardware support for Silent | ||
| Payments is nascent on Bitcoin as well. | ||
|
|
||
| ==Abstract data structures== | ||
|
|
||
| The following abstract structures summarize the values exchanged or derived; field | ||
| encodings are as defined above. They are illustrative, not an API. | ||
|
|
||
| A silent-payment address: | ||
|
|
||
| <pre> | ||
| SilentPaymentAddress { | ||
| scan_pubkey: serP(B_scan) // 33 bytes | ||
| spend_pubkey: serP(B_spend) // 33 bytes; B_spend,m for a labeled address | ||
| } | ||
| // wire: Bech32m( hrp, version=0, scan_pubkey || spend_pubkey ) | ||
| </pre> | ||
|
|
||
| Aggregated input data computed by a sender: | ||
|
|
||
| <pre> | ||
| AggregatedInputs { | ||
| a: scalar // sender only | ||
| A: point // = a·G | ||
| input_hash: scalar // = H_Inputs(outpoint_L || serP(A)) | ||
| } | ||
| </pre> | ||
|
|
||
| A derived silent-payment output (for index k): | ||
|
|
||
| <pre> | ||
| SilentPaymentOutput { | ||
| spend_pubkey: P_k = B_spend + t_k·G | ||
| blinding_pubkey: BK_k = bk_k·G | ||
| // scriptPubKey = OP_1 <x_only(P_k)>, asset/amount blinded to BK_k | ||
| } | ||
| </pre> | ||
|
|
||
| The light-client view per eligible transaction: | ||
|
|
||
| <pre> | ||
| PartialTweak = serP( input_hash · A ) // published by the index/tweak server | ||
| </pre> | ||
|
|
||
| ==Test Vectors== | ||
|
|
||
| The following worked example fixes all inputs and lists every intermediate and final | ||
| value, so that an independent implementation can reproduce the construction | ||
| byte-for-byte. All byte strings are hex. | ||
|
|
||
| Receiver keys (32-byte scalars): | ||
|
|
||
| <pre> | ||
| b_scan = 1111111111111111111111111111111111111111111111111111111111111111 | ||
| b_spend = 2222222222222222222222222222222222222222222222222222222222222222 | ||
| </pre> | ||
|
|
||
| Two eligible inputs (any BIP-352-eligible type), with private keys and outpoints: | ||
|
|
||
| <pre> | ||
| input 0: priv = 3131...31 (0x31 x32), outpoint txid = 1010...10 (0x10 x32), vout = 0 | ||
| input 1: priv = 3232...32 (0x32 x32), outpoint txid = 2020...20 (0x20 x32), vout = 1 | ||
| </pre> | ||
|
|
||
| Aggregated input values (outpoint_L is input 0, the lexicographically smaller): | ||
|
|
||
| <pre> | ||
| A = 031195a8046dcbb8e17034bca630065e7a0982e4e36f6f7e5a8d4554e4846fcd99 | ||
| input_hash = d392922c00280a7e8d282182f5026f2fddbc74c1e1de18b4822128b2b77ec641 | ||
| </pre> | ||
|
|
||
| Per-output derived values: | ||
|
|
||
| <pre> | ||
| k = 0: | ||
| P_k (spend pubkey) = 02a29d9716417c964ca9e477343e71ffe730a4991a3eaad668eabec84e9feb7931 | ||
| BK_k (blinding pub) = 0344e1289497e6da66fde710d2f38de053fc07355e405524401d7d609df5a1a8cc | ||
| bk_k (blinding priv) = 70ab8897b64bd21b427339ff4d014b883191ef6425862246c53bfc27a59aa3f0 | ||
| spend priv (b_spend + t_k) = f03c436d2cd67ae1fecf7d88a38aa3a03c0abea43feaf6da8eb71e2e3a866bda | ||
| scriptPubKey = 5120a29d9716417c964ca9e477343e71ffe730a4991a3eaad668eabec84e9feb7931 | ||
|
|
||
| k = 1: | ||
| P_k (spend pubkey) = 0229d77654023af267dbe9cb7ff1956f947c816f203494381308387168fb010c92 | ||
| BK_k (blinding pub) = 03efdeda770ccdbe8bf466fba48bfd2b2c436ab0c04658fc6d6c277de5078129fa | ||
| bk_k (blinding priv) = 945ba73a9804f62089c7d2ffdc079031031f0aebab372cec17ef9c110ebceb10 | ||
| spend priv (b_spend + t_k) = 9eff3472230fc83ef5ea8f8c80401c4eecd595a048bd2482a107d3a49baa5a58 | ||
| scriptPubKey = 512029d77654023af267dbe9cb7ff1956f947c816f203494381308387168fb010c92 | ||
| </pre> | ||
|
|
||
| The unlabeled mainnet (HRP <code>lqsp</code>) address for these keys: | ||
|
|
||
| <pre> | ||
| lqsp1qqd8n2k7uklxq4aegau7vawtptkgxsja4kt99lpv6krctwpq8tpc65qjxd4lu4etruh9sngx3su9mtqp5fqzxz7re59y5nnez9p03ht3lyudcfhfe | ||
| </pre> | ||
|
|
||
| A conforming implementation MUST reproduce <code>A</code>, <code>input_hash</code>, | ||
| and for each <code>k</code> the values <code>P_k</code>, <code>BK_k</code>, | ||
| <code>bk_k</code>, the spend private key, and the scriptPubKey, and MUST produce the | ||
| address above. Because the asset and amount blinding factors are randomized per | ||
| output, the full blinded output is not byte-reproducible; the recovery property is | ||
| instead stated as: an output blinded to <code>BK_k</code> by the construction above | ||
| unblinds correctly under <code>bk_k</code>, and fails to unblind under any other key. | ||
|
|
||
| ==Backwards Compatibility== | ||
|
|
||
| This document defines a new, opt-in address type and output convention. It introduces | ||
| no consensus change and does not affect existing addresses, descriptors, or | ||
| transactions. Wallets that do not implement it are unaffected; a silent-payment | ||
| output, once created, is an ordinary confidential Taproot output on the chain and is | ||
| spent by an ordinary key-path signature, so existing relay and validation rules apply | ||
| unchanged. | ||
|
|
||
| Discovering and spending silent-payment outputs requires wallet support (scanning and | ||
| tweak-aware signing). Hardware signers require firmware support for additive key | ||
| tweaks, which does not exist in common protocols today; until then, silent-payment | ||
| outputs are usable with software signing. | ||
|
|
||
| ==Reference Implementation== | ||
|
|
||
| A reference implementation, built on the cryptographic primitives of the Liquid Wallet | ||
| Kit (LWK), reproduces the test vectors in this document byte-for-byte and demonstrates | ||
| that a confidential output blinded to the shared-secret-derived key can be unblinded | ||
| non-interactively by the receiver. Wallet integration — scanning, signing, and | ||
| transaction building — is left to implementations. | ||
|
|
||
| ==Acknowledgements== | ||
|
|
||
| This specification builds directly on BIP-352 and the BIP-352 index server | ||
| specification, and on the Confidential Transactions and CT-descriptor work of the | ||
| Elements Project. | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would a third person who wants to detect silent payments, be able to compute blinding keys for each transaction and then check if it unblinds to identify Silent payments?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should not be possible as this is using the same hardness assumption from BIP-352 that the blinding key is computed from the sender+receiver Diffie Hellman shared secret, which should never be computable by third parties.