Skip to content

Instantly share code, notes, and snippets.

@guggero
Created November 20, 2024 17:21
Show Gist options
  • Select an option

  • Save guggero/569241aa9fec57e287101187bd28d1c5 to your computer and use it in GitHub Desktop.

Select an option

Save guggero/569241aa9fec57e287101187bd28d1c5 to your computer and use it in GitHub Desktop.

Step 0 - create a seed and derive xpub (skip this if you have an actual hardware wallet)

We're going to use chantools as a signer, so we don't have to deal with an actual hardware wallet for testing in regtest. The output of this step will just be the xpub of a P2TR account of the seed and the seed's master key fingerprint.

For this example we use the BIP39 seed "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon cactus" which is equivalent to the master root key tprv8ZgxMBicQKsPdKXUBQMuH7B5NAfjBmTdsT2hitzzLWB3BZJURFLTqfdJPr15WkH8huiTrr6k5aCwLoWDxLxhqi8waU7WcrZ9f6kGyJGP2BL.

#!/bin/bash

ROOTKEY="tprv8ZgxMBicQKsPdKXUBQMuH7B5NAfjBmTdsT2hitzzLWB3BZJURFLTqfdJPr15WkH8huiTrr6k5aCwLoWDxLxhqi8waU7WcrZ9f6kGyJGP2BL"

# This should probably be different on regtest, but lnd only uses network ID 0.
DERIVATIONPATH="m/86'/0'/0'"

XPUB=$(chantools --regtest derivekey --path "$DERIVATIONPATH" --rootkey $ROOTKEY | grep xpub | cut -d':' -f2 | xargs)

# We use chantools as a signer which doesn't need a specific fingerprint to identify the seed.
# So we just use a dummy value of the correct format.
FINGERPRINT=00112233

echo xpub: $XPUB
echo master key fingerprint: $FINGERPRINT

Step 1 - import xpub into lnd

We now import the xpub into lnd. This can be the same lnd that tapd is connected to, or any other instance. Then we derive an address from that account and show its public key.

#!/bin/bash

LNCLI=reg_alice
ACCOUNT=group-keys

# Import xpub into lnd wallet.
$LNCLI wallet accounts import --address_type p2tr --master_key_fingerprint $FINGERPRINT $XPUB $ACCOUNT

# Derive new p2tr address.
ADDRESS=$($LNCLI newaddress p2tr --account $ACCOUNT | jq -r .address)
echo address: $ADDRESS

# Show all addresses of imported account.
$LNCLI wallet addresses list --account_name $ACCOUNT

Which outputs:

{
    "account":  {
        "name":  "group-keys",
        "address_type":  "TAPROOT_PUBKEY",
        "extended_public_key":  "tpubDD4ho2vw7ckz1kn1xMNpxTwMvRKWPAqmqo4oppTcW2x96nEw1sJXvWUsTovwwcvZ5mdb98eN8b5E4drMDWmzL1hRzqXfr4BnPwNQZCuw9tv",
        "master_key_fingerprint":  "00112233",
        "derivation_path":  "m/86'/0'/0'",
        "external_key_count":  0,
        "internal_key_count":  0,
        "watch_only":  true
    },
    "dry_run_external_addrs":  [],
    "dry_run_internal_addrs":  []
}

address: bcrt1p0svqqvsaxtu5te5nxpq8qa9xg89rrenfg4xxvkhlqcu7hhrwzl9qjjnytg

{
    "account_with_addresses":  [
        {
            "name":  "group-keys",
            "address_type":  "TAPROOT_PUBKEY",
            "derivation_path":  "m/86'/0'/0'",
            "addresses":  [
                {
                    "address":  "bcrt1p0svqqvsaxtu5te5nxpq8qa9xg89rrenfg4xxvkhlqcu7hhrwzl9qjjnytg",
                    "is_internal":  false,
                    "balance":  "0",
                    "derivation_path":  "m/86'/0'/1'/0/0",
                    "public_key":  "02eaf1e2c9eb14d96b36a5804c758324170ad41eea98ebb9fd5b57b2e4a3365c70"
                }
            ]
        }
    ]
}

The above now also shows us our group public key we're going to want to use. Note that the derivation path shown for the address is wrong (should be "m/86'/0'/0'/0/0"), due to a bug in lnd. This is only wrong in the output of the specific RPC response but should be correct in the PSBTs later on.

