Add sweeptimelock command

pull/3/head
Oliver Gugger 4 years ago
parent a2114a28f4
commit 5034c6bab6
No known key found for this signature in database
GPG Key ID: 8E4256593F177720

@ -109,28 +109,9 @@ func bruteForceChannels(cfg *config, entries []*SummaryEntry,
}
func addrInCache(addr string, perCommitPoint *btcec.PublicKey) (string, error) {
// First parse address to get targetPubKeyHash from it later.
targetAddr, err := btcutil.DecodeAddress(addr, chainParams)
targetPubKeyHash, err := parseAddr(addr)
if err != nil {
return "", err
}
var targetPubKeyHash []byte
// Make the check on the decoded address according to the active
// network (testnet or mainnet only).
if !targetAddr.IsForNet(chainParams) {
return "", fmt.Errorf(
"address: %v is not valid for this network: %v",
targetAddr.String(), chainParams.Name,
)
}
// Must be a bech32 native SegWit address.
switch targetAddr.(type) {
case *btcutil.AddressWitnessPubKeyHash:
targetPubKeyHash = targetAddr.ScriptAddress()
default:
return "", fmt.Errorf("address: must be a bech32 P2WPKH address")
return "", fmt.Errorf("error parsing addr: %v", err)
}
// Loop through all cached payment base point keys, tweak each of it
@ -223,3 +204,30 @@ func deriveChildren(key *hdkeychain.ExtendedKey, path []uint32) (
}
return currentKey, nil
}
func parseAddr(addr string) ([]byte, error) {
// First parse address to get targetPubKeyHash from it later.
targetAddr, err := btcutil.DecodeAddress(addr, chainParams)
if err != nil {
return nil, err
}
var targetPubKeyHash []byte
// Make the check on the decoded address according to the active
// network (testnet or mainnet only).
if !targetAddr.IsForNet(chainParams) {
return nil, fmt.Errorf(
"address: %v is not valid for this network: %v",
targetAddr.String(), chainParams.Name,
)
}
// Must be a bech32 native SegWit address.
switch targetAddr.(type) {
case *btcutil.AddressWitnessPubKeyHash:
targetPubKeyHash = targetAddr.ScriptAddress()
default:
return nil, fmt.Errorf("address: must be a bech32 P2WPKH address")
}
return targetPubKeyHash, nil
}

