From 3044d9f796a6cb50dfd6d8c8ca8605043686f181 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 7 Nov 2023 20:22:38 -0600 Subject: [PATCH 1/4] lnd: add ExtractChannel function --- lnd/chanbackup.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/lnd/chanbackup.go b/lnd/chanbackup.go index 5799356..21e130c 100644 --- a/lnd/chanbackup.go +++ b/lnd/chanbackup.go @@ -4,6 +4,9 @@ import ( "bytes" "fmt" + "github.com/btcsuite/btcd/btcutil/hdkeychain" + "github.com/btcsuite/btcd/chaincfg" + "github.com/lightninglabs/chantools/dump" "github.com/lightningnetwork/lnd/chanbackup" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/keychain" @@ -35,3 +38,32 @@ func CreateChannelBackup(db *channeldb.DB, multiFile *chanbackup.MultiFile, } return nil } + +// ExtractChannel extracts a single channel from the given backup file and +// returns it as a dump.BackupSingle struct. +func ExtractChannel(extendedKey *hdkeychain.ExtendedKey, + chainParams *chaincfg.Params, multiFilePath, + channelPoint string) (*dump.BackupSingle, error) { + + multiFile := chanbackup.NewMultiFile(multiFilePath) + keyRing := &HDKeyRing{ + ExtendedKey: extendedKey, + ChainParams: chainParams, + } + + multi, err := multiFile.ExtractMulti(keyRing) + if err != nil { + return nil, fmt.Errorf("could not extract multi file: %w", err) + } + + channels := dump.BackupDump(multi, chainParams) + for _, channel := range channels { + channel := channel + + if channel.FundingOutpoint == channelPoint { + return &channel, nil + } + } + + return nil, fmt.Errorf("channel %s not found in backup", channelPoint) +} From 5bc49376a330ea72a5c033c555a4c6c0d6a03d44 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 7 Nov 2023 20:24:56 -0600 Subject: [PATCH 2/4] sweeptimelock: make start CSV timeout+channels configurable --- cmd/chantools/sweeptimelock.go | 8 +++---- cmd/chantools/sweeptimelockmanual.go | 26 +++++++++++------------ cmd/chantools/sweeptimelockmanual_test.go | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/cmd/chantools/sweeptimelock.go b/cmd/chantools/sweeptimelock.go index d4cd932..8a1c98d 100644 --- a/cmd/chantools/sweeptimelock.go +++ b/cmd/chantools/sweeptimelock.go @@ -239,7 +239,7 @@ func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiURL string, ), input.DeriveRevocationPubkey( target.revocationBasePoint, target.commitPoint, - ), target.lockScript, maxCsvTimeout, + ), target.lockScript, 0, maxCsvTimeout, ) if err != nil { log.Errorf("Could not create matching script for %s "+ @@ -346,14 +346,14 @@ func pubKeyFromHex(pubKeyHex string) (*btcec.PublicKey, error) { } func bruteForceDelay(delayPubkey, revocationPubkey *btcec.PublicKey, - targetScript []byte, maxCsvTimeout uint16) (int32, []byte, []byte, - error) { + targetScript []byte, startCsvTimeout, maxCsvTimeout uint16) (int32, + []byte, []byte, error) { if len(targetScript) != 34 { return 0, nil, nil, fmt.Errorf("invalid target script: %s", targetScript) } - for i := uint16(0); i <= maxCsvTimeout; i++ { + for i := startCsvTimeout; i <= maxCsvTimeout; i++ { s, err := input.CommitScriptToSelf( uint32(i), delayPubkey, revocationPubkey, ) diff --git a/cmd/chantools/sweeptimelockmanual.go b/cmd/chantools/sweeptimelockmanual.go index ca6935c..72d1564 100644 --- a/cmd/chantools/sweeptimelockmanual.go +++ b/cmd/chantools/sweeptimelockmanual.go @@ -136,15 +136,15 @@ func (c *sweepTimeLockManualCommand) Execute(_ *cobra.Command, _ []string) error return sweepTimeLockManual( extendedKey, c.APIURL, c.SweepAddr, c.TimeLockAddr, - remoteRevPoint, c.MaxCsvLimit, c.MaxNumChansTotal, + remoteRevPoint, 0, c.MaxCsvLimit, 0, c.MaxNumChansTotal, c.MaxNumChanUpdates, c.Publish, c.FeeRate, ) } func sweepTimeLockManual(extendedKey *hdkeychain.ExtendedKey, apiURL string, sweepAddr, timeLockAddr string, remoteRevPoint *btcec.PublicKey, - maxCsvTimeout, maxNumChannels uint16, maxNumChanUpdates uint64, - publish bool, feeRate uint32) error { + startCsvTimeout, maxCsvTimeout, startNumChannels, maxNumChannels uint16, + maxNumChanUpdates uint64, publish bool, feeRate uint32) error { // First of all, we need to parse the lock addr and make sure we can // brute force the script with the information we have. If not, we can't @@ -179,10 +179,10 @@ func sweepTimeLockManual(extendedKey *hdkeychain.ExtendedKey, apiURL string, delayDesc *keychain.KeyDescriptor commitPoint *btcec.PublicKey ) - for i := uint16(0); i < maxNumChannels; i++ { + for i := startNumChannels; i < maxNumChannels; i++ { csvTimeout, script, scriptHash, commitPoint, delayDesc, err = tryKey( - baseKey, remoteRevPoint, maxCsvTimeout, lockScript, - uint32(i), maxNumChanUpdates, + baseKey, remoteRevPoint, startCsvTimeout, maxCsvTimeout, + lockScript, uint32(i), maxNumChanUpdates, ) if err == nil { @@ -305,7 +305,7 @@ func sweepTimeLockManual(extendedKey *hdkeychain.ExtendedKey, apiURL string, } func tryKey(baseKey *hdkeychain.ExtendedKey, remoteRevPoint *btcec.PublicKey, - maxCsvTimeout uint16, lockScript []byte, idx uint32, + startCsvTimeout, maxCsvTimeout uint16, lockScript []byte, idx uint32, maxNumChanUpdates uint64) (int32, []byte, []byte, *btcec.PublicKey, *keychain.KeyDescriptor, error) { @@ -338,7 +338,7 @@ func tryKey(baseKey *hdkeychain.ExtendedKey, remoteRevPoint *btcec.PublicKey, // points and CSV values. csvTimeout, script, scriptHash, commitPoint, err := bruteForceDelayPoint( delayPrivKey.PubKey(), remoteRevPoint, revRoot, lockScript, - maxCsvTimeout, maxNumChanUpdates, + startCsvTimeout, maxCsvTimeout, maxNumChanUpdates, ) if err == nil { return csvTimeout, script, scriptHash, commitPoint, @@ -403,7 +403,7 @@ func tryKey(baseKey *hdkeychain.ExtendedKey, remoteRevPoint *btcec.PublicKey, csvTimeout, script, scriptHash, commitPoint, err = bruteForceDelayPoint( delayPrivKey.PubKey(), remoteRevPoint, revRoot2, lockScript, - maxCsvTimeout, maxNumChanUpdates, + startCsvTimeout, maxCsvTimeout, maxNumChanUpdates, ) if err == nil { return csvTimeout, script, scriptHash, commitPoint, @@ -444,7 +444,7 @@ func tryKey(baseKey *hdkeychain.ExtendedKey, remoteRevPoint *btcec.PublicKey, csvTimeout, script, scriptHash, commitPoint, err = bruteForceDelayPoint( delayPrivKey.PubKey(), remoteRevPoint, revRoot3, lockScript, - maxCsvTimeout, maxNumChanUpdates, + startCsvTimeout, maxCsvTimeout, maxNumChanUpdates, ) if err == nil { return csvTimeout, script, scriptHash, commitPoint, @@ -462,8 +462,8 @@ func tryKey(baseKey *hdkeychain.ExtendedKey, remoteRevPoint *btcec.PublicKey, func bruteForceDelayPoint(delayBase, revBase *btcec.PublicKey, revRoot *shachain.RevocationProducer, lockScript []byte, - maxCsvTimeout uint16, maxChanUpdates uint64) (int32, []byte, []byte, - *btcec.PublicKey, error) { + startCsvTimeout, maxCsvTimeout uint16, maxChanUpdates uint64) (int32, + []byte, []byte, *btcec.PublicKey, error) { for i := uint64(0); i < maxChanUpdates; i++ { revPreimage, err := revRoot.AtIndex(i) @@ -475,7 +475,7 @@ func bruteForceDelayPoint(delayBase, revBase *btcec.PublicKey, csvTimeout, script, scriptHash, err := bruteForceDelay( input.TweakPubKey(delayBase, commitPoint), input.DeriveRevocationPubkey(revBase, commitPoint), - lockScript, maxCsvTimeout, + lockScript, startCsvTimeout, maxCsvTimeout, ) if err != nil { diff --git a/cmd/chantools/sweeptimelockmanual_test.go b/cmd/chantools/sweeptimelockmanual_test.go index 9c0323c..b22bec6 100644 --- a/cmd/chantools/sweeptimelockmanual_test.go +++ b/cmd/chantools/sweeptimelockmanual_test.go @@ -86,7 +86,7 @@ func TestSweepTimeLockManual(t *testing.T) { revPubKey, _ := btcec.ParsePubKey(revPubKeyBytes) _, _, _, _, _, err = tryKey( - baseKey, revPubKey, defaultCsvLimit, lockScript, + baseKey, revPubKey, 0, defaultCsvLimit, lockScript, tc.keyIndex, 500, ) require.NoError(t, err) From dee18ed80ce9afd285bd97158a3fbabbabd085ad Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 7 Nov 2023 20:29:27 -0600 Subject: [PATCH 3/4] sweeptimelockmanual: rename variable --- cmd/chantools/sweeptimelockmanual.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/chantools/sweeptimelockmanual.go b/cmd/chantools/sweeptimelockmanual.go index 72d1564..5782fd7 100644 --- a/cmd/chantools/sweeptimelockmanual.go +++ b/cmd/chantools/sweeptimelockmanual.go @@ -34,8 +34,8 @@ type sweepTimeLockManualCommand struct { TimeLockAddr string RemoteRevocationBasePoint string - MaxNumChansTotal uint16 - MaxNumChanUpdates uint64 + MaxNumChannelsTotal uint16 + MaxNumChanUpdates uint64 rootKey *rootKey inputs *inputFlags @@ -83,7 +83,7 @@ address is always the one that's longer (because it's P2WSH and not P2PKH).`, "limit to use", ) cc.cmd.Flags().Uint16Var( - &cc.MaxNumChansTotal, "maxnumchanstotal", maxKeys, "maximum "+ + &cc.MaxNumChannelsTotal, "maxnumchanstotal", maxKeys, "maximum "+ "number of keys to try, set to maximum number of "+ "channels the local node potentially has or had", ) @@ -136,7 +136,7 @@ func (c *sweepTimeLockManualCommand) Execute(_ *cobra.Command, _ []string) error return sweepTimeLockManual( extendedKey, c.APIURL, c.SweepAddr, c.TimeLockAddr, - remoteRevPoint, 0, c.MaxCsvLimit, 0, c.MaxNumChansTotal, + remoteRevPoint, 0, c.MaxCsvLimit, 0, c.MaxNumChannelsTotal, c.MaxNumChanUpdates, c.Publish, c.FeeRate, ) } From a13262f2ff91af3bbb3dcc797b86e8120ca7add2 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 7 Nov 2023 20:25:03 -0600 Subject: [PATCH 4/4] sweeptimelockmanual: allow using channel backup file Instead of needing to manually dump the channel backup file, look for the remote revocation base point and then have the CSV delay and channel derivation index being brute forced, we can extract all that info directly from the channel backup file. --- cmd/chantools/sweeptimelockmanual.go | 96 +++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 2 deletions(-) diff --git a/cmd/chantools/sweeptimelockmanual.go b/cmd/chantools/sweeptimelockmanual.go index 5782fd7..ab64e1d 100644 --- a/cmd/chantools/sweeptimelockmanual.go +++ b/cmd/chantools/sweeptimelockmanual.go @@ -37,6 +37,9 @@ type sweepTimeLockManualCommand struct { MaxNumChannelsTotal uint16 MaxNumChanUpdates uint64 + ChannelBackup string + ChannelPoint string + rootKey *rootKey inputs *inputFlags cmd *cobra.Command @@ -56,6 +59,9 @@ and only the channel.backup file is available. To get the value for --remoterevbasepoint you must use the dumpbackup command, then look up the value for RemoteChanCfg -> RevocationBasePoint -> PubKey. +Alternatively you can directly use the --frombackup and --channelpoint flags to +pull the required information from the given channel.backup file automatically. + To get the value for --timelockaddr you must look up the channel's funding output on chain, then follow it to the force close output. The time locked address is always the one that's longer (because it's P2WSH and not P2PKH).`, @@ -64,6 +70,14 @@ address is always the one that's longer (because it's P2WSH and not P2PKH).`, --timelockaddr bc1q............ \ --remoterevbasepoint 03xxxxxxx \ --feerate 10 \ + --publish + +chantools sweeptimelockmanual \ + --sweepaddr bc1q..... \ + --timelockaddr bc1q............ \ + --frombackup channel.backup \ + --channelpoint f39310xxxxxxxxxx:1 \ + --feerate 10 \ --publish`, RunE: cc.Execute, } @@ -105,6 +119,16 @@ address is always the one that's longer (because it's P2WSH and not P2PKH).`, "remote node's revocation base point, can be found "+ "in a channel.backup file", ) + cc.cmd.Flags().StringVar( + &cc.ChannelBackup, "frombackup", "", "channel backup file to "+ + "read the channel information from", + ) + cc.cmd.Flags().StringVar( + &cc.ChannelPoint, "channelpoint", "", "channel point to use "+ + "for locating the channel in the channel backup file "+ + "specified in the --frombackup flag, "+ + "format: txid:index", + ) cc.rootKey = newRootKey(cc.cmd, "deriving keys") cc.inputs = newInputFlags(cc.cmd) @@ -126,9 +150,68 @@ func (c *sweepTimeLockManualCommand) Execute(_ *cobra.Command, _ []string) error return fmt.Errorf("time lock addr is required") } + var ( + startCsvLimit uint16 + maxCsvLimit = c.MaxCsvLimit + startNumChannelsTotal uint16 + maxNumChannelsTotal = c.MaxNumChannelsTotal + remoteRevocationBasePoint = c.RemoteRevocationBasePoint + ) + + // We either support specifying the remote revocation base point + // manually, in which case the CSV limit and number of channels are not + // known, or we can use the channel backup file to get the required + // information from there directly. + switch { + case c.RemoteRevocationBasePoint != "": + // Nothing to do here but continue below with the info provided + // by the user. + + case c.ChannelBackup != "": + if c.ChannelPoint == "" { + return fmt.Errorf("channel point is required with " + + "--frombackup") + } + + backupChan, err := lnd.ExtractChannel( + extendedKey, chainParams, c.ChannelBackup, + c.ChannelPoint, + ) + if err != nil { + return fmt.Errorf("error extracting channel: %w", err) + } + + remoteCfg := backupChan.RemoteChanCfg + remoteRevocationBasePoint = remoteCfg.RevocationBasePoint.PubKey + + startCsvLimit = remoteCfg.CsvDelay + maxCsvLimit = startCsvLimit + 1 + + delayPath, err := lnd.ParsePath( + backupChan.LocalChanCfg.DelayBasePoint.Path, + ) + if err != nil { + return fmt.Errorf("error parsing delay path: %w", err) + } + if len(delayPath) != 5 { + return fmt.Errorf("invalid delay path '%v'", delayPath) + } + + startNumChannelsTotal = uint16(delayPath[4]) + maxNumChannelsTotal = startNumChannelsTotal + 1 + + case c.ChannelBackup != "" && c.RemoteRevocationBasePoint != "": + return fmt.Errorf("cannot use both --frombackup and " + + "--remoterevbasepoint at the same time") + + default: + return fmt.Errorf("either --frombackup or " + + "--remoterevbasepoint is required") + } + // The remote revocation base point must also be set and a valid EC // point. - remoteRevPoint, err := pubKeyFromHex(c.RemoteRevocationBasePoint) + remoteRevPoint, err := pubKeyFromHex(remoteRevocationBasePoint) if err != nil { return fmt.Errorf("invalid remote revocation base point: %w", err) @@ -136,7 +219,8 @@ func (c *sweepTimeLockManualCommand) Execute(_ *cobra.Command, _ []string) error return sweepTimeLockManual( extendedKey, c.APIURL, c.SweepAddr, c.TimeLockAddr, - remoteRevPoint, 0, c.MaxCsvLimit, 0, c.MaxNumChannelsTotal, + remoteRevPoint, startCsvLimit, maxCsvLimit, + startNumChannelsTotal, maxNumChannelsTotal, c.MaxNumChanUpdates, c.Publish, c.FeeRate, ) } @@ -146,6 +230,14 @@ func sweepTimeLockManual(extendedKey *hdkeychain.ExtendedKey, apiURL string, startCsvTimeout, maxCsvTimeout, startNumChannels, maxNumChannels uint16, maxNumChanUpdates uint64, publish bool, feeRate uint32) error { + log.Debugf("Starting to brute force the time lock script, using: "+ + "remote_rev_base_point=%x, start_csv_limit=%d, "+ + "max_csv_limit=%d, start_num_channels=%d, "+ + "max_num_channels=%d, max_num_chan_updates=%d", + remoteRevPoint.SerializeCompressed(), startCsvTimeout, + maxCsvTimeout, startNumChannels, maxNumChannels, + maxNumChanUpdates) + // First of all, we need to parse the lock addr and make sure we can // brute force the script with the information we have. If not, we can't // continue anyway.