From 4f343dd8f10284be487c7dffc6d30b588b94fda5 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Sun, 30 Aug 2020 14:00:19 +0200 Subject: [PATCH] Add sweeptimelockmanual command --- README.md | 77 +++++-- btc/explorer_api.go | 18 ++ cmd/chantools/main.go | 7 +- cmd/chantools/sweeptimelock.go | 85 ++++---- cmd/chantools/sweeptimelockmanual.go | 302 +++++++++++++++++++++++++++ lnd/hdkeychain.go | 59 ++++++ 6 files changed, 486 insertions(+), 62 deletions(-) create mode 100644 cmd/chantools/sweeptimelockmanual.go diff --git a/README.md b/README.md index 51a6e1c..57544a0 100644 --- a/README.md +++ b/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 ``` diff --git a/btc/explorer_api.go b/btc/explorer_api.go index 7cb0f05..f00ed6d 100644 --- a/btc/explorer_api.go +++ b/btc/explorer_api.go @@ -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)) diff --git a/cmd/chantools/main.go b/cmd/chantools/main.go index 052f393..a57f7ef 100644 --- a/cmd/chantools/main.go +++ b/cmd/chantools/main.go @@ -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{}, diff --git a/cmd/chantools/sweeptimelock.go b/cmd/chantools/sweeptimelock.go index 4a24367..a8e0b2c 100644 --- a/cmd/chantools/sweeptimelock.go +++ b/cmd/chantools/sweeptimelock.go @@ -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) } diff --git a/cmd/chantools/sweeptimelockmanual.go b/cmd/chantools/sweeptimelockmanual.go new file mode 100644 index 0000000..c5584fd --- /dev/null +++ b/cmd/chantools/sweeptimelockmanual.go @@ -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") +} diff --git a/lnd/hdkeychain.go b/lnd/hdkeychain.go index 4ef92b9..9a1fa98 100644 --- a/lnd/hdkeychain.go +++ b/lnd/hdkeychain.go @@ -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