diff --git a/btc/summary.go b/btc/summary.go index d2be37d..f7a966d 100644 --- a/btc/summary.go +++ b/btc/summary.go @@ -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 } diff --git a/cln/signer.go b/cln/signer.go index 5685cde..0fa7c60 100644 --- a/cln/signer.go +++ b/cln/signer.go @@ -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) diff --git a/cmd/chantools/sweepremoteclosed.go b/cmd/chantools/sweepremoteclosed.go index 598bbfd..24f8675 100644 --- a/cmd/chantools/sweepremoteclosed.go +++ b/cmd/chantools/sweepremoteclosed.go @@ -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)) + } } } diff --git a/cmd/chantools/triggerforceclose.go b/cmd/chantools/triggerforceclose.go index ce2cfa7..e7a23ab 100644 --- a/cmd/chantools/triggerforceclose.go +++ b/cmd/chantools/triggerforceclose.go @@ -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 } diff --git a/cmd/chantools/zombierecovery_makeoffer.go b/cmd/chantools/zombierecovery_makeoffer.go index fcf2220..f4738f8 100644 --- a/cmd/chantools/zombierecovery_makeoffer.go +++ b/cmd/chantools/zombierecovery_makeoffer.go @@ -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! diff --git a/cmd/chantools/zombierecovery_signoffer.go b/cmd/chantools/zombierecovery_signoffer.go index 224e7ec..7fcf636 100644 --- a/cmd/chantools/zombierecovery_signoffer.go +++ b/cmd/chantools/zombierecovery_signoffer.go @@ -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 } diff --git a/dataformat/summary.go b/dataformat/summary.go index 69465f1..0fc5ef7 100644 --- a/dataformat/summary.go +++ b/dataformat/summary.go @@ -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) { diff --git a/doc/chantools_zombierecovery_makeoffer.md b/doc/chantools_zombierecovery_makeoffer.md index b6cedbd..4a82dc1 100644 --- a/doc/chantools_zombierecovery_makeoffer.md +++ b/doc/chantools_zombierecovery_makeoffer.md @@ -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 diff --git a/doc/chantools_zombierecovery_signoffer.md b/doc/chantools_zombierecovery_signoffer.md index 2a86be5..7a6b1c5 100644 --- a/doc/chantools_zombierecovery_signoffer.md +++ b/doc/chantools_zombierecovery_signoffer.md @@ -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 diff --git a/lnd/brontide.go b/lnd/brontide.go index 8fc35bc..b5d3a7a 100644 --- a/lnd/brontide.go +++ b/lnd/brontide.go @@ -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 } diff --git a/lnd/mock.go b/lnd/mock.go new file mode 100644 index 0000000..de323be --- /dev/null +++ b/lnd/mock.go @@ -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 +} diff --git a/lnd/signer.go b/lnd/signer.go index 3d142ed..f78588a 100644 --- a/lnd/signer.go +++ b/lnd/signer.go @@ -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)