mirror of https://github.com/guggero/chantools
Compare commits
No commits in common. 'master' and 'v0.9.0' have entirely different histories.
@ -1,53 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
pull_request:
|
||||
branches:
|
||||
- "*"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
# go needs absolute directories, using the $HOME variable doesn't work here.
|
||||
GOCACHE: /home/runner/work/go/pkg/build
|
||||
GOPATH: /home/runner/work/go
|
||||
GO_VERSION: 1.22.3
|
||||
|
||||
jobs:
|
||||
########################
|
||||
# lint code
|
||||
########################
|
||||
lint:
|
||||
name: lint code
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: git checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: lint
|
||||
run: make lint
|
||||
|
||||
########################
|
||||
# run unit tests
|
||||
########################
|
||||
unit-test:
|
||||
name: run unit tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: git checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: setup go ${{ env.GO_VERSION }}
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '${{ env.GO_VERSION }}'
|
||||
|
||||
- name: run unit tests
|
||||
run: make unit
|
@ -1,4 +1,3 @@
|
||||
.idea
|
||||
/chantools
|
||||
results
|
||||
/chantools-v*
|
||||
|
@ -1,83 +0,0 @@
|
||||
package btc
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
inputCharset = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ" +
|
||||
"&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\\\"\\\\ "
|
||||
checksumCharset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
||||
generator = []uint64{
|
||||
0xf5dee51989, 0xa9fdca3312, 0x1bab10e32d, 0x3706b1677a,
|
||||
0x644d626ffd,
|
||||
}
|
||||
)
|
||||
|
||||
func descriptorSumPolymod(symbols []uint64) uint64 {
|
||||
chk := uint64(1)
|
||||
for _, value := range symbols {
|
||||
top := chk >> 35
|
||||
chk = (chk&0x7ffffffff)<<5 ^ value
|
||||
for i := range 5 {
|
||||
if (top>>i)&1 != 0 {
|
||||
chk ^= generator[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
return chk
|
||||
}
|
||||
|
||||
func descriptorSumExpand(s string) []uint64 {
|
||||
groups := []uint64{}
|
||||
symbols := []uint64{}
|
||||
for _, c := range s {
|
||||
v := strings.IndexRune(inputCharset, c)
|
||||
if v < 0 {
|
||||
return nil
|
||||
}
|
||||
symbols = append(symbols, uint64(v&31))
|
||||
groups = append(groups, uint64(v>>5))
|
||||
if len(groups) == 3 {
|
||||
symbols = append(
|
||||
symbols, groups[0]*9+groups[1]*3+groups[2],
|
||||
)
|
||||
groups = []uint64{}
|
||||
}
|
||||
}
|
||||
if len(groups) == 1 {
|
||||
symbols = append(symbols, groups[0])
|
||||
} else if len(groups) == 2 {
|
||||
symbols = append(symbols, groups[0]*3+groups[1])
|
||||
}
|
||||
return symbols
|
||||
}
|
||||
|
||||
func DescriptorSumCreate(s string) string {
|
||||
symbols := append(descriptorSumExpand(s), 0, 0, 0, 0, 0, 0, 0, 0)
|
||||
checksum := descriptorSumPolymod(symbols) ^ 1
|
||||
builder := strings.Builder{}
|
||||
for i := range 8 {
|
||||
builder.WriteByte(checksumCharset[(checksum>>(5*(7-i)))&31])
|
||||
}
|
||||
return s + "#" + builder.String()
|
||||
}
|
||||
|
||||
func DescriptorSumCheck(s string, require bool) bool {
|
||||
if !strings.Contains(s, "#") {
|
||||
return !require
|
||||
}
|
||||
if s[len(s)-9] != '#' {
|
||||
return false
|
||||
}
|
||||
for _, c := range s[len(s)-8:] {
|
||||
if !strings.ContainsRune(checksumCharset, c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
symbols := append(
|
||||
descriptorSumExpand(s[:len(s)-9]),
|
||||
uint64(strings.Index(checksumCharset, s[len(s)-8:])),
|
||||
)
|
||||
return descriptorSumPolymod(symbols) == 1
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package btc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var testCases = []struct {
|
||||
descriptor string
|
||||
expectedSum string
|
||||
}{{
|
||||
descriptor: "addr(mkmZxiEcEd8ZqjQWVZuC6so5dFMKEFpN2j)",
|
||||
expectedSum: "#02wpgw69",
|
||||
}, {
|
||||
descriptor: "tr(cRhCT5vC5NdnSrQ2Jrah6NPCcth41uT8DWFmA6uD8R4x2ufucnYX)",
|
||||
expectedSum: "#gwfmkgga",
|
||||
}}
|
||||
|
||||
func TestDescriptorSum(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
sum := DescriptorSumCreate(tc.descriptor)
|
||||
require.Equal(t, tc.descriptor+tc.expectedSum, sum)
|
||||
|
||||
DescriptorSumCheck(sum, true)
|
||||
}
|
||||
}
|
@ -1,606 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/chantools/lnd"
|
||||
"github.com/lightninglabs/pool/account"
|
||||
"github.com/lightninglabs/pool/poolscript"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
poolMainnetFirstBatchBlock = 648168
|
||||
defaultMaxNumBlocks = 200000
|
||||
defaultMaxNumAccounts = 20
|
||||
defaultMaxNumBatchKeys = 500
|
||||
oddByte = input.PubKeyFormatCompressedOdd
|
||||
)
|
||||
|
||||
var (
|
||||
initialBatchKeyBytes, _ = hex.DecodeString(account.InitialBatchKey)
|
||||
initialBatchKey, _ = btcec.ParsePubKey(initialBatchKeyBytes)
|
||||
|
||||
mainnetAuctioneerKeyHex = "028e87bdd134238f8347f845d9ecc827b843d0d1e2" +
|
||||
"7cdcb46da704d916613f4fce"
|
||||
)
|
||||
|
||||
type closePoolAccountCommand struct {
|
||||
APIURL string
|
||||
Outpoint string
|
||||
AuctioneerKey string
|
||||
Publish bool
|
||||
SweepAddr string
|
||||
FeeRate uint32
|
||||
|
||||
MinExpiry uint32
|
||||
MaxNumBlocks uint32
|
||||
MaxNumAccounts uint32
|
||||
MaxNumBatchKeys uint32
|
||||
|
||||
rootKey *rootKey
|
||||
cmd *cobra.Command
|
||||
}
|
||||
|
||||
func newClosePoolAccountCommand() *cobra.Command {
|
||||
cc := &closePoolAccountCommand{}
|
||||
cc.cmd = &cobra.Command{
|
||||
Use: "closepoolaccount",
|
||||
Short: "Tries to close a Pool account that has expired",
|
||||
Long: `In case a Pool account cannot be closed normally with the
|
||||
poold daemon it can be closed with this command. The account **MUST** have
|
||||
expired already, otherwise this command doesn't work since a signature from the
|
||||
auctioneer is necessary.
|
||||
|
||||
You need to know the account's last unspent outpoint. That can either be
|
||||
obtained by running 'pool accounts list' `,
|
||||
Example: `chantools closepoolaccount \
|
||||
--outpoint xxxxxxxxx:y \
|
||||
--sweepaddr bc1q..... \
|
||||
--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().StringVar(
|
||||
&cc.Outpoint, "outpoint", "", "last account outpoint of the "+
|
||||
"account to close (<txid>:<txindex>)",
|
||||
)
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.AuctioneerKey, "auctioneerkey", mainnetAuctioneerKeyHex,
|
||||
"the auctioneer's static public key",
|
||||
)
|
||||
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 recover the funds "+
|
||||
"to; specify '"+lnd.AddressDeriveFromWallet+"' to "+
|
||||
"derive a new address from the seed automatically",
|
||||
)
|
||||
cc.cmd.Flags().Uint32Var(
|
||||
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
|
||||
"use for the sweep transaction in sat/vByte",
|
||||
)
|
||||
cc.cmd.Flags().Uint32Var(
|
||||
&cc.MinExpiry, "minexpiry", poolMainnetFirstBatchBlock,
|
||||
"the block to start brute forcing the expiry from",
|
||||
)
|
||||
cc.cmd.Flags().Uint32Var(
|
||||
&cc.MaxNumBlocks, "maxnumblocks", defaultMaxNumBlocks, "the "+
|
||||
"maximum number of blocks to try when brute forcing "+
|
||||
"the expiry",
|
||||
)
|
||||
cc.cmd.Flags().Uint32Var(
|
||||
&cc.MaxNumAccounts, "maxnumaccounts", defaultMaxNumAccounts,
|
||||
"the number of account indices to try at most",
|
||||
)
|
||||
cc.cmd.Flags().Uint32Var(
|
||||
&cc.MaxNumBatchKeys, "maxnumbatchkeys", defaultMaxNumBatchKeys,
|
||||
"the number of batch keys to try at most",
|
||||
)
|
||||
|
||||
cc.rootKey = newRootKey(cc.cmd, "deriving keys")
|
||||
|
||||
return cc.cmd
|
||||
}
|
||||
|
||||
func (c *closePoolAccountCommand) Execute(_ *cobra.Command, _ []string) error {
|
||||
extendedKey, err := c.rootKey.read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading root key: %w", err)
|
||||
}
|
||||
|
||||
// Make sure sweep addr is set.
|
||||
err = lnd.CheckAddress(
|
||||
c.SweepAddr, chainParams, true, "sweep", lnd.AddrTypeP2WKH,
|
||||
lnd.AddrTypeP2TR,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse account outpoint and auctioneer key.
|
||||
outpoint, err := lnd.ParseOutpoint(c.Outpoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing account outpoint: %w", err)
|
||||
}
|
||||
|
||||
auctioneerKeyBytes, err := hex.DecodeString(c.AuctioneerKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding auctioneer key: %w", err)
|
||||
}
|
||||
|
||||
auctioneerKey, err := btcec.ParsePubKey(auctioneerKeyBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing auctioneer key: %w", err)
|
||||
}
|
||||
|
||||
// Set default values.
|
||||
if c.FeeRate == 0 {
|
||||
c.FeeRate = defaultFeeSatPerVByte
|
||||
}
|
||||
return closePoolAccount(
|
||||
extendedKey, c.APIURL, outpoint, auctioneerKey, c.SweepAddr,
|
||||
c.Publish, c.FeeRate, c.MinExpiry, c.MinExpiry+c.MaxNumBlocks,
|
||||
c.MaxNumAccounts, c.MaxNumBatchKeys,
|
||||
)
|
||||
}
|
||||
|
||||
func closePoolAccount(extendedKey *hdkeychain.ExtendedKey, apiURL string,
|
||||
outpoint *wire.OutPoint, auctioneerKey *btcec.PublicKey,
|
||||
sweepAddr string, publish bool, feeRate uint32, minExpiry,
|
||||
maxNumBlocks, maxNumAccounts, maxNumBatchKeys uint32) error {
|
||||
|
||||
var (
|
||||
estimator input.TxWeightEstimator
|
||||
signer = &lnd.Signer{
|
||||
ExtendedKey: extendedKey,
|
||||
ChainParams: chainParams,
|
||||
}
|
||||
api = newExplorerAPI(apiURL)
|
||||
)
|
||||
|
||||
sweepScript, err := lnd.PrepareWalletAddress(
|
||||
sweepAddr, chainParams, &estimator, extendedKey, "sweep",
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx, err := api.Transaction(outpoint.Hash.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error looking up TX %s: %w",
|
||||
outpoint.Hash.String(), err)
|
||||
}
|
||||
|
||||
txOut := tx.Vout[outpoint.Index]
|
||||
if txOut.Outspend.Spent {
|
||||
return fmt.Errorf("outpoint %v is already spent", outpoint)
|
||||
}
|
||||
|
||||
pkScript, err := hex.DecodeString(txOut.ScriptPubkey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding pk script %s: %w",
|
||||
txOut.ScriptPubkey, err)
|
||||
}
|
||||
log.Debugf("Brute forcing pk script %x for outpoint %v", pkScript,
|
||||
outpoint)
|
||||
|
||||
script, err := txscript.ParsePkScript(pkScript)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing pk script: %w", err)
|
||||
}
|
||||
|
||||
// Let's derive the account key family's extended key first.
|
||||
path := []uint32{
|
||||
lnd.HardenedKeyStart + uint32(keychain.BIP0043Purpose),
|
||||
lnd.HardenedKeyStart + chainParams.HDCoinType,
|
||||
lnd.HardenedKeyStart + uint32(poolscript.AccountKeyFamily),
|
||||
0,
|
||||
}
|
||||
accountBaseKey, err := lnd.DeriveChildren(extendedKey, path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deriving account base key: %w", err)
|
||||
}
|
||||
|
||||
// Try our luck.
|
||||
var (
|
||||
acct *poolAccount
|
||||
accountVersion account.Version
|
||||
)
|
||||
switch script.Class() {
|
||||
case txscript.WitnessV0ScriptHashTy:
|
||||
accountVersion = account.VersionInitialNoVersion
|
||||
|
||||
case txscript.WitnessV1TaprootTy:
|
||||
accountVersion = account.VersionTaprootEnabled
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported script class %v", script.Class())
|
||||
}
|
||||
|
||||
acct, err = bruteForceAccountScript(
|
||||
accountBaseKey, auctioneerKey, minExpiry, maxNumBlocks,
|
||||
maxNumAccounts, maxNumBatchKeys, pkScript,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error brute forcing account script: %w", err)
|
||||
}
|
||||
|
||||
log.Debugf("Found pool account %s", acct.String())
|
||||
|
||||
sweepTx := wire.NewMsgTx(2)
|
||||
sweepTx.LockTime = acct.expiry
|
||||
sweepValue := int64(txOut.Value)
|
||||
|
||||
// Create the transaction input.
|
||||
sweepTx.TxIn = []*wire.TxIn{{
|
||||
PreviousOutPoint: *outpoint,
|
||||
}}
|
||||
|
||||
// Calculate the fee based on the given fee rate and our weight
|
||||
// estimation.
|
||||
var (
|
||||
prevOutFetcher = txscript.NewCannedPrevOutputFetcher(
|
||||
pkScript, sweepValue,
|
||||
)
|
||||
signDesc = &input.SignDescriptor{
|
||||
KeyDesc: keychain.KeyDescriptor{
|
||||
KeyLocator: keychain.KeyLocator{
|
||||
Family: poolscript.AccountKeyFamily,
|
||||
Index: acct.keyIndex,
|
||||
},
|
||||
},
|
||||
SingleTweak: acct.keyTweak,
|
||||
WitnessScript: acct.witnessScript,
|
||||
Output: &wire.TxOut{
|
||||
PkScript: pkScript,
|
||||
Value: sweepValue,
|
||||
},
|
||||
InputIndex: 0,
|
||||
PrevOutputFetcher: prevOutFetcher,
|
||||
}
|
||||
)
|
||||
|
||||
switch accountVersion {
|
||||
case account.VersionInitialNoVersion:
|
||||
estimator.AddWitnessInput(poolscript.ExpiryWitnessSize)
|
||||
signDesc.HashType = txscript.SigHashAll
|
||||
signDesc.SignMethod = input.WitnessV0SignMethod
|
||||
signDesc.SigHashes = txscript.NewTxSigHashes(
|
||||
sweepTx, prevOutFetcher,
|
||||
)
|
||||
|
||||
case account.VersionTaprootEnabled:
|
||||
estimator.AddWitnessInput(poolscript.TaprootExpiryWitnessSize)
|
||||
signDesc.HashType = txscript.SigHashDefault
|
||||
signDesc.SignMethod = input.TaprootScriptSpendSignMethod
|
||||
}
|
||||
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()
|
||||
totalFee := feeRateKWeight.FeeForWeight(estimator.Weight())
|
||||
|
||||
// Add our sweep destination output.
|
||||
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.
|
||||
sig, err := signer.SignOutputRaw(sweepTx, signDesc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error signing sweep tx: %w", err)
|
||||
}
|
||||
|
||||
switch accountVersion {
|
||||
case account.VersionInitialNoVersion:
|
||||
ourSig := append(sig.Serialize(), byte(signDesc.HashType))
|
||||
sweepTx.TxIn[0].Witness = poolscript.SpendExpiry(
|
||||
acct.witnessScript, ourSig,
|
||||
)
|
||||
|
||||
case account.VersionTaprootEnabled:
|
||||
sweepTx.TxIn[0].Witness = poolscript.SpendExpiryTaproot(
|
||||
acct.witnessScript, sig.Serialize(), acct.controlBlock,
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type poolAccount struct {
|
||||
keyIndex uint32
|
||||
expiry uint32
|
||||
sharedKey [32]byte
|
||||
batchKey []byte
|
||||
keyTweak []byte
|
||||
witnessScript []byte
|
||||
controlBlock []byte
|
||||
version poolscript.Version
|
||||
}
|
||||
|
||||
func (a *poolAccount) String() string {
|
||||
return fmt.Sprintf("key_index=%d, expiry=%d, shared_key=%x, "+
|
||||
"batch_key=%x, key_tweak=%x, witness_script=%x, version=%d",
|
||||
a.keyIndex, a.expiry, a.sharedKey[:], a.batchKey, a.keyTweak,
|
||||
a.witnessScript, a.version)
|
||||
}
|
||||
|
||||
func bruteForceAccountScript(accountBaseKey *hdkeychain.ExtendedKey,
|
||||
auctioneerKey *btcec.PublicKey, minExpiry, maxExpiry, maxNumAccounts,
|
||||
maxNumBatchKeys uint32, targetScript []byte) (*poolAccount, error) {
|
||||
|
||||
// The outermost loop is over the possible accounts.
|
||||
for i := range maxNumAccounts {
|
||||
accountExtendedKey, err := accountBaseKey.DeriveNonStandard(i)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error deriving account key: "+
|
||||
"%w", err)
|
||||
}
|
||||
|
||||
accountPrivKey, err := accountExtendedKey.ECPrivKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error deriving private key: "+
|
||||
"%w", err)
|
||||
}
|
||||
log.Debugf("Trying trader key %x...",
|
||||
accountPrivKey.PubKey().SerializeCompressed())
|
||||
|
||||
sharedKey, err := lnd.ECDH(accountPrivKey, auctioneerKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error deriving shared key: "+
|
||||
"%w", err)
|
||||
}
|
||||
|
||||
// The next loop is over the batch keys.
|
||||
batchKeyIndex := uint32(0)
|
||||
currentBatchKey := initialBatchKey
|
||||
for batchKeyIndex < maxNumBatchKeys {
|
||||
// And then finally the loop over the actual account
|
||||
// expiry in blocks.
|
||||
acct, err := fastScript(
|
||||
i, minExpiry, maxExpiry,
|
||||
accountPrivKey.PubKey(), auctioneerKey,
|
||||
currentBatchKey, sharedKey, targetScript,
|
||||
)
|
||||
if err == nil {
|
||||
return acct, nil
|
||||
}
|
||||
acct, err = fastScriptTaproot(
|
||||
poolscript.VersionTaprootMuSig2, i, minExpiry,
|
||||
maxExpiry, accountPrivKey.PubKey(),
|
||||
auctioneerKey, currentBatchKey, sharedKey,
|
||||
targetScript,
|
||||
)
|
||||
if err == nil {
|
||||
return acct, nil
|
||||
}
|
||||
acct, err = fastScriptTaproot(
|
||||
poolscript.VersionTaprootMuSig2V100RC2, i,
|
||||
minExpiry, maxExpiry,
|
||||
accountPrivKey.PubKey(), auctioneerKey,
|
||||
currentBatchKey, sharedKey, targetScript,
|
||||
)
|
||||
if err == nil {
|
||||
return acct, nil
|
||||
}
|
||||
|
||||
currentBatchKey = poolscript.IncrementKey(
|
||||
currentBatchKey,
|
||||
)
|
||||
batchKeyIndex++
|
||||
}
|
||||
|
||||
log.Debugf("Tried account index %d of %d", i, maxNumAccounts)
|
||||
}
|
||||
|
||||
return nil, errors.New("account script not derived")
|
||||
}
|
||||
|
||||
func fastScript(keyIndex, expiryFrom, expiryTo uint32, traderKey, auctioneerKey,
|
||||
batchKey *btcec.PublicKey, secret [32]byte,
|
||||
targetScript []byte) (*poolAccount, error) {
|
||||
|
||||
script, err := txscript.ParsePkScript(targetScript)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if script.Class() != txscript.WitnessV0ScriptHashTy {
|
||||
return nil, errors.New("incompatible script class")
|
||||
}
|
||||
|
||||
traderKeyTweak := poolscript.TraderKeyTweak(batchKey, secret, traderKey)
|
||||
tweakedTraderKey := input.TweakPubKeyWithTweak(
|
||||
traderKey, traderKeyTweak,
|
||||
)
|
||||
tweakedAuctioneerKey := input.TweakPubKey(
|
||||
auctioneerKey, tweakedTraderKey,
|
||||
)
|
||||
|
||||
for block := expiryFrom; block <= expiryTo; block++ {
|
||||
builder := txscript.NewScriptBuilder()
|
||||
|
||||
builder.AddData(tweakedTraderKey.SerializeCompressed())
|
||||
builder.AddOp(txscript.OP_CHECKSIGVERIFY)
|
||||
|
||||
builder.AddData(tweakedAuctioneerKey.SerializeCompressed())
|
||||
builder.AddOp(txscript.OP_CHECKSIG)
|
||||
|
||||
builder.AddOp(txscript.OP_IFDUP)
|
||||
builder.AddOp(txscript.OP_NOTIF)
|
||||
builder.AddInt64(int64(block))
|
||||
builder.AddOp(txscript.OP_CHECKLOCKTIMEVERIFY)
|
||||
builder.AddOp(txscript.OP_ENDIF)
|
||||
|
||||
currentScript, err := builder.Script()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error building script: %w", err)
|
||||
}
|
||||
|
||||
currentPkScript, err := input.WitnessScriptHash(currentScript)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error hashing script: %w", err)
|
||||
}
|
||||
if !bytes.Equal(currentPkScript, targetScript) {
|
||||
continue
|
||||
}
|
||||
|
||||
return &poolAccount{
|
||||
keyIndex: keyIndex,
|
||||
expiry: block,
|
||||
sharedKey: secret,
|
||||
batchKey: batchKey.SerializeCompressed(),
|
||||
keyTweak: traderKeyTweak,
|
||||
witnessScript: currentScript,
|
||||
version: poolscript.VersionWitnessScript,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("account script not derived")
|
||||
}
|
||||
|
||||
func fastScriptTaproot(scriptVersion poolscript.Version, keyIndex, expiryFrom,
|
||||
expiryTo uint32, traderKey, auctioneerKey, batchKey *btcec.PublicKey,
|
||||
secret [32]byte, targetScript []byte) (*poolAccount, error) {
|
||||
|
||||
parsedScript, err := txscript.ParsePkScript(targetScript)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parsedScript.Class() != txscript.WitnessV1TaprootTy {
|
||||
return nil, errors.New("incompatible script class")
|
||||
}
|
||||
|
||||
traderKeyTweak := poolscript.TraderKeyTweak(batchKey, secret, traderKey)
|
||||
tweakedTraderKey := input.TweakPubKeyWithTweak(
|
||||
traderKey, traderKeyTweak,
|
||||
)
|
||||
|
||||
var muSig2Version input.MuSig2Version
|
||||
switch scriptVersion {
|
||||
// The v0.4.0 MuSig2 implementation requires the keys to be serialized
|
||||
// using the Schnorr (32-byte x-only) serialization format.
|
||||
case poolscript.VersionTaprootMuSig2:
|
||||
muSig2Version = input.MuSig2Version040
|
||||
|
||||
var err error
|
||||
auctioneerKey, err = schnorr.ParsePubKey(
|
||||
schnorr.SerializePubKey(auctioneerKey),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing auctioneer key: "+
|
||||
"%w", err)
|
||||
}
|
||||
|
||||
traderKey, err = schnorr.ParsePubKey(
|
||||
schnorr.SerializePubKey(traderKey),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing trader key: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
// The v1.0.0-rc2 MuSig2 implementation works with the regular, 33-byte
|
||||
// compressed keys, so we can just pass them in as they are.
|
||||
case poolscript.VersionTaprootMuSig2V100RC2:
|
||||
muSig2Version = input.MuSig2Version100RC2
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid account version <%d>",
|
||||
scriptVersion)
|
||||
}
|
||||
|
||||
for block := expiryFrom; block <= expiryTo; block++ {
|
||||
builder := txscript.NewScriptBuilder()
|
||||
|
||||
builder.AddData(schnorr.SerializePubKey(tweakedTraderKey))
|
||||
builder.AddOp(txscript.OP_CHECKSIGVERIFY)
|
||||
|
||||
builder.AddInt64(int64(block))
|
||||
builder.AddOp(txscript.OP_CHECKLOCKTIMEVERIFY)
|
||||
|
||||
script, err := builder.Script()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rootHash := txscript.NewBaseTapLeaf(script).TapHash()
|
||||
aggregateKey, err := input.MuSig2CombineKeys(
|
||||
muSig2Version, []*btcec.PublicKey{
|
||||
auctioneerKey, traderKey,
|
||||
}, true, &input.MuSig2Tweaks{
|
||||
TaprootTweak: rootHash[:],
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error combining keys: %w", err)
|
||||
}
|
||||
|
||||
currentKey := schnorr.SerializePubKey(aggregateKey.FinalKey)
|
||||
if !bytes.Equal(currentKey, targetScript[2:]) {
|
||||
continue
|
||||
}
|
||||
|
||||
odd := aggregateKey.FinalKey.SerializeCompressed()[0] == oddByte
|
||||
controlBlock := txscript.ControlBlock{
|
||||
InternalKey: aggregateKey.PreTweakedKey,
|
||||
LeafVersion: txscript.BaseLeafVersion,
|
||||
OutputKeyYIsOdd: odd,
|
||||
}
|
||||
blockBytes, err := controlBlock.ToBytes()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error serializing control "+
|
||||
"block: %w", err)
|
||||
}
|
||||
|
||||
return &poolAccount{
|
||||
keyIndex: keyIndex,
|
||||
expiry: block,
|
||||
sharedKey: secret,
|
||||
batchKey: batchKey.SerializeCompressed(),
|
||||
keyTweak: traderKeyTweak,
|
||||
witnessScript: script,
|
||||
controlBlock: blockBytes,
|
||||
version: scriptVersion,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("account script not derived")
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/lightninglabs/chantools/lnd"
|
||||
"github.com/lightninglabs/pool/poolscript"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type testAccount struct {
|
||||
name string
|
||||
rootKey string
|
||||
pkScript string
|
||||
minExpiry uint32
|
||||
}
|
||||
|
||||
var (
|
||||
auctioneerKeyBytes, _ = hex.DecodeString(
|
||||
"0353c7c0d3258c4957331b86af335568232e9af8df61330cee3a7488b61c" +
|
||||
"f6c298",
|
||||
)
|
||||
auctioneerKey, _ = btcec.ParsePubKey(auctioneerKeyBytes)
|
||||
|
||||
testAccounts = []testAccount{{
|
||||
name: "regtest taproot (v1)",
|
||||
rootKey: "tprv8ZgxMBicQKsPdkvdLKn7HG2hhZ9Ewsgze1Yj3KDEcvb6H5U" +
|
||||
"519UtfoPPP3hYVgFTn7hXmvE41qaugbaYiZN8wM1HoQHhs3AzSwg" +
|
||||
"xGYdD8gM",
|
||||
pkScript: "512001e8d17b83358476534aae4eae2062ea9025dfd858cd81" +
|
||||
"7bac5f439969da92a6",
|
||||
minExpiry: 1600,
|
||||
}, {
|
||||
name: "regtest taproot (v2)",
|
||||
rootKey: "tprv8ZgxMBicQKsPdkvdLKn7HG2hhZ9Ewsgze1Yj3KDEcvb6H5U" +
|
||||
"519UtfoPPP3hYVgFTn7hXmvE41qaugbaYiZN8wM1HoQHhs3AzSwg" +
|
||||
"xGYdD8gM",
|
||||
pkScript: "51209dfee24b87f5c35d5a310496a64fab70641bd03d40d5cc" +
|
||||
"3720f6061f7435778a",
|
||||
minExpiry: 2060,
|
||||
}, {
|
||||
name: "regtest segwit (v0)",
|
||||
rootKey: "tprv8ZgxMBicQKsPdkvdLKn7HG2hhZ9Ewsgze1Yj3KDEcvb6H5U" +
|
||||
"519UtfoPPP3hYVgFTn7hXmvE41qaugbaYiZN8wM1HoQHhs3AzSwg" +
|
||||
"xGYdD8gM",
|
||||
pkScript: "00201acfd449370aca0f744141bc6fe1f9fe326aa57a9cd35f" +
|
||||
"bc2f8f15af4c0f4597",
|
||||
minExpiry: 1600,
|
||||
}}
|
||||
)
|
||||
|
||||
func TestClosePoolAccount(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
path := []uint32{
|
||||
lnd.HardenedKeyStart + uint32(keychain.BIP0043Purpose),
|
||||
lnd.HardenedKeyStart + chaincfg.RegressionNetParams.HDCoinType,
|
||||
lnd.HardenedKeyStart + uint32(poolscript.AccountKeyFamily),
|
||||
0,
|
||||
}
|
||||
const (
|
||||
maxBlocks = 50
|
||||
maxAccounts = 5
|
||||
maxBatchKeys = 10
|
||||
)
|
||||
|
||||
for _, tc := range testAccounts {
|
||||
t.Run(tc.name, func(tt *testing.T) {
|
||||
tt.Parallel()
|
||||
|
||||
extendedKey, err := hdkeychain.NewKeyFromString(
|
||||
tc.rootKey,
|
||||
)
|
||||
require.NoError(tt, err)
|
||||
accountBaseKey, err := lnd.DeriveChildren(
|
||||
extendedKey, path,
|
||||
)
|
||||
require.NoError(tt, err)
|
||||
targetScriptBytes, err := hex.DecodeString(tc.pkScript)
|
||||
require.NoError(tt, err)
|
||||
|
||||
acct, err := bruteForceAccountScript(
|
||||
accountBaseKey, auctioneerKey, tc.minExpiry,
|
||||
tc.minExpiry+maxBlocks, maxAccounts,
|
||||
maxBatchKeys, targetScriptBytes,
|
||||
)
|
||||
require.NoError(tt, err)
|
||||
t.Logf("Found account: %v", acct)
|
||||
})
|
||||
}
|
||||
}
|
@ -1,233 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
|
||||
"github.com/lightninglabs/chantools/lnd"
|
||||
"github.com/lightningnetwork/lnd/aezeed"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type createWalletCommand struct {
|
||||
WalletDBDir string
|
||||
GenerateSeed bool
|
||||
|
||||
rootKey *rootKey
|
||||
cmd *cobra.Command
|
||||
}
|
||||
|
||||
func newCreateWalletCommand() *cobra.Command {
|
||||
cc := &createWalletCommand{}
|
||||
cc.cmd = &cobra.Command{
|
||||
Use: "createwallet",
|
||||
Short: "Create a new lnd compatible wallet.db file from an " +
|
||||
"existing seed or by generating a new one",
|
||||
Long: `Creates a new wallet that can be used with lnd or with
|
||||
chantools. The wallet can be created from an existing seed or a new one can be
|
||||
generated (use --generateseed).`,
|
||||
Example: `chantools createwallet \
|
||||
--walletdbdir ~/.lnd/data/chain/bitcoin/mainnet`,
|
||||
RunE: cc.Execute,
|
||||
}
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.WalletDBDir, "walletdbdir", "", "the folder to create the "+
|
||||
"new wallet.db file in",
|
||||
)
|
||||
cc.cmd.Flags().BoolVar(
|
||||
&cc.GenerateSeed, "generateseed", false, "generate a new "+
|
||||
"seed instead of using an existing one",
|
||||
)
|
||||
|
||||
cc.rootKey = newRootKey(cc.cmd, "creating the new wallet")
|
||||
|
||||
return cc.cmd
|
||||
}
|
||||
|
||||
func (c *createWalletCommand) Execute(_ *cobra.Command, _ []string) error {
|
||||
var (
|
||||
publicWalletPw = lnwallet.DefaultPublicPassphrase
|
||||
privateWalletPw = lnwallet.DefaultPrivatePassphrase
|
||||
masterRootKey *hdkeychain.ExtendedKey
|
||||
birthday time.Time
|
||||
err error
|
||||
)
|
||||
|
||||
// Check that we have a wallet DB.
|
||||
if c.WalletDBDir == "" {
|
||||
return errors.New("wallet DB directory is required")
|
||||
}
|
||||
|
||||
// Make sure the directory (and parents) exists.
|
||||
if err := os.MkdirAll(c.WalletDBDir, 0700); err != nil {
|
||||
return fmt.Errorf("error creating wallet DB directory '%s': %w",
|
||||
c.WalletDBDir, err)
|
||||
}
|
||||
|
||||
// Check if we should create a new seed or read if from the console or
|
||||
// environment.
|
||||
if c.GenerateSeed {
|
||||
fmt.Printf("Generating new lnd compatible aezeed...\n")
|
||||
seed, err := aezeed.New(
|
||||
keychain.KeyDerivationVersionTaproot, nil, time.Now(),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating new seed: %w", err)
|
||||
}
|
||||
birthday = seed.BirthdayTime()
|
||||
|
||||
// Derive the master extended key from the seed.
|
||||
masterRootKey, err = hdkeychain.NewMaster(
|
||||
seed.Entropy[:], chainParams,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to derive master extended "+
|
||||
"key: %w", err)
|
||||
}
|
||||
|
||||
passphrase, err := lnd.ReadPassphrase("shouldn't use")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading passphrase: %w", err)
|
||||
}
|
||||
|
||||
mnemonic, err := seed.ToMnemonic(passphrase)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error converting seed to "+
|
||||
"mnemonic: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Generated new seed")
|
||||
printCipherSeedWords(mnemonic[:])
|
||||
} else {
|
||||
masterRootKey, birthday, err = c.rootKey.readWithBirthday()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// To automate things with chantools, we also offer reading the wallet
|
||||
// password from environment variables.
|
||||
pw := []byte(strings.TrimSpace(os.Getenv(lnd.PasswordEnvName)))
|
||||
|
||||
// Because we cannot differentiate between an empty and a non-existent
|
||||
// environment variable, we need a special character that indicates that
|
||||
// no password should be used. We use a single dash (-) for that as that
|
||||
// would be too short for an explicit password anyway.
|
||||
switch {
|
||||
// The user indicated in the environment variable that no passphrase
|
||||
// should be used. We don't set any value.
|
||||
case string(pw) == "-":
|
||||
|
||||
// The environment variable didn't contain anything, we'll read the
|
||||
// passphrase from the terminal.
|
||||
case len(pw) == 0:
|
||||
fmt.Printf("\n\nThe wallet password is used to encrypt the " +
|
||||
"wallet.db file itself and is unrelated to the seed.\n")
|
||||
pw, err = lnd.PasswordFromConsole("Input new wallet password: ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pw2, err := lnd.PasswordFromConsole(
|
||||
"Confirm new wallet password: ",
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !bytes.Equal(pw, pw2) {
|
||||
return errors.New("passwords don't match")
|
||||
}
|
||||
|
||||
if len(pw) > 0 {
|
||||
publicWalletPw = pw
|
||||
privateWalletPw = pw
|
||||
}
|
||||
|
||||
// There was a password in the environment, just use it directly.
|
||||
default:
|
||||
publicWalletPw = pw
|
||||
privateWalletPw = pw
|
||||
}
|
||||
|
||||
// Try to create the wallet.
|
||||
loader, err := btcwallet.NewWalletLoader(
|
||||
chainParams, 0, btcwallet.LoaderWithLocalWalletDB(
|
||||
c.WalletDBDir, true, 0,
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating wallet loader: %w", err)
|
||||
}
|
||||
|
||||
_, err = loader.CreateNewWalletExtendedKey(
|
||||
publicWalletPw, privateWalletPw, masterRootKey, birthday,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating new wallet: %w", err)
|
||||
}
|
||||
|
||||
if err := loader.UnloadWallet(); err != nil {
|
||||
return fmt.Errorf("error unloading wallet: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Wallet created successfully at %v\n", c.WalletDBDir)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func printCipherSeedWords(mnemonicWords []string) {
|
||||
fmt.Println("!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " +
|
||||
"RESTORE THE WALLET!!!")
|
||||
fmt.Println()
|
||||
|
||||
fmt.Println("---------------BEGIN LND CIPHER SEED---------------")
|
||||
|
||||
numCols := 4
|
||||
colWords := monoWidthColumns(mnemonicWords, numCols)
|
||||
for i := 0; i < len(colWords); i += numCols {
|
||||
fmt.Printf("%2d. %3s %2d. %3s %2d. %3s %2d. %3s\n",
|
||||
i+1, colWords[i], i+2, colWords[i+1], i+3,
|
||||
colWords[i+2], i+4, colWords[i+3])
|
||||
}
|
||||
|
||||
fmt.Println("---------------END LND CIPHER SEED-----------------")
|
||||
|
||||
fmt.Println("\n!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " +
|
||||
"RESTORE THE WALLET!!!")
|
||||
}
|
||||
|
||||
// monoWidthColumns takes a set of words, and the number of desired columns,
|
||||
// and returns a new set of words that have had white space appended to the
|
||||
// word in order to create a mono-width column.
|
||||
func monoWidthColumns(words []string, ncols int) []string {
|
||||
// Determine max size of words in each column.
|
||||
colWidths := make([]int, ncols)
|
||||
for i, word := range words {
|
||||
col := i % ncols
|
||||
curWidth := colWidths[col]
|
||||
if len(word) > curWidth {
|
||||
colWidths[col] = len(word)
|
||||
}
|
||||
}
|
||||
|
||||
// Append whitespace to each word to make columns mono-width.
|
||||
finalWords := make([]string, len(words))
|
||||
for i, word := range words {
|
||||
col := i % ncols
|
||||
width := colWidths[col]
|
||||
|
||||
diff := width - len(word)
|
||||
finalWords[i] = word + strings.Repeat(" ", diff)
|
||||
}
|
||||
|
||||
return finalWords
|
||||
}
|
@ -1,365 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/mempool"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/lightninglabs/chantools/lnd"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type doubleSpendInputs struct {
|
||||
APIURL string
|
||||
InputOutpoints []string
|
||||
Publish bool
|
||||
SweepAddr string
|
||||
FeeRate uint32
|
||||
RecoveryWindow uint32
|
||||
|
||||
rootKey *rootKey
|
||||
cmd *cobra.Command
|
||||
}
|
||||
|
||||
func newDoubleSpendInputsCommand() *cobra.Command {
|
||||
cc := &doubleSpendInputs{}
|
||||
cc.cmd = &cobra.Command{
|
||||
Use: "doublespendinputs",
|
||||
Short: "Replace a transaction by double spending its input",
|
||||
Long: `Tries to double spend the given inputs by deriving the
|
||||
private for the address and sweeping the funds to the given address. This can
|
||||
only be used with inputs that belong to an lnd wallet.`,
|
||||
Example: `chantools doublespendinputs \
|
||||
--inputoutpoints xxxxxxxxx:y,xxxxxxxxx:y \
|
||||
--sweepaddr bc1q..... \
|
||||
--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().StringSliceVar(
|
||||
&cc.InputOutpoints, "inputoutpoints", []string{},
|
||||
"list of outpoints to double spend in the format txid:vout",
|
||||
)
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.SweepAddr, "sweepaddr", "", "address to recover the funds "+
|
||||
"to; specify '"+lnd.AddressDeriveFromWallet+"' to "+
|
||||
"derive a new address from the seed automatically",
|
||||
)
|
||||
cc.cmd.Flags().Uint32Var(
|
||||
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
|
||||
"use for the sweep transaction in sat/vByte",
|
||||
)
|
||||
cc.cmd.Flags().Uint32Var(
|
||||
&cc.RecoveryWindow, "recoverywindow", defaultRecoveryWindow,
|
||||
"number of keys to scan per internal/external branch; output "+
|
||||
"will consist of double this amount of keys",
|
||||
)
|
||||
cc.cmd.Flags().BoolVar(
|
||||
&cc.Publish, "publish", false, "publish replacement TX to "+
|
||||
"the chain API instead of just printing the TX",
|
||||
)
|
||||
|
||||
cc.rootKey = newRootKey(cc.cmd, "deriving the input keys")
|
||||
|
||||
return cc.cmd
|
||||
}
|
||||
|
||||
func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error {
|
||||
extendedKey, err := c.rootKey.read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading root key: %w", err)
|
||||
}
|
||||
|
||||
// Make sure sweep addr is set.
|
||||
err = lnd.CheckAddress(
|
||||
c.SweepAddr, chainParams, true, "sweep", lnd.AddrTypeP2WKH,
|
||||
lnd.AddrTypeP2TR,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Make sure we have at least one input.
|
||||
if len(c.InputOutpoints) == 0 {
|
||||
return errors.New("inputoutpoints are required")
|
||||
}
|
||||
|
||||
api := newExplorerAPI(c.APIURL)
|
||||
|
||||
addresses := make([]btcutil.Address, 0, len(c.InputOutpoints))
|
||||
outpoints := make([]*wire.OutPoint, 0, len(c.InputOutpoints))
|
||||
privKeys := make([]*secp256k1.PrivateKey, 0, len(c.InputOutpoints))
|
||||
|
||||
// Get the addresses for the inputs.
|
||||
for _, inputOutpoint := range c.InputOutpoints {
|
||||
addrString, err := api.Address(inputOutpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addr, err := btcutil.DecodeAddress(addrString, chainParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addresses = append(addresses, addr)
|
||||
|
||||
txHash, err := chainhash.NewHashFromStr(inputOutpoint[:64])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vout, err := strconv.Atoi(inputOutpoint[65:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outpoint := wire.NewOutPoint(txHash, uint32(vout))
|
||||
|
||||
outpoints = append(outpoints, outpoint)
|
||||
}
|
||||
|
||||
// Create the paths for the addresses.
|
||||
p2wkhPath, err := lnd.ParsePath(lnd.WalletDefaultDerivationPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p2trPath, err := lnd.ParsePath(lnd.WalletBIP86DerivationPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start with the txweight estimator.
|
||||
var estimator input.TxWeightEstimator
|
||||
sweepScript, err := lnd.PrepareWalletAddress(
|
||||
c.SweepAddr, chainParams, &estimator, extendedKey, "sweep",
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Find the key for the given addresses and add their
|
||||
// output weight to the tx estimator.
|
||||
for _, addr := range addresses {
|
||||
var key *hdkeychain.ExtendedKey
|
||||
switch addr.(type) {
|
||||
case *btcutil.AddressWitnessPubKeyHash:
|
||||
key, err = iterateOverPath(
|
||||
extendedKey, addr, p2wkhPath, c.RecoveryWindow,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
estimator.AddP2WKHInput()
|
||||
|
||||
case *btcutil.AddressTaproot:
|
||||
key, err = iterateOverPath(
|
||||
extendedKey, addr, p2trPath, c.RecoveryWindow,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
estimator.AddTaprootKeySpendInput(
|
||||
txscript.SigHashDefault,
|
||||
)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("address type %T not supported", addr)
|
||||
}
|
||||
|
||||
// Get the private key.
|
||||
privKey, err := key.ECPrivKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
privKeys = append(privKeys, privKey)
|
||||
}
|
||||
|
||||
// Now that we have the keys, we can create the transaction.
|
||||
prevOuts := make(map[wire.OutPoint]*wire.TxOut)
|
||||
|
||||
// Next get the full value of the inputs.
|
||||
var totalInput btcutil.Amount
|
||||
for _, outpoint := range outpoints {
|
||||
// Get the transaction.
|
||||
tx, err := api.Transaction(outpoint.Hash.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
value := tx.Vout[outpoint.Index].Value
|
||||
|
||||
// Get the output index.
|
||||
totalInput += btcutil.Amount(value)
|
||||
|
||||
scriptPubkey, err := hex.DecodeString(
|
||||
tx.Vout[outpoint.Index].ScriptPubkey,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add the output to the map.
|
||||
prevOuts[*outpoint] = &wire.TxOut{
|
||||
Value: int64(value),
|
||||
PkScript: scriptPubkey,
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the fee.
|
||||
feeRateKWeight := chainfee.SatPerKVByte(1000 * c.FeeRate).FeePerKWeight()
|
||||
totalFee := feeRateKWeight.FeeForWeight(estimator.Weight())
|
||||
|
||||
// Create the transaction.
|
||||
tx := wire.NewMsgTx(2)
|
||||
|
||||
// Add the inputs.
|
||||
for _, outpoint := range outpoints {
|
||||
tx.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: *outpoint,
|
||||
Sequence: mempool.MaxRBFSequence,
|
||||
})
|
||||
}
|
||||
|
||||
tx.AddTxOut(wire.NewTxOut(int64(totalInput-totalFee), sweepScript))
|
||||
|
||||
// Calculate the signature hash.
|
||||
prevOutFetcher := txscript.NewMultiPrevOutFetcher(prevOuts)
|
||||
sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher)
|
||||
|
||||
// Sign the inputs depending on the address type.
|
||||
for i, outpoint := range outpoints {
|
||||
switch addresses[i].(type) {
|
||||
case *btcutil.AddressWitnessPubKeyHash:
|
||||
witness, err := txscript.WitnessSignature(
|
||||
tx, sigHashes, i, prevOuts[*outpoint].Value,
|
||||
prevOuts[*outpoint].PkScript,
|
||||
txscript.SigHashAll, privKeys[i], true,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx.TxIn[i].Witness = witness
|
||||
|
||||
case *btcutil.AddressTaproot:
|
||||
rawTxSig, err := txscript.RawTxInTaprootSignature(
|
||||
tx, sigHashes, i,
|
||||
prevOuts[*outpoint].Value,
|
||||
prevOuts[*outpoint].PkScript,
|
||||
[]byte{}, txscript.SigHashDefault, privKeys[i],
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx.TxIn[i].Witness = wire.TxWitness{
|
||||
rawTxSig,
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("address type %T not supported",
|
||||
addresses[i])
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize the transaction.
|
||||
var txBuf bytes.Buffer
|
||||
if err := tx.Serialize(&txBuf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Print the transaction.
|
||||
fmt.Printf("Sweeping transaction:\n%x\n", txBuf.Bytes())
|
||||
|
||||
// Publish the transaction.
|
||||
if c.Publish {
|
||||
txid, err := api.PublishTx(hex.EncodeToString(txBuf.Bytes()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Published transaction with txid %s\n", txid)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// iterateOverPath iterates over the given key path and tries to find the
|
||||
// private key that corresponds to the given address.
|
||||
func iterateOverPath(baseKey *hdkeychain.ExtendedKey, addr btcutil.Address,
|
||||
path []uint32, maxTries uint32) (*hdkeychain.ExtendedKey, error) {
|
||||
|
||||
for i := range maxTries {
|
||||
// Check for both the external and internal branch.
|
||||
for _, branch := range []uint32{0, 1} {
|
||||
// Create the path to derive the key.
|
||||
addrPath := append(path, branch, i) //nolint:gocritic
|
||||
|
||||
// Derive the key.
|
||||
derivedKey, err := lnd.DeriveChildren(baseKey, addrPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var address btcutil.Address
|
||||
switch addr.(type) {
|
||||
case *btcutil.AddressWitnessPubKeyHash:
|
||||
// Get the address for the derived key.
|
||||
derivedAddr, err := derivedKey.Address(chainParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
address, err = btcutil.NewAddressWitnessPubKeyHash(
|
||||
derivedAddr.ScriptAddress(), chainParams,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case *btcutil.AddressTaproot:
|
||||
|
||||
pubkey, err := derivedKey.ECPubKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pubkey = txscript.ComputeTaprootKeyNoScript(pubkey)
|
||||
|
||||
address, err = btcutil.NewAddressTaproot(
|
||||
schnorr.SerializePubKey(pubkey), chainParams,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Compare the addresses.
|
||||
if address.String() == addr.String() {
|
||||
return derivedKey, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("could not find key for address %s", addr.String())
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/lightninglabs/chantools/lnd"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
zombieBucket = []byte("zombie-index")
|
||||
)
|
||||
|
||||
type dropGraphZombiesCommand struct {
|
||||
ChannelDB string
|
||||
NodeIdentityKey string
|
||||
FixOnly bool
|
||||
|
||||
SingleChannel uint64
|
||||
|
||||
cmd *cobra.Command
|
||||
}
|
||||
|
||||
func newDropGraphZombiesCommand() *cobra.Command {
|
||||
cc := &dropGraphZombiesCommand{}
|
||||
cc.cmd = &cobra.Command{
|
||||
Use: "dropgraphzombies",
|
||||
Short: "Remove all channels identified as zombies from the " +
|
||||
"graph to force a re-sync of the graph",
|
||||
Long: `This command removes all channels that were identified as
|
||||
zombies from the local graph.
|
||||
|
||||
This will cause lnd to re-download all those channels from the network and can
|
||||
be helpful to fix a graph that is out of sync with the network.
|
||||
|
||||
CAUTION: Running this command will make it impossible to use the channel DB
|
||||
with an older version of lnd. Downgrading is not possible and you'll need to
|
||||
run lnd ` + lndVersion + ` or later after using this command!'`,
|
||||
Example: `chantools dropgraphzombies \
|
||||
--channeldb ~/.lnd/data/graph/mainnet/channel.db`,
|
||||
RunE: cc.Execute,
|
||||
}
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.ChannelDB, "channeldb", "", "lnd channel.db file to drop "+
|
||||
"zombies from",
|
||||
)
|
||||
|
||||
return cc.cmd
|
||||
}
|
||||
|
||||
func (c *dropGraphZombiesCommand) Execute(_ *cobra.Command, _ []string) error {
|
||||
// Check that we have a channel DB.
|
||||
if c.ChannelDB == "" {
|
||||
return errors.New("channel DB is required")
|
||||
}
|
||||
db, err := lnd.OpenDB(c.ChannelDB, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error opening rescue DB: %w", err)
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
log.Infof("Dropping zombie channel bucket")
|
||||
|
||||
rwTx, err := db.BeginReadWriteTx()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
success := false
|
||||
defer func() {
|
||||
if !success {
|
||||
_ = rwTx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
edges := rwTx.ReadWriteBucket(edgeBucket)
|
||||
if edges == nil {
|
||||
return channeldb.ErrGraphNoEdgesFound
|
||||
}
|
||||
|
||||
if err := edges.DeleteNestedBucket(zombieBucket); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
success = true
|
||||
return rwTx.Commit()
|
||||
}
|
@ -1,531 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/mempool"
|
||||
"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/spf13/cobra"
|
||||
)
|
||||
|
||||
type pullAnchorCommand struct {
|
||||
APIURL string
|
||||
SponsorInput string
|
||||
AnchorAddrs []string
|
||||
ChangeAddr string
|
||||
FeeRate uint32
|
||||
|
||||
rootKey *rootKey
|
||||
cmd *cobra.Command
|
||||
}
|
||||
|
||||
func newPullAnchorCommand() *cobra.Command {
|
||||
cc := &pullAnchorCommand{}
|
||||
cc.cmd = &cobra.Command{
|
||||
Use: "pullanchor",
|
||||
Short: "Attempt to CPFP an anchor output of a channel",
|
||||
Long: `Use this command to confirm a channel force close
|
||||
transaction of an anchor output channel type. This will attempt to CPFP the
|
||||
330 byte anchor output created for your node.`,
|
||||
Example: `chantools pullanchor \
|
||||
--sponsorinput txid:vout \
|
||||
--anchoraddr bc1q..... \
|
||||
--changeaddr bc1q..... \
|
||||
--feerate 30`,
|
||||
RunE: cc.Execute,
|
||||
}
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
|
||||
"be esplora compatible)",
|
||||
)
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.SponsorInput, "sponsorinput", "", "the input to use to "+
|
||||
"sponsor the CPFP transaction; must be owned by the "+
|
||||
"lnd node that owns the anchor output",
|
||||
)
|
||||
cc.cmd.Flags().StringArrayVar(
|
||||
&cc.AnchorAddrs, "anchoraddr", nil, "the address of the "+
|
||||
"anchor output (p2wsh or p2tr output with 330 "+
|
||||
"satoshis) that should be pulled; can be specified "+
|
||||
"multiple times per command to pull multiple anchors "+
|
||||
"with a single transaction",
|
||||
)
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.ChangeAddr, "changeaddr", "", "the change address to "+
|
||||
"send the remaining funds back to; specify '"+
|
||||
lnd.AddressDeriveFromWallet+"' to derive a new "+
|
||||
"address from the seed automatically",
|
||||
)
|
||||
cc.cmd.Flags().Uint32Var(
|
||||
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
|
||||
"use for the sweep transaction in sat/vByte",
|
||||
)
|
||||
|
||||
cc.rootKey = newRootKey(cc.cmd, "deriving keys")
|
||||
|
||||
return cc.cmd
|
||||
}
|
||||
|
||||
func (c *pullAnchorCommand) Execute(_ *cobra.Command, _ []string) error {
|
||||
extendedKey, err := c.rootKey.read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading root key: %w", err)
|
||||
}
|
||||
|
||||
// Make sure all input is provided.
|
||||
if c.SponsorInput == "" {
|
||||
return errors.New("sponsor input is required")
|
||||
}
|
||||
if len(c.AnchorAddrs) == 0 {
|
||||
return errors.New("at least one anchor addr is required")
|
||||
}
|
||||
for _, anchorAddr := range c.AnchorAddrs {
|
||||
err = lnd.CheckAddress(
|
||||
anchorAddr, chainParams, true, "anchor",
|
||||
lnd.AddrTypeP2WSH, lnd.AddrTypeP2TR,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = lnd.CheckAddress(
|
||||
c.ChangeAddr, chainParams, true, "change", lnd.AddrTypeP2WKH,
|
||||
lnd.AddrTypeP2TR,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outpoint, err := lnd.ParseOutpoint(c.SponsorInput)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing sponsor input outpoint: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
// Set default values.
|
||||
if c.FeeRate == 0 {
|
||||
c.FeeRate = defaultFeeSatPerVByte
|
||||
}
|
||||
return createPullTransactionTemplate(
|
||||
extendedKey, c.APIURL, outpoint, c.AnchorAddrs, c.ChangeAddr,
|
||||
c.FeeRate,
|
||||
)
|
||||
}
|
||||
|
||||
type targetAnchor struct {
|
||||
addr string
|
||||
keyDesc *keychain.KeyDescriptor
|
||||
outpoint wire.OutPoint
|
||||
utxo *wire.TxOut
|
||||
script []byte
|
||||
scriptTree *input.AnchorScriptTree
|
||||
}
|
||||
|
||||
func createPullTransactionTemplate(rootKey *hdkeychain.ExtendedKey,
|
||||
apiURL string, sponsorOutpoint *wire.OutPoint, anchorAddrs []string,
|
||||
changeAddr string, feeRate uint32) error {
|
||||
|
||||
var (
|
||||
signer = &lnd.Signer{
|
||||
ExtendedKey: rootKey,
|
||||
ChainParams: chainParams,
|
||||
}
|
||||
api = newExplorerAPI(apiURL)
|
||||
estimator input.TxWeightEstimator
|
||||
)
|
||||
|
||||
changeScript, err := lnd.PrepareWalletAddress(
|
||||
changeAddr, chainParams, &estimator, rootKey, "change",
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Make sure the sponsor input is a P2WPKH or P2TR input and is known
|
||||
// to the block explorer, so we can fetch the witness utxo.
|
||||
sponsorTx, err := api.Transaction(sponsorOutpoint.Hash.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching sponsor tx: %w", err)
|
||||
}
|
||||
sponsorTxOut := sponsorTx.Vout[sponsorOutpoint.Index]
|
||||
sponsorPkScript, err := hex.DecodeString(sponsorTxOut.ScriptPubkey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding sponsor pkscript: %w", err)
|
||||
}
|
||||
|
||||
sponsorType, err := txscript.ParsePkScript(sponsorPkScript)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing sponsor pkscript: %w", err)
|
||||
}
|
||||
var sponsorSigHashType txscript.SigHashType
|
||||
switch sponsorType.Class() {
|
||||
case txscript.WitnessV0PubKeyHashTy:
|
||||
estimator.AddP2WKHInput()
|
||||
sponsorSigHashType = txscript.SigHashAll
|
||||
|
||||
case txscript.WitnessV1TaprootTy:
|
||||
sponsorSigHashType = txscript.SigHashDefault
|
||||
estimator.AddTaprootKeySpendInput(sponsorSigHashType)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported sponsor input type: %v",
|
||||
sponsorType.Class())
|
||||
}
|
||||
|
||||
tx := wire.NewMsgTx(2)
|
||||
packet, err := psbt.NewFromUnsignedTx(tx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating PSBT: %w", err)
|
||||
}
|
||||
|
||||
// Let's add the sponsor input to the PSBT.
|
||||
sponsorUtxo := &wire.TxOut{
|
||||
Value: int64(sponsorTxOut.Value),
|
||||
PkScript: sponsorPkScript,
|
||||
}
|
||||
packet.UnsignedTx.TxIn = append(packet.UnsignedTx.TxIn, &wire.TxIn{
|
||||
PreviousOutPoint: *sponsorOutpoint,
|
||||
Sequence: mempool.MaxRBFSequence,
|
||||
})
|
||||
packet.Inputs = append(packet.Inputs, psbt.PInput{
|
||||
WitnessUtxo: sponsorUtxo,
|
||||
SighashType: sponsorSigHashType,
|
||||
})
|
||||
|
||||
targets, err := addAnchorInputs(
|
||||
anchorAddrs, packet, api, &estimator, rootKey,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding anchor inputs: %w", err)
|
||||
}
|
||||
|
||||
// Now we can calculate the fee and add the change output.
|
||||
anchorAmt := uint64(len(anchorAddrs)) * 330
|
||||
totalOutputValue := btcutil.Amount(sponsorTxOut.Value + anchorAmt)
|
||||
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()
|
||||
totalFee := feeRateKWeight.FeeForWeight(estimator.Weight())
|
||||
|
||||
log.Infof("Fee %d sats of %d total amount (estimated weight %d)",
|
||||
totalFee, totalOutputValue, estimator.Weight())
|
||||
|
||||
packet.UnsignedTx.TxOut = append(packet.UnsignedTx.TxOut, &wire.TxOut{
|
||||
Value: int64(totalOutputValue - totalFee),
|
||||
PkScript: changeScript,
|
||||
})
|
||||
packet.Outputs = append(packet.Outputs, psbt.POutput{})
|
||||
|
||||
prevOutFetcher := txscript.NewMultiPrevOutFetcher(
|
||||
map[wire.OutPoint]*wire.TxOut{
|
||||
*sponsorOutpoint: sponsorUtxo,
|
||||
},
|
||||
)
|
||||
for idx := range targets {
|
||||
prevOutFetcher.AddPrevOut(
|
||||
targets[idx].outpoint, targets[idx].utxo,
|
||||
)
|
||||
}
|
||||
|
||||
// And now we sign the anchor inputs.
|
||||
for idx := range targets {
|
||||
target := targets[idx]
|
||||
signDesc := &input.SignDescriptor{
|
||||
KeyDesc: *target.keyDesc,
|
||||
WitnessScript: target.script,
|
||||
Output: target.utxo,
|
||||
PrevOutputFetcher: prevOutFetcher,
|
||||
InputIndex: idx + 1,
|
||||
}
|
||||
|
||||
var anchorWitness wire.TxWitness
|
||||
switch {
|
||||
// Simple Taproot Channel:
|
||||
case target.scriptTree != nil:
|
||||
signDesc.SignMethod = input.TaprootKeySpendSignMethod
|
||||
signDesc.HashType = txscript.SigHashDefault
|
||||
signDesc.TapTweak = target.scriptTree.TapscriptRoot
|
||||
|
||||
anchorSig, err := signer.SignOutputRaw(
|
||||
packet.UnsignedTx, signDesc,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error signing anchor "+
|
||||
"input: %w", err)
|
||||
}
|
||||
|
||||
anchorWitness = wire.TxWitness{
|
||||
anchorSig.Serialize(),
|
||||
}
|
||||
|
||||
// Anchor Channel:
|
||||
default:
|
||||
signDesc.SignMethod = input.WitnessV0SignMethod
|
||||
signDesc.HashType = txscript.SigHashAll
|
||||
|
||||
anchorSig, err := signer.SignOutputRaw(
|
||||
packet.UnsignedTx, signDesc,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error signing anchor "+
|
||||
"input: %w", err)
|
||||
}
|
||||
|
||||
anchorWitness = make(wire.TxWitness, 2)
|
||||
anchorWitness[0] = append(
|
||||
anchorSig.Serialize(),
|
||||
byte(txscript.SigHashAll),
|
||||
)
|
||||
anchorWitness[1] = target.script
|
||||
}
|
||||
|
||||
var witnessBuf bytes.Buffer
|
||||
err = psbt.WriteTxWitness(&witnessBuf, anchorWitness)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error serializing witness: %w", err)
|
||||
}
|
||||
|
||||
packet.Inputs[idx+1].FinalScriptWitness = witnessBuf.Bytes()
|
||||
}
|
||||
|
||||
packetBase64, err := packet.B64Encode()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error encoding PSBT: %w", err)
|
||||
}
|
||||
|
||||
log.Infof("Prepared PSBT follows, please now call\n" +
|
||||
"'lncli wallet psbt finalize <psbt>' to finalize the\n" +
|
||||
"transaction, then publish it manually or by using\n" +
|
||||
"'lncli wallet publishtx <final_tx>':\n\n" + packetBase64 +
|
||||
"\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addAnchorInputs(anchorAddrs []string, packet *psbt.Packet,
|
||||
api *btc.ExplorerAPI, estimator *input.TxWeightEstimator,
|
||||
rootKey *hdkeychain.ExtendedKey) ([]targetAnchor, error) {
|
||||
|
||||
// Fetch the additional info we need for the anchor output as well.
|
||||
results := make([]targetAnchor, len(anchorAddrs))
|
||||
for idx, anchorAddr := range anchorAddrs {
|
||||
anchorTx, anchorIndex, err := api.Outpoint(anchorAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching anchor "+
|
||||
"outpoint: %w", err)
|
||||
}
|
||||
anchorTxHash, err := chainhash.NewHashFromStr(anchorTx.TXID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding anchor txid: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
addr, err := btcutil.DecodeAddress(anchorAddr, chainParams)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding address: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
anchorPkScript, err := txscript.PayToAddrScript(addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating pk script: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
target := targetAnchor{
|
||||
addr: anchorAddr,
|
||||
utxo: &wire.TxOut{
|
||||
Value: 330,
|
||||
PkScript: anchorPkScript,
|
||||
},
|
||||
outpoint: wire.OutPoint{
|
||||
Hash: *anchorTxHash,
|
||||
Index: uint32(anchorIndex),
|
||||
},
|
||||
}
|
||||
switch addr.(type) {
|
||||
case *btcutil.AddressWitnessScriptHash:
|
||||
estimator.AddWitnessInput(input.AnchorWitnessSize)
|
||||
|
||||
anchorKeyDesc, anchorWitnessScript, err := findAnchorKey(
|
||||
rootKey, anchorPkScript,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not find "+
|
||||
"key for anchor address %v: %w",
|
||||
anchorAddr, err)
|
||||
}
|
||||
|
||||
target.keyDesc = anchorKeyDesc
|
||||
target.script = anchorWitnessScript
|
||||
|
||||
case *btcutil.AddressTaproot:
|
||||
estimator.AddTaprootKeySpendInput(
|
||||
txscript.SigHashDefault,
|
||||
)
|
||||
|
||||
anchorKeyDesc, scriptTree, err := findTaprootAnchorKey(
|
||||
rootKey, anchorPkScript,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not find "+
|
||||
"key for anchor address %v: %w",
|
||||
anchorAddr, err)
|
||||
}
|
||||
|
||||
target.keyDesc = anchorKeyDesc
|
||||
target.scriptTree = scriptTree
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported address type: %T",
|
||||
addr)
|
||||
}
|
||||
|
||||
log.Infof("Found multisig key %x for anchor pk script %x",
|
||||
target.keyDesc.PubKey.SerializeCompressed(),
|
||||
anchorPkScript)
|
||||
|
||||
packet.UnsignedTx.TxIn = append(
|
||||
packet.UnsignedTx.TxIn, &wire.TxIn{
|
||||
PreviousOutPoint: target.outpoint,
|
||||
Sequence: mempool.MaxRBFSequence,
|
||||
},
|
||||
)
|
||||
packet.Inputs = append(packet.Inputs, psbt.PInput{
|
||||
WitnessUtxo: target.utxo,
|
||||
WitnessScript: target.script,
|
||||
})
|
||||
|
||||
results[idx] = target
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func findAnchorKey(rootKey *hdkeychain.ExtendedKey,
|
||||
targetScript []byte) (*keychain.KeyDescriptor, []byte, error) {
|
||||
|
||||
family := keychain.KeyFamilyMultiSig
|
||||
localMultisig, err := lnd.DeriveChildren(rootKey, []uint32{
|
||||
lnd.HardenedKeyStart + uint32(keychain.BIP0043Purpose),
|
||||
lnd.HardenedKeyStart + chainParams.HDCoinType,
|
||||
lnd.HardenedKeyStart + uint32(family),
|
||||
0,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("could not derive local "+
|
||||
"multisig key: %w", err)
|
||||
}
|
||||
|
||||
// Loop through the local multisig keys to find the target anchor
|
||||
// script.
|
||||
for index := range uint32(math.MaxInt16) {
|
||||
currentKey, err := localMultisig.DeriveNonStandard(index)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error deriving child "+
|
||||
"key: %w", err)
|
||||
}
|
||||
|
||||
currentPubkey, err := currentKey.ECPubKey()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error deriving public "+
|
||||
"key: %w", err)
|
||||
}
|
||||
|
||||
script, err := input.CommitScriptAnchor(currentPubkey)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error deriving script: "+
|
||||
"%w", err)
|
||||
}
|
||||
|
||||
pkScript, err := input.WitnessScriptHash(script)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error deriving script "+
|
||||
"hash: %w", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(pkScript, targetScript) {
|
||||
continue
|
||||
}
|
||||
|
||||
return &keychain.KeyDescriptor{
|
||||
PubKey: currentPubkey,
|
||||
KeyLocator: keychain.KeyLocator{
|
||||
Family: family,
|
||||
Index: index,
|
||||
},
|
||||
}, script, nil
|
||||
}
|
||||
|
||||
return nil, nil, errors.New("no matching pubkeys found")
|
||||
}
|
||||
|
||||
func findTaprootAnchorKey(rootKey *hdkeychain.ExtendedKey,
|
||||
targetScript []byte) (*keychain.KeyDescriptor, *input.AnchorScriptTree,
|
||||
error) {
|
||||
|
||||
family := keychain.KeyFamilyPaymentBase
|
||||
localPayment, err := lnd.DeriveChildren(rootKey, []uint32{
|
||||
lnd.HardenedKeyStart + uint32(keychain.BIP0043Purpose),
|
||||
lnd.HardenedKeyStart + chainParams.HDCoinType,
|
||||
lnd.HardenedKeyStart + uint32(family),
|
||||
0,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("could not derive local "+
|
||||
"multisig key: %w", err)
|
||||
}
|
||||
|
||||
// Loop through the local multisig keys to find the target anchor
|
||||
// script.
|
||||
for index := range uint32(math.MaxInt16) {
|
||||
currentKey, err := localPayment.DeriveNonStandard(index)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error deriving child "+
|
||||
"key: %w", err)
|
||||
}
|
||||
|
||||
currentPubkey, err := currentKey.ECPubKey()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error deriving public "+
|
||||
"key: %w", err)
|
||||
}
|
||||
|
||||
scriptTree, err := input.NewAnchorScriptTree(currentPubkey)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error deriving taproot "+
|
||||
"key: %w", err)
|
||||
}
|
||||
|
||||
pkScript, err := input.PayToTaprootScript(scriptTree.TaprootKey)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error deriving pk "+
|
||||
"script: %w", err)
|
||||
}
|
||||
if !bytes.Equal(pkScript, targetScript) {
|
||||
continue
|
||||
}
|
||||
|
||||
return &keychain.KeyDescriptor{
|
||||
PubKey: currentPubkey,
|
||||
KeyLocator: keychain.KeyLocator{
|
||||
Family: family,
|
||||
Index: index,
|
||||
},
|
||||
}, scriptTree, nil
|
||||
}
|
||||
|
||||
return nil, nil, errors.New("no matching pubkeys found")
|
||||
}
|
@ -1,425 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/chantools/lnd"
|
||||
"github.com/lightninglabs/loop/loopdb"
|
||||
"github.com/lightninglabs/loop/swap"
|
||||
"github.com/lightninglabs/loop/utils"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
errSwapNotFound = errors.New("loop in swap not found")
|
||||
)
|
||||
|
||||
type recoverLoopInCommand struct {
|
||||
TxID string
|
||||
Vout uint32
|
||||
SwapHash string
|
||||
SweepAddr string
|
||||
OutputAmt uint64
|
||||
FeeRate uint32
|
||||
StartKeyIndex int
|
||||
NumTries int
|
||||
|
||||
APIURL string
|
||||
Publish bool
|
||||
|
||||
LoopDbDir string
|
||||
SqliteFile string
|
||||
|
||||
rootKey *rootKey
|
||||
cmd *cobra.Command
|
||||
}
|
||||
|
||||
func newRecoverLoopInCommand() *cobra.Command {
|
||||
cc := &recoverLoopInCommand{}
|
||||
cc.cmd = &cobra.Command{
|
||||
Use: "recoverloopin",
|
||||
Short: "Recover a loop in swap that the loop daemon " +
|
||||
"is not able to sweep",
|
||||
Example: `chantools recoverloopin \
|
||||
--txid abcdef01234... \
|
||||
--vout 0 \
|
||||
--swap_hash abcdef01234... \
|
||||
--loop_db_dir /path/to/loop/db/dir \
|
||||
--sweep_addr bc1pxxxxxxx \
|
||||
--feerate 10`,
|
||||
RunE: cc.Execute,
|
||||
}
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.TxID, "txid", "", "transaction id of the on-chain "+
|
||||
"transaction that created the HTLC",
|
||||
)
|
||||
cc.cmd.Flags().Uint32Var(
|
||||
&cc.Vout, "vout", 0, "output index of the on-chain "+
|
||||
"transaction that created the HTLC",
|
||||
)
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.SwapHash, "swap_hash", "", "swap hash of the loop in "+
|
||||
"swap",
|
||||
)
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.LoopDbDir, "loop_db_dir", "", "path to the loop "+
|
||||
"database directory, where the loop.db file is located",
|
||||
)
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.SweepAddr, "sweepaddr", "", "address to recover the funds "+
|
||||
"to; specify '"+lnd.AddressDeriveFromWallet+"' to "+
|
||||
"derive a new address from the seed automatically",
|
||||
)
|
||||
cc.cmd.Flags().Uint32Var(
|
||||
&cc.FeeRate, "feerate", 0, "fee rate to "+
|
||||
"use for the sweep transaction in sat/vByte",
|
||||
)
|
||||
cc.cmd.Flags().IntVar(
|
||||
&cc.NumTries, "num_tries", 1000, "number of tries to "+
|
||||
"try to find the correct key index",
|
||||
)
|
||||
cc.cmd.Flags().IntVar(
|
||||
&cc.StartKeyIndex, "start_key_index", 0, "start key index "+
|
||||
"to try to find the correct key index",
|
||||
)
|
||||
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().Uint64Var(
|
||||
&cc.OutputAmt, "output_amt", 0, "amount of the output to sweep",
|
||||
)
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.SqliteFile, "sqlite_file", "", "optional path to the loop "+
|
||||
"sqlite database file, if not specified, the default "+
|
||||
"location will be loaded from --loop_db_dir",
|
||||
)
|
||||
|
||||
cc.rootKey = newRootKey(cc.cmd, "deriving starting key")
|
||||
|
||||
return cc.cmd
|
||||
}
|
||||
|
||||
func (c *recoverLoopInCommand) Execute(_ *cobra.Command, _ []string) error {
|
||||
extendedKey, err := c.rootKey.read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading root key: %w", err)
|
||||
}
|
||||
|
||||
if c.TxID == "" {
|
||||
return errors.New("txid is required")
|
||||
}
|
||||
|
||||
if c.SwapHash == "" {
|
||||
return errors.New("swap_hash is required")
|
||||
}
|
||||
|
||||
if c.LoopDbDir == "" {
|
||||
return errors.New("loop_db_dir is required")
|
||||
}
|
||||
|
||||
err = lnd.CheckAddress(
|
||||
c.SweepAddr, chainParams, true, "sweep", lnd.AddrTypeP2WKH,
|
||||
lnd.AddrTypeP2TR,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
api := newExplorerAPI(c.APIURL)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||
defer cancel()
|
||||
|
||||
signer := &lnd.Signer{
|
||||
ExtendedKey: extendedKey,
|
||||
ChainParams: chainParams,
|
||||
}
|
||||
|
||||
// Try to fetch the swap from the boltdb.
|
||||
var (
|
||||
store loopdb.SwapStore
|
||||
loopIn *loopdb.LoopIn
|
||||
)
|
||||
|
||||
// First check if a boltdb file exists.
|
||||
if lnrpc.FileExists(filepath.Join(c.LoopDbDir, "loop.db")) {
|
||||
store, err = loopdb.NewBoltSwapStore(c.LoopDbDir, chainParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
loopIn, err = findLoopInSwap(ctx, store, c.SwapHash)
|
||||
if err != nil && !errors.Is(err, errSwapNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// If the loopin is not found yet, try to fetch it from the sqlite db.
|
||||
if loopIn == nil {
|
||||
if c.SqliteFile == "" {
|
||||
c.SqliteFile = filepath.Join(
|
||||
c.LoopDbDir, "loop_sqlite.db",
|
||||
)
|
||||
}
|
||||
|
||||
sqliteDb, err := loopdb.NewSqliteStore(
|
||||
&loopdb.SqliteConfig{
|
||||
DatabaseFileName: c.SqliteFile,
|
||||
SkipMigrations: true,
|
||||
}, chainParams,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sqliteDb.Close()
|
||||
|
||||
loopIn, err = findLoopInSwap(ctx, sqliteDb, c.SwapHash)
|
||||
if err != nil && !errors.Is(err, errSwapNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// If the loopin is still not found, return an error.
|
||||
if loopIn == nil {
|
||||
return errSwapNotFound
|
||||
}
|
||||
|
||||
// If the swap is an external htlc, we require the output amount to be
|
||||
// set, as a lot of failure cases steam from the output amount being
|
||||
// wrong.
|
||||
if loopIn.Contract.ExternalHtlc && c.OutputAmt == 0 {
|
||||
return errors.New("output_amt is required for external htlc")
|
||||
}
|
||||
|
||||
fmt.Println("Loop expires at block height", loopIn.Contract.CltvExpiry)
|
||||
|
||||
outputValue := loopIn.Contract.AmountRequested
|
||||
if c.OutputAmt != 0 {
|
||||
outputValue = btcutil.Amount(c.OutputAmt)
|
||||
}
|
||||
|
||||
// Get the swaps htlc.
|
||||
htlc, err := utils.GetHtlc(
|
||||
loopIn.Hash, &loopIn.Contract.SwapContract, chainParams,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the destination address.
|
||||
var estimator input.TxWeightEstimator
|
||||
sweepScript, err := lnd.PrepareWalletAddress(
|
||||
c.SweepAddr, chainParams, &estimator, extendedKey, "sweep",
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Calculate the sweep fee.
|
||||
err = htlc.AddTimeoutToEstimator(&estimator)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
feeRateKWeight := chainfee.SatPerKVByte(
|
||||
1000 * c.FeeRate,
|
||||
).FeePerKWeight()
|
||||
fee := feeRateKWeight.FeeForWeight(estimator.Weight())
|
||||
|
||||
txID, err := chainhash.NewHashFromStr(c.TxID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the htlc outpoint.
|
||||
htlcOutpoint := wire.OutPoint{
|
||||
Hash: *txID,
|
||||
Index: c.Vout,
|
||||
}
|
||||
|
||||
// Compose tx.
|
||||
sweepTx := wire.NewMsgTx(2)
|
||||
|
||||
sweepTx.LockTime = uint32(loopIn.Contract.CltvExpiry)
|
||||
|
||||
// Add HTLC input.
|
||||
sweepTx.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: htlcOutpoint,
|
||||
Sequence: 0,
|
||||
})
|
||||
|
||||
// Add output for the destination address.
|
||||
sweepTx.AddTxOut(&wire.TxOut{
|
||||
PkScript: sweepScript,
|
||||
Value: int64(outputValue) - int64(fee),
|
||||
})
|
||||
|
||||
// If the htlc is version 2, we need to brute force the key locator, as
|
||||
// it is not stored in the database.
|
||||
var rawTx []byte
|
||||
if htlc.Version == swap.HtlcV2 {
|
||||
fmt.Println("Brute forcing key index...")
|
||||
for i := c.StartKeyIndex; i < c.StartKeyIndex+c.NumTries; i++ {
|
||||
rawTx, err = getSignedTx(
|
||||
signer, sweepTx, htlc,
|
||||
keychain.KeyFamily(swap.KeyFamily), uint32(i),
|
||||
outputValue,
|
||||
)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if rawTx == nil {
|
||||
return errors.New("failed to brute force key index, " +
|
||||
"please try again with a higher start key " +
|
||||
"index")
|
||||
}
|
||||
} else {
|
||||
rawTx, err = getSignedTx(
|
||||
signer, sweepTx, htlc,
|
||||
loopIn.Contract.HtlcKeys.ClientScriptKeyLocator.Family,
|
||||
loopIn.Contract.HtlcKeys.ClientScriptKeyLocator.Index,
|
||||
outputValue,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Publish TX.
|
||||
if c.Publish {
|
||||
response, err := api.PublishTx(
|
||||
hex.EncodeToString(rawTx),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("Published TX %s, response: %s",
|
||||
sweepTx.TxHash().String(), response)
|
||||
} else {
|
||||
fmt.Printf("Success, we successfully created the sweep "+
|
||||
"transaction. Please publish this using any bitcoin "+
|
||||
"node:\n\n%x\n\n", rawTx)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSignedTx(signer *lnd.Signer, sweepTx *wire.MsgTx, htlc *swap.Htlc,
|
||||
keyFamily keychain.KeyFamily, keyIndex uint32,
|
||||
outputValue btcutil.Amount) ([]byte, error) {
|
||||
|
||||
// Create the sign descriptor.
|
||||
prevTxOut := &wire.TxOut{
|
||||
PkScript: htlc.PkScript,
|
||||
Value: int64(outputValue),
|
||||
}
|
||||
prevOutputFetcher := txscript.NewCannedPrevOutputFetcher(
|
||||
prevTxOut.PkScript, prevTxOut.Value,
|
||||
)
|
||||
|
||||
signDesc := &input.SignDescriptor{
|
||||
KeyDesc: keychain.KeyDescriptor{
|
||||
KeyLocator: keychain.KeyLocator{
|
||||
Family: keyFamily,
|
||||
Index: keyIndex,
|
||||
},
|
||||
},
|
||||
WitnessScript: htlc.TimeoutScript(),
|
||||
HashType: htlc.SigHash(),
|
||||
InputIndex: 0,
|
||||
PrevOutputFetcher: prevOutputFetcher,
|
||||
Output: prevTxOut,
|
||||
}
|
||||
switch htlc.Version {
|
||||
case swap.HtlcV2:
|
||||
signDesc.SignMethod = input.WitnessV0SignMethod
|
||||
|
||||
case swap.HtlcV3:
|
||||
signDesc.SignMethod = input.TaprootScriptSpendSignMethod
|
||||
}
|
||||
|
||||
sig, err := signer.SignOutputRaw(sweepTx, signDesc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
witness, err := htlc.GenTimeoutWitness(sig.Serialize())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sweepTx.TxIn[0].Witness = witness
|
||||
|
||||
rawTx, err := encodeTx(sweepTx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sigHashes := txscript.NewTxSigHashes(sweepTx, prevOutputFetcher)
|
||||
|
||||
// Verify the signature. This will throw an error if the signature is
|
||||
// invalid and allows us to bruteforce the key index.
|
||||
vm, err := txscript.NewEngine(
|
||||
prevTxOut.PkScript, sweepTx, 0, txscript.StandardVerifyFlags,
|
||||
nil, sigHashes, prevTxOut.Value, prevOutputFetcher,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = vm.Execute()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rawTx, nil
|
||||
}
|
||||
|
||||
func findLoopInSwap(ctx context.Context, store loopdb.SwapStore,
|
||||
swapHash string) (*loopdb.LoopIn, error) {
|
||||
|
||||
swaps, err := store.FetchLoopInSwaps(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, s := range swaps {
|
||||
if s.Hash.String() == swapHash {
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errSwapNotFound
|
||||
}
|
||||
|
||||
// encodeTx encodes a tx to raw bytes.
|
||||
func encodeTx(tx *wire.MsgTx) ([]byte, error) {
|
||||
var buffer bytes.Buffer
|
||||
err := tx.BtcEncode(&buffer, 0, wire.WitnessEncoding)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawTx := buffer.Bytes()
|
||||
|
||||
return rawTx, nil
|
||||
}
|
@ -1,229 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/lightninglabs/chantools/lnd"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrAddrNotFound = errors.New("address not found")
|
||||
)
|
||||
|
||||
type rescueTweakedKeyCommand struct {
|
||||
Path string
|
||||
TargetAddr string
|
||||
NumTries uint64
|
||||
|
||||
rootKey *rootKey
|
||||
cmd *cobra.Command
|
||||
}
|
||||
|
||||
func newRescueTweakedKeyCommand() *cobra.Command {
|
||||
cc := &rescueTweakedKeyCommand{}
|
||||
cc.cmd = &cobra.Command{
|
||||
Use: "rescuetweakedkey",
|
||||
Short: "Attempt to rescue funds locked in an address with a " +
|
||||
"key that was affected by a specific bug in lnd",
|
||||
Long: `There very likely is no reason to run this command
|
||||
unless you exactly know why or were told by the author of this tool to use it.
|
||||
`,
|
||||
Example: `chantools rescuetweakedkey \
|
||||
--path "m/1017'/0'/5'/0/0'" \
|
||||
--targetaddr bc1pxxxxxxx`,
|
||||
RunE: cc.Execute,
|
||||
}
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.Path, "path", "", "BIP32 derivation path to derive the "+
|
||||
"starting key from; must start with \"m/\"",
|
||||
)
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.TargetAddr, "targetaddr", "", "address the funds are "+
|
||||
"locked in",
|
||||
)
|
||||
cc.cmd.Flags().Uint64Var(
|
||||
&cc.NumTries, "numtries", 10_000_000, "the number of "+
|
||||
"mutations to try",
|
||||
)
|
||||
|
||||
cc.rootKey = newRootKey(cc.cmd, "deriving starting key")
|
||||
|
||||
return cc.cmd
|
||||
}
|
||||
|
||||
func (c *rescueTweakedKeyCommand) Execute(_ *cobra.Command, _ []string) error {
|
||||
extendedKey, err := c.rootKey.read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading root key: %w", err)
|
||||
}
|
||||
|
||||
if c.Path == "" {
|
||||
return errors.New("path is required")
|
||||
}
|
||||
|
||||
childKey, _, _, err := lnd.DeriveKey(extendedKey, c.Path, chainParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not derive key: %w", err)
|
||||
}
|
||||
|
||||
startKey, err := childKey.ECPrivKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deriving private key: %w", err)
|
||||
}
|
||||
|
||||
targetAddr, err := lnd.ParseAddress(c.TargetAddr, chainParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing target addr: %w", err)
|
||||
}
|
||||
|
||||
return testPattern(startKey, targetAddr, c.NumTries)
|
||||
}
|
||||
|
||||
func testPattern(startKey *btcec.PrivateKey, targetAddr btcutil.Address,
|
||||
max uint64) error {
|
||||
|
||||
currentKey := copyPrivKey(startKey)
|
||||
for idx := uint64(0); idx <= max; idx++ {
|
||||
match, err := pubKeyMatchesAddr(currentKey.PubKey(), targetAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error matching key to address: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
if match {
|
||||
log.Infof("Success! Found private key %x for "+
|
||||
"address %v\n", currentKey.Serialize(),
|
||||
targetAddr)
|
||||
return nil
|
||||
}
|
||||
|
||||
mutateWithTweak(currentKey)
|
||||
|
||||
match, err = pubKeyMatchesAddr(currentKey.PubKey(), targetAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error matching key to address: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
if match {
|
||||
log.Infof("Success! Found private key %x for "+
|
||||
"address %v\n", currentKey.Serialize(),
|
||||
targetAddr)
|
||||
return nil
|
||||
}
|
||||
|
||||
keyCopy := copyPrivKey(currentKey)
|
||||
mutateWithSign(keyCopy)
|
||||
|
||||
match, err = pubKeyMatchesAddr(keyCopy.PubKey(), targetAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error matching key to address: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
if match {
|
||||
log.Infof("Success! Found private key %x for "+
|
||||
"address %v\n", keyCopy.Serialize(),
|
||||
targetAddr)
|
||||
return nil
|
||||
}
|
||||
|
||||
if idx != 0 && idx%5000 == 0 {
|
||||
fmt.Printf("Tested %d of %d mutations\n", idx, max)
|
||||
}
|
||||
}
|
||||
|
||||
match, err := pubKeyMatchesAddr(currentKey.PubKey(), targetAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error matching key to address: %w", err)
|
||||
}
|
||||
|
||||
if match {
|
||||
log.Infof("Success! Found private key %x for address %v\n",
|
||||
currentKey.Serialize(), targetAddr)
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("%w: key for address %v not found after %d attempts",
|
||||
ErrAddrNotFound, targetAddr.String(), max)
|
||||
}
|
||||
|
||||
func pubKeyMatchesAddr(pubKey *btcec.PublicKey, addr btcutil.Address) (bool,
|
||||
error) {
|
||||
|
||||
switch typedAddr := addr.(type) {
|
||||
case *btcutil.AddressWitnessPubKeyHash:
|
||||
hash160 := btcutil.Hash160(pubKey.SerializeCompressed())
|
||||
|
||||
return bytes.Equal(hash160, typedAddr.WitnessProgram()), nil
|
||||
|
||||
case *btcutil.AddressTaproot:
|
||||
taprootKey := txscript.ComputeTaprootKeyNoScript(pubKey)
|
||||
|
||||
return bytes.Equal(
|
||||
schnorr.SerializePubKey(taprootKey),
|
||||
typedAddr.WitnessProgram(),
|
||||
), nil
|
||||
|
||||
default:
|
||||
return false, fmt.Errorf("unsupported address type <%T>",
|
||||
typedAddr)
|
||||
}
|
||||
}
|
||||
|
||||
func copyPrivKey(privKey *btcec.PrivateKey) *btcec.PrivateKey {
|
||||
privKeyCopy := *privKey
|
||||
return &btcec.PrivateKey{
|
||||
Key: privKeyCopy.Key,
|
||||
}
|
||||
}
|
||||
|
||||
func mutateWithSign(privKey *btcec.PrivateKey) {
|
||||
privKeyScalar := &privKey.Key
|
||||
pub := privKey.PubKey()
|
||||
|
||||
// Step 5.
|
||||
//
|
||||
// Negate d if P.y is odd.
|
||||
pubKeyBytes := pub.SerializeCompressed()
|
||||
if pubKeyBytes[0] == secp256k1.PubKeyFormatCompressedOdd {
|
||||
privKeyScalar.Negate()
|
||||
}
|
||||
}
|
||||
|
||||
func mutateWithTweak(privKey *btcec.PrivateKey) {
|
||||
// If the corresponding public key has an odd y coordinate, then we'll
|
||||
// negate the private key as specified in BIP 341.
|
||||
privKeyScalar := &privKey.Key
|
||||
pubKeyBytes := privKey.PubKey().SerializeCompressed()
|
||||
if pubKeyBytes[0] == secp256k1.PubKeyFormatCompressedOdd {
|
||||
privKeyScalar.Negate()
|
||||
}
|
||||
|
||||
// Next, we'll compute the tap tweak hash that commits to the internal
|
||||
// key and the merkle script root. We'll snip off the extra parity byte
|
||||
// from the compressed serialization and use that directly.
|
||||
schnorrKeyBytes := pubKeyBytes[1:]
|
||||
tapTweakHash := chainhash.TaggedHash(
|
||||
chainhash.TagTapTweak, schnorrKeyBytes, []byte{},
|
||||
)
|
||||
|
||||
// Map the private key to a ModNScalar which is needed to perform
|
||||
// operation mod the curve order.
|
||||
var tweakScalar btcec.ModNScalar
|
||||
tweakScalar.SetBytes((*[32]byte)(tapTweakHash))
|
||||
|
||||
// Now that we have the private key in its may negated form, we'll add
|
||||
// the script root as a tweak. As we're using a ModNScalar all
|
||||
// operations are already normalized mod the curve order.
|
||||
_ = privKeyScalar.Add(&tweakScalar)
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
privKeyBytes, _ = hex.DecodeString(
|
||||
"571e2fc5e99f91596f7561da9f605cbf2e2342a166593eef041862b6a8b7" +
|
||||
"4f35",
|
||||
)
|
||||
pubKeyOrigBytes, _ = hex.DecodeString(
|
||||
"032ec305fb12642fd3b1091d1cba88ebb7b1a8dbc256b35789b7e223a1b3" +
|
||||
"75f0b7",
|
||||
)
|
||||
pubKeyNegBytes, _ = hex.DecodeString(
|
||||
"022ec305fb12642fd3b1091d1cba88ebb7b1a8dbc256b35789b7e223a1b3" +
|
||||
"75f0b7",
|
||||
)
|
||||
pubKeyNegTweakBytes, _ = hex.DecodeString(
|
||||
"0322b5c94ec4dc3a8843edc7448a0aad389d43e0f8d1b35b546dd1aad70f" +
|
||||
"b2c45b",
|
||||
)
|
||||
pubKeyNegTweakTweakBytes, _ = hex.DecodeString(
|
||||
"03f4cd1ff9efa8198e33e5a110dc690c1472d56c01287893c2f8ed55f61e" +
|
||||
"a767d1",
|
||||
)
|
||||
)
|
||||
|
||||
func TestTweak(t *testing.T) {
|
||||
privKey, pubKey := btcec.PrivKeyFromBytes(privKeyBytes)
|
||||
require.Equal(t, pubKeyOrigBytes, pubKey.SerializeCompressed())
|
||||
|
||||
privKeyCopy := copyPrivKey(privKey)
|
||||
require.Equal(t, privKey, privKeyCopy)
|
||||
|
||||
mutateWithSign(privKeyCopy)
|
||||
require.NotEqual(t, privKey, privKeyCopy)
|
||||
require.Equalf(
|
||||
t, pubKeyNegBytes, privKeyCopy.PubKey().SerializeCompressed(),
|
||||
"%x", privKeyCopy.PubKey().SerializeCompressed(),
|
||||
)
|
||||
|
||||
mutateWithTweak(privKeyCopy)
|
||||
require.NotEqual(t, privKey, privKeyCopy)
|
||||
require.Equalf(
|
||||
t, pubKeyNegTweakBytes,
|
||||
privKeyCopy.PubKey().SerializeCompressed(),
|
||||
"%x", privKeyCopy.PubKey().SerializeCompressed(),
|
||||
)
|
||||
|
||||
mutateWithTweak(privKeyCopy)
|
||||
require.NotEqual(t, privKey, privKeyCopy)
|
||||
require.Equalf(
|
||||
t, pubKeyNegTweakTweakBytes,
|
||||
privKeyCopy.PubKey().SerializeCompressed(),
|
||||
"%x", privKeyCopy.PubKey().SerializeCompressed(),
|
||||
)
|
||||
}
|
@ -1,91 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
chantools_lnd "github.com/lightninglabs/chantools/lnd"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/tv42/zbase32"
|
||||
)
|
||||
|
||||
var (
|
||||
signedMsgPrefix = []byte("Lightning Signed Message:")
|
||||
)
|
||||
|
||||
type signMessageCommand struct {
|
||||
Msg string
|
||||
|
||||
rootKey *rootKey
|
||||
cmd *cobra.Command
|
||||
}
|
||||
|
||||
func newSignMessageCommand() *cobra.Command {
|
||||
cc := &signMessageCommand{}
|
||||
cc.cmd = &cobra.Command{
|
||||
Use: "signmessage",
|
||||
Short: "Sign a message with the node's private key.",
|
||||
Long: `Sign msg with the resident node's private key.
|
||||
Returns the signature as a zbase32 string.`,
|
||||
Example: `chantools signmessage --msg=foobar`,
|
||||
RunE: cc.Execute,
|
||||
}
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.Msg, "msg", "", "the message to sign",
|
||||
)
|
||||
|
||||
cc.rootKey = newRootKey(cc.cmd, "decrypting the backup")
|
||||
|
||||
return cc.cmd
|
||||
}
|
||||
|
||||
func (c *signMessageCommand) Execute(_ *cobra.Command, _ []string) error {
|
||||
if c.Msg == "" {
|
||||
return errors.New("please enter a valid msg")
|
||||
}
|
||||
|
||||
extendedKey, err := c.rootKey.read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading root key: %w", err)
|
||||
}
|
||||
|
||||
signer := &chantools_lnd.Signer{
|
||||
ExtendedKey: extendedKey,
|
||||
ChainParams: chainParams,
|
||||
}
|
||||
|
||||
// Create the key locator for the node key.
|
||||
keyLocator := keychain.KeyLocator{
|
||||
Family: keychain.KeyFamilyNodeKey,
|
||||
Index: 0,
|
||||
}
|
||||
|
||||
// Fetch the private key for node key.
|
||||
privKey, err := signer.FetchPrivateKey(&keychain.KeyDescriptor{
|
||||
KeyLocator: keyLocator,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a new signer.
|
||||
privKeyMsgSigner := keychain.NewPrivKeyMessageSigner(
|
||||
privKey, keyLocator,
|
||||
)
|
||||
|
||||
// Prepend the special lnd prefix.
|
||||
// See: https://github.com/lightningnetwork/lnd/blob/63e698ec4990e678089533561fd95cfd684b67db/rpcserver.go#L1576 .
|
||||
msg := []byte(c.Msg)
|
||||
msg = append(signedMsgPrefix, msg...)
|
||||
sigBytes, err := privKeyMsgSigner.SignMessageCompact(msg, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Encode the signature.
|
||||
sig := zbase32.EncodeToString(sigBytes)
|
||||
fmt.Println(sig)
|
||||
|
||||
return nil
|
||||
}
|
@ -1,255 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/lightninglabs/chantools/lnd"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
errNoPathFound = errors.New("no matching derivation path found")
|
||||
)
|
||||
|
||||
type signPSBTCommand struct {
|
||||
Psbt string
|
||||
FromRawPsbtFile string
|
||||
ToRawPsbtFile string
|
||||
|
||||
rootKey *rootKey
|
||||
cmd *cobra.Command
|
||||
}
|
||||
|
||||
func newSignPSBTCommand() *cobra.Command {
|
||||
cc := &signPSBTCommand{}
|
||||
cc.cmd = &cobra.Command{
|
||||
Use: "signpsbt",
|
||||
Short: "Sign a Partially Signed Bitcoin Transaction (PSBT)",
|
||||
Long: `Sign a PSBT with a master root key. The PSBT must contain
|
||||
an input that is owned by the master root key.`,
|
||||
Example: `chantools signpsbt \
|
||||
--psbt <the_base64_encoded_psbt>
|
||||
|
||||
chantools signpsbt --fromrawpsbtfile <file_with_psbt>`,
|
||||
RunE: cc.Execute,
|
||||
}
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.Psbt, "psbt", "", "Partially Signed Bitcoin Transaction "+
|
||||
"to sign",
|
||||
)
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.FromRawPsbtFile, "fromrawpsbtfile", "", "the file containing "+
|
||||
"the raw, binary encoded PSBT packet to sign",
|
||||
)
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.ToRawPsbtFile, "torawpsbtfile", "", "the file to write "+
|
||||
"the resulting signed raw, binary encoded PSBT packet "+
|
||||
"to",
|
||||
)
|
||||
|
||||
cc.rootKey = newRootKey(cc.cmd, "signing the PSBT")
|
||||
|
||||
return cc.cmd
|
||||
}
|
||||
|
||||
func (c *signPSBTCommand) Execute(_ *cobra.Command, _ []string) error {
|
||||
extendedKey, err := c.rootKey.read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading root key: %w", err)
|
||||
}
|
||||
|
||||
signer := &lnd.Signer{
|
||||
ExtendedKey: extendedKey,
|
||||
ChainParams: chainParams,
|
||||
}
|
||||
|
||||
var packet *psbt.Packet
|
||||
|
||||
// Decode the PSBT, either from the command line or the binary file.
|
||||
switch {
|
||||
case c.Psbt != "":
|
||||
packet, err = psbt.NewFromRawBytes(
|
||||
bytes.NewReader([]byte(c.Psbt)), true,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding PSBT: %w", err)
|
||||
}
|
||||
|
||||
case c.FromRawPsbtFile != "":
|
||||
f, err := os.Open(c.FromRawPsbtFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error opening PSBT file '%s': %w",
|
||||
c.FromRawPsbtFile, err)
|
||||
}
|
||||
|
||||
packet, err = psbt.NewFromRawBytes(f, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding PSBT from file "+
|
||||
"'%s': %w", c.FromRawPsbtFile, err)
|
||||
}
|
||||
|
||||
default:
|
||||
return errors.New("either the PSBT or the raw PSBT file " +
|
||||
"must be set")
|
||||
}
|
||||
|
||||
err = signPsbt(extendedKey, packet, signer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error signing PSBT: %w", err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case c.ToRawPsbtFile != "":
|
||||
f, err := os.Create(c.ToRawPsbtFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating PSBT file '%s': %w",
|
||||
c.ToRawPsbtFile, err)
|
||||
}
|
||||
|
||||
if err := packet.Serialize(f); err != nil {
|
||||
return fmt.Errorf("error serializing PSBT to file "+
|
||||
"'%s': %w", c.ToRawPsbtFile, err)
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully signed PSBT and wrote it to file "+
|
||||
"'%s'\n", c.ToRawPsbtFile)
|
||||
|
||||
default:
|
||||
var buf bytes.Buffer
|
||||
if err := packet.Serialize(&buf); err != nil {
|
||||
return fmt.Errorf("error serializing PSBT: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully signed PSBT:\n\n%s\n",
|
||||
base64.StdEncoding.EncodeToString(buf.Bytes()))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func signPsbt(rootKey *hdkeychain.ExtendedKey,
|
||||
packet *psbt.Packet, signer *lnd.Signer) error {
|
||||
|
||||
for inputIndex := range packet.Inputs {
|
||||
pIn := &packet.Inputs[inputIndex]
|
||||
|
||||
// Check that we have an input with a derivation path that
|
||||
// belongs to the root key.
|
||||
derivationPath, err := findMatchingDerivationPath(rootKey, pIn)
|
||||
if errors.Is(err, errNoPathFound) {
|
||||
log.Infof("No matching derivation path found for "+
|
||||
"input %d, skipping", inputIndex)
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not find matching derivation "+
|
||||
"path: %w", err)
|
||||
}
|
||||
|
||||
if len(derivationPath) < 5 {
|
||||
return fmt.Errorf("invalid derivation path, expected "+
|
||||
"at least 5 elements, got %d",
|
||||
len(derivationPath))
|
||||
}
|
||||
|
||||
localKey, err := lnd.DeriveChildren(rootKey, derivationPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not derive local key: %w", err)
|
||||
}
|
||||
|
||||
if pIn.WitnessUtxo == nil {
|
||||
return fmt.Errorf("invalid PSBT, input %d is missing "+
|
||||
"witness UTXO", inputIndex)
|
||||
}
|
||||
utxo := pIn.WitnessUtxo
|
||||
|
||||
// The signing is a bit different for P2WPKH, we need to specify
|
||||
// the pk script as the witness script.
|
||||
var witnessScript []byte
|
||||
if txscript.IsPayToWitnessPubKeyHash(utxo.PkScript) {
|
||||
witnessScript = utxo.PkScript
|
||||
} else {
|
||||
if len(pIn.WitnessScript) == 0 {
|
||||
return fmt.Errorf("invalid PSBT, input %d is "+
|
||||
"missing witness script", inputIndex)
|
||||
}
|
||||
witnessScript = pIn.WitnessScript
|
||||
}
|
||||
|
||||
localPrivateKey, err := localKey.ECPrivKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting private key: %w", err)
|
||||
}
|
||||
|
||||
// Do we already have a partial signature for our key?
|
||||
localPubKey := localPrivateKey.PubKey().SerializeCompressed()
|
||||
haveSig := false
|
||||
for _, partialSig := range pIn.PartialSigs {
|
||||
if bytes.Equal(partialSig.PubKey, localPubKey) {
|
||||
haveSig = true
|
||||
}
|
||||
}
|
||||
if haveSig {
|
||||
log.Infof("Already have a partial signature for input "+
|
||||
"%d and local key %x, skipping", inputIndex,
|
||||
localPubKey)
|
||||
continue
|
||||
}
|
||||
|
||||
err = signer.AddPartialSignatureForPrivateKey(
|
||||
packet, localPrivateKey, utxo, witnessScript,
|
||||
inputIndex,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding partial signature: %w",
|
||||
err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findMatchingDerivationPath(rootKey *hdkeychain.ExtendedKey,
|
||||
pIn *psbt.PInput) ([]uint32, error) {
|
||||
|
||||
pubKey, err := rootKey.ECPubKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting public key: %w", err)
|
||||
}
|
||||
|
||||
pubKeyHash := btcutil.Hash160(pubKey.SerializeCompressed())
|
||||
fingerprint := binary.LittleEndian.Uint32(pubKeyHash[:4])
|
||||
|
||||
if len(pIn.Bip32Derivation) == 0 {
|
||||
return nil, errNoPathFound
|
||||
}
|
||||
|
||||
for _, derivation := range pIn.Bip32Derivation {
|
||||
// A special case where there is only a single derivation path
|
||||
// and the master key fingerprint is not set, we assume we are
|
||||
// the correct signer... This might not be correct, but we have
|
||||
// no way of knowing.
|
||||
if derivation.MasterKeyFingerprint == 0 &&
|
||||
len(pIn.Bip32Derivation) == 1 {
|
||||
|
||||
return derivation.Bip32Path, nil
|
||||
}
|
||||
|
||||
// The normal case, where a derivation path has the master
|
||||
// fingerprint set.
|
||||
if derivation.MasterKeyFingerprint == fingerprint {
|
||||
return derivation.Bip32Path, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errNoPathFound
|
||||
}
|
@ -1,441 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"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/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
sweepRemoteClosedDefaultRecoveryWindow = 200
|
||||
sweepDustLimit = 600
|
||||
)
|
||||
|
||||
type sweepRemoteClosedCommand struct {
|
||||
RecoveryWindow uint32
|
||||
APIURL string
|
||||
Publish bool
|
||||
SweepAddr string
|
||||
FeeRate uint32
|
||||
|
||||
rootKey *rootKey
|
||||
cmd *cobra.Command
|
||||
}
|
||||
|
||||
func newSweepRemoteClosedCommand() *cobra.Command {
|
||||
cc := &sweepRemoteClosedCommand{}
|
||||
cc.cmd = &cobra.Command{
|
||||
Use: "sweepremoteclosed",
|
||||
Short: "Go through all the addresses that could have funds of " +
|
||||
"channels that were force-closed by the remote party. " +
|
||||
"A public block explorer is queried for each address " +
|
||||
"and if any balance is found, all funds are swept to " +
|
||||
"a given address",
|
||||
Long: `This command helps users sweep funds that are in
|
||||
outputs of channels that were force-closed by the remote party. This command
|
||||
only needs to be used if no channel.backup file is available. By manually
|
||||
contacting the remote peers and asking them to force-close the channels, the
|
||||
funds can be swept after the force-close transaction was confirmed.
|
||||
|
||||
Supported remote force-closed channel types are:
|
||||
- STATIC_REMOTE_KEY (a.k.a. tweakless channels)
|
||||
- ANCHOR (a.k.a. anchor output channels)
|
||||
- SIMPLE_TAPROOT (a.k.a. simple taproot channels)
|
||||
`,
|
||||
Example: `chantools sweepremoteclosed \
|
||||
--recoverywindow 300 \
|
||||
--feerate 20 \
|
||||
--sweepaddr bc1q..... \
|
||||
--publish`,
|
||||
RunE: cc.Execute,
|
||||
}
|
||||
cc.cmd.Flags().Uint32Var(
|
||||
&cc.RecoveryWindow, "recoverywindow",
|
||||
sweepRemoteClosedDefaultRecoveryWindow, "number of keys to "+
|
||||
"scan per derivation path",
|
||||
)
|
||||
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 recover the funds "+
|
||||
"to; specify '"+lnd.AddressDeriveFromWallet+"' to "+
|
||||
"derive a new address from the seed automatically",
|
||||
)
|
||||
cc.cmd.Flags().Uint32Var(
|
||||
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
|
||||
"use for the sweep transaction in sat/vByte",
|
||||
)
|
||||
|
||||
cc.rootKey = newRootKey(cc.cmd, "sweeping the wallet")
|
||||
|
||||
return cc.cmd
|
||||
}
|
||||
|
||||
func (c *sweepRemoteClosedCommand) Execute(_ *cobra.Command, _ []string) error {
|
||||
extendedKey, err := c.rootKey.read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading root key: %w", err)
|
||||
}
|
||||
|
||||
// Make sure sweep addr is set.
|
||||
err = lnd.CheckAddress(
|
||||
c.SweepAddr, chainParams, true, "sweep", lnd.AddrTypeP2WKH,
|
||||
lnd.AddrTypeP2TR,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set default values.
|
||||
if c.RecoveryWindow == 0 {
|
||||
c.RecoveryWindow = sweepRemoteClosedDefaultRecoveryWindow
|
||||
}
|
||||
if c.FeeRate == 0 {
|
||||
c.FeeRate = defaultFeeSatPerVByte
|
||||
}
|
||||
|
||||
return sweepRemoteClosed(
|
||||
extendedKey, c.APIURL, c.SweepAddr, c.RecoveryWindow, c.FeeRate,
|
||||
c.Publish,
|
||||
)
|
||||
}
|
||||
|
||||
type targetAddr struct {
|
||||
addr btcutil.Address
|
||||
pubKey *btcec.PublicKey
|
||||
path string
|
||||
keyDesc *keychain.KeyDescriptor
|
||||
vouts []*btc.Vout
|
||||
script []byte
|
||||
scriptTree *input.CommitScriptTree
|
||||
}
|
||||
|
||||
func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
|
||||
sweepAddr string, recoveryWindow uint32, feeRate uint32,
|
||||
publish bool) error {
|
||||
|
||||
var estimator input.TxWeightEstimator
|
||||
sweepScript, err := lnd.PrepareWalletAddress(
|
||||
sweepAddr, chainParams, &estimator, extendedKey, "sweep",
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
targets []*targetAddr
|
||||
api = newExplorerAPI(apiURL)
|
||||
)
|
||||
for index := range recoveryWindow {
|
||||
path := fmt.Sprintf("m/1017'/%d'/%d'/0/%d",
|
||||
chainParams.HDCoinType, keychain.KeyFamilyPaymentBase,
|
||||
index)
|
||||
parsedPath, err := lnd.ParsePath(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing path: %w", err)
|
||||
}
|
||||
|
||||
hdKey, err := lnd.DeriveChildren(
|
||||
extendedKey, parsedPath,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("eror deriving children: %w", err)
|
||||
}
|
||||
|
||||
privKey, err := hdKey.ECPrivKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not derive private "+
|
||||
"key: %w", err)
|
||||
}
|
||||
|
||||
foundTargets, err := queryAddressBalances(
|
||||
privKey.PubKey(), path, &keychain.KeyDescriptor{
|
||||
PubKey: privKey.PubKey(),
|
||||
KeyLocator: keychain.KeyLocator{
|
||||
Family: keychain.KeyFamilyPaymentBase,
|
||||
Index: index,
|
||||
},
|
||||
}, api,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not query API for "+
|
||||
"addresses with funds: %w", err)
|
||||
}
|
||||
targets = append(targets, foundTargets...)
|
||||
}
|
||||
|
||||
// Create estimator and transaction template.
|
||||
var (
|
||||
signDescs []*input.SignDescriptor
|
||||
sweepTx = wire.NewMsgTx(2)
|
||||
totalOutputValue = uint64(0)
|
||||
prevOutFetcher = txscript.NewMultiPrevOutFetcher(nil)
|
||||
)
|
||||
|
||||
// Add all found target outputs.
|
||||
for _, target := range targets {
|
||||
for _, vout := range target.vouts {
|
||||
totalOutputValue += vout.Value
|
||||
|
||||
txHash, err := chainhash.NewHashFromStr(
|
||||
vout.Outspend.Txid,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing tx hash: %w",
|
||||
err)
|
||||
}
|
||||
pkScript, err := lnd.GetWitnessAddrScript(
|
||||
target.addr, chainParams,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting pk script: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
prevOutPoint := wire.OutPoint{
|
||||
Hash: *txHash,
|
||||
Index: uint32(vout.Outspend.Vin),
|
||||
}
|
||||
prevTxOut := &wire.TxOut{
|
||||
PkScript: pkScript,
|
||||
Value: int64(vout.Value),
|
||||
}
|
||||
prevOutFetcher.AddPrevOut(prevOutPoint, prevTxOut)
|
||||
txIn := &wire.TxIn{
|
||||
PreviousOutPoint: prevOutPoint,
|
||||
Sequence: wire.MaxTxInSequenceNum,
|
||||
}
|
||||
sweepTx.TxIn = append(sweepTx.TxIn, txIn)
|
||||
inputIndex := len(sweepTx.TxIn) - 1
|
||||
|
||||
var signDesc *input.SignDescriptor
|
||||
switch target.addr.(type) {
|
||||
case *btcutil.AddressWitnessPubKeyHash:
|
||||
estimator.AddP2WKHInput()
|
||||
|
||||
signDesc = &input.SignDescriptor{
|
||||
KeyDesc: *target.keyDesc,
|
||||
WitnessScript: target.script,
|
||||
Output: prevTxOut,
|
||||
HashType: txscript.SigHashAll,
|
||||
PrevOutputFetcher: prevOutFetcher,
|
||||
InputIndex: inputIndex,
|
||||
}
|
||||
|
||||
case *btcutil.AddressWitnessScriptHash:
|
||||
estimator.AddWitnessInput(
|
||||
input.ToRemoteConfirmedWitnessSize,
|
||||
)
|
||||
txIn.Sequence = 1
|
||||
|
||||
signDesc = &input.SignDescriptor{
|
||||
KeyDesc: *target.keyDesc,
|
||||
WitnessScript: target.script,
|
||||
Output: prevTxOut,
|
||||
HashType: txscript.SigHashAll,
|
||||
PrevOutputFetcher: prevOutFetcher,
|
||||
InputIndex: inputIndex,
|
||||
}
|
||||
|
||||
case *btcutil.AddressTaproot:
|
||||
estimator.AddWitnessInput(
|
||||
input.TaprootToRemoteWitnessSize,
|
||||
)
|
||||
txIn.Sequence = 1
|
||||
|
||||
tree := target.scriptTree
|
||||
controlBlock, err := tree.CtrlBlockForPath(
|
||||
input.ScriptPathSuccess,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
controlBlockBytes, err := controlBlock.ToBytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
script := tree.SettleLeaf.Script
|
||||
signMethod := input.TaprootScriptSpendSignMethod
|
||||
signDesc = &input.SignDescriptor{
|
||||
KeyDesc: *target.keyDesc,
|
||||
WitnessScript: script,
|
||||
Output: prevTxOut,
|
||||
HashType: txscript.SigHashDefault,
|
||||
PrevOutputFetcher: prevOutFetcher,
|
||||
ControlBlock: controlBlockBytes,
|
||||
InputIndex: inputIndex,
|
||||
SignMethod: signMethod,
|
||||
TapTweak: tree.TapscriptRoot,
|
||||
}
|
||||
}
|
||||
|
||||
signDescs = append(signDescs, signDesc)
|
||||
}
|
||||
}
|
||||
|
||||
if len(targets) == 0 || totalOutputValue < sweepDustLimit {
|
||||
return fmt.Errorf("found %d sweep targets with total value "+
|
||||
"of %d satoshis which is below the dust limit of %d",
|
||||
len(targets), totalOutputValue, sweepDustLimit)
|
||||
}
|
||||
|
||||
// Calculate the fee based on the given fee rate and our weight
|
||||
// estimation.
|
||||
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()
|
||||
totalFee := feeRateKWeight.FeeForWeight(estimator.Weight())
|
||||
|
||||
log.Infof("Fee %d sats of %d total amount (estimated weight %d)",
|
||||
totalFee, totalOutputValue, estimator.Weight())
|
||||
|
||||
sweepTx.TxOut = []*wire.TxOut{{
|
||||
Value: int64(totalOutputValue) - int64(totalFee),
|
||||
PkScript: sweepScript,
|
||||
}}
|
||||
|
||||
// Sign the transaction now.
|
||||
var (
|
||||
signer = &lnd.Signer{
|
||||
ExtendedKey: extendedKey,
|
||||
ChainParams: chainParams,
|
||||
}
|
||||
sigHashes = txscript.NewTxSigHashes(sweepTx, prevOutFetcher)
|
||||
)
|
||||
for idx, desc := range signDescs {
|
||||
desc.SigHashes = sigHashes
|
||||
desc.InputIndex = idx
|
||||
|
||||
switch {
|
||||
// Simple Taproot Channels.
|
||||
case desc.SignMethod == input.TaprootScriptSpendSignMethod:
|
||||
witness, err := input.TaprootCommitSpendSuccess(
|
||||
signer, desc, sweepTx, nil,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sweepTx.TxIn[idx].Witness = witness
|
||||
|
||||
// Anchor Channels.
|
||||
case len(desc.WitnessScript) > 0:
|
||||
witness, err := input.CommitSpendToRemoteConfirmed(
|
||||
signer, desc, sweepTx,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sweepTx.TxIn[idx].Witness = witness
|
||||
|
||||
// Static Remote Key Channels.
|
||||
default:
|
||||
// The txscript library expects the witness script of a
|
||||
// P2WKH descriptor to be set to the pkScript of the
|
||||
// output...
|
||||
desc.WitnessScript = desc.Output.PkScript
|
||||
witness, err := input.CommitSpendNoDelay(
|
||||
signer, desc, sweepTx, true,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sweepTx.TxIn[idx].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 queryAddressBalances(pubKey *btcec.PublicKey, path string,
|
||||
keyDesc *keychain.KeyDescriptor, api *btc.ExplorerAPI) ([]*targetAddr,
|
||||
error) {
|
||||
|
||||
var targets []*targetAddr
|
||||
queryAddr := func(address btcutil.Address, script []byte,
|
||||
scriptTree *input.CommitScriptTree) error {
|
||||
|
||||
unspent, err := api.Unspent(address.EncodeAddress())
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not query unspent: %w", err)
|
||||
}
|
||||
|
||||
if len(unspent) > 0 {
|
||||
log.Infof("Found %d unspent outputs for address %v",
|
||||
len(unspent), address.EncodeAddress())
|
||||
targets = append(targets, &targetAddr{
|
||||
addr: address,
|
||||
pubKey: pubKey,
|
||||
path: path,
|
||||
keyDesc: keyDesc,
|
||||
vouts: unspent,
|
||||
script: script,
|
||||
scriptTree: scriptTree,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
p2wkh, err := lnd.P2WKHAddr(pubKey, chainParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := queryAddr(p2wkh, nil, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p2anchor, script, err := lnd.P2AnchorStaticRemote(pubKey, chainParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := queryAddr(p2anchor, script, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p2tr, scriptTree, err := lnd.P2TaprootStaticRemote(pubKey, chainParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := queryAddr(p2tr, nil, scriptTree); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return targets, nil
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/lightninglabs/chantools/lnd"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var sweepTimeLockManualCases = []struct {
|
||||
baseKey string
|
||||
keyIndex uint32
|
||||
timeLockAddr string
|
||||
remoteRevPubKey string
|
||||
}{{
|
||||
// New format with ECDH revocation root.
|
||||
baseKey: "tprv8dgoXnQWBN4CGGceRYMW495kWcrUZKZVFwMmbzpduFp1D4pi" +
|
||||
"3B2t37zTG5Fx66XWPDQYi3Q5vqDgmmZ5ffrqZ9H4s2EhJu9WaJjY3SKaWDK",
|
||||
keyIndex: 7,
|
||||
timeLockAddr: "bcrt1qf9zv4qtxh27c954rhlzg4tx58xh0vgssuu0csrlep0jdnv" +
|
||||
"lx9xesmcl5qx",
|
||||
remoteRevPubKey: "03235261ed5aaaf9fec0e91d5e1a4d17f1a2c7442f1c43806d" +
|
||||
"32c9bd34abd002a3",
|
||||
}, {
|
||||
// Old format with plain private key as revocation root.
|
||||
baseKey: "tprv8dgoXnQWBN4CGGceRYMW495kWcrUZKZVFwMmbzpduFp1D4pi" +
|
||||
"3B2t37zTG5Fx66XWPDQYi3Q5vqDgmmZ5ffrqZ9H4s2EhJu9WaJjY3SKaWDK",
|
||||
keyIndex: 6,
|
||||
timeLockAddr: "bcrt1qa5rrlswxefc870k7rsza5hhqd37uytczldjk5t0vzd95u9" +
|
||||
"hs8xlsfdc3zf",
|
||||
remoteRevPubKey: "03e82cdf164ce5aba253890e066129f134ca8d7e072ce5ad55" +
|
||||
"c721b9a13545ee04",
|
||||
}, {
|
||||
// New format with ECDH revocation root.
|
||||
baseKey: "tprv8fCiPGhoYhWESQg3kgubCizcHo21drnP9Fa5j9fFKCmbME" +
|
||||
"ipgodofyXcf4NFhD4k55GM1Ym3JUUDonpEXcsjnyTDUMmkzMK9pCnGPH3NJ5i",
|
||||
keyIndex: 0,
|
||||
timeLockAddr: "bcrt1qmkyn0tqx6mpg5aujgjhzaw27rvvymdfc3xhgawp48zy8v" +
|
||||
"3rlw45qzmjqrr",
|
||||
remoteRevPubKey: "02dfecdc259a7e1cff36a67328ded3b4dae30369a3035e4f91" +
|
||||
"1ce7ac4a80b28e5d",
|
||||
}, {
|
||||
// Old format with plain private key as revocation root. Test data
|
||||
// created with lnd v0.12.0-beta (old shachain root creation)
|
||||
baseKey: "tprv8e3Mee42NcUd2MbwxBCJyEEhvKa8KqjiDR76M7ym4DJSfZk" +
|
||||
"fDyA46XZeA4kTj8YKktWrjGBDThxxcL4HBF89jDKseu24XtugVMNsm3GhHwK",
|
||||
keyIndex: 0,
|
||||
timeLockAddr: "bcrt1qux548e45wlg9sufhgd8ldfzqrapl303g5sj7xg5w637sge" +
|
||||
"dst0wsk0xags",
|
||||
remoteRevPubKey: "03647afa9c04025e997a5b7ecd2dd949f8f60f6880a94af73a" +
|
||||
"0d4f48f166d127d1",
|
||||
}, {
|
||||
// New format with ECDH revocation root but this test data was created
|
||||
// when already the old format was present, this leads to the situation
|
||||
// where the idx for the shachain root (revocation root) is equal to
|
||||
// the delay basepoint index. Normally when starting a node after
|
||||
// lnd with the version v0.13.0-beta onwords, the index is always
|
||||
// +1 compared to the delay basepoint index.
|
||||
baseKey: "tprv8e3Mee42NcUd2MbwxBCJyEEhvKa8KqjiDR76M7ym4DJSfZ" +
|
||||
"kfDyA46XZeA4kTj8YKktWrjGBDThxxcL4HBF89jDKseu24XtugVMNsm3GhHwK",
|
||||
keyIndex: 1,
|
||||
timeLockAddr: "bcrt1qsj7c97fj9xh8znlkjtg4x45xstypk5zp3kcnt5f5u6ps" +
|
||||
"rhetju2srseqrh",
|
||||
remoteRevPubKey: "0341692a025ad552c62689a630ff24d9439e3752d8e0ac5cb4" +
|
||||
"1b5e71ab2bd46d0f",
|
||||
}}
|
||||
|
||||
func TestSweepTimeLockManual(t *testing.T) {
|
||||
for _, tc := range sweepTimeLockManualCases {
|
||||
// First, 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(
|
||||
tc.timeLockAddr, &chaincfg.RegressionNetParams,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
baseKey, err := hdkeychain.NewKeyFromString(tc.baseKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
revPubKeyBytes, _ := hex.DecodeString(tc.remoteRevPubKey)
|
||||
revPubKey, _ := btcec.ParsePubKey(revPubKeyBytes)
|
||||
|
||||
_, _, _, _, _, err = tryKey(
|
||||
baseKey, revPubKey, 0, defaultCsvLimit, lockScript,
|
||||
tc.keyIndex, 500,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1,266 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/connmgr"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/chantools/lnd"
|
||||
"github.com/lightningnetwork/lnd/brontide"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lncfg"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/peer"
|
||||
"github.com/lightningnetwork/lnd/tor"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
dialTimeout = time.Minute
|
||||
|
||||
defaultTorDNSHostPort = "soa.nodes.lightning.directory:53"
|
||||
)
|
||||
|
||||
type triggerForceCloseCommand struct {
|
||||
Peer string
|
||||
ChannelPoint string
|
||||
|
||||
APIURL string
|
||||
|
||||
TorProxy string
|
||||
|
||||
rootKey *rootKey
|
||||
cmd *cobra.Command
|
||||
}
|
||||
|
||||
func newTriggerForceCloseCommand() *cobra.Command {
|
||||
cc := &triggerForceCloseCommand{}
|
||||
cc.cmd = &cobra.Command{
|
||||
Use: "triggerforceclose",
|
||||
Short: "Connect to a Lightning Network peer and send " +
|
||||
"specific messages to trigger a force close of the " +
|
||||
"specified channel",
|
||||
Long: `Asks the specified remote peer to force close a specific
|
||||
channel by first sending a channel re-establish message, and if that doesn't
|
||||
work, a custom error message (in case the peer is a specific version of CLN that
|
||||
does not properly respond to a Data Loss Protection re-establish message).'`,
|
||||
Example: `chantools triggerforceclose \
|
||||
--peer 03abce...@xx.yy.zz.aa:9735 \
|
||||
--channel_point abcdef01234...:x`,
|
||||
RunE: cc.Execute,
|
||||
}
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.Peer, "peer", "", "remote peer address "+
|
||||
"(<pubkey>@<host>[:<port>])",
|
||||
)
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.ChannelPoint, "channel_point", "", "funding transaction "+
|
||||
"outpoint of the channel to trigger the force close "+
|
||||
"of (<txid>:<txindex>)",
|
||||
)
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
|
||||
"be esplora compatible)",
|
||||
)
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.TorProxy, "torproxy", "", "SOCKS5 proxy to use for Tor "+
|
||||
"connections (to .onion addresses)",
|
||||
)
|
||||
cc.rootKey = newRootKey(cc.cmd, "deriving the identity key")
|
||||
|
||||
return cc.cmd
|
||||
}
|
||||
|
||||
func (c *triggerForceCloseCommand) Execute(_ *cobra.Command, _ []string) error {
|
||||
extendedKey, err := c.rootKey.read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading root key: %w", err)
|
||||
}
|
||||
|
||||
identityPath := lnd.IdentityPath(chainParams)
|
||||
child, pubKey, _, err := lnd.DeriveKey(
|
||||
extendedKey, identityPath, chainParams,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not derive identity key: %w", err)
|
||||
}
|
||||
identityPriv, err := child.ECPrivKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get identity private key: %w", err)
|
||||
}
|
||||
identityECDH := &keychain.PrivKeyECDH{
|
||||
PrivKey: identityPriv,
|
||||
}
|
||||
|
||||
outPoint, err := parseOutPoint(c.ChannelPoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing channel point: %w", err)
|
||||
}
|
||||
|
||||
err = requestForceClose(
|
||||
c.Peer, c.TorProxy, pubKey, *outPoint, identityECDH,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error requesting force close: %w", err)
|
||||
}
|
||||
|
||||
log.Infof("Message sent, waiting for force close transaction to " +
|
||||
"appear in mempool")
|
||||
|
||||
api := newExplorerAPI(c.APIURL)
|
||||
channelAddress, err := api.Address(c.ChannelPoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting channel address: %w", err)
|
||||
}
|
||||
|
||||
spends, err := api.Spends(channelAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting spends: %w", err)
|
||||
}
|
||||
for len(spends) == 0 {
|
||||
log.Infof("No spends found yet, waiting 5 seconds...")
|
||||
time.Sleep(5 * time.Second)
|
||||
spends, err = api.Spends(channelAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting spends: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("Found force close transaction %v", spends[0].TXID)
|
||||
log.Infof("You can now use the sweepremoteclosed command to sweep " +
|
||||
"the funds from the channel")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func noiseDial(idKey keychain.SingleKeyECDH, lnAddr *lnwire.NetAddress,
|
||||
netCfg tor.Net, timeout time.Duration) (*brontide.Conn, error) {
|
||||
|
||||
return brontide.Dial(idKey, lnAddr, timeout, netCfg.Dial)
|
||||
}
|
||||
|
||||
func connectPeer(peerHost, torProxy string, peerPubKey *btcec.PublicKey,
|
||||
identity keychain.SingleKeyECDH,
|
||||
dialTimeout time.Duration) (*peer.Brontide, error) {
|
||||
|
||||
var dialNet tor.Net = &tor.ClearNet{}
|
||||
if torProxy != "" {
|
||||
dialNet = &tor.ProxyNet{
|
||||
SOCKS: torProxy,
|
||||
DNS: defaultTorDNSHostPort,
|
||||
StreamIsolation: false,
|
||||
SkipProxyForClearNetTargets: true,
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("Attempting to resolve peer address %v", peerHost)
|
||||
peerAddr, err := lncfg.ParseLNAddressString(
|
||||
peerHost, "9735", dialNet.ResolveTCPAddr,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing peer address: %w", err)
|
||||
}
|
||||
|
||||
log.Debugf("Attempting to dial resolved peer address %v",
|
||||
peerAddr.String())
|
||||
conn, err := noiseDial(identity, peerAddr, dialNet, dialTimeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error dialing peer: %w", err)
|
||||
}
|
||||
|
||||
log.Infof("Attempting to establish p2p connection to peer %x, dial"+
|
||||
"timeout is %v", peerPubKey.SerializeCompressed(), dialTimeout)
|
||||
req := &connmgr.ConnReq{
|
||||
Addr: peerAddr,
|
||||
Permanent: false,
|
||||
}
|
||||
p, err := lnd.ConnectPeer(conn, req, chainParams, identity)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error connecting to peer: %w", err)
|
||||
}
|
||||
|
||||
log.Infof("Connection established to peer %x",
|
||||
peerPubKey.SerializeCompressed())
|
||||
|
||||
// We'll wait until the peer is active.
|
||||
select {
|
||||
case <-p.ActiveSignal():
|
||||
case <-p.QuitSignal():
|
||||
return nil, fmt.Errorf("peer %x disconnected",
|
||||
peerPubKey.SerializeCompressed())
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func requestForceClose(peerHost, torProxy string, peerPubKey *btcec.PublicKey,
|
||||
channelPoint wire.OutPoint, identity keychain.SingleKeyECDH) error {
|
||||
|
||||
p, err := connectPeer(
|
||||
peerHost, torProxy, peerPubKey, identity, dialTimeout,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error connecting to peer: %w", err)
|
||||
}
|
||||
|
||||
channelID := lnwire.NewChanIDFromOutPoint(channelPoint)
|
||||
|
||||
// Channel ID (32 byte) + u16 for the data length (which will be 0).
|
||||
data := make([]byte, 34)
|
||||
copy(data[:32], channelID[:])
|
||||
|
||||
log.Infof("Sending channel re-establish to peer to trigger force "+
|
||||
"close of channel %v", channelPoint)
|
||||
|
||||
err = p.SendMessageLazy(true, &lnwire.ChannelReestablish{
|
||||
ChanID: channelID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("Sending channel error message to peer to trigger force "+
|
||||
"close of channel %v", channelPoint)
|
||||
|
||||
_ = lnwire.SetCustomOverrides([]uint16{
|
||||
lnwire.MsgError, lnwire.MsgChannelReestablish,
|
||||
})
|
||||
msg, err := lnwire.NewCustom(lnwire.MsgError, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = p.SendMessageLazy(true, msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error sending message: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseOutPoint(s string) (*wire.OutPoint, error) {
|
||||
split := strings.Split(s, ":")
|
||||
if len(split) != 2 || len(split[0]) == 0 || len(split[1]) == 0 {
|
||||
return nil, fmt.Errorf("invalid channel point format: %v", s)
|
||||
}
|
||||
|
||||
index, err := strconv.ParseInt(split[1], 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode output index: %w", err)
|
||||
}
|
||||
|
||||
txid, err := chainhash.NewHashFromStr(split[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse hex string: %w", err)
|
||||
}
|
||||
|
||||
return &wire.OutPoint{
|
||||
Hash: *txid,
|
||||
Index: uint32(index),
|
||||
}, nil
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
key1Bytes, _ = hex.DecodeString(
|
||||
"0201943d78d61c8ad50ba57164830f536c156d8d89d979448bef3e67f564" +
|
||||
"ea0ab6",
|
||||
)
|
||||
key1, _ = btcec.ParsePubKey(key1Bytes)
|
||||
key2Bytes, _ = hex.DecodeString(
|
||||
"038b88de18064024e9da4dfc9c804283b3077a265dcd73ad3615b50badcb" +
|
||||
"debd5b",
|
||||
)
|
||||
key2, _ = btcec.ParsePubKey(key2Bytes)
|
||||
addr = "bc1qp5jnhnavt32fjwhnf5ttpvvym7e0syp79q5l9skz545q62d8u2uq05" +
|
||||
"ul63"
|
||||
)
|
||||
|
||||
func TestMatchScript(t *testing.T) {
|
||||
ok, _, err := matchScript(addr, key1, key2, &chaincfg.MainNetParams)
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
## chantools closepoolaccount
|
||||
|
||||
Tries to close a Pool account that has expired
|
||||
|
||||
### Synopsis
|
||||
|
||||
In case a Pool account cannot be closed normally with the
|
||||
poold daemon it can be closed with this command. The account **MUST** have
|
||||
expired already, otherwise this command doesn't work since a signature from the
|
||||
auctioneer is necessary.
|
||||
|
||||
You need to know the account's last unspent outpoint. That can either be
|
||||
obtained by running 'pool accounts list'
|
||||
|
||||
```
|
||||
chantools closepoolaccount [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
chantools closepoolaccount \
|
||||
--outpoint xxxxxxxxx:y \
|
||||
--sweepaddr bc1q..... \
|
||||
--feerate 10 \
|
||||
--publish
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
--apiurl string API URL to use (must be esplora compatible) (default "https://blockstream.info/api")
|
||||
--auctioneerkey string the auctioneer's static public key (default "028e87bdd134238f8347f845d9ecc827b843d0d1e27cdcb46da704d916613f4fce")
|
||||
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
|
||||
--feerate uint32 fee rate to use for the sweep transaction in sat/vByte (default 30)
|
||||
-h, --help help for closepoolaccount
|
||||
--maxnumaccounts uint32 the number of account indices to try at most (default 20)
|
||||
--maxnumbatchkeys uint32 the number of batch keys to try at most (default 500)
|
||||
--maxnumblocks uint32 the maximum number of blocks to try when brute forcing the expiry (default 200000)
|
||||
--minexpiry uint32 the block to start brute forcing the expiry from (default 648168)
|
||||
--outpoint string last account outpoint of the account to close (<txid>:<txindex>)
|
||||
--publish publish sweep TX to the chain API instead of just printing the TX
|
||||
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
|
||||
--sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
|
||||
--walletdb string read the seed/master root key to use fro deriving keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-r, --regtest Indicates if regtest parameters should be used
|
||||
-s, --signet Indicates if the public signet parameters should be used
|
||||
-t, --testnet Indicates if testnet parameters should be used
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels
|
||||
|
@ -1,44 +0,0 @@
|
||||
## chantools createwallet
|
||||
|
||||
Create a new lnd compatible wallet.db file from an existing seed or by generating a new one
|
||||
|
||||
### Synopsis
|
||||
|
||||
Creates a new wallet that can be used with lnd or with
|
||||
chantools. The wallet can be created from an existing seed or a new one can be
|
||||
generated (use --generateseed).
|
||||
|
||||
```
|
||||
chantools createwallet [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
chantools createwallet \
|
||||
--walletdbdir ~/.lnd/data/chain/bitcoin/mainnet
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
|
||||
--generateseed generate a new seed instead of using an existing one
|
||||
-h, --help help for createwallet
|
||||
--rootkey string BIP32 HD root key of the wallet to use for creating the new wallet; leave empty to prompt for lnd 24 word aezeed
|
||||
--walletdb string read the seed/master root key to use fro creating the new wallet from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
|
||||
--walletdbdir string the folder to create the new wallet.db file in
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-r, --regtest Indicates if regtest parameters should be used
|
||||
-s, --signet Indicates if the public signet parameters should be used
|
||||
-t, --testnet Indicates if testnet parameters should be used
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels
|
||||
|
@ -1,51 +0,0 @@
|
||||
## chantools doublespendinputs
|
||||
|
||||
Replace a transaction by double spending its input
|
||||
|
||||
### Synopsis
|
||||
|
||||
Tries to double spend the given inputs by deriving the
|
||||
private for the address and sweeping the funds to the given address. This can
|
||||
only be used with inputs that belong to an lnd wallet.
|
||||
|
||||
```
|
||||
chantools doublespendinputs [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
chantools doublespendinputs \
|
||||
--inputoutpoints xxxxxxxxx:y,xxxxxxxxx:y \
|
||||
--sweepaddr bc1q..... \
|
||||
--feerate 10 \
|
||||
--publish
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
--apiurl string API URL to use (must be esplora compatible) (default "https://blockstream.info/api")
|
||||
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
|
||||
--feerate uint32 fee rate to use for the sweep transaction in sat/vByte (default 30)
|
||||
-h, --help help for doublespendinputs
|
||||
--inputoutpoints strings list of outpoints to double spend in the format txid:vout
|
||||
--publish publish replacement TX to the chain API instead of just printing the TX
|
||||
--recoverywindow uint32 number of keys to scan per internal/external branch; output will consist of double this amount of keys (default 2500)
|
||||
--rootkey string BIP32 HD root key of the wallet to use for deriving the input keys; leave empty to prompt for lnd 24 word aezeed
|
||||
--sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
|
||||
--walletdb string read the seed/master root key to use fro deriving the input keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-r, --regtest Indicates if regtest parameters should be used
|
||||
-s, --signet Indicates if the public signet parameters should be used
|
||||
-t, --testnet Indicates if testnet parameters should be used
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels
|
||||
|
@ -1,46 +0,0 @@
|
||||
## chantools dropgraphzombies
|
||||
|
||||
Remove all channels identified as zombies from the graph to force a re-sync of the graph
|
||||
|
||||
### Synopsis
|
||||
|
||||
This command removes all channels that were identified as
|
||||
zombies from the local graph.
|
||||
|
||||
This will cause lnd to re-download all those channels from the network and can
|
||||
be helpful to fix a graph that is out of sync with the network.
|
||||
|
||||
CAUTION: Running this command will make it impossible to use the channel DB
|
||||
with an older version of lnd. Downgrading is not possible and you'll need to
|
||||
run lnd v0.18.0-beta or later after using this command!'
|
||||
|
||||
```
|
||||
chantools dropgraphzombies [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
chantools dropgraphzombies \
|
||||
--channeldb ~/.lnd/data/graph/mainnet/channel.db
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
--channeldb string lnd channel.db file to drop zombies from
|
||||
-h, --help help for dropgraphzombies
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-r, --regtest Indicates if regtest parameters should be used
|
||||
-s, --signet Indicates if the public signet parameters should be used
|
||||
-t, --testnet Indicates if testnet parameters should be used
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels
|
||||
|
@ -1,50 +0,0 @@
|
||||
## chantools pullanchor
|
||||
|
||||
Attempt to CPFP an anchor output of a channel
|
||||
|
||||
### Synopsis
|
||||
|
||||
Use this command to confirm a channel force close
|
||||
transaction of an anchor output channel type. This will attempt to CPFP the
|
||||
330 byte anchor output created for your node.
|
||||
|
||||
```
|
||||
chantools pullanchor [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
chantools pullanchor \
|
||||
--sponsorinput txid:vout \
|
||||
--anchoraddr bc1q..... \
|
||||
--changeaddr bc1q..... \
|
||||
--feerate 30
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
--anchoraddr stringArray the address of the anchor output (p2wsh or p2tr output with 330 satoshis) that should be pulled; can be specified multiple times per command to pull multiple anchors with a single transaction
|
||||
--apiurl string API URL to use (must be esplora compatible) (default "https://blockstream.info/api")
|
||||
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
|
||||
--changeaddr string the change address to send the remaining funds back to; specify 'fromseed' to derive a new address from the seed automatically
|
||||
--feerate uint32 fee rate to use for the sweep transaction in sat/vByte (default 30)
|
||||
-h, --help help for pullanchor
|
||||
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
|
||||
--sponsorinput string the input to use to sponsor the CPFP transaction; must be owned by the lnd node that owns the anchor output
|
||||
--walletdb string read the seed/master root key to use fro deriving keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-r, --regtest Indicates if regtest parameters should be used
|
||||
-s, --signet Indicates if the public signet parameters should be used
|
||||
-t, --testnet Indicates if testnet parameters should be used
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels
|
||||
|
@ -1,53 +0,0 @@
|
||||
## chantools recoverloopin
|
||||
|
||||
Recover a loop in swap that the loop daemon is not able to sweep
|
||||
|
||||
```
|
||||
chantools recoverloopin [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
chantools recoverloopin \
|
||||
--txid abcdef01234... \
|
||||
--vout 0 \
|
||||
--swap_hash abcdef01234... \
|
||||
--loop_db_dir /path/to/loop/db/dir \
|
||||
--sweep_addr bc1pxxxxxxx \
|
||||
--feerate 10
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
--apiurl string API URL to use (must be esplora compatible) (default "https://blockstream.info/api")
|
||||
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
|
||||
--feerate uint32 fee rate to use for the sweep transaction in sat/vByte
|
||||
-h, --help help for recoverloopin
|
||||
--loop_db_dir string path to the loop database directory, where the loop.db file is located
|
||||
--num_tries int number of tries to try to find the correct key index (default 1000)
|
||||
--output_amt uint amount of the output to sweep
|
||||
--publish publish sweep TX to the chain API instead of just printing the TX
|
||||
--rootkey string BIP32 HD root key of the wallet to use for deriving starting key; leave empty to prompt for lnd 24 word aezeed
|
||||
--sqlite_file string optional path to the loop sqlite database file, if not specified, the default location will be loaded from --loop_db_dir
|
||||
--start_key_index int start key index to try to find the correct key index
|
||||
--swap_hash string swap hash of the loop in swap
|
||||
--sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
|
||||
--txid string transaction id of the on-chain transaction that created the HTLC
|
||||
--vout uint32 output index of the on-chain transaction that created the HTLC
|
||||
--walletdb string read the seed/master root key to use fro deriving starting key from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-r, --regtest Indicates if regtest parameters should be used
|
||||
-s, --signet Indicates if the public signet parameters should be used
|
||||
-t, --testnet Indicates if testnet parameters should be used
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels
|
||||
|
@ -1,46 +0,0 @@
|
||||
## chantools rescuetweakedkey
|
||||
|
||||
Attempt to rescue funds locked in an address with a key that was affected by a specific bug in lnd
|
||||
|
||||
### Synopsis
|
||||
|
||||
There very likely is no reason to run this command
|
||||
unless you exactly know why or were told by the author of this tool to use it.
|
||||
|
||||
|
||||
```
|
||||
chantools rescuetweakedkey [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
chantools rescuetweakedkey \
|
||||
--path "m/1017'/0'/5'/0/0'" \
|
||||
--targetaddr bc1pxxxxxxx
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
|
||||
-h, --help help for rescuetweakedkey
|
||||
--numtries uint the number of mutations to try (default 10000000)
|
||||
--path string BIP32 derivation path to derive the starting key from; must start with "m/"
|
||||
--rootkey string BIP32 HD root key of the wallet to use for deriving starting key; leave empty to prompt for lnd 24 word aezeed
|
||||
--targetaddr string address the funds are locked in
|
||||
--walletdb string read the seed/master root key to use fro deriving starting key from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-r, --regtest Indicates if regtest parameters should be used
|
||||
-s, --signet Indicates if the public signet parameters should be used
|
||||
-t, --testnet Indicates if testnet parameters should be used
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels
|
||||
|
@ -1,41 +0,0 @@
|
||||
## chantools signmessage
|
||||
|
||||
Sign a message with the node's private key.
|
||||
|
||||
### Synopsis
|
||||
|
||||
Sign msg with the resident node's private key.
|
||||
Returns the signature as a zbase32 string.
|
||||
|
||||
```
|
||||
chantools signmessage [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
chantools signmessage --msg=foobar
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
|
||||
-h, --help help for signmessage
|
||||
--msg string the message to sign
|
||||
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
|
||||
--walletdb string read the seed/master root key to use fro decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-r, --regtest Indicates if regtest parameters should be used
|
||||
-s, --signet Indicates if the public signet parameters should be used
|
||||
-t, --testnet Indicates if testnet parameters should be used
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels
|
||||
|
@ -1,46 +0,0 @@
|
||||
## chantools signpsbt
|
||||
|
||||
Sign a Partially Signed Bitcoin Transaction (PSBT)
|
||||
|
||||
### Synopsis
|
||||
|
||||
Sign a PSBT with a master root key. The PSBT must contain
|
||||
an input that is owned by the master root key.
|
||||
|
||||
```
|
||||
chantools signpsbt [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
chantools signpsbt \
|
||||
--psbt <the_base64_encoded_psbt>
|
||||
|
||||
chantools signpsbt --fromrawpsbtfile <file_with_psbt>
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
|
||||
--fromrawpsbtfile string the file containing the raw, binary encoded PSBT packet to sign
|
||||
-h, --help help for signpsbt
|
||||
--psbt string Partially Signed Bitcoin Transaction to sign
|
||||
--rootkey string BIP32 HD root key of the wallet to use for signing the PSBT; leave empty to prompt for lnd 24 word aezeed
|
||||
--torawpsbtfile string the file to write the resulting signed raw, binary encoded PSBT packet to
|
||||
--walletdb string read the seed/master root key to use fro signing the PSBT from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-r, --regtest Indicates if regtest parameters should be used
|
||||
-s, --signet Indicates if the public signet parameters should be used
|
||||
-t, --testnet Indicates if testnet parameters should be used
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels
|
||||
|
@ -1,58 +0,0 @@
|
||||
## chantools sweepremoteclosed
|
||||
|
||||
Go through all the addresses that could have funds of channels that were force-closed by the remote party. A public block explorer is queried for each address and if any balance is found, all funds are swept to a given address
|
||||
|
||||
### Synopsis
|
||||
|
||||
This command helps users sweep funds that are in
|
||||
outputs of channels that were force-closed by the remote party. This command
|
||||
only needs to be used if no channel.backup file is available. By manually
|
||||
contacting the remote peers and asking them to force-close the channels, the
|
||||
funds can be swept after the force-close transaction was confirmed.
|
||||
|
||||
Supported remote force-closed channel types are:
|
||||
- STATIC_REMOTE_KEY (a.k.a. tweakless channels)
|
||||
- ANCHOR (a.k.a. anchor output channels)
|
||||
- SIMPLE_TAPROOT (a.k.a. simple taproot channels)
|
||||
|
||||
|
||||
```
|
||||
chantools sweepremoteclosed [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
chantools sweepremoteclosed \
|
||||
--recoverywindow 300 \
|
||||
--feerate 20 \
|
||||
--sweepaddr bc1q..... \
|
||||
--publish
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
--apiurl string API URL to use (must be esplora compatible) (default "https://blockstream.info/api")
|
||||
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
|
||||
--feerate uint32 fee rate to use for the sweep transaction in sat/vByte (default 30)
|
||||
-h, --help help for sweepremoteclosed
|
||||
--publish publish sweep TX to the chain API instead of just printing the TX
|
||||
--recoverywindow uint32 number of keys to scan per derivation path (default 200)
|
||||
--rootkey string BIP32 HD root key of the wallet to use for sweeping the wallet; leave empty to prompt for lnd 24 word aezeed
|
||||
--sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
|
||||
--walletdb string read the seed/master root key to use fro sweeping the wallet from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-r, --regtest Indicates if regtest parameters should be used
|
||||
-s, --signet Indicates if the public signet parameters should be used
|
||||
-t, --testnet Indicates if testnet parameters should be used
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels
|
||||
|
@ -1,48 +0,0 @@
|
||||
## chantools triggerforceclose
|
||||
|
||||
Connect to a Lightning Network peer and send specific messages to trigger a force close of the specified channel
|
||||
|
||||
### Synopsis
|
||||
|
||||
Asks the specified remote peer to force close a specific
|
||||
channel by first sending a channel re-establish message, and if that doesn't
|
||||
work, a custom error message (in case the peer is a specific version of CLN that
|
||||
does not properly respond to a Data Loss Protection re-establish message).'
|
||||
|
||||
```
|
||||
chantools triggerforceclose [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
chantools triggerforceclose \
|
||||
--peer 03abce...@xx.yy.zz.aa:9735 \
|
||||
--channel_point abcdef01234...:x
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
--apiurl string API URL to use (must be esplora compatible) (default "https://blockstream.info/api")
|
||||
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
|
||||
--channel_point string funding transaction outpoint of the channel to trigger the force close of (<txid>:<txindex>)
|
||||
-h, --help help for triggerforceclose
|
||||
--peer string remote peer address (<pubkey>@<host>[:<port>])
|
||||
--rootkey string BIP32 HD root key of the wallet to use for deriving the identity key; leave empty to prompt for lnd 24 word aezeed
|
||||
--torproxy string SOCKS5 proxy to use for Tor connections (to .onion addresses)
|
||||
--walletdb string read the seed/master root key to use fro deriving the identity key from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-r, --regtest Indicates if regtest parameters should be used
|
||||
-s, --signet Indicates if the public signet parameters should be used
|
||||
-t, --testnet Indicates if testnet parameters should be used
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue