From 832fb071cf036167adaadb01e74c41d6a57f628b Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 1 Jun 2022 06:52:23 +0300 Subject: [PATCH 1/3] feat: initial commit --- test/integration/silent-payments.spec.ts | 91 ++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 test/integration/silent-payments.spec.ts diff --git a/test/integration/silent-payments.spec.ts b/test/integration/silent-payments.spec.ts new file mode 100644 index 000000000..1f98dc654 --- /dev/null +++ b/test/integration/silent-payments.spec.ts @@ -0,0 +1,91 @@ +import BIP32Factory from 'bip32'; +import * as ecc from 'tiny-secp256k1'; +import { describe, it } from 'mocha'; + +import { regtestUtils } from './_regtest'; +import * as bitcoin from '../..'; +import { toXOnly } from '../../src/psbt/bip371'; + +const rng = require('randombytes'); +const regtest = regtestUtils.network; +bitcoin.initEccLib(ecc); +const bip32 = BIP32Factory(ecc); + +describe('bitcoinjs-lib (silent payments)', () => { + it('can create (and broadcast via 3PBP) a simple silent payment', async () => { + const { senderKeyPair, receiverKeyPair, sharedSecret } = initParticipants(); + // this is what the sender sees/scans + const silentPublicKey = toXOnly(receiverKeyPair.publicKey); + + // the input being spent + const { output: p2wpkhOutput } = bitcoin.payments.p2wpkh({ + pubkey: senderKeyPair.publicKey, + network: regtest, + }); + + // amount from faucet + const amount = 42e4; + // amount to send + const sendAmount = amount - 1e4; + // get faucet + const unspent = await regtestUtils.faucetComplex(p2wpkhOutput!, amount); + + const psbt = new bitcoin.Psbt({ network: regtest }); + psbt.addInput({ + hash: unspent.txId, + index: 0, + witnessUtxo: { value: amount, script: p2wpkhOutput! }, + }); + + // destination + const { address } = bitcoin.payments.p2tr({ + internalPubkey: silentPublicKey, + hash: sharedSecret, + network: regtest, + }); + psbt.addOutput({ value: sendAmount, address: address! }); + + psbt.signInput(0, senderKeyPair); + + psbt.finalizeAllInputs(); + const tx = psbt.extractTransaction(); + const rawTx = tx.toBuffer(); + + const hex = rawTx.toString('hex'); + + await regtestUtils.broadcast(hex); + await regtestUtils.verify({ + txId: tx.getId(), + address: address!, + vout: 0, + value: sendAmount, + }); + }); +}); + +function initParticipants() { + const receiverKeyPair = bip32.fromSeed(rng(64), regtest); + const senderKeyPair = bip32.fromSeed(rng(64), regtest); + + + const senderSharedSecret = ecc.pointMultiply( + receiverKeyPair.publicKey, + senderKeyPair.privateKey!, + ); + + const receiverSharedSecred = ecc.pointMultiply( + senderKeyPair.publicKey, + receiverKeyPair.privateKey!, + ); + + if (!toBuffer(receiverSharedSecred!).equals(toBuffer(senderSharedSecret!))) + throw new Error('Shared secret missmatch.'); + + return { + receiverKeyPair, + senderKeyPair, + sharedSecret: toXOnly(Buffer.from(receiverSharedSecred!)), + }; +} + +const toBuffer = (a: Uint8Array) => Buffer.from(a); From 006212ed8dd102b6dc1ae2b8d3f8256de5f6b6ee Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 1 Jun 2022 14:08:12 +0300 Subject: [PATCH 2/3] feat: spend from tweaked silent address --- test/integration/silent-payments.spec.ts | 160 +++++++++++++++++------ test/integration/taproot.utils.ts | 40 ++++++ 2 files changed, 161 insertions(+), 39 deletions(-) create mode 100644 test/integration/taproot.utils.ts diff --git a/test/integration/silent-payments.spec.ts b/test/integration/silent-payments.spec.ts index 1f98dc654..18a310a48 100644 --- a/test/integration/silent-payments.spec.ts +++ b/test/integration/silent-payments.spec.ts @@ -6,6 +6,8 @@ import { regtestUtils } from './_regtest'; import * as bitcoin from '../..'; import { toXOnly } from '../../src/psbt/bip371'; +import { tweakSigner } from './taproot.utils'; + const rng = require('randombytes'); const regtest = regtestUtils.network; bitcoin.initEccLib(ecc); @@ -13,61 +15,90 @@ const bip32 = BIP32Factory(ecc); describe('bitcoinjs-lib (silent payments)', () => { it('can create (and broadcast via 3PBP) a simple silent payment', async () => { + // for simplicity the transactions in this test have only one input and one output + const { senderKeyPair, receiverKeyPair, sharedSecret } = initParticipants(); - // this is what the sender sees/scans + + // this is what the sender sees/scans (from twitter bio, public forum, truck door) const silentPublicKey = toXOnly(receiverKeyPair.publicKey); - // the input being spent - const { output: p2wpkhOutput } = bitcoin.payments.p2wpkh({ - pubkey: senderKeyPair.publicKey, + const senderUtxo = await fundP2pkhUtxo(senderKeyPair.publicKey); + + // amount to pay the silent address + const payAmount = senderUtxo.value - 1e4; + const { + psbt: payPsbt, + address: tweakedSilentAddress, + } = buildPayToSilentAddress( + senderUtxo.txId, + senderUtxo, + silentPublicKey, + payAmount, + sharedSecret, + ); + payPsbt.signInput(0, senderKeyPair).finalizeAllInputs(); + + // the transaction paying to the silent address + const payTx = payPsbt.extractTransaction(); + await broadcastAndVerifyTx(payTx, tweakedSilentAddress!, payAmount); + + // the amount the receiver will spend + const sendAmount = payAmount - 1e4; + // the utxo with the tweaked silent address + const receiverUtxo = { value: payAmount, script: payTx.outs[0].script }; + const { psbt: spendPsbt, address } = buildSpendFromSilentAddress( + payTx.getId(), + receiverUtxo, + silentPublicKey, + sendAmount, + sharedSecret, + ); + + const tweakedSigner = tweakSigner(receiverKeyPair!, { + tweakHash: sharedSecret, network: regtest, }); + spendPsbt.signInput(0, tweakedSigner).finalizeAllInputs(); - // amount from faucet - const amount = 42e4; - // amount to send - const sendAmount = amount - 1e4; - // get faucet - const unspent = await regtestUtils.faucetComplex(p2wpkhOutput!, amount); - - const psbt = new bitcoin.Psbt({ network: regtest }); - psbt.addInput({ - hash: unspent.txId, - index: 0, - witnessUtxo: { value: amount, script: p2wpkhOutput! }, - }); - - // destination - const { address } = bitcoin.payments.p2tr({ - internalPubkey: silentPublicKey, - hash: sharedSecret, - network: regtest, - }); - psbt.addOutput({ value: sendAmount, address: address! }); + // the transaction spending from the silent address + const spendTx = spendPsbt.extractTransaction(); + await broadcastAndVerifyTx(spendTx, address!, sendAmount); + }); +}); - psbt.signInput(0, senderKeyPair); +async function fundP2pkhUtxo(senderPubKey: Buffer) { + // the input being spent + const { output: p2wpkhOutput } = bitcoin.payments.p2wpkh({ + pubkey: senderPubKey, + network: regtest, + }); - psbt.finalizeAllInputs(); - const tx = psbt.extractTransaction(); - const rawTx = tx.toBuffer(); + // amount from faucet + const amount = 42e4; + // get faucet + const unspent = await regtestUtils.faucetComplex(p2wpkhOutput!, amount); - const hex = rawTx.toString('hex'); + return { value: amount, script: p2wpkhOutput!, txId: unspent.txId }; +} - await regtestUtils.broadcast(hex); - await regtestUtils.verify({ - txId: tx.getId(), - address: address!, - vout: 0, - value: sendAmount, - }); +async function broadcastAndVerifyTx( + tx: bitcoin.Transaction, + address: string, + value: number, +) { + await regtestUtils.broadcast(tx.toBuffer().toString('hex')); + await regtestUtils.verify({ + txId: tx.getId(), + address: address!, + vout: 0, + value, }); -}); +} function initParticipants() { const receiverKeyPair = bip32.fromSeed(rng(64), regtest); const senderKeyPair = bip32.fromSeed(rng(64), regtest); - const senderSharedSecret = ecc.pointMultiply( receiverKeyPair.publicKey, senderKeyPair.privateKey!, @@ -88,4 +119,55 @@ function initParticipants() { }; } +function buildPayToSilentAddress( + prevOutTxId: string, + witnessUtxo: { value: number; script: Buffer }, + silentPublicKey: Buffer, + sendAmount: number, + sharedSecret: Buffer, +) { + const psbt = new bitcoin.Psbt({ network: regtest }); + psbt.addInput({ + hash: prevOutTxId, + index: 0, + witnessUtxo, + }); + + // destination + const { address } = bitcoin.payments.p2tr({ + internalPubkey: silentPublicKey, + hash: sharedSecret, + network: regtest, + }); + psbt.addOutput({ value: sendAmount, address: address! }); + + return { psbt, address }; +} + +function buildSpendFromSilentAddress( + prevOutTxId: string, + witnessUtxo: { value: number; script: Buffer }, + silentPublicKey: Buffer, + sendAmount: number, + sharedSecret: Buffer, +) { + const psbt = new bitcoin.Psbt({ network: regtest }); + psbt.addInput({ + hash: prevOutTxId, + index: 0, + witnessUtxo, + tapInternalKey: silentPublicKey, + tapMerkleRoot: sharedSecret, + }); + + // random address value, not important + const address = + 'bcrt1pqknex3jwpsaatu5e5dcjw70nac3fr5k5y3hcxr4hgg6rljzp59nqs6a0vh'; + psbt.addOutput({ + value: sendAmount, + address, + }); + + return { psbt, address }; +} const toBuffer = (a: Uint8Array) => Buffer.from(a); diff --git a/test/integration/taproot.utils.ts b/test/integration/taproot.utils.ts new file mode 100644 index 000000000..aae539d2b --- /dev/null +++ b/test/integration/taproot.utils.ts @@ -0,0 +1,40 @@ +import * as ecc from 'tiny-secp256k1'; +import ECPairFactory from 'ecpair'; +import { toXOnly } from '../../src/psbt/bip371'; +import * as bitcoin from '../..'; + +const ECPair = ECPairFactory(ecc); + +// This logic will be extracted to ecpair +export function tweakSigner( + signer: bitcoin.Signer, + opts: any = {}, +): bitcoin.Signer { + // @ts-ignore + let privateKey: Uint8Array | undefined = signer.privateKey!; + if (!privateKey) { + throw new Error('Private key is required for tweaking signer!'); + } + if (signer.publicKey[0] === 3) { + privateKey = ecc.privateNegate(privateKey); + } + + const tweakedPrivateKey = ecc.privateAdd( + privateKey, + tapTweakHash(toXOnly(signer.publicKey), opts.tweakHash), + ); + if (!tweakedPrivateKey) { + throw new Error('Invalid tweaked private key!'); + } + + return ECPair.fromPrivateKey(Buffer.from(tweakedPrivateKey), { + network: opts.network, + }); +} + +function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer { + return bitcoin.crypto.taggedHash( + 'TapTweak', + Buffer.concat(h ? [pubKey, h] : [pubKey]), + ); +} From 8cd26dfae939a4566fe4c05e71b3625646a97138 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Wed, 1 Jun 2022 14:21:31 +0300 Subject: [PATCH 3/3] chore: fix lint --- test/integration/silent-payments.spec.ts | 30 +++++++++++++++--------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/test/integration/silent-payments.spec.ts b/test/integration/silent-payments.spec.ts index 18a310a48..be5c00ece 100644 --- a/test/integration/silent-payments.spec.ts +++ b/test/integration/silent-payments.spec.ts @@ -14,18 +14,18 @@ bitcoin.initEccLib(ecc); const bip32 = BIP32Factory(ecc); describe('bitcoinjs-lib (silent payments)', () => { + // for simplicity the transactions in this test have only one input and one output it('can create (and broadcast via 3PBP) a simple silent payment', async () => { - // for simplicity the transactions in this test have only one input and one output - const { senderKeyPair, receiverKeyPair, sharedSecret } = initParticipants(); // this is what the sender sees/scans (from twitter bio, public forum, truck door) const silentPublicKey = toXOnly(receiverKeyPair.publicKey); const senderUtxo = await fundP2pkhUtxo(senderKeyPair.publicKey); - // amount to pay the silent address const payAmount = senderUtxo.value - 1e4; + + // The sender pays to the tweaked slient adddress const { psbt: payPsbt, address: tweakedSilentAddress, @@ -42,10 +42,12 @@ describe('bitcoinjs-lib (silent payments)', () => { const payTx = payPsbt.extractTransaction(); await broadcastAndVerifyTx(payTx, tweakedSilentAddress!, payAmount); - // the amount the receiver will spend - const sendAmount = payAmount - 1e4; // the utxo with the tweaked silent address const receiverUtxo = { value: payAmount, script: payTx.outs[0].script }; + // the amount the receiver will spend + const sendAmount = payAmount - 1e4; + + // the receiver spends from the tweaked silent address const { psbt: spendPsbt, address } = buildSpendFromSilentAddress( payTx.getId(), receiverUtxo, @@ -66,7 +68,9 @@ describe('bitcoinjs-lib (silent payments)', () => { }); }); -async function fundP2pkhUtxo(senderPubKey: Buffer) { +async function fundP2pkhUtxo( + senderPubKey: Buffer, +): Promise<{ value: number; script: Buffer; txId: string }> { // the input being spent const { output: p2wpkhOutput } = bitcoin.payments.p2wpkh({ pubkey: senderPubKey, @@ -85,7 +89,7 @@ async function broadcastAndVerifyTx( tx: bitcoin.Transaction, address: string, value: number, -) { +): Promise { await regtestUtils.broadcast(tx.toBuffer().toString('hex')); await regtestUtils.verify({ txId: tx.getId(), @@ -95,7 +99,11 @@ async function broadcastAndVerifyTx( }); } -function initParticipants() { +function initParticipants(): { + receiverKeyPair: bitcoin.Signer; + senderKeyPair: bitcoin.Signer; + sharedSecret: Buffer; +} { const receiverKeyPair = bip32.fromSeed(rng(64), regtest); const senderKeyPair = bip32.fromSeed(rng(64), regtest); @@ -125,7 +133,7 @@ function buildPayToSilentAddress( silentPublicKey: Buffer, sendAmount: number, sharedSecret: Buffer, -) { +): { psbt: bitcoin.Psbt; address: string } { const psbt = new bitcoin.Psbt({ network: regtest }); psbt.addInput({ hash: prevOutTxId, @@ -141,7 +149,7 @@ function buildPayToSilentAddress( }); psbt.addOutput({ value: sendAmount, address: address! }); - return { psbt, address }; + return { psbt, address: address! }; } function buildSpendFromSilentAddress( @@ -150,7 +158,7 @@ function buildSpendFromSilentAddress( silentPublicKey: Buffer, sendAmount: number, sharedSecret: Buffer, -) { +): { psbt: bitcoin.Psbt; address: string } { const psbt = new bitcoin.Psbt({ network: regtest }); psbt.addInput({ hash: prevOutTxId,