Merge pull request #194 from lightninglabs/triggerforceclose-lnd-19

lnd: improve brontide mock, improve triggerforceclose, make zombierecovery makeoffer CLN compatible
This commit is contained in:
Oliver Gugger 2025-06-18 08:59:11 +02:00 committed by GitHub
commit f794333c0b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 404 additions and 118 deletions

View file

@ -47,6 +47,9 @@ func SummarizeChannels(api *ExplorerAPI, channels []*dataformat.SummaryEntry,
} else {
summaryFile.OpenChannels++
summaryFile.FundsOpenChannels += channel.LocalBalance
summaryFile.OpenChannelList = append(
summaryFile.OpenChannelList, channel,
)
channel.ClosingTX = nil
channel.HasPotential = true
}

View file

@ -95,6 +95,45 @@ func (s *Signer) FindMultisigKey(targetPubkey, peerPubKey *btcec.PublicKey,
return nil, errors.New("no matching pubkeys found")
}
func (s *Signer) AddPartialSignatureWithDesc(packet *psbt.Packet,
signDesc *input.SignDescriptor) error {
ourSigRaw, err := s.SignOutputRaw(packet.UnsignedTx, signDesc)
if err != nil {
return fmt.Errorf("error signing with our key: %w", err)
}
ourSig := append(ourSigRaw.Serialize(), byte(signDesc.HashType))
// Because of the way we derive keys in CLN, the public key in the key
// descriptor is the peer's public key, not our own. So we need to
// derive our own public key from the private key.
ourPrivKey, err := s.FetchPrivateKey(&signDesc.KeyDesc)
if err != nil {
return fmt.Errorf("error fetching private key for descriptor "+
"%v: %w", signDesc.KeyDesc, err)
}
ourPubKey := ourPrivKey.PubKey()
// Great, we were able to create our sig, let's add it to the PSBT.
updater, err := psbt.NewUpdater(packet)
if err != nil {
return fmt.Errorf("error creating PSBT updater: %w", err)
}
status, err := updater.Sign(
signDesc.InputIndex, ourSig, ourPubKey.SerializeCompressed(),
nil, signDesc.WitnessScript,
)
if err != nil {
return fmt.Errorf("error adding signature to PSBT: %w", err)
}
if status != 0 {
return fmt.Errorf("unexpected status for signature update, "+
"got %d wanted 0", status)
}
return nil
}
func (s *Signer) AddPartialSignature(packet *psbt.Packet,
keyDesc keychain.KeyDescriptor, utxo *wire.TxOut, witnessScript []byte,
inputIndex int) error {
@ -112,40 +151,8 @@ func (s *Signer) AddPartialSignature(packet *psbt.Packet,
packet.UnsignedTx, prevOutFetcher,
),
}
ourSigRaw, err := s.SignOutputRaw(packet.UnsignedTx, signDesc)
if err != nil {
return fmt.Errorf("error signing with our key: %w", err)
}
ourSig := append(ourSigRaw.Serialize(), byte(txscript.SigHashAll))
// Because of the way we derive keys in CLN, the public key in the key
// descriptor is the peer's public key, not our own. So we need to
// derive our own public key from the private key.
ourPrivKey, err := s.FetchPrivateKey(&keyDesc)
if err != nil {
return fmt.Errorf("error fetching private key for descriptor "+
"%v: %w", keyDesc, err)
}
ourPubKey := ourPrivKey.PubKey()
// Great, we were able to create our sig, let's add it to the PSBT.
updater, err := psbt.NewUpdater(packet)
if err != nil {
return fmt.Errorf("error creating PSBT updater: %w", err)
}
status, err := updater.Sign(
inputIndex, ourSig, ourPubKey.SerializeCompressed(), nil,
witnessScript,
)
if err != nil {
return fmt.Errorf("error adding signature to PSBT: %w", err)
}
if status != 0 {
return fmt.Errorf("unexpected status for signature update, "+
"got %d wanted 0", status)
}
return nil
return s.AddPartialSignatureWithDesc(packet, signDesc)
}
var _ lnd.ChannelSigner = (*Signer)(nil)

View file

