Compare commits

..

No commits in common. 'master' and 'v0.12.1' have entirely different histories.

1
.gitignore vendored

@ -1,4 +1,3 @@
.idea
/chantools
results
/chantools-v*

@ -20,13 +20,14 @@ VERSION_TAG = $(shell git describe --tags)
VERSION_CHECK = @$(call print, "Building master with date version tag")
BUILD_SYSTEM = darwin-amd64 \
darwin-arm64 \
linux-386 \
linux-amd64 \
linux-armv6 \
linux-armv7 \
linux-arm64 \
windows-amd64
windows-386 \
windows-amd64 \
windows-arm
# By default we will build all systems. But with the 'sys' tag, a specific
# system can be specified. This is useful to release for a subset of

@ -30,7 +30,7 @@ Example (make sure you always use the latest version!):
```shell
$ cd /tmp
$ wget -O chantools.tar.gz https://github.com/lightninglabs/chantools/releases/download/v0.12.2/chantools-linux-amd64-v0.12.2.tar.gz
$ wget -O chantools.tar.gz https://github.com/lightninglabs/chantools/releases/download/v0.12.0/chantools-linux-amd64-v0.12.0.tar.gz
$ tar -zxvf chantools.tar.gz
$ sudo mv chantools-*/chantools /usr/local/bin/
```
@ -107,8 +107,7 @@ Scenarios:
Another reason might be that the peer is a CLN node with a specific version
that doesn't react to force close requests normally. You can use the
[`chantools triggerforceclose` command](doc/chantools_triggerforceclose.md) in
that case (should work with CLN peers of a certain version that don't respond
to normal force close requests).
that case (ONLY works with CLN peers of a certain version).
## What should I NEVER do?
@ -187,7 +186,7 @@ compacting the DB).
in the user's home directory) is safely moved away (or the whole folder
renamed) before continuing.<br/>
To start the on-chain recovery, [follow the sub step "Starting On-Chain
Recovery" of this guide][recovery].
Recovery" of this guide][2].
Don't follow the whole guide, only this single chapter!
<br/><br/>
This step is completed once the `lncli getinfo` command shows both
@ -213,7 +212,7 @@ compacting the DB).
channels!
If the list stays un-changed for several hours, it means not all channels
could be restored using this method.
[One explanation can be found here.][safety-zombie]
[One explanation can be found here.][1]
4. **Install chantools**: To try to recover the remaining channels, we are going
to use `chantools`.
@ -411,7 +410,6 @@ Available Commands:
chanbackup Create a channel.backup file from a channel database
closepoolaccount Tries to close a Pool account that has expired
compactdb Create a copy of a channel.db file in safe/read-only mode
createwallet Create a new lnd compatible wallet.db file from an existing seed or by generating a new one
deletepayments Remove all (failed) payments from a channel DB
derivekey Derive a key with a specific derivation path
doublespendinputs Tries to double spend the given inputs by deriving the private for the address and sweeping the funds to the given address. This can only be used with inputs that belong to an lnd wallet.
@ -431,14 +429,12 @@ Available Commands:
rescuefunding Rescue funds locked in a funding multisig output that never resulted in a proper channel; this is the command the initiator of the channel needs to run
rescuetweakedkey Attempt to rescue funds locked in an address with a key that was affected by a specific bug in lnd
showrootkey Extract and show the BIP32 HD root key from the 24 word lnd aezeed
signmessage Sign a message with the nodes identity pubkey.
signpsbt Sign a Partially Signed Bitcoin Transaction (PSBT)
signrescuefunding Rescue funds locked in a funding multisig output that never resulted in a proper channel; this is the command the remote node (the non-initiator) of the channel needs to run
summary Compile a summary about the current state of channels
sweeptimelock Sweep the force-closed state after the time lock has expired
sweeptimelockmanual Sweep the force-closed state of a single channel manually if only a channel backup file is available
sweepremoteclosed Go through all the addresses that could have funds of channels that were force-closed by the remote party. A public block explorer is queried for each address and if any balance is found, all funds are swept to a given address
triggerforceclose Connect to a peer and send request to trigger a force close of the specified channel
triggerforceclose Connect to a peer and send a custom message to trigger a force close of the specified channel
vanitygen Generate a seed with a custom lnd node identity public key that starts with the given prefix
walletinfo Shows info about an lnd wallet.db file and optionally extracts the BIP32 HD root key
zombierecovery Try rescuing funds stuck in channels with zombie nodes
@ -473,7 +469,6 @@ Legend:
| [chanbackup](doc/chantools_chanbackup.md) | :pencil: Extract a `channel.backup` file from a `channel.db` file |
| [closepoolaccount](doc/chantools_closepoolaccount.md) | :pencil: Manually close an expired Lightning Pool account |
| [compactdb](doc/chantools_compactdb.md) | Run database compaction manually to reclaim space |
| [createwallet](doc/chantools_createwallet.md) | :pencil: Create a new lnd compatible wallet.db file from an existing seed or by generating a new one |
| [deletepayments](doc/chantools_deletepayments.md) | Remove ALL payments from a `channel.db` file to reduce size |
| [derivekey](doc/chantools_derivekey.md) | :pencil: Derive a single private/public key from `lnd`'s seed, use to test seed |
| [doublespendinputs](doc/chantools_doublespendinputs.md) | :pencil: Tries to double spend the given inputs by deriving the private for the address and sweeping the funds to the given address |
@ -487,20 +482,18 @@ Legend:
| [forceclose](doc/chantools_forceclose.md) | :pencil: (:skull: :warning:) Publish an old channel state from a `channel.db` file |
| [genimportscript](doc/chantools_genimportscript.md) | :pencil: Create a script/text file that can be used to import `lnd` keys into other software |
| [migratedb](doc/chantools_migratedb.md) | Upgrade the `channel.db` file to the latest version |
| [pullanchor](doc/chantools_pullanchor.md) | :pencil: Attempt to CPFP an anchor output of a channel |
| [pullanchor](doc/chantools_pullanchor.md) | :pencil: Attempt to CPFP an anchor output of a channel |
| [recoverloopin](doc/chantools_recoverloopin.md) | :pencil: Recover funds from a failed Lightning Loop inbound swap |
| [removechannel](doc/chantools_removechannel.md) | (:skull: :warning:) Remove a single channel from a `channel.db` file |
| [rescueclosed](doc/chantools_rescueclosed.md) | :pencil: (:pushpin:) Rescue funds in a legacy (pre `STATIC_REMOTE_KEY`) channel output |
| [rescuefunding](doc/chantools_rescuefunding.md) | :pencil: (:pushpin:) Rescue funds from a funding transaction. Deprecated, use [zombierecovery](doc/chantools_zombierecovery.md) instead |
| [showrootkey](doc/chantools_showrootkey.md) | :pencil: Display the master root key (`xprv`) from your seed (DO NOT SHARE WITH ANYONE) |
| [signmessage](doc/chantools_signmessage.md) | :pencil: Sign a message with the nodes identity pubkey. |
| [signpsbt](doc/chantools_signpsbt.md) | :pencil: Sign a Partially Signed Bitcoin Transaction (PSBT) |
| [signrescuefunding](doc/chantools_signrescuefunding.md) | :pencil: (:pushpin:) Sign to funds from a funding transaction. Deprecated, use [zombierecovery](doc/chantools_zombierecovery.md) instead |
| [summary](doc/chantools_summary.md) | Create a summary of channel funds from a `channel.db` file |
| [sweepremoteclosed](doc/chantools_sweepremoteclosed.md) | :pencil: Find channel funds from remotely force closed channels and sweep them |
| [sweeptimelock](doc/chantools_sweeptimelock.md) | :pencil: Sweep funds in locally force closed channels once time lock has expired (requires `channel.db`) |
| [sweeptimelockmanual](doc/chantools_sweeptimelockmanual.md) | :pencil: Manually sweep funds in a locally force closed channel where no `channel.db` file is available |
| [triggerforceclose](doc/chantools_triggerforceclose.md) | :pencil: (:pushpin:) Request a peer to force close a channel |
| [triggerforceclose](doc/chantools_triggerforceclose.md) | :pencil: (:pushpin:) Request certain CLN peers to force close a channel that don't react to normal SCB recovery requests |
| [vanitygen](doc/chantools_vanitygen.md) | Generate an `lnd` seed for a node public key that starts with a certain sequence of hex digits |
| [walletinfo](doc/chantools_walletinfo.md) | Show information from a `wallet.db` file, requires access to the wallet password |
| [zombierecovery](doc/chantools_zombierecovery.md) | :pencil: Cooperatively rescue funds from channels where normal recovery is not possible (see [full guide here][zombie-recovery]) |

