Coinjoin Transaction (4 inputs, 4 outputs) - Legacy P2PKH
To follow along this tutorial
|
What is a Coinjoin
The signatures, one per input, inside a transaction are completely independent of each other. This means that it’s possible for Bitcoin users to agree on a set of inputs to spend, and a set of outputs to pay to, and then to individually and separately sign a transaction and later merge their signatures. The transaction is not valid and won’t be accepted by the network until all signatures are provided, and no one will sign a transaction which is not to their liking.
To use this to increase privacy, the N users would agree on a uniform output size and provide inputs amounting to at least that size. The transaction would have N outputs of that size and potentially N more change outputs if some of the users provided input in excess of the target. All would sign the transaction, and then the transaction could be transmitted. No risk of theft at any point.
Consider the following transactions made at the same time: A purchases an item from B, C purchases an item from D, and E purchases an item from F. Without Coinjoin, the public blockchain ledger would record three separate transactions for each input-output match. With Coinjoin, only one single transaction is recorded. The ledger would show that bitcoins were paid from A, C, and E addresses to B, D, and F. By masking the deals made by all parties, an observer can’t, with full certainty, determine who sent bitcoins to whom.
To read more about Coinjoin:
-
Top-notch fungibility framework ZeroLink
-
The current most advanced Coinjoin wallet Wasabi Wallet
What we will do
Let’s create a Coinjoin transaction mixing four different transactions.
We will do the following:
Inputs | -> | Outputs |
---|---|---|
Alice_1 |
-> |
Bob_1 |
Carol_1 |
-> |
Dave_1 |
Eve_1 |
-> |
Mallory_2 |
Mallory_1 |
-> |
Alice_2 |
The four signers agree on a uniform output amount of 0.2 BTC.
We will create four UTXOs to spend from, with different amounts but with at least 0.2 BTC. If someone spend a UTXO that is more than 0.2 BTC, we will create a additional output for his change.
Creating UTXOs to spend
sendmany "" '{"n4SvybJicv79X1Uc4o3fYXWGwXadA53FSq":0.2, "mh1HAVWhKkzcvF41MNRKfakVvPV2sfaf3R":0.2, "mqbYBESF4bib4VTmsqe6twxMDKtVpeeJpt":0.25, "mwkyEhauZdkziHJijx9rwZgShr9gYi9Hkh": 0.3}'
Here we send:
-
0.2 BTC to alice_1 P2PKH address
-
0.2 BTC to carol_1 P2PKH address
-
Send 0.25 BTC to eve_1 P2PKH address
-
Send 0.3 BTC to mallory_1 P2PKH address
generatetoaddress 1 bcrt1qnqud2pjfpkqrnfzxy4kp5g98r8v886wgvs9e7r
Then we need to know which UTXO corresponds to which address.
gettransaction TX_ID
Find the output index (or vout) under |
scantxoutset start '["addr(n4SvybJicv79X1Uc4o3fYXWGwXadA53FSq)", "addr(mh1HAVWhKkzcvF41MNRKfakVvPV2sfaf3R)", "addr(mqbYBESF4bib4VTmsqe6twxMDKtVpeeJpt)", "addr(mwkyEhauZdkziHJijx9rwZgShr9gYi9Hkh)"]'
Creating the Coinjoin transaction
Now let’s spend the UTXOs and create four new ones. A facilitator, here alice_1, will create the transaction, sign his input, then pass the partially-signed transaction to the next participant and so on until everybody has signed and someone broadcast it.
const bitcoin = require('bitcoinjs-lib')
const { alice, bob, carol, dave, eve, mallory } = require('./wallets.json')
const network = bitcoin.networks.regtest
const keyPairAlice1 = bitcoin.ECPair.fromWIF(alice[1].wif, network)
const keyPairCarol1 = bitcoin.ECPair.fromWIF(carol[1].wif, network)
const keyPairEve1 = bitcoin.ECPair.fromWIF(eve[1].wif, network)
const keyPairMallory1 = bitcoin.ECPair.fromWIF(mallory[1].wif, network)
const nonWitnessUtxo = Buffer.from('TX_HEX', 'hex') (1)
1 | Get TX_HEX with "getrawtransaction TX_ID" |
const psbt = new bitcoin.Psbt({network})
.addInput({
hash: 'TX_ID',
index: TX_OUT,
nonWitnessUtxo
}) (1)
.addInput({
hash: 'TX_ID',
index: TX_OUT,
nonWitnessUtxo
})
.addInput({
hash: 'TX_ID',
index: TX_OUT,
nonWitnessUtxo
})
.addInput({
hash: 'TX_ID',
index: TX_OUT,
nonWitnessUtxo
})
.addOutput({
address: bob[1].p2pkh,
value: 2e7,
}) (2)
.addOutput({
address: dave[1].p2pkh,
value: 2e7,
})
.addOutput({
address: mallory[2].p2pkh,
value: 2e7,
})
.addOutput({
address: alice[2].p2pkh,
value: 2e7,
})
.addOutput({
address: eve[1].p2pkh,
value: 5e6 - 5e4,
}) (3)
.addOutput({
address: mallory[1].p2pkh,
value: 1e7 - 5e4,
})
1 | Inputs referencing UTXOs we have just created |
2 | 0.2 BTC payment outputs |
3 | We also have two more recipients that will get back their change (eve_1 and mallory_1) |
These UTXOs will not be coinjoined. One might reasonably assume that the 4950000 satoshis UTXO belongs to eve_1 and that the 9950000 satoshis UTXO belongs to mallory_1. Let’s also subtract the mining fees (0.0005 BTC each) from them. We can have different policies regarding who has to pay for the mining fees, it is just easier that way for our example. |
The miner fee is calculated by subtracting the outputs from the inputs.
(20000000 + 20000000 + 25000000 + 30000000)ins - (20000000 + 20000000 + 20000000 + 20000000 + 4950000 + 9950000)outs = 100 000 100 000 sats
It equals to 0,001 BTC, this is the mining fee.
psbt.signInput(0, keyPairAlice1)
psbt.signInput(1, keyPairCarol1)
psbt.signInput(2, keyPairEve1)
psbt.signInput(3, keyPairMallory1)
psbt.validateSignaturesOfInput(0)
psbt.validateSignaturesOfInput(1)
psbt.validateSignaturesOfInput(2)
psbt.validateSignaturesOfInput(3)
Participants are signing with the default SIGHASH_ALL flag, which prevents inputs or outputs from being manipulated after the fact.
|
psbt.finalizeAllInputs()
console.log('Transaction hexadecimal:')
console.log(psbt.extractTransaction().toHex())
decoderawtransaction TX_HEX