Add Silent Payments for the Liquid Network - #37
Conversation
A Standards Track draft specifying BIP-352 Silent Payments adapted to the Liquid Network's Confidential Transactions. Each normative rule is tagged as BIP-352-derived, a Liquid adaptation, or an open design choice (stated preferentially). Includes worked test vectors and abstract data structures.
|
|
||
| ==Reference Implementation== | ||
|
|
||
| A reference implementation demonstrating address encoding, input aggregation, |
There was a problem hiding this comment.
A reference implementation in python would be nice to review this ELIP
There was a problem hiding this comment.
I have working examples using LWK in Rust, is that acceptable? not too familiar with Python
There was a problem hiding this comment.
There was a problem hiding this comment.
I attempted to wire in everything together in a Python implementation, but the blinding logic for Confidential Transactions exposed on the Rust crate is not as readily available on python tooling, and my skills in python are not good enough to rewire everything to ensure it will work correctly.
I have tried to clean up the Rust implementation and added more comments throughout mapping to the ELIP documentation. I hope that will sufficient?
There was a problem hiding this comment.
Have you tried using wallycore? If that doesnt work maybe we can replace LWK with a more lighter elements crate?
| 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 |
There was a problem hiding this comment.
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.
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.
Trim BIP-352 re-explanation to focus on Liquid-specific differences: - Shorten abstract and motivation prose - Reduce BIP-352-compliant sections to brief restatements - Remove Rationale section (reasoning is stated inline in each [Liquid]/[Choice] design section)
The flow is essentially identical to BIP-352; fold the three subsections (filter omission, server interface) into one paragraph plus the interface example.
Switch silent-payment outputs from confidential P2WPKH to confidential Taproot (OP_1 <x_only(P_k)>), with P_k used directly per BIP-352 (no script tree, no taptweak). This aligns the output, spend path, and index-server data with BIP-352's x-only conventions verbatim. - Output is now Taproot-only; eligible inputs keep BIP-352's full set (P2TR, P2WPKH, P2SH-P2WPKH, P2PKH), so an SP output is itself eligible as a later input and the even-Y rule applies unchanged. - Spending is an ordinary BIP-340 key-path spend (even-Y normalized). - Collapse the now-pure-BIP-352 sections (input aggregation, eligible inputs, shared secret, output representation) into one consolidated 'Reused from BIP-352 unchanged' section. - Update test vectors (Taproot scriptPubKeys) and reference-implementation note (verified against LWK, reproduces vectors byte-for-byte).
…ls section The previous lq/tlq HRP collided with ordinary Liquid CONFIDENTIAL addresses (blech32 lq/tlq), defeating the distinct-HRP goal. Switch to lqsp/tlqsp, which differ from every existing Liquid HRP (ex/tex unconfidential, lq/tlq confidential) and from Bitcoin's sp/tsp. Update the test-vector address accordingly (verified against LWK). Also fold the Labels section into the consolidated BIP-352 reused section (labels need no Liquid adaptation); the one substantive note — the blinding key is label-independent — moves to the blinding-key section.
Reduce the Reference Implementation paragraph to what the public reference covers: it reproduces the test vectors byte-for-byte and demonstrates non-interactive unblinding of the shared-secret-blinded output. Wallet integration (scanning, signing, transaction building) is left to implementations, rather than enumerated here.
| non-interactively by the receiver. Wallet integration — scanning, signing, and | ||
| transaction building — is left to implementations. | ||
|
|
||
| ==Acknowledgements== |
There was a problem hiding this comment.
JAN3 should be in the acknowledgements if this BIP is awarded the bounty
| tweaks, which does not exist in common protocols today; until then, silent-payment | ||
| outputs are usable with software signing. | ||
|
|
||
| ==Reference Implementation== |
There was a problem hiding this comment.
The point of the reference implementation in the BIP is like a sort of documentation. I will review your rust code but I feel it should be in python, or something thats close to human readable code. You can check https://github.com/bitcoin/bips/blob/master/bip-0352/reference.py
There was a problem hiding this comment.
There should also be implementation of the tweak server and decryption by the wallet
There was a problem hiding this comment.
Had a cursory look at https://github.com/42Pupusas/elip-sp-reference/blob/main/src/lib.rs and it looks good. Im just concerned if rust is human readable enough for a BIP.
There was a problem hiding this comment.
Ok the wallycore for sure was the easier path in Python. I reused most of the BIP-352 reference implementation that uses libsecp256klab and a bech32 impl from Peter Wuille, then added wallycore blinding functions where appropriate to create a parallel Python reference implementation.
I am not a good judge of whether the Python code is more human readable as I have strong biases against Python and for Rust, but that particular bikeshed is not worth exploring, so I do hope the review is easier with the new Python code.
| # 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]'''. |
There was a problem hiding this comment.
Actually, we need to think harder on this. On-chain, there is actually some Taproot usage, so SP payments get masked. But on liquid, if there is no usage at all, we might run into issues.
A taproot output will definitely be SP in liquid I think. We need to analyse usage
There was a problem hiding this comment.
A quick 500 block scan shows that Taproot usage is very small, around 3% .
So a TR output won't "definitely" be SP, but the anonymity set is quite small.
Is this enough nudge to drop taproot and use p2pkh instead?
There was a problem hiding this comment.
I feel P2PKH is better, but I would like to get a second opinion on this. More specifically, understand why the mainchain decided to use Taproot.
There was a problem hiding this comment.
I was digging through the old BIP352 threads last weeks to get more clarity for the taproot output being the default.
I was unable to find much discussion around it, but here is some explicit comments on it I found:
-
Forward Looking MuSig Interop - Taproot makes MuSig/FROST implementations simpler/possible
-
Scanning Efficiency Concerns - The TR Tweak makes scanning easier when the payment has several inputs.
Also to consider from the implementation difference is that with TR output , the script is the x-only key, so we only do direct x-coordinate comparison. With P2PKH the scanner has to serialize compressed, HASH160, then compare. The overhead is not immense but should be considered.
All three have their merits I can see, just unsure of the tradeoff with anonymity set size in Liquid.
There was a problem hiding this comment.
Also the BIP-352 itself generalizes the choices to just
"^ Why only taproot outputs? Providing too much optionality for the protocol makes it difficult to implement and can be at odds with the goal of providing the best privacy. Limiting to taproot outputs helps simplify the implementation significantly while also putting users in the best eventual anonymity set."
There was a problem hiding this comment.
IMO it's more important that SP for elements/liquid be directly derived from BIP-352. This increases security and it inherits all the review from upstream and can help with re-use of tooling etc.
Currently there is much more P2PKH than TR, but that will likely/hopefully change (as it will on Bitcoin with e.g. more usage of L2 systems using TR addresses and LN using TR addresses).
Also, the anonymity of SP addresses within the larger set of TR addresses is a nice-to-have additional feature, and not the intended privacy goal of SP.
| 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. |
There was a problem hiding this comment.
| * '''[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. | |
| * '''[Liquid]''' — the rule is an adaptation due to Confidential Transactions and Liquid's deployed output types. |
Theres still a lot of AI generated verboseness, imo. I feel they can be cleaned up and we dont need to add anything new in this BIP than whats absolutely needed.
For me it feels like ===Output blinding key '''[Liquid]'''=== is the only real specification in this doc and the remaining spec is just to use exisiting BIP 352 logic. Everything can be condensed imo
There was a problem hiding this comment.
will push a condensed revision once a path has been settled for TR vs P2PKH
There was a problem hiding this comment.
Please do not wait for me to get back to make the changes as I may take a while to reply and another reviewer might want to review this, etc. Try to make the PR as up to date as possible whenever you get time to work on it.
There was a problem hiding this comment.
Condensed the ELIP to only the specific changes needed for Liquid, reusing and adoption the notation conventions in BIP-352.
| bk_k = hashLiquidSilentPayments/Blind( serP(S) || ser32(k) ) (a 32-byte scalar) | ||
| BK_k = bk_k·G |
There was a problem hiding this comment.
This bit is still not clear to me.
A and S can be computed by a third party. What is k?
Actually what is hashLiquidSilentPayments/Blind( serP(S) || ser32(k) ) ? can we just write the math and not generic fn names
There was a problem hiding this comment.
k is the output index counter the sender chooses and keeps for a specific SP address (0 first payment, +1 recurring), not transmitted on-chain.
A third party who knows the receiver's address can compute A and S from public data, but to derive the blinding key for a specific output they'd have to brute-force the gap limit × every address × every transaction.
can we just write the math and not generic fn names
is following the notation style in BIP-352 ok?
so changing to:
tag = "LiquidSilentPayments/Blind"
bk_k = int( SHA256(SHA256(tag)||SHA256(tag) || serP(S) || ser32(k) ) ) mod n
There was a problem hiding this comment.
-
Pin the exact ASCII tag string (LiquidSilentPayments/Blind) verbatim, since interop depends on every implementation hashing the identical bytes. A one-character difference silently breaks unblinding across wallets.
-
Define it the same way BIP-352 defines its own: state the BIP-340 tagged-hash construction once in the conventions section then use the named form hashLiquidSilentPayments/Blind(...). Please note that
LiquidSilentPayments/Blindshould be in the subscript like BIP-352
There was a problem hiding this comment.
A third party who knows the receiver's address can compute A and S from public data, but to derive the blinding key for a specific output they'd have to brute-force the
gap limit × every address × every transaction.
This is wrong. I was wrong earlier, a third party cannot compute S and that's the security. Brute-forcing isn't hard
There was a problem hiding this comment.
The new Privacy section should now be clearer and explicit about where the privacy and security assumptions come from.
Conventions: state the BIP-340 tagged-hash construction once and pin the exact ASCII tag strings (incl. LiquidSilentPayments/Blind, 26 bytes) in a table; switch the derivation steps to the named hash_<tag>() subscript form, matching BIP-352 and removing the define-after-use tag_* inline forms. Privacy: rewrite the rationale. The previous text wrongly claimed S is derivable from the public A and B_scan; that is the computational Diffie-Hellman problem. Privacy rests on the secrecy of b_scan; the address is not secret and the output index k is not secret. Align tweak terminology to BIP-352 (partial tweak -> tweak). README: add the ELIP index row.
276f423 to
28d6c18
Compare
| spent by an ordinary key-path signature, so existing relay and validation rules apply. | ||
| Wallets that don't implement this are unaffected. | ||
|
|
||
| ==Reference Implementations== |
There was a problem hiding this comment.
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.
Okay ignore my comment about MIT license as other ELIPS are BSD license as well
There was a problem hiding this comment.
This should be moved to this repo
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.
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
Move the Rust and Python reference implementations from the standalone
elip-sp-reference repo into elip-silent-payments-liquid/{rust,python}/,
following the bitcoin/bips convention of a folder named after the spec
(e.g. bip-0352/) holding its supporting code.
Update the ELIP's Reference Implementations section to link in-tree
instead of to the external repo.
|
A few comments: “internal byte order” of the specify explicitly handling if In the final version a BIP-32 path should be specifed (and will be different to BIP-352). |
…BIP-32 path - Define outpoint_L txid 'internal byte order' unambiguously (raw dSHA256 bytes as serialized in a tx input, reverse of RPC hex display) - Skip output index k when bk_k is 0 or >= n, mirroring BIP-352's t_k handling; update Python (from_bytes_nonzero_checked) and Rust references - Specify BIP-32 derivation path with SLIP-44 coin type 1776 (Liquid): m/352'/1776'/account'/1'/0 (scan), m/352'/1776'/account'/0'/0 (spend)
delta1
left a comment
There was a problem hiding this comment.
Hi @42Pupusas. This ELIP is missing a Copyright section. Some other review comments follow.
| <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 adds one thing on top: even someone who | ||
| holds <code>b_scan</code> learns only that an output is a silent payment, not its amount or asset. |
There was a problem hiding this comment.
The claim "even someone who holds b_scan learns only that an output is a silent payment, not its amount or asset" is false.
b_scan is sufficient to derive bk_k and fully unblind amounts. S = b_scan·(input_hash·A) is computable from b_scan alone plus public chain data, and bk_k = hash(serP(S)||k) mod n
There was a problem hiding this comment.
Ok I was getting confused I because of the DH secret step, but yes , this is correct, a light wallet/client that hands its b_scan key for non-interactive unblinding, will lose CT confidentiality.
Adding an explicit comment on the tradeoff.
| ** If <code>bk_k = 0</code>, increment <code>k</code> and continue | ||
| * 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 | ||
|
|
There was a problem hiding this comment.
Ambiguous "increment k and continue" when bk_k = 0.
Unclear whether P_k/scriptPubKey also re-derives with the new k, or only the blinding key recomputes. If misimplemented, sender and receiver derive different blinding keys for that output, making it unblindable.
Maybe clarify that P_k must also be rederived with new k
| nonce, | ||
| script_pubkey, | ||
| witness: TxOutWitness { | ||
| surjection_proof: None, |
There was a problem hiding this comment.
build_confidential_sp_txout sets surjection_proof: None in the TxOutWitness. Liquid consensus requires a surjection proof on every confidential output; without one the transaction is rejected by every node. The Python counterpart similarly produces no surjection proof in its returned dict. The tests pass because unblinding reads only the rangeproof, not the surjection proof — but any implementer copying this code would produce unbroadcastable transactions. A comment or a stub surjection proof should be added.
| eng.input(&lowest_outpoint(outpoints)); | ||
| eng.input(&a_pubkey.serialize()); | ||
| Scalar::from_be_bytes(InputsHash::from_engine(eng).to_byte_array()) | ||
| .expect("input_hash < curve order") |
There was a problem hiding this comment.
Rust panics instead of reducing mod n for hash ≥ n; Python reduces; implementations disagree.
Scalar::from_be_bytes(...).expect("input_hash < curve order") in input_hash (and identically in blinding_secret) panics on the ~2⁻¹²⁸ chance the hash output ≥ n. The Python reference uses Scalar.from_bytes_wrapping() which reduces mod n, matching BIP-352's spec text ("interpreted as a 256-bit big-endian integer mod n"). The two reference implementations have incompatible behavior on this edge case.
There was a problem hiding this comment.
After double checking BIP-352 Creating Outputs
If input_hash is not a valid scalar, i.e., if input_hash = 0 or input_hash is larger or equal to the secp256k1 group order, fail
So in this case I think the Rust behaviour is correct and its the Python ref that needs to fail instead of wrapping.
…scan-key privacy, outpoint example - Harmonize input_hash/t_k out-of-range handling with BIP-352 v1.0.2: fail, don't reduce mod n (Python switched to from_bytes_checked; Rust gains zero checks) - Document that build_confidential_sp_txout omits the consensus-required asset surjection proof and that wallets must add one at transaction assembly - Clarify bk_k skip: the whole output index k is skipped and both P_k and bk_k re-derive from the next index - Correct Privacy section: a b_scan holder can unblind outputs (bk_k derives from S), not merely detect them; local scanning required for confidentiality against a scanning server - Add a worked byte-level example of outpoint_L internal byte order
build_confidential_sp_txout now takes the transaction inputs' (generator, asset tag, abf) and produces a consensus-required surjection proof instead of omitting it — Rust via secp256k1_zkp::SurjectionProof::new, Python via wally.asset_surjectionproof. The Rust test verifies the proof against the input generators.
…itself wallycore's asset_surjectionproof is _wrap_bin-wrapped: it takes the 7 input arguments, sizes the proof via asset_surjectionproof_len internally, and returns the buffer. Passing bytes_out ourselves raised TypeError. Confirmed by running the Python suite with wallycore: 5/5 pass, surjection proof round-trips.
|
Appreciate all the reviews.
Both reference implementations updated to match, test vectors unchanged. |
|
@42Pupusas please see #42 as well which includes an LWK implementation and I think the efforts could be aligned and possibly split between you two (maybe not 50/50 but there should be some discussion) |
|
@delta1 not sure how to make the determination of the split, either for the work, or the bounty. while the reference implementation described here was not integrated directly with LWK (uses it as a dep instead) to keep the implementation self contained, the work to integrate has mostly been finished on my end, just didn't push to LWK as I was waiting for feedback on this issue I opened clarifying some details, which has not yet received any traction. I also don't see any clear deviations on the implementations that I could incorporate in to this spec, but happy to get suggestions on this as your comment on the other thread indicates . |
|
I will be representing JAN3. This PR is a more well-rounded and reviewed spec, so this should be the one that is merged. But I do see some new ideas in #42, like Input eligibility. @soloking1412 pls add your review to this PR on how it can be improved, and that will be used to judge how the bounty should be split @42Pupusas pls do not wait for LWK to get back to you, as the team is super busy. Pls make necessary assumptions and push the code. Code can be changed quickly if there is some assumption that was wrong, but waiting for human feedback isn't as quick. |
|
Happy to see this merged as the spec — it's the more mature document, and the structure of tagging each rule as BIP-352-derived vs. Liquid adaptation vs. open choice is genuinely good. The internal-byte-order clarification for @42Pupusas, answering your question about what could be incorporated — below is a review from having implemented this independently in LWK, ordered roughly by severity. I've checked each item against the current branch, after the 7 July round of fixes, so these are all still open. Happy to open PRs against your branch for any of them. Also FWIW I've already switched the LWK implementation's blinding tag to 1. Spec is written against BIP-352 v1.0.2 — missing
|
|
Should have linked it in the review above: the implementation these findings came from is Blockstream/lwk#170 (renamed since the earlier cross-reference in this thread). It's integrated into lwk_wollet rather than standalone — silent payment outputs are unblinded with the derived bk_k, exposed as ordinary wallet UTXOs, and spendable through the existing tx builder. It covers address generation and parsing, sending, scanning, labels, and the Tweak Server client (/getinfo, /tweaks/:height, /filters/:height) with BIP-158 filter matching, so the receive path runs end to end without sharing any key with the server. Conformance: the official BIP-352 send and receive test vectors pass in both directions, plus tests over real blinded, signed Liquid transactions to cover the blinding that BIP-352 doesn't. The blinding tag is already LiquidSilentPayments/Blind, matching this spec. Each of the findings above is enforced in that code — K_max on both the sender and the scanning loop, the vout flags cleared before hashing, peg-ins contributing their outpoint but not a key, and transactions spending witness version > 1 skipped entirely. Happy to port any of it into this spec's reference implementations. |
| 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.
| 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.
Why do we need this file in the reference?
| @@ -0,0 +1,292 @@ | |||
| """Silent Payments for the Liquid Network — Python reference implementation. | |||
|
|
|||
| Written in the style of the canonical BIP-352 reference | |||
There was a problem hiding this comment.
this file seems to be committed by mistake
There was a problem hiding this comment.
Lets follow the structure followed in https://github.com/bitcoin/bips/tree/master/bip-0352 . We dont need a toml file
There was a problem hiding this comment.
same. no need of an extra readme for the reference impl
|
|
||
|
|
||
| def compute_tweak(input_pubkeys: List[GE], outpoints: List[bytes]) -> Tuple[GE, Scalar, GE]: | ||
| """Compute (T = input_hash * A, input_hash, A) from the eligible input pubkeys.""" |
There was a problem hiding this comment.
The tweak does not seem to be specified in the ELIP. Can we add that?
…rence, specify Tweak Server
- Flatten python/elip_sp_reference/{__init__,core,bech32m}.py into a single
reference.py + bech32m.py alongside the ELIP, matching bitcoin/bips'
bip-0352/ flat-script layout. Drop the package's pyproject.toml, README.md,
.gitignore and __init__.py re-export shim per reviewer request.
- Trim verbose AI-generated-sounding docstrings/comments to what's needed.
- Remove the Rust reference implementation entirely (rust/) so there is one
reference implementation, in the BIP-352 reference.py style, as requested.
- Add a normative Tweak Server subsection to the ELIP: the server publishes
T = input_hash*A per transaction and derives S = b_scan*T; previously only
implemented in code (compute_tweak/shared_secret_from_tweak) but not
specified in the document.
- Update Reference Implementation section and README to match the new layout.
The tweak server is BIP-352's index server unchanged (T = input_hash*A per transaction, S = b_scan*T client-side); a standalone section restated that at length. Fold it back to one sentence in the Light-client scanning bullet, matching the doc's condensed style for other unchanged-from-BIP-352 pieces.
Split into its own bullet (rather than tacked onto light-client scanning) and cut to the essentials: what's unchanged from BIP-352, and the two equations that matter (T = input_hash*A, S = b_scan*T).
Fixes for four hard-technical issues, cross-checked against primary sources (BIP-352 v1.1.0/reference.py, Elements primitives/transaction.h) before applying: 1. K_max = 2323 (BIP-352 v1.1.0's per-group recipient DoS mitigation) was entirely absent from spec and reference. Added as normative: sender MUST fail if a recipient group exceeds it, receiver MUST NOT scan past k=K_max. Confirmed present in both create_outputs() and scanning() of the upstream BIP-352 reference.py. 2. outpoint_L's vout was ambiguous for peg-in/issuance inputs: Elements overloads vout's high bits as OUTPOINT_ISSUANCE_FLAG (1<<31) and OUTPOINT_PEGIN_FLAG (1<<30), cleared on parse (confirmed in elements/src/primitives/transaction.h). Spec now states vout is always the parsed index with both flags cleared. 3. Input eligibility was undefined for peg-in inputs (BIP-352 has no notion of them, and a Liquid-only client can't read a Bitcoin-chain prevout's scriptPubKey to classify it). Made normative: peg-ins are ineligible (contribute no pubkey) but their outpoint still participates in outpoint_L; issuance/reissuance inputs are eligible under the unchanged BIP-352 rules. 4. The bk_k out-of-range handling contradicted BIP-352: the spec described 'skip to k+1' as mirroring BIP-352's t_k handling, but BIP-352 fails outright (Scalar.from_bytes_checked raises, no skip logic anywhere in the upstream reference.py's scanning()/create_outputs()). Changed spec and reference.py's blinding_privkey to fail, matching t_k's existing handling in the same file and eliminating the divergent-loop bug a receiver following BIP-352's scanning loop would hit. 6. The index-server ref (bitcoin/bips/blob/master/bip-0352/index-server.mediawiki) 404s; the spec lives at github.com/silent-payments/BIP0352-index-server-specification. Also noted that its dust_limit filtering doesn't apply on Liquid (amounts are confidential, so a server can't identify dust) — servers MUST report dust_limit = 0. Verified: uv run pytest -v elip-silent-payments-liquid/test_vectors.py — 5/5 pass unchanged (the blinding_privkey fail-vs-skip change is a no-op on the happy-path test vectors, as expected: P(bk_k out of range) ~ 2^-128). Left for separate triage: softer/stylistic items from the same review (BIP-158 filter rationale, regtest HRP collision with testnet, missing 'match on crypto not policy' scanning warning, additional test vector coverage for labels/peg-ins/testnet addresses, Layer header).
…esses Addresses review point ElementsProject#9 (test vector coverage gaps). The BIP-352 label mechanism was previously unimplemented and untested in the reference despite the ELIP claiming to inherit it 'exactly' from BIP-352; peg-in input eligibility was specified in the mediawiki (previous commit) but had no corresponding test; and testnet/regtest address sharing of tlqsp was only implicitly covered via test_address_round_trip's 'liquid-testnet' case with no explicit regtest check. reference.py: added label_tweak, labeled_spend_pubkey, and labeled_output_spend_privkey (B_m = B_spend + label_tweak(b_scan, m)*G, per BIP-352's Address encoding / Labels section, unchanged on Liquid). test_vectors.py: three new tests. - test_labels: sender/receiver spend-key agreement for a labeled address across several m values, plus m=0 (change) distinctness from m=1. - test_pegin_input_excluded_from_shared_secret: a peg-in's private key never enters the aggregate a_sum/A, but its outpoint can still be the lexicographically-smallest outpoint_L, changing input_hash even though it contributes nothing to the shared secret. - test_testnet_and_regtest_addresses: explicit assertion that 'liquid-testnet' and 'liquid-regtest' encode/decode identically under tlqsp, and both remain distinct from mainnet's lqsp. README.md: updated coverage list. Verified: 8/8 tests pass (up from 5/5), including the 3 new ones.
The claimed rationale ("CT doesn't blind scriptPubKey, so a client
matches candidate scripts directly") was a non-sequitur: BIP-158
filters are built from already-visible scriptPubKeys on Bitcoin too,
and exist purely as a bandwidth optimization to avoid downloading
full blocks while scanning — a client on Liquid faces the identical
problem BIP-352 Appendix A raises for Bitcoin. Confirmed BIP-157/158
(NODE_COMPACT_FILTERS, getcfilters/cfheaders/cfcheckpt, block filter
index, getblockfilter RPC) is present in Elements unmodified from
Bitcoin Core, so there is no actual divergence from BIP-352 to
document here; dropping the bullet rather than asserting a false
justification.
Point ElementsProject#5's original bullet was wrong (CT blinding scriptPubKey was never the relevant fact) and got dropped entirely last commit. But readers coming from BIP-352 need an explicit answer on whether BIP-158 filters still work on Liquid, not silence. Reinstate the bullet stating the actual, verified fact: Elements implements BIP-157/158 unchanged from Bitcoin Core (service bit, P2P messages, getblockfilter RPC all present), and filters are built from scriptPubKeys — which CT never touches — so light-client scanning for candidate P_k works identically to Bitcoin.
|
Appreciate the thorough review @soloking1412 . Incorporated most of the points, however I will push back on a few points.
This was intentional to mirror the BIP-352 spec as closely as possible, see Address Encoding section "The human-readable part "sp" for mainnet, "tsp" for testnets (e.g. signet, testnet)"
Would be restating/duplicating BIP-352 logic, an ongoing theme of the review process has been to not restate logic already in the BIP-352 spec.
I considered Wallet but the spec includes the tweak-server section, which is explicitly a server-side, non-wallet component |
The reference implementation's directory listing and running instructions duplicated content already covered by the ELIP spec itself and the top-level repo README; drop the extra file to avoid two sources of truth going stale against each other.
This is an ELIP spec repo, not a project meant to own Python tooling ignore rules; the reference implementation is a small standalone example, not something this repo's own .gitignore should be scoped around.
|
@emjshrx cleaned up the reference implementation directory to only have the python ref, and cut down the prose on the comments. only difference to BIP-352 now is they vendor in the secp256k1lab library in with the reference implementation, while here we install from github, is the vendoring needed? |
Thanks — and agreed on two of the three. 10 (Layer): fair enough, 7 (HRP): your call, and mirroring BIP-352 is a reasonable principle to hold to. One data point in case it shifts the balance: BIP-352 groups "signet, testnet", which is exactly the pair that shares the 8 (keep scanning on a match): I'd push back here, because the part I'm after isn't in BIP-352 to duplicate — BIP-352 has no unblinding step, so it can't cover this case. BIP-352's warning is about wallet policy, e.g. dust. On Liquid there's a second failure mode: after a cryptographic match at index One line covers it, and it's additive rather than a restatement:
Happy to open a PR with just that if it's easier. |
This ELIP specifies BIP-352 Silent Payments adapted to the Liquid Network's Confidential Transactions. Each normative rule is tagged as BIP-352-derived, a Liquid adaptation, or an open design choice (stated preferentially). Includes worked test vectors and abstract data structures.