lnd/docs/psbt.md
2021-10-04 11:17:09 +02:00

38 KiB

PSBT

This document describes various use cases around the topic of Partially Signed Bitcoin Transactions (PSBTs). lnd's wallet now features a full set of PSBT functionality, including creating, signing and funding channels with PSBTs.

See BIP174 for a full description of the PSBT format and the different roles that a participant in a PSBT can have.

To avoid possible malleability, all inputs to a funding transaction must be segwit spends, meaning that P2PKH and normal P2SH cannot be used. An error will be returned if any inputs are not segwit spends.

Creating/funding a PSBT

The first step for every transaction that is constructed using a PSBT flow is to select inputs (UTXOs) to fund the desired output and to add a change output that sends the remaining funds back to the own wallet.

This wallet psbt fund command is very similar to bitcoind's walletcreatefundedpsbt command. One main difference is that you can specify a template PSBT in the lncli variant that contains the output(s) and optional inputs. Another difference is that for the --outputs flag, lncli expects the amounts to be in satoshis instead of fractions of a bitcoin.

Simple example: fund PSBT that sends to address

Let's start with a very simple example and assume we want to send half a coin to the address bcrt1qjrdns4f5zwkv29ln86plqzs092yd5fg6nsz8re:

⛰  lncli wallet psbt fund --outputs='{"bcrt1qjrdns4f5zwkv29ln86plqzs092yd5fg6nsz8re":50000000}'
{
        "psbt": "cHNidP8BAHECAAAAAeJQY2VLRtutKgQYFUajEKpjFfl0Uyrm6x23OumDpe/4AQAAAAD/////AkxREgEAAAAAFgAUv6pTgbKHN60CZ+RQn5yOuH6c2WiA8PoCAAAAABYAFJDbOFU0E6zFF/M+g/AKDyqI2iUaAAAAAAABAOsCAAAAAAEBbxqXgEf9DlzcqqNM610s5pL1X258ra6+KJ22etb7HAcBAAAAAAAAAAACACT0AAAAAAAiACC7U1W0iJGhQ6o7CexDh5k36V6v3256xpA9/xmB2BybTFZdDQQAAAAAFgAUKp2ThzhswyM2QHlyvmMB6tQB7V0CSDBFAiEA4Md8RIZYqFdUPsgDyomlzMJL9bJ6Ho23JGTihXtEelgCIAeNXRLyt88SOuuWFVn3IodCE4U5D6DojIHesRmikF28ASEDHYFzMEAxfmfq98eSSnZtUwb1w7mAtHG65y8qiRFNnIkAAAAAAQEfVl0NBAAAAAAWABQqnZOHOGzDIzZAeXK+YwHq1AHtXQEDBAEAAAAAAAA=",
        "change_output_index": 0,
        "locks": [
                {
                        "id": "ede19a92ed321a4705f8a1cccc1d4f6182545d4bb4fae08bd5937831b7e38f98",
                        "outpoint": "f8efa583e93ab71debe62a5374f91563aa10a3461518042aaddb464b656350e2:1",
                        "expiration": 1601553408
                }
        ]
}

The first thing we notice in the response is that an outpoint was locked. That means, the UTXO that was chosen to fund the PSBT is currently locked and cannot be used by the internal wallet or any other RPC call. This lock will be released automatically either after 10 minutes (timeout) or once a transaction that spends the UTXO is published.

If we inspect the PSBT that was created, we see that the locked input was indeed selected, the UTXO information was attached and a change output (at index 0) was created as well:

⛰  bitcoin-cli decodepsbt cHNidP8BAHECAAAAAeJQY2VLRtutKgQYFUajEKpjFfl0Uyrm6x23OumDpe/4AQAAAAD/////AkxREgEAAAAAFgAUv6pTgbKHN60CZ+RQn5yOuH6c2WiA8PoCAAAAABYAFJDbOFU0E6zFF/M+g/AKDyqI2iUaAAAAAAABAOsCAAAAAAEBbxqXgEf9DlzcqqNM610s5pL1X258ra6+KJ22etb7HAcBAAAAAAAAAAACACT0AAAAAAAiACC7U1W0iJGhQ6o7CexDh5k36V6v3256xpA9/xmB2BybTFZdDQQAAAAAFgAUKp2ThzhswyM2QHlyvmMB6tQB7V0CSDBFAiEA4Md8RIZYqFdUPsgDyomlzMJL9bJ6Ho23JGTihXtEelgCIAeNXRLyt88SOuuWFVn3IodCE4U5D6DojIHesRmikF28ASEDHYFzMEAxfmfq98eSSnZtUwb1w7mAtHG65y8qiRFNnIkAAAAAAQEfVl0NBAAAAAAWABQqnZOHOGzDIzZAeXK+YwHq1AHtXQEDBAEAAAAAAAA=
{
  "tx": {
    "txid": "33a316d62ddf74656967754d26ea83a3cb89e03ae44578d965156d4b71b1fce7",
    "hash": "33a316d62ddf74656967754d26ea83a3cb89e03ae44578d965156d4b71b1fce7",
    "version": 2,
    "size": 113,
    "vsize": 113,
    "weight": 452,
    "locktime": 0,
    "vin": [
      {
        "txid": "f8efa583e93ab71debe62a5374f91563aa10a3461518042aaddb464b656350e2",
        "vout": 1,
        "scriptSig": {
          "asm": "",
          "hex": ""
        },
        "sequence": 4294967295
      }
    ],
    "vout": [
      {
        "value": 0.17977676,
        "n": 0,
        "scriptPubKey": {
          "asm": "0 bfaa5381b28737ad0267e4509f9c8eb87e9cd968",
          "hex": "0014bfaa5381b28737ad0267e4509f9c8eb87e9cd968",
          "reqSigs": 1,
          "type": "witness_v0_keyhash",
          "addresses": [
            "bcrt1qh7498qdjsum66qn8u3gfl8ywhplfektg6mutfs"
          ]
        }
      },
      {
        "value": 0.50000000,
        "n": 1,
        "scriptPubKey": {
          "asm": "0 90db38553413acc517f33e83f00a0f2a88da251a",
          "hex": "001490db38553413acc517f33e83f00a0f2a88da251a",
          "reqSigs": 1,
          "type": "witness_v0_keyhash",
          "addresses": [
            "bcrt1qjrdns4f5zwkv29ln86plqzs092yd5fg6nsz8re"
          ]
        }
      }
    ]
  },
  "unknown": {
  },
  "inputs": [
    {
      "witness_utxo": {
...
      },
      "non_witness_utxo": {
        ...
      },
      "sighash": "ALL"
    }
  ],
  "outputs": [
...
  ],
  "fee": 0.00007050
}

