Polish code and document commands

pull/3/head
Oliver Gugger 4 years ago
parent 48608729a5
commit 08461f849e
No known key found for this signature in database
GPG Key ID: 8E4256593F177720

@ -1,7 +1,150 @@
# Channel tools
This tool works with the output of lnd's `listchannels` command and creates
a summary of the on-chain state of these channels.
This tool provides four helper functions that can be used to rescue funds locked
in lnd channels in case lnd itself cannot run properly any more.
**WARNING**: This tool will query public block explorer APIs, your privacy
**WARNING**: This tool was specifically built for a certain rescue operation and
might not be well-suited for your use case. Or not all edge cases for your needs
are coded properly. Please look at the code to understand what it does before
you use it for anything serious.
**WARNING 2**: This tool will query public block explorer APIs, your privacy
might not be preserved. Use at your own risk.
## Overview
```text
Usage:
chantools [OPTIONS] <command>
Application Options:
--apiurl= API URL to use (must be esplora compatible). (default: https://blockstream.info/api)
--listchannels= The channel input is in the format of lncli's listchannels format. Specify '-' to read from stdin.
--pendingchannels= The channel input is in the format of lncli's pendingchannels format. Specify '-' to read from stdin.
--fromsummary= The channel input is in the format of this tool's channel summary. Specify '-' to read from stdin.
--fromchanneldb= The channel input is in the format of an lnd channel.db file.
Help Options:
-h, --help Show this help message
Available commands:
forceclose Force-close the last state that is in the channel.db provided
rescueclosed Try finding the private keys for funds that are in outputs of remotely force-closed channels
summary Compile a summary about the current state of channels.
sweeptimelock Sweep the force-closed state after the time lock has expired
```
## summary command
```text
Usage:
chantools [OPTIONS] summary
```
From a list of channels, find out what their state is by querying the funding
transaction on a block explorer API.
Example command 1:
```bash
lncli listchannels | chantools --listchannels - summary
```
Example command 2:
```bash
chantools --fromchanneldb ~/.lnd/data/graph/mainnet/channel.db
```
## rescueclosed command
```text
Usage:
chantools [OPTIONS] rescueclosed [rescueclosed-OPTIONS]
[rescueclosed command options]
--rootkey= BIP32 HD root key to use.
--channeldb= The lnd channel.db file to use for rescuing force-closed channels.
```
If channels have already been force-closed by the remote peer, this command
tries to find the private keys to sweep the funds from the output that belongs
to our side. This can only be used if we have a channel DB that contains the
latest commit point. Normally you would use SCB to get the funds from those
channels. But this method can help if the other node doesn't know about the
channels any more but we still have the channel.db from the moment they
force-closed.
Example command:
```bash
chantools --fromsummary results/summary-xxxx-yyyy.json \
rescueclosed \
--channeldb ~/.lnd/data/graph/mainnet/channel.db \
--rootkey xprvxxxxxxxxxx
```
## forceclose command
```text
Usage:
chantools [OPTIONS] forceclose [forceclose-OPTIONS]
[forceclose command options]
--rootkey= BIP32 HD root key to use.
--channeldb= The lnd channel.db file to use for force-closing channels.
--publish Should the force-closing TX be published to the chain API?
```
If you are certain that a node is offline for good (AFTER you've tried SCB!) and
a channel is still open, you can use this method to force-close your latest
state that you have in your channel.db.
**!!! WARNING !!! DANGER !!! WARNING !!!**
If you do this and the state that you publish is *not* the latest state, then
the remote node *could* punish you by taking the whole channel amount *if* they
come online before you can sweep the funds from the time locked (144 - 2000
blocks) transaction *or* they have a watch tower looking out for them.
**This should absolutely be the last resort and you have been warned!**
Example command:
```bash
chantools --fromsummary results/summary-xxxx-yyyy.json \
forceclose \
--channeldb ~/.lnd/data/graph/mainnet/channel.db \
--rootkey xprvxxxxxxxxxx \
--publish
```
## sweeptimelock command
```text
Usage:
chantools [OPTIONS] sweeptimelock [sweeptimelock-OPTIONS]
[sweeptimelock command options]
--rootkey= BIP32 HD root key to use.
--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)
```
Use this command to sweep the funds from channels that you force-closed with the
`forceclose` command. You **MUST** use the result file that was created with the
`forceclose` command, otherwise it won't work. You also have to wait until the
highest time lock (can be up to 2000 blocks which is more than two weeks) of all
the channels has passed. If you only want to sweep channels that have the
default CSV limit of 1 day, you can set the `--maxcsvlimit` parameter to 144.
Example command:
```bash
chantools --fromsummary results/forceclose-xxxx-yyyy.json \
sweeptimelock
--rootkey xprvxxxxxxxxxx \
--publish \
--sweepaddr bc1q.....
```

@ -3,7 +3,7 @@ package main
import (
"fmt"
"os"
"github.com/guggero/chantools"
)

@ -1,5 +1,7 @@
package chantools
import "github.com/lightningnetwork/lnd/keychain"
type ClosingTX struct {
TXID string `json:"txid"`
ForceClose bool `json:"force_close"`
@ -9,10 +11,19 @@ type ClosingTX struct {
ConfHeight uint32 `json:"conf_height"`
}
type Basepoint struct {
type BasePoint struct {
Family uint16 `json:"family,omitempty"`
Index uint32 `json:"index,omitempty"`
Pubkey string `json:"pubkey"`
PubKey string `json:"pubkey"`
}
func (b *BasePoint) toDesc() *keychain.KeyDescriptor {
return &keychain.KeyDescriptor{
KeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamily(b.Family),
Index: b.Index,
},
}
}
type Out struct {
@ -25,8 +36,8 @@ type ForceClose struct {
TXID string `json:"txid"`
Serialized string `json:"serialized"`
CSVDelay uint16 `json:"csv_delay"`
DelayBasepoint *Basepoint `json:"delay_basepoint"`
RevocationBasepoint *Basepoint `json:"revocation_basepoint"`
DelayBasePoint *BasePoint `json:"delay_basepoint"`
RevocationBasePoint *BasePoint `json:"revocation_basepoint"`
CommitPoint string `json:"commit_point"`
Outs []*Out `json:"outs"`
}

@ -16,20 +16,14 @@ import (
"github.com/lightningnetwork/lnd/input"
)
func forceCloseChannels(cfg *config, entries []*SummaryEntry,
chanDb *channeldb.DB, publish bool) error {
func forceCloseChannels(extendedKey *hdkeychain.ExtendedKey,
entries []*SummaryEntry, chanDb *channeldb.DB, publish bool) error {
channels, err := chanDb.FetchAllChannels()
if err != nil {
return err
}
chainApi := &chainApi{baseUrl: cfg.ApiUrl}
extendedKey, err := hdkeychain.NewKeyFromString(cfg.RootKey)
if err != nil {
return err
}
signer := &signer{extendedKey: extendedKey}
// Go through all channels in the DB, find the still open ones and
@ -51,8 +45,8 @@ func forceCloseChannels(cfg *config, entries []*SummaryEntry,
localCommit := channel.LocalCommitment
localCommitTx := localCommit.CommitTx
if localCommitTx == nil {
log.Errorf("Cannot force-close, no local commit TX for "+
"channel %s", channelEntry.ChannelPoint)
log.Errorf("Cannot force-close, no local commit TX "+
"for channel %s", channelEntry.ChannelPoint)
continue
}
@ -91,18 +85,22 @@ func forceCloseChannels(cfg *config, entries []*SummaryEntry,
return err
}
point := input.ComputeCommitmentPoint(revocationPreimage[:])
// Store all information that we collected into the channel
// entry file so we don't need to use the channel.db file for
// the next step.
channelEntry.ForceClose = &ForceClose{
TXID: hash.String(),
Serialized: serialized,
DelayBasepoint: &Basepoint{
DelayBasePoint: &BasePoint{
Family: uint16(basepoint.Family),
Index: basepoint.Index,
Pubkey: hex.EncodeToString(
PubKey: hex.EncodeToString(
basepoint.PubKey.SerializeCompressed(),
),
},
RevocationBasepoint: &Basepoint{
Pubkey: hex.EncodeToString(
RevocationBasePoint: &BasePoint{
PubKey: hex.EncodeToString(
revpoint.PubKey.SerializeCompressed(),
),
},
@ -130,8 +128,8 @@ func forceCloseChannels(cfg *config, entries []*SummaryEntry,
if err != nil {
return err
}
log.Infof("Published TX %s, response: %s", hash.String(),
response)
log.Infof("Published TX %s, response: %s",
hash.String(), response)
}
}

@ -16,12 +16,10 @@ const (
type config struct {
ApiUrl string `long:"apiurl" description:"API URL to use (must be esplora compatible)."`
RootKey string `long:"rootkey" description:"BIP32 HD root key to use."`
ListChannels string `long:"listchannels" description:"The channel input is in the format of lncli's listchannels format. Specify '-' to read from stdin."`
PendingChannels string `long:"pendingchannels" description:"The channel input is in the format of lncli's pendingchannels format. Specify '-' to read from stdin."`
FromSummary string `long:"fromsummary" description:"The channel input is in the format of this tool's channel summary. Specify '-' to read from stdin."`
FromChannelDB string `long:"fromchanneldb" description:"The channel input is in the format of an lnd channel.db file."`
ChannelDB string `long:"channeldb" description:"The lnd channel.db file to use for rescuing or force-closing channels."`
}
var (
@ -47,12 +45,12 @@ func Main() error {
&rescueClosedCommand{},
)
_, _ = parser.AddCommand(
"forceclose", "Force-close the last state that is in the " +
"forceclose", "Force-close the last state that is in the "+
"channel.db provided", "",
&forceCloseCommand{},
)
_, _ = parser.AddCommand(
"sweeptimelock", "Sweep the force-closed state after the time " +
"sweeptimelock", "Sweep the force-closed state after the time "+
"lock has expired", "",
&sweepTimeLockCommand{},
)
@ -69,26 +67,29 @@ func (c *summaryCommand) Execute(args []string) error {
if err != nil {
return err
}
return collectChanSummary(cfg, entries)
return summarizeChannels(cfg.ApiUrl, entries)
}
type rescueClosedCommand struct{}
type rescueClosedCommand struct {
RootKey string `long:"rootkey" description:"BIP32 HD root key to use."`
ChannelDB string `long:"channeldb" description:"The lnd channel.db file to use for rescuing force-closed channels."`
}
func (c *rescueClosedCommand) Execute(args []string) error {
// Check that root key is valid.
if cfg.RootKey == "" {
if c.RootKey == "" {
return fmt.Errorf("root key is required")
}
_, err := hdkeychain.NewKeyFromString(cfg.RootKey)
extendedKey, err := hdkeychain.NewKeyFromString(c.RootKey)
if err != nil {
return fmt.Errorf("error parsing root key: %v", err)
}
// Check that we have a channel DB.
if cfg.ChannelDB == "" {
if c.ChannelDB == "" {
return fmt.Errorf("rescue DB is required")
}
db, err := channeldb.Open(path.Dir(cfg.ChannelDB))
db, err := channeldb.Open(path.Dir(c.ChannelDB))
if err != nil {
return fmt.Errorf("error opening rescue DB: %v", err)
}
@ -98,19 +99,29 @@ func (c *rescueClosedCommand) Execute(args []string) error {
if err != nil {
return err
}
return bruteForceChannels(cfg, entries, db)
return rescueClosedChannels(extendedKey, entries, db)
}
type forceCloseCommand struct {
Publish bool `long:"publish" description:"Should the force-closing TX be published to the chain API?"`
RootKey string `long:"rootkey" description:"BIP32 HD root key to use."`
ChannelDB string `long:"channeldb" description:"The lnd channel.db file to use for force-closing channels."`
Publish bool `long:"publish" description:"Should the force-closing TX be published to the chain API?"`
}
func (c *forceCloseCommand) Execute(args []string) error {
// Check that root key is valid.
if c.RootKey == "" {
return fmt.Errorf("root key is required")
}
extendedKey, err := hdkeychain.NewKeyFromString(c.RootKey)
if err != nil {
return fmt.Errorf("error parsing root key: %v", err)
}
// Check that we have a channel DB.
if cfg.ChannelDB == "" {
if c.ChannelDB == "" {
return fmt.Errorf("rescue DB is required")
}
db, err := channeldb.Open(path.Dir(cfg.ChannelDB))
db, err := channeldb.Open(path.Dir(c.ChannelDB))
if err != nil {
return fmt.Errorf("error opening rescue DB: %v", err)
}
@ -120,24 +131,26 @@ func (c *forceCloseCommand) Execute(args []string) error {
if err != nil {
return err
}
return forceCloseChannels(cfg, entries, db, c.Publish)
return forceCloseChannels(extendedKey, entries, db, c.Publish)
}
type sweepTimeLockCommand struct {
Publish bool `long:"publish" description:"Should the sweep TX be published to the chain API?"`
RootKey string `long:"rootkey" description:"BIP32 HD root key to use."`
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)"`
}
func (c *sweepTimeLockCommand) Execute(args []string) error {
// Check that root key is valid.
if cfg.RootKey == "" {
if c.RootKey == "" {
return fmt.Errorf("root key is required")
}
_, err := hdkeychain.NewKeyFromString(cfg.RootKey)
extendedKey, err := hdkeychain.NewKeyFromString(c.RootKey)
if err != nil {
return fmt.Errorf("error parsing root key: %v", err)
}
// Make sure sweep addr is set.
if c.SweepAddr == "" {
return fmt.Errorf("sweep addr is required")
@ -148,7 +161,15 @@ func (c *sweepTimeLockCommand) Execute(args []string) error {
if err != nil {
return err
}
return sweepTimeLock(cfg, entries, c.SweepAddr, c.Publish)
// Set default value
if c.MaxCsvLimit == 0 {
c.MaxCsvLimit = 2000
}
return sweepTimeLock(
extendedKey, cfg.ApiUrl, entries, c.SweepAddr, c.MaxCsvLimit,
c.Publish,
)
}
func setupLogging() {

@ -31,10 +31,10 @@ type cacheEntry struct {
pubKey *btcec.PublicKey
}
func bruteForceChannels(cfg *config, entries []*SummaryEntry,
chanDb *channeldb.DB) error {
func rescueClosedChannels(extendedKey *hdkeychain.ExtendedKey,
entries []*SummaryEntry, chanDb *channeldb.DB) error {
err := fillCache(cfg.RootKey)
err := fillCache(extendedKey)
if err != nil {
return err
}
@ -148,12 +148,7 @@ func addrInCache(addr string, perCommitPoint *btcec.PublicKey) (string, error) {
return "", errAddrNotFound
}
func fillCache(rootKey string) error {
extendedKey, err := hdkeychain.NewKeyFromString(rootKey)
if err != nil {
return err
}
func fillCache(extendedKey *hdkeychain.ExtendedKey) error {
cache = make([]*cacheEntry, cacheSize)
for i := 0; i < cacheSize; i++ {
@ -230,4 +225,4 @@ func parseAddr(addr string) ([]byte, error) {
return nil, fmt.Errorf("address: must be a bech32 P2WPKH address")
}
return targetPubKeyHash, nil
}
}

@ -2,6 +2,7 @@ package chantools
import (
"fmt"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
@ -24,12 +25,8 @@ func (s *signer) SignOutputRaw(tx *wire.MsgTx,
if err != nil {
return nil, err
}
privKey, err = maybeTweakPrivKey(signDesc, privKey)
if err != nil {
return nil, err
}
privKey = maybeTweakPrivKey(signDesc, privKey)
amt := signDesc.Output.Value
sig, err := txscript.RawTxInWitnessSignature(
tx, signDesc.SigHashes, signDesc.InputIndex, amt,
@ -64,26 +61,14 @@ func (s *signer) fetchPrivKey(descriptor *keychain.KeyDescriptor) (
return key.ECPrivKey()
}
// maybeTweakPrivKey examines the single and double tweak parameters on the
// passed sign descriptor and may perform a mapping on the passed private key
// in order to utilize the tweaks, if populated.
// maybeTweakPrivKey examines the single tweak parameters on the passed sign
// descriptor and may perform a mapping on the passed private key in order to
// utilize the tweaks, if populated.
func maybeTweakPrivKey(signDesc *input.SignDescriptor,
privKey *btcec.PrivateKey) (*btcec.PrivateKey, error) {
var retPriv *btcec.PrivateKey
switch {
privKey *btcec.PrivateKey) *btcec.PrivateKey {
case signDesc.SingleTweak != nil:
retPriv = input.TweakPrivKey(privKey,
signDesc.SingleTweak)
case signDesc.DoubleTweak != nil:
retPriv = input.DeriveRevocationPrivKey(privKey,
signDesc.DoubleTweak)
default:
retPriv = privKey
if signDesc.SingleTweak != nil {
return input.TweakPrivKey(privKey, signDesc.SingleTweak)
}
return retPriv, nil
}
return privKey
}

@ -7,12 +7,11 @@ import (
"time"
)
func collectChanSummary(cfg *config, channels []*SummaryEntry) error {
func summarizeChannels(apiUrl string, channels []*SummaryEntry) error {
summaryFile := &SummaryEntryFile{
Channels: channels,
}
chainApi := &chainApi{baseUrl: cfg.ApiUrl}
chainApi := &chainApi{baseUrl: apiUrl}
for idx, channel := range channels {
tx, err := chainApi.Transaction(channel.FundingTXID)
@ -44,7 +43,7 @@ func collectChanSummary(cfg *config, channels []*SummaryEntry) error {
}
} else {
summaryFile.OpenChannels++
summaryFile.FundsOpenChannels += uint64(channel.LocalBalance)
summaryFile.FundsOpenChannels += channel.LocalBalance
channel.ClosingTX = nil
}
@ -92,11 +91,11 @@ func reportOutspend(api *chainApi, summaryFile *SummaryEntryFile,
return err
}
summaryFile.FundsClosedChannels += uint64(entry.LocalBalance)
summaryFile.FundsClosedChannels += entry.LocalBalance
if isCoopClose(spendTx) {
summaryFile.CoopClosedChannels++
summaryFile.FundsCoopClose += uint64(entry.LocalBalance)
summaryFile.FundsCoopClose += entry.LocalBalance
entry.ClosingTX.ForceClose = false
return nil
}
@ -143,7 +142,7 @@ func reportOutspend(api *chainApi, summaryFile *SummaryEntryFile,
}
} else {
entry.ClosingTX.AllOutsSpent = true
summaryFile.FundsClosedSpent += uint64(entry.LocalBalance)
summaryFile.FundsClosedSpent += entry.LocalBalance
summaryFile.FullySpentChannels++
}

@ -4,36 +4,37 @@ 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/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
)
const (
maxCsvTimeout = 15000
feeSatPerByte = 3
feeSatPerByte = 2
)
func sweepTimeLock(cfg *config, entries []*SummaryEntry, sweepAddr string,
func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiUrl string,
entries []*SummaryEntry, sweepAddr string, maxCsvTimeout int,
publish bool) error {
extendedKey, err := hdkeychain.NewKeyFromString(cfg.RootKey)
if err != nil {
return err
}
// Create signer and transaction template.
signer := &signer{extendedKey: extendedKey}
chainApi := &chainApi{baseUrl: apiUrl}
sweepTx := wire.NewMsgTx(2)
entryIndex := 0
value := int64(0)
totalOutputValue := int64(0)
signDescs := make([]*input.SignDescriptor, 0)
for _, entry := range entries {
if entry.ClosingTX == nil || entry.ForceClose == nil ||
entry.ClosingTX.AllOutsSpent || entry.LocalBalance == 0 {
// Skip entries that can't be swept.
if entry.ClosingTX == nil ||
entry.ForceClose == nil ||
entry.ClosingTX.AllOutsSpent ||
entry.LocalBalance == 0 {
log.Infof("Not sweeping %s, info missing or all spent",
entry.ChannelPoint)
@ -42,161 +43,120 @@ func sweepTimeLock(cfg *config, entries []*SummaryEntry, sweepAddr string,
fc := entry.ForceClose
// Find index of sweepable output of commitment TX.
txindex := -1
if len(fc.Outs) == 1 {
txindex = 0
if fc.Outs[0].Value != uint64(entry.LocalBalance) {
log.Errorf("Potential value mismatch! %d vs %d (%s)",
if fc.Outs[0].Value != entry.LocalBalance {
log.Errorf("Potential value mismatch! %d vs "+
"%d (%s)",
fc.Outs[0].Value, entry.LocalBalance,
entry.ChannelPoint)
}
} else {
for idx, out := range fc.Outs {
if out.Value == uint64(entry.LocalBalance) {
if out.Value == entry.LocalBalance {
txindex = idx
}
}
}
if txindex == -1 {
log.Errorf("Could not find sweep output for chan %s",
entry.ChannelPoint)
continue
}
txHash, err := chainhash.NewHashFromStr(fc.TXID)
if err != nil {
return fmt.Errorf("error parsing tx hash: %v", err)
}
commitPointBytes, err := hex.DecodeString(fc.CommitPoint)
if err != nil {
return fmt.Errorf("error parsing commit point: %v", err)
}
commitPoint, err := btcec.ParsePubKey(commitPointBytes, btcec.S256())
// Prepare sweep script parameters.
commitPoint, err := pubKeyFromHex(fc.CommitPoint)
if err != nil {
return fmt.Errorf("error parsing commit point: %v", err)
}
revPointBytes, err := hex.DecodeString(fc.RevocationBasepoint.Pubkey)
revBase, err := pubKeyFromHex(fc.RevocationBasePoint.PubKey)
if err != nil {
return fmt.Errorf("error parsing commit point: %v", err)
}
revPoint, err := btcec.ParsePubKey(revPointBytes, btcec.S256())
if err != nil {
return fmt.Errorf("error parsing commit point: %v", err)
}
delayKeyDesc := &keychain.KeyDescriptor{
KeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamily(fc.DelayBasepoint.Family),
Index: fc.DelayBasepoint.Index,
},
}
delayPrivkey, err := signer.fetchPrivKey(delayKeyDesc)
delayDesc := fc.DelayBasePoint.toDesc()
delayPrivKey, err := signer.fetchPrivKey(delayDesc)
if err != nil {
return fmt.Errorf("error getting private key: %v", err)
}
delayBase := delayPrivKey.PubKey()
delayKey := input.TweakPubKey(delayPrivkey.PubKey(), commitPoint)
revocationKey := input.DeriveRevocationPubkey(
revPoint, commitPoint,
)
var (
csvTimeout = int32(-1)
script []byte
scriptHash []byte
// 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,
)
targetScript, err := hex.DecodeString(fc.Outs[txindex].Script)
if err != nil {
return fmt.Errorf("error parsing target script: %v", err)
}
if len(targetScript) != 34 {
log.Errorf("invalid target script: %x", targetScript)
continue
}
for i := 0; csvTimeout == -1 && i < maxCsvTimeout; i++ {
s, err := input.CommitScriptToSelf(
uint32(i), delayKey, revocationKey,
)
if err != nil {
return fmt.Errorf("error creating script: %v", err)
}
sh, err := input.WitnessScriptHash(s)
if err != nil {
return fmt.Errorf("error hashing script: %v", err)
}
if bytes.Equal(targetScript[0:8], sh[0:8]) {
csvTimeout = int32(i)
script = s
scriptHash = sh
}
}
if csvTimeout == -1 || len(script) == 0 {
log.Errorf("Could not create matching script for %s " +
"or csv too high: %d", entry.ChannelPoint,
csvTimeout)
log.Errorf("Could not create matching script for %s "+
"or csv too high: %v", entry.ChannelPoint,
err)
continue
}
// Create the transaction input.
txHash, err := chainhash.NewHashFromStr(fc.TXID)
if err != nil {
return fmt.Errorf("error parsing tx hash: %v", err)
}
sweepTx.TxIn = append(sweepTx.TxIn, &wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: *txHash,
Index: uint32(txindex),
},
SignatureScript: nil,
Witness: nil,
Sequence: input.LockTimeToSequence(
Sequence: input.LockTimeToSequence(
false, uint32(csvTimeout),
),
})
singleTweak := input.SingleTweakBytes(
commitPoint, delayPrivkey.PubKey(),
)
// Create the sign descriptor for the input.
signDesc := &input.SignDescriptor{
KeyDesc: *delayKeyDesc,
SingleTweak: singleTweak,
KeyDesc: *delayDesc,
SingleTweak: input.SingleTweakBytes(
commitPoint, delayBase,
),
WitnessScript: script,
Output: &wire.TxOut{
PkScript: scriptHash,
Value: int64(fc.Outs[txindex].Value),
Value: int64(fc.Outs[txindex].Value),
},
HashType: txscript.SigHashAll,
InputIndex: entryIndex,
HashType: txscript.SigHashAll,
}
value += int64(fc.Outs[txindex].Value)
totalOutputValue += int64(fc.Outs[txindex].Value)
signDescs = append(signDescs, signDesc)
entryIndex++
}
if len(signDescs) != len(sweepTx.TxIn) {
return fmt.Errorf("length mismatch")
}
sweepScript, err := pkhScript(sweepAddr)
// Add our sweep destination output.
sweepScript, err := getWP2PKHScript(sweepAddr)
if err != nil {
return err
}
sweepTx.TxOut = []*wire.TxOut{{
Value: value,
Value: totalOutputValue,
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.
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 = value - fee
fee := int64(size * feeSatPerByte)
sweepTx.TxOut[0].Value = totalOutputValue - fee
// Sign again after output fixing.
sigHashes = txscript.NewTxSigHashes(sweepTx)
@ -208,20 +168,42 @@ func sweepTimeLock(cfg *config, entries []*SummaryEntry, sweepAddr string,
}
sweepTx.TxIn[idx].Witness = witness
}
var buf bytes.Buffer
err = sweepTx.Serialize(&buf)
if err != nil {
return err
}
log.Infof("Fee %d sats of %d total amount (for size %d)",
fee, value, sweepTx.SerializeSize())
log.Infof("Transaction: %x", buf.Bytes())
fee, totalOutputValue, sweepTx.SerializeSize())
// Publish TX.
if publish {
response, err := chainApi.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 pkhScript(addr string) ([]byte, error) {
func pubKeyFromHex(pubKeyHex string) (*btcec.PublicKey, error) {
pointBytes, err := hex.DecodeString(pubKeyHex)
if err != nil {
return nil, fmt.Errorf("error hex decoding pub key: %v", err)
}
return btcec.ParsePubKey(
pointBytes, btcec.S256(),
)
}
func getWP2PKHScript(addr string) ([]byte, error) {
targetPubKeyHash, err := parseAddr(addr)
if err != nil {
return nil, err
@ -232,3 +214,37 @@ func pkhScript(addr string) ([]byte, error) {
return builder.Script()
}
func bruteForceDelay(delayPubkey, revocationPubkey *btcec.PublicKey,
targetScriptHex string, 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)
}
for i := 0; i <= maxCsvTimeout; i++ {
s, err := input.CommitScriptToSelf(
uint32(i), delayPubkey, revocationPubkey,
)
if err != nil {
return 0, nil, nil, fmt.Errorf("error creating "+
"script: %v", err)
}
sh, err := input.WitnessScriptHash(s)
if err != nil {
return 0, nil, nil, fmt.Errorf("error hashing script: "+
"%v", err)
}
if bytes.Equal(targetScript[0:8], sh[0:8]) {
return int32(i), s, sh, nil
}
}
return 0, nil, nil, fmt.Errorf("csv timeout not found for target "+
"script %s", targetScriptHex)
}

Loading…
Cancel
Save