@ -1,232 +0,0 @@
package main
import (
"bytes"
"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 fmt.Errorf("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 fmt.Errorf("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
}

@ -10,7 +10,6 @@ import (
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/mempool"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
@ -233,10 +232,7 @@ func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error {
// Add the inputs.
for _, outpoint := range outpoints {
tx.AddTxIn(&wire.TxIn{
PreviousOutPoint: *outpoint,
Sequence: mempool.MaxRBFSequence,
})
tx.AddTxIn(wire.NewTxIn(outpoint, nil, nil))
}
tx.AddTxOut(wire.NewTxOut(int64(totalInput-totalFee), sweepScript))

@ -2,14 +2,9 @@ package main
import (
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
"path/filepath"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
@ -19,21 +14,15 @@ import (
"github.com/lightninglabs/loop/swap"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/spf13/cobra"
)
var (
errSwapNotFound = fmt.Errorf("loop in swap not found")
)
type recoverLoopInCommand struct {
TxID string
Vout uint32
SwapHash string
SweepAddr string
OutputAmt uint64
FeeRate uint32
StartKeyIndex int
NumTries int
@ -41,8 +30,7 @@ type recoverLoopInCommand struct {
APIURL string
Publish bool
LoopDbDir string
SqliteFile string
LoopDbDir string
rootKey *rootKey
cmd *cobra.Command
@ -104,14 +92,6 @@ func newRecoverLoopInCommand() *cobra.Command {
&cc.Publish, "publish", false, "publish sweep TX to the chain "+
"API instead of just printing the TX",
)
cc.cmd.Flags().Uint64Var(
&cc.OutputAmt, "output_amt", 0, "amount of the output to sweep",
)
cc.cmd.Flags().StringVar(
&cc.SqliteFile, "sqlite_file", "", "optional path to the loop "+
"sqlite database file, if not specified, the default "+
"location will be loaded from --loop_db_dir",
)
cc.rootKey = newRootKey(cc.cmd, "deriving starting key")
@ -145,78 +125,36 @@ func (c *recoverLoopInCommand) Execute(_ *cobra.Command, _ []string) error {
}
api := newExplorerAPI(c.APIURL)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
signer := &lnd.Signer{
ExtendedKey: extendedKey,
ChainParams: chainParams,
}
// Try to fetch the swap from the boltdb.
var (
store loopdb.SwapStore
loopIn *loopdb.LoopIn
)
// First check if a boltdb file exists.
if lnrpc.FileExists(filepath.Join(c.LoopDbDir, "loop.db")) {
store, err = loopdb.NewBoltSwapStore(c.LoopDbDir, chainParams)
if err != nil {
return err
}
defer store.Close()
loopIn, err = findLoopInSwap(ctx, store, c.SwapHash)
if err != nil && !errors.Is(err, errSwapNotFound) {
return err
}
// Try to fetch the swap from the database.
store, err := loopdb.NewBoltSwapStore(c.LoopDbDir, chainParams)
if err != nil {
return err
}
defer store.Close()
// If the loopin is not found yet, try to fetch it from the sqlite db.
if loopIn == nil {
if c.SqliteFile == "" {
c.SqliteFile = filepath.Join(
c.LoopDbDir, "loop_sqlite.db",
)
}
sqliteDb, err := loopdb.NewSqliteStore(
&loopdb.SqliteConfig{
DatabaseFileName: c.SqliteFile,
SkipMigrations: true,
}, chainParams,
)
if err != nil {
return err
}
defer sqliteDb.Close()
swaps, err := store.FetchLoopInSwaps()
if err != nil {
return err
}
loopIn, err = findLoopInSwap(ctx, sqliteDb, c.SwapHash)
if err != nil && !errors.Is(err, errSwapNotFound) {
return err
var loopIn *loopdb.LoopIn
for _, s := range swaps {
if s.Hash.String() == c.SwapHash {
loopIn = s
break
}
}
// If the loopin is still not found, return an error.
if loopIn == nil {
return errSwapNotFound
}
// If the swap is an external htlc, we require the output amount to be
// set, as a lot of failure cases steam from the output amount being
// wrong.
if loopIn.Contract.ExternalHtlc && c.OutputAmt == 0 {
return fmt.Errorf("output_amt is required for external htlc")
return fmt.Errorf("swap not found")
}
fmt.Println("Loop expires at block height", loopIn.Contract.CltvExpiry)
outputValue := loopIn.Contract.AmountRequested
if c.OutputAmt != 0 {
outputValue = btcutil.Amount(c.OutputAmt)
}
// Get the swaps htlc.
htlc, err := loop.GetHtlc(
loopIn.Hash, &loopIn.Contract.SwapContract, chainParams,
@ -270,7 +208,7 @@ func (c *recoverLoopInCommand) Execute(_ *cobra.Command, _ []string) error {
// Add output for the destination address.
sweepTx.AddTxOut(&wire.TxOut{
PkScript: sweepScript,
Value: int64(outputValue) - int64(fee),
Value: int64(loopIn.Contract.AmountRequested) - int64(fee),
})
// If the htlc is version 2, we need to brute force the key locator, as
@ -280,9 +218,8 @@ func (c *recoverLoopInCommand) Execute(_ *cobra.Command, _ []string) error {
fmt.Println("Brute forcing key index...")
for i := c.StartKeyIndex; i < c.StartKeyIndex+c.NumTries; i++ {
rawTx, err = getSignedTx(
signer, sweepTx, htlc,
signer, loopIn, sweepTx, htlc,
keychain.KeyFamily(swap.KeyFamily), uint32(i),
outputValue,
)
if err == nil {
break
@ -295,10 +232,9 @@ func (c *recoverLoopInCommand) Execute(_ *cobra.Command, _ []string) error {
}
} else {
rawTx, err = getSignedTx(
signer, sweepTx, htlc,
signer, loopIn, sweepTx, htlc,
loopIn.Contract.HtlcKeys.ClientScriptKeyLocator.Family,
loopIn.Contract.HtlcKeys.ClientScriptKeyLocator.Index,
outputValue,
)
if err != nil {
return err
@ -324,14 +260,14 @@ func (c *recoverLoopInCommand) Execute(_ *cobra.Command, _ []string) error {
return nil
}
func getSignedTx(signer *lnd.Signer, sweepTx *wire.MsgTx, htlc *swap.Htlc,
keyFamily keychain.KeyFamily, keyIndex uint32,
outputValue btcutil.Amount) ([]byte, error) {
func getSignedTx(signer *lnd.Signer, loopIn *loopdb.LoopIn, sweepTx *wire.MsgTx,
htlc *swap.Htlc, keyFamily keychain.KeyFamily,
keyIndex uint32) ([]byte, error) {
// Create the sign descriptor.
prevTxOut := &wire.TxOut{
PkScript: htlc.PkScript,
Value: int64(outputValue),
Value: int64(loopIn.Contract.AmountRequested),
}
prevOutputFetcher := txscript.NewCannedPrevOutputFetcher(
prevTxOut.PkScript, prevTxOut.Value,
@ -395,23 +331,6 @@ func getSignedTx(signer *lnd.Signer, sweepTx *wire.MsgTx, htlc *swap.Htlc,
return rawTx, nil
}
func findLoopInSwap(ctx context.Context, store loopdb.SwapStore,
swapHash string) (*loopdb.LoopIn, error) {
swaps, err := store.FetchLoopInSwaps(ctx)
if err != nil {
return nil, err
}
for _, s := range swaps {
if s.Hash.String() == swapHash {
return s, nil
}
}
return nil, errSwapNotFound
}
// encodeTx encodes a tx to raw bytes.
func encodeTx(tx *wire.MsgTx) ([]byte, error) {
var buffer bytes.Buffer

@ -209,7 +209,7 @@ func (c *rescueFundingCommand) Execute(_ *cobra.Command, _ []string) error {
Index: c.LocalKeyIndex,
},
}
privKey, err := signer.FetchPrivateKey(localKeyDesc)
privKey, err := signer.FetchPrivKey(localKeyDesc)
if err != nil {
return fmt.Errorf("error deriving local key: %w", err)
}

@ -1,12 +1,14 @@
package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strings"
"syscall"
"time"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
@ -20,6 +22,7 @@ import (
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/peer"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh/terminal"
)
const (
@ -30,12 +33,12 @@ const (
// version is the current version of the tool. It is set during build.
// NOTE: When changing this, please also update the version in the
// download link shown in the README.
version = "0.13.1"
version = "0.12.1"
na = "n/a"
// lndVersion is the current version of lnd that we support. This is
// shown in some commands that affect the database and its migrations.
lndVersion = "v0.17.4-beta"
lndVersion = "v0.17.0-beta"
Commit = ""
)
@ -98,7 +101,6 @@ func main() {
rootCmd.AddCommand(
newChanBackupCommand(),
newClosePoolAccountCommand(),
newCreateWalletCommand(),
newCompactDBCommand(),
newDeletePaymentsCommand(),
newDeriveKeyCommand(),
@ -121,9 +123,7 @@ func main() {
newRescueFundingCommand(),
newRescueTweakedKeyCommand(),
newShowRootKeyCommand(),
newSignMessageCommand(),
newSignRescueFundingCommand(),
newSignPSBTCommand(),
newSummaryCommand(),
newSweepTimeLockCommand(),
newSweepTimeLockManualCommand(),
@ -141,9 +141,8 @@ func main() {
}
type rootKey struct {
RootKey string
BIP39 bool
WalletDB string
RootKey string
BIP39 bool
}
func newRootKey(cmd *cobra.Command, desc string) *rootKey {
@ -158,12 +157,6 @@ func newRootKey(cmd *cobra.Command, desc string) *rootKey {
"passphrase from the terminal instead of asking for "+
"lnd seed format or providing the --rootkey flag",
)
cmd.Flags().StringVar(
&r.WalletDB, "walletdb", "", "read the seed/master root key "+
"to use fro "+desc+" from an lnd wallet.db file "+
"instead of asking for a seed or providing the "+
"--rootkey flag",
)
return r
}
@ -186,39 +179,6 @@ func (r *rootKey) readWithBirthday() (*hdkeychain.ExtendedKey, time.Time,
extendedKey, err := btc.ReadMnemonicFromTerminal(chainParams)
return extendedKey, time.Unix(0, 0), err
case r.WalletDB != "":
wallet, pw, cleanup, err := lnd.OpenWallet(
r.WalletDB, chainParams,
)
if err != nil {
return nil, time.Unix(0, 0), fmt.Errorf("error "+
"opening wallet '%s': %w", r.WalletDB, err)
}
defer func() {
if err := cleanup(); err != nil {
log.Errorf("error closing wallet: %v", err)
}
}()
extendedKeyBytes, err := lnd.DecryptWalletRootKey(
wallet.Database(), pw,
)
if err != nil {
return nil, time.Unix(0, 0), fmt.Errorf("error "+
"decrypting wallet root key: %w", err)
}
extendedKey, err := hdkeychain.NewKeyFromString(
string(extendedKeyBytes),
)
if err != nil {
return nil, time.Unix(0, 0), fmt.Errorf("error "+
"parsing master key: %w", err)
}
return extendedKey, wallet.Manager.Birthday(), nil
default:
return lnd.ReadAezeed(chainParams)
}
@ -303,6 +263,27 @@ func readInput(input string) ([]byte, error) {
return ioutil.ReadFile(input)
}
func passwordFromConsole(userQuery string) ([]byte, error) {
// Read from terminal (if there is one).
if terminal.IsTerminal(int(syscall.Stdin)) { //nolint
fmt.Print(userQuery)
pw, err := terminal.ReadPassword(int(syscall.Stdin)) //nolint
if err != nil {
return nil, err
}
fmt.Println()
return pw, nil
}
// Read from stdin as a fallback.
reader := bufio.NewReader(os.Stdin)
pw, err := reader.ReadBytes('\n')
if err != nil {
return nil, err
}
return pw, nil
}
func setupLogging() {
setSubLogger("CHAN", log)
addSubLogger("CHDB", channeldb.UseLogger)
@ -345,6 +326,10 @@ func setSubLogger(subsystem string, logger btclog.Logger,
}
}
func noConsole() ([]byte, error) {
return nil, fmt.Errorf("wallet db requires console access")
}
func newExplorerAPI(apiURL string) *btc.ExplorerAPI {
// Override for testnet if default is used.
if apiURL == defaultAPIURL &&

@ -1,90 +0,0 @@
package main
import (
"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 fmt.Errorf("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
}

@ -1,226 +0,0 @@
package main
import (
"bytes"
"encoding/base64"
"encoding/binary"
"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"
)
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 fmt.Errorf("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 {
// Check that we have an input with a derivation path that belongs to
// the root key.
derivationPath, inputIndex, err := findMatchingDerivationPath(
rootKey, packet,
)
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 packet.Inputs[inputIndex].WitnessUtxo == nil {
return fmt.Errorf("invalid PSBT, input %d is missing witness "+
"UTXO", inputIndex)
}
utxo := packet.Inputs[inputIndex].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(packet.Inputs[inputIndex].WitnessScript) == 0 {
return fmt.Errorf("invalid PSBT, input %d is missing "+
"witness script", inputIndex)
}
witnessScript = packet.Inputs[inputIndex].WitnessScript
}
localPrivateKey, err := localKey.ECPrivKey()
if err != nil {
return fmt.Errorf("error getting private key: %w", err)
}
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,
packet *psbt.Packet) ([]uint32, int, error) {
pubKey, err := rootKey.ECPubKey()
if err != nil {
return nil, 0, fmt.Errorf("error getting public key: %w", err)
}
pubKeyHash := btcutil.Hash160(pubKey.SerializeCompressed())
fingerprint := binary.LittleEndian.Uint32(pubKeyHash[:4])
for idx, input := range packet.Inputs {
if len(input.Bip32Derivation) == 0 {
continue
}
for _, derivation := range input.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(input.Bip32Derivation) == 1 {
return derivation.Bip32Path, idx, nil
}
// The normal case, where a derivation path has the
// master fingerprint set.
if derivation.MasterKeyFingerprint == fingerprint {
return derivation.Bip32Path, idx, nil
}
}
}
return nil, 0, fmt.Errorf("no matching derivation path found")
}

@ -2,11 +2,11 @@ package main
import (
"fmt"
"net"
"strconv"
"strings"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/connmgr"
"github.com/btcsuite/btcd/wire"
@ -15,15 +15,12 @@ import (
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/peer"
"github.com/lightningnetwork/lnd/tor"
"github.com/spf13/cobra"
)
var (
dialTimeout = time.Minute
defaultTorDNSHostPort = "soa.nodes.lightning.directory:53"
)
type triggerForceCloseCommand struct {
@ -32,8 +29,6 @@ type triggerForceCloseCommand struct {
APIURL string
TorProxy string
rootKey *rootKey
cmd *cobra.Command
}
@ -42,13 +37,13 @@ func newTriggerForceCloseCommand() *cobra.Command {
cc := &triggerForceCloseCommand{}
cc.cmd = &cobra.Command{
Use: "triggerforceclose",
Short: "Connect to a Lightning Network peer and send " +
"specific messages to trigger a force close of the " +
"specified channel",
Long: `Asks the specified remote peer to force close a specific
channel by first sending a channel re-establish message, and if that doesn't
work, a custom error message (in case the peer is a specific version of CLN that
does not properly respond to a Data Loss Protection re-establish message).'`,
Short: "Connect to a CLN peer and send a custom message to " +
"trigger a force close of the specified channel",
Long: `Certain versions of CLN didn't properly react to error
messages sent by peers and therefore didn't follow the DLP protocol to recover
channel funds using SCB. This command can be used to trigger a force close with
those earlier versions of CLN (this command will not work for lnd peers or CLN
peers of a different version).`,
Example: `chantools triggerforceclose \
--peer 03abce...@xx.yy.zz.aa:9735 \
--channel_point abcdef01234...:x`,
@ -67,10 +62,6 @@ does not properly respond to a Data Loss Protection re-establish message).'`,
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
"be esplora compatible)",
)
cc.cmd.Flags().StringVar(
&cc.TorProxy, "torproxy", "", "SOCKS5 proxy to use for Tor "+
"connections (to .onion addresses)",
)
cc.rootKey = newRootKey(cc.cmd, "deriving the identity key")
return cc.cmd
@ -97,139 +88,56 @@ func (c *triggerForceCloseCommand) Execute(_ *cobra.Command, _ []string) error {
PrivKey: identityPriv,
}
outPoint, err := parseOutPoint(c.ChannelPoint)
if err != nil {
return fmt.Errorf("error parsing channel point: %w", err)
}
err = requestForceClose(
c.Peer, c.TorProxy, pubKey, outPoint, identityECDH,
peerAddr, err := lncfg.ParseLNAddressString(
c.Peer, "9735", net.ResolveTCPAddr,
)
if err != nil {
return fmt.Errorf("error requesting force close: %w", err)
return fmt.Errorf("error parsing peer address: %w", err)
}
log.Infof("Message sent, waiting for force close transaction to " +
"appear in mempool")
api := newExplorerAPI(c.APIURL)
channelAddress, err := api.Address(c.ChannelPoint)
if err != nil {
return fmt.Errorf("error getting channel address: %w", err)
}
spends, err := api.Spends(channelAddress)
outPoint, err := parseOutPoint(c.ChannelPoint)
if err != nil {
return fmt.Errorf("error getting spends: %w", err)
}
for len(spends) == 0 {
log.Infof("No spends found yet, waiting 5 seconds...")
time.Sleep(5 * time.Second)
spends, err = api.Spends(channelAddress)
if err != nil {
return fmt.Errorf("error getting spends: %w", err)
}
}
log.Infof("Found force close transaction %v", spends[0].TXID)
log.Infof("You can now use the sweepremoteclosed command to sweep " +
"the funds from the channel")
return nil
}
func noiseDial(idKey keychain.SingleKeyECDH, lnAddr *lnwire.NetAddress,
netCfg tor.Net, timeout time.Duration) (*brontide.Conn, error) {
return brontide.Dial(idKey, lnAddr, timeout, netCfg.Dial)
}
func connectPeer(peerHost, torProxy string, peerPubKey *btcec.PublicKey,
identity keychain.SingleKeyECDH,
dialTimeout time.Duration) (*peer.Brontide, error) {
var dialNet tor.Net = &tor.ClearNet{}
if torProxy != "" {
dialNet = &tor.ProxyNet{
SOCKS: torProxy,
DNS: defaultTorDNSHostPort,
StreamIsolation: false,
SkipProxyForClearNetTargets: true,
}
return fmt.Errorf("error parsing channel point: %w", err)
}
channelID := lnwire.NewChanIDFromOutPoint(outPoint)
log.Debugf("Attempting to resolve peer address %v", peerHost)
peerAddr, err := lncfg.ParseLNAddressString(
peerHost, "9735", dialNet.ResolveTCPAddr,
conn, err := noiseDial(
identityECDH, peerAddr, &tor.ClearNet{}, dialTimeout,
)
if err != nil {
return nil, fmt.Errorf("error parsing peer address: %w", err)
return fmt.Errorf("error dialing peer: %w", err)
}
log.Debugf("Attempting to dial resolved peer address %v",
peerAddr.String())
conn, err := noiseDial(identity, peerAddr, dialNet, dialTimeout)
if err != nil {
return nil, fmt.Errorf("error dialing peer: %w", err)
}
log.Infof("Attempting to establish p2p connection to peer %x, dial"+
"timeout is %v", peerPubKey.SerializeCompressed(), dialTimeout)
log.Infof("Attempting to connect to peer %x, dial timeout is %v",
pubKey.SerializeCompressed(), dialTimeout)
req := &connmgr.ConnReq{
Addr: peerAddr,
Permanent: false,
}
p, err := lnd.ConnectPeer(conn, req, chainParams, identity)
p, err := lnd.ConnectPeer(conn, req, chainParams, identityECDH)
if err != nil {
return nil, fmt.Errorf("error connecting to peer: %w", err)
return fmt.Errorf("error connecting to peer: %w", err)
}
log.Infof("Connection established to peer %x",
peerPubKey.SerializeCompressed())
pubKey.SerializeCompressed())
// We'll wait until the peer is active.
select {
case <-p.ActiveSignal():
case <-p.QuitSignal():
return nil, fmt.Errorf("peer %x disconnected",
peerPubKey.SerializeCompressed())
return fmt.Errorf("peer %x disconnected",
pubKey.SerializeCompressed())
}
return p, nil
}
func requestForceClose(peerHost, torProxy string, peerPubKey *btcec.PublicKey,
channelPoint *wire.OutPoint, identity keychain.SingleKeyECDH) error {
p, err := connectPeer(
peerHost, torProxy, peerPubKey, identity, dialTimeout,
)
if err != nil {
return fmt.Errorf("error connecting to peer: %w", err)
}
channelID := lnwire.NewChanIDFromOutPoint(channelPoint)
// Channel ID (32 byte) + u16 for the data length (which will be 0).
data := make([]byte, 34)
copy(data[:32], channelID[:])
log.Infof("Sending channel re-establish to peer to trigger force "+
"close of channel %v", channelPoint)
err = p.SendMessageLazy(true, &lnwire.ChannelReestablish{
ChanID: channelID,
})
if err != nil {
return err
}
log.Infof("Sending channel error message to peer to trigger force "+
"close of channel %v", channelPoint)
"close of channel %v", c.ChannelPoint)
_ = lnwire.SetCustomOverrides([]uint16{
lnwire.MsgError, lnwire.MsgChannelReestablish,
})
_ = lnwire.SetCustomOverrides([]uint16{lnwire.MsgError})
msg, err := lnwire.NewCustom(lnwire.MsgError, data)
if err != nil {
return err
@ -240,9 +148,41 @@ func requestForceClose(peerHost, torProxy string, peerPubKey *btcec.PublicKey,
return fmt.Errorf("error sending message: %w", err)
}
log.Infof("Message sent, waiting for force close transaction to " +
"appear in mempool")
api := newExplorerAPI(c.APIURL)
channelAddress, err := api.Address(c.ChannelPoint)
if err != nil {
return fmt.Errorf("error getting channel address: %w", err)
}
spends, err := api.Spends(channelAddress)
if err != nil {
return fmt.Errorf("error getting spends: %w", err)
}
for len(spends) == 0 {
log.Infof("No spends found yet, waiting 5 seconds...")
time.Sleep(5 * time.Second)
spends, err = api.Spends(channelAddress)
if err != nil {
return fmt.Errorf("error getting spends: %w", err)
}
}
log.Infof("Found force close transaction %v", spends[0].TXID)
log.Infof("You can now use the sweepremoteclosed command to sweep " +
"the funds from the channel")
return nil
}
func noiseDial(idKey keychain.SingleKeyECDH, lnAddr *lnwire.NetAddress,
netCfg tor.Net, timeout time.Duration) (*brontide.Conn, error) {
return brontide.Dial(idKey, lnAddr, timeout, netCfg.Dial)
}
func parseOutPoint(s string) (*wire.OutPoint, error) {
split := strings.Split(s, ":")
if len(split) != 2 || len(split[0]) == 0 || len(split[1]) == 0 {

@ -1,19 +1,28 @@
package main
import (
"errors"
"fmt"
"os"
"strings"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcwallet/snacl"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/wallet"
"github.com/btcsuite/btcwallet/walletdb"
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/spf13/cobra"
"go.etcd.io/bbolt"
)
const (
passwordEnvName = "WALLET_PASSWORD"
walletInfoFormat = `
Identity Pubkey: %x
BIP32 HD extended root key: %s
@ -29,7 +38,19 @@ Scope: m/%d'/%d'
)
var (
defaultAccount = uint32(waddrmgr.DefaultAccountNum)
// Namespace from github.com/btcsuite/btcwallet/wallet/wallet.go.
waddrmgrNamespaceKey = []byte("waddrmgr")
// Bucket names from github.com/btcsuite/btcwallet/waddrmgr/db.go.
mainBucketName = []byte("main")
masterPrivKeyName = []byte("mpriv")
cryptoPrivKeyName = []byte("cpriv")
masterHDPrivName = []byte("mhdpriv")
defaultAccount = uint32(waddrmgr.DefaultAccountNum)
openCallbacks = &waddrmgr.OpenCallbacks{
ObtainSeed: noConsole,
ObtainPrivatePass: noConsole,
}
)
type walletInfoCommand struct {
@ -77,24 +98,75 @@ or simply press <enter> without entering a password when being prompted.`,
}
func (c *walletInfoCommand) Execute(_ *cobra.Command, _ []string) error {
var (
publicWalletPw = lnwallet.DefaultPublicPassphrase
privateWalletPw = lnwallet.DefaultPrivatePassphrase
err error
)
// Check that we have a wallet DB.
if c.WalletDB == "" {
return fmt.Errorf("wallet DB is required")
}
w, privateWalletPw, cleanup, err := lnd.OpenWallet(
c.WalletDB, chainParams,
// To automate things with chantools, we also offer reading the wallet
// password from environment variables.
pw := []byte(strings.TrimSpace(os.Getenv(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:
pw, err = passwordFromConsole("Input wallet password: ")
if err != nil {
return err
}
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 load and open the wallet.
db, err := walletdb.Open(
"bdb", lncfg.CleanAndExpandPath(c.WalletDB), false,
lnd.DefaultOpenTimeout,
)
if errors.Is(err, bbolt.ErrTimeout) {
return fmt.Errorf("error opening wallet database, make sure " +
"lnd is not running and holding the exclusive lock " +
"on the wallet")
}
if err != nil {
return fmt.Errorf("error opening wallet file '%s': %w",
c.WalletDB, err)
return fmt.Errorf("error opening wallet database: %w", err)
}
defer func() { _ = db.Close() }()
defer func() {
if err := cleanup(); err != nil {
log.Errorf("error closing wallet: %v", err)
}
}()
w, err := wallet.Open(db, publicWalletPw, openCallbacks, chainParams, 0)
if err != nil {
return err
}
// Start and unlock the wallet.
w.Start()
defer w.Stop()
err = w.Unlock(privateWalletPw, nil)
if err != nil {
return err
}
// Print the wallet info and if requested the root key.
identityKey, scopeInfo, err := walletInfo(w, c.DumpAddrs)
@ -103,9 +175,7 @@ func (c *walletInfoCommand) Execute(_ *cobra.Command, _ []string) error {
}
rootKey := na
if c.WithRootKey {
masterHDPrivKey, err := lnd.DecryptWalletRootKey(
w.Database(), privateWalletPw,
)
masterHDPrivKey, err := decryptRootKey(db, privateWalletPw)
if err != nil {
return err
}
@ -189,7 +259,7 @@ func walletInfo(w *wallet.Wallet, dumpAddrs bool) (*btcec.PublicKey, string,
err = walletdb.View(
w.Database(), func(tx walletdb.ReadTx) error {
waddrmgrNs := tx.ReadBucket(
lnd.WaddrmgrNamespaceKey,
waddrmgrNamespaceKey,
)
return mgr.ForEachAccountAddress(
@ -234,3 +304,64 @@ func printScopeInfo(name string, w *wallet.Wallet,
return scopeInfo, nil
}
func decryptRootKey(db walletdb.DB, privPassphrase []byte) ([]byte, error) {
// Step 1: Load the encryption parameters and encrypted keys from the
// database.
var masterKeyPrivParams []byte
var cryptoKeyPrivEnc []byte
var masterHDPrivEnc []byte
err := walletdb.View(db, func(tx walletdb.ReadTx) error {
ns := tx.ReadBucket(waddrmgrNamespaceKey)
if ns == nil {
return fmt.Errorf("namespace '%s' does not exist",
waddrmgrNamespaceKey)
}
mainBucket := ns.NestedReadBucket(mainBucketName)
if mainBucket == nil {
return fmt.Errorf("bucket '%s' does not exist",
mainBucketName)
}
val := mainBucket.Get(masterPrivKeyName)
if val != nil {
masterKeyPrivParams = make([]byte, len(val))
copy(masterKeyPrivParams, val)
}
val = mainBucket.Get(cryptoPrivKeyName)
if val != nil {
cryptoKeyPrivEnc = make([]byte, len(val))
copy(cryptoKeyPrivEnc, val)
}
val = mainBucket.Get(masterHDPrivName)
if val != nil {
masterHDPrivEnc = make([]byte, len(val))
copy(masterHDPrivEnc, val)
}
return nil
})
if err != nil {
return nil, err
}
// Step 2: Unmarshal the master private key parameters and derive
// key from passphrase.
var masterKeyPriv snacl.SecretKey
if err := masterKeyPriv.Unmarshal(masterKeyPrivParams); err != nil {
return nil, err
}
if err := masterKeyPriv.DeriveKey(&privPassphrase); err != nil {
return nil, err
}
// Step 3: Decrypt the keys in the correct order.
cryptoKeyPriv := &snacl.CryptoKey{}
cryptoKeyPrivBytes, err := masterKeyPriv.Decrypt(cryptoKeyPrivEnc)
if err != nil {
return nil, err
}
copy(cryptoKeyPriv[:], cryptoKeyPrivBytes)
return cryptoKeyPriv.Decrypt(masterHDPrivEnc)
}

@ -3,7 +3,6 @@ package main
import (
"testing"
"github.com/lightninglabs/chantools/lnd"
"github.com/stretchr/testify/require"
)
@ -21,7 +20,7 @@ func TestWalletInfo(t *testing.T) {
WithRootKey: true,
}
t.Setenv(lnd.PasswordEnvName, testPassPhrase)
t.Setenv(passwordEnvName, testPassPhrase)
err := info.Execute(nil, nil)
require.NoError(t, err)

@ -6,6 +6,7 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
@ -27,8 +28,6 @@ type zombieRecoveryMakeOfferCommand struct {
Node2 string
FeeRate uint32
MatchOnly bool
rootKey *rootKey
cmd *cobra.Command
}
@ -65,10 +64,6 @@ a counter offer.`,
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
"use for the sweep transaction in sat/vByte",
)
cc.cmd.Flags().BoolVar(
&cc.MatchOnly, "matchonly", false, "only match the keys, "+
"don't create an offer",
)
cc.rootKey = newRootKey(cc.cmd, "signing the offer")
@ -87,12 +82,12 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command,
c.FeeRate = defaultFeeSatPerVByte
}
node1Bytes, err := os.ReadFile(c.Node1)
node1Bytes, err := ioutil.ReadFile(c.Node1)
if err != nil {
return fmt.Errorf("error reading node1 key file %s: %w",
c.Node1, err)
}
node2Bytes, err := os.ReadFile(c.Node2)
node2Bytes, err := ioutil.ReadFile(c.Node2)
if err != nil {
return fmt.Errorf("error reading node2 key file %s: %w",
c.Node2, err)
@ -158,22 +153,6 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command,
}
}
// If we're only matching, we can stop here.
if c.MatchOnly {
ourPubKeys, err := parseKeys(keys1.Node1.MultisigKeys)
if err != nil {
return fmt.Errorf("error parsing their keys: %w", err)
}
theirPubKeys, err := parseKeys(keys2.Node2.MultisigKeys)
if err != nil {
return fmt.Errorf("error parsing our keys: %w", err)
}
return matchKeys(
keys1.Channels, ourPubKeys, theirPubKeys, chainParams,
)
}
// Make sure one of the nodes is ours.
_, pubKey, _, err := lnd.DeriveKey(
extendedKey, lnd.IdentityPath(chainParams), chainParams,
@ -227,19 +206,52 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command,
return fmt.Errorf("payout address missing")
}
ourPubKeys, err := parseKeys(ourKeys)
if err != nil {
return fmt.Errorf("error parsing their keys: %w", err)
ourPubKeys := make([]*btcec.PublicKey, len(ourKeys))
theirPubKeys := make([]*btcec.PublicKey, len(theirKeys))
for idx, pubKeyHex := range ourKeys {
ourPubKeys[idx], err = pubKeyFromHex(pubKeyHex)
if err != nil {
return fmt.Errorf("error parsing our pubKey: %w", err)
}
}
theirPubKeys, err := parseKeys(theirKeys)
if err != nil {
return fmt.Errorf("error parsing our keys: %w", err)
for idx, pubKeyHex := range theirKeys {
theirPubKeys[idx], err = pubKeyFromHex(pubKeyHex)
if err != nil {
return fmt.Errorf("error parsing their pubKey: %w", err)
}
}
err = matchKeys(keys1.Channels, ourPubKeys, theirPubKeys, chainParams)
if err != nil {
return err
// Loop through all channels and all keys now, this will definitely take
// a while.
channelLoop:
for _, channel := range keys1.Channels {
for ourKeyIndex, ourKey := range ourPubKeys {
for _, theirKey := range theirPubKeys {
match, witnessScript, err := matchScript(
channel.Address, ourKey, theirKey,
chainParams,
)
if err != nil {
return fmt.Errorf("error matching "+
"keys to script: %w", err)
}
if match {
channel.ourKeyIndex = uint32(ourKeyIndex)
channel.ourKey = ourKey
channel.theirKey = theirKey
channel.witnessScript = witnessScript
log.Infof("Found keys for channel %s",
channel.ChanPoint)
continue channelLoop
}
}
}
return fmt.Errorf("didn't find matching multisig keys for "+
"channel %s", channel.ChanPoint)
}
// Let's now sum up the tally of how much of the rescued funds should
@ -369,8 +381,10 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command,
return fmt.Errorf("error creating PSBT from TX: %w", err)
}
// First we add the necessary information to the psbt package so that
// we can sign the transaction with SIGHASH_ALL.
signer := &lnd.Signer{
ExtendedKey: extendedKey,
ChainParams: chainParams,
}
for idx, txIn := range inputs {
channel := keys1.Channels[idx]
@ -397,16 +411,6 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command,
Value: channel.theirKey.SerializeCompressed(),
},
)
}
// Loop a second time through the inputs and sign each input. We now
// have all the witness/nonwitness data filled in the psbt package.
signer := &lnd.Signer{
ExtendedKey: extendedKey,
ChainParams: chainParams,
}
for idx, txIn := range inputs {
channel := keys1.Channels[idx]
keyDesc := keychain.KeyDescriptor{
PubKey: channel.ourKey,
@ -440,64 +444,6 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command,
return nil
}
// parseKeys parses a list of string keys into public keys.
func parseKeys(keys []string) ([]*btcec.PublicKey, error) {
pubKeys := make([]*btcec.PublicKey, 0, len(keys))
for _, key := range keys {
pubKey, err := pubKeyFromHex(key)
if err != nil {
return nil, err
}
pubKeys = append(pubKeys, pubKey)
}
return pubKeys, nil
}
// matchKeys tries to match the keys from the two nodes. It updates the channels
// with the correct keys and witness scripts.
func matchKeys(channels []*channel, ourPubKeys, theirPubKeys []*btcec.PublicKey,
chainParams *chaincfg.Params) error {
// Loop through all channels and all keys now, this will definitely take
// a while.
channelLoop:
for _, channel := range channels {
for ourKeyIndex, ourKey := range ourPubKeys {
for _, theirKey := range theirPubKeys {
match, witnessScript, err := matchScript(
channel.Address, ourKey, theirKey,
chainParams,
)
if err != nil {
return fmt.Errorf("error matching "+
"keys to script: %w", err)
}
if match {
channel.ourKeyIndex = uint32(ourKeyIndex)
channel.ourKey = ourKey
channel.theirKey = theirKey
channel.witnessScript = witnessScript
log.Infof("Found keys for channel %s: "+
"our key %x, their key %x",
channel.ChanPoint,
ourKey.SerializeCompressed(),
theirKey.SerializeCompressed())
continue channelLoop
}
}
}
return fmt.Errorf("didn't find matching multisig keys for "+
"channel %s", channel.ChanPoint)
}
return nil
}
func matchScript(address string, key1, key2 *btcec.PublicKey,
params *chaincfg.Params) (bool, []byte, error) {

@ -6,7 +6,6 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"time"
"github.com/lightninglabs/chantools/lnd"
@ -21,8 +20,6 @@ type zombieRecoveryPrepareKeysCommand struct {
MatchFile string
PayoutAddr string
NumKeys uint32
rootKey *rootKey
cmd *cobra.Command
}
@ -50,12 +47,7 @@ correct ones for the matched channels.`,
cc.cmd.Flags().StringVar(
&cc.PayoutAddr, "payout_addr", "", "the address where this "+
"node's rescued funds should be sent to, must be a "+
"P2WPKH (native SegWit) address",
)
cc.cmd.Flags().Uint32Var(
&cc.NumKeys, "num_keys", numMultisigKeys, "the number of "+
"multisig keys to derive",
)
"P2WPKH (native SegWit) address")
cc.rootKey = newRootKey(cc.cmd, "deriving the multisig keys")
@ -116,9 +108,9 @@ func (c *zombieRecoveryPrepareKeysCommand) Execute(_ *cobra.Command,
}
// Derive all 2500 keys now, this might take a while.
for index := uint32(0); index < c.NumKeys; index++ {
for index := 0; index < numMultisigKeys; index++ {
_, pubKey, _, err := lnd.DeriveKey(
extendedKey, lnd.MultisigPath(chainParams, int(index)),
extendedKey, lnd.MultisigPath(chainParams, index),
chainParams,
)
if err != nil {
@ -142,5 +134,5 @@ func (c *zombieRecoveryPrepareKeysCommand) Execute(_ *cobra.Command,
fileName := fmt.Sprintf("results/preparedkeys-%s-%s.json",
time.Now().Format("2006-01-02"), pubKeyStr)
log.Infof("Writing result to %s", fileName)
return os.WriteFile(fileName, matchBytes, 0644)
return ioutil.WriteFile(fileName, matchBytes, 0644)
}

@ -23,7 +23,6 @@ https://github.com/lightninglabs/chantools/.
* [chantools chanbackup](chantools_chanbackup.md) - Create a channel.backup file from a channel database
* [chantools closepoolaccount](chantools_closepoolaccount.md) - Tries to close a Pool account that has expired
* [chantools compactdb](chantools_compactdb.md) - Create a copy of a channel.db file in safe/read-only mode
* [chantools createwallet](chantools_createwallet.md) - Create a new lnd compatible wallet.db file from an existing seed or by generating a new one
* [chantools deletepayments](chantools_deletepayments.md) - Remove all (failed) payments from a channel DB
* [chantools derivekey](chantools_derivekey.md) - Derive a key with a specific derivation path
* [chantools doublespendinputs](chantools_doublespendinputs.md) - Replace a transaction by double spending its input
@ -44,14 +43,12 @@ https://github.com/lightninglabs/chantools/.
* [chantools rescuefunding](chantools_rescuefunding.md) - Rescue funds locked in a funding multisig output that never resulted in a proper channel; this is the command the initiator of the channel needs to run
* [chantools rescuetweakedkey](chantools_rescuetweakedkey.md) - Attempt to rescue funds locked in an address with a key that was affected by a specific bug in lnd
* [chantools showrootkey](chantools_showrootkey.md) - Extract and show the BIP32 HD root key from the 24 word lnd aezeed
* [chantools signmessage](chantools_signmessage.md) - Sign a message with the node's private key.
* [chantools signpsbt](chantools_signpsbt.md) - Sign a Partially Signed Bitcoin Transaction (PSBT)
* [chantools signrescuefunding](chantools_signrescuefunding.md) - Rescue funds locked in a funding multisig output that never resulted in a proper channel; this is the command the remote node (the non-initiator) of the channel needs to run
* [chantools summary](chantools_summary.md) - Compile a summary about the current state of channels
* [chantools sweepremoteclosed](chantools_sweepremoteclosed.md) - Go through all the addresses that could have funds of channels that were force-closed by the remote party. A public block explorer is queried for each address and if any balance is found, all funds are swept to a given address
* [chantools sweeptimelock](chantools_sweeptimelock.md) - Sweep the force-closed state after the time lock has expired
* [chantools sweeptimelockmanual](chantools_sweeptimelockmanual.md) - Sweep the force-closed state of a single channel manually if only a channel backup file is available
* [chantools triggerforceclose](chantools_triggerforceclose.md) - Connect to a Lightning Network peer and send specific messages to trigger a force close of the specified channel
* [chantools triggerforceclose](chantools_triggerforceclose.md) - Connect to a CLN peer and send a custom message to trigger a force close of the specified channel
* [chantools vanitygen](chantools_vanitygen.md) - Generate a seed with a custom lnd node identity public key that starts with the given prefix
* [chantools walletinfo](chantools_walletinfo.md) - Shows info about an lnd wallet.db file and optionally extracts the BIP32 HD root key
* [chantools zombierecovery](chantools_zombierecovery.md) - Try rescuing funds stuck in channels with zombie nodes

@ -27,7 +27,6 @@ chantools chanbackup \
-h, --help help for chanbackup
--multi_file string lnd channel.backup file to create
--rootkey string BIP32 HD root key of the wallet to use for creating the backup; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro creating the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -42,7 +42,6 @@ chantools closepoolaccount \
--publish publish sweep TX to the chain API instead of just printing the TX
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
--sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
--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

@ -1,44 +0,0 @@
## 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

@ -10,7 +10,7 @@ If only the failed payments should be deleted (and not the successful ones), the
CAUTION: Running this command will make it impossible to use the channel DB
with an older version of lnd. Downgrading is not possible and you'll need to
run lnd v0.17.4-beta or later after using this command!'
run lnd v0.17.0-beta or later after using this command!'
```
chantools deletepayments [flags]

@ -23,13 +23,12 @@ chantools derivekey --identity
### 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 derivekey
--identity derive the lnd identity_pubkey
--neuter don't output private key(s), only public key(s)
--path string BIP32 derivation path to derive; must start with "m/"
--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
--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 derivekey
--identity derive the lnd identity_pubkey
--neuter don't output private key(s), only public key(s)
--path string BIP32 derivation path to derive; must start with "m/"
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
```
### Options inherited from parent commands

@ -34,7 +34,6 @@ chantools doublespendinputs \
--recoverywindow uint32 number of keys to scan per internal/external branch; output will consist of double this amount of keys (default 2500)
--rootkey string BIP32 HD root key of the wallet to use for deriving the input keys; leave empty to prompt for lnd 24 word aezeed
--sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
--walletdb string read the seed/master root key to use fro deriving the input keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -12,7 +12,7 @@ without removing any other data.
CAUTION: Running this command will make it impossible to use the channel DB
with an older version of lnd. Downgrading is not possible and you'll need to
run lnd v0.17.4-beta or later after using this command!'
run lnd v0.17.0-beta or later after using this command!'
```
chantools dropchannelgraph [flags]

@ -12,7 +12,7 @@ be helpful to fix a graph that is out of sync with the network.
CAUTION: Running this command will make it impossible to use the channel DB
with an older version of lnd. Downgrading is not possible and you'll need to
run lnd v0.17.4-beta or later after using this command!'
run lnd v0.17.0-beta or later after using this command!'
```
chantools dropgraphzombies [flags]

@ -25,7 +25,6 @@ chantools dumpbackup \
-h, --help help for dumpbackup
--multi_file string lnd channel.backup file to dump
--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

@ -61,11 +61,10 @@ chantools fakechanbackup --from_channel_graph lncli_describegraph.json \
--channelpoint string funding transaction outpoint of the channel to rescue (<txid>:<txindex>) as it is displayed on 1ml.com
--from_channel_graph string the full LN channel graph in the JSON format that the 'lncli describegraph' returns
-h, --help help for fakechanbackup
--multi_file string the fake channel backup file to create (default "results/fake-2024-01-26-02-27-52.backup")
--multi_file string the fake channel backup file to create (default "results/fake-2023-04-11-16-33-35.backup")
--remote_node_addr string the remote node connection information in the format pubkey@host:port
--rootkey string BIP32 HD root key of the wallet to use for encrypting the backup; leave empty to prompt for lnd 24 word aezeed
--short_channel_id string the short channel ID in the format <blockheight>x<transactionindex>x<outputindex>
--walletdb string read the seed/master root key to use fro encrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -27,7 +27,6 @@ chantools filterbackup \
-h, --help help for filterbackup
--multi_file string lnd channel.backup file to filter
--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

@ -28,7 +28,6 @@ chantools fixoldbackup \
-h, --help help for fixoldbackup
--multi_file string lnd channel.backup file to fix
--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

@ -43,7 +43,6 @@ chantools forceclose \
--pendingchannels string channel input is in the format of lncli's pendingchannels format; specify '-' to read from stdin
--publish publish force-closing TX to the chain API instead of just printing the TX
--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

@ -51,7 +51,6 @@ chantools genimportscript --format bitcoin-cli \
--rescanfrom uint32 block number to rescan from; will be set automatically from the wallet birthday if the lnd 24 word aezeed is entered (default 500000)
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
--stdout write generated import script to standard out instead of writing it to a file
--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

@ -11,7 +11,7 @@ needs to read the database content.
CAUTION: Running this command will make it impossible to use the channel DB
with an older version of lnd. Downgrading is not possible and you'll need to
run lnd v0.17.4-beta or later after using this command!'
run lnd v0.17.0-beta or later after using this command!'
```
chantools migratedb [flags]

@ -33,7 +33,6 @@ chantools pullanchor \
-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

@ -27,16 +27,13 @@ chantools recoverloopin \
-h, --help help for recoverloopin
--loop_db_dir string path to the loop database directory, where the loop.db file is located
--num_tries int number of tries to try to find the correct key index (default 1000)
--output_amt uint amount of the output to sweep
--publish publish sweep TX to the chain API instead of just printing the TX
--rootkey string BIP32 HD root key of the wallet to use for deriving starting key; leave empty to prompt for lnd 24 word aezeed
--sqlite_file string optional path to the loop sqlite database file, if not specified, the default location will be loaded from --loop_db_dir
--start_key_index int start key index to try to find the correct key index
--swap_hash string swap hash of the loop in swap
--sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
--txid string transaction id of the on-chain transaction that created the HTLC
--vout uint32 output index of the on-chain transaction that created the HTLC
--walletdb string read the seed/master root key to use fro deriving starting key from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -11,7 +11,7 @@ channel was never confirmed on chain!
CAUTION: Running this command will make it impossible to use the channel DB
with an older version of lnd. Downgrading is not possible and you'll need to
run lnd v0.17.4-beta or later after using this command!
run lnd v0.17.0-beta or later after using this command!
```
chantools removechannel [flags]

@ -60,7 +60,6 @@ chantools rescueclosed --fromsummary results/summary-xxxxxx.json \
--lnd_log string the lnd log file to read to get the commit_point values when rescuing multiple channels at the same time
--pendingchannels string channel input is in the format of lncli's pendingchannels format; specify '-' to read from stdin
--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

@ -50,7 +50,6 @@ chantools rescuefunding \
--remotepubkey string in case a channel DB is not available (but perhaps a channel backup file), the remote multisig public key can be specified manually
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
--sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
--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

@ -29,7 +29,6 @@ chantools rescuetweakedkey \
--path string BIP32 derivation path to derive the starting key from; must start with "m/"
--rootkey string BIP32 HD root key of the wallet to use for deriving starting key; leave empty to prompt for lnd 24 word aezeed
--targetaddr string address the funds are locked in
--walletdb string read the seed/master root key to use fro deriving starting key from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -21,10 +21,9 @@ chantools showrootkey
### 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 showrootkey
--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
--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 showrootkey
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
```
### Options inherited from parent commands

@ -1,41 +0,0 @@
## 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

@ -1,46 +0,0 @@
## 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

@ -26,11 +26,10 @@ chantools signrescuefunding \
### 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 signrescuefunding
--psbt string Partially Signed Bitcoin Transaction that was provided by the initiator of the channel to rescue
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
--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
--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 signrescuefunding
--psbt string Partially Signed Bitcoin Transaction that was provided by the initiator of the channel to rescue
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
```
### Options inherited from parent commands

@ -41,7 +41,6 @@ chantools sweepremoteclosed \
--recoverywindow uint32 number of keys to scan per derivation path (default 200)
--rootkey string BIP32 HD root key of the wallet to use for sweeping the wallet; leave empty to prompt for lnd 24 word aezeed
--sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
--walletdb string read the seed/master root key to use fro sweeping the wallet from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -41,7 +41,6 @@ chantools sweeptimelock \
--publish publish sweep TX to the chain API instead of just printing the TX
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
--sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
--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

@ -63,7 +63,6 @@ chantools sweeptimelockmanual \
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
--sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
--timelockaddr string address of the time locked commitment output where the funds are stuck in
--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

@ -1,13 +1,14 @@
## chantools triggerforceclose
Connect to a Lightning Network peer and send specific messages to trigger a force close of the specified channel
Connect to a CLN peer and send a custom message to trigger a force close of the specified channel
### Synopsis
Asks the specified remote peer to force close a specific
channel by first sending a channel re-establish message, and if that doesn't
work, a custom error message (in case the peer is a specific version of CLN that
does not properly respond to a Data Loss Protection re-establish message).'
Certain versions of CLN didn't properly react to error
messages sent by peers and therefore didn't follow the DLP protocol to recover
channel funds using SCB. This command can be used to trigger a force close with
those earlier versions of CLN (this command will not work for lnd peers or CLN
peers of a different version).
```
chantools triggerforceclose [flags]
@ -30,8 +31,6 @@ chantools triggerforceclose \
-h, --help help for triggerforceclose
--peer string remote peer address (<pubkey>@<host>[:<port>])
--rootkey string BIP32 HD root key of the wallet to use for deriving the identity key; leave empty to prompt for lnd 24 word aezeed
--torproxy string SOCKS5 proxy to use for Tor connections (to .onion addresses)
--walletdb string read the seed/master root key to use fro deriving the identity key from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -31,11 +31,9 @@ chantools zombierecovery makeoffer \
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
--feerate uint32 fee rate to use for the sweep transaction in sat/vByte (default 30)
-h, --help help for makeoffer
--matchonly only match the keys, don't create an offer
--node1_keys string the JSON file generated in theprevious step ('preparekeys') command of node 1
--node2_keys string the JSON file generated in theprevious step ('preparekeys') command of node 2
--rootkey string BIP32 HD root key of the wallet to use for signing the offer; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro signing the offer from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -28,10 +28,8 @@ chantools zombierecovery preparekeys \
--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 preparekeys
--match_file string the match JSON file that was sent to both nodes by the match maker
--num_keys uint32 the number of multisig keys to derive (default 2500)
--payout_addr string the address where this node's rescued funds should be sent to, must be a P2WPKH (native SegWit) address
--rootkey string BIP32 HD root key of the wallet to use for deriving the multisig keys; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro deriving the multisig keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -21,11 +21,10 @@ chantools zombierecovery signoffer \
### 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 signoffer
--psbt string the base64 encoded PSBT that the other party sent as an offer to rescue funds
--rootkey string BIP32 HD root key of the wallet to use for signing the offer; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro signing the offer from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
--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 signoffer
--psbt string the base64 encoded PSBT that the other party sent as an offer to rescue funds
--rootkey string BIP32 HD root key of the wallet to use for signing the offer; leave empty to prompt for lnd 24 word aezeed
```
### Options inherited from parent commands

@ -3,13 +3,13 @@ module github.com/lightninglabs/chantools
go 1.21
require (
github.com/btcsuite/btcd v0.24.1-0.20240123000108-62e6af035ec5
github.com/btcsuite/btcd v0.23.5-0.20230905170901-80f5a0ffdf36
github.com/btcsuite/btcd/btcec/v2 v2.3.2
github.com/btcsuite/btcd/btcutil v1.1.5
github.com/btcsuite/btcd/btcutil v1.1.4-0.20230904040416-d4f519f5dc05
github.com/btcsuite/btcd/btcutil/psbt v1.1.8
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f
github.com/btcsuite/btcwallet v0.16.10-0.20240127010340-16b422a2e8bf
github.com/btcsuite/btcwallet v0.16.10-0.20230804184612-07be54bc22cf
github.com/btcsuite/btcwallet/wallet/txrules v1.2.0
github.com/btcsuite/btcwallet/walletdb v1.4.0
github.com/coreos/bbolt v1.3.3
@ -17,13 +17,13 @@ require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1
github.com/gogo/protobuf v1.3.2 // indirect
github.com/hasura/go-graphql-client v0.9.1
github.com/lightninglabs/loop v0.26.6-beta
github.com/lightninglabs/loop v0.23.0-beta
github.com/lightninglabs/pool v0.6.2-beta.0.20230329135228-c3bffb52df3a
// The current version of lnd we are compatible with, mostly affects the
// commands that touch the channel DB and has an impact on the DB schema.
// NOTE: When updating this version, make sure to also update the string in
// cmd/chantools/root.go.
github.com/lightningnetwork/lnd v0.17.4-beta
github.com/lightningnetwork/lnd v0.17.0-beta
github.com/lightningnetwork/lnd/kvdb v1.4.4
github.com/lightningnetwork/lnd/queue v1.1.1
github.com/lightningnetwork/lnd/ticker v1.1.1
@ -31,20 +31,15 @@ require (
github.com/spf13/cobra v1.1.3
github.com/stretchr/testify v1.8.4
go.etcd.io/bbolt v1.3.7
golang.org/x/crypto v0.21.0
golang.org/x/crypto v0.17.0
golang.org/x/oauth2 v0.11.0
)
require github.com/tv42/zbase32 v0.0.0-20220222190657-f76a9fc892fa
require (
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
github.com/aead/siphash v1.0.1 // indirect
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/andybalholm/brotli v1.0.3 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/btcsuite/btcwallet/wallet/txauthor v1.3.2 // indirect
github.com/btcsuite/btcwallet/wallet/txsizes v1.2.3 // indirect
@ -54,16 +49,11 @@ require (
github.com/btcsuite/winsvc v1.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/containerd/continuity v0.3.0 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect
github.com/decred/dcrd/lru v1.0.0 // indirect
github.com/docker/cli v20.10.17+incompatible // indirect
github.com/docker/docker v24.0.9+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.1 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/fergusstrange/embedded-postgres v1.10.0 // indirect
@ -71,45 +61,40 @@ require (
github.com/go-logr/logr v1.3.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
github.com/golang-migrate/migrate/v4 v4.16.1 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.5.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.14.3 // indirect
github.com/jackc/pgconn v1.14.0 // indirect
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgtype v1.14.0 // indirect
github.com/jackc/pgx/v4 v4.18.2 // indirect
github.com/jackc/pgx/v4 v4.18.1 // indirect
github.com/jessevdk/go-flags v1.4.0 // indirect
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/jrick/logrotate v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/json-iterator/go v1.1.11 // indirect
github.com/juju/loggo v0.0.0-20210728185423-eebad3a902c4 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kkdai/bstream v1.0.0 // indirect
github.com/klauspost/compress v1.16.0 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/lib/pq v1.10.7 // indirect
github.com/lightninglabs/aperture v0.1.21-beta.0.20230705004936-87bb996a4030 // indirect
github.com/lib/pq v1.10.3 // indirect
github.com/lightninglabs/aperture v0.1.20-beta // indirect
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect
github.com/lightninglabs/lndclient v0.17.4-1 // indirect
github.com/lightninglabs/loop/swapserverrpc v1.0.5 // indirect
github.com/lightninglabs/lndclient v0.16.0-10 // indirect
github.com/lightninglabs/loop/swapserverrpc v1.0.4 // indirect
github.com/lightninglabs/neutrino v0.16.0 // indirect
github.com/lightninglabs/neutrino/cache v1.1.1 // indirect
github.com/lightninglabs/pool/auctioneerrpc v1.0.7 // indirect
@ -118,26 +103,19 @@ require (
github.com/lightningnetwork/lnd/healthcheck v1.2.3 // indirect
github.com/lightningnetwork/lnd/tlv v1.1.1 // indirect
github.com/ltcsuite/ltcd v0.0.0-20191228044241-92166e412499 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mholt/archiver/v3 v3.5.0 // indirect
github.com/miekg/dns v1.1.43 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/nwaples/rardecode v1.1.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/opencontainers/runc v1.1.12 // indirect
github.com/ory/dockertest/v3 v3.10.0 // indirect
github.com/pierrec/lz4/v4 v4.1.15 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pierrec/lz4/v4 v4.1.8 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.11.1 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.30.0 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.26.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/rogpeppe/fastuuid v1.2.0 // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect
@ -149,9 +127,6 @@ require (
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
@ -164,24 +139,23 @@ require (
go.etcd.io/etcd/server/v3 v3.5.7 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 // indirect
go.opentelemetry.io/otel v1.20.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.3.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.0.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.0.1 // indirect
go.opentelemetry.io/otel/metric v1.20.0 // indirect
go.opentelemetry.io/otel/sdk v1.3.0 // indirect
go.opentelemetry.io/otel/sdk v1.0.1 // indirect
go.opentelemetry.io/otel/trace v1.20.0 // indirect
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
go.opentelemetry.io/proto/otlp v0.9.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.17.0 // indirect
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/term v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect
golang.org/x/tools v0.9.1 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect
@ -190,7 +164,7 @@ require (
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/errgo.v1 v1.0.1 // indirect
gopkg.in/macaroon-bakery.v2 v2.1.0 // indirect
gopkg.in/macaroon-bakery.v2 v2.0.1 // indirect
gopkg.in/macaroon.v2 v2.1.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect

894
go.sum

File diff suppressed because it is too large Load Diff

@ -2,7 +2,6 @@ package lnd
import (
"bufio"
"errors"
"fmt"
"os"
"regexp"
@ -12,47 +11,20 @@ import (
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcwallet/snacl"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/wallet"
"github.com/btcsuite/btcwallet/walletdb"
"github.com/lightningnetwork/lnd/aezeed"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnwallet"
"go.etcd.io/bbolt"
"golang.org/x/crypto/ssh/terminal"
)
const (
MnemonicEnvName = "AEZEED_MNEMONIC"
PassphraseEnvName = "AEZEED_PASSPHRASE"
PasswordEnvName = "WALLET_PASSWORD"
)
var (
numberDotsRegex = regexp.MustCompile(`[\d.\-\n\r\t]*`)
multipleSpaces = regexp.MustCompile(" [ ]+")
openCallbacks = &waddrmgr.OpenCallbacks{
ObtainSeed: noConsole,
ObtainPrivatePass: noConsole,
}
// Namespace from github.com/btcsuite/btcwallet/wallet/wallet.go.
WaddrmgrNamespaceKey = []byte("waddrmgr")
// Bucket names from github.com/btcsuite/btcwallet/waddrmgr/db.go.
mainBucketName = []byte("main")
masterPrivKeyName = []byte("mpriv")
cryptoPrivKeyName = []byte("cpriv")
masterHDPrivName = []byte("mhdpriv")
)
func noConsole() ([]byte, error) {
return nil, fmt.Errorf("wallet db requires console access")
}
// ReadAezeed reads an aezeed from the console or the environment variable.
func ReadAezeed(params *chaincfg.Params) (*hdkeychain.ExtendedKey, time.Time,
error) {
@ -95,32 +67,6 @@ func ReadAezeed(params *chaincfg.Params) (*hdkeychain.ExtendedKey, time.Time,
len(cipherSeedMnemonic), 24)
}
passphraseBytes, err := ReadPassphrase("doesn't have")
if err != nil {
return nil, time.Unix(0, 0), err
}
var mnemonic aezeed.Mnemonic
copy(mnemonic[:], cipherSeedMnemonic)
// If we're unable to map it back into the ciphertext, then either the
// mnemonic is wrong, or the passphrase is wrong.
cipherSeed, err := mnemonic.ToCipherSeed(passphraseBytes)
if err != nil {
return nil, time.Unix(0, 0), fmt.Errorf("failed to decrypt "+
"seed with passphrase: %w", err)
}
rootKey, err := hdkeychain.NewMaster(cipherSeed.Entropy[:], params)
if err != nil {
return nil, time.Unix(0, 0), fmt.Errorf("failed to derive " +
"master extended key")
}
return rootKey, cipherSeed.BirthdayTime(), nil
}
// ReadPassphrase reads a cipher seed passphrase from the console or the
// environment variable.
func ReadPassphrase(verb string) ([]byte, error) {
// Additionally, the user may have a passphrase, that will also need to
// be provided so the daemon can properly decipher the cipher seed.
// Try the environment variable first.
@ -139,14 +85,14 @@ func ReadPassphrase(verb string) ([]byte, error) {
// The environment variable didn't contain anything, we'll read the
// passphrase from the terminal.
case passphrase == "":
fmt.Printf("Input your cipher seed passphrase (press enter "+
"if your seed %s a passphrase): ", verb)
fmt.Printf("Input your cipher seed passphrase (press enter " +
"if your seed doesn't have a passphrase): ")
var err error
passphraseBytes, err = terminal.ReadPassword(
int(syscall.Stdin), //nolint
)
if err != nil {
return nil, err
return nil, time.Unix(0, 0), err
}
fmt.Println()
@ -155,176 +101,20 @@ func ReadPassphrase(verb string) ([]byte, error) {
passphraseBytes = []byte(passphrase)
}
return passphraseBytes, nil
}
// PasswordFromConsole reads a password from the console or stdin.
func PasswordFromConsole(userQuery string) ([]byte, error) {
// Read from terminal (if there is one).
if terminal.IsTerminal(int(syscall.Stdin)) { //nolint
fmt.Print(userQuery)
pw, err := terminal.ReadPassword(int(syscall.Stdin)) //nolint
if err != nil {
return nil, err
}
fmt.Println()
return pw, nil
}
// Read from stdin as a fallback.
reader := bufio.NewReader(os.Stdin)
pw, err := reader.ReadBytes('\n')
if err != nil {
return nil, err
}
return pw, nil
}
// OpenWallet opens a lnd compatible wallet and returns it, along with the
// private wallet password.
func OpenWallet(walletDbPath string,
chainParams *chaincfg.Params) (*wallet.Wallet, []byte, func() error,
error) {
var (
publicWalletPw = lnwallet.DefaultPublicPassphrase
privateWalletPw = lnwallet.DefaultPrivatePassphrase
err error
)
// To automate things with chantools, we also offer reading the wallet
// password from environment variables.
pw := []byte(strings.TrimSpace(os.Getenv(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:
pw, err = PasswordFromConsole("Input wallet password: ")
if err != nil {
return nil, nil, nil, err
}
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 load and open the wallet.
db, err := walletdb.Open(
"bdb", lncfg.CleanAndExpandPath(walletDbPath), false,
DefaultOpenTimeout,
)
if errors.Is(err, bbolt.ErrTimeout) {
return nil, nil, nil, fmt.Errorf("error opening wallet " +
"database, make sure lnd is not running and holding " +
"the exclusive lock on the wallet")
}
if err != nil {
return nil, nil, nil, fmt.Errorf("error opening wallet "+
"database: %w", err)
}
w, err := wallet.Open(db, publicWalletPw, openCallbacks, chainParams, 0)
if err != nil {
_ = db.Close()
return nil, nil, nil, fmt.Errorf("error opening wallet %w", err)
}
// Start and unlock the wallet.
w.Start()
err = w.Unlock(privateWalletPw, nil)
if err != nil {
w.Stop()
_ = db.Close()
return nil, nil, nil, err
}
cleanup := func() error {
w.Stop()
if err := db.Close(); err != nil {
return err
}
return nil
}
return w, privateWalletPw, cleanup, nil
}
// DecryptWalletRootKey decrypts a lnd compatible wallet's root key.
func DecryptWalletRootKey(db walletdb.DB,
privatePassphrase []byte) ([]byte, error) {
// Step 1: Load the encryption parameters and encrypted keys from the
// database.
var masterKeyPrivParams []byte
var cryptoKeyPrivEnc []byte
var masterHDPrivEnc []byte
err := walletdb.View(db, func(tx walletdb.ReadTx) error {
ns := tx.ReadBucket(WaddrmgrNamespaceKey)
if ns == nil {
return fmt.Errorf("namespace '%s' does not exist",
WaddrmgrNamespaceKey)
}
mainBucket := ns.NestedReadBucket(mainBucketName)
if mainBucket == nil {
return fmt.Errorf("bucket '%s' does not exist",
mainBucketName)
}
val := mainBucket.Get(masterPrivKeyName)
if val != nil {
masterKeyPrivParams = make([]byte, len(val))
copy(masterKeyPrivParams, val)
}
val = mainBucket.Get(cryptoPrivKeyName)
if val != nil {
cryptoKeyPrivEnc = make([]byte, len(val))
copy(cryptoKeyPrivEnc, val)
}
val = mainBucket.Get(masterHDPrivName)
if val != nil {
masterHDPrivEnc = make([]byte, len(val))
copy(masterHDPrivEnc, val)
}
var mnemonic aezeed.Mnemonic
copy(mnemonic[:], cipherSeedMnemonic)
return nil
})
// If we're unable to map it back into the ciphertext, then either the
// mnemonic is wrong, or the passphrase is wrong.
cipherSeed, err := mnemonic.ToCipherSeed(passphraseBytes)
if err != nil {
return nil, err
}
// Step 2: Unmarshal the master private key parameters and derive
// key from passphrase.
var masterKeyPriv snacl.SecretKey
if err := masterKeyPriv.Unmarshal(masterKeyPrivParams); err != nil {
return nil, err
}
if err := masterKeyPriv.DeriveKey(&privatePassphrase); err != nil {
return nil, err
return nil, time.Unix(0, 0), fmt.Errorf("failed to decrypt "+
"seed with passphrase: %w", err)
}
// Step 3: Decrypt the keys in the correct order.
cryptoKeyPriv := &snacl.CryptoKey{}
cryptoKeyPrivBytes, err := masterKeyPriv.Decrypt(cryptoKeyPrivEnc)
rootKey, err := hdkeychain.NewMaster(cipherSeed.Entropy[:], params)
if err != nil {
return nil, err
return nil, time.Unix(0, 0), fmt.Errorf("failed to derive " +
"master extended key")
}
copy(cryptoKeyPriv[:], cryptoKeyPrivBytes)
return cryptoKeyPriv.Decrypt(masterHDPrivEnc)
return rootKey, cipherSeed.BirthdayTime(), nil
}

@ -30,15 +30,15 @@ func (s *Signer) SignOutputRaw(tx *wire.MsgTx,
// First attempt to fetch the private key which corresponds to the
// specified public key.
privKey, err := s.FetchPrivateKey(&signDesc.KeyDesc)
privKey, err := s.FetchPrivKey(&signDesc.KeyDesc)
if err != nil {
return nil, err
}
return s.SignOutputRawWithPrivateKey(tx, signDesc, privKey)
return s.SignOutputRawWithPrivkey(tx, signDesc, privKey)
}
func (s *Signer) SignOutputRawWithPrivateKey(tx *wire.MsgTx,
func (s *Signer) SignOutputRawWithPrivkey(tx *wire.MsgTx,
signDesc *input.SignDescriptor,
privKey *secp256k1.PrivateKey) (input.Signature, error) {
@ -122,7 +122,7 @@ func (s *Signer) ComputeInputScript(_ *wire.MsgTx, _ *input.SignDescriptor) (
return nil, fmt.Errorf("unimplemented")
}
func (s *Signer) FetchPrivateKey(descriptor *keychain.KeyDescriptor) (
func (s *Signer) FetchPrivKey(descriptor *keychain.KeyDescriptor) (
*btcec.PrivateKey, error) {
key, err := DeriveChildren(s.ExtendedKey, []uint32{
@ -181,58 +181,6 @@ func (s *Signer) AddPartialSignature(packet *psbt.Packet,
return nil
}
func (s *Signer) AddPartialSignatureForPrivateKey(packet *psbt.Packet,
privateKey *btcec.PrivateKey, utxo *wire.TxOut, witnessScript []byte,
inputIndex int) error {
// Now we add our partial signature.
prevOutFetcher := wallet.PsbtPrevOutputFetcher(packet)
signDesc := &input.SignDescriptor{
WitnessScript: witnessScript,
Output: utxo,
InputIndex: inputIndex,
HashType: txscript.SigHashAll,
PrevOutputFetcher: prevOutFetcher,
SigHashes: txscript.NewTxSigHashes(
packet.UnsignedTx, prevOutFetcher,
),
}
ourSigRaw, err := s.SignOutputRawWithPrivateKey(
packet.UnsignedTx, signDesc, privateKey,
)
if err != nil {
return fmt.Errorf("error signing with our key: %w", err)
}
ourSig := append(ourSigRaw.Serialize(), byte(txscript.SigHashAll))
// Great, we were able to create our sig, let's add it to the PSBT.
updater, err := psbt.NewUpdater(packet)
if err != nil {
return fmt.Errorf("error creating PSBT updater: %w", err)
}
// If the witness script is the pk script for a P2WPKH output, then we
// need to blank it out for the PSBT code, otherwise it interprets it as
// a P2WSH.
if txscript.IsPayToWitnessPubKeyHash(utxo.PkScript) {
witnessScript = nil
}
status, err := updater.Sign(
inputIndex, ourSig, privateKey.PubKey().SerializeCompressed(),
nil, witnessScript,
)
if err != nil {
return fmt.Errorf("error adding signature to PSBT: %w", err)
}
if status != 0 {
return fmt.Errorf("unexpected status for signature update, "+
"got %d wanted 0", status)
}
return nil
}
// maybeTweakPrivKey examines the single tweak parameters on the passed sign
// descriptor and may perform a mapping on the passed private key in order to
// utilize the tweaks, if populated.

@ -3,7 +3,7 @@ module github.com/lightninglabs/chantools/tools
go 1.18
require (
github.com/btcsuite/btcd v0.24.0
github.com/btcsuite/btcd v0.23.4
github.com/golangci/golangci-lint v1.51.2
github.com/ory/go-acc v0.2.8
github.com/rinchsan/gosimports v0.1.5
@ -32,8 +32,8 @@ require (
github.com/breml/bidichk v0.2.3 // indirect
github.com/breml/errchkjson v0.3.0 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.1.3 // indirect
github.com/btcsuite/btcd/btcutil v1.1.5 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
github.com/btcsuite/btcd/btcutil v1.1.0 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
@ -194,7 +194,7 @@ require (
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.6.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

@ -92,20 +92,16 @@ github.com/breml/errchkjson v0.3.0 h1:YdDqhfqMT+I1vIxPSas44P+9Z9HzJwCeAzjB8PxP1x
github.com/breml/errchkjson v0.3.0/go.mod h1:9Cogkyv9gcT8HREpzi3TiqBxCqDzo8awa92zSDFcofU=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
github.com/btcsuite/btcd v0.24.0 h1:gL3uHE/IaFj6fcZSu03SvqPMSx7s/dPzfpG/atRwWdo=
github.com/btcsuite/btcd v0.24.0/go.mod h1:K4IDc1593s8jKXIF7yS7yCTSxrknB9z0STzc2j6XgE4=
github.com/btcsuite/btcd v0.23.4 h1:IzV6qqkfwbItOS/sg/aDfPDsjPP8twrCOE2R93hxMlQ=
github.com/btcsuite/btcd v0.23.4/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY=
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
github.com/btcsuite/btcd/btcec/v2 v2.1.3 h1:xM/n3yIhHAhHy04z4i43C8p4ehixJZMsnrVJkgl+MTE=
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
github.com/btcsuite/btcd/btcutil v1.1.0 h1:MO4klnGY+EWJdoWF12Wkuf4AWDBPMpZNeN/jRLrklUU=
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8=
github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
@ -324,7 +320,6 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORR
github.com/gordonklaus/ineffassign v0.0.0-20230107090616-13ace0543b28 h1:9alfqbrhuD+9fLZ4iaAVwhlp5PEhmnBt7yvK2Oy5C1U=
github.com/gordonklaus/ineffassign v0.0.0-20230107090616-13ace0543b28/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gostaticanalysis/analysisutil v0.0.3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
github.com/gostaticanalysis/analysisutil v0.1.0/go.mod h1:dMhHRU9KTiDcuLGdy87/2gTR8WruwYZrKdRq9m1O6uw=
github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk=
@ -1093,8 +1088,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

Loading…
Cancel
Save