Advanced example: fund PSBT with manual coin selection

Let's now look at how we can implement manual coin selection by using the fund command. We again want to send half a coin to bcrt1qjrdns4f5zwkv29ln86plqzs092yd5fg6nsz8re but we want to select our inputs manually.

The first step is to look at all available UTXOs and choose. To do so, we use the listunspent command:

⛰  lncli listunspent
{
        "utxos": [
                {
                        "address_type": 0,
                        "address": "bcrt1qmsq36rtc6ap3m0m6jryu0ez923et6kxrv46t4w",
                        "amount_sat": 100000000,
                        "pk_script": "0014dc011d0d78d7431dbf7a90c9c7e4455472bd58c3",
                        "outpoint": "3597b451ff56bc901eb806e8c644a004e934b4c208679756b4cddc455c768c48:1",
                        "confirmations": 6
                },
                {
                        "address_type": 0,
                        "address": "bcrt1q92we8pecdnpjxdjq09etuccpat2qrm2acu4256",
                        "amount_sat": 67984726,
                        "pk_script": "00142a9d9387386cc32336407972be6301ead401ed5d",
                        "outpoint": "f8efa583e93ab71debe62a5374f91563aa10a3461518042aaddb464b656350e2:1",
                        "confirmations": 24
                },
...
        ]
}

Next, we choose these two inputs and create the PSBT:

⛰  lncli wallet psbt fund --outputs='{"bcrt1qjrdns4f5zwkv29ln86plqzs092yd5fg6nsz8re":50000000}' \
    --inputs='["3597b451ff56bc901eb806e8c644a004e934b4c208679756b4cddc455c768c48:1","f8efa583e93ab71debe62a5374f91563aa10a3461518042aaddb464b656350e2:1"]'
{
        "psbt": "cHNidP8BAJoCAAAAAkiMdlxF3M20VpdnCMK0NOkEoETG6Aa4HpC8Vv9RtJc1AQAAAAAAAAAA4lBjZUtG260qBBgVRqMQqmMV+XRTKubrHbc66YOl7/gBAAAAAAAAAAACgPD6AgAAAAAWABSQ2zhVNBOsxRfzPoPwCg8qiNolGtIkCAcAAAAAFgAUuvRP5r7qAvj0egDxyX9/FH+vukgAAAAAAAEA3gIAAAAAAQEr9IZcho/gV/6fH8C8P+yhNRZP+l3YuxsyatdYcS0S6AEAAAAA/v///wLI/8+yAAAAABYAFDXoRFwgXNO5VVtVq2WpaENh6blAAOH1BQAAAAAWABTcAR0NeNdDHb96kMnH5EVUcr1YwwJHMEQCIDqugtYLp4ebJAZvOdieshLi1lLuPl2tHQG4jM4ybwEGAiBeMpCkbHBmzYvljxb1JBQyVAMuoco0xIfi+5OQdHuXaAEhAnH96NhTW09X0npE983YBsHUoMPI4U4xBtHenpZVTEqpVwAAAAEBHwDh9QUAAAAAFgAU3AEdDXjXQx2/epDJx+RFVHK9WMMBAwQBAAAAAAEA6wIAAAAAAQFvGpeAR/0OXNyqo0zrXSzmkvVfbnytrr4onbZ61vscBwEAAAAAAAAAAAIAJPQAAAAAACIAILtTVbSIkaFDqjsJ7EOHmTfpXq/fbnrGkD3/GYHYHJtMVl0NBAAAAAAWABQqnZOHOGzDIzZAeXK+YwHq1AHtXQJIMEUCIQDgx3xEhlioV1Q+yAPKiaXMwkv1snoejbckZOKFe0R6WAIgB41dEvK3zxI665YVWfcih0IThTkPoOiMgd6xGaKQXbwBIQMdgXMwQDF+Z+r3x5JKdm1TBvXDuYC0cbrnLyqJEU2ciQAAAAABAR9WXQ0EAAAAABYAFCqdk4c4bMMjNkB5cr5jAerUAe1dAQMEAQAAAAAAAA==",
        "change_output_index": 1,
        "locks": [
                {
                        "id": "ede19a92ed321a4705f8a1cccc1d4f6182545d4bb4fae08bd5937831b7e38f98",
                        "outpoint": "3597b451ff56bc901eb806e8c644a004e934b4c208679756b4cddc455c768c48:1",
                        "expiration": 1601560626
                },
                {
                        "id": "ede19a92ed321a4705f8a1cccc1d4f6182545d4bb4fae08bd5937831b7e38f98",
                        "outpoint": "f8efa583e93ab71debe62a5374f91563aa10a3461518042aaddb464b656350e2:1",
                        "expiration": 1601560626
                }
        ]
}

Inspecting this PSBT, we notice that the two inputs were chosen and a large change change output was added at index 1:

