Polish code and document commands

pull/3/head
Oliver Gugger 5 years ago
parent 48608729a5
commit 08461f849e
No known key found for this signature in database
GPG Key ID: 8E4256593F177720

@ -1,7 +1,150 @@
# Channel tools # Channel tools
This tool works with the output of lnd's `listchannels` command and creates This tool provides four helper functions that can be used to rescue funds locked
a summary of the on-chain state of these channels. in lnd channels in case lnd itself cannot run properly any more.
**WARNING**: This tool will query public block explorer APIs, your privacy **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
are coded properly. Please look at the code to understand what it does before
you use it for anything serious.
**WARNING 2**: This tool will query public block explorer APIs, your privacy
might not be preserved. Use at your own risk. might not be preserved. Use at your own risk.
## Overview
```text
Usage:
chantools [OPTIONS] <command>
Application Options:
--apiurl= API URL to use (must be esplora compatible). (default: https://blockstream.info/api)
--listchannels= The channel input is in the format of lncli's listchannels format. Specify '-' to read from stdin.
--pendingchannels= The channel input is in the format of lncli's pendingchannels format. Specify '-' to read from stdin.
--fromsummary= The channel input is in the format of this tool's channel summary. Specify '-' to read from stdin.
--fromchanneldb= The channel input is in the format of an lnd channel.db file.
Help Options:
-h, --help Show this help message
Available commands:
forceclose Force-close the last state that is in the channel.db provided
rescueclosed Try finding the private keys for funds that are in outputs of remotely force-closed channels
summary Compile a summary about the current state of channels.
sweeptimelock Sweep the force-closed state after the time lock has expired
```
## summary command
```text
Usage:
chantools [OPTIONS] summary
```
From a list of channels, find out what their state is by querying the funding
transaction on a block explorer API.
Example command 1:
```bash
lncli listchannels | chantools --listchannels - summary
```
Example command 2:
```bash
chantools --fromchanneldb ~/.lnd/data/graph/mainnet/channel.db
```
## rescueclosed command
```text
Usage:
chantools [OPTIONS] rescueclosed [rescueclosed-OPTIONS]
[rescueclosed command options]
--rootkey= BIP32 HD root key to use.
--channeldb= The lnd channel.db file to use for rescuing force-closed channels.
```
If channels have already been force-closed by the remote peer, this command
tries to find the private keys to sweep the funds from the output that belongs
to our side. This can only be used if we have a channel DB that contains the
latest commit point. Normally you would use SCB to get the funds from those
channels. But this method can help if the other node doesn't know about the
channels any more but we still have the channel.db from the moment they
force-closed.
Example command:
```bash
chantools --fromsummary results/summary-xxxx-yyyy.json \
rescueclosed \
--channeldb ~/.lnd/data/graph/mainnet/channel.db \
--rootkey xprvxxxxxxxxxx
```
## forceclose command
```text
Usage:
chantools [OPTIONS] forceclose [forceclose-OPTIONS]
[forceclose command options]
--rootkey= BIP32 HD root key to use.
--channeldb= The lnd channel.db file to use for force-closing channels.
--publish Should the force-closing TX be published to the chain API?
```
If you are certain that a node is offline for good (AFTER you've tried SCB!) and
a channel is still open, you can use this method to force-close your latest
state that you have in your channel.db.
**!!! WARNING !!! DANGER !!! WARNING !!!**
If you do this and the state that you publish is *not* the latest state, then
the remote node *could* punish you by taking the whole channel amount *if* they
come online before you can sweep the funds from the time locked (144 - 2000
blocks) transaction *or* they have a watch tower looking out for them.
**This should absolutely be the last resort and you have been warned!**
Example command:
```bash
chantools --fromsummary results/summary-xxxx-yyyy.json \
forceclose \
--channeldb ~/.lnd/data/graph/mainnet/channel.db \
--rootkey xprvxxxxxxxxxx \
--publish
```
## sweeptimelock command
```text
Usage:
chantools [OPTIONS] sweeptimelock [sweeptimelock-OPTIONS]
[sweeptimelock command options]
--rootkey= BIP32 HD root key to use.
--publish Should the sweep TX be published to the chain API?
--sweepaddr= The address the funds should be sweeped to
--maxcsvlimit= Maximum CSV limit to use. (default 2000)
```
Use this command to sweep the funds from channels that you force-closed with the
`forceclose` command. You **MUST** use the result file that was created with the
`forceclose` command, otherwise it won't work. You also have to wait until the
highest time lock (can be up to 2000 blocks which is more than two weeks) of all
the channels has passed. If you only want to sweep channels that have the
default CSV limit of 1 day, you can set the `--maxcsvlimit` parameter to 144.
Example command:
```bash
chantools --fromsummary results/forceclose-xxxx-yyyy.json \
sweeptimelock
--rootkey xprvxxxxxxxxxx \
--publish \
--sweepaddr bc1q.....
```

@ -3,7 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"github.com/guggero/chantools" "github.com/guggero/chantools"
) )