Step 2 - send some funds to the address

To be able to create a template transaction easily, we just send some funds to the address. Don't send too much but also not just above dust. A couple of thousand satoshis should be fine.

#!/bin/bash

LNCLI=reg_alice
ACCOUNT=group-keys

BITCOINCLI=reg_bitcoin
MINE="regtest mine 1"

# Send 2k satoshis to generated address.
$BITCOINCLI sendtoaddress $ADDRESS 0.00002

# Mine a block (or wait for one to be mined on non-regtest)
$MINE

# Show account balance.
$LNCLI walletbalance --account $ACCOUNT

# Extract the outpoint of the now confirmed UTXO.
OUTPOINT=$($LNCLI listunspent | jq -r ".utxos[] | select(.address | contains(\"$ADDRESS\")) | .outpoint")

echo outpoint: $OUTPOINT

Output:

{
    "total_balance":  "2000",
    "confirmed_balance":  "2000",
    "unconfirmed_balance":  "0",
    "locked_balance":  "0",
    "reserved_balance_anchor_chan":  "20000",
    "account_balance":  {
        "group-keys":  {
            "confirmed_balance":  "2000",
            "unconfirmed_balance":  "0"
        }
    }
}

outpoint: 94abfc1b03c3f8556a3ad386bece690b4461bb219796ac8ef0939dd1ad8046e6:1

Step 3 - create the template PSBT

We now create a template from attempting to spend those 2k sats again. But don't worry, we're not actually going to spend them. First we lease/lock the UTXO to signal we're going to use it. Then we create a spend PSBT for that input.

#!/bin/bash

LNCLI=reg_alice
ACCOUNT=group-keys

BITCOINCLI=reg_bitcoin

# The lock ID identifies the application locking the UTXO, we just use
# 32 random bytes here as it doesn't really matter.
LOCKID=$(head -c 32 /dev/random|xxd -p -c32)

# Lease the output (prerequisite for PSBT funding).
$LNCLI wallet leaseoutput --outpoint $OUTPOINT --lockid $LOCKID --expiry 3600

# Create a PSBT that spends the UTXO into the same address again.
# Specifying the change output index 0 will make sure this creates a
# one-input-one-output transaction by adding any left-over balance after
# fee estimation to the given single output.
PSBT=$($LNCLI wallet psbt fundtemplate --account $ACCOUNT --inputs="[\"$OUTPOINT\"]" --outputs="[\"$ADDRESS:123\"]" --change_type p2tr --change_output_index 0 --sat_per_vbyte 1 | jq -r .psbt)

echo psbt: $PSBT

# Print the decoded PSBT.
$BITCOINCLI decodepsbt $PSBT

Output:

{
    "expiration":  "1732126195"
}

psbt: cHNidP8BAF4CAAAAAeZGgK3RnZPwjqyWlyG7YUQLac6+htM6alX4wwMb/KuUAQAAAAAAAAAAAWEHAAAAAAAAIlEgfBgAMh0y+UXmkzBAcHSmQcox5mlFTGZa/wY569xuF8oAAAAAAAEBK9AHAAAAAAAAIlEgfBgAMh0y+UXmkzBAcHSmQcox5mlFTGZa/wY569xuF8oiBgLq8eLJ6xTZazalgEx1gyQXCtQe6pjruf1bV7LkozZccBgzIhEAVgAAgAAAAIAAAACAAAAAAAAAAAAhFurx4snrFNlrNqWATHWDJBcK1B7qmOu5/VtXsuSjNlxwGQAzIhEAVgAAgAAAAIAAAACAAAAAAAAAAAAAAA==