⛰  bitcoin-cli  decodepsbt cHNidP8BAJoCAAAAAkiMdlxF3M20VpdnCMK0NOkEoETG6Aa4HpC8Vv9RtJc1AQAAAAAAAAAA4lBjZUtG260qBBgVRqMQqmMV+XRTKubrHbc66YOl7/gBAAAAAAAAAAACgPD6AgAAAAAWABSQ2zhVNBOsxRfzPoPwCg8qiNolGtIkCAcAAAAAFgAUuvRP5r7qAvj0egDxyX9/FH+vukgAAAAAAAEA3gIAAAAAAQEr9IZcho/gV/6fH8C8P+yhNRZP+l3YuxsyatdYcS0S6AEAAAAA/v///wLI/8+yAAAAABYAFDXoRFwgXNO5VVtVq2WpaENh6blAAOH1BQAAAAAWABTcAR0NeNdDHb96kMnH5EVUcr1YwwJHMEQCIDqugtYLp4ebJAZvOdieshLi1lLuPl2tHQG4jM4ybwEGAiBeMpCkbHBmzYvljxb1JBQyVAMuoco0xIfi+5OQdHuXaAEhAnH96NhTW09X0npE983YBsHUoMPI4U4xBtHenpZVTEqpVwAAAAEBHwDh9QUAAAAAFgAU3AEdDXjXQx2/epDJx+RFVHK9WMMBAwQBAAAAAAEA6wIAAAAAAQFvGpeAR/0OXNyqo0zrXSzmkvVfbnytrr4onbZ61vscBwEAAAAAAAAAAAIAJPQAAAAAACIAILtTVbSIkaFDqjsJ7EOHmTfpXq/fbnrGkD3/GYHYHJtMVl0NBAAAAAAWABQqnZOHOGzDIzZAeXK+YwHq1AHtXQJIMEUCIQDgx3xEhlioV1Q+yAPKiaXMwkv1snoejbckZOKFe0R6WAIgB41dEvK3zxI665YVWfcih0IThTkPoOiMgd6xGaKQXbwBIQMdgXMwQDF+Z+r3x5JKdm1TBvXDuYC0cbrnLyqJEU2ciQAAAAABAR9WXQ0EAAAAABYAFCqdk4c4bMMjNkB5cr5jAerUAe1dAQMEAQAAAAAAAA==
{
"tx": {
  "txid": "e62356b99c3097eaa1241ff8e39b996917e66b13e4c0ccba3698982d746c3b76",
  "hash": "e62356b99c3097eaa1241ff8e39b996917e66b13e4c0ccba3698982d746c3b76",
  "version": 2,
  "size": 154,
  "vsize": 154,
  "weight": 616,
  "locktime": 0,
  "vin": [
    {
      "txid": "3597b451ff56bc901eb806e8c644a004e934b4c208679756b4cddc455c768c48",
      "vout": 1,
      "scriptSig": {
        "asm": "",
        "hex": ""
      },
      "sequence": 0
    },
    {
      "txid": "f8efa583e93ab71debe62a5374f91563aa10a3461518042aaddb464b656350e2",
      "vout": 1,
      "scriptSig": {
        "asm": "",
        "hex": ""
      },
      "sequence": 0
    }
  ],
  "vout": [
    {
      "value": 0.50000000,
      "n": 0,
      "scriptPubKey": {
        "asm": "0 90db38553413acc517f33e83f00a0f2a88da251a",
        "hex": "001490db38553413acc517f33e83f00a0f2a88da251a",
        "reqSigs": 1,
        "type": "witness_v0_keyhash",
        "addresses": [
          "bcrt1qjrdns4f5zwkv29ln86plqzs092yd5fg6nsz8re"
        ]
      }
    },
    {
      "value": 1.17974226,
      "n": 1,
      "scriptPubKey": {
        "asm": "0 baf44fe6beea02f8f47a00f1c97f7f147fafba48",
        "hex": "0014baf44fe6beea02f8f47a00f1c97f7f147fafba48",
        "reqSigs": 1,
        "type": "witness_v0_keyhash",
        "addresses": [
          "bcrt1qht6yle47agp03ar6qrcujlmlz3l6lwjgjv36zl"
        ]
      }
    }
  ]
},
"unknown": {
},
"inputs": [
...
],
"outputs": [
...
],
"fee": 0.00010500
}

Signing and finalizing a PSBT

Assuming we now want to sign the transaction that we created in the previous example, we simply pass it to the finalize sub command of the wallet:

⛰  lncli wallet psbt finalize cHNidP8BAJoCAAAAAkiMdlxF3M20VpdnCMK0NOkEoETG6Aa4HpC8Vv9RtJc1AQAAAAAAAAAA4lBjZUtG260qBBgVRqMQqmMV+XRTKubrHbc66YOl7/gBAAAAAAAAAAACgPD6AgAAAAAWABSQ2zhVNBOsxRfzPoPwCg8qiNolGtIkCAcAAAAAFgAUuvRP5r7qAvj0egDxyX9/FH+vukgAAAAAAAEA3gIAAAAAAQEr9IZcho/gV/6fH8C8P+yhNRZP+l3YuxsyatdYcS0S6AEAAAAA/v///wLI/8+yAAAAABYAFDXoRFwgXNO5VVtVq2WpaENh6blAAOH1BQAAAAAWABTcAR0NeNdDHb96kMnH5EVUcr1YwwJHMEQCIDqugtYLp4ebJAZvOdieshLi1lLuPl2tHQG4jM4ybwEGAiBeMpCkbHBmzYvljxb1JBQyVAMuoco0xIfi+5OQdHuXaAEhAnH96NhTW09X0npE983YBsHUoMPI4U4xBtHenpZVTEqpVwAAAAEBHwDh9QUAAAAAFgAU3AEdDXjXQx2/epDJx+RFVHK9WMMBAwQBAAAAAAEA6wIAAAAAAQFvGpeAR/0OXNyqo0zrXSzmkvVfbnytrr4onbZ61vscBwEAAAAAAAAAAAIAJPQAAAAAACIAILtTVbSIkaFDqjsJ7EOHmTfpXq/fbnrGkD3/GYHYHJtMVl0NBAAAAAAWABQqnZOHOGzDIzZAeXK+YwHq1AHtXQJIMEUCIQDgx3xEhlioV1Q+yAPKiaXMwkv1snoejbckZOKFe0R6WAIgB41dEvK3zxI665YVWfcih0IThTkPoOiMgd6xGaKQXbwBIQMdgXMwQDF+Z+r3x5JKdm1TBvXDuYC0cbrnLyqJEU2ciQAAAAABAR9WXQ0EAAAAABYAFCqdk4c4bMMjNkB5cr5jAerUAe1dAQMEAQAAAAAAAA==
{
      "psbt": "cHNidP8BAJoCAAAAAkiMdlxF3M20VpdnCMK0NOkEoETG6Aa4HpC8Vv9RtJc1AQAAAAAAAAAA4lBjZUtG260qBBgVRqMQqmMV+XRTKubrHbc66YOl7/gBAAAAAAAAAAACgPD6AgAAAAAWABSQ2zhVNBOsxRfzPoPwCg8qiNolGtIkCAcAAAAAFgAUuvRP5r7qAvj0egDxyX9/FH+vukgAAAAAAAEA3gIAAAAAAQEr9IZcho/gV/6fH8C8P+yhNRZP+l3YuxsyatdYcS0S6AEAAAAA/v///wLI/8+yAAAAABYAFDXoRFwgXNO5VVtVq2WpaENh6blAAOH1BQAAAAAWABTcAR0NeNdDHb96kMnH5EVUcr1YwwJHMEQCIDqugtYLp4ebJAZvOdieshLi1lLuPl2tHQG4jM4ybwEGAiBeMpCkbHBmzYvljxb1JBQyVAMuoco0xIfi+5OQdHuXaAEhAnH96NhTW09X0npE983YBsHUoMPI4U4xBtHenpZVTEqpVwAAAAEBHwDh9QUAAAAAFgAU3AEdDXjXQx2/epDJx+RFVHK9WMMBCGwCSDBFAiEAuiv52IX5wZlYJqqVGsQPfeQ/kneCNRD34v5yplNpuMYCIECHVUhjHPKSiWSsYEKD4JWGAyUwQHgDytA1whFOyLclASECg7PDfGE/uURta5/R42Vso6QKmVAgYMhjWlXENkE/x+QAAQDrAgAAAAABAW8al4BH/Q5c3KqjTOtdLOaS9V9ufK2uviidtnrW+xwHAQAAAAAAAAAAAgAk9AAAAAAAIgAgu1NVtIiRoUOqOwnsQ4eZN+ler99uesaQPf8Zgdgcm0xWXQ0EAAAAABYAFCqdk4c4bMMjNkB5cr5jAerUAe1dAkgwRQIhAODHfESGWKhXVD7IA8qJpczCS/Wyeh6NtyRk4oV7RHpYAiAHjV0S8rfPEjrrlhVZ9yKHQhOFOQ+g6IyB3rEZopBdvAEhAx2BczBAMX5n6vfHkkp2bVMG9cO5gLRxuucvKokRTZyJAAAAAAEBH1ZdDQQAAAAAFgAUKp2ThzhswyM2QHlyvmMB6tQB7V0BCGwCSDBFAiEAqK7FSrqWe2non0kl96yu2+gSXGPYPC7ZjzVZEMMWtpYCIGTzCDHZhJYGPrsnBWU8o0Eyd4nBa+6d037xGFcGUYJLASECORgkj75Xu8+DTh8bqYBIvNx1hSxV7VSJOwY6jam6LY8AAAA=",
      "final_tx": "02000000000102488c765c45dccdb456976708c2b434e904a044c6e806b81e90bc56ff51b49735010000000000000000e25063654b46dbad2a04181546a310aa6315f974532ae6eb1db73ae983a5eff80100000000000000000280f0fa020000000016001490db38553413acc517f33e83f00a0f2a88da251ad224080700000000160014baf44fe6beea02f8f47a00f1c97f7f147fafba4802483045022100ba2bf9d885f9c1995826aa951ac40f7de43f9277823510f7e2fe72a65369b8c6022040875548631cf2928964ac604283e09586032530407803cad035c2114ec8b72501210283b3c37c613fb9446d6b9fd1e3656ca3a40a99502060c8635a55c436413fc7e402483045022100a8aec54aba967b69e89f4925f7acaedbe8125c63d83c2ed98f355910c316b696022064f30831d98496063ebb2705653ca341327789c16bee9dd37ef118570651824b0121023918248fbe57bbcf834e1f1ba98048bcdc75852c55ed54893b063a8da9ba2d8f00000000"
}

