Skip to content
Open
Show file tree
Hide file tree
Changes from 25 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
2,074 changes: 2,074 additions & 0 deletions packages/caravan-psbt/src/fixtures/bip375_vectors.json

Large diffs are not rendered by default.

208 changes: 208 additions & 0 deletions packages/caravan-psbt/src/psbtv2/dleq.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { describe, expect, it } from "vitest";
import { secp256k1 } from "@noble/curves/secp256k1";

import {
generateDLEQProof,
multiplyCompressedPoint,
verifyDLEQProof,
} from "./dleq";
function hex(s: string): Buffer {
Comment thread
jvgelder marked this conversation as resolved.
return Buffer.from(s, "hex");
}

function pubkeyFromSecret(secret: Buffer): Buffer {
return Buffer.from(
secp256k1.ProjectivePoint.BASE.multiply(
BigInt(`0x${secret.toString("hex")}`),
).toRawBytes(true),
);
}

describe("BIP374 DLEQ", () => {
it("generates a proof that verifies", () => {
const secret = hex(
"0000000000000000000000000000000000000000000000000000000000000001",
);
const baseSecret = hex(
"0000000000000000000000000000000000000000000000000000000000000002",
);
const auxRand = Buffer.alloc(32, 0);

const publicKey = pubkeyFromSecret(secret);
const basePoint = pubkeyFromSecret(baseSecret);
const result = multiplyCompressedPoint(basePoint, secret);

const proof = generateDLEQProof({
secret,
basePoint,
auxRand,
});

expect(proof.length).toBe(64);

expect(
verifyDLEQProof({
proof,
publicKey,
basePoint,
result,
}),
).toBe(true);
});

it("fails verification with the wrong public key", () => {
const secret = hex(
"0000000000000000000000000000000000000000000000000000000000000001",
);
const wrongSecret = hex(
"0000000000000000000000000000000000000000000000000000000000000003",
);
const baseSecret = hex(
"0000000000000000000000000000000000000000000000000000000000000002",
);

const basePoint = pubkeyFromSecret(baseSecret);
const result = multiplyCompressedPoint(basePoint, secret);
const wrongPublicKey = pubkeyFromSecret(wrongSecret);

const proof = generateDLEQProof({
secret,
basePoint,
auxRand: Buffer.alloc(32, 0),
});

expect(
verifyDLEQProof({
proof,
publicKey: wrongPublicKey,
basePoint,
result,
}),
).toBe(false);
});

it("fails verification with the wrong base point", () => {
const secret = hex(
"0000000000000000000000000000000000000000000000000000000000000001",
);
const baseSecret = hex(
"0000000000000000000000000000000000000000000000000000000000000002",
);
const wrongBaseSecret = hex(
"0000000000000000000000000000000000000000000000000000000000000003",
);

const publicKey = pubkeyFromSecret(secret);
const basePoint = pubkeyFromSecret(baseSecret);
const wrongBasePoint = pubkeyFromSecret(wrongBaseSecret);
const result = multiplyCompressedPoint(basePoint, secret);

const proof = generateDLEQProof({
secret,
basePoint,
auxRand: Buffer.alloc(32, 0),
});

expect(
verifyDLEQProof({
proof,
publicKey,
basePoint: wrongBasePoint,
result,
}),
).toBe(false);
});

it("fails verification with the wrong ECDH result", () => {
const secret = hex(
"0000000000000000000000000000000000000000000000000000000000000001",
);
const baseSecret = hex(
"0000000000000000000000000000000000000000000000000000000000000002",
);
const wrongSecret = hex(
"0000000000000000000000000000000000000000000000000000000000000003",
);

const publicKey = pubkeyFromSecret(secret);
const basePoint = pubkeyFromSecret(baseSecret);
const wrongResult = multiplyCompressedPoint(basePoint, wrongSecret);

const proof = generateDLEQProof({
secret,
basePoint,
auxRand: Buffer.alloc(32, 0),
});

expect(
verifyDLEQProof({
proof,
publicKey,
basePoint,
result: wrongResult,
}),
).toBe(false);
});

it("fails verification when proof bytes are mutated", () => {
const secret = hex(
"0000000000000000000000000000000000000000000000000000000000000001",
);
const baseSecret = hex(
"0000000000000000000000000000000000000000000000000000000000000002",
);

const publicKey = pubkeyFromSecret(secret);
const basePoint = pubkeyFromSecret(baseSecret);
const result = multiplyCompressedPoint(basePoint, secret);

const proof = generateDLEQProof({
secret,
basePoint,
auxRand: Buffer.alloc(32, 0),
});

const mutated = Buffer.from(proof);
mutated[0] ^= 1;

expect(
verifyDLEQProof({
proof: mutated,
publicKey,
basePoint,
result,
}),
).toBe(false);
});

it("rejects malformed proof lengths", () => {
const secret = hex(
"0000000000000000000000000000000000000000000000000000000000000001",
);
const baseSecret = hex(
"0000000000000000000000000000000000000000000000000000000000000002",
);

const publicKey = pubkeyFromSecret(secret);
const basePoint = pubkeyFromSecret(baseSecret);
const result = multiplyCompressedPoint(basePoint, secret);

expect(
verifyDLEQProof({
proof: Buffer.alloc(63),
publicKey,
basePoint,
result,
}),
).toBe(false);

expect(
verifyDLEQProof({
proof: Buffer.alloc(65),
publicKey,
basePoint,
result,
}),
).toBe(false);
});
});
Loading