mirror of https://github.com/guggero/chantools
Compare commits
80 Commits
Author | SHA1 | Date |
---|---|---|
|
d173f8ca1c | 1 week ago |
|
65d6e52299 | 1 week ago |
|
551e2a056a | 1 week ago |
|
7bd21fba58 | 1 week ago |
|
1f43c4a0ad | 1 week ago |
|
32fd44dbe1 | 1 week ago |
|
85f207c58f | 1 month ago |
|
da904ae1d7 | 1 month ago |
|
14aa06fa41 | 2 months ago |
|
601789f445 | 2 months ago |
|
cc284baa67 | 2 months ago |
|
6acc81815e | 2 months ago |
|
e3285daf5b | 2 months ago |
|
179773fdb9 | 2 months ago |
|
0fd58ee7eb | 2 months ago |
|
7c405057bd | 2 months ago |
|
d07251df9c | 3 months ago |
|
7f840cf03b | 3 months ago |
|
f062b53a21 | 3 months ago |
|
24cd530c65 | 3 months ago |
|
c54184b8d0 | 3 months ago |
|
997d86cd84 | 3 months ago |
|
676ba60197 | 3 months ago |
|
3e419da317 | 3 months ago |
|
6a81614b1b | 3 months ago |
|
71b824e105 | 3 months ago |
|
1a46f9099f | 3 months ago |
|
9f8484bb89 | 3 months ago |
|
e80dcbfb67 | 3 months ago |
|
5c39df02d3 | 3 months ago |
|
37179e5215 | 3 months ago |
|
b169634d85 | 3 months ago |
|
a3a00d410a | 3 months ago |
|
450c2777af | 3 months ago |
|
c4162303b7 | 3 months ago |
|
486af2e99a | 4 months ago |
|
3b3daddfee | 4 months ago |
|
c75f9ff91a | 5 months ago |
|
3fbf8d0bd2 | 5 months ago |
|
cf4cabbd2a | 5 months ago |
|
78c41b4acf | 5 months ago |
|
a0e5f0613d | 5 months ago |
|
d9af0e36e5 | 5 months ago |
|
3b50a5ce16 | 5 months ago |
|
d5d5a91430 | 5 months ago |
|
82a03a65ef | 5 months ago |
|
65cc3fdf6e | 5 months ago |
|
79f65bb1a1 | 5 months ago |
|
341d3af108 | 5 months ago |
|
3865a7757e | 6 months ago |
|
ad3c1ad2de | 6 months ago |
|
399a23adba | 6 months ago |
|
a05962e03e | 6 months ago |
|
7e3ea44fd4 | 6 months ago |
|
53085d34d0 | 6 months ago |
|
d830ebe57a | 6 months ago |
|
858995a317 | 6 months ago |
|
b777d4436d | 6 months ago |
|
5cf7fd60c4 | 6 months ago |
|
92fdb156e0 | 6 months ago |
|
2abc29d01d | 6 months ago |
|
fd18186a82 | 6 months ago |
|
7227c7f101 | 6 months ago |
|
801f881274 | 6 months ago |
|
c89cede963 | 6 months ago |
|
00c7f7eb98 | 6 months ago |
|
798a6d0927 | 6 months ago |
|
7e110d8d46 | 6 months ago |
|
0ebb732576 | 6 months ago |
|
8d28e1b2f6 | 6 months ago |
|
a5a884bff9 | 7 months ago |
|
bed41c1533 | 7 months ago |
|
a44746912c | 8 months ago |
|
55208e0218 | 8 months ago |
|
a13262f2ff | 8 months ago |
|
dee18ed80c | 8 months ago |
|
5bc49376a3 | 8 months ago |
|
3044d9f796 | 8 months ago |
|
f9343e5c3d | 8 months ago |
|
abb0343059 | 8 months ago |
@ -1,3 +1,4 @@
|
|||||||
|
.idea
|
||||||
/chantools
|
/chantools
|
||||||
results
|
results
|
||||||
/chantools-v*
|
/chantools-v*
|
||||||
|
@ -0,0 +1,233 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||||
|
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
|
||||||
|
"github.com/lightninglabs/chantools/lnd"
|
||||||
|
"github.com/lightningnetwork/lnd/aezeed"
|
||||||
|
"github.com/lightningnetwork/lnd/keychain"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type createWalletCommand struct {
|
||||||
|
WalletDBDir string
|
||||||
|
GenerateSeed bool
|
||||||
|
|
||||||
|
rootKey *rootKey
|
||||||
|
cmd *cobra.Command
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCreateWalletCommand() *cobra.Command {
|
||||||
|
cc := &createWalletCommand{}
|
||||||
|
cc.cmd = &cobra.Command{
|
||||||
|
Use: "createwallet",
|
||||||
|
Short: "Create a new lnd compatible wallet.db file from an " +
|
||||||
|
"existing seed or by generating a new one",
|
||||||
|
Long: `Creates a new wallet that can be used with lnd or with
|
||||||
|
chantools. The wallet can be created from an existing seed or a new one can be
|
||||||
|
generated (use --generateseed).`,
|
||||||
|
Example: `chantools createwallet \
|
||||||
|
--walletdbdir ~/.lnd/data/chain/bitcoin/mainnet`,
|
||||||
|
RunE: cc.Execute,
|
||||||
|
}
|
||||||
|
cc.cmd.Flags().StringVar(
|
||||||
|
&cc.WalletDBDir, "walletdbdir", "", "the folder to create the "+
|
||||||
|
"new wallet.db file in",
|
||||||
|
)
|
||||||
|
cc.cmd.Flags().BoolVar(
|
||||||
|
&cc.GenerateSeed, "generateseed", false, "generate a new "+
|
||||||
|
"seed instead of using an existing one",
|
||||||
|
)
|
||||||
|
|
||||||
|
cc.rootKey = newRootKey(cc.cmd, "creating the new wallet")
|
||||||
|
|
||||||
|
return cc.cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *createWalletCommand) Execute(_ *cobra.Command, _ []string) error {
|
||||||
|
var (
|
||||||
|
publicWalletPw = lnwallet.DefaultPublicPassphrase
|
||||||
|
privateWalletPw = lnwallet.DefaultPrivatePassphrase
|
||||||
|
masterRootKey *hdkeychain.ExtendedKey
|
||||||
|
birthday time.Time
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check that we have a wallet DB.
|
||||||
|
if c.WalletDBDir == "" {
|
||||||
|
return errors.New("wallet DB directory is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the directory (and parents) exists.
|
||||||
|
if err := os.MkdirAll(c.WalletDBDir, 0700); err != nil {
|
||||||
|
return fmt.Errorf("error creating wallet DB directory '%s': %w",
|
||||||
|
c.WalletDBDir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we should create a new seed or read if from the console or
|
||||||
|
// environment.
|
||||||
|
if c.GenerateSeed {
|
||||||
|
fmt.Printf("Generating new lnd compatible aezeed...\n")
|
||||||
|
seed, err := aezeed.New(
|
||||||
|
keychain.KeyDerivationVersionTaproot, nil, time.Now(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating new seed: %w", err)
|
||||||
|
}
|
||||||
|
birthday = seed.BirthdayTime()
|
||||||
|
|
||||||
|
// Derive the master extended key from the seed.
|
||||||
|
masterRootKey, err = hdkeychain.NewMaster(
|
||||||
|
seed.Entropy[:], chainParams,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to derive master extended "+
|
||||||
|
"key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
passphrase, err := lnd.ReadPassphrase("shouldn't use")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error reading passphrase: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mnemonic, err := seed.ToMnemonic(passphrase)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error converting seed to "+
|
||||||
|
"mnemonic: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Generated new seed")
|
||||||
|
printCipherSeedWords(mnemonic[:])
|
||||||
|
} else {
|
||||||
|
masterRootKey, birthday, err = c.rootKey.readWithBirthday()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// To automate things with chantools, we also offer reading the wallet
|
||||||
|
// password from environment variables.
|
||||||
|
pw := []byte(strings.TrimSpace(os.Getenv(lnd.PasswordEnvName)))
|
||||||
|
|
||||||
|
// Because we cannot differentiate between an empty and a non-existent
|
||||||
|
// environment variable, we need a special character that indicates that
|
||||||
|
// no password should be used. We use a single dash (-) for that as that
|
||||||
|
// would be too short for an explicit password anyway.
|
||||||
|
switch {
|
||||||
|
// The user indicated in the environment variable that no passphrase
|
||||||
|
// should be used. We don't set any value.
|
||||||
|
case string(pw) == "-":
|
||||||
|
|
||||||
|
// The environment variable didn't contain anything, we'll read the
|
||||||
|
// passphrase from the terminal.
|
||||||
|
case len(pw) == 0:
|
||||||
|
fmt.Printf("\n\nThe wallet password is used to encrypt the " +
|
||||||
|
"wallet.db file itself and is unrelated to the seed.\n")
|
||||||
|
pw, err = lnd.PasswordFromConsole("Input new wallet password: ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pw2, err := lnd.PasswordFromConsole(
|
||||||
|
"Confirm new wallet password: ",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(pw, pw2) {
|
||||||
|
return errors.New("passwords don't match")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pw) > 0 {
|
||||||
|
publicWalletPw = pw
|
||||||
|
privateWalletPw = pw
|
||||||
|
}
|
||||||
|
|
||||||
|
// There was a password in the environment, just use it directly.
|
||||||
|
default:
|
||||||
|
publicWalletPw = pw
|
||||||
|
privateWalletPw = pw
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to create the wallet.
|
||||||
|
loader, err := btcwallet.NewWalletLoader(
|
||||||
|
chainParams, 0, btcwallet.LoaderWithLocalWalletDB(
|
||||||
|
c.WalletDBDir, true, 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating wallet loader: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = loader.CreateNewWalletExtendedKey(
|
||||||
|
publicWalletPw, privateWalletPw, masterRootKey, birthday,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating new wallet: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := loader.UnloadWallet(); err != nil {
|
||||||
|
return fmt.Errorf("error unloading wallet: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Wallet created successfully at %v\n", c.WalletDBDir)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printCipherSeedWords(mnemonicWords []string) {
|
||||||
|
fmt.Println("!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " +
|
||||||
|
"RESTORE THE WALLET!!!")
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
fmt.Println("---------------BEGIN LND CIPHER SEED---------------")
|
||||||
|
|
||||||
|
numCols := 4
|
||||||
|
colWords := monoWidthColumns(mnemonicWords, numCols)
|
||||||
|
for i := 0; i < len(colWords); i += numCols {
|
||||||
|
fmt.Printf("%2d. %3s %2d. %3s %2d. %3s %2d. %3s\n",
|
||||||
|
i+1, colWords[i], i+2, colWords[i+1], i+3,
|
||||||
|
colWords[i+2], i+4, colWords[i+3])
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("---------------END LND CIPHER SEED-----------------")
|
||||||
|
|
||||||
|
fmt.Println("\n!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " +
|
||||||
|
"RESTORE THE WALLET!!!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// monoWidthColumns takes a set of words, and the number of desired columns,
|
||||||
|
// and returns a new set of words that have had white space appended to the
|
||||||
|
// word in order to create a mono-width column.
|
||||||
|
func monoWidthColumns(words []string, ncols int) []string {
|
||||||
|
// Determine max size of words in each column.
|
||||||
|
colWidths := make([]int, ncols)
|
||||||
|
for i, word := range words {
|
||||||
|
col := i % ncols
|
||||||
|
curWidth := colWidths[col]
|
||||||
|
if len(word) > curWidth {
|
||||||
|
colWidths[col] = len(word)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append whitespace to each word to make columns mono-width.
|
||||||
|
finalWords := make([]string, len(words))
|
||||||
|
for i, word := range words {
|
||||||
|
col := i % ncols
|
||||||
|
width := colWidths[col]
|
||||||
|
|
||||||
|
diff := width - len(word)
|
||||||
|
finalWords[i] = word + strings.Repeat(" ", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalWords
|
||||||
|
}
|
@ -0,0 +1,531 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
|
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||||
|
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcd/mempool"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/lightninglabs/chantools/btc"
|
||||||
|
"github.com/lightninglabs/chantools/lnd"
|
||||||
|
"github.com/lightningnetwork/lnd/input"
|
||||||
|
"github.com/lightningnetwork/lnd/keychain"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type pullAnchorCommand struct {
|
||||||
|
APIURL string
|
||||||
|
SponsorInput string
|
||||||
|
AnchorAddrs []string
|
||||||
|
ChangeAddr string
|
||||||
|
FeeRate uint32
|
||||||
|
|
||||||
|
rootKey *rootKey
|
||||||
|
cmd *cobra.Command
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPullAnchorCommand() *cobra.Command {
|
||||||
|
cc := &pullAnchorCommand{}
|
||||||
|
cc.cmd = &cobra.Command{
|
||||||
|
Use: "pullanchor",
|
||||||
|
Short: "Attempt to CPFP an anchor output of a channel",
|
||||||
|
Long: `Use this command to confirm a channel force close
|
||||||
|
transaction of an anchor output channel type. This will attempt to CPFP the
|
||||||
|
330 byte anchor output created for your node.`,
|
||||||
|
Example: `chantools pullanchor \
|
||||||
|
--sponsorinput txid:vout \
|
||||||
|
--anchoraddr bc1q..... \
|
||||||
|
--changeaddr bc1q..... \
|
||||||
|
--feerate 30`,
|
||||||
|
RunE: cc.Execute,
|
||||||
|
}
|
||||||
|
cc.cmd.Flags().StringVar(
|
||||||
|
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
|
||||||
|
"be esplora compatible)",
|
||||||
|
)
|
||||||
|
cc.cmd.Flags().StringVar(
|
||||||
|
&cc.SponsorInput, "sponsorinput", "", "the input to use to "+
|
||||||
|
"sponsor the CPFP transaction; must be owned by the "+
|
||||||
|
"lnd node that owns the anchor output",
|
||||||
|
)
|
||||||
|
cc.cmd.Flags().StringArrayVar(
|
||||||
|
&cc.AnchorAddrs, "anchoraddr", nil, "the address of the "+
|
||||||
|
"anchor output (p2wsh or p2tr output with 330 "+
|
||||||
|
"satoshis) that should be pulled; can be specified "+
|
||||||
|
"multiple times per command to pull multiple anchors "+
|
||||||
|
"with a single transaction",
|
||||||
|
)
|
||||||
|
cc.cmd.Flags().StringVar(
|
||||||
|
&cc.ChangeAddr, "changeaddr", "", "the change address to "+
|
||||||
|
"send the remaining funds back to; specify '"+
|
||||||
|
lnd.AddressDeriveFromWallet+"' to derive a new "+
|
||||||
|
"address from the seed automatically",
|
||||||
|
)
|
||||||
|
cc.cmd.Flags().Uint32Var(
|
||||||
|
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
|
||||||
|
"use for the sweep transaction in sat/vByte",
|
||||||
|
)
|
||||||
|
|
||||||
|
cc.rootKey = newRootKey(cc.cmd, "deriving keys")
|
||||||
|
|
||||||
|
return cc.cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *pullAnchorCommand) Execute(_ *cobra.Command, _ []string) error {
|
||||||
|
extendedKey, err := c.rootKey.read()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error reading root key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure all input is provided.
|
||||||
|
if c.SponsorInput == "" {
|
||||||
|
return errors.New("sponsor input is required")
|
||||||
|
}
|
||||||
|
if len(c.AnchorAddrs) == 0 {
|
||||||
|
return errors.New("at least one anchor addr is required")
|
||||||
|
}
|
||||||
|
for _, anchorAddr := range c.AnchorAddrs {
|
||||||
|
err = lnd.CheckAddress(
|
||||||
|
anchorAddr, chainParams, true, "anchor",
|
||||||
|
lnd.AddrTypeP2WSH, lnd.AddrTypeP2TR,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = lnd.CheckAddress(
|
||||||
|
c.ChangeAddr, chainParams, true, "change", lnd.AddrTypeP2WKH,
|
||||||
|
lnd.AddrTypeP2TR,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
outpoint, err := lnd.ParseOutpoint(c.SponsorInput)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing sponsor input outpoint: %w",
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default values.
|
||||||
|
if c.FeeRate == 0 {
|
||||||
|
c.FeeRate = defaultFeeSatPerVByte
|
||||||
|
}
|
||||||
|
return createPullTransactionTemplate(
|
||||||
|
extendedKey, c.APIURL, outpoint, c.AnchorAddrs, c.ChangeAddr,
|
||||||
|
c.FeeRate,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type targetAnchor struct {
|
||||||
|
addr string
|
||||||
|
keyDesc *keychain.KeyDescriptor
|
||||||
|
outpoint wire.OutPoint
|
||||||
|
utxo *wire.TxOut
|
||||||
|
script []byte
|
||||||
|
scriptTree *input.AnchorScriptTree
|
||||||
|
}
|
||||||
|
|
||||||
|
func createPullTransactionTemplate(rootKey *hdkeychain.ExtendedKey,
|
||||||
|
apiURL string, sponsorOutpoint *wire.OutPoint, anchorAddrs []string,
|
||||||
|
changeAddr string, feeRate uint32) error {
|
||||||
|
|
||||||
|
var (
|
||||||
|
signer = &lnd.Signer{
|
||||||
|
ExtendedKey: rootKey,
|
||||||
|
ChainParams: chainParams,
|
||||||
|
}
|
||||||
|
api = newExplorerAPI(apiURL)
|
||||||
|
estimator input.TxWeightEstimator
|
||||||
|
)
|
||||||
|
|
||||||
|
changeScript, err := lnd.PrepareWalletAddress(
|
||||||
|
changeAddr, chainParams, &estimator, rootKey, "change",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the sponsor input is a P2WPKH or P2TR input and is known
|
||||||
|
// to the block explorer, so we can fetch the witness utxo.
|
||||||
|
sponsorTx, err := api.Transaction(sponsorOutpoint.Hash.String())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error fetching sponsor tx: %w", err)
|
||||||
|
}
|
||||||
|
sponsorTxOut := sponsorTx.Vout[sponsorOutpoint.Index]
|
||||||
|
sponsorPkScript, err := hex.DecodeString(sponsorTxOut.ScriptPubkey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error decoding sponsor pkscript: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sponsorType, err := txscript.ParsePkScript(sponsorPkScript)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing sponsor pkscript: %w", err)
|
||||||
|
}
|
||||||
|
var sponsorSigHashType txscript.SigHashType
|
||||||
|
switch sponsorType.Class() {
|
||||||
|
case txscript.WitnessV0PubKeyHashTy:
|
||||||
|
estimator.AddP2WKHInput()
|
||||||
|
sponsorSigHashType = txscript.SigHashAll
|
||||||
|
|
||||||
|
case txscript.WitnessV1TaprootTy:
|
||||||
|
sponsorSigHashType = txscript.SigHashDefault
|
||||||
|
estimator.AddTaprootKeySpendInput(sponsorSigHashType)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported sponsor input type: %v",
|
||||||
|
sponsorType.Class())
|
||||||
|
}
|
||||||
|
|
||||||
|
tx := wire.NewMsgTx(2)
|
||||||
|
packet, err := psbt.NewFromUnsignedTx(tx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating PSBT: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let's add the sponsor input to the PSBT.
|
||||||
|
sponsorUtxo := &wire.TxOut{
|
||||||
|
Value: int64(sponsorTxOut.Value),
|
||||||
|
PkScript: sponsorPkScript,
|
||||||
|
}
|
||||||
|
packet.UnsignedTx.TxIn = append(packet.UnsignedTx.TxIn, &wire.TxIn{
|
||||||
|
PreviousOutPoint: *sponsorOutpoint,
|
||||||
|
Sequence: mempool.MaxRBFSequence,
|
||||||
|
})
|
||||||
|
packet.Inputs = append(packet.Inputs, psbt.PInput{
|
||||||
|
WitnessUtxo: sponsorUtxo,
|
||||||
|
SighashType: sponsorSigHashType,
|
||||||
|
})
|
||||||
|
|
||||||
|
targets, err := addAnchorInputs(
|
||||||
|
anchorAddrs, packet, api, &estimator, rootKey,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error adding anchor inputs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we can calculate the fee and add the change output.
|
||||||
|
anchorAmt := uint64(len(anchorAddrs)) * 330
|
||||||
|
totalOutputValue := btcutil.Amount(sponsorTxOut.Value + anchorAmt)
|
||||||
|
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()
|
||||||
|
totalFee := feeRateKWeight.FeeForWeight(estimator.Weight())
|
||||||
|
|
||||||
|
log.Infof("Fee %d sats of %d total amount (estimated weight %d)",
|
||||||
|
totalFee, totalOutputValue, estimator.Weight())
|
||||||
|
|
||||||
|
packet.UnsignedTx.TxOut = append(packet.UnsignedTx.TxOut, &wire.TxOut{
|
||||||
|
Value: int64(totalOutputValue - totalFee),
|
||||||
|
PkScript: changeScript,
|
||||||
|
})
|
||||||
|
packet.Outputs = append(packet.Outputs, psbt.POutput{})
|
||||||
|
|
||||||
|
prevOutFetcher := txscript.NewMultiPrevOutFetcher(
|
||||||
|
map[wire.OutPoint]*wire.TxOut{
|
||||||
|
*sponsorOutpoint: sponsorUtxo,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
for idx := range targets {
|
||||||
|
prevOutFetcher.AddPrevOut(
|
||||||
|
targets[idx].outpoint, targets[idx].utxo,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// And now we sign the anchor inputs.
|
||||||
|
for idx := range targets {
|
||||||
|
target := targets[idx]
|
||||||
|
signDesc := &input.SignDescriptor{
|
||||||
|
KeyDesc: *target.keyDesc,
|
||||||
|
WitnessScript: target.script,
|
||||||
|
Output: target.utxo,
|
||||||
|
PrevOutputFetcher: prevOutFetcher,
|
||||||
|
InputIndex: idx + 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
var anchorWitness wire.TxWitness
|
||||||
|
switch {
|
||||||
|
// Simple Taproot Channel:
|
||||||
|
case target.scriptTree != nil:
|
||||||
|
signDesc.SignMethod = input.TaprootKeySpendSignMethod
|
||||||
|
signDesc.HashType = txscript.SigHashDefault
|
||||||
|
signDesc.TapTweak = target.scriptTree.TapscriptRoot
|
||||||
|
|
||||||
|
anchorSig, err := signer.SignOutputRaw(
|
||||||
|
packet.UnsignedTx, signDesc,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error signing anchor "+
|
||||||
|
"input: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
anchorWitness = wire.TxWitness{
|
||||||
|
anchorSig.Serialize(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anchor Channel:
|
||||||
|
default:
|
||||||
|
signDesc.SignMethod = input.WitnessV0SignMethod
|
||||||
|
signDesc.HashType = txscript.SigHashAll
|
||||||
|
|
||||||
|
anchorSig, err := signer.SignOutputRaw(
|
||||||
|
packet.UnsignedTx, signDesc,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error signing anchor "+
|
||||||
|
"input: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
anchorWitness = make(wire.TxWitness, 2)
|
||||||
|
anchorWitness[0] = append(
|
||||||
|
anchorSig.Serialize(),
|
||||||
|
byte(txscript.SigHashAll),
|
||||||
|
)
|
||||||
|
anchorWitness[1] = target.script
|
||||||
|
}
|
||||||
|
|
||||||
|
var witnessBuf bytes.Buffer
|
||||||
|
err = psbt.WriteTxWitness(&witnessBuf, anchorWitness)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error serializing witness: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
packet.Inputs[idx+1].FinalScriptWitness = witnessBuf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
packetBase64, err := packet.B64Encode()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error encoding PSBT: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Prepared PSBT follows, please now call\n" +
|
||||||
|
"'lncli wallet psbt finalize <psbt>' to finalize the\n" +
|
||||||
|
"transaction, then publish it manually or by using\n" +
|
||||||
|
"'lncli wallet publishtx <final_tx>':\n\n" + packetBase64 +
|
||||||
|
"\n")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addAnchorInputs(anchorAddrs []string, packet *psbt.Packet,
|
||||||
|
api *btc.ExplorerAPI, estimator *input.TxWeightEstimator,
|
||||||
|
rootKey *hdkeychain.ExtendedKey) ([]targetAnchor, error) {
|
||||||
|
|
||||||
|
// Fetch the additional info we need for the anchor output as well.
|
||||||
|
results := make([]targetAnchor, len(anchorAddrs))
|
||||||
|
for idx, anchorAddr := range anchorAddrs {
|
||||||
|
anchorTx, anchorIndex, err := api.Outpoint(anchorAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error fetching anchor "+
|
||||||
|
"outpoint: %w", err)
|
||||||
|
}
|
||||||
|
anchorTxHash, err := chainhash.NewHashFromStr(anchorTx.TXID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error decoding anchor txid: %w",
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
|
||||||
|
addr, err := btcutil.DecodeAddress(anchorAddr, chainParams)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error decoding address: %w",
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
|
||||||
|
anchorPkScript, err := txscript.PayToAddrScript(addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error creating pk script: %w",
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
|
||||||
|
target := targetAnchor{
|
||||||
|
addr: anchorAddr,
|
||||||
|
utxo: &wire.TxOut{
|
||||||
|
Value: 330,
|
||||||
|
PkScript: anchorPkScript,
|
||||||
|
},
|
||||||
|
outpoint: wire.OutPoint{
|
||||||
|
Hash: *anchorTxHash,
|
||||||
|
Index: uint32(anchorIndex),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
switch addr.(type) {
|
||||||
|
case *btcutil.AddressWitnessScriptHash:
|
||||||
|
estimator.AddWitnessInput(input.AnchorWitnessSize)
|
||||||
|
|
||||||
|
anchorKeyDesc, anchorWitnessScript, err := findAnchorKey(
|
||||||
|
rootKey, anchorPkScript,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not find "+
|
||||||
|
"key for anchor address %v: %w",
|
||||||
|
anchorAddr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
target.keyDesc = anchorKeyDesc
|
||||||
|
target.script = anchorWitnessScript
|
||||||
|
|
||||||
|
case *btcutil.AddressTaproot:
|
||||||
|
estimator.AddTaprootKeySpendInput(
|
||||||
|
txscript.SigHashDefault,
|
||||||
|
)
|
||||||
|
|
||||||
|
anchorKeyDesc, scriptTree, err := findTaprootAnchorKey(
|
||||||
|
rootKey, anchorPkScript,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not find "+
|
||||||
|
"key for anchor address %v: %w",
|
||||||
|
anchorAddr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
target.keyDesc = anchorKeyDesc
|
||||||
|
target.scriptTree = scriptTree
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported address type: %T",
|
||||||
|
addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Found multisig key %x for anchor pk script %x",
|
||||||
|
target.keyDesc.PubKey.SerializeCompressed(),
|
||||||
|
anchorPkScript)
|
||||||
|
|
||||||
|
packet.UnsignedTx.TxIn = append(
|
||||||
|
packet.UnsignedTx.TxIn, &wire.TxIn{
|
||||||
|
PreviousOutPoint: target.outpoint,
|
||||||
|
Sequence: mempool.MaxRBFSequence,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
packet.Inputs = append(packet.Inputs, psbt.PInput{
|
||||||
|
WitnessUtxo: target.utxo,
|
||||||
|
WitnessScript: target.script,
|
||||||
|
})
|
||||||
|
|
||||||
|
results[idx] = target
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findAnchorKey(rootKey *hdkeychain.ExtendedKey,
|
||||||
|
targetScript []byte) (*keychain.KeyDescriptor, []byte, error) {
|
||||||
|
|
||||||
|
family := keychain.KeyFamilyMultiSig
|
||||||
|
localMultisig, err := lnd.DeriveChildren(rootKey, []uint32{
|
||||||
|
lnd.HardenedKeyStart + uint32(keychain.BIP0043Purpose),
|
||||||
|
lnd.HardenedKeyStart + chainParams.HDCoinType,
|
||||||
|
lnd.HardenedKeyStart + uint32(family),
|
||||||
|
0,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("could not derive local "+
|
||||||
|
"multisig key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through the local multisig keys to find the target anchor
|
||||||
|
// script.
|
||||||
|
for index := range uint32(math.MaxInt16) {
|
||||||
|
currentKey, err := localMultisig.DeriveNonStandard(index)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("error deriving child "+
|
||||||
|
"key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPubkey, err := currentKey.ECPubKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("error deriving public "+
|
||||||
|
"key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
script, err := input.CommitScriptAnchor(currentPubkey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("error deriving script: "+
|
||||||
|
"%w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pkScript, err := input.WitnessScriptHash(script)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("error deriving script "+
|
||||||
|
"hash: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(pkScript, targetScript) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return &keychain.KeyDescriptor{
|
||||||
|
PubKey: currentPubkey,
|
||||||
|
KeyLocator: keychain.KeyLocator{
|
||||||
|
Family: family,
|
||||||
|
Index: index,
|
||||||
|
},
|
||||||
|
}, script, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil, errors.New("no matching pubkeys found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func findTaprootAnchorKey(rootKey *hdkeychain.ExtendedKey,
|
||||||
|
targetScript []byte) (*keychain.KeyDescriptor, *input.AnchorScriptTree,
|
||||||
|
error) {
|
||||||
|
|
||||||
|
family := keychain.KeyFamilyPaymentBase
|
||||||
|
localPayment, err := lnd.DeriveChildren(rootKey, []uint32{
|
||||||
|
lnd.HardenedKeyStart + uint32(keychain.BIP0043Purpose),
|
||||||
|
lnd.HardenedKeyStart + chainParams.HDCoinType,
|
||||||
|
lnd.HardenedKeyStart + uint32(family),
|
||||||
|
0,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("could not derive local "+
|
||||||
|
"multisig key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through the local multisig keys to find the target anchor
|
||||||
|
// script.
|
||||||
|
for index := range uint32(math.MaxInt16) {
|
||||||
|
currentKey, err := localPayment.DeriveNonStandard(index)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("error deriving child "+
|
||||||
|
"key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPubkey, err := currentKey.ECPubKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("error deriving public "+
|
||||||
|
"key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptTree, err := input.NewAnchorScriptTree(currentPubkey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("error deriving taproot "+
|
||||||
|
"key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pkScript, err := input.PayToTaprootScript(scriptTree.TaprootKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("error deriving pk "+
|
||||||
|
"script: %w", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(pkScript, targetScript) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return &keychain.KeyDescriptor{
|
||||||
|
PubKey: currentPubkey,
|
||||||
|
KeyLocator: keychain.KeyLocator{
|
||||||
|
Family: family,
|
||||||
|
Index: index,
|
||||||
|
},
|
||||||
|
}, scriptTree, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil, errors.New("no matching pubkeys found")
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
chantools_lnd "github.com/lightninglabs/chantools/lnd"
|
||||||
|
"github.com/lightningnetwork/lnd/keychain"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/tv42/zbase32"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
signedMsgPrefix = []byte("Lightning Signed Message:")
|
||||||
|
)
|
||||||
|
|
||||||
|
type signMessageCommand struct {
|
||||||
|
Msg string
|
||||||
|
|
||||||
|
rootKey *rootKey
|
||||||
|
cmd *cobra.Command
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSignMessageCommand() *cobra.Command {
|
||||||
|
cc := &signMessageCommand{}
|
||||||
|
cc.cmd = &cobra.Command{
|
||||||
|
Use: "signmessage",
|
||||||
|
Short: "Sign a message with the node's private key.",
|
||||||
|
Long: `Sign msg with the resident node's private key.
|
||||||
|
Returns the signature as a zbase32 string.`,
|
||||||
|
Example: `chantools signmessage --msg=foobar`,
|
||||||
|
RunE: cc.Execute,
|
||||||
|
}
|
||||||
|
cc.cmd.Flags().StringVar(
|
||||||
|
&cc.Msg, "msg", "", "the message to sign",
|
||||||
|
)
|
||||||
|
|
||||||
|
cc.rootKey = newRootKey(cc.cmd, "decrypting the backup")
|
||||||
|
|
||||||
|
return cc.cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *signMessageCommand) Execute(_ *cobra.Command, _ []string) error {
|
||||||
|
if c.Msg == "" {
|
||||||
|
return errors.New("please enter a valid msg")
|
||||||
|
}
|
||||||
|
|
||||||
|
extendedKey, err := c.rootKey.read()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error reading root key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signer := &chantools_lnd.Signer{
|
||||||
|
ExtendedKey: extendedKey,
|
||||||
|
ChainParams: chainParams,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the key locator for the node key.
|
||||||
|
keyLocator := keychain.KeyLocator{
|
||||||
|
Family: keychain.KeyFamilyNodeKey,
|
||||||
|
Index: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the private key for node key.
|
||||||
|
privKey, err := signer.FetchPrivateKey(&keychain.KeyDescriptor{
|
||||||
|
KeyLocator: keyLocator,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new signer.
|
||||||
|
privKeyMsgSigner := keychain.NewPrivKeyMessageSigner(
|
||||||
|
privKey, keyLocator,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Prepend the special lnd prefix.
|
||||||
|
// See: https://github.com/lightningnetwork/lnd/blob/63e698ec4990e678089533561fd95cfd684b67db/rpcserver.go#L1576 .
|
||||||
|
msg := []byte(c.Msg)
|
||||||
|
msg = append(signedMsgPrefix, msg...)
|
||||||
|
sigBytes, err := privKeyMsgSigner.SignMessageCompact(msg, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode the signature.
|
||||||
|
sig := zbase32.EncodeToString(sigBytes)
|
||||||
|
fmt.Println(sig)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,255 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
|
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||||
|
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/lightninglabs/chantools/lnd"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errNoPathFound = errors.New("no matching derivation path found")
|
||||||
|
)
|
||||||
|
|
||||||
|
type signPSBTCommand struct {
|
||||||
|
Psbt string
|
||||||
|
FromRawPsbtFile string
|
||||||
|
ToRawPsbtFile string
|
||||||
|
|
||||||
|
rootKey *rootKey
|
||||||
|
cmd *cobra.Command
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSignPSBTCommand() *cobra.Command {
|
||||||
|
cc := &signPSBTCommand{}
|
||||||
|
cc.cmd = &cobra.Command{
|
||||||
|
Use: "signpsbt",
|
||||||
|
Short: "Sign a Partially Signed Bitcoin Transaction (PSBT)",
|
||||||
|
Long: `Sign a PSBT with a master root key. The PSBT must contain
|
||||||
|
an input that is owned by the master root key.`,
|
||||||
|
Example: `chantools signpsbt \
|
||||||
|
--psbt <the_base64_encoded_psbt>
|
||||||
|
|
||||||
|
chantools signpsbt --fromrawpsbtfile <file_with_psbt>`,
|
||||||
|
RunE: cc.Execute,
|
||||||
|
}
|
||||||
|
cc.cmd.Flags().StringVar(
|
||||||
|
&cc.Psbt, "psbt", "", "Partially Signed Bitcoin Transaction "+
|
||||||
|
"to sign",
|
||||||
|
)
|
||||||
|
cc.cmd.Flags().StringVar(
|
||||||
|
&cc.FromRawPsbtFile, "fromrawpsbtfile", "", "the file containing "+
|
||||||
|
"the raw, binary encoded PSBT packet to sign",
|
||||||
|
)
|
||||||
|
cc.cmd.Flags().StringVar(
|
||||||
|
&cc.ToRawPsbtFile, "torawpsbtfile", "", "the file to write "+
|
||||||
|
"the resulting signed raw, binary encoded PSBT packet "+
|
||||||
|
"to",
|
||||||
|
)
|
||||||
|
|
||||||
|
cc.rootKey = newRootKey(cc.cmd, "signing the PSBT")
|
||||||
|
|
||||||
|
return cc.cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *signPSBTCommand) Execute(_ *cobra.Command, _ []string) error {
|
||||||
|
extendedKey, err := c.rootKey.read()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error reading root key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signer := &lnd.Signer{
|
||||||
|
ExtendedKey: extendedKey,
|
||||||
|
ChainParams: chainParams,
|
||||||
|
}
|
||||||
|
|
||||||
|
var packet *psbt.Packet
|
||||||
|
|
||||||
|
// Decode the PSBT, either from the command line or the binary file.
|
||||||
|
switch {
|
||||||
|
case c.Psbt != "":
|
||||||
|
packet, err = psbt.NewFromRawBytes(
|
||||||
|
bytes.NewReader([]byte(c.Psbt)), true,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error decoding PSBT: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
case c.FromRawPsbtFile != "":
|
||||||
|
f, err := os.Open(c.FromRawPsbtFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error opening PSBT file '%s': %w",
|
||||||
|
c.FromRawPsbtFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
packet, err = psbt.NewFromRawBytes(f, false)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error decoding PSBT from file "+
|
||||||
|
"'%s': %w", c.FromRawPsbtFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return errors.New("either the PSBT or the raw PSBT file " +
|
||||||
|
"must be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = signPsbt(extendedKey, packet, signer)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error signing PSBT: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case c.ToRawPsbtFile != "":
|
||||||
|
f, err := os.Create(c.ToRawPsbtFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating PSBT file '%s': %w",
|
||||||
|
c.ToRawPsbtFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := packet.Serialize(f); err != nil {
|
||||||
|
return fmt.Errorf("error serializing PSBT to file "+
|
||||||
|
"'%s': %w", c.ToRawPsbtFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Successfully signed PSBT and wrote it to file "+
|
||||||
|
"'%s'\n", c.ToRawPsbtFile)
|
||||||
|
|
||||||
|
default:
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := packet.Serialize(&buf); err != nil {
|
||||||
|
return fmt.Errorf("error serializing PSBT: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Successfully signed PSBT:\n\n%s\n",
|
||||||
|
base64.StdEncoding.EncodeToString(buf.Bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func signPsbt(rootKey *hdkeychain.ExtendedKey,
|
||||||
|
packet *psbt.Packet, signer *lnd.Signer) error {
|
||||||
|
|
||||||
|
for inputIndex := range packet.Inputs {
|
||||||
|
pIn := &packet.Inputs[inputIndex]
|
||||||
|
|
||||||
|
// Check that we have an input with a derivation path that
|
||||||
|
// belongs to the root key.
|
||||||
|
derivationPath, err := findMatchingDerivationPath(rootKey, pIn)
|
||||||
|
if errors.Is(err, errNoPathFound) {
|
||||||
|
log.Infof("No matching derivation path found for "+
|
||||||
|
"input %d, skipping", inputIndex)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not find matching derivation "+
|
||||||
|
"path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(derivationPath) < 5 {
|
||||||
|
return fmt.Errorf("invalid derivation path, expected "+
|
||||||
|
"at least 5 elements, got %d",
|
||||||
|
len(derivationPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
localKey, err := lnd.DeriveChildren(rootKey, derivationPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not derive local key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pIn.WitnessUtxo == nil {
|
||||||
|
return fmt.Errorf("invalid PSBT, input %d is missing "+
|
||||||
|
"witness UTXO", inputIndex)
|
||||||
|
}
|
||||||
|
utxo := pIn.WitnessUtxo
|
||||||
|
|
||||||
|
// The signing is a bit different for P2WPKH, we need to specify
|
||||||
|
// the pk script as the witness script.
|
||||||
|
var witnessScript []byte
|
||||||
|
if txscript.IsPayToWitnessPubKeyHash(utxo.PkScript) {
|
||||||
|
witnessScript = utxo.PkScript
|
||||||
|
} else {
|
||||||
|
if len(pIn.WitnessScript) == 0 {
|
||||||
|
return fmt.Errorf("invalid PSBT, input %d is "+
|
||||||
|
"missing witness script", inputIndex)
|
||||||
|
}
|
||||||
|
witnessScript = pIn.WitnessScript
|
||||||
|
}
|
||||||
|
|
||||||
|
localPrivateKey, err := localKey.ECPrivKey()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error getting private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do we already have a partial signature for our key?
|
||||||
|
localPubKey := localPrivateKey.PubKey().SerializeCompressed()
|
||||||
|
haveSig := false
|
||||||
|
for _, partialSig := range pIn.PartialSigs {
|
||||||
|
if bytes.Equal(partialSig.PubKey, localPubKey) {
|
||||||
|
haveSig = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if haveSig {
|
||||||
|
log.Infof("Already have a partial signature for input "+
|
||||||
|
"%d and local key %x, skipping", inputIndex,
|
||||||
|
localPubKey)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = signer.AddPartialSignatureForPrivateKey(
|
||||||
|
packet, localPrivateKey, utxo, witnessScript,
|
||||||
|
inputIndex,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error adding partial signature: %w",
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findMatchingDerivationPath(rootKey *hdkeychain.ExtendedKey,
|
||||||
|
pIn *psbt.PInput) ([]uint32, error) {
|
||||||
|
|
||||||
|
pubKey, err := rootKey.ECPubKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting public key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubKeyHash := btcutil.Hash160(pubKey.SerializeCompressed())
|
||||||
|
fingerprint := binary.LittleEndian.Uint32(pubKeyHash[:4])
|
||||||
|
|
||||||
|
if len(pIn.Bip32Derivation) == 0 {
|
||||||
|
return nil, errNoPathFound
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, derivation := range pIn.Bip32Derivation {
|
||||||
|
// A special case where there is only a single derivation path
|
||||||
|
// and the master key fingerprint is not set, we assume we are
|
||||||
|
// the correct signer... This might not be correct, but we have
|
||||||
|
// no way of knowing.
|
||||||
|
if derivation.MasterKeyFingerprint == 0 &&
|
||||||
|
len(pIn.Bip32Derivation) == 1 {
|
||||||
|
|
||||||
|
return derivation.Bip32Path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// The normal case, where a derivation path has the master
|
||||||
|
// fingerprint set.
|
||||||
|
if derivation.MasterKeyFingerprint == fingerprint {
|
||||||
|
return derivation.Bip32Path, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errNoPathFound
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
## chantools createwallet
|
||||||
|
|
||||||
|
Create a new lnd compatible wallet.db file from an existing seed or by generating a new one
|
||||||
|
|
||||||
|
### Synopsis
|
||||||
|
|
||||||
|
Creates a new wallet that can be used with lnd or with
|
||||||
|
chantools. The wallet can be created from an existing seed or a new one can be
|
||||||
|
generated (use --generateseed).
|
||||||
|
|
||||||
|
```
|
||||||
|
chantools createwallet [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```
|
||||||
|
chantools createwallet \
|
||||||
|
--walletdbdir ~/.lnd/data/chain/bitcoin/mainnet
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```
|
||||||
|
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
|
||||||
|
--generateseed generate a new seed instead of using an existing one
|
||||||
|
-h, --help help for createwallet
|
||||||
|
--rootkey string BIP32 HD root key of the wallet to use for creating the new wallet; leave empty to prompt for lnd 24 word aezeed
|
||||||
|
--walletdb string read the seed/master root key to use fro creating the new wallet from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
|
||||||
|
--walletdbdir string the folder to create the new wallet.db file in
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
-r, --regtest Indicates if regtest parameters should be used
|
||||||
|
-s, --signet Indicates if the public signet parameters should be used
|
||||||
|
-t, --testnet Indicates if testnet parameters should be used
|
||||||
|
```
|
||||||
|
|
||||||
|
### SEE ALSO
|
||||||
|
|
||||||
|
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels
|
||||||
|
|
@ -0,0 +1,50 @@
|
|||||||
|
## chantools pullanchor
|
||||||
|
|
||||||
|
Attempt to CPFP an anchor output of a channel
|
||||||
|
|
||||||
|
### Synopsis
|
||||||
|
|
||||||
|
Use this command to confirm a channel force close
|
||||||
|
transaction of an anchor output channel type. This will attempt to CPFP the
|
||||||
|
330 byte anchor output created for your node.
|
||||||
|
|
||||||
|
```
|
||||||
|
chantools pullanchor [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```
|
||||||
|
chantools pullanchor \
|
||||||
|
--sponsorinput txid:vout \
|
||||||
|
--anchoraddr bc1q..... \
|
||||||
|
--changeaddr bc1q..... \
|
||||||
|
--feerate 30
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```
|
||||||
|
--anchoraddr stringArray the address of the anchor output (p2wsh or p2tr output with 330 satoshis) that should be pulled; can be specified multiple times per command to pull multiple anchors with a single transaction
|
||||||
|
--apiurl string API URL to use (must be esplora compatible) (default "https://blockstream.info/api")
|
||||||
|
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
|
||||||
|
--changeaddr string the change address to send the remaining funds back to; specify 'fromseed' to derive a new address from the seed automatically
|
||||||
|
--feerate uint32 fee rate to use for the sweep transaction in sat/vByte (default 30)
|
||||||
|
-h, --help help for pullanchor
|
||||||
|
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
|
||||||
|
--sponsorinput string the input to use to sponsor the CPFP transaction; must be owned by the lnd node that owns the anchor output
|
||||||
|
--walletdb string read the seed/master root key to use fro deriving keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
-r, --regtest Indicates if regtest parameters should be used
|
||||||
|
-s, --signet Indicates if the public signet parameters should be used
|
||||||
|
-t, --testnet Indicates if testnet parameters should be used
|
||||||
|
```
|
||||||
|
|
||||||
|
### SEE ALSO
|
||||||
|
|
||||||
|
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels
|
||||||
|
|
@ -0,0 +1,41 @@
|
|||||||
|
## chantools signmessage
|
||||||
|
|
||||||
|
Sign a message with the node's private key.
|
||||||
|
|
||||||
|
### Synopsis
|
||||||
|
|
||||||
|
Sign msg with the resident node's private key.
|
||||||
|
Returns the signature as a zbase32 string.
|
||||||
|
|
||||||
|
```
|
||||||
|
chantools signmessage [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```
|
||||||
|
chantools signmessage --msg=foobar
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```
|
||||||
|
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
|
||||||
|
-h, --help help for signmessage
|
||||||
|
--msg string the message to sign
|
||||||
|
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
|
||||||
|
--walletdb string read the seed/master root key to use fro decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
-r, --regtest Indicates if regtest parameters should be used
|
||||||
|
-s, --signet Indicates if the public signet parameters should be used
|
||||||
|
-t, --testnet Indicates if testnet parameters should be used
|
||||||
|
```
|
||||||
|
|
||||||
|
### SEE ALSO
|
||||||
|
|
||||||
|
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels
|
||||||
|
|
@ -0,0 +1,46 @@
|
|||||||
|
## chantools signpsbt
|
||||||
|
|
||||||
|
Sign a Partially Signed Bitcoin Transaction (PSBT)
|
||||||
|
|
||||||
|
### Synopsis
|
||||||
|
|
||||||
|
Sign a PSBT with a master root key. The PSBT must contain
|
||||||
|
an input that is owned by the master root key.
|
||||||
|
|
||||||
|
```
|
||||||
|
chantools signpsbt [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```
|
||||||
|
chantools signpsbt \
|
||||||
|
--psbt <the_base64_encoded_psbt>
|
||||||
|
|
||||||
|
chantools signpsbt --fromrawpsbtfile <file_with_psbt>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```
|
||||||
|
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
|
||||||
|
--fromrawpsbtfile string the file containing the raw, binary encoded PSBT packet to sign
|
||||||
|
-h, --help help for signpsbt
|
||||||
|
--psbt string Partially Signed Bitcoin Transaction to sign
|
||||||
|
--rootkey string BIP32 HD root key of the wallet to use for signing the PSBT; leave empty to prompt for lnd 24 word aezeed
|
||||||
|
--torawpsbtfile string the file to write the resulting signed raw, binary encoded PSBT packet to
|
||||||
|
--walletdb string read the seed/master root key to use fro signing the PSBT from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
-r, --regtest Indicates if regtest parameters should be used
|
||||||
|
-s, --signet Indicates if the public signet parameters should be used
|
||||||
|
-t, --testnet Indicates if testnet parameters should be used
|
||||||
|
```
|
||||||
|
|
||||||
|
### SEE ALSO
|
||||||
|
|
||||||
|
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue