Feat/sp sending#496
Conversation
Added encoders and decoders for the given silent payment fields. Since we add new fields that are not allowed by v0 we also need to delete them when converting to v0.
Allow the user to set and get a silentpayment object instead of a script. As per BIP375 we need to make sure there is either an OUPUT_SCRIPT or PSBT_OUT_SP_V0_INFO updated getter, validation and unsignedtx accordingly.
- Add silentpayment.ts with pure BIP352 crypto primitives: eligibleIndices, sumECDHShares, computeInputHash, deriveSilentPaymentOutput, bip352TaggedHash - Add computeSilentPaymentOutputScripts() to PsbtV2 (Signer role) implementing BIP375 output script derivation from ECDH shares
🦋 Changeset detectedLatest commit: 5a12230 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Return empty string instead of changing the signature, feels vary shaky.
Credits to @macgyver13 taken from bitcoin/bips#2046
- validate SegWit version range - use full compressed spend keys in derivation - enforce BIP352 scalar validity checks
Instead of having multiple places where we define checks call the proper assert functions. Also added a missing Bspend check.
Our assert was contained the wrong message (mentioned k instead of m). Also added a separate assert for 0 < k < Kmax (2323) output index check.
|
This pull request has been inactive for 30 days and has been marked as stale. It will be closed in 7 days if no further activity occurs. To keep this PR open, add the "long-lived" label or comment on it. |
|
@Legend101Zz could you have a look ? |
Hi @jvgelder — really sorry for the delay. I hadn’t had a chance to review this closely yet, but I’ll start going through it today or tomorrow. Thanks a lot for the PR! |
Thanks note this is the first part after this we should add the DLEQ proofs which I think should be part of this PR. After that we need to add the actual usage of this code to the wallet? to allow users to specify the silent payment address they want to send to (and thus address decoding too) |
Hi @jvgelder thanks for the heads up ... let me get back to you once I go through the PR , finally got the chance to review it ... thanks for your patience :) |
Removed function overhead. Moved ProjectivePoint for reusability.
We previously did not check dleq so we disabled those tests now we do so we reenable them.
There was a problem hiding this comment.
@jvgelder Thanks for the patience here, and thanks for pushing this PR forward. I finally got time to do an initial deeper review.
Overall, this looks like a solid PSBT-layer foundation for BIP375/Silent Payments. The current version is much stronger than the first pass I looked at :)
The main things I think still need attention before this is safe to merge are:
- enforce
SIGHASH_ALLwhen adding partial signatures to a PSBT with Silent Payment outputs - verify that BIP32 derivation pubkeys used for P2WPKH/P2SH-P2WPKH/P2PKH Silent Payment derivation actually match the prevout script
- either fully support labeled Silent Payment outputs or reject/keep them non-final until label-aware verification exists
After this PSBT layer is correct, I think the next PRs should tackle the usable Caravan path: Silent Payment address decoding, coordinator PSBTv2 construction, clear input eligibility checks, signer/wallet handling for ECDH shares and DLEQ proofs, and a precise Taproot-support story, do you plan to have all those in this PR itself ?
| throw Error("PsbtV2 input hight locktime is gte 500000000."); | ||
| } | ||
| } | ||
| this.validateSilentPayments(); |
There was a problem hiding this comment.
I think this needs one more BIP375 guard before signatures are accepted on a
silent-payment PSBT. validateSilentPayments() rejects explicit
PSBT_IN_SIGHASH_TYPE values that are not SIGHASH_ALL, but addPartialSig()
can still add a partial signature whose trailing sighash byte is SIGHASH_NONE
or SIGHASH_SINGLE when no PSBT_IN_SIGHASH_TYPE field is present.
Like , BIP375 says that if silent payment outputs are present, a signer must fail when
the sighash type is not SIGHASH_ALL , if no sighash type is provided, it must
sign using SIGHASH_ALL. The current code only uses handleSighashType() to
update modifiable flags after inserting the signature, so a non-ALL signature
can be serialized and reparsed successfully.
So should we reject this in addPartialSig() or in handleSighashType() when
this.hasSilentPaymentOutputs is true? A regression test should cover a silent
payment PSBT where PSBT_OUT_SCRIPT is already computed and addPartialSig()
is called with a signature ending in 0x02 or 0x03.
There was a problem hiding this comment.
| pubkeyBytes = getTaprootOutputKeyFromWitnessUtxo(witnessUtxo); | ||
| } | ||
|
|
||
| if (!pubkeyBytes) { |
There was a problem hiding this comment.
So for P2WPKH, P2SH-P2WPKH, and P2PKH inputs, this uses the first
PSBT_IN_BIP32_DERIVATION key as the input public key for DLEQ verification
and input_hash right, but I do not see a check that this pubkey actually matches
the prevout script.
That leaves a dangerous case: like a PSBT can spend a P2WPKH prevout whose script
hash commits to pubkey A, but include a BIP32 derivation key for pubkey B. The
DLEQ proof can be valid for B, and the SP output script can be computed from B,
while the final transaction is signed with A. The transaction can still be
consensus-valid, but the receiver scans using the actual input pubkey A and
will not find the output.
This may not affect Caravan's current classic multisig coordinator path directly,
because P2WSH/P2SH-P2WSH multisig inputs are not BIP352 shared-secret inputs.
But since this PR is adding generic BIP375 support in @caravan/psbt, I think
the P2WPKH/P2SH-P2WPKH/P2PKH public-key path should still verify that the
PSBT_IN_BIP32_DERIVATION pubkey matches the prevout script before using it for
DLEQ verification and input_hash.
Otherwise, a PSBT can provide a derivation pubkey B, generate a valid DLEQ proof
and SP output for B, while the actual input spends a prevout committed to pubkey
A. The final transaction can still be valid, but receivers scan using the actual
input pubkey revealed on-chain, so they would derive with A and miss the output.
There was a problem hiding this comment.
see new vectors : https://github.com/caravan-bitcoin/caravan/pull/496/changes#diff-bc1ee7a599cdf1acc66b16e760460b1be919fd420a1484423afc522a5510d3ffR679-R713
thanks to macgyver13
if you have more vectors you can think of :) https://github.com/macgyver13/bip375-test-generator/commits/bip32-more-cases/
|
|
||
| // Only non-labeled SP outputs can be recomputed from public PSBT data. | ||
| // Labeled outputs require recipient-side label state / scan private key. | ||
| const computedSPScanKeys = new Set<string>(); |
There was a problem hiding this comment.
I think labeled outputs need to be either fully verified or explicitly rejected
for now. Right now validation skips outputs that have PSBT_OUT_SP_V0_LABEL
when deciding which scan keys need ECDH coverage and output-script validation.
That means a labeled SP output with an arbitrary PSBT_OUT_SCRIPT can pass
validation once PSBT_GLOBAL_TX_MODIFIABLE is zeroed, even with no ECDH share
or DLEQ proof for that scan key.
This is especially risky because BIP375 uses labels for SP change verification.
If this implementation cannot derive or verify labeled outputs yet, I think the
safe behavior is to fail validation when a labeled SP output has a script, or to
keep it non-final/in-progress until label-aware verification exists. Otherwise
the PSBT layer can bless an output script that is not tied to the stored silent
payment data, what do you think ?
There was a problem hiding this comment.
Just talked with @macgyver13 and the PSBT_OUT_SP_V0_LABEL is just there as a hint for the hardware wallet: hey the output address i sent you is actually one of our labeled addresses when you verify you need to derive it properly to arrive at the same address.
To clarify the scan will most likely always has to live in the coordinator otherwise it will be close to impossible to scan for your incoming payments. Hence the coordinator can actually also derive labeled address from the scan key.
Thanks a lot for taking the time to review it, this gives me some more energy to move it forward.
Good one, wasn't sure if I should alter those functions but a simple assert (if silent output && sighash != All ) should suffice I guess.
Il have a look.
While we are at it we should support it in full :)
I Might but just to manage the expectations a bit I am also working on various other projects, if we can get more people excited, I would direct more of my energy (and tokens) here ^^. |
Replace the first-BIP32-pubkey fallback with candidate pubkey recovery from BIP32 derivations, partial signatures, and finalized input scripts. Only pubkeys matching the spent output are used for DLEQ verification and input_hash recomputation, preventing SP outputs from being derived from a decoy key.
Add Silent Payment helpers that recover an input public key only when a candidate pubkey matches the prevout script. This prepares validation to reject PSBTs that provide decoy pubkeys for Silent Payment shares or DLEQ proofs.
Use only input pubkeys that match the spent prevout script when verifying silent payment DLEQ proofs and computing BIP352 input_hash. This prevents PSBTs from deriving silent payment outputs from a decoy pubkey while spending with a different key. Also tighten SP signing/validation guards for computed outputs, NUMS taproot script-path inputs, and non-SIGHASH_ALL partial signatures.
| ) { | ||
| console.warn( | ||
| `Output ${i} is missing previous out script or amount. Skipping.`, | ||
| `Output ${i} is missing SP_INFO, script or amount. Skipping.`, |
There was a problem hiding this comment.
should have added SP_INFO instead of replaced previous out
| indices.push(value.toString("hex")); | ||
| } | ||
| return indices; | ||
| if (!value) return ""; |
There was a problem hiding this comment.
return "" and checking on empty string seems like a bad idea
There was a problem hiding this comment.
would it be possible to maintain the throw when PSBT_OUT_SP_V0_INFO is not present in the output map.
macgyver13
left a comment
There was a problem hiding this comment.
Thanks for getting the ball rolling on the silent payments PSBT integration!
| const eBytes = taggedHash( | ||
| "BIP0374/challenge", | ||
| Buffer.concat([ | ||
| ABytes, | ||
| BBytes, | ||
| CBytes, | ||
| Buffer.from(G.toRawBytes(true)), | ||
| R1Bytes, | ||
| R2Bytes, | ||
| m, | ||
| ]), | ||
| ); |
There was a problem hiding this comment.
tidy: function for dleqChallenge could be reused in verify too
| multiplyCompressedPoint, | ||
| verifyDLEQProof, | ||
| } from "./dleq"; | ||
| import { verifySilentPaymentDLEQProof } from "./silentpayment"; |
| export function bip352TaggedHash(tag: string, data: Buffer): Buffer { | ||
| const tagHash = createHash("sha256").update(tag).digest(); | ||
| return createHash("sha256") | ||
| .update(tagHash) | ||
| .update(tagHash) | ||
| .update(data) | ||
| .digest(); | ||
| } |
There was a problem hiding this comment.
duplicate taggedHash implementation in dleq.ts
| indices.push(value.toString("hex")); | ||
| } | ||
| return indices; | ||
| if (!value) return ""; |
There was a problem hiding this comment.
would it be possible to maintain the throw when PSBT_OUT_SP_V0_INFO is not present in the output map.
Even though the version number is lower the tests should be newer.
- Created reusable taggedChallenge/tagged hash function (can be replaced with bitcoinJs tagged hash if needed) - Added verifyDleq proof for external use to prevent recalculation of points.
Instead of returning an empty string, throw error and handle it.
Previously removed to much readded previous out.
What kind of change does this PR introduce?
feature
Issue Number:
Fixes #467
Summary
Implements sending to silent payment Bip352 address via psbtv2 bip375.
Still work in progress but gives some insights in how I plan to integrate.
TODO:
Does this PR introduce a breaking change?
Should not introduce any breaking changes
Checklist
npm run changeset)Other information
Have you read the contributing guide?
Yes