@ -347,7 +347,10 @@ func findTargetsCln(hsmSecret [32]byte, pubKeys []*btcec.PublicKey,
targets []*targetAddr
api = newExplorerAPI(apiURL)
)
for _, pubKey := range pubKeys {
for idx, pubKey := range pubKeys {
log.Infof("Trying to find targets for pubkey %x (%d of %d)",
pubKey.SerializeCompressed(), idx+1, len(pubKeys))
for index := range recoveryWindow {
desc := &keychain.KeyDescriptor{
PubKey: pubKey,
@ -370,6 +373,14 @@ func findTargetsCln(hsmSecret [32]byte, pubKeys []*btcec.PublicKey,
"for addresses with funds: %w", err)
}
targets = append(targets, foundTargets...)
if idx > 0 && idx%200 == 0 {
log.Infof("Tried %d addresses for pubkey "+
"%x (%d of %d), found %d targets so "+
"far", index+1,
pubKey.SerializeCompressed(), idx+1,
len(pubKeys), len(targets))
}
}
}

View file

@ -169,11 +169,24 @@ func (c *triggerForceCloseCommand) Execute(_ *cobra.Command, _ []string) error {
pubKeys []string
outputs []string
)
for _, openChan := range channels {
for idx, openChan := range channels {
addr := pickAddr(openChan.Node2Info.Node.Addresses)
peerAddr := fmt.Sprintf("%s@%s", openChan.Node2, addr)
if c.TorProxy == "" &&
strings.Contains(addr, ".onion") {
log.Infof("Skipping channel %s with peer %s "+
"because it is a Tor address and no "+
"Tor proxy is configured",
openChan.ChanPoint, peerAddr)
continue
}
log.Infof("Attempting to force close channel %s with "+
"peer %s", openChan.ChanPoint, peerAddr)
"peer %s (channel %d of %d)",
openChan.ChanPoint, peerAddr, idx+1,
len(channels))
outputAddrs, err := closeChannel(
identityPriv, api, openChan.ChanPoint,
@ -181,7 +194,8 @@ func (c *triggerForceCloseCommand) Execute(_ *cobra.Command, _ []string) error {
)
if err != nil {
log.Errorf("Error closing channel %s, "+
"skipping: %v", openChan.ChanPoint, err)
"skipping and trying next one. "+
"Reason: %v", openChan.ChanPoint, err)
continue
}
@ -220,7 +234,7 @@ func pickAddr(addrs []*gqAddress) string {
// We'll pick the first address that is not a Tor address.
for _, addr := range addrs {
if !strings.HasSuffix(addr.Address, ".onion") {
if !strings.Contains(addr.Address, ".onion") {
return addr.Address
}
}
@ -262,6 +276,8 @@ func closeChannel(identityPriv *btcec.PrivateKey, api *btc.ExplorerAPI,
if err != nil {
return nil, fmt.Errorf("error getting spends: %w", err)
}
counter := 0
for len(spends) == 0 {
log.Infof("No spends found yet, waiting 5 seconds...")
time.Sleep(5 * time.Second)
@ -269,6 +285,12 @@ func closeChannel(identityPriv *btcec.PrivateKey, api *btc.ExplorerAPI,
if err != nil {
return nil, fmt.Errorf("error getting spends: %w", err)
}
counter++
if counter >= 12 {
return nil, errors.New("no spends found after 60 " +
"seconds, aborting re-try loop")
}
}
log.Infof("Found force close transaction %v", spends[0].TXID)
@ -289,7 +311,11 @@ func noiseDial(idKey keychain.SingleKeyECDH, lnAddr *lnwire.NetAddress,
}
func connectPeer(peerHost, torProxy string, identity keychain.SingleKeyECDH,
dialTimeout time.Duration) (*peer.Brontide, error) {
dialTimeout time.Duration) (*peer.Brontide, func() error, error) {
cleanup := func() error {
return nil
}
var dialNet tor.Net = &tor.ClearNet{}
if torProxy != "" {
@ -306,7 +332,8 @@ func connectPeer(peerHost, torProxy string, identity keychain.SingleKeyECDH,
peerHost, "9735", dialNet.ResolveTCPAddr,
)
if err != nil {
return nil, fmt.Errorf("error parsing peer address: %w", err)
return nil, cleanup, fmt.Errorf("error parsing peer address: "+
"%w", err)
}
peerPubKey := peerAddr.IdentityKey
@ -315,7 +342,11 @@ func connectPeer(peerHost, torProxy string, identity keychain.SingleKeyECDH,
peerAddr.String())
conn, err := noiseDial(identity, peerAddr, dialNet, dialTimeout)
if err != nil {
return nil, fmt.Errorf("error dialing peer: %w", err)
return nil, cleanup, fmt.Errorf("error dialing peer: %w", err)
}
cleanup = func() error {
return conn.Close()
}
log.Infof("Attempting to establish p2p connection to peer %x, dial"+
@ -324,9 +355,20 @@ func connectPeer(peerHost, torProxy string, identity keychain.SingleKeyECDH,
Addr: peerAddr,
Permanent: false,
}
p, err := lnd.ConnectPeer(conn, req, chainParams, identity)
p, channelDB, err := lnd.ConnectPeer(conn, req, chainParams, identity)
if err != nil {
return nil, fmt.Errorf("error connecting to peer: %w", err)
return nil, cleanup, fmt.Errorf("error connecting to peer: %w",
err)
}
cleanup = func() error {
p.Disconnect(errors.New("done with peer"))
if channelDB != nil {
if err := channelDB.Close(); err != nil {
log.Errorf("Error closing channel DB: %v", err)
}
}
return conn.Close()
}
log.Infof("Connection established to peer %x",
@ -336,17 +378,23 @@ func connectPeer(peerHost, torProxy string, identity keychain.SingleKeyECDH,
select {
case <-p.ActiveSignal():
case <-p.QuitSignal():
return nil, fmt.Errorf("peer %x disconnected",
return nil, cleanup, fmt.Errorf("peer %x disconnected",
peerPubKey.SerializeCompressed())
}
return p, nil
return p, cleanup, nil
}
func requestForceClose(peerHost, torProxy string, channelPoint wire.OutPoint,
identity keychain.SingleKeyECDH) error {
p, err := connectPeer(peerHost, torProxy, identity, dialTimeout)
p, cleanup, err := connectPeer(
peerHost, torProxy, identity, dialTimeout,
)
defer func() {
_ = cleanup()
}()
if err != nil {
return fmt.Errorf("error connecting to peer: %w", err)
}
@ -383,6 +431,9 @@ func requestForceClose(peerHost, torProxy string, channelPoint wire.OutPoint,
return fmt.Errorf("error sending message: %w", err)
}
// Wait a few seconds to give the peer time to process the message.
time.Sleep(5 * time.Second)
return nil
}

View file

@ -24,6 +24,7 @@ import (
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/wallet"
"github.com/lightninglabs/chantools/cln"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/input"
@ -40,6 +41,8 @@ type zombieRecoveryMakeOfferCommand struct {
MatchOnly bool
HsmSecret string
rootKey *rootKey
cmd *cobra.Command
}
@ -80,6 +83,12 @@ a counter offer.`,
&cc.MatchOnly, "matchonly", false, "only match the keys, "+
"don't create an offer",
)
cc.cmd.Flags().StringVar(
&cc.HsmSecret, "hsm_secret", "", "the hex encoded HSM secret "+
"to use for deriving the multisig keys for a CLN "+
"node; obtain by running 'xxd -p -c32 "+
"~/.lightning/bitcoin/hsm_secret'",
)
cc.rootKey = newRootKey(cc.cmd, "signing the offer")
@ -89,11 +98,6 @@ a counter offer.`,
func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command,
_ []string) error {
extendedKey, err := c.rootKey.read()
if err != nil {
return fmt.Errorf("error reading root key: %w", err)
}
if c.FeeRate == 0 {
c.FeeRate = defaultFeeSatPerVByte
}
@ -183,20 +187,72 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command,
}
}
// Make sure one of the nodes is ours.
_, pubKey, _, err := lnd.DeriveKey(
extendedKey, lnd.IdentityPath(chainParams), chainParams,
var (
signer lnd.ChannelSigner
ourNode *btcec.PublicKey
)
if err != nil {
return fmt.Errorf("error deriving identity pubkey: %w", err)
switch {
case c.HsmSecret != "":
secretBytes, err := hex.DecodeString(c.HsmSecret)
if err != nil {
return fmt.Errorf("error decoding HSM secret: %w", err)
}
var hsmSecret [32]byte
copy(hsmSecret[:], secretBytes)
ourNode, _, err = cln.NodeKey(hsmSecret)
if err != nil {
return fmt.Errorf("error deriving CLN node pubkey: %w",
err)
}
signer = &cln.Signer{
HsmSecret: hsmSecret,
}
default:
extendedKey, err := c.rootKey.read()
if err != nil {
return fmt.Errorf("error reading root key: %w", err)
}
_, ourNode, _, err = lnd.DeriveKey(
extendedKey, lnd.IdentityPath(chainParams), chainParams,
)
if err != nil {
return fmt.Errorf("error deriving identity pubkey: %w",
err)
}
signer = &lnd.Signer{
ExtendedKey: extendedKey,
ChainParams: chainParams,
}
}
pubKeyStr := hex.EncodeToString(pubKey.SerializeCompressed())
// Make sure one of the nodes is ours.
pubKeyStr := hex.EncodeToString(ourNode.SerializeCompressed())
if keys1.Node1.PubKey != pubKeyStr && keys1.Node2.PubKey != pubKeyStr {
return fmt.Errorf("derived pubkey %s from seed but that key "+
"was not found in the match files", pubKeyStr)
}
// We need to have the peer pubkey ready, in case we're using a CLN
// signer.
peerPubKeyStr := keys1.Node1.PubKey
if keys1.Node1.PubKey == pubKeyStr {
peerPubKeyStr = keys1.Node2.PubKey
}
peerPubKeyBytes, err := hex.DecodeString(peerPubKeyStr)
if err != nil {
return fmt.Errorf("error decoding peer pubkey: %w", err)
}
peerPubKey, err := btcec.ParsePubKey(peerPubKeyBytes)
if err != nil {
return fmt.Errorf("error parsing peer pubkey: %w", err)
}
// Pick the correct list of keys. There are 4 possibilities, given 2
// files with 2 node slots each.
var (
@ -344,6 +400,12 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command,
PrevOutputFetcher: prevOutFetcher,
}
// For CLN, we also need to set the peer's public key in the
// key descriptor.
if _, ok := signer.(*cln.Signer); ok {
signDesc.KeyDesc.PubKey = peerPubKey
}
switch a := channelAddr.(type) {
case *btcutil.AddressWitnessScriptHash:
estimator.AddWitnessInput(input.MultiSigWitnessSize)
@ -359,9 +421,15 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command,
signDesc.HashType = txscript.SigHashDefault
signDesc.SignMethod = input.TaprootKeySpendSignMethod
lndSigner, ok := signer.(*lnd.Signer)
if !ok {
return errors.New("taproot channels not " +
"supported for CLN")
}
err := addMuSig2Data(
extendedKey, &pIn, channel, theirChannels[idx],
op, a.WitnessProgram(),
lndSigner.ExtendedKey, &pIn, channel,
theirChannels[idx], op, a.WitnessProgram(),
)
if err != nil {
return fmt.Errorf("error adding MuSig2 data: "+
@ -487,18 +555,20 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command,
// Loop a second time through the inputs and sign each input. We now
// have all the witness/non-witness data filled in the psbt package.
signer := &lnd.Signer{
ExtendedKey: extendedKey,
ChainParams: chainParams,
}
for idx := range packet.UnsignedTx.TxIn {
signDesc := signDescs[idx]
// If we're dealing with a taproot channel, we'll need to
// create a MuSig2 partial signature.
if signDesc.SignMethod == input.TaprootKeySpendSignMethod {
lndSigner, ok := signer.(*lnd.Signer)
if !ok {
return errors.New("taproot channels not yet " +
"supported for CLN")
}
err := muSig2PartialSign(
signer, &signDesc.KeyDesc, packet, idx,
lndSigner, &signDesc.KeyDesc, packet, idx,
)
if err != nil {
return fmt.Errorf("error creating MuSig2 "+
@ -508,34 +578,11 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command,
continue
}
ourSigRaw, err := signer.SignOutputRaw(
packet.UnsignedTx, signDesc,
)
err = signer.AddPartialSignatureWithDesc(packet, signDesc)
if err != nil {
return fmt.Errorf("error signing with our key: %w", err)
}
ourSig := append(ourSigRaw.Serialize(), byte(signDesc.HashType))
// Great, we were able to create our sig, let's add it to the
// PSBT.
updater, err := psbt.NewUpdater(packet)
if err != nil {
return fmt.Errorf("error creating PSBT updater: %w",
return fmt.Errorf("error adding partial signature: %w",
err)
}
status, err := updater.Sign(
idx, ourSig,
signDesc.KeyDesc.PubKey.SerializeCompressed(), nil,
signDesc.WitnessScript,
)
if err != nil {
return fmt.Errorf("error adding signature to PSBT: %w",
err)
}
if status != 0 {
return fmt.Errorf("unexpected status for signature "+
"update, got %d wanted 0", status)
}
}
// Looks like we're done!

View file

@ -11,6 +11,7 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/txscript"
"github.com/lightninglabs/chantools/btc"
"github.com/lightninglabs/chantools/cln"
"github.com/lightninglabs/chantools/lnd"
"github.com/spf13/cobra"
@ -22,6 +23,9 @@ type zombieRecoverySignOfferCommand struct {
HsmSecret string
RemotePeer string
APIURL string
Publish bool
rootKey *rootKey
cmd *cobra.Command
}
@ -54,6 +58,16 @@ peer to recover funds from one or more channels.`,
"peer node identity key, only required when running "+
"'signoffer' on the CLN side",
)
cc.cmd.Flags().StringVar(
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use for "+
"publishing the final transaction (must be esplora "+
"compatible)",
)
cc.cmd.Flags().BoolVar(
&cc.Publish, "publish", false, "if set, the final PSBT "+
"will be published to the network after signing, "+
"otherwise it will just be printed to stdout",
)
cc.rootKey = newRootKey(cc.cmd, "signing the offer")
@ -115,11 +129,13 @@ func (c *zombieRecoverySignOfferCommand) Execute(_ *cobra.Command,
}
}
return signOffer(packet, signer, remoteNode)
return signOffer(
packet, signer, remoteNode, newExplorerAPI(c.APIURL), c.Publish,
)
}
func signOffer(packet *psbt.Packet, signer lnd.ChannelSigner,
peerPubKey *btcec.PublicKey) error {
peerPubKey *btcec.PublicKey, api *btc.ExplorerAPI, publish bool) error {
// Now let's check that the packet has the expected proprietary key with
// our pubkey that we need to sign with.
@ -243,9 +259,19 @@ func signOffer(packet *psbt.Packet, signer lnd.ChannelSigner,
return fmt.Errorf("unable to serialize final TX: %w", err)
}
fmt.Printf("Success, we counter signed the PSBT and extracted the "+
"final\ntransaction. Please publish this using any bitcoin "+
"node:\n\n%x\n\n", buf.Bytes())
// Publish TX.
if publish {
response, err := api.PublishTx(hex.EncodeToString(buf.Bytes()))
if err != nil {
return err
}
log.Infof("Published TX %s, response: %s",
finalTx.TxHash().String(), response)
} else {
fmt.Printf("Success, we counter signed the PSBT and extracted "+
"the final\ntransaction. Please publish this using "+
"any bitcoin node:\n\n%x\n\n", buf.Bytes())
}
return nil
}

View file

@ -95,6 +95,7 @@ type SummaryEntryFile struct {
FundsClosedSpent uint64 `json:"funds_closed_channels_spent"`
FundsForceClose uint64 `json:"funds_force_closed_maybe_ours"`
FundsCoopClose uint64 `json:"funds_coop_closed_maybe_ours"`
OpenChannelList []*SummaryEntry `json:"open_channel_list"`
}
func ExtractSummaryFromDump(data string) ([]*SummaryEntry, error) {

View file

@ -31,6 +31,7 @@ chantools zombierecovery makeoffer \
--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 makeoffer
--hsm_secret string the hex encoded HSM secret to use for deriving the multisig keys for a CLN node; obtain by running 'xxd -p -c32 ~/.lightning/bitcoin/hsm_secret'
--matchonly only match the keys, don't create an offer
--node1_keys string the JSON file generated in theprevious step ('preparekeys') command of node 1
--node2_keys string the JSON file generated in theprevious step ('preparekeys') command of node 2

View file

@ -21,10 +21,12 @@ chantools zombierecovery signoffer \
### Options
```
--apiurl string API URL to use for publishing the final transaction (must be esplora compatible) (default "https://api.node-recovery.com")
--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 signoffer
--hsm_secret string the hex encoded HSM secret to use for deriving the multisig keys for a CLN node; obtain by running 'xxd -p -c32 ~/.lightning/bitcoin/hsm_secret'
--psbt string the base64 encoded PSBT that the other party sent as an offer to rescue funds
--publish if set, the final PSBT will be published to the network after signing, otherwise it will just be printed to stdout
--remote_peer string the hex encoded remote peer node identity key, only required when running 'signoffer' on the CLN side
--rootkey string BIP32 HD root key of the wallet to use for signing the offer; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use for signing the offer from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag

View file

@ -3,6 +3,7 @@ package lnd
import (
"errors"
"fmt"
"math/rand"
"os"
"time"
@ -10,11 +11,16 @@ import (
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/connmgr"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/aliasmgr"
"github.com/lightningnetwork/lnd/brontide"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channelnotifier"
"github.com/lightningnetwork/lnd/discovery"
"github.com/lightningnetwork/lnd/feature"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/graph/db/models"
"github.com/lightningnetwork/lnd/htlcswitch"
"github.com/lightningnetwork/lnd/htlcswitch/hodl"
"github.com/lightningnetwork/lnd/keychain"
@ -24,7 +30,9 @@ import (
"github.com/lightningnetwork/lnd/lntest/mock"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwallet/chancloser"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/msgmux"
"github.com/lightningnetwork/lnd/netann"
"github.com/lightningnetwork/lnd/peer"
"github.com/lightningnetwork/lnd/pool"
@ -46,11 +54,12 @@ var (
func ConnectPeer(conn *brontide.Conn, connReq *connmgr.ConnReq,
netParams *chaincfg.Params,
identityECDH keychain.SingleKeyECDH) (*peer.Brontide, error) {
identityECDH keychain.SingleKeyECDH) (*peer.Brontide, *channeldb.DB,
error) {
featureMgr, err := feature.NewManager(feature.Config{})
if err != nil {
return nil, err
return nil, nil, err
}
initFeatures := featureMgr.Get(feature.SetInit)
@ -65,7 +74,7 @@ func ConnectPeer(conn *brontide.Conn, connReq *connmgr.ConnReq,
}
errBuffer, err := queue.NewCircularBuffer(500)
if err != nil {
return nil, err
return nil, nil, err
}
pongBuf := make([]byte, lnwire.MaxPongBytes)
@ -92,27 +101,31 @@ func ConnectPeer(conn *brontide.Conn, connReq *connmgr.ConnReq,
)
if err := writePool.Start(); err != nil {
return nil, fmt.Errorf("unable to start write pool: %w", err)
return nil, nil, fmt.Errorf("unable to start write pool: %w",
err)
}
if err := readPool.Start(); err != nil {
return nil, fmt.Errorf("unable to start read pool: %w", err)
return nil, nil, fmt.Errorf("unable to start read pool: %w",
err)
}
randNum := rand.Int31()
backend, err := kvdb.GetBoltBackend(&kvdb.BoltBackendConfig{
DBPath: os.TempDir(),
DBFileName: "channel.db",
DBFileName: fmt.Sprintf("channel-%d.db", randNum),
NoFreelistSync: true,
AutoCompact: false,
AutoCompactMinAge: kvdb.DefaultBoltAutoCompactMinAge,
DBTimeout: kvdb.DefaultDBTimeout,
})
if err != nil {
return nil, err
return nil, nil, err
}
channelDB, err := channeldb.CreateWithBackend(backend)
if err != nil {
return nil, err
_ = backend.Close()
return nil, nil, err
}
gossiper := discovery.New(discovery.Config{
@ -175,11 +188,53 @@ func ConnectPeer(conn *brontide.Conn, connReq *connmgr.ConnReq,
PubKey: identityECDH.PubKey(),
})
chanStatusMgr, err := netann.NewChanStatusManager(&netann.
ChanStatusConfig{
ChanStatusSampleInterval: 30 * time.Second,
ChanDisableTimeout: 2 * time.Minute,
DB: channelDB.ChannelStateDB(),
IsChannelActive: func(lnwire.ChannelID) bool {
return true
},
ApplyChannelUpdate: func(*lnwire.ChannelUpdate1,
*wire.OutPoint, bool) error {
return nil
},
})
if err != nil {
_ = channelDB.Close()
return nil, nil, fmt.Errorf("unable to create channel status "+
"manager: %w", err)
}
channelNotifier := channelnotifier.New(channelDB.ChannelStateDB())
interceptableSwitchNotifier := &mock.ChainNotifier{
EpochChan: make(chan *chainntnfs.BlockEpoch, 1),
}
interceptableSwitchNotifier.EpochChan <- &chainntnfs.BlockEpoch{
Height: 1,
}
interceptableSwitch, err := htlcswitch.NewInterceptableSwitch(
&htlcswitch.InterceptableSwitchConfig{
CltvRejectDelta: 13,
CltvInterceptDelta: 16,
Notifier: interceptableSwitchNotifier,
},
)
if err != nil {
_ = channelDB.Close()
return nil, nil, fmt.Errorf("unable to create interceptable "+
"switch: %w", err)
}
pCfg := peer.Config{
Conn: conn,
ConnReq: connReq,
Conn: conn,
ConnReq: connReq,
PubKeyBytes: [33]byte(
identityECDH.PubKey().SerializeCompressed(),
),
Addr: peerAddr,
Inbound: false,
Features: initFeatures,
LegacyFeatures: legacyFeatures,
OutgoingCltvRejectDelta: lncfg.DefaultOutgoingCltvRejectDelta,
@ -187,9 +242,30 @@ func ConnectPeer(conn *brontide.Conn, connReq *connmgr.ConnReq,
ErrorBuffer: errBuffer,
WritePool: writePool,
ReadPool: readPool,
Switch: &mockMessageSwitch{},
InterceptSwitch: interceptableSwitch,
ChannelDB: channelDB.ChannelStateDB(),
ChainArb: nil,
AuthGossiper: gossiper,
ChainNotifier: &mock.ChainNotifier{},
ChanStatusMgr: chanStatusMgr,
ChainIO: &mock.ChainIO{},
FeeEstimator: nil,
Signer: nil,
SigPool: nil,
Wallet: &lnwallet.LightningWallet{
WalletController: &mock.WalletController{},
},
ChainNotifier: &mock.ChainNotifier{},
BestBlockView: chainntnfs.NewBestBlockTracker(
&mock.ChainNotifier{},
),
RoutingPolicy: models.ForwardingPolicy{},
Sphinx: nil,
WitnessBeacon: nil,
Invoices: nil,
ChannelNotifier: channelNotifier,
HtlcNotifier: nil,
TowerClient: nil,
DisconnectPeer: func(key *btcec.PublicKey) error {
fmt.Printf("Peer %x disconnected\n",
key.SerializeCompressed())
@ -201,23 +277,20 @@ func ConnectPeer(conn *brontide.Conn, connReq *connmgr.ConnReq,
return lnwire.NodeAnnouncement{},
errors.New("unimplemented")
},
PongBuf: pongBuf,
PrunePersistentPeerConnection: func(_ [33]byte) {},
FetchLastChanUpdate: func(_ lnwire.ShortChannelID) (
*lnwire.ChannelUpdate1, error) {
return nil, errors.New("unimplemented")
},
FundingManager: nil,
Hodl: &hodl.Config{},
UnsafeReplay: false,
MaxOutgoingCltvExpiry: htlcswitch.DefaultMaxOutgoingCltvExpiry,
MaxChannelFeeAllocation: htlcswitch.DefaultMaxLinkFeeAllocation,
CoopCloseTargetConfs: defaultCoopCloseTargetConfs,
MaxAnchorsCommitFeeRate: commitFee.FeePerKWeight(),
CoopCloseTargetConfs: defaultCoopCloseTargetConfs,
ServerPubKey: [33]byte{},
ChannelCommitInterval: defaultChannelCommitInterval,
PendingCommitInterval: defaultPendingCommitInterval,
ChannelCommitBatchSize: defaultChannelCommitBatchSize,
@ -241,7 +314,19 @@ func ConnectPeer(conn *brontide.Conn, connReq *connmgr.ConnReq,
return nil
},
Quit: make(chan struct{}),
AuxLeafStore: fn.None[lnwallet.AuxLeafStore](),
AuxSigner: fn.None[lnwallet.AuxSigner](),
AuxResolver: fn.None[lnwallet.AuxContractResolver](),
AuxTrafficShaper: fn.None[htlcswitch.AuxTrafficShaper](),
PongBuf: pongBuf,
DisallowRouteBlinding: false,
DisallowQuiescence: false,
MaxFeeExposure: 0,
MsgRouter: fn.None[msgmux.Router](),
AuxChanCloser: fn.None[chancloser.AuxChanCloser](),
ShouldFwdExpEndorsement: nil,
NoDisconnectOnPongFailure: false,
Quit: make(chan struct{}),
}
copy(pCfg.PubKeyBytes[:], peerAddr.IdentityKey.SerializeCompressed())
@ -249,8 +334,9 @@ func ConnectPeer(conn *brontide.Conn, connReq *connmgr.ConnReq,
p := peer.NewBrontide(pCfg)
if err := p.Start(); err != nil {
return nil, err
_ = channelDB.Close()
return nil, nil, err
}
return p, nil
return p, channelDB, nil
}

40
lnd/mock.go Normal file
View file

@ -0,0 +1,40 @@
package lnd
import (
"github.com/lightningnetwork/lnd/htlcswitch"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire"
)
// mockMessageSwitch is a mock implementation of the messageSwitch interface
// used for testing without relying on a *htlcswitch.Switch in unit tests.
type mockMessageSwitch struct {
links []htlcswitch.ChannelUpdateHandler
}
// BestHeight currently returns a dummy value.
func (m *mockMessageSwitch) BestHeight() uint32 {
return 0
}
// CircuitModifier currently returns a dummy value.
func (m *mockMessageSwitch) CircuitModifier() htlcswitch.CircuitModifier {
return nil
}
// RemoveLink currently does nothing.
func (m *mockMessageSwitch) RemoveLink(lnwire.ChannelID) {}
// CreateAndAddLink currently returns a dummy value.
func (m *mockMessageSwitch) CreateAndAddLink(htlcswitch.ChannelLinkConfig,
*lnwallet.LightningChannel) error {
return nil
}
// GetLinksByInterface returns the active links.
func (m *mockMessageSwitch) GetLinksByInterface([33]byte) (
[]htlcswitch.ChannelUpdateHandler, error) {
return m.links, nil
}

View file

@ -35,6 +35,9 @@ type ChannelSigner interface {
FindMultisigKey(targetPubkey, peerPubKey *btcec.PublicKey,
maxNumKeys uint32) (*keychain.KeyDescriptor, error)
AddPartialSignatureWithDesc(packet *psbt.Packet,
signDesc *input.SignDescriptor) error
AddPartialSignature(packet *psbt.Packet,
keyDesc keychain.KeyDescriptor, utxo *wire.TxOut,
witnessScript []byte, inputIndex int) error
@ -223,11 +226,18 @@ func (s *Signer) AddPartialSignature(packet *psbt.Packet,
packet.UnsignedTx, prevOutFetcher,
),
}
return s.AddPartialSignatureWithDesc(packet, signDesc)
}
func (s *Signer) AddPartialSignatureWithDesc(packet *psbt.Packet,
signDesc *input.SignDescriptor) error {
ourSigRaw, err := s.SignOutputRaw(packet.UnsignedTx, signDesc)
if err != nil {
return fmt.Errorf("error signing with our key: %w", err)
}
ourSig := append(ourSigRaw.Serialize(), byte(txscript.SigHashAll))
ourSig := append(ourSigRaw.Serialize(), byte(signDesc.HashType))
// Great, we were able to create our sig, let's add it to the PSBT.
updater, err := psbt.NewUpdater(packet)
@ -235,8 +245,9 @@ func (s *Signer) AddPartialSignature(packet *psbt.Packet,
return fmt.Errorf("error creating PSBT updater: %w", err)
}
status, err := updater.Sign(
inputIndex, ourSig, keyDesc.PubKey.SerializeCompressed(), nil,
witnessScript,
signDesc.InputIndex, ourSig,
signDesc.KeyDesc.PubKey.SerializeCompressed(), nil,
signDesc.WitnessScript,
)
if err != nil {
return fmt.Errorf("error adding signature to PSBT: %w", err)