Skip to content

Commit

Permalink
Add sweeptimelockmanual command
Browse files Browse the repository at this point in the history
  • Loading branch information
guggero committed Aug 30, 2020
1 parent 18ef40f commit 4f343dd
Show file tree
Hide file tree
Showing 6 changed files with 486 additions and 62 deletions.
77 changes: 60 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
+ [signrescuefunding](#signrescuefunding)
+ [summary](#summary)
+ [sweeptimelock](#sweeptimelock)
+ [sweeptimelockmanual](#sweeptimelockmanual)
+ [vanitygen](#vanitygen)
+ [walletinfo](#walletinfo)

Expand Down Expand Up @@ -209,23 +210,24 @@ Help Options:
-h, --help Show this help message
Available commands:
chanbackup Create a channel.backup file from a channel database.
compactdb Open a source channel.db database file in safe/read-only mode and copy it to a fresh database, compacting it in the process.
derivekey Derive a key with a specific derivation path from the BIP32 HD root key.
dumpbackup Dump the content of a channel.backup file.
dumpchannels Dump all channel information from lnd's channel database.
filterbackup Filter an lnd channel.backup file and remove certain channels.
fixoldbackup Fixes an old channel.backup file that is affected by the lnd issue #3881 (unable to derive shachain root key).
forceclose Force-close the last state that is in the channel.db provided.
genimportscript Generate a script containing the on-chain keys of an lnd wallet that can be imported into other software like bitcoind.
rescueclosed Try finding the private keys for funds that are in outputs of remotely force-closed channels.
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.
showrootkey Extract and show the BIP32 HD root key from the 24 word lnd aezeed.
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.
vanitygen Generate a seed with a custom lnd node identity public key that starts with the given prefix.
walletinfo Shows relevant information about an lnd wallet.db file and optionally extracts the BIP32 HD root key.
chanbackup Create a channel.backup file from a channel database.
compactdb Open a source channel.db database file in safe/read-only mode and copy it to a fresh database, compacting it in the process.
derivekey Derive a key with a specific derivation path from the BIP32 HD root key.
dumpbackup Dump the content of a channel.backup file.
dumpchannels Dump all channel information from lnd's channel database.
filterbackup Filter an lnd channel.backup file and remove certain channels.
fixoldbackup Fixes an old channel.backup file that is affected by the lnd issue #3881 (unable to derive shachain root key).
forceclose Force-close the last state that is in the channel.db provided.
genimportscript Generate a script containing the on-chain keys of an lnd wallet that can be imported into other software like bitcoind.
rescueclosed Try finding the private keys for funds that are in outputs of remotely force-closed channels.
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.
showrootkey Extract and show the BIP32 HD root key from the 24 word lnd aezeed.
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
vanitygen Generate a seed with a custom lnd node identity public key that starts with the given prefix.
walletinfo Shows relevant information about an lnd wallet.db file and optionally extracts the BIP32 HD root key.
```

## Commands
Expand Down Expand Up @@ -610,6 +612,47 @@ chantools --fromsummary results/forceclose-xxxx-yyyy.json \
--sweepaddr bc1q.....
```

### sweeptimelockmanual

```text
Usage:
chantools [OPTIONS] sweeptimelockmanual [sweeptimelockmanual-OPTIONS]
[sweeptimelockmanual command options]
--rootkey= BIP32 HD root key to use. Leave empty to prompt for lnd 24 word aezeed.
--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)
--feerate= The fee rate to use for the sweep transaction in sat/vByte. (default 2 sat/vByte)
--timelockaddr= The address of the time locked commitment output where the funds are stuck in.
--remoterevbasepoint= The remote's revocation base point, can be found in a channel.backup file.
```

Sweep the locally force closed state of a single channel manually if only a
channel backup file is available. This can only be used if a channel is force
closed from the local node but then that node's state is lost and only the
`channel.backup` file is available.

To get the value for `--remoterevbasepoint` you must use the
[`dumpbackup`](#dumpbackup) command, then look up the value for
`RemoteChanCfg -> RevocationBasePoint -> PubKey`.

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).

Example command:

```bash
chantools sweeptimelockmanual \
--rootkey xprvxxxxxxxxxx \
--sweepaddr bc1q..... \
--timelockaddr bc1q............ \
--remoterevbasepoint 03xxxxxxx \
--feerate 10 \
--publish
```

### vanitygen

```
Expand Down
18 changes: 18 additions & 0 deletions btc/explorer_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type ExplorerAPI struct {
}

type TX struct {
TXID string `json:"txid"`
Vin []*Vin `json:"vin"`
Vout []*Vout `json:"vout"`
}
Expand Down Expand Up @@ -71,6 +72,23 @@ func (a *ExplorerAPI) Transaction(txid string) (*TX, error) {
return tx, nil
}

func (a *ExplorerAPI) Outpoint(addr string) (*TX, int, error) {
var txs []*TX
err := fetchJSON(fmt.Sprintf("%s/address/%s/txs", a.BaseURL, addr), &txs)
if err != nil {
return nil, 0, err
}
for _, tx := range txs {
for idx, vout := range tx.Vout {
if vout.ScriptPubkeyAddr == addr {
return tx, idx, nil
}
}
}

return nil, 0, fmt.Errorf("no tx found")
}

func (a *ExplorerAPI) PublishTx(rawTxHex string) (string, error) {
url := fmt.Sprintf("%s/tx", a.BaseURL)
resp, err := http.Post(url, "text/plain", strings.NewReader(rawTxHex))
Expand Down
7 changes: 6 additions & 1 deletion cmd/chantools/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (

const (
defaultAPIURL = "https://blockstream.info/api"
version = "0.4.1"
version = "0.5.0"
)

var (
Expand Down Expand Up @@ -86,6 +86,11 @@ func runCommandParser() error {
"sweeptimelock", "Sweep the force-closed state after the time "+
"lock has expired.", "", &sweepTimeLockCommand{},
)
_, _ = parser.AddCommand(
"sweeptimelockmanual", "Sweep the force-closed state of a "+
"single channel manually if only a channel backup "+
"file is available", "", &sweepTimeLockManualCommand{},
)
_, _ = parser.AddCommand(
"dumpchannels", "Dump all channel information from lnd's "+
"channel database.", "", &dumpChannelsCommand{},
Expand Down
85 changes: 41 additions & 44 deletions cmd/chantools/sweeptimelock.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import (
"bytes"
"encoding/hex"
"fmt"

"github.com/btcsuite/btcd/btcec"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"

"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
Expand All @@ -17,14 +18,16 @@ import (
)

const (
feeSatPerByte = 2
defaultFeeSatPerVByte = 2
defaultCsvLimit = 2000
)

type sweepTimeLockCommand struct {
RootKey string `long:"rootkey" description:"BIP32 HD root key to use. Leave empty to prompt for lnd 24 word aezeed."`
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)"`
FeeRate uint32 `long:"feerate" description:"The fee rate to use for the sweep transaction in sat/vByte. (default 2 sat/vByte)"`
}

func (c *sweepTimeLockCommand) Execute(_ []string) error {
Expand Down Expand Up @@ -58,19 +61,22 @@ func (c *sweepTimeLockCommand) Execute(_ []string) error {
return err
}

// Set default value
// Set default values.
if c.MaxCsvLimit == 0 {
c.MaxCsvLimit = 2000
c.MaxCsvLimit = defaultCsvLimit
}
if c.FeeRate == 0 {
c.FeeRate = defaultFeeSatPerVByte
}
return sweepTimeLock(
extendedKey, cfg.APIURL, entries, c.SweepAddr, c.MaxCsvLimit,
c.Publish,
c.Publish, c.FeeRate,
)
}

func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiURL string,
entries []*dataformat.SummaryEntry, sweepAddr string, maxCsvTimeout int,
publish bool) error {
publish bool, feeRate uint32) error {

// Create signer and transaction template.
signer := &lnd.Signer{
Expand All @@ -82,6 +88,7 @@ func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiURL string,
sweepTx := wire.NewMsgTx(2)
totalOutputValue := int64(0)
signDescs := make([]*input.SignDescriptor, 0)
var estimator input.TxWeightEstimator

for _, entry := range entries {
// Skip entries that can't be swept.
Expand Down Expand Up @@ -135,12 +142,18 @@ func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiURL string,
}
delayBase := delayPrivKey.PubKey()

lockScript, err := hex.DecodeString(fc.Outs[txindex].Script)
if err != nil {
return fmt.Errorf("error parsing target script: %v",
err)
}

// We can't rely on the CSV delay of the channel DB to be
// correct. But it doesn't cost us a lot to just brute force it.
csvTimeout, script, scriptHash, err := bruteForceDelay(
input.TweakPubKey(delayBase, commitPoint),
input.DeriveRevocationPubkey(revBase, commitPoint),
fc.Outs[txindex].Script, maxCsvTimeout,
lockScript, maxCsvTimeout,
)
if err != nil {
log.Errorf("Could not create matching script for %s "+
Expand Down Expand Up @@ -179,40 +192,33 @@ func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiURL string,
}
totalOutputValue += int64(fc.Outs[txindex].Value)
signDescs = append(signDescs, signDesc)

// Account for the input weight.
estimator.AddWitnessInput(input.ToLocalTimeoutWitnessSize)
}

// Add our sweep destination output.
sweepScript, err := lnd.GetP2WPKHScript(sweepAddr, chainParams)
if err != nil {
return err
}
estimator.AddP2WKHOutput()

// Calculate the fee based on the given fee rate and our weight
// estimation.
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()
totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight()))

log.Infof("Fee %d sats of %d total amount (estimated weight %d)",
totalFee, totalOutputValue, estimator.Weight())

sweepTx.TxOut = []*wire.TxOut{{
Value: totalOutputValue,
Value: totalOutputValue - int64(totalFee),
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.

// Sign the transaction now.
sigHashes := txscript.NewTxSigHashes(sweepTx)
for idx, desc := range signDescs {
desc.SigHashes = sigHashes
desc.InputIndex = idx
witness, err := input.CommitSpendTimeout(signer, desc, sweepTx)
if err != nil {
return err
}
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()
fee := int64(size * feeSatPerByte)
sweepTx.TxOut[0].Value = totalOutputValue - fee

// Sign again after output fixing.
sigHashes = txscript.NewTxSigHashes(sweepTx)
for idx, desc := range signDescs {
desc.SigHashes = sigHashes
witness, err := input.CommitSpendTimeout(signer, desc, sweepTx)
Expand All @@ -227,8 +233,6 @@ func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiURL string,
if err != nil {
return err
}
log.Infof("Fee %d sats of %d total amount (for size %d)",
fee, totalOutputValue, sweepTx.SerializeSize())

// Publish TX.
if publish {
Expand All @@ -251,23 +255,16 @@ func pubKeyFromHex(pubKeyHex string) (*btcec.PublicKey, error) {
if err != nil {
return nil, fmt.Errorf("error hex decoding pub key: %v", err)
}
return btcec.ParsePubKey(
pointBytes, btcec.S256(),
)
return btcec.ParsePubKey(pointBytes, btcec.S256())
}

func bruteForceDelay(delayPubkey, revocationPubkey *btcec.PublicKey,
targetScriptHex string, maxCsvTimeout int) (int32, []byte, []byte,
targetScript []byte, 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)
targetScript)
}
for i := 0; i <= maxCsvTimeout; i++ {
s, err := input.CommitScriptToSelf(
Expand All @@ -287,5 +284,5 @@ func bruteForceDelay(delayPubkey, revocationPubkey *btcec.PublicKey,
}
}
return 0, nil, nil, fmt.Errorf("csv timeout not found for target "+
"script %s", targetScriptHex)
"script %s", targetScript)
}
Loading

0 comments on commit 4f343dd

Please sign in to comment.