mirror of https://github.com/guggero/chantools
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
582 lines
18 KiB
Go
582 lines
18 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"fmt"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
"github.com/btcsuite/btcd/txscript"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/lightninglabs/chantools/btc"
|
|
"github.com/lightninglabs/chantools/lnd"
|
|
"github.com/lightningnetwork/lnd/input"
|
|
"github.com/lightningnetwork/lnd/keychain"
|
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
|
"github.com/lightningnetwork/lnd/shachain"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
const (
|
|
keyBasePath = "m/1017'/%d'"
|
|
maxKeys = 500
|
|
maxPoints = 500
|
|
)
|
|
|
|
type sweepTimeLockManualCommand struct {
|
|
APIURL string
|
|
Publish bool
|
|
SweepAddr string
|
|
MaxCsvLimit uint16
|
|
FeeRate uint32
|
|
TimeLockAddr string
|
|
RemoteRevocationBasePoint string
|
|
|
|
MaxNumChannelsTotal uint16
|
|
MaxNumChanUpdates uint64
|
|
|
|
ChannelBackup string
|
|
ChannelPoint string
|
|
|
|
rootKey *rootKey
|
|
inputs *inputFlags
|
|
cmd *cobra.Command
|
|
}
|
|
|
|
func newSweepTimeLockManualCommand() *cobra.Command {
|
|
cc := &sweepTimeLockManualCommand{}
|
|
cc.cmd = &cobra.Command{
|
|
Use: "sweeptimelockmanual",
|
|
Short: "Sweep the force-closed state of a single channel " +
|
|
"manually if only a channel backup file is available",
|
|
Long: `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 command,
|
|
then look up the value for RemoteChanCfg -> RevocationBasePoint -> PubKey.
|
|
|
|
Alternatively you can directly use the --frombackup and --channelpoint flags to
|
|
pull the required information from the given channel.backup file automatically.
|
|
|
|
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: `chantools sweeptimelockmanual \
|
|
--sweepaddr bc1q..... \
|
|
--timelockaddr bc1q............ \
|
|
--remoterevbasepoint 03xxxxxxx \
|
|
--feerate 10 \
|
|
--publish
|
|
|
|
chantools sweeptimelockmanual \
|
|
--sweepaddr bc1q..... \
|
|
--timelockaddr bc1q............ \
|
|
--frombackup channel.backup \
|
|
--channelpoint f39310xxxxxxxxxx:1 \
|
|
--feerate 10 \
|
|
--publish`,
|
|
RunE: cc.Execute,
|
|
}
|
|
cc.cmd.Flags().StringVar(
|
|
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
|
|
"be esplora compatible)",
|
|
)
|
|
cc.cmd.Flags().BoolVar(
|
|
&cc.Publish, "publish", false, "publish sweep TX to the chain "+
|
|
"API instead of just printing the TX",
|
|
)
|
|
cc.cmd.Flags().StringVar(
|
|
&cc.SweepAddr, "sweepaddr", "", "address to sweep the funds to",
|
|
)
|
|
cc.cmd.Flags().Uint16Var(
|
|
&cc.MaxCsvLimit, "maxcsvlimit", defaultCsvLimit, "maximum CSV "+
|
|
"limit to use",
|
|
)
|
|
cc.cmd.Flags().Uint16Var(
|
|
&cc.MaxNumChannelsTotal, "maxnumchanstotal", maxKeys, "maximum "+
|
|
"number of keys to try, set to maximum number of "+
|
|
"channels the local node potentially has or had",
|
|
)
|
|
cc.cmd.Flags().Uint64Var(
|
|
&cc.MaxNumChanUpdates, "maxnumchanupdates", maxPoints,
|
|
"maximum number of channel updates to try, set to maximum "+
|
|
"number of times the channel was used",
|
|
)
|
|
cc.cmd.Flags().Uint32Var(
|
|
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
|
|
"use for the sweep transaction in sat/vByte",
|
|
)
|
|
cc.cmd.Flags().StringVar(
|
|
&cc.TimeLockAddr, "timelockaddr", "", "address of the time "+
|
|
"locked commitment output where the funds are stuck in",
|
|
)
|
|
cc.cmd.Flags().StringVar(
|
|
&cc.RemoteRevocationBasePoint, "remoterevbasepoint", "", ""+
|
|
"remote node's revocation base point, can be found "+
|
|
"in a channel.backup file",
|
|
)
|
|
cc.cmd.Flags().StringVar(
|
|
&cc.ChannelBackup, "frombackup", "", "channel backup file to "+
|
|
"read the channel information from",
|
|
)
|
|
cc.cmd.Flags().StringVar(
|
|
&cc.ChannelPoint, "channelpoint", "", "channel point to use "+
|
|
"for locating the channel in the channel backup file "+
|
|
"specified in the --frombackup flag, "+
|
|
"format: txid:index",
|
|
)
|
|
|
|
cc.rootKey = newRootKey(cc.cmd, "deriving keys")
|
|
cc.inputs = newInputFlags(cc.cmd)
|
|
|
|
return cc.cmd
|
|
}
|
|
|
|
func (c *sweepTimeLockManualCommand) Execute(_ *cobra.Command, _ []string) error {
|
|
extendedKey, err := c.rootKey.read()
|
|
if err != nil {
|
|
return fmt.Errorf("error reading root key: %w", 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")
|
|
}
|
|
|
|
var (
|
|
startCsvLimit uint16
|
|
maxCsvLimit = c.MaxCsvLimit
|
|
startNumChannelsTotal uint16
|
|
maxNumChannelsTotal = c.MaxNumChannelsTotal
|
|
remoteRevocationBasePoint = c.RemoteRevocationBasePoint
|
|
)
|
|
|
|
// We either support specifying the remote revocation base point
|
|
// manually, in which case the CSV limit and number of channels are not
|
|
// known, or we can use the channel backup file to get the required
|
|
// information from there directly.
|
|
switch {
|
|
case c.RemoteRevocationBasePoint != "":
|
|
// Nothing to do here but continue below with the info provided
|
|
// by the user.
|
|
|
|
case c.ChannelBackup != "":
|
|
if c.ChannelPoint == "" {
|
|
return fmt.Errorf("channel point is required with " +
|
|
"--frombackup")
|
|
}
|
|
|
|
backupChan, err := lnd.ExtractChannel(
|
|
extendedKey, chainParams, c.ChannelBackup,
|
|
c.ChannelPoint,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("error extracting channel: %w", err)
|
|
}
|
|
|
|
remoteCfg := backupChan.RemoteChanCfg
|
|
remoteRevocationBasePoint = remoteCfg.RevocationBasePoint.PubKey
|
|
|
|
startCsvLimit = remoteCfg.CsvDelay
|
|
maxCsvLimit = startCsvLimit + 1
|
|
|
|
delayPath, err := lnd.ParsePath(
|
|
backupChan.LocalChanCfg.DelayBasePoint.Path,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing delay path: %w", err)
|
|
}
|
|
if len(delayPath) != 5 {
|
|
return fmt.Errorf("invalid delay path '%v'", delayPath)
|
|
}
|
|
|
|
startNumChannelsTotal = uint16(delayPath[4])
|
|
maxNumChannelsTotal = startNumChannelsTotal + 1
|
|
|
|
case c.ChannelBackup != "" && c.RemoteRevocationBasePoint != "":
|
|
return fmt.Errorf("cannot use both --frombackup and " +
|
|
"--remoterevbasepoint at the same time")
|
|
|
|
default:
|
|
return fmt.Errorf("either --frombackup or " +
|
|
"--remoterevbasepoint is required")
|
|
}
|
|
|
|
// The remote revocation base point must also be set and a valid EC
|
|
// point.
|
|
remoteRevPoint, err := pubKeyFromHex(remoteRevocationBasePoint)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid remote revocation base point: %w",
|
|
err)
|
|
}
|
|
|
|
return sweepTimeLockManual(
|
|
extendedKey, c.APIURL, c.SweepAddr, c.TimeLockAddr,
|
|
remoteRevPoint, startCsvLimit, maxCsvLimit,
|
|
startNumChannelsTotal, maxNumChannelsTotal,
|
|
c.MaxNumChanUpdates, c.Publish, c.FeeRate,
|
|
)
|
|
}
|
|
|
|
func sweepTimeLockManual(extendedKey *hdkeychain.ExtendedKey, apiURL string,
|
|
sweepAddr, timeLockAddr string, remoteRevPoint *btcec.PublicKey,
|
|
startCsvTimeout, maxCsvTimeout, startNumChannels, maxNumChannels uint16,
|
|
maxNumChanUpdates uint64, publish bool, feeRate uint32) error {
|
|
|
|
log.Debugf("Starting to brute force the time lock script, using: "+
|
|
"remote_rev_base_point=%x, start_csv_limit=%d, "+
|
|
"max_csv_limit=%d, start_num_channels=%d, "+
|
|
"max_num_channels=%d, max_num_chan_updates=%d",
|
|
remoteRevPoint.SerializeCompressed(), startCsvTimeout,
|
|
maxCsvTimeout, startNumChannels, maxNumChannels,
|
|
maxNumChanUpdates)
|
|
|
|
// 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: %w", 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: %w", err)
|
|
}
|
|
baseKey, err := lnd.DeriveChildren(extendedKey, basePath)
|
|
if err != nil {
|
|
return fmt.Errorf("could not derive base key: %w", 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 := startNumChannels; i < maxNumChannels; i++ {
|
|
csvTimeout, script, scriptHash, commitPoint, delayDesc, err = tryKey(
|
|
baseKey, remoteRevPoint, startCsvTimeout, maxCsvTimeout,
|
|
lockScript, uint32(i), maxNumChanUpdates,
|
|
)
|
|
|
|
if err == nil {
|
|
log.Infof("Found keys at index %d with CSV timeout %d",
|
|
i, csvTimeout)
|
|
|
|
break
|
|
}
|
|
|
|
log.Infof("Tried %d of %d keys.", i+1, 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: %w", 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.
|
|
prevOutFetcher := txscript.NewCannedPrevOutputFetcher(
|
|
scriptHash, sweepValue,
|
|
)
|
|
sigHashes := txscript.NewTxSigHashes(sweepTx, prevOutFetcher)
|
|
signDesc := &input.SignDescriptor{
|
|
KeyDesc: *delayDesc,
|
|
SingleTweak: input.SingleTweakBytes(
|
|
commitPoint, delayDesc.PubKey,
|
|
),
|
|
WitnessScript: script,
|
|
Output: &wire.TxOut{
|
|
PkScript: scriptHash,
|
|
Value: sweepValue,
|
|
},
|
|
InputIndex: 0,
|
|
SigHashes: sigHashes,
|
|
PrevOutputFetcher: prevOutFetcher,
|
|
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 tryKey(baseKey *hdkeychain.ExtendedKey, remoteRevPoint *btcec.PublicKey,
|
|
startCsvTimeout, maxCsvTimeout uint16, lockScript []byte, idx uint32,
|
|
maxNumChanUpdates uint64) (int32, []byte, []byte, *btcec.PublicKey,
|
|
*keychain.KeyDescriptor, error) {
|
|
|
|
// The easy part first, let's derive the delay base point.
|
|
delayPath := []uint32{
|
|
lnd.HardenedKey(uint32(keychain.KeyFamilyDelayBase)),
|
|
0, idx,
|
|
}
|
|
delayPrivKey, err := lnd.PrivKeyFromPath(baseKey, delayPath)
|
|
if err != nil {
|
|
return 0, nil, nil, nil, nil, err
|
|
}
|
|
|
|
// Get the revocation base point first, so we can calculate our
|
|
// commit point. We start with the old way where the revocation index
|
|
// was the same as the other indices. This applies to all channels
|
|
// opened with versions prior to and including lnd v0.12.0-beta.
|
|
revPath := []uint32{
|
|
lnd.HardenedKey(uint32(
|
|
keychain.KeyFamilyRevocationRoot,
|
|
)), 0, idx,
|
|
}
|
|
revRoot, err := lnd.ShaChainFromPath(baseKey, revPath, nil)
|
|
if err != nil {
|
|
return 0, nil, nil, nil, nil, 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,
|
|
startCsvTimeout, maxCsvTimeout, maxNumChanUpdates,
|
|
)
|
|
if err == nil {
|
|
return csvTimeout, script, scriptHash, commitPoint,
|
|
&keychain.KeyDescriptor{
|
|
PubKey: delayPrivKey.PubKey(),
|
|
KeyLocator: keychain.KeyLocator{
|
|
Family: keychain.KeyFamilyDelayBase,
|
|
Index: idx,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// We could not derive the secrets to sweep the to_local output using
|
|
// the old shachain root creation. Starting with lnd release
|
|
// v0.13.0-beta the index for the revocation path creating the shachain
|
|
// root changed. Now the shachain root is created using ECDH
|
|
// with the local multisig public key
|
|
// (for mainnet: m/1017'/0'/1'/0/idx). But we need to account for a
|
|
// special case here. If the node was started with a version prior to
|
|
// and including v0.12.0-beta the idx for the new shachain root
|
|
// revocation is not one larger because idx 0 was already used for the
|
|
// old creation scheme hence we need to replicate this behaviour here.
|
|
// First trying the shachain root creation with the same index and if
|
|
// this does not derive the secrets we increase the index of the
|
|
// revocation key path by one (for mainnet: m/1017'/0'/5'/0/idx+1).
|
|
// The exact path which was used for the shachain root can be seen
|
|
// in the channel.backup file for every specific channel. The old
|
|
// scheme has always a public key specified.The new one uses a key
|
|
// locator and does not have a public key specified (nil).
|
|
// Example
|
|
// ShaChainRootDesc: (dump.KeyDescriptor) {
|
|
// Path: (string) (len=17) "m/1017'/1'/5'/0/1",
|
|
// PubKey: (string) (len=5) "<nil>"
|
|
//
|
|
// For more details:
|
|
// https://github.com/lightningnetwork/lnd/commit/bb84f0ebc88620050dec7cf4be6283f5cba8b920
|
|
//
|
|
// Now the new shachain root revocation scheme is tried with
|
|
// two different indicies as described above.
|
|
revPath2 := []uint32{
|
|
lnd.HardenedKey(uint32(
|
|
keychain.KeyFamilyRevocationRoot,
|
|
)), 0, idx,
|
|
}
|
|
|
|
// Now we try the same with the new revocation producer format.
|
|
multiSigPath := []uint32{
|
|
lnd.HardenedKey(uint32(keychain.KeyFamilyMultiSig)),
|
|
0, idx,
|
|
}
|
|
multiSigPrivKey, err := lnd.PrivKeyFromPath(baseKey, multiSigPath)
|
|
if err != nil {
|
|
return 0, nil, nil, nil, nil, err
|
|
}
|
|
|
|
revRoot2, err := lnd.ShaChainFromPath(
|
|
baseKey, revPath2, multiSigPrivKey.PubKey(),
|
|
)
|
|
if err != nil {
|
|
return 0, nil, nil, nil, nil, err
|
|
}
|
|
|
|
csvTimeout, script, scriptHash, commitPoint, err = bruteForceDelayPoint(
|
|
delayPrivKey.PubKey(), remoteRevPoint, revRoot2, lockScript,
|
|
startCsvTimeout, maxCsvTimeout, maxNumChanUpdates,
|
|
)
|
|
if err == nil {
|
|
return csvTimeout, script, scriptHash, commitPoint,
|
|
&keychain.KeyDescriptor{
|
|
PubKey: delayPrivKey.PubKey(),
|
|
KeyLocator: keychain.KeyLocator{
|
|
Family: keychain.KeyFamilyDelayBase,
|
|
Index: idx,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// Now we try to increase the index by 1 to account for the situation
|
|
// where the node was started with a version after (including)
|
|
// v0.13.0-beta
|
|
revPath3 := []uint32{
|
|
lnd.HardenedKey(uint32(
|
|
keychain.KeyFamilyRevocationRoot,
|
|
)), 0, idx + 1,
|
|
}
|
|
|
|
// Now we try the same with the new revocation producer format.
|
|
multiSigPath = []uint32{
|
|
lnd.HardenedKey(uint32(keychain.KeyFamilyMultiSig)),
|
|
0, idx,
|
|
}
|
|
multiSigPrivKey, err = lnd.PrivKeyFromPath(baseKey, multiSigPath)
|
|
if err != nil {
|
|
return 0, nil, nil, nil, nil, err
|
|
}
|
|
|
|
revRoot3, err := lnd.ShaChainFromPath(
|
|
baseKey, revPath3, multiSigPrivKey.PubKey(),
|
|
)
|
|
if err != nil {
|
|
return 0, nil, nil, nil, nil, err
|
|
}
|
|
|
|
csvTimeout, script, scriptHash, commitPoint, err = bruteForceDelayPoint(
|
|
delayPrivKey.PubKey(), remoteRevPoint, revRoot3, lockScript,
|
|
startCsvTimeout, maxCsvTimeout, maxNumChanUpdates,
|
|
)
|
|
if err == nil {
|
|
return csvTimeout, script, scriptHash, commitPoint,
|
|
&keychain.KeyDescriptor{
|
|
PubKey: delayPrivKey.PubKey(),
|
|
KeyLocator: keychain.KeyLocator{
|
|
Family: keychain.KeyFamilyDelayBase,
|
|
Index: idx,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
return 0, nil, nil, nil, nil, fmt.Errorf("target script not derived")
|
|
}
|
|
|
|
func bruteForceDelayPoint(delayBase, revBase *btcec.PublicKey,
|
|
revRoot *shachain.RevocationProducer, lockScript []byte,
|
|
startCsvTimeout, maxCsvTimeout uint16, maxChanUpdates uint64) (int32,
|
|
[]byte, []byte, *btcec.PublicKey, error) {
|
|
|
|
for i := uint64(0); i < maxChanUpdates; 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, startCsvTimeout, maxCsvTimeout,
|
|
)
|
|
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
return csvTimeout, script, scriptHash, commitPoint, nil
|
|
}
|
|
|
|
return 0, nil, nil, nil, fmt.Errorf("target script not derived")
|
|
}
|