That final transaction can now, in theory, be broadcast. But it is very important that you do not publish it manually if any of the involved outputs are used to fund a channel. See the safety warning below to learn the reason for this.

Opening a channel by using a PSBT

This is a step-by-step guide on how to open a channel with lnd by using a PSBT as the funding transaction.
We will use bitcoind to create and sign the transaction just to keep the example simple. Of course any other PSBT compatible wallet could be used and the process would likely be spread out over multiple signing steps. The goal of this example is not to cover each and every possible edge case but to help users of lnd understand what inputs the lncli utility expects.

The goal is to open a channel of 1'234'567 satoshis to the node 03db1e56e5f76bc4018cf6f03d1bb98a7ae96e3f18535e929034f85e7f1ca2b8ac by using a PSBT. That means, lnd can have a wallet balance of 0 and is still able to open a channel. We'll jump into an example right away.

The new funding flow has a small caveat: Time matters.

When opening a channel using the PSBT flow, we start the negotiation with the remote peer immediately so we can obtain their multisig key they are going to use for the channel. Then we pause the whole process until we get a fully signed transaction back from the user. Unfortunately there is no reliable way to know after how much time the remote node starts to clean up and "forgets" about the pending channel. If the remote node is an lnd node, we know it's after 10 minutes. So as long as the whole process takes less than 10 minutes, everything should work fine.

Safety warning

DO NOT PUBLISH the finished transaction by yourself or with another tool. lnd MUST publish it in the proper funding flow order OR THE FUNDS CAN BE LOST!

This is very important to remember when using wallets like Wasabi for instance, where the "publish" button is very easy to hit by accident.

1. Use the new --psbt flag in lncli openchannel

The new --psbt flag in the openchannel command starts an interactive dialog between lncli and the user. Below the command you see an example output from a regtest setup. Of course all values will be different.

⛰  lncli openchannel --node_key 03db1e56e5f76bc4018cf6f03d1bb98a7ae96e3f18535e929034f85e7f1ca2b8ac --local_amt 1234567 --psbt
Starting PSBT funding flow with pending channel ID fc7853889a04d33b8115bd79ebc99c5eea80d894a0bead40fae5a06bcbdccd3d.
PSBT funding initiated with peer 03db1e56e5f76bc4018cf6f03d1bb98a7ae96e3f18535e929034f85e7f1ca2b8ac.
Please create a PSBT that sends 0.01234567 BTC (1234567 satoshi) to the funding address bcrt1qh33ghvgjj3ef625nl9jxz6nnrz2z9e65vsdey7w5msrklgr6rc0sv0s08q.

Example with bitcoind:
        bitcoin-cli walletcreatefundedpsbt [] '[{"bcrt1qh33ghvgjj3ef625nl9jxz6nnrz2z9e65vsdey7w5msrklgr6rc0sv0s08q":0.01234567}]'

Or if you are using a wallet that can fund a PSBT directly (currently not
possible with bitcoind), you can use this PSBT that contains the same address
and amount: cHNidP8BADUCAAAAAAGH1hIAAAAAACIAILxii7ESlHKdKpP5ZGFqcxiUIudUZBuSedTcB2+geh4fAAAAAAAA

Paste the funded PSBT here to continue the funding flow.
Base64 encoded PSBT:

The command line now waits until a PSBT is entered. We'll create one in the next step. Make sure to use a new shell window/tab for the next commands and leave the prompt from the openchannel running as is.

2a. Use bitcoind to create a funding transaction

The output of the last command already gave us an example command to use with bitcoind. We'll go ahead and execute it now. The meaning of this command is something like "bitcoind, give me a PSBT that sends the given amount to the given address, choose any input you see fit":