{
  "tx": {
    "txid": "fa72d1361d87802a10bc7bbfdfc59de47c47b3f5126d69ce02f5044c41257bfc",
    "hash": "fa72d1361d87802a10bc7bbfdfc59de47c47b3f5126d69ce02f5044c41257bfc",
    "version": 2,
    "size": 94,
    "vsize": 94,
    "weight": 376,
    "locktime": 0,
    "vin": [
      {
        "txid": "94abfc1b03c3f8556a3ad386bece690b4461bb219796ac8ef0939dd1ad8046e6",
        "vout": 1,
        "scriptSig": {
          "asm": "",
          "hex": ""
        },
        "sequence": 0
      }
    ],
    "vout": [
      {
        "value": 0.00001889,
        "n": 0,
        "scriptPubKey": {
          "asm": "1 7c1800321d32f945e69330407074a641ca31e669454c665aff0639ebdc6e17ca",
          "desc": "rawtr(7c1800321d32f945e69330407074a641ca31e669454c665aff0639ebdc6e17ca)#hf49xftv",
          "hex": "51207c1800321d32f945e69330407074a641ca31e669454c665aff0639ebdc6e17ca",
          "address": "bcrt1p0svqqvsaxtu5te5nxpq8qa9xg89rrenfg4xxvkhlqcu7hhrwzl9qjjnytg",
          "type": "witness_v1_taproot"
        }
      }
    ]
  },
  "global_xpubs": [
  ],
  "psbt_version": 0,
  "proprietary": [
  ],
  "unknown": {
  },
  "inputs": [
    {
      "witness_utxo": {
        "amount": 0.00002000,
        "scriptPubKey": {
          "asm": "1 7c1800321d32f945e69330407074a641ca31e669454c665aff0639ebdc6e17ca",
          "desc": "rawtr(7c1800321d32f945e69330407074a641ca31e669454c665aff0639ebdc6e17ca)#hf49xftv",
          "hex": "51207c1800321d32f945e69330407074a641ca31e669454c665aff0639ebdc6e17ca",
          "address": "bcrt1p0svqqvsaxtu5te5nxpq8qa9xg89rrenfg4xxvkhlqcu7hhrwzl9qjjnytg",
          "type": "witness_v1_taproot"
        }
      },
      "bip32_derivs": [
        {
          "pubkey": "02eaf1e2c9eb14d96b36a5804c758324170ad41eea98ebb9fd5b57b2e4a3365c70",
          "master_fingerprint": "33221100",
          "path": "m/86h/0h/0h/0/0"
        }
      ],
      "taproot_bip32_derivs": [
        {
          "pubkey": "eaf1e2c9eb14d96b36a5804c758324170ad41eea98ebb9fd5b57b2e4a3365c70",
          "master_fingerprint": "33221100",
          "path": "m/86h/0h/0h/0/0",
          "leaf_hashes": [
          ]
        }
      ]
    }
  ],
  "outputs": [
    {
    }
  ],
  "fee": 0.00000111
}

Step X - full script for reference

#!/bin/bash

# Variables
LNCLI=reg_alice
ACCOUNT=group-keys

BITCOINCLI=reg_bitcoin
MINE="regtest mine 1"


# Import xpub into lnd wallet.
$LNCLI wallet accounts import --address_type p2tr --master_key_fingerprint $FINGERPRINT $XPUB $ACCOUNT

# Derive new p2tr address.
ADDRESS=$($LNCLI newaddress p2tr --account $ACCOUNT | jq -r .address)
echo address: $ADDRESS

# Show all addresses of imported account.
$LNCLI wallet addresses list --account_name $ACCOUNT


# Send 2k satoshis to generated address.
$BITCOINCLI sendtoaddress $ADDRESS 0.00002

# Mine a block (or wait for one to be mined on non-regtest)
$MINE

# Show account balance.
$LNCLI walletbalance --account $ACCOUNT

# Extract the outpoint of the now confirmed UTXO.
OUTPOINT=$($LNCLI listunspent | jq -r ".utxos[] | select(.address | contains(\"$ADDRESS\")) | .outpoint")

echo outpoint: $OUTPOINT


# The lock ID identifies the application locking the UTXO, we just use
# 32 random bytes here as it doesn't really matter.
LOCKID=$(head -c 32 /dev/random|xxd -p -c32)

# Lease the output (prerequisite for PSBT funding).
$LNCLI wallet leaseoutput --outpoint $OUTPOINT --lockid $LOCKID --expiry 3600

# Create a PSBT that spends the UTXO into the same address again.
# Specifying the change output index 0 will make sure this creates a
# one-input-one-output transaction by adding any left-over balance after
# fee estimation to the given single output.
PSBT=$($LNCLI wallet psbt fundtemplate --account $ACCOUNT --inputs="[\"$OUTPOINT\"]" --outputs="[\"$ADDRESS:123\"]" --change_type p2tr --change_output_index 0 --sat_per_vbyte 1 | jq -r .psbt)

echo psbt: $PSBT

# Print the decoded PSBT.
$BITCOINCLI decodepsbt $PSBT
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment