mirror of
https://github.com/guggero/chantools
synced 2024-11-11 01:10:42 +00:00
251 lines
6.5 KiB
Go
251 lines
6.5 KiB
Go
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"
|
|
)
|
|
|
|
const (
|
|
feeSatPerByte = 2
|
|
)
|
|
|
|
func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiUrl string,
|
|
entries []*SummaryEntry, sweepAddr string, maxCsvTimeout int,
|
|
publish bool) error {
|
|
|
|
// Create signer and transaction template.
|
|
signer := &signer{extendedKey: extendedKey}
|
|
chainApi := &chainApi{baseUrl: apiUrl}
|
|
|
|
sweepTx := wire.NewMsgTx(2)
|
|
totalOutputValue := int64(0)
|
|
signDescs := make([]*input.SignDescriptor, 0)
|
|
|
|
for _, entry := range entries {
|
|
// 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)
|
|
continue
|
|
}
|
|
|
|
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 != 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
|
|
}
|
|
|
|
// Prepare sweep script parameters.
|
|
commitPoint, err := pubKeyFromHex(fc.CommitPoint)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing commit point: %v", err)
|
|
}
|
|
revBase, err := pubKeyFromHex(fc.RevocationBasePoint.PubKey)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing commit point: %v", err)
|
|
}
|
|
delayDesc := fc.DelayBasePoint.toDesc()
|
|
delayPrivKey, err := signer.fetchPrivKey(delayDesc)
|
|
if err != nil {
|
|
return fmt.Errorf("error getting private key: %v", err)
|
|
}
|
|
delayBase := delayPrivKey.PubKey()
|
|
|
|
// 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,
|
|
)
|
|
if err != nil {
|
|
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),
|
|
},
|
|
Sequence: input.LockTimeToSequence(
|
|
false, uint32(csvTimeout),
|
|
),
|
|
})
|
|
|
|
// Create the sign descriptor for the input.
|
|
signDesc := &input.SignDescriptor{
|
|
KeyDesc: *delayDesc,
|
|
SingleTweak: input.SingleTweakBytes(
|
|
commitPoint, delayBase,
|
|
),
|
|
WitnessScript: script,
|
|
Output: &wire.TxOut{
|
|
PkScript: scriptHash,
|
|
Value: int64(fc.Outs[txindex].Value),
|
|
},
|
|
HashType: txscript.SigHashAll,
|
|
}
|
|
totalOutputValue += int64(fc.Outs[txindex].Value)
|
|
signDescs = append(signDescs, signDesc)
|
|
}
|
|
|
|
// Add our sweep destination output.
|
|
sweepScript, err := getWP2PKHScript(sweepAddr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sweepTx.TxOut = []*wire.TxOut{{
|
|
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 = 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)
|
|
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, 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 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
|
|
}
|
|
builder := txscript.NewScriptBuilder()
|
|
builder.AddOp(txscript.OP_0)
|
|
builder.AddData(targetPubKeyHash)
|
|
|
|
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)
|
|
}
|