Skip to content

Commit 14dae85

Browse files
committed
feat: spend from tweaked silent address
1 parent f87b5b0 commit 14dae85

File tree

2 files changed

+161
-39
lines changed

2 files changed

+161
-39
lines changed

test/integration/silent-payments.spec.ts

+121-39
Original file line numberDiff line numberDiff line change
@@ -6,68 +6,99 @@ import { regtestUtils } from './_regtest';
66
import * as bitcoin from '../..';
77
import { toXOnly } from '../../src/psbt/bip371';
88

9+
import { tweakSigner } from './taproot.utils';
10+
911
const rng = require('randombytes');
1012
const regtest = regtestUtils.network;
1113
bitcoin.initEccLib(ecc);
1214
const bip32 = BIP32Factory(ecc);
1315

1416
describe('bitcoinjs-lib (silent payments)', () => {
1517
it('can create (and broadcast via 3PBP) a simple silent payment', async () => {
18+
// for simplicity the transactions in this test have only one input and one output
19+
1620
const { senderKeyPair, receiverKeyPair, sharedSecret } = initParticipants();
17-
// this is what the sender sees/scans
21+
22+
// this is what the sender sees/scans (from twitter bio, public forum, truck door)
1823
const silentPublicKey = toXOnly(receiverKeyPair.publicKey);
1924

20-
// the input being spent
21-
const { output: p2wpkhOutput } = bitcoin.payments.p2wpkh({
22-
pubkey: senderKeyPair.publicKey,
25+
const senderUtxo = await fundP2pkhUtxo(senderKeyPair.publicKey);
26+
27+
// amount to pay the silent address
28+
const payAmount = senderUtxo.value - 1e4;
29+
const {
30+
psbt: payPsbt,
31+
address: tweakedSilentAddress,
32+
} = buildPayToSilentAddress(
33+
senderUtxo.txId,
34+
senderUtxo,
35+
silentPublicKey,
36+
payAmount,
37+
sharedSecret,
38+
);
39+
payPsbt.signInput(0, senderKeyPair).finalizeAllInputs();
40+
41+
// the transaction paying to the silent address
42+
const payTx = payPsbt.extractTransaction();
43+
await broadcastAndVerifyTx(payTx, tweakedSilentAddress!, payAmount);
44+
45+
// the amount the receiver will spend
46+
const sendAmount = payAmount - 1e4;
47+
// the utxo with the tweaked silent address
48+
const receiverUtxo = { value: payAmount, script: payTx.outs[0].script };
49+
const { psbt: spendPsbt, address } = buildSpendFromSilentAddress(
50+
payTx.getId(),
51+
receiverUtxo,
52+
silentPublicKey,
53+
sendAmount,
54+
sharedSecret,
55+
);
56+
57+
const tweakedSigner = tweakSigner(receiverKeyPair!, {
58+
tweakHash: sharedSecret,
2359
network: regtest,
2460
});
61+
spendPsbt.signInput(0, tweakedSigner).finalizeAllInputs();
2562

26-
// amount from faucet
27-
const amount = 42e4;
28-
// amount to send
29-
const sendAmount = amount - 1e4;
30-
// get faucet
31-
const unspent = await regtestUtils.faucetComplex(p2wpkhOutput!, amount);
32-
33-
const psbt = new bitcoin.Psbt({ network: regtest });
34-
psbt.addInput({
35-
hash: unspent.txId,
36-
index: 0,
37-
witnessUtxo: { value: amount, script: p2wpkhOutput! },
38-
});
39-
40-
// destination
41-
const { address } = bitcoin.payments.p2tr({
42-
internalPubkey: silentPublicKey,
43-
hash: sharedSecret,
44-
network: regtest,
45-
});
46-
psbt.addOutput({ value: sendAmount, address: address! });
63+
// the transaction spending from the silent address
64+
const spendTx = spendPsbt.extractTransaction();
65+
await broadcastAndVerifyTx(spendTx, address!, sendAmount);
66+
});
67+
});
4768

48-
psbt.signInput(0, senderKeyPair);
69+
async function fundP2pkhUtxo(senderPubKey: Buffer) {
70+
// the input being spent
71+
const { output: p2wpkhOutput } = bitcoin.payments.p2wpkh({
72+
pubkey: senderPubKey,
73+
network: regtest,
74+
});
4975

50-
psbt.finalizeAllInputs();
51-
const tx = psbt.extractTransaction();
52-
const rawTx = tx.toBuffer();
76+
// amount from faucet
77+
const amount = 42e4;
78+
// get faucet
79+
const unspent = await regtestUtils.faucetComplex(p2wpkhOutput!, amount);
5380

54-
const hex = rawTx.toString('hex');
81+
return { value: amount, script: p2wpkhOutput!, txId: unspent.txId };
82+
}
5583

56-
await regtestUtils.broadcast(hex);
57-
await regtestUtils.verify({
58-
txId: tx.getId(),
59-
address: address!,
60-
vout: 0,
61-
value: sendAmount,
62-
});
84+
async function broadcastAndVerifyTx(
85+
tx: bitcoin.Transaction,
86+
address: string,
87+
value: number,
88+
) {
89+
await regtestUtils.broadcast(tx.toBuffer().toString('hex'));
90+
await regtestUtils.verify({
91+
txId: tx.getId(),
92+
address: address!,
93+
vout: 0,
94+
value,
6395
});
64-
});
96+
}
6597

