mirror of
https://github.com/guggero/chantools.git
synced 2026-08-13 12:33:34 +02:00
Merge pull request #193 from lightninglabs/sweepremoteclosed-cln
CLN: Make sweepremoteclosed and triggerforceclose CLN compatible
This commit is contained in:
commit
27138373ce
9 changed files with 512 additions and 107 deletions
82
README.md
82
README.md
|
|
@ -9,7 +9,9 @@
|
|||
* [Commands](#commands)
|
||||
|
||||
This tool provides helper functions that can be used to rescue funds locked in
|
||||
`lnd` channels in case `lnd` itself cannot run properly anymore.
|
||||
`lnd` channels in case `lnd` itself cannot run properly anymore (some commands
|
||||
also support Core Lightning (CLN), check [command overview](#commands) below
|
||||
for a list of compatible commands).
|
||||
|
||||
**WARNING**: This tool was specifically built for a certain rescue operation and
|
||||
might not be well-suited for your use case. Or not all edge cases for your needs
|
||||
|
|
@ -30,7 +32,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.13.4/chantools-linux-amd64-v0.13.4.tar.gz
|
||||
$ wget -O chantools.tar.gz https://github.com/lightninglabs/chantools/releases/download/v0.13.7/chantools-linux-amd64-v0.13.7.tar.gz
|
||||
$ tar -zxvf chantools.tar.gz
|
||||
$ sudo mv chantools-*/chantools /usr/local/bin/
|
||||
```
|
||||
|
|
@ -472,44 +474,46 @@ Legend:
|
|||
- :skull: Danger of loss of funds, only use when instructed to.
|
||||
- :pushpin: Command was created for a very specific version or use case and most
|
||||
likely does not apply to 99.9% of users
|
||||
- **CLN**: Command is compatible with Core Lightning (CLN), use `--hsm_secret`
|
||||
flag instead of root key or wallet.
|
||||
|
||||
| Command | Use when |
|
||||
|-------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [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 |
|
||||
| [dropchannelgraph](doc/chantools_dropchannelgraph.md) | (:warning:) Completely drop the channel graph from a `channel.db` to force re-sync |
|
||||
| [dropgraphzombies](doc/chantools_dropgraphzombies.md) | Drop all zombie channels from a `channel.db` to force a graph re-sync |
|
||||
| [dumpbackup](doc/chantools_dumpbackup.md) | :pencil: Show the content of a `channel.backup` file as text |
|
||||
| [dumpchannels](doc/chantools_dumpchannels.md) | Show the content of a `channel.db` file as text |
|
||||
| [fakechanbackup](doc/chantools_fakechanbackup.md) | :pencil: Create a fake `channel.backup` file from public information |
|
||||
| [filterbackup](doc/chantools_filterbackup.md) | :pencil: Remove a channel from a `channel.backup` file |
|
||||
| [fixoldbackup](doc/chantools_fixoldbackup.md) | :pencil: (:pushpin:) Fixes an issue with old `channel.backup` files |
|
||||
| [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 |
|
||||
| [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 |
|
||||
| [scbforceclose](doc/chantools_scbforceclose.md) | :pencil: :warning: :skull: Force close a channel using the latest state from a channel backup. EXTREMELY DANGEROUS, read help text! |
|
||||
| [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 |
|
||||
| [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]) |
|
||||
| Command | Use when |
|
||||
|-------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [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 |
|
||||
| [dropchannelgraph](doc/chantools_dropchannelgraph.md) | ( :warning: ) Completely drop the channel graph from a `channel.db` to force re-sync (not recommended while channels are open!) |
|
||||
| [dropgraphzombies](doc/chantools_dropgraphzombies.md) | Drop all zombie channels from a `channel.db` to force a graph re-sync |
|
||||
| [dumpbackup](doc/chantools_dumpbackup.md) | :pencil: Show the content of a `channel.backup` file as text |
|
||||
| [dumpchannels](doc/chantools_dumpchannels.md) | Show the content of a `channel.db` file as text |
|
||||
| [fakechanbackup](doc/chantools_fakechanbackup.md) | :pencil: Create a fake `channel.backup` file from public information |
|
||||
| [filterbackup](doc/chantools_filterbackup.md) | :pencil: Remove a channel from a `channel.backup` file |
|
||||
| [fixoldbackup](doc/chantools_fixoldbackup.md) | :pencil: ( :pushpin: ) Fixes an issue with old `channel.backup` files |
|
||||
| [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 |
|
||||
| [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 |
|
||||
| [scbforceclose](doc/chantools_scbforceclose.md) | :pencil: :warning: :skull: Force close a channel using the latest state from a channel backup. EXTREMELY DANGEROUS, read help text! |
|
||||
| [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: (**CLN**) 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: (**CLN** :pushpin: ) Request a peer to force close a channel |
|
||||
| [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: (**CLN**) Cooperatively rescue funds from channels where normal recovery is not possible (see [full guide here][zombie-recovery]) |
|
||||
|
||||
[safety]: https://github.com/lightningnetwork/lnd/blob/master/docs/safety.md
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,17 @@ import (
|
|||
)
|
||||
|
||||
type Signer struct {
|
||||
*input.MusigSessionManager
|
||||
|
||||
HsmSecret [32]byte
|
||||
|
||||
// SwapDescKeyAfterDerive is a boolean that indicates that after
|
||||
// deriving the private key from the key descriptor (which interprets
|
||||
// the public key as the peer's public key), we should swap the public
|
||||
// key in the key descriptor to the actual derived public key. This is
|
||||
// required for P2WKH signatures that need to have the public key in the
|
||||
// witness stack.
|
||||
SwapDescKeyAfterDerive bool
|
||||
}
|
||||
|
||||
func (s *Signer) SignOutputRaw(tx *wire.MsgTx,
|
||||
|
|
@ -28,9 +38,22 @@ func (s *Signer) SignOutputRaw(tx *wire.MsgTx,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if s.SwapDescKeyAfterDerive {
|
||||
// If we need to swap the public key in the descriptor, we do so
|
||||
// now. This is required for P2WKH signatures that need to have
|
||||
// the public key in the witness stack.
|
||||
signDesc.KeyDesc.PubKey = privKey.PubKey()
|
||||
}
|
||||
|
||||
return lnd.SignOutputRawWithPrivateKey(tx, signDesc, privKey)
|
||||
}
|
||||
|
||||
func (s *Signer) ComputeInputScript(_ *wire.MsgTx, _ *input.SignDescriptor) (
|
||||
*input.Script, error) {
|
||||
|
||||
return nil, errors.New("unimplemented")
|
||||
}
|
||||
|
||||
func (s *Signer) FetchPrivateKey(
|
||||
descriptor *keychain.KeyDescriptor) (*btcec.PrivateKey, error) {
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
|
|
@ -16,9 +20,13 @@ import (
|
|||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/chantools/btc"
|
||||
"github.com/lightninglabs/chantools/cln"
|
||||
"github.com/lightninglabs/chantools/lnd"
|
||||
"github.com/lightningnetwork/lnd/fn/v2"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lncfg"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
|
@ -38,6 +46,10 @@ type sweepRemoteClosedCommand struct {
|
|||
SweepAddr string
|
||||
FeeRate uint32
|
||||
|
||||
HsmSecret string
|
||||
PeerPubKeys string
|
||||
KnownOutputs string
|
||||
|
||||
rootKey *rootKey
|
||||
cmd *cobra.Command
|
||||
}
|
||||
|
|
@ -92,19 +104,36 @@ Supported remote force-closed channel types are:
|
|||
"use for the sweep transaction in sat/vByte",
|
||||
)
|
||||
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.HsmSecret, "hsm_secret", "", "the hex encoded HSM secret "+
|
||||
"to use for deriving the multisig keys for a CLN "+
|
||||
"node; obtain by running 'xxd -p -c32 "+
|
||||
"~/.lightning/bitcoin/hsm_secret'",
|
||||
)
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.PeerPubKeys, "peers", "", "comma separated list of "+
|
||||
"hex encoded public keys of the remote peers "+
|
||||
"to recover funds from, only required when using "+
|
||||
"--hsm_secret to derive the keys; can also be a file "+
|
||||
"name to a file that contains the public keys, one "+
|
||||
"per line",
|
||||
)
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.KnownOutputs, "known_outputs", "", "a comma separated "+
|
||||
"list of known output addresses to use for matching "+
|
||||
"against, instead of querying the API; can also be "+
|
||||
"a file name to a file that contains the known "+
|
||||
"outputs, one per line",
|
||||
)
|
||||
|
||||
cc.rootKey = newRootKey(cc.cmd, "sweeping the wallet")
|
||||
|
||||
return cc.cmd
|
||||
}
|
||||
|
||||
func (c *sweepRemoteClosedCommand) Execute(_ *cobra.Command, _ []string) error {
|
||||
extendedKey, err := c.rootKey.read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading root key: %w", err)
|
||||
}
|
||||
|
||||
// Make sure sweep addr is set.
|
||||
err = lnd.CheckAddress(
|
||||
err := lnd.CheckAddress(
|
||||
c.SweepAddr, chainParams, true, "sweep", lnd.AddrTypeP2WKH,
|
||||
lnd.AddrTypeP2TR,
|
||||
)
|
||||
|
|
@ -120,9 +149,123 @@ func (c *sweepRemoteClosedCommand) Execute(_ *cobra.Command, _ []string) error {
|
|||
c.FeeRate = defaultFeeSatPerVByte
|
||||
}
|
||||
|
||||
var (
|
||||
signer lnd.ChannelSigner
|
||||
estimator input.TxWeightEstimator
|
||||
knownOutputs []string
|
||||
sweepScript []byte
|
||||
targets []*targetAddr
|
||||
)
|
||||
|
||||
if c.KnownOutputs != "" {
|
||||
knownOutputs, err = listOrFile(c.KnownOutputs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading known outputs: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
for _, output := range knownOutputs {
|
||||
_, err = lnd.ParseAddress(output, chainParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing known output "+
|
||||
"address %s: %w", output, err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("Using %d known outputs for matching.",
|
||||
len(knownOutputs))
|
||||
}
|
||||
|
||||
switch {
|
||||
case c.HsmSecret != "":
|
||||
secretBytes, err := hex.DecodeString(c.HsmSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding HSM secret: %w", err)
|
||||
}
|
||||
|
||||
var hsmSecret [32]byte
|
||||
copy(hsmSecret[:], secretBytes)
|
||||
|
||||
if c.PeerPubKeys == "" {
|
||||
return errors.New("invalid peer public keys, must be " +
|
||||
"a comma separated list of hex encoded " +
|
||||
"public keys or a file name")
|
||||
}
|
||||
|
||||
var pubKeys []*btcec.PublicKey
|
||||
hexPubKeys, err := listOrFile(c.PeerPubKeys)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading peer public keys: %w",
|
||||
err)
|
||||
}
|
||||
for _, pubKeyHex := range hexPubKeys {
|
||||
pkHex, err := hex.DecodeString(pubKeyHex)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding peer "+
|
||||
"public key hex %s: %w", pubKeyHex, err)
|
||||
}
|
||||
|
||||
pk, err := btcec.ParsePubKey(pkHex)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing peer public "+
|
||||
"key hex %s: %w", pubKeyHex, err)
|
||||
}
|
||||
|
||||
pubKeys = append(pubKeys, pk)
|
||||
}
|
||||
|
||||
log.Infof("Using %d peer public keys for recovery.",
|
||||
len(pubKeys))
|
||||
|
||||
signer = &cln.Signer{
|
||||
HsmSecret: hsmSecret,
|
||||
}
|
||||
|
||||
targets, err = findTargetsCln(
|
||||
hsmSecret, pubKeys, c.APIURL, c.RecoveryWindow,
|
||||
knownOutputs,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding targets: %w", err)
|
||||
}
|
||||
|
||||
sweepScript, err = lnd.CheckAndEstimateAddress(
|
||||
c.SweepAddr, chainParams, &estimator, "sweep",
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
default:
|
||||
extendedKey, err := c.rootKey.read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading root key: %w", err)
|
||||
}
|
||||
|
||||
signer = &lnd.Signer{
|
||||
ExtendedKey: extendedKey,
|
||||
ChainParams: chainParams,
|
||||
}
|
||||
|
||||
targets, err = findTargetsLnd(
|
||||
extendedKey, c.APIURL, c.RecoveryWindow, knownOutputs,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding targets: %w", err)
|
||||
}
|
||||
|
||||
sweepScript, err = lnd.PrepareWalletAddress(
|
||||
c.SweepAddr, chainParams, &estimator, extendedKey,
|
||||
"sweep",
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return sweepRemoteClosed(
|
||||
extendedKey, c.APIURL, c.SweepAddr, c.RecoveryWindow, c.FeeRate,
|
||||
c.Publish,
|
||||
signer, &estimator, sweepScript, targets,
|
||||
newExplorerAPI(c.APIURL), c.FeeRate, c.Publish,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -135,17 +278,8 @@ type targetAddr struct {
|
|||
scriptTree *input.CommitScriptTree
|
||||
}
|
||||
|
||||
func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
|
||||
sweepAddr string, recoveryWindow uint32, feeRate uint32,
|
||||
publish bool) error {
|
||||
|
||||
var estimator input.TxWeightEstimator
|
||||
sweepScript, err := lnd.PrepareWalletAddress(
|
||||
sweepAddr, chainParams, &estimator, extendedKey, "sweep",
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
func findTargetsLnd(extendedKey *hdkeychain.ExtendedKey, apiURL string,
|
||||
recoveryWindow uint32, knownOutputs []string) ([]*targetAddr, error) {
|
||||
|
||||
var (
|
||||
targets []*targetAddr
|
||||
|
|
@ -157,17 +291,18 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
|
|||
index)
|
||||
parsedPath, err := lnd.ParsePath(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing path: %w", err)
|
||||
return nil, fmt.Errorf("error parsing path: %w", err)
|
||||
}
|
||||
|
||||
hdKey, err := lnd.DeriveChildren(extendedKey, parsedPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("eror deriving children: %w", err)
|
||||
return nil, fmt.Errorf("eror deriving children: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
privKey, err := hdKey.ECPrivKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not derive private "+
|
||||
return nil, fmt.Errorf("could not derive private "+
|
||||
"key: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -178,10 +313,10 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
|
|||
Family: keychain.KeyFamilyPaymentBase,
|
||||
Index: index,
|
||||
},
|
||||
}, api,
|
||||
}, api, knownOutputs,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not query API for "+
|
||||
return nil, fmt.Errorf("could not query API for "+
|
||||
"addresses with funds: %w", err)
|
||||
}
|
||||
targets = append(targets, foundTargets...)
|
||||
|
|
@ -193,14 +328,61 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
|
|||
api, recoveryWindow, extendedKey,
|
||||
)
|
||||
if err != nil && !errors.Is(err, errAddrNotFound) {
|
||||
return fmt.Errorf("could not check ancient channel points: %w",
|
||||
err)
|
||||
return nil, fmt.Errorf("could not check ancient channel "+
|
||||
"points: %w", err)
|
||||
}
|
||||
|
||||
if len(ancientChannelTargets) > 0 {
|
||||
targets = append(targets, ancientChannelTargets...)
|
||||
}
|
||||
|
||||
return targets, nil
|
||||
}
|
||||
|
||||
func findTargetsCln(hsmSecret [32]byte, pubKeys []*btcec.PublicKey,
|
||||
apiURL string, recoveryWindow uint32,
|
||||
knownOutputs []string) ([]*targetAddr, error) {
|
||||
|
||||
var (
|
||||
targets []*targetAddr
|
||||
api = newExplorerAPI(apiURL)
|
||||
)
|
||||
for _, pubKey := range pubKeys {
|
||||
for index := range recoveryWindow {
|
||||
desc := &keychain.KeyDescriptor{
|
||||
PubKey: pubKey,
|
||||
KeyLocator: keychain.KeyLocator{
|
||||
Family: keychain.KeyFamilyPaymentBase,
|
||||
Index: index,
|
||||
},
|
||||
}
|
||||
_, privKey, err := cln.DeriveKeyPair(hsmSecret, desc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not derive "+
|
||||
"private key: %w", err)
|
||||
}
|
||||
|
||||
foundTargets, err := queryAddressBalances(
|
||||
privKey.PubKey(), desc, api, knownOutputs,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not query API "+
|
||||
"for addresses with funds: %w", err)
|
||||
}
|
||||
targets = append(targets, foundTargets...)
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("Found %d addresses with funds to sweep.", len(targets))
|
||||
|
||||
return targets, nil
|
||||
}
|
||||
|
||||
func sweepRemoteClosed(signer lnd.ChannelSigner,
|
||||
estimator *input.TxWeightEstimator, sweepScript []byte,
|
||||
targets []*targetAddr, api *btc.ExplorerAPI, feeRate uint32,
|
||||
publish bool) error {
|
||||
|
||||
// Create estimator and transaction template.
|
||||
var (
|
||||
signDescs []*input.SignDescriptor
|
||||
|
|
@ -332,13 +514,7 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
|
|||
}}
|
||||
|
||||
// Sign the transaction now.
|
||||
var (
|
||||
signer = &lnd.Signer{
|
||||
ExtendedKey: extendedKey,
|
||||
ChainParams: chainParams,
|
||||
}
|
||||
sigHashes = txscript.NewTxSigHashes(sweepTx, prevOutFetcher)
|
||||
)
|
||||
var sigHashes = txscript.NewTxSigHashes(sweepTx, prevOutFetcher)
|
||||
for idx, desc := range signDescs {
|
||||
desc.SigHashes = sigHashes
|
||||
desc.InputIndex = idx
|
||||
|
|
@ -370,6 +546,13 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
|
|||
// P2WKH descriptor to be set to the pkScript of the
|
||||
// output...
|
||||
desc.WitnessScript = desc.Output.PkScript
|
||||
|
||||
// For CLN we need to activate a flag to make sure we
|
||||
// put the correct public key on the witness stack.
|
||||
if clnSigner, ok := signer.(*cln.Signer); ok {
|
||||
clnSigner.SwapDescKeyAfterDerive = true
|
||||
}
|
||||
|
||||
witness, err := input.CommitSpendNoDelay(
|
||||
signer, desc, sweepTx,
|
||||
len(desc.SingleTweak) == 0,
|
||||
|
|
@ -382,7 +565,7 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
|
|||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = sweepTx.Serialize(&buf)
|
||||
err := sweepTx.Serialize(&buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -404,13 +587,19 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
|
|||
}
|
||||
|
||||
func queryAddressBalances(pubKey *btcec.PublicKey,
|
||||
keyDesc *keychain.KeyDescriptor, api *btc.ExplorerAPI) ([]*targetAddr,
|
||||
error) {
|
||||
keyDesc *keychain.KeyDescriptor, api *btc.ExplorerAPI,
|
||||
knownOutputs []string) ([]*targetAddr, error) {
|
||||
|
||||
var targets []*targetAddr
|
||||
queryAddr := func(address btcutil.Address, script []byte,
|
||||
scriptTree *input.CommitScriptTree) error {
|
||||
|
||||
if len(knownOutputs) > 0 {
|
||||
if !slices.Contains(knownOutputs, address.String()) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
unspent, err := api.Unspent(address.EncodeAddress())
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not query unspent: %w", err)
|
||||
|
|
@ -580,3 +769,21 @@ func checkAncientChannelPoints(api *btc.ExplorerAPI, numKeys uint32,
|
|||
|
||||
return targets, nil
|
||||
}
|
||||
|
||||
func listOrFile(listOrPath string) ([]string, error) {
|
||||
if lnrpc.FileExists(lncfg.CleanAndExpandPath(listOrPath)) {
|
||||
contents, err := os.ReadFile(listOrPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading file %s: %w",
|
||||
listOrPath, err)
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`[,\s]+`)
|
||||
parts := re.Split(string(contents), -1)
|
||||
return fn.Filter(parts, func(s string) bool {
|
||||
return len(strings.TrimSpace(s)) > 0
|
||||
}), nil
|
||||
}
|
||||
|
||||
return strings.Split(listOrPath, ","), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -10,8 +13,12 @@ import (
|
|||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/connmgr"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/hasura/go-graphql-client"
|
||||
"github.com/lightninglabs/chantools/btc"
|
||||
"github.com/lightninglabs/chantools/cln"
|
||||
"github.com/lightninglabs/chantools/lnd"
|
||||
"github.com/lightningnetwork/lnd/brontide"
|
||||
"github.com/lightningnetwork/lnd/fn/v2"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lncfg"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
|
|
@ -30,10 +37,13 @@ type triggerForceCloseCommand struct {
|
|||
Peer string
|
||||
ChannelPoint string
|
||||
|
||||
APIURL string
|
||||
APIURL string
|
||||
AllPublicChannels bool
|
||||
|
||||
TorProxy string
|
||||
|
||||
HsmSecret string
|
||||
|
||||
rootKey *rootKey
|
||||
cmd *cobra.Command
|
||||
}
|
||||
|
|
@ -67,67 +77,197 @@ 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().BoolVar(
|
||||
&cc.AllPublicChannels, "all_public_channels", false,
|
||||
"query all public channels from the Amboss API and attempt "+
|
||||
"to trigger a force close for each of them",
|
||||
)
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.TorProxy, "torproxy", "", "SOCKS5 proxy to use for Tor "+
|
||||
"connections (to .onion addresses)",
|
||||
)
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.HsmSecret, "hsm_secret", "", "the hex encoded HSM secret "+
|
||||
"to use for deriving the node key for a CLN "+
|
||||
"node; obtain by running 'xxd -p -c32 "+
|
||||
"~/.lightning/bitcoin/hsm_secret'",
|
||||
)
|
||||
cc.rootKey = newRootKey(cc.cmd, "deriving the identity key")
|
||||
|
||||
return cc.cmd
|
||||
}
|
||||
|
||||
func (c *triggerForceCloseCommand) Execute(_ *cobra.Command, _ []string) error {
|
||||
extendedKey, err := c.rootKey.read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading root key: %w", err)
|
||||
var identityPriv *btcec.PrivateKey
|
||||
switch {
|
||||
case c.HsmSecret != "":
|
||||
secretBytes, err := hex.DecodeString(c.HsmSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding HSM secret: %w", err)
|
||||
}
|
||||
|
||||
var hsmSecret [32]byte
|
||||
copy(hsmSecret[:], secretBytes)
|
||||
|
||||
_, identityPriv, err = cln.NodeKey(hsmSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deriving identity key: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
default:
|
||||
extendedKey, err := c.rootKey.read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading root key: %w", err)
|
||||
}
|
||||
|
||||
identityPath := lnd.IdentityPath(chainParams)
|
||||
child, _, _, err := lnd.DeriveKey(
|
||||
extendedKey, identityPath, chainParams,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not derive identity key: %w",
|
||||
err)
|
||||
}
|
||||
identityPriv, err = child.ECPrivKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get identity private "+
|
||||
"key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
identityPath := lnd.IdentityPath(chainParams)
|
||||
child, pubKey, _, err := lnd.DeriveKey(
|
||||
extendedKey, identityPath, chainParams,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not derive identity key: %w", err)
|
||||
api := newExplorerAPI(c.APIURL)
|
||||
switch {
|
||||
case c.ChannelPoint != "" && c.Peer != "":
|
||||
_, err := closeChannel(
|
||||
identityPriv, api, c.ChannelPoint, c.Peer, c.TorProxy,
|
||||
)
|
||||
return err
|
||||
|
||||
case c.AllPublicChannels:
|
||||
client := graphql.NewClient(
|
||||
"https://api.amboss.space/graphql", nil,
|
||||
)
|
||||
ourNodeKey := hex.EncodeToString(
|
||||
identityPriv.PubKey().SerializeCompressed(),
|
||||
)
|
||||
|
||||
log.Infof("Fetching public channels for node %s", ourNodeKey)
|
||||
channels, err := fetchChannels(client, ourNodeKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching channels: %w", err)
|
||||
}
|
||||
|
||||
channels = fn.Filter(channels, func(c *gqChannel) bool {
|
||||
return c.ClosureInfo.ClosedHeight == 0
|
||||
})
|
||||
|
||||
log.Infof("Found %d public open channels, attempting to force "+
|
||||
"close each of them", len(channels))
|
||||
|
||||
var (
|
||||
pubKeys []string
|
||||
outputs []string
|
||||
)
|
||||
for _, openChan := range channels {
|
||||
addr := pickAddr(openChan.Node2Info.Node.Addresses)
|
||||
peerAddr := fmt.Sprintf("%s@%s", openChan.Node2, addr)
|
||||
log.Infof("Attempting to force close channel %s with "+
|
||||
"peer %s", openChan.ChanPoint, peerAddr)
|
||||
|
||||
outputAddrs, err := closeChannel(
|
||||
identityPriv, api, openChan.ChanPoint,
|
||||
peerAddr, c.TorProxy,
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("Error closing channel %s, "+
|
||||
"skipping: %v", openChan.ChanPoint, err)
|
||||
continue
|
||||
}
|
||||
|
||||
pubKeys = append(pubKeys, openChan.Node2)
|
||||
outputs = append(outputs, outputAddrs...)
|
||||
}
|
||||
|
||||
peersBytes := []byte(strings.Join(pubKeys, "\n"))
|
||||
outputsBytes := []byte(strings.Join(outputs, "\n"))
|
||||
|
||||
fileName := fmt.Sprintf("results/forceclose-peers-%s.txt",
|
||||
time.Now().Format("2006-01-02"))
|
||||
log.Infof("Writing peers to %s", fileName)
|
||||
err = os.WriteFile(fileName, peersBytes, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error writing peers to file: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
fileName = fmt.Sprintf("results/forceclose-addresses-%s.txt",
|
||||
time.Now().Format("2006-01-02"))
|
||||
log.Infof("Writing addresses to %s", fileName)
|
||||
return os.WriteFile(fileName, outputsBytes, 0644)
|
||||
|
||||
default:
|
||||
return errors.New("either --channel_point and --peer or " +
|
||||
"--all_public_channels must be specified")
|
||||
}
|
||||
identityPriv, err := child.ECPrivKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get identity private key: %w", err)
|
||||
}
|
||||
|
||||
func pickAddr(addrs []*gqAddress) string {
|
||||
// If there's only one address, we'll just return that one.
|
||||
if len(addrs) == 1 {
|
||||
return addrs[0].Address
|
||||
}
|
||||
|
||||
// We'll pick the first address that is not a Tor address.
|
||||
for _, addr := range addrs {
|
||||
if !strings.HasSuffix(addr.Address, ".onion") {
|
||||
return addr.Address
|
||||
}
|
||||
}
|
||||
|
||||
// If all addresses are Tor addresses, we'll just return the first one.
|
||||
if len(addrs) > 0 {
|
||||
return addrs[0].Address
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func closeChannel(identityPriv *btcec.PrivateKey, api *btc.ExplorerAPI,
|
||||
channelPoint, peer, torProxy string) ([]string, error) {
|
||||
|
||||
identityECDH := &keychain.PrivKeyECDH{
|
||||
PrivKey: identityPriv,
|
||||
}
|
||||
|
||||
outPoint, err := parseOutPoint(c.ChannelPoint)
|
||||
outPoint, err := parseOutPoint(channelPoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing channel point: %w", err)
|
||||
return nil, fmt.Errorf("error parsing channel point: %w", err)
|
||||
}
|
||||
|
||||
err = requestForceClose(
|
||||
c.Peer, c.TorProxy, pubKey, *outPoint, identityECDH,
|
||||
)
|
||||
err = requestForceClose(peer, torProxy, *outPoint, identityECDH)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error requesting force close: %w", err)
|
||||
return nil, fmt.Errorf("error requesting force close: %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)
|
||||
channelAddress, err := api.Address(channelPoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting channel address: %w", err)
|
||||
return nil, fmt.Errorf("error getting channel address: %w", err)
|
||||
}
|
||||
|
||||
spends, err := api.Spends(channelAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting spends: %w", err)
|
||||
return nil, 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)
|
||||
return nil, fmt.Errorf("error getting spends: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +275,11 @@ func (c *triggerForceCloseCommand) Execute(_ *cobra.Command, _ []string) error {
|
|||
log.Infof("You can now use the sweepremoteclosed command to sweep " +
|
||||
"the funds from the channel")
|
||||
|
||||
return nil
|
||||
outputAddrs := fn.Map(spends[0].Vout, func(v *btc.Vout) string {
|
||||
return v.ScriptPubkeyAddr
|
||||
})
|
||||
|
||||
return outputAddrs, nil
|
||||
}
|
||||
|
||||
func noiseDial(idKey keychain.SingleKeyECDH, lnAddr *lnwire.NetAddress,
|
||||
|
|
@ -144,8 +288,7 @@ func noiseDial(idKey keychain.SingleKeyECDH, lnAddr *lnwire.NetAddress,
|
|||
return brontide.Dial(idKey, lnAddr, timeout, netCfg.Dial)
|
||||
}
|
||||
|
||||
func connectPeer(peerHost, torProxy string, peerPubKey *btcec.PublicKey,
|
||||
identity keychain.SingleKeyECDH,
|
||||
func connectPeer(peerHost, torProxy string, identity keychain.SingleKeyECDH,
|
||||
dialTimeout time.Duration) (*peer.Brontide, error) {
|
||||
|
||||
var dialNet tor.Net = &tor.ClearNet{}
|
||||
|
|
@ -166,6 +309,8 @@ func connectPeer(peerHost, torProxy string, peerPubKey *btcec.PublicKey,
|
|||
return nil, fmt.Errorf("error parsing peer address: %w", err)
|
||||
}
|
||||
|
||||
peerPubKey := peerAddr.IdentityKey
|
||||
|
||||
log.Debugf("Attempting to dial resolved peer address %v",
|
||||
peerAddr.String())
|
||||
conn, err := noiseDial(identity, peerAddr, dialNet, dialTimeout)
|
||||
|
|
@ -198,12 +343,10 @@ func connectPeer(peerHost, torProxy string, peerPubKey *btcec.PublicKey,
|
|||
return p, nil
|
||||
}
|
||||
|
||||
func requestForceClose(peerHost, torProxy string, peerPubKey *btcec.PublicKey,
|
||||
channelPoint wire.OutPoint, identity keychain.SingleKeyECDH) error {
|
||||
func requestForceClose(peerHost, torProxy string, channelPoint wire.OutPoint,
|
||||
identity keychain.SingleKeyECDH) error {
|
||||
|
||||
p, err := connectPeer(
|
||||
peerHost, torProxy, peerPubKey, identity, dialTimeout,
|
||||
)
|
||||
p, err := connectPeer(peerHost, torProxy, identity, dialTimeout)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error connecting to peer: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,10 @@ P.S.: If you don't want to be notified about future matches, please let me know.
|
|||
`
|
||||
)
|
||||
|
||||
type gqAddress struct {
|
||||
Address string `graphql:"addr"`
|
||||
}
|
||||
|
||||
type gqChannel struct {
|
||||
ChanPoint string `graphql:"chan_point"`
|
||||
Capacity string `graphql:"capacity"`
|
||||
|
|
@ -70,6 +74,11 @@ type gqChannel struct {
|
|||
} `graphql:"closure_info"`
|
||||
Node1 string `graphql:"node1_pub"`
|
||||
Node2 string `graphql:"node2_pub"`
|
||||
Node2Info struct {
|
||||
Node struct {
|
||||
Addresses []*gqAddress `graphql:"addresses"`
|
||||
} `graphql:"node"`
|
||||
} `graphql:"node2_info"`
|
||||
ChannelID string `graphql:"long_channel_id"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ chantools sweepremoteclosed \
|
|||
--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 sweepremoteclosed
|
||||
--hsm_secret string the hex encoded HSM secret to use for deriving the multisig keys for a CLN node; obtain by running 'xxd -p -c32 ~/.lightning/bitcoin/hsm_secret'
|
||||
--peers string comma separated list of hex encoded public keys of the remote peers to recover funds from, only required when using --hsm_secret to derive the keys
|
||||
--publish publish sweep TX to the chain API instead of just printing the TX
|
||||
--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
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ chantools triggerforceclose \
|
|||
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
|
||||
--channel_point string funding transaction outpoint of the channel to trigger the force close of (<txid>:<txindex>)
|
||||
-h, --help help for triggerforceclose
|
||||
--hsm_secret string the hex encoded HSM secret to use for deriving the node key for a CLN node; obtain by running 'xxd -p -c32 ~/.lightning/bitcoin/hsm_secret'
|
||||
--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)
|
||||
|
|
|
|||
|
|
@ -477,9 +477,19 @@ func PrepareWalletAddress(addr string, chainParams *chaincfg.Params,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if estimator != nil {
|
||||
estimator.AddP2WKHOutput()
|
||||
}
|
||||
|
||||
return txscript.PayToAddrScript(p2wkhAddr)
|
||||
}
|
||||
|
||||
return CheckAndEstimateAddress(addr, chainParams, estimator, hint)
|
||||
}
|
||||
|
||||
func CheckAndEstimateAddress(addr string, chainParams *chaincfg.Params,
|
||||
estimator *input.TxWeightEstimator, hint string) ([]byte, error) {
|
||||
|
||||
parsedAddr, err := ParseAddress(addr, chainParams)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s address is invalid: %w", hint, err)
|
||||
|
|
|
|||
|
|
@ -21,9 +21,14 @@ import (
|
|||
)
|
||||
|
||||
type ChannelSigner interface {
|
||||
input.MuSig2Signer
|
||||
|
||||
SignOutputRaw(tx *wire.MsgTx,
|
||||
signDesc *input.SignDescriptor) (input.Signature, error)
|
||||
|
||||
ComputeInputScript(tx *wire.MsgTx,
|
||||
desc *input.SignDescriptor) (*input.Script, error)
|
||||
|
||||
FetchPrivateKey(descriptor *keychain.KeyDescriptor) (
|
||||
*btcec.PrivateKey, error)
|
||||
|
||||
|
|
@ -133,6 +138,7 @@ func SignOutputRawWithPrivateKey(tx *wire.MsgTx,
|
|||
// Chop off the sighash flag at the end of the signature.
|
||||
return ecdsa.ParseDERSignature(sig[:len(sig)-1])
|
||||
}
|
||||
|
||||
func (s *Signer) ComputeInputScript(_ *wire.MsgTx, _ *input.SignDescriptor) (
|
||||
*input.Script, error) {
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue