Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
81981ca
chore(psbt): Add silent payment fields
jvgelder Mar 24, 2026
dcfc2a9
chore(psbt): Add silent payment fields
jvgelder Mar 24, 2026
8b2a8ab
chore(psbt): Add output functionality
jvgelder Mar 24, 2026
ed8b289
feat(psbt): Add BIP352 silent payment output script computation
jvgelder Mar 24, 2026
296fa75
fix(psbt): Prevent creating breaking change with PSBT_OUT_SCRIPT
jvgelder Mar 24, 2026
80e7c11
chore(psbtv2): Added test vectors from BIP375
jvgelder Mar 30, 2026
8f0df4b
chore(psbtv2): Add validation
jvgelder Mar 30, 2026
0e57be0
fix(psbtv2): correct silent payment output derivation
jvgelder Mar 30, 2026
23cff68
chore(psbtv2): Replace checks with assert calls
jvgelder Mar 30, 2026
ea81977
chore(psbtv2): Proper label and k checking
jvgelder Mar 31, 2026
1408cc2
fix(caravan-psbt): harden BIP375 silent payment validation
jvgelder May 20, 2026
20b7395
feat(caravan-psbt): add BIP374 DLEQ proofs for silent payments
jvgelder May 20, 2026
124ba9e
chore(dleq): Cleanup functions
jvgelder May 20, 2026
e1f7cfd
chore(bip375): Enable all tests
jvgelder May 20, 2026
8ee5f65
fix(psbtv2): Fix nums check
jvgelder May 20, 2026
d1db68e
fix(caravan-psbt): require prevout-matched keys for SP derivation
jvgelder Jun 5, 2026
8e662e4
refactor(caravan-psbt): add prevout-matched SP pubkey helpers
jvgelder Jun 5, 2026
d97620a
test(caravan-psbt): update BIP375 vector fixtures
jvgelder Jun 5, 2026
80bfccc
fix(caravan-psbt): enforce prevout-matched silent payment input keys
jvgelder Jun 5, 2026
98ce2c3
refactor(caravan-psbt): expose NUMS script-path detection for validation
jvgelder Jun 10, 2026
a391dd2
fix(caravan-psbt): require prevout-matched keys for SP derivation
jvgelder Jun 10, 2026
af82d43
chore(pstv2): Update vectors
jvgelder Jun 10, 2026
6fc4630
chore(psbtv2): Cleanup redundant code
jvgelder Jun 23, 2026
0275c92
chore(psbtv2): Revert PSBT_OUT_SCRIPT changes
jvgelder Jun 23, 2026
5a12230
chore(psbtv2): Improve missing warning
jvgelder Jun 23, 2026
e1a6b39
chore(psbtv2): Cleanup and remove dead code
jvgelder Jul 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/ripe-turkeys-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@caravan/psbt": minor
---

Add BIP375 silent payment sending support to PsbtV2

- Add PSBT_OUT_SP_V0_INFO and PSBT_OUT_SP_V0_LABEL getters and methods
- Add PSBT_GLOBAL_SP_ECDH_SHARE, PSBT_GLOBAL_SP_DLEQ getters and methods
- Add PSBT_IN_SP_ECDH_SHARE, PSBT_IN_SP_DLEQ getters and methods
- Add silentPayment option to addOutput() for SP output construction
- Add computeSilentPaymentOutputScripts() implementing BIP352 derivation
- Add hasSilentPaymentOutputs, hasAllSPOutputScripts, hasCompleteECDHCoverage predicates
- Add eligibleIndices, sumECDHShares, computeInputHash, deriveSilentPaymentOutput to silentpayment.ts
- Update isReadyForSigner to enforce BIP375 signing rules
- Update getTransactionId() for BIP375 unique identification
1,717 changes: 1,717 additions & 0 deletions packages/caravan-psbt/src/fixtures/bip375_vectors.json

Large diffs are not rendered by default.

120 changes: 120 additions & 0 deletions packages/caravan-psbt/src/psbtv2/psbtv2-bip375.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* BIP375 test vectors for PsbtV2 silent payment PSBT validation.
*/

import { describe, expect, test } from "vitest";
import { PsbtV2 } from "./psbtv2";
import bip375Vectors from "../fixtures/bip375_vectors.json";
import { KeyType } from "src/psbtv2/types";

const SKIP_DESCRIPTIONS = new Set([
"ecdh coverage: invalid proof in PSBT_IN_SP_DLEQ field",
"ecdh coverage: invalid proof in PSBT_GLOBAL_SP_DLEQ field",
]);

