#offline #sign #bitcoin #cold #private-key #cli

app firma-cli

firma-cli is a command line interface tool to create bitcoin multisig wallets with private keys stored on offline devices

8 releases (breaking)

0.20.0 Aug 6, 2021
0.19.0 Dec 23, 2020
0.18.0 Dec 15, 2020
0.17.0 Dec 3, 2020
0.13.0 Oct 14, 2020

#6 in #cold

MIT license

225KB
4.5K SLoC

MIT license Crates

Creating a 2of2 multisig wallet p2wsh

During the following step we are going to create a multisig wallet 2of2 in testnet and we are going to sign and broadcast a transaction. It is required a synced bitcoin node.

In the first steps we are going to create two master keys.

Create first Master Key

This step creates the first master key. (It is possible to create the key with dice, see firma-offline dice --help)

firma-offline random --key-name a1
{
  "id": {
    "kind": "MasterSecret",
    "name": "a1",
    "network": "testnet"
  },
  "key": "tprv8ZgxMBicQKsPdQsGb1U22Lw7bPwhbxRkV8Q1mf8mv42q6HpJS7MW5hx1J44gKK6m2pQyC32mG1i6v1P9C97MDx7MvKZzgoXTpcwUgTSEobm"
}

We could have encrypted the key before saving on disk, for example leveraging existing gpg setups like so

 # encryption key creation and storage in encrypted gpg
  dd if=/dev/urandom bs=1 count=32 | gpg --encrypt -r 'DEADBEEF!' >encryption_key.gpg

  # bitcoin private key creation
  gpg --decrypt encryption_key.gpg | firma-offline --encrypt random --key-name a1

in this latter case the key file ~/.firma/testnet/keys/a1/master_secret.json looks like this

{
  "t": "encrypted",
  "c": {
    "t": "base64",
    "c": "5Vqdw61WCoxyvl6mj6WBGEPzzI/SCLxwukHbbxCYsQphkPdoGDMaPhLL7Jg7Ok4yJa7E79LiiOSaRcszjnyLH3lfskF3ii2u5qTcacQhmuh5HV8d275hGAoYejY24MU58h/4Mo5A3om6woRpIgABmAEFXGCeTsjgvvXO+iD0EJA2tse+YQJRhMQGYCMeNH7BcrnWrAhhu3eBCsZdt0j5bsrP6aX3DLQIW6uUhsP7nklscGdRstu82+NEkdwonP+hBXBrkFxfe9DCer/x4IbeZF6TGA=="
  }
}

In this latter case to read the plain text of the key

gpg --decrypt encryption_key.gpg | firma-offline --encrypt export --kind MasterSecret --name a1
{
  "id": {
    "kind": "MasterSecret",
    "name": "a1e",
    "network": "testnet"
  },
  "key": "tprv8ZgxMBicQKsPdHXHYrBsowgZXAh1bisk2nxvJLqRakJ9tZDLTLgFSGZDH79bKF19cTkmW8LHV3ZFbRytQAxjXx1MUFrrzpdfxiFcqqfpjkf"
}

Note: if the wallet files are encrypted, any command need to be fed with the encryption_key to encrypt and decrypt the data.

Create second Master Key

This one is created providing dice launches:

firma-offline dice --key-name a2 --faces 20 -l 12 -l 11 -l 1 -l 1 -l 16 -l 8 -l 1 -l 12 -l 7 -l 4 -l 12 -l 8 -l 1 -l 1 -l 1 -l 1 -l 1 -l 1 -l 1 -l 1 -l 1 -l 1 -l 1 -l 1 -l 1 -l 1 -l 1 -l 1 -l 1 -l 1 -l 1 -l 1 -l 1 -l 1 -l 18 -l 19 -l 12 -l 1 -l 16 -l 1 -l 18 -l 1 -l 13 -l 1 -l 1 -l 16 -l 4 -l 3 -l 1 -l 1 -l 1 -l 1 -l 1 -l 20 -l 19 -l 18 -l 17 -l 12 -l 2
{
  "dice": {
    "faces": 20,
    "launches": "[12, 11, 1, 1, 16, 8, 1, 12, 7, 4, 12, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 18, 19, 12, 1, 16, 1, 18, 1, 13, 1, 1, 16, 4, 3, 1, 1, 1, 1, 1, 20, 19, 18, 17, 12, 2]",
    "value": "33146769803929392242686007705600000000000000300775117168313561497600063822621"
  },
  "id": {
    "kind": "MasterSecret",
    "name": "a2",
    "network": "testnet"
  },
  "key": "tprv8ZgxMBicQKsPdGNW7N9EPsGpWBc56L8kKncoZfxC83M5ipBV2fMhujCBnxiTx33HnqiERg6fYsgKVBdSMNKm8nEcESHfUXAUecnyWnrx6Ls"
}

Create the 2of2 multisig wallet

