Skip to content

Commit 4f343dd

Browse files
committed
Add sweeptimelockmanual command
1 parent 18ef40f commit 4f343dd

File tree

6 files changed

+486
-62
lines changed

6 files changed

+486
-62
lines changed

README.md

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
+ [signrescuefunding](#signrescuefunding)
2222
+ [summary](#summary)
2323
+ [sweeptimelock](#sweeptimelock)
24+
+ [sweeptimelockmanual](#sweeptimelockmanual)
2425
+ [vanitygen](#vanitygen)
2526
+ [walletinfo](#walletinfo)
2627

@@ -209,23 +210,24 @@ Help Options:
209210
-h, --help Show this help message
210211
211212
Available commands:
212-
chanbackup Create a channel.backup file from a channel database.
213-
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.
214-
derivekey Derive a key with a specific derivation path from the BIP32 HD root key.
215-
dumpbackup Dump the content of a channel.backup file.
216-
dumpchannels Dump all channel information from lnd's channel database.
217-
filterbackup Filter an lnd channel.backup file and remove certain channels.
218-
fixoldbackup Fixes an old channel.backup file that is affected by the lnd issue #3881 (unable to derive shachain root key).
219-
forceclose Force-close the last state that is in the channel.db provided.
220-
genimportscript Generate a script containing the on-chain keys of an lnd wallet that can be imported into other software like bitcoind.
221-
rescueclosed Try finding the private keys for funds that are in outputs of remotely force-closed channels.
222-
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.
223-
showrootkey Extract and show the BIP32 HD root key from the 24 word lnd aezeed.
224-
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.
225-
summary Compile a summary about the current state of channels.
226-
sweeptimelock Sweep the force-closed state after the time lock has expired.
227-
vanitygen Generate a seed with a custom lnd node identity public key that starts with the given prefix.
228-
walletinfo Shows relevant information about an lnd wallet.db file and optionally extracts the BIP32 HD root key.
213+
chanbackup Create a channel.backup file from a channel database.
214+
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.
215+
derivekey Derive a key with a specific derivation path from the BIP32 HD root key.
216+
dumpbackup Dump the content of a channel.backup file.
217+
dumpchannels Dump all channel information from lnd's channel database.
218+
filterbackup Filter an lnd channel.backup file and remove certain channels.
219+
fixoldbackup Fixes an old channel.backup file that is affected by the lnd issue #3881 (unable to derive shachain root key).
220+
forceclose Force-close the last state that is in the channel.db provided.
221+
genimportscript Generate a script containing the on-chain keys of an lnd wallet that can be imported into other software like bitcoind.
222+
rescueclosed Try finding the private keys for funds that are in outputs of remotely force-closed channels.
223+
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.
224+
showrootkey Extract and show the BIP32 HD root key from the 24 word lnd aezeed.
225+
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.
226+
summary Compile a summary about the current state of channels.
227+
sweeptimelock Sweep the force-closed state after the time lock has expired.
228+
sweeptimelockmanual Sweep the force-closed state of a single channel manually if only a channel backup file is available
229+
vanitygen Generate a seed with a custom lnd node identity public key that starts with the given prefix.
230+
walletinfo Shows relevant information about an lnd wallet.db file and optionally extracts the BIP32 HD root key.
229231
```
230232

231233
## Commands
@@ -610,6 +612,47 @@ chantools --fromsummary results/forceclose-xxxx-yyyy.json \
610612
--sweepaddr bc1q.....
611613
```
612614

615+
### sweeptimelockmanual
616+
617+
```text
618+
Usage:
619+
chantools [OPTIONS] sweeptimelockmanual [sweeptimelockmanual-OPTIONS]
620+
621+
[sweeptimelockmanual command options]
622+
--rootkey= BIP32 HD root key to use. Leave empty to prompt for lnd 24 word aezeed.
623+
--publish Should the sweep TX be published to the chain API?
624+
--sweepaddr= The address the funds should be sweeped to.
625+
--maxcsvlimit= Maximum CSV limit to use. (default 2000)
626+
--feerate= The fee rate to use for the sweep transaction in sat/vByte. (default 2 sat/vByte)
627+
--timelockaddr= The address of the time locked commitment output where the funds are stuck in.
628+
--remoterevbasepoint= The remote's revocation base point, can be found in a channel.backup file.
629+
```
630+
631+
Sweep the locally force closed state of a single channel manually if only a
632+
channel backup file is available. This can only be used if a channel is force
633+
closed from the local node but then that node's state is lost and only the
634+
`channel.backup` file is available.
635+
636+
To get the value for `--remoterevbasepoint` you must use the
637+
[`dumpbackup`](#dumpbackup) command, then look up the value for
638+
`RemoteChanCfg -> RevocationBasePoint -> PubKey`.
639+
640+
To get the value for `--timelockaddr` you must look up the channel's funding
641+
output on chain, then follow it to the force close output. The time locked
642+
address is always the one that's longer (because it's P2WSH and not P2PKH).
643+
644+
Example command:
645+
646+
```bash
647+
chantools sweeptimelockmanual \
648+
--rootkey xprvxxxxxxxxxx \
649+
--sweepaddr bc1q..... \
650+
--timelockaddr bc1q............ \
651+
--remoterevbasepoint 03xxxxxxx \
652+
--feerate 10 \
653+
--publish
654+
```
655+
613656
### vanitygen
614657

615658
```

btc/explorer_api.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type ExplorerAPI struct {
1818
}
1919

2020
type TX struct {
21+
TXID string `json:"txid"`
2122
Vin []*Vin `json:"vin"`
2223
Vout []*Vout `json:"vout"`
2324
}
@@ -71,6 +72,23 @@ func (a *ExplorerAPI) Transaction(txid string) (*TX, error) {
7172
return tx, nil
7273
}
7374

75+
func (a *ExplorerAPI) Outpoint(addr string) (*TX, int, error) {
76+
var txs []*TX
77+
err := fetchJSON(fmt.Sprintf("%s/address/%s/txs", a.BaseURL, addr), &txs)
78+
if err != nil {
79+
return nil, 0, err
80+
}
81+
for _, tx := range txs {
82+
for idx, vout := range tx.Vout {
83+
if vout.ScriptPubkeyAddr == addr {
84+
return tx, idx, nil
85+
}
86+
}
87+
}
88+
89+
return nil, 0, fmt.Errorf("no tx found")
90+
}
91+
7492
func (a *ExplorerAPI) PublishTx(rawTxHex string) (string, error) {
7593
url := fmt.Sprintf("%s/tx", a.BaseURL)
7694
resp, err := http.Post(url, "text/plain", strings.NewReader(rawTxHex))

cmd/chantools/main.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323

2424
const (
2525
defaultAPIURL = "https://blockstream.info/api"
26-
version = "0.4.1"
26+
version = "0.5.0"
2727
)
2828

2929
var (
@@ -86,6 +86,11 @@ func runCommandParser() error {
8686
"sweeptimelock", "Sweep the force-closed state after the time "+
8787
"lock has expired.", "", &sweepTimeLockCommand{},
8888
)
89+
_, _ = parser.AddCommand(
90+
"sweeptimelockmanual", "Sweep the force-closed state of a "+
91+
"single channel manually if only a channel backup "+
92+
"file is available", "", &sweepTimeLockManualCommand{},
93+
)
8994
_, _ = parser.AddCommand(
9095
"dumpchannels", "Dump all channel information from lnd's "+
9196
"channel database.", "", &dumpChannelsCommand{},

cmd/chantools/sweeptimelock.go

Lines changed: 41 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import (
44
"bytes"
55
"encoding/hex"
66
"fmt"
7-
87
"github.com/btcsuite/btcd/btcec"
8+
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
9+
910
"github.com/btcsuite/btcd/chaincfg/chainhash"
1011
"github.com/btcsuite/btcd/txscript"
1112
"github.com/btcsuite/btcd/wire"
@@ -17,14 +18,16 @@ import (
1718
)
1819

1920
const (
20-
feeSatPerByte = 2
21+
defaultFeeSatPerVByte = 2
22+
defaultCsvLimit = 2000
2123
)
2224

2325
type sweepTimeLockCommand struct {
2426
RootKey string `long:"rootkey" description:"BIP32 HD root key to use. Leave empty to prompt for lnd 24 word aezeed."`
2527
Publish bool `long:"publish" description:"Should the sweep TX be published to the chain API?"`
26-
SweepAddr string `long:"sweepaddr" description:"The address the funds should be sweeped to"`
28+
SweepAddr string `long:"sweepaddr" description:"The address the funds should be sweeped to."`
2729
MaxCsvLimit int `long:"maxcsvlimit" description:"Maximum CSV limit to use. (default 2000)"`
30+
FeeRate uint32 `long:"feerate" description:"The fee rate to use for the sweep transaction in sat/vByte. (default 2 sat/vByte)"`
2831
}
2932

3033
func (c *sweepTimeLockCommand) Execute(_ []string) error {
@@ -58,19 +61,22 @@ func (c *sweepTimeLockCommand) Execute(_ []string) error {
5861
return err
5962
}
6063

61-
// Set default value
64+
// Set default values.
6265
if c.MaxCsvLimit == 0 {
63-
c.MaxCsvLimit = 2000
66+
c.MaxCsvLimit = defaultCsvLimit
67+
}
68+
if c.FeeRate == 0 {
69+
c.FeeRate = defaultFeeSatPerVByte
6470
}
6571
return sweepTimeLock(
6672
extendedKey, cfg.APIURL, entries, c.SweepAddr, c.MaxCsvLimit,
67-
c.Publish,
73+
c.Publish, c.FeeRate,
6874
)
6975
}
7076

7177
func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiURL string,
7278
entries []*dataformat.SummaryEntry, sweepAddr string, maxCsvTimeout int,
73-
publish bool) error {
79+
publish bool, feeRate uint32) error {
7480

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

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

145+
lockScript, err := hex.DecodeString(fc.Outs[txindex].Script)
146+
if err != nil {
147+
return fmt.Errorf("error parsing target script: %v",
148+
err)
149+
}
150+
138151
// We can't rely on the CSV delay of the channel DB to be
139152
// correct. But it doesn't cost us a lot to just brute force it.
140153
csvTimeout, script, scriptHash, err := bruteForceDelay(
141154
input.TweakPubKey(delayBase, commitPoint),
142155
input.DeriveRevocationPubkey(revBase, commitPoint),
143-
fc.Outs[txindex].Script, maxCsvTimeout,
156+
lockScript, maxCsvTimeout,
144157
)
145158
if err != nil {
146159
log.Errorf("Could not create matching script for %s "+
@@ -179,40 +192,33 @@ func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiURL string,
179192
}
180193
totalOutputValue += int64(fc.Outs[txindex].Value)
181194
signDescs = append(signDescs, signDesc)
195+
196+
// Account for the input weight.
197+
estimator.AddWitnessInput(input.ToLocalTimeoutWitnessSize)
182198
}
183199

184200
// Add our sweep destination output.
185201
sweepScript, err := lnd.GetP2WPKHScript(sweepAddr, chainParams)
186202
if err != nil {
187203
return err
188204
}
205+
estimator.AddP2WKHOutput()
206+
207+
// Calculate the fee based on the given fee rate and our weight
208+
// estimation.
209+
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()
210+
totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight()))
211+
212+
log.Infof("Fee %d sats of %d total amount (estimated weight %d)",
213+
totalFee, totalOutputValue, estimator.Weight())
214+
189215
sweepTx.TxOut = []*wire.TxOut{{
190-
Value: totalOutputValue,
216+
Value: totalOutputValue - int64(totalFee),
191217
PkScript: sweepScript,
192218
}}
193-
194-
// Very naive fee estimation algorithm: Sign a first time as if we would
195-
// send the whole amount with zero fee, just to estimate how big the
196-
// transaction would get in bytes. Then adjust the fee and sign again.
219+
220+
// Sign the transaction now.
197221
sigHashes := txscript.NewTxSigHashes(sweepTx)
198-
for idx, desc := range signDescs {
199-
desc.SigHashes = sigHashes
200-
desc.InputIndex = idx
201-
witness, err := input.CommitSpendTimeout(signer, desc, sweepTx)
202-
if err != nil {
203-
return err
204-
}
205-
sweepTx.TxIn[idx].Witness = witness
206-
}
207-
208-
// Calculate a fee. This won't be very accurate so the feeSatPerByte
209-
// should at least be 2 to not risk falling below the 1 sat/byte limit.
210-
size := sweepTx.SerializeSize()
211-
fee := int64(size * feeSatPerByte)
212-
sweepTx.TxOut[0].Value = totalOutputValue - fee
213-
214-
// Sign again after output fixing.
215-
sigHashes = txscript.NewTxSigHashes(sweepTx)
216222
for idx, desc := range signDescs {
217223
desc.SigHashes = sigHashes
218224
witness, err := input.CommitSpendTimeout(signer, desc, sweepTx)
@@ -227,8 +233,6 @@ func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiURL string,
227233
if err != nil {
228234
return err
229235
}
230-
log.Infof("Fee %d sats of %d total amount (for size %d)",
231-
fee, totalOutputValue, sweepTx.SerializeSize())
232236

233237
// Publish TX.
234238
if publish {
@@ -251,23 +255,16 @@ func pubKeyFromHex(pubKeyHex string) (*btcec.PublicKey, error) {
251255
if err != nil {
252256
return nil, fmt.Errorf("error hex decoding pub key: %v", err)
253257
}
254-
return btcec.ParsePubKey(
255-
pointBytes, btcec.S256(),
256-
)
258+
return btcec.ParsePubKey(pointBytes, btcec.S256())
257259
}
258260

259261
func bruteForceDelay(delayPubkey, revocationPubkey *btcec.PublicKey,
260-
targetScriptHex string, maxCsvTimeout int) (int32, []byte, []byte,
262+
targetScript []byte, maxCsvTimeout int) (int32, []byte, []byte,
261263
error) {
262264

263-
targetScript, err := hex.DecodeString(targetScriptHex)
264-
if err != nil {
265-
return 0, nil, nil, fmt.Errorf("error parsing target script: "+
266-
"%v", err)
267-
}
268265
if len(targetScript) != 34 {
269266
return 0, nil, nil, fmt.Errorf("invalid target script: %s",
270-
targetScriptHex)
267+
targetScript)
271268
}
272269
for i := 0; i <= maxCsvTimeout; i++ {
273270
s, err := input.CommitScriptToSelf(
@@ -287,5 +284,5 @@ func bruteForceDelay(delayPubkey, revocationPubkey *btcec.PublicKey,
287284
}
288285
}
289286
return 0, nil, nil, fmt.Errorf("csv timeout not found for target "+
290-
"script %s", targetScriptHex)
287+
"script %s", targetScript)
291288
}

0 commit comments

Comments
 (0)