|
|
|
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.ReadAezeed(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")
|
|
|
|
}
|