@ -30,7 +30,8 @@ func collectChanSummary(cfg *config, channels []*SummaryEntry) error {
if outspend.Spent {
summaryFile.ClosedChannels++
channel.ClosingTX = &ClosingTX{
TXID: outspend.Txid,
TXID: outspend.Txid,
ConfHeight: uint32(outspend.Status.BlockHeight),
}
err := reportOutspend(

@ -6,11 +6,12 @@ type ClosingTX struct {
AllOutsSpent bool `json:"all_outputs_spent"`
OurAddr string `json:"our_addr"`
SweepPrivkey string `json:"sweep_privkey"`
ConfHeight uint32 `json:"conf_height"`
}
type Basepoint struct {
Family uint16 `json:"family"`
Index uint32 `json:"index"`
Family uint16 `json:"family,omitempty"`
Index uint32 `json:"index,omitempty"`
Pubkey string `json:"pubkey"`
}
@ -21,12 +22,13 @@ type Out struct {
}
type ForceClose struct {
TXID string `json:"txid"`
Serialized string `json:"serialized"`
CSVDelay uint16 `json:"csv_delay"`
DelayBasepoint *Basepoint `json:"delay_basepoint"`
CommitPoint string `json:"commit_point"`
Outs []*Out `json:"outs"`
TXID string `json:"txid"`
Serialized string `json:"serialized"`
CSVDelay uint16 `json:"csv_delay"`
DelayBasepoint *Basepoint `json:"delay_basepoint"`
RevocationBasepoint *Basepoint `json:"revocation_basepoint"`
CommitPoint string `json:"commit_point"`
Outs []*Out `json:"outs"`
}
type SummaryEntry struct {

@ -9,13 +9,11 @@ import (
"io/ioutil"
"time"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
)
func forceCloseChannels(cfg *config, entries []*SummaryEntry,
@ -25,8 +23,8 @@ func forceCloseChannels(cfg *config, entries []*SummaryEntry,
if err != nil {
return err
}
chainApi := &chainApi{baseUrl:cfg.ApiUrl}
chainApi := &chainApi{baseUrl: cfg.ApiUrl}
extendedKey, err := hdkeychain.NewKeyFromString(cfg.RootKey)
if err != nil {
@ -85,6 +83,7 @@ func forceCloseChannels(cfg *config, entries []*SummaryEntry,
// Calculate commit point.
basepoint := channel.LocalChanCfg.DelayBasePoint
revpoint := channel.RemoteChanCfg.RevocationBasePoint
revocationPreimage, err := channel.RevocationProducer.AtIndex(
localCommit.CommitHeight,
)
@ -98,11 +97,20 @@ func forceCloseChannels(cfg *config, entries []*SummaryEntry,
DelayBasepoint: &Basepoint{
Family: uint16(basepoint.Family),
Index: basepoint.Index,
Pubkey: hex.EncodeToString(
basepoint.PubKey.SerializeCompressed(),
),
},
RevocationBasepoint: &Basepoint{
Pubkey: hex.EncodeToString(
revpoint.PubKey.SerializeCompressed(),
),
},
CommitPoint: hex.EncodeToString(
point.SerializeCompressed(),
),
Outs: make([]*Out, len(localCommitTx.TxOut)),
Outs: make([]*Out, len(localCommitTx.TxOut)),
CSVDelay: channel.LocalChanCfg.CsvDelay,
}
for idx, out := range localCommitTx.TxOut {
script, err := txscript.DisasmString(out.PkScript)
@ -115,7 +123,7 @@ func forceCloseChannels(cfg *config, entries []*SummaryEntry,
Value: uint64(out.Value),
}
}
// Publish TX.
if publish {
response, err := chainApi.PublishTx(serialized)
@ -139,50 +147,6 @@ func forceCloseChannels(cfg *config, entries []*SummaryEntry,
return ioutil.WriteFile(fileName, summaryBytes, 0644)
}
type signer struct {
extendedKey *hdkeychain.ExtendedKey
}
func (s *signer) SignOutputRaw(tx *wire.MsgTx,
signDesc *input.SignDescriptor) ([]byte, error) {
witnessScript := signDesc.WitnessScript
// First attempt to fetch the private key which corresponds to the
// specified public key.
privKey, err := s.fetchPrivKey(&signDesc.KeyDesc)
if err != nil {
return nil, err
}
amt := signDesc.Output.Value
sig, err := txscript.RawTxInWitnessSignature(
tx, signDesc.SigHashes, signDesc.InputIndex, amt,
witnessScript, signDesc.HashType, privKey,
)
if err != nil {
return nil, err
}
// Chop off the sighash flag at the end of the signature.
return sig[:len(sig)-1], nil
}
func (s *signer) fetchPrivKey(descriptor *keychain.KeyDescriptor) (
*btcec.PrivateKey, error) {
key, err := deriveChildren(s.extendedKey, []uint32{
hardenedKeyStart + uint32(keychain.BIP0043Purpose),
hardenedKeyStart + chainParams.HDCoinType,
hardenedKeyStart + uint32(descriptor.Family),
0,
descriptor.Index,
})
if err != nil {
return nil, err
}
return key.ECPrivKey()
}
type LightningChannel struct {
localChanCfg channeldb.ChannelConfig
remoteChanCfg channeldb.ChannelConfig

@ -51,6 +51,11 @@ func Main() error {
"channel.db provided", "",
&forceCloseCommand{},
)
_, _ = parser.AddCommand(
"sweeptimelock", "Sweep the force-closed state after the time " +
"lock has expired", "",
&sweepTimeLockCommand{},
)
_, err := parser.Parse()
return err
@ -118,6 +123,34 @@ func (c *forceCloseCommand) Execute(args []string) error {
return forceCloseChannels(cfg, entries, db, c.Publish)
}
type sweepTimeLockCommand struct {
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"`
}
func (c *sweepTimeLockCommand) Execute(args []string) error {
// Check that root key is valid.
if cfg.RootKey == "" {
return fmt.Errorf("root key is required")
}
_, err := hdkeychain.NewKeyFromString(cfg.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")
}
// Parse channel entries from any of the possible input files.
entries, err := ParseInput(cfg)
if err != nil {
return err
}
return sweepTimeLock(cfg, entries, c.SweepAddr, c.Publish)
}
func setupLogging() {
logWriter.RegisterSubLogger("CHAN", log)
err := logWriter.InitLogRotator("./results/chantools.log", 10, 3)

@ -0,0 +1,89 @@
package chantools
import (
"fmt"
"github.com/btcsuite/btcd/btcec"
"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"
)
type signer struct {
extendedKey *hdkeychain.ExtendedKey
}
func (s *signer) SignOutputRaw(tx *wire.MsgTx,
signDesc *input.SignDescriptor) ([]byte, error) {
witnessScript := signDesc.WitnessScript
// First attempt to fetch the private key which corresponds to the
// specified public key.
privKey, err := s.fetchPrivKey(&signDesc.KeyDesc)
if err != nil {
return nil, err
}
privKey, err = maybeTweakPrivKey(signDesc, privKey)
if err != nil {
return nil, err
}
amt := signDesc.Output.Value
sig, err := txscript.RawTxInWitnessSignature(
tx, signDesc.SigHashes, signDesc.InputIndex, amt,
witnessScript, signDesc.HashType, privKey,
)
if err != nil {
return nil, err
}
// Chop off the sighash flag at the end of the signature.
return sig[:len(sig)-1], nil
}
func (s *signer) ComputeInputScript(tx *wire.MsgTx,
signDesc *input.SignDescriptor) (*input.Script, error) {
return nil, fmt.Errorf("unimplemented")
}
func (s *signer) fetchPrivKey(descriptor *keychain.KeyDescriptor) (
*btcec.PrivateKey, error) {
key, err := deriveChildren(s.extendedKey, []uint32{
hardenedKeyStart + uint32(keychain.BIP0043Purpose),
hardenedKeyStart + chainParams.HDCoinType,
hardenedKeyStart + uint32(descriptor.Family),
0,
descriptor.Index,
})
if err != nil {
return nil, err
}
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.
func maybeTweakPrivKey(signDesc *input.SignDescriptor,
privKey *btcec.PrivateKey) (*btcec.PrivateKey, error) {
var retPriv *btcec.PrivateKey
switch {
case signDesc.SingleTweak != nil:
retPriv = input.TweakPrivKey(privKey,
signDesc.SingleTweak)
case signDesc.DoubleTweak != nil:
retPriv = input.DeriveRevocationPrivKey(privKey,
signDesc.DoubleTweak)
default:
retPriv = privKey
}
return retPriv, nil
}

@ -0,0 +1,234 @@
package chantools
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
)
func sweepTimeLock(cfg *config, entries []*SummaryEntry, sweepAddr string,
publish bool) error {
extendedKey, err := hdkeychain.NewKeyFromString(cfg.RootKey)
if err != nil {
return err
}
signer := &signer{extendedKey: extendedKey}
sweepTx := wire.NewMsgTx(2)
entryIndex := 0
value := int64(0)
signDescs := make([]*input.SignDescriptor, 0)
for _, entry := range entries {
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)
continue
}
fc := entry.ForceClose
txindex := -1
if len(fc.Outs) == 1 {
txindex = 0
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 == 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())
if err != nil {
return fmt.Errorf("error parsing commit point: %v", err)
}
revPointBytes, err := hex.DecodeString(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)
if err != nil {
return fmt.Errorf("error getting private key: %v", err)
}
delayKey := input.TweakPubKey(delayPrivkey.PubKey(), commitPoint)
revocationKey := input.DeriveRevocationPubkey(
revPoint, commitPoint,
)
var (
csvTimeout = int32(-1)
script []byte
scriptHash []byte
)
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)
continue
}
sweepTx.TxIn = append(sweepTx.TxIn, &wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: *txHash,
Index: uint32(txindex),
},
SignatureScript: nil,
Witness: nil,
Sequence: input.LockTimeToSequence(
false, uint32(csvTimeout),
),
})
singleTweak := input.SingleTweakBytes(
commitPoint, delayPrivkey.PubKey(),
)
signDesc := &input.SignDescriptor{
KeyDesc: *delayKeyDesc,
SingleTweak: singleTweak,
WitnessScript: script,
Output: &wire.TxOut{
PkScript: scriptHash,
Value: int64(fc.Outs[txindex].Value),
},
HashType: txscript.SigHashAll,
InputIndex: entryIndex,
}
value += 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)
if err != nil {
return err
}
sweepTx.TxOut = []*wire.TxOut{{
Value: value,
PkScript: sweepScript,
}}
sigHashes := txscript.NewTxSigHashes(sweepTx)
for idx, desc := range signDescs {
desc.SigHashes = sigHashes
witness, err := input.CommitSpendTimeout(signer, desc, sweepTx)
if err != nil {
return err
}
sweepTx.TxIn[idx].Witness = witness
}
size := sweepTx.SerializeSize()
fee := int64(size*feeSatPerByte)
sweepTx.TxOut[0].Value = value - 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)
if err != nil {
return err
}
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())
return nil
}
func pkhScript(addr string) ([]byte, error) {
targetPubKeyHash, err := parseAddr(addr)
if err != nil {
return nil, err
}
builder := txscript.NewScriptBuilder()
builder.AddOp(txscript.OP_0)
builder.AddData(targetPubKeyHash)
return builder.Script()
}
Loading…
Cancel
Save