bitcoinjs / bitcoinjs-lib

A javascript Bitcoin library for node.js and browsers.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Signing a PBST generated with bitcoinjs-lib failed on my Coldcard

Yug-Damon opened this issue · comments

Hello, 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

https://github.com/bitcoinjs/bitcoinjs-lib/blob/v6.1.5/test/integration/transactions.spec.ts#L570-L574

Please see this example for an example of what is expected of bip32Derivation.

Your the path variable must be the entire path from the master key (identified with the masterFingerprint) all the way to the pubkey key.

Your path currently only goes down to the last hardened path, so I assume you need to include /0/0 and 0/1 for the buyer and seller paths.

Thank you so much to take the time to help me.
I had a look at the example but it's not so clear how articulate bip32Derivation around a p2wsh(p2ms) scenario

I've changed the bip32Derivation of the addInput relative to buyer's UTXO as follow:

path: "m/84'/0'/0'/0/" + this.buyerMultisigAddressIndex

this.buyerMultisigAddressIndex is the index used to create the multisig address in the first place:

const derivatedBuyerPubKey = bip32.fromBase58(buyerOutputDescriptor.pubkey, network).derive(0).derive(index).publicKey // index = this.buyerMultisigAddressIndex

I still have the same issue tho : "Unknown multisig wallet" on my Coldcard when I'm trying to sign the PSBT

Can you provide some insight about the role of the different parts of the code :

  • Is bip32Derivation key of the psbt.addInput required because I've used bip32.fromBase58 to generate my multisig address and the derivation paths must match ?
  • Since I'm trying to achieve a SegWit transaction I should provide witnessScript and witnessUtxo to the pst.addInput function, so far I'm using witnessScript = p2ms.output and witnessUtxo = p2wsh.output, am I correct ?
  • Does bip32Derivation[x].pubkey must match the addresses used to locked the UTXO ?
  • Does bip32Derivation[x].path must match the derivation path used to create the multisig address
  • I'm thinking about change to a sh(p2ms) addresses, does it make things easier ?

Thank you

Is bip32Derivation key of the psbt.addInput required because I've used bip32.fromBase58 to generate my multisig address and the derivation paths must match ?

ColdCard does not store every key you derive. It only stores the root seed. When it signs, it needs to know "Is this the correct seed?" (the masterFingerprint tells it that) "Where is the key I'm signing?" (the path tells it that) "Am I sure that this key I derived is the correct public key for signing?" (pubkey tells it that). So for EVERY KEY, you need ONE bip32Derivation object added to the array for each input.

Since I'm trying to achieve a SegWit transaction I should provide witnessScript and witnessUtxo to the pst.addInput function, so far I'm using witnessScript = p2ms.output and witnessUtxo = p2wsh.output, am I correct ?

Yes.

Does bip32Derivation[x].pubkey must match the addresses used to locked the UTXO ?

pubkey must match the public key that is used in the multisig.

Does bip32Derivation[x].path must match the derivation path used to create the multisig address