6698
function initParticipants() {
6799
const receiverKeyPair = bip32.fromSeed(rng(64), regtest);
68100
const senderKeyPair = bip32.fromSeed(rng(64), regtest);
69101

70-
71102
const senderSharedSecret = ecc.pointMultiply(
72103
receiverKeyPair.publicKey,
73104
senderKeyPair.privateKey!,
@@ -88,4 +119,55 @@ function initParticipants() {
88119
};
89120
}
90121

122+
function buildPayToSilentAddress(
123+
prevOutTxId: string,
124+
witnessUtxo: { value: number; script: Buffer },
125+
silentPublicKey: Buffer,
126+
sendAmount: number,
127+
sharedSecret: Buffer,
128+
) {
129+
const psbt = new bitcoin.Psbt({ network: regtest });
130+
psbt.addInput({
131+
hash: prevOutTxId,
132+
index: 0,
133+
witnessUtxo,
134+
});
135+
136+
// destination
137+
const { address } = bitcoin.payments.p2tr({
138+
internalPubkey: silentPublicKey,
139+
hash: sharedSecret,
140+
network: regtest,
141+
});
142+
psbt.addOutput({ value: sendAmount, address: address! });
143+
144+
return { psbt, address };
145+
}
146+
147+
function buildSpendFromSilentAddress(
148+
prevOutTxId: string,
149+
witnessUtxo: { value: number; script: Buffer },
150+
silentPublicKey: Buffer,
151+
sendAmount: number,
152+
sharedSecret: Buffer,
153+
) {
154+
const psbt = new bitcoin.Psbt({ network: regtest });
155+
psbt.addInput({
156+
hash: prevOutTxId,
157+
index: 0,
158+
witnessUtxo,
159+
tapInternalKey: silentPublicKey,
160+
tapMerkleRoot: sharedSecret,
161+
});
162+
163+
// random address value, not important
164+
const address =
165+
'bcrt1pqknex3jwpsaatu5e5dcjw70nac3fr5k5y3hcxr4hgg6rljzp59nqs6a0vh';
166+
psbt.addOutput({
167+
value: sendAmount,
168+
address,
169+
});
170+
171+
return { psbt, address };
172+
}
91173
const toBuffer = (a: Uint8Array) => Buffer.from(a);

test/integration/taproot.utils.ts

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import * as ecc from 'tiny-secp256k1';
2+
import ECPairFactory from 'ecpair';
3+
import { toXOnly } from '../../src/psbt/bip371';
4+
import * as bitcoin from '../..';
5+
6+
const ECPair = ECPairFactory(ecc);
7+
8+
// This logic will be extracted to ecpair
9+
export function tweakSigner(
10+
signer: bitcoin.Signer,
11+
opts: any = {},
12+
): bitcoin.Signer {
13+
// @ts-ignore
14+
let privateKey: Uint8Array | undefined = signer.privateKey!;
15+
if (!privateKey) {
16+
throw new Error('Private key is required for tweaking signer!');
17+
}
18+
if (signer.publicKey[0] === 3) {
19+
privateKey = ecc.privateNegate(privateKey);
20+
}
21+
22+
const tweakedPrivateKey = ecc.privateAdd(
23+
privateKey,
24+
tapTweakHash(toXOnly(signer.publicKey), opts.tweakHash),
25+
);
26+
if (!tweakedPrivateKey) {
27+
throw new Error('Invalid tweaked private key!');
28+
}
29+
30+
return ECPair.fromPrivateKey(Buffer.from(tweakedPrivateKey), {
31+
network: opts.network,
32+
});
33+
}
34+
35+
function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer {
36+
return bitcoin.crypto.taggedHash(
37+
'TapTweak',
38+
Buffer.concat(h ? [pubKey, h] : [pubKey]),
39+
);
40+
}

0 commit comments

Comments
 (0)