mirror of
https://github.com/guggero/chantools
synced 2024-11-07 03:20:43 +00:00
Add sweeptimelockmanual command
This commit is contained in:
parent
18ef40fe5a
commit
4f343dd8f1
77
README.md
77
README.md
@ -21,6 +21,7 @@
|
||||
+ [signrescuefunding](#signrescuefunding)
|
||||
+ [summary](#summary)
|
||||
+ [sweeptimelock](#sweeptimelock)
|
||||
+ [sweeptimelockmanual](#sweeptimelockmanual)
|
||||
+ [vanitygen](#vanitygen)
|
||||
+ [walletinfo](#walletinfo)
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
```
|
||||
|
@ -18,6 +18,7 @@ type ExplorerAPI struct {
|
||||
}
|
||||
|
||||
type TX struct {
|
||||
TXID string `json:"txid"`
|
||||
Vin []*Vin `json:"vin"`
|
||||
Vout []*Vout `json:"vout"`
|
||||
}
|
||||
@ -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))
|
||||
|
@ -23,7 +23,7 @@ import (
|
||||
|
||||
const (
|
||||
defaultAPIURL = "https://blockstream.info/api"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -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{},
|
||||
|
@ -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"
|
||||
@ -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 {
|
||||
@ -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{
|
||||
@ -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.
|
||||
@ -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 "+
|
||||
@ -179,6 +192,9 @@ 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.
|
||||
@ -186,33 +202,23 @@ func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiURL string,
|
||||
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)
|
||||
@ -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 {
|
||||
@ -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(
|
||||
@ -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)
|
||||
}
|
||||
|
302
cmd/chantools/sweeptimelockmanual.go
Normal file
302
cmd/chantools/sweeptimelockmanual.go
Normal file
@ -0,0 +1,302 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil/hdkeychain"
|
||||
"github.com/guggero/chantools/btc"
|
||||
"github.com/guggero/chantools/lnd"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/lightningnetwork/lnd/shachain"
|
||||
)
|
||||
|
||||
const (
|
||||
keyBasePath = "m/1017'/%d'"
|
||||
maxKeys = 500
|
||||
maxPoints = 500
|
||||
)
|
||||
|
||||
type sweepTimeLockManualCommand 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."`
|
||||
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)"`
|
||||
TimeLockAddr string `long:"timelockaddr" description:"The address of the time locked commitment output where the funds are stuck in."`
|
||||
RemoteRevocationBasePoint string `long:"remoterevbasepoint" description:"The remote's revocation base point, can be found in a channel.backup file."`
|
||||
}
|
||||
|
||||
func (c *sweepTimeLockManualCommand) Execute(_ []string) error {
|
||||
setupChainParams(cfg)
|
||||
|
||||
var (
|
||||
extendedKey *hdkeychain.ExtendedKey
|
||||
err error
|
||||
)
|
||||
|
||||
// Check that root key is valid or fall back to console input.
|
||||
switch {
|
||||
case c.RootKey != "":
|
||||
extendedKey, err = hdkeychain.NewKeyFromString(c.RootKey)
|
||||
|
||||
default:
|
||||
extendedKey, _, err = lnd.ReadAezeedFromTerminal(chainParams)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading root key: %v", err)
|
||||
}
|
||||
|
||||
// Make sure the sweep and time lock addrs are set.
|
||||
if c.SweepAddr == "" {
|
||||
return fmt.Errorf("sweep addr is required")
|
||||
}
|
||||
if c.TimeLockAddr == "" {
|
||||
return fmt.Errorf("time lock addr is required")
|
||||
}
|
||||
|
||||
// The remote revocation base point must also be set and a valid EC
|
||||
// point.
|
||||
remoteRevPoint, err := pubKeyFromHex(c.RemoteRevocationBasePoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid remote revocation base point: %v",
|
||||
err)
|
||||
}
|
||||
|
||||
// Set default values.
|
||||
if c.MaxCsvLimit == 0 {
|
||||
c.MaxCsvLimit = defaultCsvLimit
|
||||
}
|
||||
if c.FeeRate == 0 {
|
||||
c.FeeRate = defaultFeeSatPerVByte
|
||||
}
|
||||
return sweepTimeLockManual(
|
||||
extendedKey, cfg.APIURL, c.SweepAddr, c.TimeLockAddr,
|
||||
remoteRevPoint, c.MaxCsvLimit, c.Publish, c.FeeRate,
|
||||
)
|
||||
}
|
||||
|
||||
func sweepTimeLockManual(extendedKey *hdkeychain.ExtendedKey, apiURL string,
|
||||
sweepAddr, timeLockAddr string, remoteRevPoint *btcec.PublicKey,
|
||||
maxCsvTimeout int, 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
|
||||
// continue anyway.
|
||||
lockScript, err := lnd.GetP2WSHScript(timeLockAddr, chainParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid time lock addr: %v", err)
|
||||
}
|
||||
|
||||
// We need to go through a lot of our keys so it makes sense to
|
||||
// pre-derive the static part of our key path.
|
||||
basePath, err := lnd.ParsePath(fmt.Sprintf(
|
||||
keyBasePath, chainParams.HDCoinType,
|
||||
))
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not derive base path: %v", err)
|
||||
}
|
||||
baseKey, err := lnd.DeriveChildren(extendedKey, basePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not derive base key: %v", err)
|
||||
}
|
||||
|
||||
// Go through all our keys now and try to find the ones that can derive
|
||||
// the script. This loop can take very long as it'll nest three times,
|
||||
// once for the key index, once for the commit points and once for the
|
||||
// CSV values. Most of the calculations should be rather cheap but the
|
||||
// number of iterations can go up to maxKeys*maxPoints*maxCsvTimeout.
|
||||
var (
|
||||
csvTimeout int32
|
||||
script []byte
|
||||
scriptHash []byte
|
||||
delayDesc *keychain.KeyDescriptor
|
||||
commitPoint *btcec.PublicKey
|
||||
)
|
||||
for i := uint32(0); i < maxKeys; i++ {
|
||||
// The easy part first, let's derive the delay base point.
|
||||
delayPath := []uint32{
|
||||
lnd.HardenedKey(uint32(keychain.KeyFamilyDelayBase)), 0,
|
||||
i,
|
||||
}
|
||||
delayPrivKey, err := lnd.PrivKeyFromPath(baseKey, delayPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the revocation base point first so we can calculate our
|
||||
// commit point.
|
||||
revPath := []uint32{
|
||||
lnd.HardenedKey(uint32(
|
||||
keychain.KeyFamilyRevocationRoot,
|
||||
)), 0, i,
|
||||
}
|
||||
revRoot, err := lnd.ShaChainFromPath(baseKey, revPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We now have everything to brute force the lock script. This
|
||||
// will take a long while as we both have to go through commit
|
||||
// points and CSV values.
|
||||
csvTimeout, script, scriptHash, commitPoint, err =
|
||||
bruteForceDelayPoint(
|
||||
delayPrivKey.PubKey(), remoteRevPoint, revRoot,
|
||||
lockScript, maxCsvTimeout,
|
||||
)
|
||||
|
||||
if err == nil {
|
||||
delayDesc = &keychain.KeyDescriptor{
|
||||
PubKey: delayPrivKey.PubKey(),
|
||||
KeyLocator: keychain.KeyLocator{
|
||||
Family: keychain.KeyFamilyDelayBase,
|
||||
Index: i,
|
||||
},
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if i != 0 && i%20 == 0 {
|
||||
fmt.Printf("Tried %d of %d keys.", i, maxKeys)
|
||||
}
|
||||
}
|
||||
|
||||
// Did we find what we looked for or did we just exhaust all
|
||||
// possibilities?
|
||||
if script == nil || delayDesc == nil {
|
||||
return fmt.Errorf("target script not derived")
|
||||
}
|
||||
|
||||
// Create signer and transaction template.
|
||||
signer := &lnd.Signer{
|
||||
ExtendedKey: extendedKey,
|
||||
ChainParams: chainParams,
|
||||
}
|
||||
api := &btc.ExplorerAPI{BaseURL: apiURL}
|
||||
|
||||
// We now know everything we need to construct the sweep transaction,
|
||||
// except for what outpoint to sweep. We'll ask the chain API to give
|
||||
// us this information.
|
||||
tx, txindex, err := api.Outpoint(timeLockAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error looking up lock address %s on chain: "+
|
||||
"%v", timeLockAddr, err)
|
||||
}
|
||||
|
||||
sweepTx := wire.NewMsgTx(2)
|
||||
sweepValue := int64(tx.Vout[txindex].Value)
|
||||
|
||||
// Create the transaction input.
|
||||
txHash, err := chainhash.NewHashFromStr(tx.TXID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing tx hash: %v", err)
|
||||
}
|
||||
sweepTx.TxIn = []*wire.TxIn{{
|
||||
PreviousOutPoint: wire.OutPoint{
|
||||
Hash: *txHash,
|
||||
Index: uint32(txindex),
|
||||
},
|
||||
Sequence: input.LockTimeToSequence(
|
||||
false, uint32(csvTimeout),
|
||||
),
|
||||
}}
|
||||
|
||||
// Calculate the fee based on the given fee rate and our weight
|
||||
// estimation.
|
||||
var estimator input.TxWeightEstimator
|
||||
estimator.AddWitnessInput(input.ToLocalTimeoutWitnessSize)
|
||||
estimator.AddP2WKHOutput()
|
||||
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()
|
||||
totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight()))
|
||||
|
||||
// Add our sweep destination output.
|
||||
sweepScript, err := lnd.GetP2WPKHScript(sweepAddr, chainParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sweepTx.TxOut = []*wire.TxOut{{
|
||||
Value: sweepValue - int64(totalFee),
|
||||
PkScript: sweepScript,
|
||||
}}
|
||||
|
||||
log.Infof("Fee %d sats of %d total amount (estimated weight %d)",
|
||||
totalFee, sweepValue, estimator.Weight())
|
||||
|
||||
// Create the sign descriptor for the input then sign the transaction.
|
||||
sigHashes := txscript.NewTxSigHashes(sweepTx)
|
||||
signDesc := &input.SignDescriptor{
|
||||
KeyDesc: *delayDesc,
|
||||
SingleTweak: input.SingleTweakBytes(
|
||||
commitPoint, delayDesc.PubKey,
|
||||
),
|
||||
WitnessScript: script,
|
||||
Output: &wire.TxOut{
|
||||
PkScript: scriptHash,
|
||||
Value: sweepValue,
|
||||
},
|
||||
InputIndex: 0,
|
||||
SigHashes: sigHashes,
|
||||
HashType: txscript.SigHashAll,
|
||||
}
|
||||
witness, err := input.CommitSpendTimeout(signer, signDesc, sweepTx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sweepTx.TxIn[0].Witness = witness
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = sweepTx.Serialize(&buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Publish TX.
|
||||
if publish {
|
||||
response, err := api.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
|
||||
|
||||
}
|
||||
|
||||
func bruteForceDelayPoint(delayBase, revBase *btcec.PublicKey,
|
||||
revRoot *shachain.RevocationProducer, lockScript []byte,
|
||||
maxCsvTimeout int) (int32, []byte, []byte, *btcec.PublicKey, error) {
|
||||
|
||||
for i := uint64(0); i < maxPoints; i++ {
|
||||
revPreimage, err := revRoot.AtIndex(i)
|
||||
if err != nil {
|
||||
return 0, nil, nil, nil, err
|
||||
}
|
||||
commitPoint := input.ComputeCommitmentPoint(revPreimage[:])
|
||||
|
||||
csvTimeout, script, scriptHash, err := bruteForceDelay(
|
||||
input.TweakPubKey(delayBase, commitPoint),
|
||||
input.DeriveRevocationPubkey(revBase, commitPoint),
|
||||
lockScript, maxCsvTimeout,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
return csvTimeout, script, scriptHash, commitPoint, nil
|
||||
}
|
||||
|
||||
return 0, nil, nil, nil, fmt.Errorf("target script not derived")
|
||||
}
|
@ -2,7 +2,9 @@ package lnd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/lightningnetwork/lnd/shachain"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@ -62,6 +64,10 @@ func ParsePath(path string) ([]uint32, error) {
|
||||
return indices, nil
|
||||
}
|
||||
|
||||
func HardenedKey(key uint32) uint32 {
|
||||
return key + HardenedKeyStart
|
||||
}
|
||||
|
||||
// DeriveKey derives the public key and private key in the WIF format for a
|
||||
// given key path of the extended key.
|
||||
func DeriveKey(extendedKey *hdkeychain.ExtendedKey, path string,
|
||||
@ -98,6 +104,35 @@ func DeriveKey(extendedKey *hdkeychain.ExtendedKey, path string,
|
||||
return derivedKey, pubKey, wif, nil
|
||||
}
|
||||
|
||||
func PrivKeyFromPath(extendedKey *hdkeychain.ExtendedKey,
|
||||
path []uint32) (*btcec.PrivateKey, error) {
|
||||
|
||||
derivedKey, err := DeriveChildren(extendedKey, path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not derive children: %v", err)
|
||||
}
|
||||
privKey, err := derivedKey.ECPrivKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not derive private key: %v", err)
|
||||
}
|
||||
return privKey, nil
|
||||
}
|
||||
|
||||
func ShaChainFromPath(extendedKey *hdkeychain.ExtendedKey,
|
||||
path []uint32) (*shachain.RevocationProducer, error) {
|
||||
|
||||
privKey, err := PrivKeyFromPath(extendedKey, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
revRoot, err := chainhash.NewHash(privKey.Serialize())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create revocation root "+
|
||||
"hash: %v", err)
|
||||
}
|
||||
return shachain.NewRevocationProducer(*revRoot), nil
|
||||
}
|
||||
|
||||
func AllDerivationPaths(params *chaincfg.Params) ([]string, [][]uint32, error) {
|
||||
mkPath := func(f keychain.KeyFamily) string {
|
||||
return fmt.Sprintf(
|
||||
@ -194,6 +229,30 @@ func GetP2WPKHScript(addr string, chainParams *chaincfg.Params) ([]byte,
|
||||
return builder.Script()
|
||||
}
|
||||
|
||||
// GetP2WSHScript creates a P2WSH output script from an address. If the address
|
||||
// is not a P2WSH address, an error is returned.
|
||||
func GetP2WSHScript(addr string, chainParams *chaincfg.Params) ([]byte,
|
||||
error) {
|
||||
|
||||
targetScriptHash, isScriptHash, err := DecodeAddressHash(
|
||||
addr, chainParams,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !isScriptHash {
|
||||
return nil, fmt.Errorf("address %s is not a P2WSH address",
|
||||
addr)
|
||||
}
|
||||
|
||||
builder := txscript.NewScriptBuilder()
|
||||
builder.AddOp(txscript.OP_0)
|
||||
builder.AddData(targetScriptHash)
|
||||
|
||||
return builder.Script()
|
||||
}
|
||||
|
||||
type HDKeyRing struct {
|
||||
ExtendedKey *hdkeychain.ExtendedKey
|
||||
ChainParams *chaincfg.Params
|
||||
|
Loading…
Reference in New Issue
Block a user