⛰  bitcoin-cli walletcreatefundedpsbt [] '[{"bcrt1qh33ghvgjj3ef625nl9jxz6nnrz2z9e65vsdey7w5msrklgr6rc0sv0s08q":0.01234567}]'
{
  "psbt": "cHNidP8BAH0CAAAAAbxLLf9+AYfqfF69QAQuETnL6cas7GDiWBZF+3xxc/Y/AAAAAAD+////AofWEgAAAAAAIgAgvGKLsRKUcp0qk/lkYWpzGJQi51RkG5J51NwHb6B6Hh+1If0jAQAAABYAFL+6THEGhybJnOkFGSRFbtCcPOG8AAAAAAABAR8wBBAkAQAAABYAFHemJ11XF7CU7WXBIJLD/qZF+6jrAAAA",
  "fee": 0.00003060,
  "changepos": 1
}

We see that bitcoind has given us a transaction that would pay 3060 satoshi in fees. Fee estimation/calculation can be changed with parameters of the walletcreatefundedpsbt command. To see all options, use bitcoin-cli help walletcreatefundedpsbt.

If we want to know what exactly is in this PSBT, we can look at it with the decodepsbt command:

⛰  bitcoin-cli decodepsbt cHNidP8BAH0CAAAAAbxLLf9+AYfqfF69QAQuETnL6cas7GDiWBZF+3xxc/Y/AAAAAAD+////AofWEgAAAAAAIgAgvGKLsRKUcp0qk/lkYWpzGJQi51RkG5J51NwHb6B6Hh+1If0jAQAAABYAFL+6THEGhybJnOkFGSRFbtCcPOG8AAAAAAABAR8wBBAkAQAAABYAFHemJ11XF7CU7WXBIJLD/qZF+6jrAAAA
{
  "tx": {
    "txid": "374504e4246a93a45b4a2c2bc31d8adc8525aa101c7b9065db6dc01c4bdfce0a",
    "hash": "374504e4246a93a45b4a2c2bc31d8adc8525aa101c7b9065db6dc01c4bdfce0a",
    "version": 2,
    "size": 125,
    "vsize": 125,
    "weight": 500,
    "locktime": 0,
    "vin": [
      {
        "txid": "3ff673717cfb451658e260ecacc6e9cb39112e0440bd5e7cea87017eff2d4bbc",
        "vout": 0,
        "scriptSig": {
          "asm": "",
          "hex": ""
        },
        "sequence": 4294967294
      }
    ],
    "vout": [
      {
        "value": 0.01234567,
        "n": 0,
        "scriptPubKey": {
          "asm": "0 bc628bb11294729d2a93f964616a73189422e754641b9279d4dc076fa07a1e1f",
          "hex": "0020bc628bb11294729d2a93f964616a73189422e754641b9279d4dc076fa07a1e1f",
          "reqSigs": 1,
          "type": "witness_v0_scripthash",
          "addresses": [
            "bcrt1qh33ghvgjj3ef625nl9jxz6nnrz2z9e65vsdey7w5msrklgr6rc0sv0s08q"
          ]
        }
      },
      {
        "value": 48.98759093,
        "n": 1,
        "scriptPubKey": {
          "asm": "0 bfba4c71068726c99ce9051924456ed09c3ce1bc",
          "hex": "0014bfba4c71068726c99ce9051924456ed09c3ce1bc",
          "reqSigs": 1,
          "type": "witness_v0_keyhash",
          "addresses": [
            "bcrt1qh7aycugxsunvn88fq5vjg3tw6zwrecduvvgre5"
          ]
        }
      }
    ]
  },
  "unknown": {
  },
  "inputs": [
    {
      "witness_utxo": {
        "amount": 48.99996720,
        "scriptPubKey": {
          "asm": "0 77a6275d5717b094ed65c12092c3fea645fba8eb",
          "hex": "001477a6275d5717b094ed65c12092c3fea645fba8eb",
          "type": "witness_v0_keyhash",
          "address": "bcrt1qw7nzwh2hz7cffmt9cysf9sl75ezlh28tzl4n4e"
        }
      }
    }
  ],
  "outputs": [
    {
    },
    {
    }
  ],
  "fee": 0.00003060
}

This tells us that we got a PSBT with a big input, the channel output and a change output for the rest. Everything is there but the signatures/witness data, which is exactly what we need.

2b. Use lnd to create a funding transaction

Starting with version v0.12.0, lnd can also create PSBTs. This assumes a scenario where one instance of lnd only has public keys (watch only mode) and a secondary, hardened and firewalled lnd instance has the corresponding private keys. On the watching only mode, the following command can be used to create the funding PSBT:

⛰  lncli wallet psbt fund --outputs='{"bcrt1qh33ghvgjj3ef625nl9jxz6nnrz2z9e65vsdey7w5msrklgr6rc0sv0s08q":1234567}'
{
        "psbt": "cHNidP8BAH0CAAAAAUiMdlxF3M20VpdnCMK0NOkEoETG6Aa4HpC8Vv9RtJc1AQAAAAD/////AofWEgAAAAAAIgAgvGKLsRKUcp0qk/lkYWpzGJQi51RkG5J51NwHb6B6Hh+X7OIFAAAAABYAFNigOB6EbCLRi+Evlv4r2yJx63NxAAAAAAABAN4CAAAAAAEBK/SGXIaP4Ff+nx/AvD/soTUWT/pd2LsbMmrXWHEtEugBAAAAAP7///8CyP/PsgAAAAAWABQ16ERcIFzTuVVbVatlqWhDYem5QADh9QUAAAAAFgAU3AEdDXjXQx2/epDJx+RFVHK9WMMCRzBEAiA6roLWC6eHmyQGbznYnrIS4tZS7j5drR0BuIzOMm8BBgIgXjKQpGxwZs2L5Y8W9SQUMlQDLqHKNMSH4vuTkHR7l2gBIQJx/ejYU1tPV9J6RPfN2AbB1KDDyOFOMQbR3p6WVUxKqVcAAAABAR8A4fUFAAAAABYAFNwBHQ1410Mdv3qQycfkRVRyvVjDAQMEAQAAAAAAAA==",
        "change_output_index": 1,
        "locks": [
                {
                        "id": "ede19a92ed321a4705f8a1cccc1d4f6182545d4bb4fae08bd5937831b7e38f98",
                        "outpoint": "3597b451ff56bc901eb806e8c644a004e934b4c208679756b4cddc455c768c48:1",
                        "expiration": 1601562037
                }
        ]
}