The first time using the firma-online tool you need to initiate the connection parameters:

firma-online connect --url http://127.0.0.1:8332 --cookie-file $HOME/.bitcoin/.cookie

This must be done once per network, and every time node configuration change.

For the example we are using the two master_key created in the previous step. From the offline machines copy $HOME/.firma/testnet/keys/a1/descriptor_public_key.json and $HOME/.firma/testnet/keys/a2/descriptor_public_key.json to the online machine. You may choose to copy the files to the correct destination directory, or use the import command which would allow to optionally encrypt this data.

firma-online create-wallet --wallet-name firma-wallet -r 2 --key-name a1 --key-name a2
{
  "created_at_height": 1934912,
  "descriptor": "wsh(multi(2,[2f15d226/48'/1'/0'/2']tpubDFHFgFr6HbP88U7grBQ44yvocSU1EGkXX1dArKRum1qvb4Y6hy4CpJuPqpKZSyVnHptf6zoaW4HUjFHXgmtfy2vTGF1fccPy2ioNvKeZUnq/0/*,[7938c502/48'/1'/0'/2']tpubDEJZZGYXbZKMNEWgCdG9XZDycYM19Y8WxNc2cYcxLYhnNvKpbFNkgAza1x4GCUAHLdxx28R6dX88VhjgmZscW8Dzw6pGDzJ8a4gUCqHh1ny/0/*))#jpa3vwyx",
  "id": {
    "kind": "Wallet",
    "name": "firma-wallet",
    "network": "testnet"
  }
}

Note wallet file wallet.json could be signed with one of the participant key using the sign_wallet command, this prevent an attacker to tamper with the watch-only wallet without getting noticed.

Create a receiving address

Create a new address from the just generated wallet. Bitcoin node parameters are not needed anymore since have been saved in $HOME/.firma/testnet/firma-wallet/descriptor.json

firma-online get-address --wallet-name firma-wallet 
{
  "address": "tb1qdkl3aufvvk2zst22dy3ffjt0kfdl79mhvu6jcwecm5exm6j8dveseklast",
  "path": "m/0/0"
}

State of indexes is saved in $HOME/.firma/testnet/wallets/firma-wallet/indexes.json and by calling the command again we have:

{
  "address": "tb1q8m2456wjxu8mlkf708d2yvtmtlg59awvd2l3jjzkmt37gtzmx6psva9fnl",
  "path": "m/0/1"
}

Send some funds to tb1qdkl3aufvvk2zst22dy3ffjt0kfdl79mhvu6jcwecm5exm6j8dveseklast

Check balance and coins

firma-online balance --wallet-name firma-wallet
{
  "confirmed": {
    "btc": "0.00000000",
    "satoshi": 0
  },
  "pending": {
    "btc": "0.00586300",
    "satoshi": 586300
  }
}
firma-online list-coins --wallet-name firma-wallet 
{
  "coins": [
    {
      "amount": 586300,
      "outpoint": "43d3a56e8afe96eeb1c3a260bae735d064e5946190d9fb90524047bd21dbf383:0",
      "unconfirmed": true
    }
  ]
}

Create the PSBT

After funds receive a confirmation we can create the PSBT specifiying the recipient and the amount, you can specify more than one recipient and you can explicitly spend specific utxo with --coin. See firma-online create-tx --help

firma-online create-tx --wallet-name firma-wallet --recipient tb1q8m2456wjxu8mlkf708d2yvtmtlg59awvd2l3jjzkmt37gtzmx6psva9fnl:22400 --psbt-name test
{
  "address_reused": [],
  "funded_psbt": {
    "name": "test",
    "psbt": "cHNidP8BAH0CAAAAAaF+ubg9XAD6ZmGErqdrAdpu4ALBdz7M+TM0oPWNu5scAAAAAAD+////AqMBAAAAAAAAIgAgaMI+YcHlEUY9mvkIVax3/a4d42jXZgcjrbWqpKNFp7l4BQAAAAAAABYAFJmYo0dJtoF0R0qMtwYR5jtL6UGJAAAAAAX8bmFtZQZ0ZXN0LWEAAQEr0AcAAAAAAAAiACASrnn5+Jhe7Sivhhv6EwzbxegCw/oJSyLNg+b0h851tAEFR1IhAuOPRdJj6043K51DVaw+MIyMHEBOuEGrv89me8fOaQLpIQLgQluouXrqa42FwP/Ki9mwFxHFQy/50SN4Zcn73HSwZFKuIgYC4EJbqLl66muNhcD/yovZsBcRxUMv+dEjeGXJ+9x0sGQMeTjFAgAAAAAAAAAAIgYC449F0mPrTjcrnUNVrD4wjIwcQE64Qau/z2Z7x85pAukMyr4y1wAAAAAAAAAAAAEBR1IhAkiG9PSdcBAS08R6LIRS6iGFbQ5ZbjY2an2EMxXcmGbPIQM2XzQNCYGxaFTBlw1c4XU4hQxj7p7ntZZDaVLjrJg39VKuIgICSIb09J1wEBLTxHoshFLqIYVtDlluNjZqfYQzFdyYZs8Myr4y1wEAAAABAAAAIgIDNl80DQmBsWhUwZcNXOF1OIUMY+6e57WWQ2lS46yYN/UMeTjFAgEAAAABAAAAAAA="
  },
  "psbt_file": "$HOME/.firma/testnet/psbts/test/psbt.json",
  "qr_files": [
    "$HOME/.firma/testnet/psbts/test/qr/qr-0.png",
    "$HOME/.firma/testnet/psbts/test/qr/qr-1.png"
  ]
}

