bitcoins-lib - Cannot sign PSBT in Sparrow

57 Views Asked by At

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

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
0

There are 0 best solutions below