Skip to content

Feat/sp sending#496

Open
jvgelder wants to merge 25 commits into
caravan-bitcoin:mainfrom
jvgelder:feat/sp-sending
Open

Feat/sp sending#496
jvgelder wants to merge 25 commits into
caravan-bitcoin:mainfrom
jvgelder:feat/sp-sending

Conversation

@jvgelder

@jvgelder jvgelder commented Mar 24, 2026

Copy link
Copy Markdown

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:

  • Improve test coverage

Does this PR introduce a breaking change?

Should not introduce any breaking changes

Checklist

  • I have tested my changes thoroughly.
  • I have added or updated tests to cover my changes (if applicable).
  • I have verified that test coverage meets or exceeds 95% (if applicable).
  • I have run the test suite locally, and all tests pass.
  • I have written tests for all new changes/features
  • I have followed the project's coding style and conventions.
  • I have created a changeset to document my changes (npm run changeset)

Other information

Have you read the contributing guide?

Yes

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-bot

changeset-bot Bot commented Mar 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5a12230

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@caravan/psbt Minor

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

@vercel

vercel Bot commented Mar 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
caravan-coordinator Ready Ready Preview, Comment Jun 23, 2026 3:55pm

Request Review

Return empty string instead of changing the signature, feels vary shaky.
- 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.
@github-actions

github-actions Bot commented May 2, 2026

Copy link
Copy Markdown
Contributor

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.

@github-actions github-actions Bot added the stale label May 2, 2026
@jvgelder

jvgelder commented May 2, 2026

Copy link
Copy Markdown
Author

@Legend101Zz could you have a look ?

@github-actions github-actions Bot removed the stale label May 3, 2026
@Legend101Zz

Copy link
Copy Markdown
Contributor

@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!

@jvgelder

jvgelder commented May 4, 2026

Copy link
Copy Markdown
Author

@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)

@Legend101Zz

Copy link
Copy Markdown
Contributor

@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 :)

jvgelder added 2 commits May 20, 2026 14:58
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.

@Legend101Zz Legend101Zz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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_ALL when 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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pubkeyBytes = getTaprootOutputKeyFromWitnessUtxo(witnessUtxo);
}

if (!pubkeyBytes) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@jvgelder jvgelder Jun 10, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


// 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>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ?

@jvgelder jvgelder Jun 10, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@jvgelder

Copy link
Copy Markdown
Author

@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 :)

Thanks a lot for taking the time to review it, this gives me some more energy to move it forward.

The main things I think still need attention before this is safe to merge are:

* enforce `SIGHASH_ALL` when adding partial signatures to a PSBT with Silent Payment outputs

Good one, wasn't sure if I should alter those functions but a simple assert (if silent output && sighash != All ) should suffice I guess.

* verify that BIP32 derivation pubkeys used for P2WPKH/P2SH-P2WPKH/P2PKH Silent Payment derivation actually match the prevout script

Il have a look.

* either fully support labeled Silent Payment outputs or reject/keep them non-final until label-aware verification exists

While we are at it we should support it in full :)

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 ?

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 ^^.
Would need some pointers on where to implement address en/decoding and how to change the UI so that it can handle silent payment output addresses.

jvgelder added 6 commits June 5, 2026 17:10
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.`,

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should have added SP_INFO instead of replaced previous out

indices.push(value.toString("hex"));
}
return indices;
if (!value) return "";

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return "" and checking on empty string seems like a bad idea

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be possible to maintain the throw when PSBT_OUT_SP_V0_INFO is not present in the output map.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes changed it back

@macgyver13 macgyver13 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for getting the ball rolling on the silent payments PSBT integration!

Comment on lines +142 to +153
const eBytes = taggedHash(
"BIP0374/challenge",
Buffer.concat([
ABytes,
BBytes,
CBytes,
Buffer.from(G.toRawBytes(true)),
R1Bytes,
R2Bytes,
m,
]),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tidy: function for dleqChallenge could be reused in verify too

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

multiplyCompressedPoint,
verifyDLEQProof,
} from "./dleq";
import { verifySilentPaymentDLEQProof } from "./silentpayment";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: import not used

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment on lines +199 to +206
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();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duplicate taggedHash implementation in dleq.ts

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

indices.push(value.toString("hex"));
}
return indices;
if (!value) return "";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be possible to maintain the throw when PSBT_OUT_SP_V0_INFO is not present in the output map.

jvgelder added 3 commits June 23, 2026 17:41
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Silent payment support

3 participants