Full Silent Payment Implementation - #145
Conversation
|
It's great to make it more digestible for review! |
08a5d40 to
2d1da69
Compare
I did include all of your fixes. a few might have been dropped during the cleanup since I removed a number of functions we no longer needed and moved helper functions that were only used by tests into their respective test files. if you notice anything specific that's missing, let me know and I'll take another look. (i squashed the |
043a824 to
3105a96
Compare
3105a96 to
c9f4918
Compare
f20926b to
9f26ec7
Compare
6443846 to
dcd028b
Compare
macgyver13
left a comment
There was a problem hiding this comment.
Lots of great work here! The single signer flow appears to be nearing completion.
This review focuses on the silent-payments specific code. Rather than scatter comments across the diff, I pushed a review branch:
notTanveer/embit@feat/silent-payments-V2...macgyver13:embit:review/silent-payments-V2
Two commits address issues that surfaced during a Claude-assisted review, makes sense to me but you may want to dig deeper:
- harden resolve BIP32 derivation match with our UTXOs - _resolve_bip32_privkey didn't bind the derived key to the input's scriptPubKey, so a PSBT claiming a path we own for an input we don't control would fold that key into a_sum. Comparison now includes parity.
- raise SPValidationError for invalid Silent Payment spend inputs in sign_with() - sign_input_with_sp_tweak returned 0 for a bad tweak. BIP-376 requires signer to fail.
The bulk of the branch is a larger change moving validation logic under the psbt.sign_with() public API, so downstream consumers get the validation rather than re-implementing it. Per-input share test vectors are explicitly disabled until that work is ready.
Doing the same for the BIP-352 vectors would likely turn up more. Happy to take a pass at that if it's useful. Reuse as much or as little of the branch as you like.
dcd028b to
e101ce5
Compare
e101ce5 to
0efca34
Compare
There was a problem hiding this comment.
AI review findings fixed in, 59908f5:
SilentPaymentsPSBT.sign_with() was crashing when it hit a key that couldn't sign. _resolve_sp_privkeys() was dropping the can_sign flag returned by resolve_signing_root(), so a public-only descriptor key ended up in resolve_input_privkey(), where hasattr(root, "secret") raises ArgumentError for a descriptor Key instead of just returning False.
PSBT.sign_with() should return 0 in this case. There was also a worse case with multi-key descriptors: if an earlier private key had already written ECDH shares, DLEQ proofs, and output scripts, hitting the public key would throw and leave the PSBT half-derived.
Also cleaned up a few related things:
- Moved the duplicated
_update_tx_modifiablelogic fromPSBTandPSBTViewinto a sharednext_tx_modifiable(). - Removed the dead
tx_modifiable_flagsblock inPSBTView.sign_with(), which could only run after nothing was signed. - Removed the unused
NETWORKSimport.
|
Branch needs to be rebased, as a PSBTv2 related hardening was committed to master |
- replaced None with error messages. - made Bech32DecodeError an EmbitError. - Subclass EmbitError so existing except EmbitError callers still catch malformed-address errors (e.g. script.address_to_scriptpubkey). - Restore the BIP-141 2-40 byte witness program limit in decode() and drop the sp/tsp special-casing; SP addresses are not witness programs. - Replace the 118 length cap with BIP-352's recommended 1023 so valid higher-version silent payment addresses decode; cite BIP-173/BIP-352.
Co-authored-by: odudex <5049478+odudex@users.noreply.github.com>
Co-authored-by: odudex <5049478+odudex@users.noreply.github.com>
Co-authored-by: odudex <5049478+odudex@users.noreply.github.com>
Co-authored-by: odudex <5049478+odudex@users.noreply.github.com> Co-authored-by: macgyver <4712150+macgyver@users.noreply.github.com>
Co-authored-by: odudex <5049478+odudex@users.noreply.github.com>
SilentPaymentsPSBT.sign_with() aborted with ArgumentError on a public-only descriptor key. _resolve_sp_privkeys() discarded the can_sign flag from resolve_signing_root(), so such a key reached resolve_input_privkey(), whose hasattr(root, "secret") probe raises on a descriptor Key instead of returning False; PSBT.sign_with() returns 0 in the same situation. With a multi-key descriptor a private key earlier in root.keys had already written ECDH shares, DLEQ proofs and output scripts before a later public key threw, leaving the PSBT half-derived. Honour can_sign before the SP send path runs. Also: - Extract next_tx_modifiable() in psbt.py. PSBT and PSBTView each carried a byte-identical copy of the BIP-370 sighash -> flag rules, so a later correction to one would silently leave the constrained-device path wrong. Going through a pure function also removes a None &= -2 TypeError on a PSBTView built with version=2 and no global_kvs, whose flags setter no-ops. - Drop the trailing tx_modifiable_flags block in PSBTView.sign_with(). Every path in sign_input() that adds a signature already updates the flags, so the block could only fire after signing nothing, stamping TX_MODIFIABLE=0 onto a PSBT this pass never touched. PSBT.sign_with() does not do this. - Remove the unused NETWORKS import in silent_payments/sp.py.
… rebase Rebasing feat/silent-payments-V2 onto upstream/master's PSBTv2 hardening series (eb6104f) merged in upstream's declarative _validate_global_key, V2_FIELDS/_validate_key scope checks, and _read_global_value eager-shape validation, but left _validate_global_fields (canonical count-field encoding, fixed-length checks, required-field presence) unwired in both PSBT.read_from and PSBTView.view. Malformed PSBT_GLOBAL_INPUT_COUNT / PSBT_GLOBAL_OUTPUT_COUNT values crashed with RuntimeError/ValueError instead of PSBTError, non-canonical compact encodings went unrejected, and PSBTView accepted a v2 PSBT missing PSBT_GLOBAL_TX_VERSION until something later touched the lazy .tx_version property. Wire _validate_global_fields into both parse paths, replacing the now-duplicate manual v0/v2 field-exclusion checks it subsumes. All 25 of upstream's tests/tests/test_psbt_versions.py cases now pass.
137a493 to
839c5ab
Compare
BIP-352 makes H the single exception to "the sending wallet MUST have access to the private key corresponding to the taproot output key", so a NUMS internal key is the one PSBT field that can drop an input from a_sum. It was believed on faith, and believing it is silent: an excluded input never reaches _resolve_sp_privkeys to be missed, yet the signer still key-path signs it, so the recipient - who scores eligibility from the on-chain witness - derives a shared secret we never used and never finds the payment. The claim now counts only once it reproduces the scriptPubKey, which is all the UTXO vouches for - the doctrine _p2sh_redeem_script and the bip32 hash160 binding already apply. Unproven, the input stays eligible: a false claim about an input we control then resolves normally, and a true one has no key to resolve and aborts the send. A BIP-341 tweaked NUMS lands there too, which is correct - such a transaction cannot be a silent payment at all. Also in this pass: - Generate every DLEQ proof before writing any of them. A proof failure used to leave shares without proofs, a state validate_sp() rejects forever, so the PSBT could never be signed again. - Restore the clear_metadata contract: KEEP_ALL is a no-op, and sp_data is dropped only once script_pubkey exists. Before the send it is the output's whole definition, and dropping it produced a PSBT that could no longer be parsed. - Reject malformed and PSBTv0 global SP fields instead of retaining them as unknowns and re-serializing them verbatim, matching the input and output paths. - Refuse derive_sp_outputs_from_keys() on a PSBT with no SP outputs instead of clearing the global shares and TX_MODIFIABLE and returning as if it had succeeded. - Range-check the silent payment address version, which bech32_encode indexes CHARSET with: 32 raised a bare IndexError and -1 wrapped to 31. - Drop the DLEQ verification claim from the validate_sp() docstring; the check needs A_sum, which the PSBT does not carry. _verify_declared_sp() does it on the send path.
This PR contains the complete Silent Payments implementation with a clean, linear commit history to make the review process easier.. covers PRs #64, #87, #95, #96, #125, #126, #130, #133,
also includes a number of fixes by @odudex , squashed into their corresponding
feat:commits to make the review process easier.