Skip to content

Commit ff157fb

Browse files
committed
feat: add BIP-352 silent payments module
Implement BIP-352 Silent Payments in wasm-utxo: address codec (sp1q.../tsp1q...), ECDH output derivation for sending, transaction scanning for receiving, spend key derivation, and label support. Validated against all 28 official BIP-352 test vectors. Ticket: BTC-3241
1 parent 75ccf91 commit ff157fb

19 files changed

Lines changed: 14981 additions & 2 deletions

File tree

packages/wasm-utxo/Cargo.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/wasm-utxo/Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ unexpected_cfgs = { level = "warn", check-cfg = [
2222

2323
[features]
2424
default = []
25-
inspect = ["dep:num-bigint", "dep:serde", "dep:serde_json", "dep:hex"]
25+
inspect = ["dep:num-bigint", "dep:serde_json", "dep:hex"]
2626

2727
[dependencies]
2828
wasm-bindgen = "0.2"
@@ -33,7 +33,8 @@ musig2 = { version = "0.3.1", default-features = false, features = ["k256"] }
3333
getrandom = { version = "0.2", features = ["js"] }
3434
pastey = "0.1"
3535
num-bigint = { version = "0.4", optional = true }
36-
serde = { version = "1.0", features = ["derive"], optional = true }
36+
serde = { version = "1.0", features = ["derive"] }
37+
serde-wasm-bindgen = "0.6"
3738
serde_json = { version = "1.0", optional = true }
3839
hex = { version = "0.4", optional = true }
3940

packages/wasm-utxo/bips/bip-0352/bip-0352.mediawiki

Lines changed: 523 additions & 0 deletions
Large diffs are not rendered by default.

packages/wasm-utxo/bips/bip-0352/reference.py

Lines changed: 380 additions & 0 deletions
Large diffs are not rendered by default.

packages/wasm-utxo/bips/bip-0352/vectors/send_and_receive_test_vectors.json

Lines changed: 5729 additions & 0 deletions
Large diffs are not rendered by default.

packages/wasm-utxo/js/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export * as inscriptions from "./inscriptions.js";
1414
export * as message from "./message.js";
1515
export * as utxolibCompat from "./utxolibCompat.js";
1616
export * as fixedScriptWallet from "./fixedScriptWallet/index.js";
17+
export * as silentPayments from "./silentPayments.js";
1718
export * as descriptorWallet from "./descriptorWallet/index.js";
1819
export * as bip32 from "./bip32.js";
1920
export * as ecpair from "./ecpair.js";
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { SilentPaymentsNamespace } from "./wasm/wasm_utxo.js";
2+
3+
export interface SilentPaymentAddressComponents {
4+
scanKey: Uint8Array; // 33-byte compressed
5+
spendKey: Uint8Array; // 33-byte compressed
6+
}
7+
8+
export interface DerivedOutput {
9+
script: Uint8Array; // P2TR scriptPubKey
10+
pubkey: Uint8Array; // 32-byte x-only
11+
tweak: Uint8Array; // 32-byte t_k
12+
}
13+
14+
export interface ScanMatch {
15+
outputIndex: number;
16+
tweak: Uint8Array; // 32-byte t_k
17+
k: number;
18+
label: number | null;
19+
labelTweak: Uint8Array | null; // 32-byte label tweak (if label matched)
20+
}
21+
22+
export interface PrivkeyInput {
23+
key: Uint8Array; // 32-byte private key
24+
isTaproot: boolean;
25+
}
26+
27+
export interface Outpoint {
28+
txid: Uint8Array; // 32-byte txid (LE)
29+
vout: number;
30+
}
31+
32+
export interface InputData {
33+
privkeys: PrivkeyInput[];
34+
outpoints: Outpoint[];
35+
}
36+
37+
export interface PubkeyInput {
38+
pubkey: Uint8Array; // 33-byte compressed pubkey
39+
}
40+
41+
export interface TaprootOutputData {
42+
pubkey: Uint8Array; // 32-byte x-only pubkey
43+
}
44+
45+
export interface TxData {
46+
inputs: PubkeyInput[];
47+
outpoints: Outpoint[];
48+
outputs: TaprootOutputData[];
49+
}
50+
51+
/**
52+
* Decode a silent payment address (sp1q.../tsp1q...) into its component keys.
53+
*/
54+
export function decodeAddress(address: string): SilentPaymentAddressComponents {
55+
return SilentPaymentsNamespace.decode_address(address) as SilentPaymentAddressComponents;
56+
}
57+
58+
/**
59+
* Encode a silent payment address from component keys.
60+
*
61+
* @param scanKey 33-byte compressed scan public key
62+
* @param spendKey 33-byte compressed spend public key
63+
* @param network coin name ("btc", "tbtc", etc.)
64+
*/
65+
export function encodeAddress(scanKey: Uint8Array, spendKey: Uint8Array, network: string): string {
66+
return SilentPaymentsNamespace.encode_address(scanKey, spendKey, network);
67+
}
68+
69+
/**
70+
* Derive output scripts for sending to silent payment recipients.
71+
*
72+
* @param inputData private keys and outpoints from the transaction inputs
73+
* @param recipients array of SP address strings (sp1q.../tsp1q...)
74+
* @returns array of derived P2TR outputs with scripts, pubkeys, and tweaks
75+
*/
76+
export function deriveOutputs(inputData: InputData, recipients: string[]): DerivedOutput[] {
77+
return SilentPaymentsNamespace.derive_outputs(inputData, recipients) as DerivedOutput[];
78+
}
79+
80+
/**
81+
* Scan a transaction for silent payment outputs addressed to this receiver.
82+
*
83+
* @param scanKey 32-byte b_scan private key
84+
* @param spendPubkey 33-byte B_spend public key
85+
* @param txData transaction data (input pubkeys, outpoints, taproot outputs)
86+
* @param labels optional array of label indices to check
87+
* @returns array of matched outputs with tweaks
88+
*/
89+
export function scanTransaction(
90+
scanKey: Uint8Array,
91+
spendPubkey: Uint8Array,
92+
txData: TxData,
93+
labels?: number[] | null,
94+
): ScanMatch[] {
95+
return SilentPaymentsNamespace.scan_transaction(
96+
scanKey,
97+
spendPubkey,
98+
txData,
99+
labels ?? null,
100+
) as ScanMatch[];
101+
}
102+
103+
/**
104+
* Derive the private key for spending a matched silent payment output.
105+
*
106+
* @param spendKey 32-byte b_spend private key
107+
* @param tweak 32-byte t_k from scanTransaction
108+
* @returns 32-byte derived private key p_k
109+
*/
110+
export function deriveSpendKey(spendKey: Uint8Array, tweak: Uint8Array): Uint8Array {
111+
return SilentPaymentsNamespace.derive_spend_key(spendKey, tweak);
112+
}
113+
114+
/**
115+
* Create a labeled silent payment address.
116+
*
117+
* @param scanKey 32-byte b_scan private key
118+
* @param spendPubkey 33-byte B_spend public key
119+
* @param labelIndex the label index m
120+
* @param network coin name ("btc", "tbtc", etc.)
121+
* @returns the labeled sp1q.../tsp1q... address string
122+
*/
123+
export function createLabeledAddress(
124+
scanKey: Uint8Array,
125+
spendPubkey: Uint8Array,
126+
labelIndex: number,
127+
network: string,
128+
): string {
129+
return SilentPaymentsNamespace.create_labeled_address(scanKey, spendPubkey, labelIndex, network);
130+
}

packages/wasm-utxo/src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,9 @@ impl From<crate::address::AddressError> for WasmUtxoError {
5858
WasmUtxoError::StringError(err.to_string())
5959
}
6060
}
61+
62+
impl From<crate::silent_payments::SilentPaymentError> for WasmUtxoError {
63+
fn from(err: crate::silent_payments::SilentPaymentError) -> Self {
64+
WasmUtxoError::StringError(err.to_string())
65+
}
66+
}

packages/wasm-utxo/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ mod networks;
1111
pub mod p2mr;
1212
pub mod paygo;
1313
pub mod psbt_ops;
14+
pub mod silent_payments;
1415
#[cfg(test)]
1516
mod test_utils;
1617
pub mod zcash;

0 commit comments

Comments
 (0)