@ -1,5 +1,7 @@
package chantools package chantools
import "github.com/lightningnetwork/lnd/keychain"
type ClosingTX struct { type ClosingTX struct {
TXID string `json:"txid"` TXID string `json:"txid"`
ForceClose bool `json:"force_close"` ForceClose bool `json:"force_close"`
@ -9,10 +11,19 @@ type ClosingTX struct {
ConfHeight uint32 `json:"conf_height"` ConfHeight uint32 `json:"conf_height"`
} }
type Basepoint struct { type BasePoint struct {
Family uint16 `json:"family,omitempty"` Family uint16 `json:"family,omitempty"`
Index uint32 `json:"index,omitempty"` Index uint32 `json:"index,omitempty"`
Pubkey string `json:"pubkey"` PubKey string `json:"pubkey"`
}
func (b *BasePoint) toDesc() *keychain.KeyDescriptor {
return &keychain.KeyDescriptor{
KeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamily(b.Family),
Index: b.Index,
},
}
} }
type Out struct { type Out struct {
@ -25,8 +36,8 @@ type ForceClose struct {
TXID string `json:"txid"` TXID string `json:"txid"`
Serialized string `json:"serialized"` Serialized string `json:"serialized"`
CSVDelay uint16 `json:"csv_delay"` CSVDelay uint16 `json:"csv_delay"`
DelayBasepoint *Basepoint `json:"delay_basepoint"` DelayBasePoint *BasePoint `json:"delay_basepoint"`
RevocationBasepoint *Basepoint `json:"revocation_basepoint"` RevocationBasePoint *BasePoint `json:"revocation_basepoint"`
CommitPoint string `json:"commit_point"` CommitPoint string `json:"commit_point"`
Outs []*Out `json:"outs"` Outs []*Out `json:"outs"`
} }

@ -16,20 +16,14 @@ import (
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
) )
func forceCloseChannels(cfg *config, entries []*SummaryEntry, func forceCloseChannels(extendedKey *hdkeychain.ExtendedKey,
chanDb *channeldb.DB, publish bool) error { entries []*SummaryEntry, chanDb *channeldb.DB, publish bool) error {
channels, err := chanDb.FetchAllChannels() channels, err := chanDb.FetchAllChannels()
if err != nil { if err != nil {
return err return err
} }
chainApi := &chainApi{baseUrl: cfg.ApiUrl} chainApi := &chainApi{baseUrl: cfg.ApiUrl}
extendedKey, err := hdkeychain.NewKeyFromString(cfg.RootKey)
if err != nil {
return err
}
signer := &signer{extendedKey: extendedKey} signer := &signer{extendedKey: extendedKey}
// Go through all channels in the DB, find the still open ones and // Go through all channels in the DB, find the still open ones and
@ -51,8 +45,8 @@ func forceCloseChannels(cfg *config, entries []*SummaryEntry,
localCommit := channel.LocalCommitment localCommit := channel.LocalCommitment
localCommitTx := localCommit.CommitTx localCommitTx := localCommit.CommitTx
if localCommitTx == nil { if localCommitTx == nil {
log.Errorf("Cannot force-close, no local commit TX for "+ log.Errorf("Cannot force-close, no local commit TX "+
"channel %s", channelEntry.ChannelPoint) "for channel %s", channelEntry.ChannelPoint)
continue continue
} }
@ -91,18 +85,22 @@ func forceCloseChannels(cfg *config, entries []*SummaryEntry,
return err return err
} }
point := input.ComputeCommitmentPoint(revocationPreimage[:]) point := input.ComputeCommitmentPoint(revocationPreimage[:])
// Store all information that we collected into the channel
// entry file so we don't need to use the channel.db file for
// the next step.
channelEntry.ForceClose = &ForceClose{ channelEntry.ForceClose = &ForceClose{
TXID: hash.String(), TXID: hash.String(),
Serialized: serialized, Serialized: serialized,
DelayBasepoint: &Basepoint{ DelayBasePoint: &BasePoint{
Family: uint16(basepoint.Family), Family: uint16(basepoint.Family),
Index: basepoint.Index, Index: basepoint.Index,
Pubkey: hex.EncodeToString( PubKey: hex.EncodeToString(
basepoint.PubKey.SerializeCompressed(), basepoint.PubKey.SerializeCompressed(),
), ),
}, },
RevocationBasepoint: &Basepoint{ RevocationBasePoint: &BasePoint{
Pubkey: hex.EncodeToString( PubKey: hex.EncodeToString(
revpoint.PubKey.SerializeCompressed(), revpoint.PubKey.SerializeCompressed(),
), ),
}, },
@ -130,8 +128,8 @@ func forceCloseChannels(cfg *config, entries []*SummaryEntry,
if err != nil { if err != nil {
return err return err
} }
log.Infof("Published TX %s, response: %s", hash.String(), log.Infof("Published TX %s, response: %s",
response) hash.String(), response)
} }
} }

@ -16,12 +16,10 @@ const (
type config struct { type config struct {
ApiUrl string `long:"apiurl" description:"API URL to use (must be esplora compatible)."` ApiUrl string `long:"apiurl" description:"API URL to use (must be esplora compatible)."`
RootKey string `long:"rootkey" description:"BIP32 HD root key to use."`
ListChannels string `long:"listchannels" description:"The channel input is in the format of lncli's listchannels format. Specify '-' to read from stdin."` ListChannels string `long:"listchannels" description:"The channel input is in the format of lncli's listchannels format. Specify '-' to read from stdin."`
PendingChannels string `long:"pendingchannels" description:"The channel input is in the format of lncli's pendingchannels format. Specify '-' to read from stdin."` PendingChannels string `long:"pendingchannels" description:"The channel input is in the format of lncli's pendingchannels format. Specify '-' to read from stdin."`
FromSummary string `long:"fromsummary" description:"The channel input is in the format of this tool's channel summary. Specify '-' to read from stdin."` FromSummary string `long:"fromsummary" description:"The channel input is in the format of this tool's channel summary. Specify '-' to read from stdin."`
FromChannelDB string `long:"fromchanneldb" description:"The channel input is in the format of an lnd channel.db file."` FromChannelDB string `long:"fromchanneldb" description:"The channel input is in the format of an lnd channel.db file."`
ChannelDB string `long:"channeldb" description:"The lnd channel.db file to use for rescuing or force-closing channels."`
} }
var ( var (
@ -47,12 +45,12 @@ func Main() error {
&rescueClosedCommand{}, &rescueClosedCommand{},
) )
_, _ = parser.AddCommand( _, _ = parser.AddCommand(
"forceclose", "Force-close the last state that is in the " + "forceclose", "Force-close the last state that is in the "+
"channel.db provided", "", "channel.db provided", "",
&forceCloseCommand{}, &forceCloseCommand{},
) )
_, _ = parser.AddCommand( _, _ = parser.AddCommand(
"sweeptimelock", "Sweep the force-closed state after the time " + "sweeptimelock", "Sweep the force-closed state after the time "+
"lock has expired", "", "lock has expired", "",
&sweepTimeLockCommand{}, &sweepTimeLockCommand{},
) )
@ -69,26 +67,29 @@ func (c *summaryCommand) Execute(args []string) error {
if err != nil { if err != nil {
return err return err
} }
return collectChanSummary(cfg, entries) return summarizeChannels(cfg.ApiUrl, entries)
} }
type rescueClosedCommand struct{} type rescueClosedCommand struct {
RootKey string `long:"rootkey" description:"BIP32 HD root key to use."`
ChannelDB string `long:"channeldb" description:"The lnd channel.db file to use for rescuing force-closed channels."`
}
func (c *rescueClosedCommand) Execute(args []string) error { func (c *rescueClosedCommand) Execute(args []string) error {
// Check that root key is valid. // Check that root key is valid.
if cfg.RootKey == "" { if c.RootKey == "" {
return fmt.Errorf("root key is required") return fmt.Errorf("root key is required")
} }
_, err := hdkeychain.NewKeyFromString(cfg.RootKey) extendedKey, err := hdkeychain.NewKeyFromString(c.RootKey)
if err != nil { if err != nil {
return fmt.Errorf("error parsing root key: %v", err) return fmt.Errorf("error parsing root key: %v", err)
} }
// Check that we have a channel DB. // Check that we have a channel DB.
if cfg.ChannelDB == "" { if c.ChannelDB == "" {
return fmt.Errorf("rescue DB is required") return fmt.Errorf("rescue DB is required")
} }
db, err := channeldb.Open(path.Dir(cfg.ChannelDB)) db, err := channeldb.Open(path.Dir(c.ChannelDB))
if err != nil { if err != nil {
return fmt.Errorf("error opening rescue DB: %v", err) return fmt.Errorf("error opening rescue DB: %v", err)
} }
@ -98,19 +99,29 @@ func (c *rescueClosedCommand) Execute(args []string) error {
if err != nil { if err != nil {
return err return err
} }
return bruteForceChannels(cfg, entries, db) return rescueClosedChannels(extendedKey, entries, db)
} }
type forceCloseCommand struct { type forceCloseCommand struct {
Publish bool `long:"publish" description:"Should the force-closing TX be published to the chain API?"` RootKey string `long:"rootkey" description:"BIP32 HD root key to use."`
ChannelDB string `long:"channeldb" description:"The lnd channel.db file to use for force-closing channels."`
Publish bool `long:"publish" description:"Should the force-closing TX be published to the chain API?"`
} }
func (c *forceCloseCommand) Execute(args []string) error { func (c *forceCloseCommand) Execute(args []string) error {
// Check that root key is valid.
if c.RootKey == "" {
return fmt.Errorf("root key is required")
}
extendedKey, err := hdkeychain.NewKeyFromString(c.RootKey)
if err != nil {
return fmt.Errorf("error parsing root key: %v", err)
}
// Check that we have a channel DB. // Check that we have a channel DB.
if cfg.ChannelDB == "" { if c.ChannelDB == "" {
return fmt.Errorf("rescue DB is required") return fmt.Errorf("rescue DB is required")
} }
db, err := channeldb.Open(path.Dir(cfg.ChannelDB)) db, err := channeldb.Open(path.Dir(c.ChannelDB))
if err != nil { if err != nil {
return fmt.Errorf("error opening rescue DB: %v", err) return fmt.Errorf("error opening rescue DB: %v", err)
} }
@ -120,24 +131,26 @@ func (c *forceCloseCommand) Execute(args []string) error {
if err != nil { if err != nil {
return err return err
} }
return forceCloseChannels(cfg, entries, db, c.Publish) return forceCloseChannels(extendedKey, entries, db, c.Publish)
} }
type sweepTimeLockCommand struct { type sweepTimeLockCommand struct {
Publish bool `long:"publish" description:"Should the sweep TX be published to the chain API?"` RootKey string `long:"rootkey" description:"BIP32 HD root key to use."`
Publish bool `long:"publish" description:"Should the sweep TX be published to the chain API?"`
SweepAddr string `long:"sweepaddr" description:"The address the funds should be sweeped to"` SweepAddr string `long:"sweepaddr" description:"The address the funds should be sweeped to"`
MaxCsvLimit int `long:"maxcsvlimit" description:"Maximum CSV limit to use. (default 2000)"`
} }
func (c *sweepTimeLockCommand) Execute(args []string) error { func (c *sweepTimeLockCommand) Execute(args []string) error {
// Check that root key is valid. // Check that root key is valid.
if cfg.RootKey == "" { if c.RootKey == "" {
return fmt.Errorf("root key is required") return fmt.Errorf("root key is required")
} }
_, err := hdkeychain.NewKeyFromString(cfg.RootKey) extendedKey, err := hdkeychain.NewKeyFromString(c.RootKey)
if err != nil { if err != nil {
return fmt.Errorf("error parsing root key: %v", err) return fmt.Errorf("error parsing root key: %v", err)
} }
// Make sure sweep addr is set. // Make sure sweep addr is set.
if c.SweepAddr == "" { if c.SweepAddr == "" {
return fmt.Errorf("sweep addr is required") return fmt.Errorf("sweep addr is required")
@ -148,7 +161,15 @@ func (c *sweepTimeLockCommand) Execute(args []string) error {
if err != nil { if err != nil {
return err return err
} }
return sweepTimeLock(cfg, entries, c.SweepAddr, c.Publish)
// Set default value
if c.MaxCsvLimit == 0 {
c.MaxCsvLimit = 2000
}
return sweepTimeLock(
extendedKey, cfg.ApiUrl, entries, c.SweepAddr, c.MaxCsvLimit,
c.Publish,
)
} }
func setupLogging() { func setupLogging() {

@ -31,10 +31,10 @@ type cacheEntry struct {
pubKey *btcec.PublicKey pubKey *btcec.PublicKey
} }
func bruteForceChannels(cfg *config, entries []*SummaryEntry, func rescueClosedChannels(extendedKey *hdkeychain.ExtendedKey,
chanDb *channeldb.DB) error { entries []*SummaryEntry, chanDb *channeldb.DB) error {
err := fillCache(cfg.RootKey) err := fillCache(extendedKey)
if err != nil { if err != nil {
return err return err
} }
@ -148,12 +148,7 @@ func addrInCache(addr string, perCommitPoint *btcec.PublicKey) (string, error) {
return "", errAddrNotFound return "", errAddrNotFound
} }
func fillCache(rootKey string) error { func fillCache(extendedKey *hdkeychain.ExtendedKey) error {
extendedKey, err := hdkeychain.NewKeyFromString(rootKey)
if err != nil {
return err
}
cache = make([]*cacheEntry, cacheSize) cache = make([]*cacheEntry, cacheSize)
for i := 0; i < cacheSize; i++ { for i := 0; i < cacheSize; i++ {
@ -230,4 +225,4 @@ func parseAddr(addr string) ([]byte, error) {
return nil, fmt.Errorf("address: must be a bech32 P2WPKH address") return nil, fmt.Errorf("address: must be a bech32 P2WPKH address")
} }
return targetPubKeyHash, nil return targetPubKeyHash, nil
} }

@ -2,6 +2,7 @@ package chantools
import ( import (
"fmt" "fmt"
"github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
@ -24,12 +25,8 @@ func (s *signer) SignOutputRaw(tx *wire.MsgTx,
if err != nil { if err != nil {
return nil, err return nil, err
} }
privKey, err = maybeTweakPrivKey(signDesc, privKey)
if err != nil {
return nil, err
}
privKey = maybeTweakPrivKey(signDesc, privKey)
amt := signDesc.Output.Value amt := signDesc.Output.Value
sig, err := txscript.RawTxInWitnessSignature( sig, err := txscript.RawTxInWitnessSignature(
tx, signDesc.SigHashes, signDesc.InputIndex, amt, tx, signDesc.SigHashes, signDesc.InputIndex, amt,
@ -64,26 +61,14 @@ func (s *signer) fetchPrivKey(descriptor *keychain.KeyDescriptor) (
return key.ECPrivKey() return key.ECPrivKey()
} }
// maybeTweakPrivKey examines the single and double tweak parameters on the // maybeTweakPrivKey examines the single tweak parameters on the passed sign
// passed sign descriptor and may perform a mapping on the passed private key // descriptor and may perform a mapping on the passed private key in order to
// in order to utilize the tweaks, if populated. // utilize the tweaks, if populated.
func maybeTweakPrivKey(signDesc *input.SignDescriptor, func maybeTweakPrivKey(signDesc *input.SignDescriptor,
privKey *btcec.PrivateKey) (*btcec.PrivateKey, error) { privKey *btcec.PrivateKey) *btcec.PrivateKey {
var retPriv *btcec.PrivateKey
switch {
case signDesc.SingleTweak != nil: if signDesc.SingleTweak != nil {
retPriv = input.TweakPrivKey(privKey, return input.TweakPrivKey(privKey, signDesc.SingleTweak)
signDesc.SingleTweak)
case signDesc.DoubleTweak != nil:
retPriv = input.DeriveRevocationPrivKey(privKey,
signDesc.DoubleTweak)
default:
retPriv = privKey
} }
return privKey
return retPriv, nil }
}

@ -7,12 +7,11 @@ import (
"time" "time"
) )
func collectChanSummary(cfg *config, channels []*SummaryEntry) error { func summarizeChannels(apiUrl string, channels []*SummaryEntry) error {
summaryFile := &SummaryEntryFile{ summaryFile := &SummaryEntryFile{
Channels: channels, Channels: channels,
} }
chainApi := &chainApi{baseUrl: apiUrl}
chainApi := &chainApi{baseUrl: cfg.ApiUrl}
for idx, channel := range channels { for idx, channel := range channels {
tx, err := chainApi.Transaction(channel.FundingTXID) tx, err := chainApi.Transaction(channel.FundingTXID)
@ -44,7 +43,7 @@ func collectChanSummary(cfg *config, channels []*SummaryEntry) error {
} }
} else { } else {
summaryFile.OpenChannels++ summaryFile.OpenChannels++
summaryFile.FundsOpenChannels += uint64(channel.LocalBalance) summaryFile.FundsOpenChannels += channel.LocalBalance
channel.ClosingTX = nil channel.ClosingTX = nil
} }
@ -92,11 +91,11 @@ func reportOutspend(api *chainApi, summaryFile *SummaryEntryFile,
return err return err
} }
summaryFile.FundsClosedChannels += uint64(entry.LocalBalance) summaryFile.FundsClosedChannels += entry.LocalBalance
if isCoopClose(spendTx) { if isCoopClose(spendTx) {
summaryFile.CoopClosedChannels++ summaryFile.CoopClosedChannels++
summaryFile.FundsCoopClose += uint64(entry.LocalBalance) summaryFile.FundsCoopClose += entry.LocalBalance
entry.ClosingTX.ForceClose = false entry.ClosingTX.ForceClose = false
return nil return nil
} }
@ -143,7 +142,7 @@ func reportOutspend(api *chainApi, summaryFile *SummaryEntryFile,
} }
} else { } else {
entry.ClosingTX.AllOutsSpent = true entry.ClosingTX.AllOutsSpent = true
summaryFile.FundsClosedSpent += uint64(entry.LocalBalance) summaryFile.FundsClosedSpent += entry.LocalBalance
summaryFile.FullySpentChannels++ summaryFile.FullySpentChannels++
} }

@ -4,36 +4,37 @@ import (
"bytes" "bytes"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil/hdkeychain" "github.com/btcsuite/btcutil/hdkeychain"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
) )
const ( const (
maxCsvTimeout = 15000 feeSatPerByte = 2
feeSatPerByte = 3
) )
func sweepTimeLock(cfg *config, entries []*SummaryEntry, sweepAddr string, func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiUrl string,
entries []*SummaryEntry, sweepAddr string, maxCsvTimeout int,
publish bool) error { publish bool) error {
extendedKey, err := hdkeychain.NewKeyFromString(cfg.RootKey) // Create signer and transaction template.
if err != nil {
return err
}
signer := &signer{extendedKey: extendedKey} signer := &signer{extendedKey: extendedKey}
chainApi := &chainApi{baseUrl: apiUrl}
sweepTx := wire.NewMsgTx(2) sweepTx := wire.NewMsgTx(2)
entryIndex := 0 totalOutputValue := int64(0)
value := int64(0)
signDescs := make([]*input.SignDescriptor, 0) signDescs := make([]*input.SignDescriptor, 0)
for _, entry := range entries { for _, entry := range entries {
if entry.ClosingTX == nil || entry.ForceClose == nil || // Skip entries that can't be swept.
entry.ClosingTX.AllOutsSpent || entry.LocalBalance == 0 { if entry.ClosingTX == nil ||
entry.ForceClose == nil ||
entry.ClosingTX.AllOutsSpent ||
entry.LocalBalance == 0 {
log.Infof("Not sweeping %s, info missing or all spent", log.Infof("Not sweeping %s, info missing or all spent",
entry.ChannelPoint) entry.ChannelPoint)
@ -42,161 +43,120 @@ func sweepTimeLock(cfg *config, entries []*SummaryEntry, sweepAddr string,
fc := entry.ForceClose fc := entry.ForceClose
// Find index of sweepable output of commitment TX.
txindex := -1 txindex := -1
if len(fc.Outs) == 1 { if len(fc.Outs) == 1 {
txindex = 0 txindex = 0
if fc.Outs[0].Value != uint64(entry.LocalBalance) { if fc.Outs[0].Value != entry.LocalBalance {
log.Errorf("Potential value mismatch! %d vs %d (%s)", log.Errorf("Potential value mismatch! %d vs "+
"%d (%s)",
fc.Outs[0].Value, entry.LocalBalance, fc.Outs[0].Value, entry.LocalBalance,
entry.ChannelPoint) entry.ChannelPoint)
} }
} else { } else {
for idx, out := range fc.Outs { for idx, out := range fc.Outs {
if out.Value == uint64(entry.LocalBalance) { if out.Value == entry.LocalBalance {
txindex = idx txindex = idx
} }
} }
} }
if txindex == -1 { if txindex == -1 {
log.Errorf("Could not find sweep output for chan %s", log.Errorf("Could not find sweep output for chan %s",
entry.ChannelPoint) entry.ChannelPoint)
continue continue
} }
txHash, err := chainhash.NewHashFromStr(fc.TXID)
if err != nil {
return fmt.Errorf("error parsing tx hash: %v", err)
}
commitPointBytes, err := hex.DecodeString(fc.CommitPoint) // Prepare sweep script parameters.
if err != nil { commitPoint, err := pubKeyFromHex(fc.CommitPoint)
return fmt.Errorf("error parsing commit point: %v", err)
}
commitPoint, err := btcec.ParsePubKey(commitPointBytes, btcec.S256())
if err != nil { if err != nil {
return fmt.Errorf("error parsing commit point: %v", err) return fmt.Errorf("error parsing commit point: %v", err)
} }
revPointBytes, err := hex.DecodeString(fc.RevocationBasepoint.Pubkey) revBase, err := pubKeyFromHex(fc.RevocationBasePoint.PubKey)
if err != nil { if err != nil {
return fmt.Errorf("error parsing commit point: %v", err) return fmt.Errorf("error parsing commit point: %v", err)
} }
revPoint, err := btcec.ParsePubKey(revPointBytes, btcec.S256()) delayDesc := fc.DelayBasePoint.toDesc()
if err != nil { delayPrivKey, err := signer.fetchPrivKey(delayDesc)
return fmt.Errorf("error parsing commit point: %v", err)
}
delayKeyDesc := &keychain.KeyDescriptor{
KeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamily(fc.DelayBasepoint.Family),
Index: fc.DelayBasepoint.Index,
},
}
delayPrivkey, err := signer.fetchPrivKey(delayKeyDesc)
if err != nil { if err != nil {
return fmt.Errorf("error getting private key: %v", err) return fmt.Errorf("error getting private key: %v", err)
} }
delayBase := delayPrivKey.PubKey()
delayKey := input.TweakPubKey(delayPrivkey.PubKey(), commitPoint) // We can't rely on the CSV delay of the channel DB to be
revocationKey := input.DeriveRevocationPubkey( // correct. But it doesn't cost us a lot to just brute force it.
revPoint, commitPoint, csvTimeout, script, scriptHash, err := bruteForceDelay(
) input.TweakPubKey(delayBase, commitPoint),
input.DeriveRevocationPubkey(revBase, commitPoint),
var ( fc.Outs[txindex].Script, maxCsvTimeout,
csvTimeout = int32(-1)
script []byte
scriptHash []byte
) )
targetScript, err := hex.DecodeString(fc.Outs[txindex].Script)
if err != nil { if err != nil {
return fmt.Errorf("error parsing target script: %v", err) log.Errorf("Could not create matching script for %s "+
} "or csv too high: %v", entry.ChannelPoint,
if len(targetScript) != 34 { err)
log.Errorf("invalid target script: %x", targetScript)
continue
}
for i := 0; csvTimeout == -1 && i < maxCsvTimeout; i++ {
s, err := input.CommitScriptToSelf(
uint32(i), delayKey, revocationKey,
)
if err != nil {
return fmt.Errorf("error creating script: %v", err)
}
sh, err := input.WitnessScriptHash(s)
if err != nil {
return fmt.Errorf("error hashing script: %v", err)
}
if bytes.Equal(targetScript[0:8], sh[0:8]) {
csvTimeout = int32(i)
script = s
scriptHash = sh
}
}
if csvTimeout == -1 || len(script) == 0 {
log.Errorf("Could not create matching script for %s " +
"or csv too high: %d", entry.ChannelPoint,
csvTimeout)
continue continue
} }
// Create the transaction input.
txHash, err := chainhash.NewHashFromStr(fc.TXID)
if err != nil {
return fmt.Errorf("error parsing tx hash: %v", err)
}
sweepTx.TxIn = append(sweepTx.TxIn, &wire.TxIn{ sweepTx.TxIn = append(sweepTx.TxIn, &wire.TxIn{
PreviousOutPoint: wire.OutPoint{ PreviousOutPoint: wire.OutPoint{
Hash: *txHash, Hash: *txHash,
Index: uint32(txindex), Index: uint32(txindex),
}, },
SignatureScript: nil, Sequence: input.LockTimeToSequence(
Witness: nil,
Sequence: input.LockTimeToSequence(
false, uint32(csvTimeout), false, uint32(csvTimeout),
), ),
}) })
singleTweak := input.SingleTweakBytes( // Create the sign descriptor for the input.
commitPoint, delayPrivkey.PubKey(),
)
signDesc := &input.SignDescriptor{ signDesc := &input.SignDescriptor{
KeyDesc: *delayKeyDesc, KeyDesc: *delayDesc,
SingleTweak: singleTweak, SingleTweak: input.SingleTweakBytes(
commitPoint, delayBase,
),
WitnessScript: script, WitnessScript: script,
Output: &wire.TxOut{ Output: &wire.TxOut{
PkScript: scriptHash, PkScript: scriptHash,
Value: int64(fc.Outs[txindex].Value), Value: int64(fc.Outs[txindex].Value),
}, },
HashType: txscript.SigHashAll, HashType: txscript.SigHashAll,
InputIndex: entryIndex,
} }
value += int64(fc.Outs[txindex].Value) totalOutputValue += int64(fc.Outs[txindex].Value)
signDescs = append(signDescs, signDesc) signDescs = append(signDescs, signDesc)
entryIndex++
}
if len(signDescs) != len(sweepTx.TxIn) {
return fmt.Errorf("length mismatch")
} }
sweepScript, err := pkhScript(sweepAddr) // Add our sweep destination output.
sweepScript, err := getWP2PKHScript(sweepAddr)
if err != nil { if err != nil {
return err return err
} }
sweepTx.TxOut = []*wire.TxOut{{ sweepTx.TxOut = []*wire.TxOut{{
Value: value, Value: totalOutputValue,
PkScript: sweepScript, PkScript: sweepScript,
}} }}
// Very naive fee estimation algorithm: Sign a first time as if we would
// send the whole amount with zero fee, just to estimate how big the
// transaction would get in bytes. Then adjust the fee and sign again.
sigHashes := txscript.NewTxSigHashes(sweepTx) sigHashes := txscript.NewTxSigHashes(sweepTx)
for idx, desc := range signDescs { for idx, desc := range signDescs {
desc.SigHashes = sigHashes desc.SigHashes = sigHashes
desc.InputIndex = idx
witness, err := input.CommitSpendTimeout(signer, desc, sweepTx) witness, err := input.CommitSpendTimeout(signer, desc, sweepTx)
if err != nil { if err != nil {
return err return err
} }
sweepTx.TxIn[idx].Witness = witness sweepTx.TxIn[idx].Witness = witness
} }
// Calculate a fee. This won't be very accurate so the feeSatPerByte
// should at least be 2 to not risk falling below the 1 sat/byte limit.
size := sweepTx.SerializeSize() size := sweepTx.SerializeSize()
fee := int64(size*feeSatPerByte) fee := int64(size * feeSatPerByte)
sweepTx.TxOut[0].Value = value - fee sweepTx.TxOut[0].Value = totalOutputValue - fee
// Sign again after output fixing. // Sign again after output fixing.
sigHashes = txscript.NewTxSigHashes(sweepTx) sigHashes = txscript.NewTxSigHashes(sweepTx)
@ -208,20 +168,42 @@ func sweepTimeLock(cfg *config, entries []*SummaryEntry, sweepAddr string,
} }
sweepTx.TxIn[idx].Witness = witness sweepTx.TxIn[idx].Witness = witness
} }
var buf bytes.Buffer var buf bytes.Buffer
err = sweepTx.Serialize(&buf) err = sweepTx.Serialize(&buf)
if err != nil { if err != nil {
return err return err
} }
log.Infof("Fee %d sats of %d total amount (for size %d)", log.Infof("Fee %d sats of %d total amount (for size %d)",
fee, value, sweepTx.SerializeSize()) fee, totalOutputValue, sweepTx.SerializeSize())
log.Infof("Transaction: %x", buf.Bytes())
// Publish TX.
if publish {
response, err := chainApi.PublishTx(
hex.EncodeToString(buf.Bytes()),
)
if err != nil {
return err
}
log.Infof("Published TX %s, response: %s",
sweepTx.TxHash().String(), response)
}
log.Infof("Transaction: %x", buf.Bytes())
return nil return nil
} }
func pkhScript(addr string) ([]byte, error) { func pubKeyFromHex(pubKeyHex string) (*btcec.PublicKey, error) {
pointBytes, err := hex.DecodeString(pubKeyHex)
if err != nil {
return nil, fmt.Errorf("error hex decoding pub key: %v", err)
}
return btcec.ParsePubKey(
pointBytes, btcec.S256(),
)
}
func getWP2PKHScript(addr string) ([]byte, error) {
targetPubKeyHash, err := parseAddr(addr) targetPubKeyHash, err := parseAddr(addr)
if err != nil { if err != nil {
return nil, err return nil, err
@ -232,3 +214,37 @@ func pkhScript(addr string) ([]byte, error) {
return builder.Script() return builder.Script()
} }
func bruteForceDelay(delayPubkey, revocationPubkey *btcec.PublicKey,
targetScriptHex string, maxCsvTimeout int) (int32, []byte, []byte,
error) {
targetScript, err := hex.DecodeString(targetScriptHex)
if err != nil {
return 0, nil, nil, fmt.Errorf("error parsing target script: "+
"%v", err)
}
if len(targetScript) != 34 {
return 0, nil, nil, fmt.Errorf("invalid target script: %s",
targetScriptHex)
}
for i := 0; i <= maxCsvTimeout; i++ {
s, err := input.CommitScriptToSelf(
uint32(i), delayPubkey, revocationPubkey,
)
if err != nil {
return 0, nil, nil, fmt.Errorf("error creating "+
"script: %v", err)
}
sh, err := input.WitnessScriptHash(s)
if err != nil {
return 0, nil, nil, fmt.Errorf("error hashing script: "+
"%v", err)
}
if bytes.Equal(targetScript[0:8], sh[0:8]) {
return int32(i), s, sh, nil
}
}
return 0, nil, nil, fmt.Errorf("csv timeout not found for target "+
"script %s", targetScriptHex)
}

Loading…
Cancel
Save