-
Notifications
You must be signed in to change notification settings - Fork 21
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
base: main
Are you sure you want to change the base?
Changes from 16 commits
d092ef7
ab997b4
ab0c3d6
5f2a62c
0124bbb
8d634b1
c40601e
520320e
237599c
28d6c18
89fa834
d5c61a1
1639a4c
2bfe65b
e0d76e7
c9a8c3a
5842092
059ff40
d9e2de3
7d2e04e
218d909
44be5b9
c36639d
f56b278
ffc5c46
bc8bb5c
9a323ad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| <pre> | ||
| ELIP: ? | ||
| Layer: Applications | ||
| Title: Silent Payments for the Liquid Network | ||
| Author: 42pupusas <technology@illuminodes.com> | ||
| 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> | ||
|
|
||
| ==Abstract== | ||
|
|
||
| This document specifies Silent Payments for the Liquid Network. The key derivation, | ||
| address format, scanning, and spending follow BIP-352<ref name="bip352">BIP-352: Silent Payments. https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki</ref> | ||
| exactly. This specification only adds Liquid-specific differences. | ||
|
|
||
| ==Copyright== | ||
|
|
||
| This document is licensed under the 3-clause BSD license. | ||
|
|
||
| ==Motivation== | ||
|
|
||
| Confidential Transactions (CT)<ref name="ct">Confidential Transactions. https://elementsproject.org/features/confidential-transactions</ref> | ||
| hide a Liquid output's asset and amount but not its | ||
| script. A receiver with one published address has to either reuse it, (linking | ||
| all their payments) or run an interactive protocol to hand out fresh ones. Silent | ||
| Payments remove that trade-off: one static address, a distinct unlinkable output per payment. | ||
|
|
||
| ==Specification== | ||
|
|
||
| We reuse the notation, functions, and conventions of BIP-352. In particular, | ||
| <code>serP(P)</code> is the SEC1 compressed encoding of a point, <code>ser32(i)</code> | ||
| serializes a 32-bit unsigned integer most-significant-byte first, <code>n</code> is the | ||
| secp256k1 curve order, and <code>hash<sub>tag</sub>(x)</code> denotes | ||
| <code>SHA256(SHA256(tag) || SHA256(tag) || x)</code> as defined in | ||
| BIP-340<ref name="bip340">BIP-340: Schnorr Signatures for secp256k1. https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki</ref>. | ||
|
|
||
| We additionally define: | ||
|
|
||
| * Let <code>blind_tag</code> be the exact 26-byte ASCII string <code>LiquidSilentPayments/Blind</code>. | ||
|
|
||
| A mismatch in these bytes breaks unblinding across implementations. The tag is disjoint | ||
| from BIP-352's <code>BIP0352/SharedSecret</code>, so the blinding key and the BIP-352 | ||
| output tweak, though both derived from <code>S</code>, do not reveal each other. | ||
|
|
||
| A silent payment on Liquid is a BIP-352 output that is additionally made a confidential | ||
| Liquid output, blinded to a per-output blinding key derived from the same shared secret. | ||
| The sender, for each output index <code>k</code>: | ||
|
|
||
| * Let <code>S</code> be the shared secret and <code>P_k</code> the output public key, derived as in BIP-352 | ||
| * Let <code>scriptPubKey = OP_1 <x_only(P_k)></code>, a BIP-341<ref name="bip341">BIP-341: Taproot: SegWit version 1 spending rules. https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki</ref> Taproot output | ||
| * Let <code>bk_k = int(hash<sub>blind_tag</sub>(serP(S) || ser32(k)))</code> | ||
| ** If <code>bk_k = 0</code> or <code>bk_k ≥ n</code>, <code>bk_k</code> is not a valid secp256k1 secret key: the output index <code>k</code> is skipped '''entirely''' — no output is created for that <code>k</code>, its <code>P_k</code> and <code>scriptPubKey</code> are never used, and both sender and receiver continue with <code>k+1</code>, re-deriving '''both''' <code>P_{k+1}</code> (and thus the <code>scriptPubKey</code>) '''and''' <code>bk_{k+1}</code> from the new index. An output's key pair <code>(P_k, bk_k)</code> MUST always share the same index <code>k</code>; re-deriving only the blinding key while keeping the previous <code>P_k</code> would make the output unblindable by the receiver. This mirrors BIP-352's handling of an out-of-range <code>t_k</code>. (The probability is about 2<sup>-128</sup> and cannot be triggered by an attacker without solving ECDH.) | ||
| * Let <code>BK_k = bk_k·G</code> | ||
| * Blind the output's asset and amount to <code>BK_k</code>, as for any confidential Liquid output. This includes all witness data required by Liquid consensus — in particular the asset ''surjection proof'', which is built from transaction-level input data as usual and is independent of the silent-payments derivation (it does not involve <code>bk_k</code> or <code>S</code>) | ||
|
|
||
| The receiver, having recomputed <code>S</code> as in BIP-352, recomputes | ||
| <code>bk_k</code> and unblinds the output with no out-of-band exchange. Deriving the | ||
| blinding key from <code>S</code> avoids both a fixed address-level blinding key (which | ||
| would link a receiver's outputs) and an interactive per-output exchange. | ||
|
|
||
| ===Differences from BIP-352=== | ||
|
|
||
| * '''Output format.''' The output is a ''confidential'' Liquid Taproot<ref name="bip341" /> output, with its asset and amount blinded to <code>BK_k</code>, rather than a bare Taproot output. | ||
| * '''<code>outpoint_L</code> encoding.''' Outpoints use the Elements consensus encoding: the 36-byte concatenation of the 32-byte txid followed by the 4-byte little-endian <code>vout</code>. The txid is in ''internal byte order'', i.e. the raw 32 bytes output by the double-SHA256 of the serialized transaction, exactly as they appear inside a serialized transaction input — the ''reverse'' of the big-endian hex string displayed by RPC interfaces and block explorers. For example, output <code>vout = 1</code> of the transaction displayed as | ||
|
|
||
| <code>112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00</code> | ||
|
|
||
| is encoded as the 36 bytes | ||
|
|
||
| <code>00ffeeddccbbaa99887766554433221100ffeeddccbbaa998877665544332211 01000000</code> | ||
|
|
||
| The test-vector txids below are given in internal byte order. | ||
| * '''Key derivation path.''' Scan and spend keys use the BIP-352 derivation scheme with the coin type replaced by SLIP-44<ref name="slip44">SLIP-0044: Registered coin types for BIP-0044. https://github.com/satoshilabs/slips/blob/master/slip-0044.md</ref> code 1776 (Liquid): <code>scan_key: m/352'/1776'/account'/1'/0</code> and <code>spend_key: m/352'/1776'/account'/0'/0</code> for mainnet, with coin type <code>1'</code> for testnet and regtest. This keeps Liquid silent-payment keys disjoint from a wallet's Bitcoin silent-payment keys (<code>m/352'/0'/...</code>). | ||
| * '''Address HRP.''' The human-readable part is <code>lqsp</code> (mainnet) or <code>tlqsp</code> (testnet and regtest) — distinct from <code>ex</code>/<code>tex</code>, <code>lq</code>/<code>tlq</code>, and Bitcoin's <code>sp</code>/<code>tsp</code>. The address payload, version symbol (<code>q</code>), and relaxed Bech32 length limit are unchanged from BIP-352. | ||
| * '''Light-client scanning.''' The BIP-158<ref name="bip158">BIP-158: Compact Block Filters for Light Clients. https://github.com/bitcoin/bips/blob/master/bip-0158.mediawiki</ref> compact-filter step is unnecessary: CT does not blind the <code>scriptPubKey</code>, so a client matches its derived candidate scripts directly against the public output scripts. The BIP-352 tweak server<ref name="indexserver">BIP-352 Silent Payments index server. https://github.com/bitcoin/bips/blob/master/bip-0352/index-server.mediawiki</ref> (publishing <code>T = input_hash·A</code> per transaction) is used unchanged. | ||
|
|
||
| Everything else follows BIP-352 exactly: input eligibility, the even-Y rule, | ||
| <code>input_hash</code>, <code>S = input_hash·a·B_scan</code>, the output tweak | ||
| <code>t_k</code>, labels, gap-limit scanning, and the Taproot key-path spend with | ||
| <code>d = b_spend + t_k</code>. | ||
|
|
||
| ===Privacy=== | ||
|
|
||
| Detecting or unblinding a silent payment requires the Diffie-Hellman shared secret | ||
| <code>S = input_hash·a·B_scan = b_scan·(input_hash·A)</code>. Computing it needs either the | ||
| sender's input secret <code>a</code> or the receiver's secret scan key <code>b_scan</code>, | ||
| so this inherits the BIP-352 privacy model. CT additionally hides the output's amount | ||
| and asset from anyone who holds neither key. | ||
|
|
||
| Since <code>bk_k</code> is derived from <code>S</code>, a party holding | ||
| <code>b_scan</code> can unblind the output, not merely detect it. A wallet that | ||
| delegates <code>b_scan</code> to a scanning server therefore reveals the amounts and | ||
| assets of all its incoming silent payments to that server (which still cannot spend | ||
| them without <code>b_spend</code>). Wallets requiring confidentiality against the | ||
| scanning server must scan locally. | ||
|
|
||
| ==Test Vectors== | ||
|
|
||
| All byte strings are hex. Receiver keys are given as raw scalars for reproducibility | ||
| (a wallet would derive them per the key derivation path above); txids are in internal | ||
| byte order: | ||
|
|
||
| <pre> | ||
| b_scan = 1111111111111111111111111111111111111111111111111111111111111111 | ||
| b_spend = 2222222222222222222222222222222222222222222222222222222222222222 | ||
| </pre> | ||
|
|
||
| Two eligible inputs: | ||
|
|
||
| <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 values (<code>outpoint_L</code> = input 0, the lexicographically smaller): | ||
|
|
||
| <pre> | ||
| A = 031195a8046dcbb8e17034bca630065e7a0982e4e36f6f7e5a8d4554e4846fcd99 | ||
| input_hash = d392922c00280a7e8d282182f5026f2fddbc74c1e1de18b4822128b2b77ec641 | ||
| </pre> | ||
|
|
||
| Per-output values: | ||
|
|
||
| <pre> | ||
| k = 0: | ||
| P_k = 02a29d9716417c964ca9e477343e71ffe730a4991a3eaad668eabec84e9feb7931 | ||
| BK_k = 0344e1289497e6da66fde710d2f38de053fc07355e405524401d7d609df5a1a8cc | ||
| bk_k = 70ab8897b64bd21b427339ff4d014b883191ef6425862246c53bfc27a59aa3f0 | ||
| spend priv = f03c436d2cd67ae1fecf7d88a38aa3a03c0abea43feaf6da8eb71e2e3a866bda | ||
| scriptPubKey = 5120a29d9716417c964ca9e477343e71ffe730a4991a3eaad668eabec84e9feb7931 | ||
|
|
||
| k = 1: | ||
| P_k = 0229d77654023af267dbe9cb7ff1956f947c816f203494381308387168fb010c92 | ||
| BK_k = 03efdeda770ccdbe8bf466fba48bfd2b2c436ab0c04658fc6d6c277de5078129fa | ||
| bk_k = 945ba73a9804f62089c7d2ffdc079031031f0aebab372cec17ef9c110ebceb10 | ||
| spend priv = 9eff3472230fc83ef5ea8f8c80401c4eecd595a048bd2482a107d3a49baa5a58 | ||
| scriptPubKey = 512029d77654023af267dbe9cb7ff1956f947c816f203494381308387168fb010c92 | ||
| </pre> | ||
|
|
||
| Mainnet address (HRP <code>lqsp</code>): | ||
|
|
||
| <pre> | ||
| lqsp1qqd8n2k7uklxq4aegau7vawtptkgxsja4kt99lpv6krctwpq8tpc65qjxd4lu4etruh9sngx3su9mtqp5fqzxz7re59y5nnez9p03ht3lyudcfhfe | ||
| </pre> | ||
|
|
||
| Because blinding factors are randomized per output, the blinded output is not | ||
| byte-reproducible. What matters is that an output blinded to <code>BK_k</code> | ||
| unblinds under <code>bk_k</code> and under no other key. | ||
|
|
||
| ==Backwards Compatibility== | ||
|
|
||
| No consensus change. A silent-payment output is an ordinary confidential Taproot output, | ||
| spent by an ordinary key-path signature, so existing relay and validation rules apply. | ||
| Wallets that don't implement this are unaffected. | ||
|
|
||
| ==Reference Implementations== | ||
|
|
||
| * Rust: [[elip-silent-payments-liquid/rust/src/lib.rs|elip-silent-payments-liquid/rust/src/lib.rs]] | ||
| * Python: [[elip-silent-payments-liquid/python/elip_sp_reference/core.py|elip-silent-payments-liquid/python/elip_sp_reference/core.py]] | ||
|
|
||
| ==Acknowledgements== | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. JAN3 should be in the acknowledgements if this BIP is awarded the bounty |
||
|
|
||
| This specification builds on BIP-352, the BIP-352 index server specification<ref name="indexserver" />, | ||
| and the Confidential Transactions work of the Elements Project<ref name="ct" />. | ||
|
|
||
| The authors thank JAN3 for sponsoring and supporting this work. | ||
|
|
||
| ==References== | ||
|
|
||
| <references /> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| # elip-silent-payments-liquid — reference implementations | ||
|
|
||
| Reference implementations for [`elip-silent-payments-liquid.mediawiki`](../elip-silent-payments-liquid.mediawiki), | ||
| following the layout convention of [bitcoin/bips](https://github.com/bitcoin/bips) | ||
| (e.g. `bip-0352/`): supporting code lives in a folder named after the ELIP. | ||
|
|
||
| Two independent implementations derive the same test vectors byte-for-byte: | ||
|
|
||
| - [`rust/`](rust) — built on `lwk_wollet`. `cargo test` to run. | ||
| - [`python/`](python) — in the canonical BIP-352 `reference.py` style, using the | ||
| pure-Python `secp256k1lab` for the curve algebra and `wallycore` only for the | ||
| Liquid Confidential Transactions plumbing. See [`python/README.md`](python/README.md). | ||
|
|
||
| Both cover: | ||
|
|
||
| - **Address encoding** — Bech32m, HRP `lqsp`/`tlqsp`, version `q` | ||
| - **Sender derivation** — input aggregation, ECDH shared secret, `P_k`, `BK_k`, `bk_k` | ||
| - **Tweak server** — `T = input_hash · A`, publish, and client-side `S = b_scan · T` | ||
| - **Receiver scanning** — recompute `P_k`, match against outputs, derive spend secret | ||
| - **Confidential output blinding and unblinding** — the ELIP's novel claim: `bk_k` | ||
| derived from the shared secret unblinds the output non-interactively | ||
| - **Test vectors** — deterministic known-answer values matching the ELIP specification | ||
|
|
||
| ## License | ||
|
|
||
| BSD-3-Clause. |
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this file seems to be committed by mistake |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| # Virtual environments | ||
| .venv/ | ||
| venv/ | ||
|
|
||
| # Build / packaging artifacts | ||
| *.egg-info/ | ||
| build/ | ||
| dist/ | ||
|
|
||
| # Caches | ||
| __pycache__/ | ||
| *.py[cod] | ||
| .pytest_cache/ |
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same. no need of an extra readme for the reference impl |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| # Python reference implementation | ||
|
|
||
| Written in the style of the canonical | ||
| [BIP-352 reference](https://github.com/bitcoin/bips/blob/master/bip-0352/reference.py): | ||
| the elliptic-curve algebra is done with the pure-Python | ||
| [`secp256k1lab`](https://github.com/secp256k1lab/secp256k1lab), so the spec math | ||
| reads literally — | ||
|
|
||
| ```python | ||
| P_k = B_spend + t_k * G # output public key | ||
| S = input_hash * a * B_scan # sender's shared secret | ||
| a = -a if is_taproot and odd_y else a # BIP-341 even-Y normalization | ||
| ``` | ||
|
|
||
| It re-derives the ELIP's test vectors byte-for-byte (the same constants as the | ||
| sibling Rust [`reference`](../rust) implementation). | ||
|
|
||
| > **INSECURE.** `secp256k1lab` is slow and not constant-time, intended for | ||
| > prototyping, test vectors, and education only. Do not use in production. | ||
|
|
||
| ## What's BIP-352 and what's new | ||
|
|
||
| Everything up to the output public key `P_k` is plain BIP-352. The two | ||
| Liquid-specific additions are: | ||
|
|
||
| 1. **The output blinding key** `bk_k = tagged_hash("LiquidSilentPayments/Blind", | ||
| serP(S) || ser32(k))`. Both parties derive it from the shared secret `S`, so | ||
| the receiver can unblind the Confidential Transactions output with no | ||
| out-of-band data. | ||
| 2. **The CT plumbing** (`build_confidential_sp_txout` / `unblind_output`) — the | ||
| one place needing Liquid primitives, so it (and only it) uses `wallycore`. | ||
| Everything else is pure `secp256k1lab`. | ||
|
|
||
| ## Layout | ||
|
|
||
| ``` | ||
| elip_sp_reference/ | ||
| __init__.py public API re-exports | ||
| core.py the protocol — addresses, inputs, shared secret, derivation, | ||
| tweak server, and CT blinding/unblinding | ||
| bech32m.py Bech32m, verbatim from BIP-350 / the BIP-352 reference | ||
| tests/ | ||
| test_vectors.py byte-pinned vectors + taproot/tweak-server/address/CT round-trips | ||
| ``` | ||
|
|
||
| ## Running | ||
|
|
||
| ```sh | ||
| python3 -m venv .venv | ||
| source .venv/bin/activate # fish: source .venv/bin/activate.fish | ||
| pip install -e '.[test]' | ||
| pytest -v | ||
| ``` | ||
|
|
||
| `secp256k1lab` is fetched from git (it is not published on PyPI). `wallycore`'s | ||
| PyPI wheels are built with Elements support, which the CT test needs; if it is | ||
| not installed, that one test is skipped automatically. |
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need this file in the reference? |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,56 @@ | ||||||||
| """Silent Payments for the Liquid Network — Python reference implementation. | ||||||||
|
|
||||||||
| Written in the style of the canonical BIP-352 reference, using the pure-Python | ||||||||
| ``secp256k1lab`` for the curve algebra (so the spec math reads literally) and | ||||||||
| ``wallycore`` only for the Liquid-specific Confidential Transactions plumbing. | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
|
|
||||||||
| INSECURE — prototyping and test vectors only. See :mod:`elip_sp_reference.core`. | ||||||||
| """ | ||||||||
|
|
||||||||
| from .core import ( | ||||||||
| TAG_INPUTS, | ||||||||
| TAG_SHARED_SECRET, | ||||||||
| TAG_BLIND, | ||||||||
| SP_ADDRESS_VERSION, | ||||||||
| ser_uint32, | ||||||||
| hrp_for, | ||||||||
| encode_silent_payment_address, | ||||||||
| decode_silent_payment_address, | ||||||||
| sum_input_privkeys, | ||||||||
| get_input_hash, | ||||||||
| sender_shared_secret, | ||||||||
| receiver_shared_secret, | ||||||||
| output_tweak, | ||||||||
| output_pubkey, | ||||||||
| output_spend_privkey, | ||||||||
| blinding_privkey, | ||||||||
| script_pubkey, | ||||||||
| compute_tweak, | ||||||||
| shared_secret_from_tweak, | ||||||||
| build_confidential_sp_txout, | ||||||||
| unblind_output, | ||||||||
| ) | ||||||||
|
|
||||||||
| __all__ = [ | ||||||||
| "TAG_INPUTS", | ||||||||
| "TAG_SHARED_SECRET", | ||||||||
| "TAG_BLIND", | ||||||||
| "SP_ADDRESS_VERSION", | ||||||||
| "ser_uint32", | ||||||||
| "hrp_for", | ||||||||
| "encode_silent_payment_address", | ||||||||
| "decode_silent_payment_address", | ||||||||
| "sum_input_privkeys", | ||||||||
| "get_input_hash", | ||||||||
| "sender_shared_secret", | ||||||||
| "receiver_shared_secret", | ||||||||
| "output_tweak", | ||||||||
| "output_pubkey", | ||||||||
| "output_spend_privkey", | ||||||||
| "blinding_privkey", | ||||||||
| "script_pubkey", | ||||||||
| "compute_tweak", | ||||||||
| "shared_secret_from_tweak", | ||||||||
| "build_confidential_sp_txout", | ||||||||
| "unblind_output", | ||||||||
| ] | ||||||||
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.
This should be moved to this repo. Follow the convention in https://github.com/bitcoin/bips/blob/master/bip-0352/reference.py and use MIT license
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.
Okay ignore my comment about MIT license as other ELIPS are BSD license as well
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.
Do you mean the reference implementations? I noticed all the other ELIPs had external reference implementations so did it that way.
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.
I think they are almost always in https://github.com/ElementsProject/elements and thats why. Having them in a personal repo seems wrong or atleast different from the conventions in the bips repo