Sign from node A

firma-offline sign --psbt-name test --key-name a1 --wallet-name firma-wallet
{
  "balances": "",
  "fee": {
    "absolute": 193,
    "absolute_fmt": "0.00000193 BTC",
    "rate": 1.0157894736842106
  },
  "info": [
    "Added signatures"
  ],
  "inputs": [
    {
      "outpoint": "43d3a56e8afe96eeb1c3a260bae735d064e5946190d9fb90524047bd21dbf383:0",
      "signatures": [
        "2f15d226"
      ],
      "value": "0.00586300 BTC"
    }
  ],
  "outputs": [
    {
      "address": "tb1q8m2456wjxu8mlkf708d2yvtmtlg59awvd2l3jjzkmt37gtzmx6psva9fnl",
      "value": "0.00022400 BTC"
    },
    {
      "address": "tb1qye8gt2pjwpdn8mj7eh3jgnl0hnfwq23lq4cat6s55hsnka3v2kss4jxsmm",
      "value": "0.00563707 BTC"
    }
  ],
  "psbt_file": "",
  "size": {
    "estimated": 190,
    "psbt": 1083,
    "unsigned": 137
  }
}

The psbt.json at ~/.firma/testnet/psbts/test/psbt.json now has 1 signature (notice the Added signatures in the output).

Sign from node B

firma-offline sign --psbt-name test --key-name a2 --wallet-name firma-wallet
{
  "balances": "TODO",
  "fee": {
    "absolute": 193,
    "absolute_fmt": "0.00000193 BTC",
    "rate": 1.0157894736842106
  },
  "info": [
    "Added signatures"
  ],
  "inputs": [
    {
      "outpoint": "43d3a56e8afe96eeb1c3a260bae735d064e5946190d9fb90524047bd21dbf383:0",
      "signatures": [
        "2f15d226",
        "7938c502"
      ],
      "value": "0.00586300 BTC"
    }
  ],
  "outputs": [
    {
      "address": "tb1q8m2456wjxu8mlkf708d2yvtmtlg59awvd2l3jjzkmt37gtzmx6psva9fnl",
      "value": "0.00022400 BTC"
    },
    {
      "address": "tb1qye8gt2pjwpdn8mj7eh3jgnl0hnfwq23lq4cat6s55hsnka3v2kss4jxsmm",
      "value": "0.00563707 BTC"
    }
  ],
  "psbt_file": "",
  "size": {
    "estimated": 190,
    "psbt": 1191,
    "unsigned": 137
  }
}

Combine, finalize and send TX

firma-online send-tx --wallet-name firma-wallet --psbt-name test --broadcast
{
  "broadcasted": true,
  "hex": "0200000000010183f3db21bd47405290fbd9906194e564d035e7ba60a2c3b1ee96fe8a6ea5d3430000000000feffffff0280570000000000002200203ed55a69d2370fbfd93e79daa2317b5fd142f5cc6abf194856dae3e42c5b3683fb99080000000000220020264e85a832705b33ee5ecde3244fefbcd2e02a3f0571d5ea14a5e13b762c55a10400473044022041785595cc34a022686213b8d7b34bac2f2bfc77e37a8fa2f2f3019b0646bbfb022047e12715e32b081be49865ffdbca73829b0231e44f77c9afcd6aa25582186e300148304502210081f298aadf2e5f68e322c030b1e97e23f45a1ac9ddf5fa4b964c1a57847f37b70220129c3d54e9f5c946511bfd30fc755846014654d4d693455fa08279a994617e410147522102e43ee99d46f46cd17d25987701576ae07129ab268ca9879e53a345546537dc862102ccbef362214e9e7ece2bcd33731cdabd0c2937d9bec9db2684fb68021297f8b752ae00000000",
  "txid": "4e08b321a79465cdbba8ad811ddaa68ffe79604406413b25b55c76b9850902e5"
}

View tx 4e08b321a79465cdbba8ad811ddaa68ffe79604406413b25b55c76b9850902e5

Dependencies

~19–29MB
~399K SLoC