3. Verify and sign the PSBT

Now that we have a valid PSBT that has everything but the final signatures/witness data, we can paste it into the prompt in lncli that is still waiting for our input.

...
Base64 encoded PSBT: cHNidP8BAH0CAAAAAbxLLf9+AYfqfF69QAQuETnL6cas7GDiWBZF+3xxc/Y/AAAAAAD+////AofWEgAAAAAAIgAgvGKLsRKUcp0qk/lkYWpzGJQi51RkG5J51NwHb6B6Hh+1If0jAQAAABYAFL+6THEGhybJnOkFGSRFbtCcPOG8AAAAAAABAR8wBBAkAQAAABYAFHemJ11XF7CU7WXBIJLD/qZF+6jrAAAA

PSBT verified by lnd, please continue the funding flow by signing the PSBT by
all required parties/devices. Once the transaction is fully signed, paste it
again here.

Base64 encoded PSBT:

We can now go ahead and sign the transaction. We are going to use bitcoind for this again, but in practice this would now happen on a hardware wallet and perhaps bitcoind would only know the public keys and couldn't sign for the transaction itself. Again, this is only an example and can't reflect all real-world use cases.

⛰  bitcoin-cli walletprocesspsbt cHNidP8BAH0CAAAAAbxLLf9+AYfqfF69QAQuETnL6cas7GDiWBZF+3xxc/Y/AAAAAAD+////AofWEgAAAAAAIgAgvGKLsRKUcp0qk/lkYWpzGJQi51RkG5J51NwHb6B6Hh+1If0jAQAAABYAFL+6THEGhybJnOkFGSRFbtCcPOG8AAAAAAABAR8wBBAkAQAAABYAFHemJ11XF7CU7WXBIJLD/qZF+6jrAAAA
{
"psbt": "cHNidP8BAH0CAAAAAbxLLf9+AYfqfF69QAQuETnL6cas7GDiWBZF+3xxc/Y/AAAAAAD+////AofWEgAAAAAAIgAgvGKLsRKUcp0qk/lkYWpzGJQi51RkG5J51NwHb6B6Hh+1If0jAQAAABYAFL+6THEGhybJnOkFGSRFbtCcPOG8AAAAAAABAR8wBBAkAQAAABYAFHemJ11XF7CU7WXBIJLD/qZF+6jrAQhrAkcwRAIgHKQbenZYvgADRd9TKGVO36NnaIgW3S12OUg8XGtSrE8CICmeaYoJ/U7Ecm+/GneY8i2hu2QCaQnuomJgzn+JAnrDASEDUBmCLcsybA5qXSRBBdZ0Uk/FQiay9NgOpv4D26yeJpAAAAA=",
"complete": true
}

If you are using the two lnd node model as described in 2b, you can achieve the same result with the following command:

⛰  lncli wallet psbt finalize cHNidP8BAH0CAAAAAUiMdlxF3M20VpdnCMK0NOkEoETG6Aa4HpC8Vv9RtJc1AQAAAAD/////AofWEgAAAAAAIgAgvGKLsRKUcp0qk/lkYWpzGJQi51RkG5J51NwHb6B6Hh+X7OIFAAAAABYAFNigOB6EbCLRi+Evlv4r2yJx63NxAAAAAAABAN4CAAAAAAEBK/SGXIaP4Ff+nx/AvD/soTUWT/pd2LsbMmrXWHEtEugBAAAAAP7///8CyP/PsgAAAAAWABQ16ERcIFzTuVVbVatlqWhDYem5QADh9QUAAAAAFgAU3AEdDXjXQx2/epDJx+RFVHK9WMMCRzBEAiA6roLWC6eHmyQGbznYnrIS4tZS7j5drR0BuIzOMm8BBgIgXjKQpGxwZs2L5Y8W9SQUMlQDLqHKNMSH4vuTkHR7l2gBIQJx/ejYU1tPV9J6RPfN2AbB1KDDyOFOMQbR3p6WVUxKqVcAAAABAR8A4fUFAAAAABYAFNwBHQ1410Mdv3qQycfkRVRyvVjDAQMEAQAAAAAAAA==
{
        "psbt": "cHNidP8BAH0CAAAAAUiMdlxF3M20VpdnCMK0NOkEoETG6Aa4HpC8Vv9RtJc1AQAAAAD/////AofWEgAAAAAAIgAgvGKLsRKUcp0qk/lkYWpzGJQi51RkG5J51NwHb6B6Hh+X7OIFAAAAABYAFNigOB6EbCLRi+Evlv4r2yJx63NxAAAAAAABAN4CAAAAAAEBK/SGXIaP4Ff+nx/AvD/soTUWT/pd2LsbMmrXWHEtEugBAAAAAP7///8CyP/PsgAAAAAWABQ16ERcIFzTuVVbVatlqWhDYem5QADh9QUAAAAAFgAU3AEdDXjXQx2/epDJx+RFVHK9WMMCRzBEAiA6roLWC6eHmyQGbznYnrIS4tZS7j5drR0BuIzOMm8BBgIgXjKQpGxwZs2L5Y8W9SQUMlQDLqHKNMSH4vuTkHR7l2gBIQJx/ejYU1tPV9J6RPfN2AbB1KDDyOFOMQbR3p6WVUxKqVcAAAABAR8A4fUFAAAAABYAFNwBHQ1410Mdv3qQycfkRVRyvVjDAQhrAkcwRAIgU3Ow7cLkKrg8BJe0U0n9qFLPizqEzY0JtjVlpWOEk14CID/4AFNfgwNENN2LoOs0C6uHgt4sk8rNoZG+VMGzOC/HASECg7PDfGE/uURta5/R42Vso6QKmVAgYMhjWlXENkE/x+QAAAA=",
        "final_tx": "02000000000101488c765c45dccdb456976708c2b434e904a044c6e806b81e90bc56ff51b497350100000000ffffffff0287d6120000000000220020bc628bb11294729d2a93f964616a73189422e754641b9279d4dc076fa07a1e1f97ece20500000000160014d8a0381e846c22d18be12f96fe2bdb2271eb73710247304402205373b0edc2e42ab83c0497b45349fda852cf8b3a84cd8d09b63565a56384935e02203ff800535f83034434dd8ba0eb340bab8782de2c93cacda191be54c1b3382fc701210283b3c37c613fb9446d6b9fd1e3656ca3a40a99502060c8635a55c436413fc7e400000000"
}

