I'm developing a Bitcoin tool
General idea
Initial situation:
- Alice & Bob want to trade
- Alice wants to buy a rubber duck from Bob for X BTC
- Alice wants to receive the rubber duck before paying Bob
- Bob wants to be paid before sending the rubber duck
- the incentives are incompatible
Collaterized transaction:
- Alice & Bob create a 2/2 multisig wallet from their pub key
- Alice put X (+ transaction fee) BTC on this wallet
- Bob can see the funds are provided
- Bob can put Y BTC as collateral to increase his trust in Alice
- Alice can put Z BTC as collateral to increase his trust in Bob
- Alice & Bob sign the transaction to release the funds
- Alice get Z
- Bob get X + Y
- the incentives are aligned
Implementation
- So far, I've used bitcoinjs-lib to implement this approach.
- Here, is the main ressource I've followed:
2/2 multisig wallet generation:
- I create 2 SegWit addresses from Alice & Bob pub keys, they deposit addresses
- I craft an output descriptor in order to confirm the generated addresses belong to this wallet
getMultisigAddress(buyerPubKey, sellerPubKey, index) {
const derivatedBuyerPubKey = bip32.fromBase58(buyerPubKey, network).derive(0).derive(index).publicKey
const derivatedSellerPubKey = bip32.fromBase58(sellerPubKey, network).derive(0).derive(index).publicKey
const p2ms = bitcoinLib.payments.p2ms({
m: 2,
pubkeys: [derivatedBuyerPubKey, derivatedSellerPubKey].sort((a, b) => a.compare(b)),
network: network,
})
const p2wsh = bitcoinLib.payments.p2wsh({
redeem: p2ms,
network: network,
})
return {
address: p2wsh.address,
witnessScript: p2wsh.redeem.output.toString('hex'),
witnessUtxo: '0020' + bitcoinLib.crypto.sha256(p2ms.output).toString('hex'),
}
}
getMultiSigWallet(buyerPubKey, sellerPubKey) {
const buyerMultisigInfo = this.getMultisigAddress(buyerPubKey, sellerPubKey, 0)
const sellerMultisigInfo = this.getMultisigAddress(buyerPubKey, sellerPubKey, 1)
const descriptor = 'wsh(sortedmulti(2, [00000000/48h/0h]' + buyerPubKey + ', [00000000/48h/0h]' + sellerPubKey + '))'
return {
descriptor,
buyerMultisigInfo,
sellerMultisigInfo
}
}
Later, when the wallet is fully fund, I generate a PSBT
PSBT generation:
createUnsignedTransaction(mTx, buyerUTXOs, sellerUTXOs, transactionFeesPerByte) {
let psbt = new bitcoinLib.Psbt({ network: network })
for(const utxo of buyerUTXOs) {
psbt.addInput({
hash: utxo.txid,
index: utxo.vout,
witnessScript: Buffer.from(mTx.step4.buyerMultisigInfo.witnessScript, 'hex'),
witnessUtxo: {
script: Buffer.from(mTx.step4.buyerMultisigInfo.witnessUtxo, 'hex'),
value: utxo.value
}
})
}
for(const utxo of sellerUTXOs) {
psbt.addInput({
hash: utxo.txid,
index: utxo.vout,
witnessScript: Buffer.from(mTx.step4.sellerMultisigInfo.witnessScript, 'hex'),
witnessUtxo: {
script: Buffer.from(mTx.step4.sellerMultisigInfo.witnessUtxo, 'hex'),
value: utxo.value
}
})
}
const {publicKey: buyerPubkey} = bip32.fromBase58(mTx.step3.buyerPubkey, network).derive(0).derive(0)
const {publicKey: sellerPubkey} = bip32.fromBase58(mTx.step3.sellerPubkey, network).derive(0).derive(0)
const {publicKey: escrowPubkey} = bip32.fromBase58(this.escrowPubkey, network).derive(0).derive(0)
const {address: buyerAddress} = bitcoinLib.payments.p2pkh({ pubkey: buyerPubkey, network })
const {address: sellerAddress} = bitcoinLib.payments.p2pkh({ pubkey: sellerPubkey, network })
const {address: escrowAddress} = bitcoinLib.payments.p2pkh({ pubkey: escrowPubkey, network })
if (buyerUTXOs.length === 0 && sellerUTXOs.length === 0) {
throw 'No UTXO available to cover the expense'
}
const sellerOutputValue = this.computeSellerOutputValue(mTx)
const outputsCount = sellerOutputValue ? 3 : 2
const transactionSize = this.estimateMultisigTransactionSize({inputsCount: buyerUTXOs.length + sellerUTXOs.length, outputsCount, numPubKeys: 2, numSignatures: 2})
const transactionFees = transactionFeesPerByte * transactionSize
const escrowOutputValue = this.computeEscrowOutputValue(mTx)
const buyerOutputValue = this.computeBuyerOutputValue(mTx, transactionFees, escrowOutputValue)
if(buyerOutputValue > 0) {
psbt.addOutput({address: buyerAddress, value: buyerOutputValue})
}
if(sellerOutputValue > 0) {
psbt.addOutput({address: sellerAddress, value: sellerOutputValue})
}
if(escrowOutputValue > 0) {
psbt.addOutput({address: escrowAddress, value: escrowOutputValue})
}
return psbt.toBase64()
}
Result
Facts:
- The generated PSBT is well imported in Sparrow
- I can see the expected fund distribution
- I can see the transaction expects 2/2 signatures
- I can open a wallet from the generator output descriptor (readonly wallet)
- This wallet is a matching wallet for this transaction
- I can find the generated addresses (and the associated funds)
Issues
When I click on Sign, Sparrow does not recognise my Coldcard as a valid input to partially sign the transaction. EDIT I've solved this issue by adding the Master fingerprint to the Output descriptor.
The PSBT is transferred to the Coldcard, then I got the following message : "Failure, PSBT does not contain any key path information."
From coldcard Github :
# Can happen w/ Electrum in watch-mode on XPUB. It doesn't know XFP and
# so doesn't insert that into PSBT.
raise FatalPSBTIssue('PSBT does not contain any key path information.')
EDIT I've solved this issue by adding to addInput :
bip32Derivation: [
{
masterFingerprint: Buffer.from('86e3ff5f', 'hex'),
pubkey: sellerNode.derive(0).derive(1).publicKey,
path: "m/84'/0'/0'/2'",
}
The PSBT is transferred to the Coldcard, then I got the following message : "Unknown multisig wallet"
From Coldcard Github :
if not psbt.active_multisig: # search for multisig wallet wal = MultisigWallet.find_match(M, N, xfp_paths) if not wal: raise FatalPSBTIssue('Unknown multisig wallet') psbt.active_multisig = wal
Thoughts
- Maybe it's an implementation issue, then I hope you can help me
- Maybe it's an approach issue, then you can educate me