From f4e8393362330ffe2d9bf748ebfe784d90b14899 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 11 Jun 2025 18:48:30 +0200 Subject: [PATCH 1/9] lnd: improve brontide mock --- lnd/brontide.go | 95 +++++++++++++++++--- lnd/mock.go | 230 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 314 insertions(+), 11 deletions(-) create mode 100644 lnd/mock.go diff --git a/lnd/brontide.go b/lnd/brontide.go index 8fc35bc..450f72a 100644 --- a/lnd/brontide.go +++ b/lnd/brontide.go @@ -10,11 +10,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 +29,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" @@ -175,11 +182,47 @@ 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 + }, + }) + + 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 { + return 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 +230,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 +265,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 +302,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()) diff --git a/lnd/mock.go b/lnd/mock.go new file mode 100644 index 0000000..7d9d9d4 --- /dev/null +++ b/lnd/mock.go @@ -0,0 +1,230 @@ +package lnd + +import ( + "net" + "sync/atomic" + "testing" + "time" + + "github.com/lightningnetwork/lnd/fn/v2" + "github.com/lightningnetwork/lnd/htlcswitch" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/peer" + "github.com/stretchr/testify/require" +) + +const ( + timeout = time.Second * 5 +) + +// 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(cid lnwire.ChannelID) {} + +// CreateAndAddLink currently returns a dummy value. +func (m *mockMessageSwitch) CreateAndAddLink(cfg htlcswitch.ChannelLinkConfig, + lnChan *lnwallet.LightningChannel) error { + + return nil +} + +// GetLinksByInterface returns the active links. +func (m *mockMessageSwitch) GetLinksByInterface(pub [33]byte) ( + []htlcswitch.ChannelUpdateHandler, error) { + + return m.links, nil +} + +// mockUpdateHandler is a mock implementation of the ChannelUpdateHandler +// interface. It is used in mockMessageSwitch's GetLinksByInterface method. +type mockUpdateHandler struct { + cid lnwire.ChannelID + isOutgoingAddBlocked atomic.Bool + isIncomingAddBlocked atomic.Bool +} + +// newMockUpdateHandler creates a new mockUpdateHandler. +func newMockUpdateHandler(cid lnwire.ChannelID) *mockUpdateHandler { + return &mockUpdateHandler{ + cid: cid, + } +} + +// HandleChannelUpdate currently does nothing. +func (m *mockUpdateHandler) HandleChannelUpdate(msg lnwire.Message) {} + +// ChanID returns the mockUpdateHandler's cid. +func (m *mockUpdateHandler) ChanID() lnwire.ChannelID { return m.cid } + +// Bandwidth currently returns a dummy value. +func (m *mockUpdateHandler) Bandwidth() lnwire.MilliSatoshi { return 0 } + +// EligibleToForward currently returns a dummy value. +func (m *mockUpdateHandler) EligibleToForward() bool { return false } + +// MayAddOutgoingHtlc currently returns nil. +func (m *mockUpdateHandler) MayAddOutgoingHtlc(lnwire.MilliSatoshi) error { return nil } + +type mockMessageConn struct { + t *testing.T + + // MessageConn embeds our interface so that the mock does not need to + // implement every function. The mock will panic if an unspecified function + // is called. + peer.MessageConn + + // writtenMessages is a channel that our mock pushes written messages into. + writtenMessages chan []byte + + readMessages chan []byte + curReadMessage []byte + + // writeRaceDetectingCounter is incremented on any function call + // associated with writing to the connection. The race detector will + // trigger on this counter if a data race exists. + writeRaceDetectingCounter int + + // readRaceDetectingCounter is incremented on any function call + // associated with reading from the connection. The race detector will + // trigger on this counter if a data race exists. + readRaceDetectingCounter int +} + +func (m *mockUpdateHandler) EnableAdds(dir htlcswitch.LinkDirection) bool { + if dir == htlcswitch.Outgoing { + return m.isOutgoingAddBlocked.Swap(false) + } + + return m.isIncomingAddBlocked.Swap(false) +} + +func (m *mockUpdateHandler) DisableAdds(dir htlcswitch.LinkDirection) bool { + if dir == htlcswitch.Outgoing { + return !m.isOutgoingAddBlocked.Swap(true) + } + + return !m.isIncomingAddBlocked.Swap(true) +} + +func (m *mockUpdateHandler) IsFlushing(dir htlcswitch.LinkDirection) bool { + switch dir { + case htlcswitch.Outgoing: + return m.isOutgoingAddBlocked.Load() + case htlcswitch.Incoming: + return m.isIncomingAddBlocked.Load() + } + + return false +} + +func (m *mockUpdateHandler) OnFlushedOnce(hook func()) { + hook() +} +func (m *mockUpdateHandler) OnCommitOnce( + _ htlcswitch.LinkDirection, hook func(), +) { + + hook() +} +func (m *mockUpdateHandler) InitStfu() <-chan fn.Result[lntypes.ChannelParty] { + // TODO(proofofkeags): Implement + c := make(chan fn.Result[lntypes.ChannelParty], 1) + + c <- fn.Errf[lntypes.ChannelParty]("InitStfu not yet implemented") + + return c +} + +func newMockConn(t *testing.T, expectedMessages int) *mockMessageConn { + return &mockMessageConn{ + t: t, + writtenMessages: make(chan []byte, expectedMessages), + readMessages: make(chan []byte, 1), + } +} + +// SetWriteDeadline mocks setting write deadline for our conn. +func (m *mockMessageConn) SetWriteDeadline(time.Time) error { + m.writeRaceDetectingCounter++ + return nil +} + +// Flush mocks a message conn flush. +func (m *mockMessageConn) Flush() (int, error) { + m.writeRaceDetectingCounter++ + return 0, nil +} + +// WriteMessage mocks sending of a message on our connection. It will push +// the bytes sent into the mock's writtenMessages channel. +func (m *mockMessageConn) WriteMessage(msg []byte) error { + m.writeRaceDetectingCounter++ + + msgCopy := make([]byte, len(msg)) + copy(msgCopy, msg) + + select { + case m.writtenMessages <- msgCopy: + case <-time.After(timeout): + m.t.Fatalf("timeout sending message: %v", msgCopy) + } + + return nil +} + +// assertWrite asserts that our mock as had WriteMessage called with the byte +// slice we expect. +func (m *mockMessageConn) assertWrite(expected []byte) { + select { + case actual := <-m.writtenMessages: + require.Equal(m.t, expected, actual) + + case <-time.After(timeout): + m.t.Fatalf("timeout waiting for write: %v", expected) + } +} + +func (m *mockMessageConn) SetReadDeadline(t time.Time) error { + m.readRaceDetectingCounter++ + return nil +} + +func (m *mockMessageConn) ReadNextHeader() (uint32, error) { + m.readRaceDetectingCounter++ + m.curReadMessage = <-m.readMessages + return uint32(len(m.curReadMessage)), nil +} + +func (m *mockMessageConn) ReadNextBody(buf []byte) ([]byte, error) { + m.readRaceDetectingCounter++ + return m.curReadMessage, nil +} + +func (m *mockMessageConn) RemoteAddr() net.Addr { + return nil +} + +func (m *mockMessageConn) LocalAddr() net.Addr { + return nil +} + +func (m *mockMessageConn) Close() error { + return nil +} From 2af18490d264d9ec5d21670dd8a4d6807629905b Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 13 Jun 2025 20:32:44 +0200 Subject: [PATCH 2/9] sweepremoteclose: add more info logging --- cmd/chantools/sweepremoteclosed.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) 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)) + } } } From f20e729b9deec957157b7a31c6879460514631fd Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 13 Jun 2025 20:32:56 +0200 Subject: [PATCH 3/9] triggerforceclose: better logging, abort retry after 60s --- cmd/chantools/triggerforceclose.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/cmd/chantools/triggerforceclose.go b/cmd/chantools/triggerforceclose.go index ce2cfa7..622193d 100644 --- a/cmd/chantools/triggerforceclose.go +++ b/cmd/chantools/triggerforceclose.go @@ -169,11 +169,13 @@ 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) 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 +183,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 } @@ -262,6 +265,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 +274,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, fmt.Errorf("no spends found after 60 " + + "seconds, aborting re-try loop") + } } log.Infof("Found force close transaction %v", spends[0].TXID) From 7bdc85b8c90434536ffbb0103699a084439b2c0b Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 13 Jun 2025 21:57:04 +0200 Subject: [PATCH 4/9] triggerforceclose: skip onion addresses when no proxy given --- cmd/chantools/triggerforceclose.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/cmd/chantools/triggerforceclose.go b/cmd/chantools/triggerforceclose.go index 622193d..306dc8f 100644 --- a/cmd/chantools/triggerforceclose.go +++ b/cmd/chantools/triggerforceclose.go @@ -172,6 +172,17 @@ func (c *triggerForceCloseCommand) Execute(_ *cobra.Command, _ []string) error { 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 (channel %d of %d)", openChan.ChanPoint, peerAddr, idx+1, @@ -223,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 } } From a246b3eaf20713873ce5c2f4a6921c2c10c9d69e Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Sat, 14 Jun 2025 14:40:54 +0200 Subject: [PATCH 5/9] lnd+triggerforceclose: close DB correctly --- cmd/chantools/triggerforceclose.go | 45 ++++++++++++++++++++++++------ lnd/brontide.go | 30 ++++++++++++-------- 2 files changed, 56 insertions(+), 19 deletions(-) diff --git a/cmd/chantools/triggerforceclose.go b/cmd/chantools/triggerforceclose.go index 306dc8f..d04f119 100644 --- a/cmd/chantools/triggerforceclose.go +++ b/cmd/chantools/triggerforceclose.go @@ -311,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 != "" { @@ -328,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 @@ -337,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"+ @@ -346,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", @@ -358,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) } @@ -405,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/lnd/brontide.go b/lnd/brontide.go index 450f72a..03a9ed5 100644 --- a/lnd/brontide.go +++ b/lnd/brontide.go @@ -3,6 +3,7 @@ package lnd import ( "errors" "fmt" + "math/rand" "os" "time" @@ -53,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) @@ -72,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) @@ -99,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{ @@ -212,7 +218,8 @@ func ConnectPeer(conn *brontide.Conn, connReq *connmgr.ConnReq, }, ) if err != nil { - return nil, fmt.Errorf("unable to create interceptable "+ + _ = channelDB.Close() + return nil, nil, fmt.Errorf("unable to create interceptable "+ "switch: %w", err) } @@ -322,8 +329,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 } From 9fa5f46c6b0ab55ce71a3fd187fe403660d84f20 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 17 Jun 2025 16:11:52 +0200 Subject: [PATCH 6/9] btc+dataformat: return open channels in summary --- btc/summary.go | 3 +++ dataformat/summary.go | 1 + 2 files changed, 4 insertions(+) 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/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) { From 78f1fe8f31cd00146c6a9e1035a246da8eb6f2de Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 17 Jun 2025 17:33:10 +0200 Subject: [PATCH 7/9] cln+lnd+zombierecovery: make fully CLN compatible --- cln/signer.go | 73 ++++++------ cmd/chantools/zombierecovery_makeoffer.go | 133 +++++++++++++++------- cmd/chantools/zombierecovery_signoffer.go | 36 +++++- lnd/signer.go | 17 ++- 4 files changed, 175 insertions(+), 84 deletions(-) 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/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/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) From f74f5b2f4e3b0d7806a2f6376b09b7f402d6e2d5 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 18 Jun 2025 08:55:07 +0200 Subject: [PATCH 8/9] multi: fix linter issues --- cmd/chantools/triggerforceclose.go | 2 +- lnd/brontide.go | 5 + lnd/mock.go | 198 +---------------------------- 3 files changed, 10 insertions(+), 195 deletions(-) diff --git a/cmd/chantools/triggerforceclose.go b/cmd/chantools/triggerforceclose.go index d04f119..e7a23ab 100644 --- a/cmd/chantools/triggerforceclose.go +++ b/cmd/chantools/triggerforceclose.go @@ -288,7 +288,7 @@ func closeChannel(identityPriv *btcec.PrivateKey, api *btc.ExplorerAPI, counter++ if counter >= 12 { - return nil, fmt.Errorf("no spends found after 60 " + + return nil, errors.New("no spends found after 60 " + "seconds, aborting re-try loop") } } diff --git a/lnd/brontide.go b/lnd/brontide.go index 03a9ed5..b5d3a7a 100644 --- a/lnd/brontide.go +++ b/lnd/brontide.go @@ -202,6 +202,11 @@ func ConnectPeer(conn *brontide.Conn, connReq *connmgr.ConnReq, 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{ diff --git a/lnd/mock.go b/lnd/mock.go index 7d9d9d4..de323be 100644 --- a/lnd/mock.go +++ b/lnd/mock.go @@ -1,22 +1,9 @@ package lnd import ( - "net" - "sync/atomic" - "testing" - "time" - - "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/htlcswitch" - "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" - "github.com/lightningnetwork/lnd/peer" - "github.com/stretchr/testify/require" -) - -const ( - timeout = time.Second * 5 ) // mockMessageSwitch is a mock implementation of the messageSwitch interface @@ -36,195 +23,18 @@ func (m *mockMessageSwitch) CircuitModifier() htlcswitch.CircuitModifier { } // RemoveLink currently does nothing. -func (m *mockMessageSwitch) RemoveLink(cid lnwire.ChannelID) {} +func (m *mockMessageSwitch) RemoveLink(lnwire.ChannelID) {} // CreateAndAddLink currently returns a dummy value. -func (m *mockMessageSwitch) CreateAndAddLink(cfg htlcswitch.ChannelLinkConfig, - lnChan *lnwallet.LightningChannel) error { +func (m *mockMessageSwitch) CreateAndAddLink(htlcswitch.ChannelLinkConfig, + *lnwallet.LightningChannel) error { return nil } // GetLinksByInterface returns the active links. -func (m *mockMessageSwitch) GetLinksByInterface(pub [33]byte) ( +func (m *mockMessageSwitch) GetLinksByInterface([33]byte) ( []htlcswitch.ChannelUpdateHandler, error) { return m.links, nil } - -// mockUpdateHandler is a mock implementation of the ChannelUpdateHandler -// interface. It is used in mockMessageSwitch's GetLinksByInterface method. -type mockUpdateHandler struct { - cid lnwire.ChannelID - isOutgoingAddBlocked atomic.Bool - isIncomingAddBlocked atomic.Bool -} - -// newMockUpdateHandler creates a new mockUpdateHandler. -func newMockUpdateHandler(cid lnwire.ChannelID) *mockUpdateHandler { - return &mockUpdateHandler{ - cid: cid, - } -} - -// HandleChannelUpdate currently does nothing. -func (m *mockUpdateHandler) HandleChannelUpdate(msg lnwire.Message) {} - -// ChanID returns the mockUpdateHandler's cid. -func (m *mockUpdateHandler) ChanID() lnwire.ChannelID { return m.cid } - -// Bandwidth currently returns a dummy value. -func (m *mockUpdateHandler) Bandwidth() lnwire.MilliSatoshi { return 0 } - -// EligibleToForward currently returns a dummy value. -func (m *mockUpdateHandler) EligibleToForward() bool { return false } - -// MayAddOutgoingHtlc currently returns nil. -func (m *mockUpdateHandler) MayAddOutgoingHtlc(lnwire.MilliSatoshi) error { return nil } - -type mockMessageConn struct { - t *testing.T - - // MessageConn embeds our interface so that the mock does not need to - // implement every function. The mock will panic if an unspecified function - // is called. - peer.MessageConn - - // writtenMessages is a channel that our mock pushes written messages into. - writtenMessages chan []byte - - readMessages chan []byte - curReadMessage []byte - - // writeRaceDetectingCounter is incremented on any function call - // associated with writing to the connection. The race detector will - // trigger on this counter if a data race exists. - writeRaceDetectingCounter int - - // readRaceDetectingCounter is incremented on any function call - // associated with reading from the connection. The race detector will - // trigger on this counter if a data race exists. - readRaceDetectingCounter int -} - -func (m *mockUpdateHandler) EnableAdds(dir htlcswitch.LinkDirection) bool { - if dir == htlcswitch.Outgoing { - return m.isOutgoingAddBlocked.Swap(false) - } - - return m.isIncomingAddBlocked.Swap(false) -} - -func (m *mockUpdateHandler) DisableAdds(dir htlcswitch.LinkDirection) bool { - if dir == htlcswitch.Outgoing { - return !m.isOutgoingAddBlocked.Swap(true) - } - - return !m.isIncomingAddBlocked.Swap(true) -} - -func (m *mockUpdateHandler) IsFlushing(dir htlcswitch.LinkDirection) bool { - switch dir { - case htlcswitch.Outgoing: - return m.isOutgoingAddBlocked.Load() - case htlcswitch.Incoming: - return m.isIncomingAddBlocked.Load() - } - - return false -} - -func (m *mockUpdateHandler) OnFlushedOnce(hook func()) { - hook() -} -func (m *mockUpdateHandler) OnCommitOnce( - _ htlcswitch.LinkDirection, hook func(), -) { - - hook() -} -func (m *mockUpdateHandler) InitStfu() <-chan fn.Result[lntypes.ChannelParty] { - // TODO(proofofkeags): Implement - c := make(chan fn.Result[lntypes.ChannelParty], 1) - - c <- fn.Errf[lntypes.ChannelParty]("InitStfu not yet implemented") - - return c -} - -func newMockConn(t *testing.T, expectedMessages int) *mockMessageConn { - return &mockMessageConn{ - t: t, - writtenMessages: make(chan []byte, expectedMessages), - readMessages: make(chan []byte, 1), - } -} - -// SetWriteDeadline mocks setting write deadline for our conn. -func (m *mockMessageConn) SetWriteDeadline(time.Time) error { - m.writeRaceDetectingCounter++ - return nil -} - -// Flush mocks a message conn flush. -func (m *mockMessageConn) Flush() (int, error) { - m.writeRaceDetectingCounter++ - return 0, nil -} - -// WriteMessage mocks sending of a message on our connection. It will push -// the bytes sent into the mock's writtenMessages channel. -func (m *mockMessageConn) WriteMessage(msg []byte) error { - m.writeRaceDetectingCounter++ - - msgCopy := make([]byte, len(msg)) - copy(msgCopy, msg) - - select { - case m.writtenMessages <- msgCopy: - case <-time.After(timeout): - m.t.Fatalf("timeout sending message: %v", msgCopy) - } - - return nil -} - -// assertWrite asserts that our mock as had WriteMessage called with the byte -// slice we expect. -func (m *mockMessageConn) assertWrite(expected []byte) { - select { - case actual := <-m.writtenMessages: - require.Equal(m.t, expected, actual) - - case <-time.After(timeout): - m.t.Fatalf("timeout waiting for write: %v", expected) - } -} - -func (m *mockMessageConn) SetReadDeadline(t time.Time) error { - m.readRaceDetectingCounter++ - return nil -} - -func (m *mockMessageConn) ReadNextHeader() (uint32, error) { - m.readRaceDetectingCounter++ - m.curReadMessage = <-m.readMessages - return uint32(len(m.curReadMessage)), nil -} - -func (m *mockMessageConn) ReadNextBody(buf []byte) ([]byte, error) { - m.readRaceDetectingCounter++ - return m.curReadMessage, nil -} - -func (m *mockMessageConn) RemoteAddr() net.Addr { - return nil -} - -func (m *mockMessageConn) LocalAddr() net.Addr { - return nil -} - -func (m *mockMessageConn) Close() error { - return nil -} From ec6b7c5aca6f139ff4fcccd8c8bdb3eab870e0b7 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 18 Jun 2025 08:58:47 +0200 Subject: [PATCH 9/9] doc: update parameters --- doc/chantools_zombierecovery_makeoffer.md | 1 + doc/chantools_zombierecovery_signoffer.md | 2 ++ 2 files changed, 3 insertions(+) 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