Interpreting the output, we now have a complete, final, and signed transaction inside the PSBT.

!!! WARNING !!!

DO NOT PUBLISH the finished transaction by yourself or with another tool. lnd MUST publish it in the proper funding flow order OR THE FUNDS CAN BE LOST!

Let's give it to lncli to continue:

...
Base64 encoded PSBT: cHNidP8BAH0CAAAAAbxLLf9+AYfqfF69QAQuETnL6cas7GDiWBZF+3xxc/Y/AAAAAAD+////AofWEgAAAAAAIgAgvGKLsRKUcp0qk/lkYWpzGJQi51RkG5J51NwHb6B6Hh+1If0jAQAAABYAFL+6THEGhybJnOkFGSRFbtCcPOG8AAAAAAABAR8wBBAkAQAAABYAFHemJ11XF7CU7WXBIJLD/qZF+6jrAQhrAkcwRAIgHKQbenZYvgADRd9TKGVO36NnaIgW3S12OUg8XGtSrE8CICmeaYoJ/U7Ecm+/GneY8i2hu2QCaQnuomJgzn+JAnrDASEDUBmCLcsybA5qXSRBBdZ0Uk/FQiay9NgOpv4D26yeJpAAAAA=
{
        "funding_txid": "374504e4246a93a45b4a2c2bc31d8adc8525aa101c7b9065db6dc01c4bdfce0a"
}

Success! We now have the final transaction ID of the published funding transaction. Now we only have to wait for some confirmations, then we can start using the freshly created channel.

Batch opening channels

The PSBT channel funding flow makes it possible to open multiple channels in one transaction. This can be achieved by taking the initial PSBT returned by the openchannel and feed it into the --base_psbt parameter of the next openchannel command. This won't work with bitcoind though, as it cannot take a PSBT as partial input for the walletcreatefundedpsbt command.

However, the bitcoin-cli examples from the command line can be combined into a single command. For example:

Channel 1:

⛰  bitcoin-cli walletcreatefundedpsbt [] '[{"tb1qywvazres587w9wyy8uw03q8j9ek6gc9crwx4jvhqcmew4xzsvqcq3jjdja":0.01000000}]'

Channel 2:

⛰  bitcoin-cli walletcreatefundedpsbt [] '[{"tb1q53626fcwwtcdc942zaf4laqnr3vg5gv4g0hakd2h7fw2pmz6428sk3ezcx":0.01000000}]'

Combined command to get batch PSBT:

⛰  bitcoin-cli walletcreatefundedpsbt [] '[{"tb1q53626fcwwtcdc942zaf4laqnr3vg5gv4g0hakd2h7fw2pmz6428sk3ezcx":0.01000000},{"tb1qywvazres587w9wyy8uw03q8j9ek6gc9crwx4jvhqcmew4xzsvqcq3jjdja":0.01000000}]'

Safety warning about batch transactions

As mentioned before, the PSBT channel funding flow works by pausing the funding negotiation with the remote peer directly after the multisig keys have been exchanged. That means, the channel isn't fully opened yet at the time the PSBT is signed. This is fine for a single channel because the signed transaction is only published after the counter-signed commitment transactions were exchanged and the funds can be spent again by both parties.

When doing batch transactions, publishing the whole transaction with multiple channel funding outputs too early could lead to loss of funds!

For example, let's say we want to open two channels. We call openchannel --psbt two times, combine the funding addresses as shown above, verify the PSBT, sign it and finally paste it into the terminal of the first command. lnd then goes ahead and finishes the negotiations with peer 1. If successful, lnd publishes the transaction. In the meantime we paste the same PSBT into the second terminal window. But by now, the peer 2 for channel 2 has timed out our funding flow and aborts the negotiation. Normally this would be fine, we would just not publish the funding transaction. But in the batch case, channel 1 has already published the transaction that contains both channel outputs. But because we never got a signature from peer 2 to spend the funds now locked in a 2-of-2 multisig, the fund are lost (unless peer 2 cooperates in a complicated, manual recovery process).

Privacy considerations when batch opening channels

Opening private (non-announced) channels within a transaction that also contains public channels that are announced to the network it is plausible for an outside observer to assume that the non-announced P2WSH output might also be a channel originating from the same node as the public channel. It is therefore recommended to not mix public/private channels within the same batch transaction.

Batching multiple channels with the same state of the private flag can be beneficial for privacy though. Such a transaction can't easily be distinguished from a batch created by Pool or Loop for example. Also, because of the PSBT funding flow, it is also not guaranteed that all channels within such a batch transaction are actually being created for the same node. It is possible to create coin join transactions that create channels for multiple different nodes.

Use --no_publish for batch transactions

To mitigate the problem described in the section above, when open multiple channels in one batch transaction, it is imperative to use the --no_publish flag for each channel but the very last. This prevents the full batch transaction to be published before each and every single channel has fully completed its funding negotiation.

Use the BatchOpenChannel RPC for safe batch channel funding

If lnd's internal wallet should fund the batch channel open transaction then the safest option is the BatchOpenChannel RPC (and its lncli batchopenchannel counterpart). The BatchOpenChannel RPC accepts a list of node pubkeys and amounts and will try to atomically open channels in a single transaction to all of the nodes. If any of the individual channel negotiations fails (for example because of a minimum channel size not being met) then the whole batch is aborted and lingering reservations/intents/pending channels are cleaned up.

Example using the CLI:

⛰  lncli batchopenchannel --sat_per_vbyte=5 '[{
    "node_pubkey": "02c95fd94d2a40e483e8a14be1625ad8a82263b37b6a32162170d8d4c13080bedb",
    "local_funding_amount": 500000,
    "private": true,
    "close_address": "2NCJnjD4CZ5JvmkEo1D3QfDM57GX62LUbep"
  }, {
    "node_pubkey": "032d57116b92b5f64f022271ebd5e9e23826c0f34ff5ae3e742ad329e0dc5ddff8",
    "local_funding_amount": 600000,
    "remote_csv_delay": 288
  }, {
    "node_pubkey": "03475f7b07f79672b9a1fd2a3a2350bc444980fe06eb3ae38b132c6f43f958947b",
    "local_funding_amount": 700000
  }, {
    "node_pubkey": "027f013b5cf6b7035744fd8d7d756e05675bf6e829bb75a80be5b9e8e641d20562",
    "local_funding_amount": 800000
  }]'

NOTE: You must be connected to each of the nodes you want to open channels to before you run the command.

Example Node.JS script

