features/silent-payments - #174
Draft
42Pupusas wants to merge 12 commits into
Draft
Conversation
Derivation coordinates (SilentPaymentAccount), scan-only material with no path to b_spend (SilentPaymentScanMaterial), and the PSET proprietary-key metadata a wallet attaches for a signer (SilentPaymentInputMeta), behind a new silentpayments feature. Living in lwk_common lets wallet and signer share the boundary without depending on each other.
It was hardcoded to all-zeros. Correct by accident today: this function only reaches BIP-143 v0 sighashes, which never commit to the genesis hash. The regression test pins both facts so a future taproot path cannot silently inherit the wrong constant.
A new silentpayments feature adds SilentPaymentSigner (scan-material export that never returns b_spend) and a silent-payment phase inside the ordinary Signer::sign: it reads untrusted SilentPaymentInputMeta off each PSET input, verifies the named account and that the tweaked key reproduces the Taproot output being spent, and only then signs. Hardware signers refuse loudly (no such protocol operation exists yet).
Shared fixtures for building deterministic transactions, outpoints and secret keys, used by the silent payment unit tests in lwk_wollet.
tests/e2e.rs imports electrum/esplora/amp0 types at file scope and the list_transactions example uses the electrum client, so feature-off builds of --all-targets failed. required-features skips the targets instead; --test e2e -- --list under default features still shows every test.
The full scan/derive/track stack behind a new silentpayments feature: bech32 lqsp addresses, sender/receiver output derivation with CT unblinding via the shared secret, input aggregation with BIP-352 eligibility, per-tx and per-block scanning (locally computed or tweak-server assisted), the cache entry and utxo views, and recipient resolution for the transaction builder. Pinned against the ELIP known-answer vectors.
WolletBuilder accepts scan-only material and a birthday; the cache tracks found outputs by script alongside descriptor-derived ones so utxos/balance/spent bookkeeping need no special cases; Update version 6 carries discovered outputs and scan progress; scan material participates in wallet status so persistence cannot mix wallets. TxBuilder gains silent payment recipients (resolved at finish, when the final inputs are known), standalone SP utxos, correct Taproot key-path weight for fee estimation, and PSET annotation that attaches verified signing metadata for the signer. Empty SP state hashes exactly as before the feature existed, keeping every persisted wallet valid.
A new Capability::SilentPayments gates discovery: script-history backends can never surface SP outputs, so backends without it honestly refuse rather than silently missing money. The Esplora client earns it by computing per-block partial tweaks locally, reading every prevout script from the paged /block/{hash}/txs listing (one page per 25 txs instead of one request per prevout, which public instances 429). Discovery progress is tracked separately from the tip so a wallet that scanned before configuring keys does not skip history, and resumes from the birthday when set. Waterfalls descriptor filtering cannot serve SP scripts, so its client only advertises the capability through the underlying Esplora path.
The e2e suite runs the full lifecycle against a real elementsd + esplora: send to an lqsp address, discover through full_scan, and spend the discovered output back through the ordinary signer flow. Negative coverage pins that a stranger discovers nothing and that a wallet without scan material claims no discovery. The stray electrum_oidc TODO is dropped: silent payments now exercise the feature-gating CI story it was standing in for.
The feature shipped default-on, which quietly enrolls every downstream consumer of lwk_wollet in a new protocol before it has settled. Opt-in matches lwk_common and lwk_signer, where it always was. CI only builds default features, so the code would otherwise compile nowhere: add an explicit feature-on test run covering the three crates.
The ELIP does specify a derivation path, so 'has no derivation path' read as if it contradicted the spec. The path reaches b_spend; the key that signs is b_spend + t_k, tweaked by the paying transaction and so off any BIP-32 path. Comments and changelog now say that.
Author
|
Worth noting I didn't build on #170 as it takes a different shape: a parallel |
42Pupusas
force-pushed
the
features/bip-352-silent-payments
branch
from
August 3, 2026 14:51
5083da4 to
e421b00
Compare
The k loop and the gap counter live in the scanner, which matches on scriptPubKey alone and never sees a TxOut; unblinding happens afterwards in the tx scanner, so a failed unblind drops the utxo without rewinding k. That is the ELIP rule, but nothing pinned it: folding the unblind into the scan loop would let a sender hide every later output of a transaction behind one matching but unblindable output at k = 0. The test fails if the unblind failure is turned into a break.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Enables Silent Payment address creation, scanning and spending capabilities on
WolletandSwSigner, motivated by JAN3's bounty, and following the ELIP proposal.Wolletis configured with scan-only material (b_scanplus the publicB_spend), so it never holdsb_spend. Scan results carry aSpendTweak, never a completed key. Correctness is verified publicly, asB_spend + spend_tweak*G == output spend pubkey.Spending goes through
SwSigner::sign(), which re-derivesb_spendinternally, re-verifies the wallet's tweak against the Taproot output actually being spent, and only then signs. The wallet attachesSilentPaymentInputMeta(account + spend tweak + expectedB_spend; never a key) as a PSET proprietary key. The signer treats that metadata as untrusted input and validates it before use.BlockchainBackend::scan_silent_payments()finds payments through a light client. Backends advertiseCapability::SilentPaymentsand answersilent_payment_tweaks()with each block'sT = input_hash·A; the wallet derives the candidate scripts those tweaks would produce for its own keys, then confirms them with an ordinary script-history query.EsploraClientearns the capability by computing tweaks from blocks locally, with no server-side change required. It reads prevout scripts from the paged/block/{hash}/txslisting (one request per 25 txs rather than one per prevout, which gets public instances to 429).A backend without the capability returns Error::SilentPaymentsUnsupportedByBackend rather than an empty result: plain script-history backends can never surface these outputs, so they refuse honestly instead of silently missing money. Waterfalls' descriptor filtering cannot serve SP scripts, so it only advertises the capability via the underlying Esplora path.
Discovery runs as part of
full_scan(). How far it has progressed is tracked separately from the wallet tip, because a wallet that scanned before its scan material was configured must not treat that history as already searched; with a birthday set, it resumes from there. Findings travel in theUpdate(wire version 6) so they persist and replay on restore like anything else a scan finds.Compatibility
Older
Updateversions decode unchanged.Empty silent-payment state hashes exactly as it did before the feature existed, so every already-persisted wallet stays valid. There are tests pinning this specifically.
Wollet::status()incorporatesB_scanandB_spend(never a secret), so changing signer or account correctly invalidates an incompatible persisted cache.The feature is opt-in (silentpayments, in all three crates), so nothing changes for existing consumers until they ask for it. The feature-off build carries no dead state, and the default workspace build/test/clippy are unaffected.
Reviewing
The diff is large (~7.8k added), so the history was rebuilt to be read commit by commit, and while drafted here as a single PR for e2e testing , could be split cleanly into two PRs at
ae600d6a(boundary + signer, then wallet).