// ── Invalid vectors ────────────────────────────────────────────────────────

describe("BIP375 invalid vectors", () => {
test.each(
bip375Vectors.invalid.filter((v) => !SKIP_DESCRIPTIONS.has(v.description)),
)("$description", ({ psbt }) => {
expect(() => {
const p = new PsbtV2(psbt);
p.validate();
}).toThrow();
});
});

// ── Valid vectors ──────────────────────────────────────────────────────────

describe("BIP375 valid vectors", () => {
test.each(
bip375Vectors.valid.filter((v) => !SKIP_DESCRIPTIONS.has(v.description)),
)("$description", ({ psbt, supplementary }) => {
const p = new PsbtV2(psbt);

expect(() => p.validate()).not.toThrow();

// ── Output checks ──────────────────────────────────────────────────────

for (const output of supplementary.outputs ?? []) {
// Output script
if (output.script) {
expect(
p.PSBT_OUT_SCRIPT[output.output_index],
`output ${output.output_index} script`,
).toBe(output.script);
}

// SP V0 info (bscan || bspend): only check unlabeled outputs.
// For labeled outputs, the supplementary bspend is the label-tweaked key
// (B_spend + label·G), but PSBT_OUT_SP_V0_INFO stores the raw bspend.
// The label relationship is verified separately via PSBT_OUT_SP_V0_LABEL.
const hasLabel = "sp_v0_label" in output && output.sp_v0_label !== null;
if (output.sp_v0_info && !hasLabel) {
const info = p.getSilentPaymentOutputInfo(output.output_index);
expect(info, `output ${output.output_index} sp_v0_info`).not.toBeNull();
const combined =
info!.bscan.toString("hex") + info!.bspend.toString("hex");
expect(combined, `output ${output.output_index} sp_v0_info`).toBe(
output.sp_v0_info,
);
}

// SP V0 label
if (hasLabel) {
expect(
p.PSBT_OUT_SP_V0_LABEL[output.output_index],
`output ${output.output_index} sp_v0_label`,
).toBe(output.sp_v0_label);
}
}

// ── Input / proof checks ───────────────────────────────────────────────

for (const proof of supplementary.sp_proofs ?? []) {
const isGlobal =
proof.input_index === undefined || proof.input_index === null;

if (isGlobal) {
if (proof.ecdh_share) {
const entry = p.PSBT_GLOBAL_SP_ECDH_SHARE.find(
(s) => s.key === KeyType.PSBT_GLOBAL_SP_ECDH_SHARE + proof.scan_key,
);
expect(
entry?.value,
`global ECDH share for scan key ${proof.scan_key}`,
).toBe(proof.ecdh_share);
}
if (proof.dleq_proof) {
const entry = p.PSBT_GLOBAL_SP_DLEQ.find(
(s) => s.key === KeyType.PSBT_GLOBAL_SP_DLEQ + proof.scan_key,
);
expect(
entry?.value,
`global DLEQ proof for scan key ${proof.scan_key}`,
).toBe(proof.dleq_proof);
}
} else {
if (proof.ecdh_share) {
const entry = p.PSBT_IN_SP_ECDH_SHARE[proof.input_index].find(
(s) => s.key === KeyType.PSBT_IN_SP_ECDH_SHARE + proof.scan_key,
);
expect(
entry?.value,
`input ${proof.input_index} ECDH share for scan key ${proof.scan_key}`,
).toBe(proof.ecdh_share);
}
if (proof.dleq_proof) {
const entry = p.PSBT_IN_SP_DLEQ[proof.input_index].find(
(s) => s.key === KeyType.PSBT_IN_SP_DLEQ + proof.scan_key,
);
expect(
entry?.value,
`input ${proof.input_index} DLEQ proof for scan key ${proof.scan_key}`,
).toBe(proof.dleq_proof);
}
}
}
});
});
6 changes: 1 addition & 5 deletions packages/caravan-psbt/src/psbtv2/psbtv2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2325,11 +2325,7 @@ describe("PsbtV2.combine", () => {
// Stub handleSighashType so addPartialSig doesn't throw on sighash
// validation.
thisPsbt.handleSighashType = vi.fn();
thisPsbt.addPartialSig(
0,
Buffer.from([0x01]),
Buffer.from([0x02]),
);
thisPsbt.addPartialSig(0, Buffer.from([0x01]), Buffer.from([0x02]));
}

return { thisPsbt, otherPsbt };
Expand Down
Loading