To demonstrate how the PSBT funding API can be used with JavaScript, we add a simple example script that imitates the behavior of lncli but does not publish the final transaction itself. This allows the app creator to publish the transaction whenever everything is ready.

multi-channel-funding.js

const fs = require('fs');
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const Buffer = require('safe-buffer').Buffer;
const randomBytes = require('random-bytes').sync;
const prompt = require('prompt');

const LND_DIR = '/home/myuser/.lnd';
const LND_HOST = 'localhost:10009';
const NETWORK = 'regtest';
const LNRPC_PROTO_DIR = '/home/myuser/projects/go/lnd/lnrpc';

const grpcOptions = {
    keepCase: true,
    longs: String,
    enums: String,
    defaults: true,
    oneofs: true,
    includeDirs: [LNRPC_PROTO_DIR],
};

const packageDefinition = protoLoader.loadSync(`${LNRPC_PROTO_DIR}/rpc.proto`, grpcOptions);
const lnrpc = grpc.loadPackageDefinition(packageDefinition).lnrpc;

process.env.GRPC_SSL_CIPHER_SUITES = 'HIGH+ECDSA';

const adminMac = fs.readFileSync(`${LND_DIR}/data/chain/bitcoin/${NETWORK}/admin.macaroon`);
const metadata = new grpc.Metadata();
metadata.add('macaroon', adminMac.toString('hex'));
const macaroonCreds = grpc.credentials.createFromMetadataGenerator((_args, callback) => {
    callback(null, metadata);
});

const lndCert = fs.readFileSync(`${LND_DIR}/tls.cert`);
const sslCreds = grpc.credentials.createSsl(lndCert);
const credentials = grpc.credentials.combineChannelCredentials(sslCreds, macaroonCreds);

const client = new lnrpc.Lightning(LND_HOST, credentials);

const params = process.argv.slice(2);

if (params.length % 2 !== 0) {
    console.log('Usage: node multi-channel-funding.js pubkey amount [pubkey amount]...')
}

const channels = [];
for (let i = 0; i < params.length; i += 2) {
    channels.push({
        pubKey: Buffer.from(params[i], 'hex'),
        amount: parseInt(params[i + 1], 10),
        pendingChanID: randomBytes(32),
        outputAddr: '',
        finalized: false,
        chanPending: null,
        cleanedUp: false,
    });
}

channels.forEach(c => {
    const openChannelMsg = {
        node_pubkey: c.pubKey,
        local_funding_amount: c.amount,
        funding_shim: {
            psbt_shim: {
                pending_chan_id: c.pendingChanID,
                no_publish: true,
            }
        }
    };
    const openChannelCall = client.OpenChannel(openChannelMsg);
    openChannelCall.on('data', function (update) {
        if (update.psbt_fund && update.psbt_fund.funding_address) {
            console.log('Got funding addr for PSBT: ' + update.psbt_fund.funding_address);
            c.outputAddr = update.psbt_fund.funding_address;
            maybeFundPSBT();
        }
        if (update.chan_pending) {
            c.chanPending = update.chan_pending;
            const txidStr = update.chan_pending.txid.reverse().toString('hex');
            console.log(`
Channels are now pending!
Expected TXID of published final transaction: ${txidStr}
`);
            process.exit(0);
        }
    });
    openChannelCall.on('error', function (e) {
        console.log('Error on open channel call: ' + e);
        tryCleanup();
    });
});

function tryCleanup() {
    function maybeExit() {
        for (let i = 0; i < channels.length; i++) {
            if (!channels[i].cleanedUp) {
                // Not all channels are cleaned up yet.
                return;
            }
        }
    }
    channels.forEach(c => {
        if (c.cleanedUp) {
            return;
        }
        if (c.chanPending === null) {
            console.log("Cleaning up channel, shim cancel")
            // The channel never made it into the pending state, let's try to
            // remove the funding shim. This is best effort. Depending on the
            // state of the channel this might fail so we don't log any errors
            // here.
            client.FundingStateStep({
                shim_cancel: {
                    pending_chan_id: c.pendingChanID,
                }
            }, () => {
                c.cleanedUp = true;
                maybeExit();
            });
        } else {
            // The channel is pending but since we aborted will never make it
            // to be confirmed. We need to tell lnd to abandon this channel
            // otherwise it will show in the pending channels for forever.
            console.log("Cleaning up channel, abandon channel")
            client.AbandonChannel({
                channel_point: {
                    funding_txid: {
                        funding_txid_bytes: c.chanPending.txid,
                    },
                    output_index: c.chanPending.output_index,
                },
                i_know_what_i_am_doing: true,
            }, () => {
                c.cleanedUp = true;
                maybeExit();
            });
        }
    });
}

function maybeFundPSBT() {
    const outputsBitcoind = [];
    const outputsLnd = {};
    for (let i = 0; i < channels.length; i++) {
        const c = channels[i];
        if (c.outputAddr === '') {
            // Not all channels did get a funding address yet.
            return;
        }

        outputsBitcoind.push({
            [c.outputAddr]: c.amount / 100000000,
        });
        outputsLnd[c.outputAddr] = c.amount;
    }

    console.log(`
Channels ready for funding transaction.
Please create a funded PSBT now.
Examples:

bitcoind:
    bitcoin-cli walletcreatefundedpsbt '[]' '${JSON.stringify(outputsBitcoind)}' 0 '{"fee_rate": 15}'

lnd:
    lncli wallet psbt fund --outputs='${JSON.stringify(outputsLnd)}' --sat_per_vbyte=15
`);

    prompt.get([{name: 'funded_psbt'}], (err, result) => {
        if (err) {
            console.log(err);

            tryCleanup();
            return;
        }
        channels.forEach(c => {
            const verifyMsg = {
                psbt_verify: {
                    funded_psbt: Buffer.from(result.funded_psbt, 'base64'),
                    pending_chan_id: c.pendingChanID,
                    skip_finalize: true
                }
            };
            client.FundingStateStep(verifyMsg, (err, res) => {
                if (err) {
                    console.log(err);

                    tryCleanup();
                    return;
                }
                if (res) {
                    c.finalized = true;
                    maybePublishPSBT();
                }
            });
        });
    });
}

function maybePublishPSBT() {
    for (let i = 0; i < channels.length; i++) {
        const c = channels[i];
        if (!channels[i].finalized) {
            // Not all channels are verified/finalized yet.
            return;
        }
    }

    console.log(`
PSBT verification successful!
You can now sign and publish the transaction.
Make sure the TXID does not change!
`);
}