Yes. The path is from the root seed all the way to the individual pubkey. (remember, the xpub (extended public key) is usually taken from a layer in the path midway (ie. m/0'/0'/0/0 pubkey will usually be derived by deriving 0/0 from the xpub at m/0'/0' etc etc... usually the xpub is of the last hardened layer (since xpubs can not derive hardened children))

I'm thinking about change to a sh(p2ms) addresses, does it make things easier ?

No. It will be the same.

Your latest comment says path: "m/84'/0'/0'/0/" + this.buyerMultisigAddressIndex but your previous comment said path: "m/84'/0'/0'/2'",.... it looks like you are missing a 2' somewhere?

I think you are just getting confused about which path goes where.

Try organizing all your data so you don't get confused... then try again.

Thank you so much for you extensive answer !
You're right, my code begins to be messy with all my tests. I've extracted the important element to a minimal script to test.

I come to that :

this.bip32 = BIP32Factory(ecc)
const cosigner1Pubkey = this.bip32.fromBase58('xpubCosigner1').publicKey
const cosigner2Pubkey = this.bip32.fromBase58('xpubCosigner2').publicKey
const cosigner1DerivatedPubKey = this.bip32.fromBase58('xpubCosigner1').derive(0).derive(2).publicKey
const cosigner2DerivatedPubKey = this.bip32.fromBase58('xpubCosigner2').derive(0).derive(2).publicKey

const p2ms2 = bitcoinLib.payments.p2ms({
    m: 2,
    pubkeys: [cosigner1DerivatedPubKey, cosigner2DerivatedPubKey].sort((a, b) => a.compare(b))
})

const {address: depositAddress, output: witnessUtxo, redeem: {output: witnessScript}} = bitcoinLib.payments.p2wsh({
    redeem: p2ms2
})

const buyerUTXOs = await new BlockStreamApi()(depositAddress)

let psbt = new bitcoinLib.Psbt()

for(const utxo of buyerUTXOs) {
    psbt.addInput({
        hash: utxo.txid,
        index: utxo.vout,
        bip32Derivation: [
            {
                pubkey: cosigner1DerivatedPubKey,
                path: "m/84'/0'/0'/2/0/2",
                masterFingerprint: Buffer.from('aefb5373', 'hex'),
            },
            {
                pubkey: cosigner2DerivatedPubKey,
                path: "m/84'/0'/0'/2/0/2",
                masterFingerprint: Buffer.from('86e3ff5f', 'hex'),
            }
        ],
        witnessScript: witnessScript,
        witnessUtxo: {
            script: witnessUtxo,
            value: utxo.value
        }
    })
}

const {address} = bitcoinLib.payments.p2wpkh({ pubkey: cosigner2Pubkey })
psbt.addOutput({address, value: 10000})
const hex = psbt.toBase64()
console.log(hex)`

Unfortunately the situation remains unchanged...

Please note I'm using the path : m/84'/0'/0'/2/0/2 for the bip32Derivation, this value is the one I got from Sparrow for this specific UTXO I have 11 000 sat on (I should learn how to use the Testnet)

My 2 original cosigner pub keys have the path m/84'/0'/0' defined
The OutputDescriptor path is m/84'/0'/0'/2 (the /2 is added automatically and I cannot save only with m/84'/0'/0')

I'm so close to succeed this but cannot figure it out the missing piece.

If the depositAddress is correct, than you should know 100% that 0/2 is the end of the path for sure.

In that case, it means one of two things:

  1. Your masterFingerprints are wrong.
  2. Your path before 0/2 is wrong.

So my question is: Is depositAddress correct? If so, where are you finding the m/84'/0'/0'/2 path from? Your previous post shows m/84'/0'/0'/2' where the final 2 is hardened. Are you sure it's not m/84'/0'/2' with only one 0?

When you create a multisig you need to keep ALL the information. Losing any of the information will cause these kinds of issues.

Thank you again for your help, I actually succeed to make my scenario to work!

Here the step of my escrow service:

  1. Users provide xpub export from they coldcards (settings->multisigs->xpub/account) they should get a pubkey, fingerprint and a derivation path with the following shape : m/48’/0’/{account}’/2’
  2. The system is building a multisig wallet addresses from these informations with the extra /0/x derivation
  3. The user fund the wallet
  4. Once the wallet balance is good, I generate a PSBT and a coldcard-multisig.txt
  5. User import the .txt file into the coldcard (settings->multisignature->import) then User uses Sparrow to sign the PSBT and upload it (the key to solve my issue!)
  6. Once all PSBTs are provided I combine them and broadcast the transaction

The only drawback is the participant must have coldcards. Not a real issue since it’s pretty popular but I wonder if I can adapt to make it work with ledger

Thank you to have enlighten some misconception I had about multi-signature, here another question I have:

  • In my process the multi-signature is automatically generated
  • For testing purpose I proceed to manually build it with Sparrow, from this wallet I generated and exported a transaction
  • it appears the only difference with the autogenerated one is the presence of the extendedPubkey (psbt:globalMap->globalXpub),
  • I haven’t figure it out any way to give this information to bitcoinjs-lib,
  • this explains why I should import the coldcards-multisignature.txt which contains these informations into the coldcard.
  • After further research embed the extendedPubkey into the PSBT should be possible according to the BIP174.
  • Do you know a way to populate globalMap->globalXpub during the psbt building process?

Do you know a way to populate globalMap->globalXpub during the psbt building process?

You can call

psbt.updateGlobal({
  globalXpub: [
    {
      extendedPubkey, // This is a 78 byte Buffer of the xpub string using base58check.decode(string) on the xpub string
      masterFingerprint, // This is 4 byte Buffer (you know what this is)
      path, // This is the path from root to xpub (I think m/48’/0’/{account}’/2’ maybe, from your description)
    },
  ],
})

It totally work ! I don't have to import the multi-sig wallet on the Coldcard anymore, that's way better on a user experience perspective.

Thank you so much for your time.