diff --git a/build/version.go b/build/version.go index ad1511c725..fe212cd6a7 100644 --- a/build/version.go +++ b/build/version.go @@ -43,11 +43,11 @@ const ( AppMinor uint = 17 // AppPatch defines the application patch for this binary. - AppPatch uint = 2 + AppPatch uint = 3 // AppPreRelease MUST only contain characters from semanticAlphabet per // the semantic versioning spec. - AppPreRelease = "beta" + AppPreRelease = "beta.rc1" ) func init() { diff --git a/channeldb/channel.go b/channeldb/channel.go index 67a32379e7..dbd796c879 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -849,6 +849,30 @@ type OpenChannel struct { sync.RWMutex } +// String returns a string representation of the channel. +func (c *OpenChannel) String() string { + indexStr := "height=%v, local_htlc_index=%v, local_log_index=%v, " + + "remote_htlc_index=%v, remote_log_index=%v" + + commit := c.LocalCommitment + local := fmt.Sprintf(indexStr, commit.CommitHeight, + commit.LocalHtlcIndex, commit.LocalLogIndex, + commit.RemoteHtlcIndex, commit.RemoteLogIndex, + ) + + commit = c.RemoteCommitment + remote := fmt.Sprintf(indexStr, commit.CommitHeight, + commit.LocalHtlcIndex, commit.LocalLogIndex, + commit.RemoteHtlcIndex, commit.RemoteLogIndex, + ) + + return fmt.Sprintf("SCID=%v, status=%v, initiator=%v, pending=%v, "+ + "local commitment has %s, remote commitment has %s", + c.ShortChannelID, c.chanStatus, c.IsInitiator, c.IsPending, + local, remote, + ) +} + // ShortChanID returns the current ShortChannelID of this channel. func (c *OpenChannel) ShortChanID() lnwire.ShortChannelID { c.RLock() @@ -2100,6 +2124,10 @@ func (c *OpenChannel) ActiveHtlcs() []HTLC { // which ones are present on their commitment. remoteHtlcs := make(map[[32]byte]struct{}) for _, htlc := range c.RemoteCommitment.Htlcs { + log.Tracef("RemoteCommitment has htlc: id=%v, update=%v "+ + "incoming=%v", htlc.HtlcIndex, htlc.LogIndex, + htlc.Incoming) + onionHash := sha256.Sum256(htlc.OnionBlob[:]) remoteHtlcs[onionHash] = struct{}{} } @@ -2108,8 +2136,16 @@ func (c *OpenChannel) ActiveHtlcs() []HTLC { // as active if *we* know them as well. activeHtlcs := make([]HTLC, 0, len(remoteHtlcs)) for _, htlc := range c.LocalCommitment.Htlcs { + log.Tracef("LocalCommitment has htlc: id=%v, update=%v "+ + "incoming=%v", htlc.HtlcIndex, htlc.LogIndex, + htlc.Incoming) + onionHash := sha256.Sum256(htlc.OnionBlob[:]) if _, ok := remoteHtlcs[onionHash]; !ok { + log.Tracef("Skipped htlc due to onion mismatched: "+ + "id=%v, update=%v incoming=%v", + htlc.HtlcIndex, htlc.LogIndex, htlc.Incoming) + continue } diff --git a/docs/release-notes/release-notes-0.17.3.md b/docs/release-notes/release-notes-0.17.3.md new file mode 100644 index 0000000000..94ca97daaf --- /dev/null +++ b/docs/release-notes/release-notes-0.17.3.md @@ -0,0 +1,57 @@ +# Release Notes +- [Bug Fixes](#bug-fixes) +- [New Features](#new-features) + - [Functional Enhancements](#functional-enhancements) + - [RPC Additions](#rpc-additions) + - [lncli Additions](#lncli-additions) +- [Improvements](#improvements) + - [Functional Updates](#functional-updates) + - [RPC Updates](#rpc-updates) + - [lncli Updates](#lncli-updates) + - [Breaking Changes](#breaking-changes) + - [Performance Improvements](#performance-improvements) + - [Technical and Architectural Updates](#technical-and-architectural-updates) + - [BOLT Spec Updates](#bolt-spec-updates) + - [Testing](#testing) + - [Database](#database) + - [Code Health](#code-health) + - [Tooling and Documentation](#tooling-and-documentation) + +# Bug Fixes + +* [Replaced](https://github.com/lightningnetwork/lnd/pull/8224) + `musig2Sessions` with a `SyncMap` used in `input` package to avoid concurrent + write to this map. + +* [Fixed](https://github.com/lightningnetwork/lnd/pull/8220) a loop variable + issue which may affect programs built using go `v1.20` and below. + +* [An issue where LND would hang on shutdown has been fixed.](https://github.com/lightningnetwork/lnd/pull/8151) + +# New Features +## Functional Enhancements +## RPC Additions +## lncli Additions + +# Improvements +## Functional Updates +## RPC Updates +## lncli Updates +## Code Health +## Breaking Changes +## Performance Improvements + +* [Optimized](https://github.com/lightningnetwork/lnd/pull/8232) the memoray + usage of `btcwallet`'s mempool. Users would need to use `bitcoind v25.0.0` + and above to take the advantage of this optimization. + +# Technical and Architectural Updates +## BOLT Spec Updates +## Testing +## Database +## Code Health +## Tooling and Documentation + +# Contributors (Alphabetical Order) +* Eugene Siegel +* Yong Yu diff --git a/go.mod b/go.mod index c42106e328..fcdab1c860 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/btcsuite/btcd/btcutil/psbt v1.1.8 github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f - github.com/btcsuite/btcwallet v0.16.10-0.20231017144732-e3ff37491e9c + github.com/btcsuite/btcwallet v0.16.10-0.20231129183218-5df09dd43358 github.com/btcsuite/btcwallet/wallet/txauthor v1.3.2 github.com/btcsuite/btcwallet/wallet/txrules v1.2.0 github.com/btcsuite/btcwallet/walletdb v1.4.0 diff --git a/go.sum b/go.sum index e6a7530926..5d07264dcc 100644 --- a/go.sum +++ b/go.sum @@ -95,8 +95,8 @@ github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2/go.mod h1:7SFka0XMvUgj3hfZtyd github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= -github.com/btcsuite/btcwallet v0.16.10-0.20231017144732-e3ff37491e9c h1:+7tbYEUj0TYYIvuvE9YP+x5dU3FT/8J6Qh8d5YvQwrE= -github.com/btcsuite/btcwallet v0.16.10-0.20231017144732-e3ff37491e9c/go.mod h1:WSKhOJWUmUOHKCKEzdt+jWAHFAE/t4RqVbCwL2pEdiU= +github.com/btcsuite/btcwallet v0.16.10-0.20231129183218-5df09dd43358 h1:lZUSo6TISHUJQxpn/AniW5gqaN1iRNS87SDWvV3AHfg= +github.com/btcsuite/btcwallet v0.16.10-0.20231129183218-5df09dd43358/go.mod h1:WSKhOJWUmUOHKCKEzdt+jWAHFAE/t4RqVbCwL2pEdiU= github.com/btcsuite/btcwallet/wallet/txauthor v1.3.2 h1:etuLgGEojecsDOYTII8rYiGHjGyV5xTqsXi+ZQ715UU= github.com/btcsuite/btcwallet/wallet/txauthor v1.3.2/go.mod h1:Zpk/LOb2sKqwP2lmHjaZT9AdaKsHPSbNLm2Uql5IQ/0= github.com/btcsuite/btcwallet/wallet/txrules v1.2.0 h1:BtEN5Empw62/RVnZ0VcJaVtVlBijnLlJY+dwjAye2Bg= diff --git a/htlcswitch/link.go b/htlcswitch/link.go index 2a1e49923d..e302cf3d8f 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -649,12 +649,13 @@ func (l *channelLink) createFailureWithUpdate(incoming bool, // flow. We'll compare out commitment chains with the remote party, and re-send // either a danging commit signature, a revocation, or both. func (l *channelLink) syncChanStates() error { - l.log.Info("attempting to re-synchronize") + chanState := l.channel.State() + + l.log.Infof("Attempting to re-synchronize channel: %v", chanState) // First, we'll generate our ChanSync message to send to the other // side. Based on this message, the remote party will decide if they // need to retransmit any data or not. - chanState := l.channel.State() localChanSyncMsg, err := chanState.ChanSyncMsg() if err != nil { return fmt.Errorf("unable to generate chan sync message for "+ diff --git a/input/musig2_session_manager.go b/input/musig2_session_manager.go index 827652cc20..b2cac4899d 100644 --- a/input/musig2_session_manager.go +++ b/input/musig2_session_manager.go @@ -8,6 +8,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lnutils" "github.com/lightningnetwork/lnd/multimutex" ) @@ -38,16 +39,18 @@ type MusigSessionManager struct { sessionMtx *multimutex.Mutex[MuSig2SessionID] - musig2Sessions map[MuSig2SessionID]*MuSig2State + musig2Sessions *lnutils.SyncMap[MuSig2SessionID, *MuSig2State] } // NewMusigSessionManager creates a new musig manager given an abstract key // fetcher. func NewMusigSessionManager(keyFetcher PrivKeyFetcher) *MusigSessionManager { return &MusigSessionManager{ - keyFetcher: keyFetcher, - musig2Sessions: make(map[MuSig2SessionID]*MuSig2State), - sessionMtx: multimutex.NewMutex[MuSig2SessionID](), + keyFetcher: keyFetcher, + musig2Sessions: &lnutils.SyncMap[ + MuSig2SessionID, *MuSig2State, + ]{}, + sessionMtx: multimutex.NewMutex[MuSig2SessionID](), } } @@ -134,9 +137,7 @@ func (m *MusigSessionManager) MuSig2CreateSession(bipVersion MuSig2Version, // // We'll use just all zeroes as the session ID for the mutex, as this // is a "global" action. - m.sessionMtx.Lock(MuSig2SessionID{}) - m.musig2Sessions[session.SessionID] = session - m.sessionMtx.Unlock(MuSig2SessionID{}) + m.musig2Sessions.Store(session.SessionID, session) return &session.MuSig2SessionInfo, nil } @@ -157,7 +158,7 @@ func (m *MusigSessionManager) MuSig2Sign(sessionID MuSig2SessionID, m.sessionMtx.Lock(sessionID) defer m.sessionMtx.Unlock(sessionID) - session, ok := m.musig2Sessions[sessionID] + session, ok := m.musig2Sessions.Load(sessionID) if !ok { return nil, fmt.Errorf("session with ID %x not found", sessionID[:]) @@ -178,7 +179,7 @@ func (m *MusigSessionManager) MuSig2Sign(sessionID MuSig2SessionID, // Clean up our local state if requested. if cleanUp { - delete(m.musig2Sessions, sessionID) + m.musig2Sessions.Delete(sessionID) } return partialSig, nil @@ -198,7 +199,7 @@ func (m *MusigSessionManager) MuSig2CombineSig(sessionID MuSig2SessionID, m.sessionMtx.Lock(sessionID) defer m.sessionMtx.Unlock(sessionID) - session, ok := m.musig2Sessions[sessionID] + session, ok := m.musig2Sessions.Load(sessionID) if !ok { return nil, false, fmt.Errorf("session with ID %x not found", sessionID[:]) @@ -231,7 +232,7 @@ func (m *MusigSessionManager) MuSig2CombineSig(sessionID MuSig2SessionID, // there is nothing more left to do. if session.HaveAllSigs { finalSig = session.session.FinalSig() - delete(m.musig2Sessions, sessionID) + m.musig2Sessions.Delete(sessionID) } return finalSig, session.HaveAllSigs, nil @@ -245,12 +246,12 @@ func (m *MusigSessionManager) MuSig2Cleanup(sessionID MuSig2SessionID) error { m.sessionMtx.Lock(sessionID) defer m.sessionMtx.Unlock(sessionID) - _, ok := m.musig2Sessions[sessionID] + _, ok := m.musig2Sessions.Load(sessionID) if !ok { return fmt.Errorf("session with ID %x not found", sessionID[:]) } - delete(m.musig2Sessions, sessionID) + m.musig2Sessions.Delete(sessionID) return nil } @@ -267,7 +268,7 @@ func (m *MusigSessionManager) MuSig2RegisterNonces(sessionID MuSig2SessionID, m.sessionMtx.Lock(sessionID) defer m.sessionMtx.Unlock(sessionID) - session, ok := m.musig2Sessions[sessionID] + session, ok := m.musig2Sessions.Load(sessionID) if !ok { return false, fmt.Errorf("session with ID %x not found", sessionID[:]) diff --git a/lntest/harness.go b/lntest/harness.go index 5915abed73..29aba13b9e 100644 --- a/lntest/harness.go +++ b/lntest/harness.go @@ -495,7 +495,8 @@ func (h *HarnessTest) Shutdown(node *node.HarnessNode) { return h.manager.shutdownNode(node) }, DefaultTimeout) - require.NoErrorf(h, err, "unable to shutdown %v", node.Name()) + require.NoErrorf(h, err, "unable to shutdown %v in %v", node.Name(), + h.manager.currentTestCase) } // SuspendNode stops the given node and returns a callback that can be used to diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 98fb3abdc9..7c383d8d23 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -904,6 +904,8 @@ func (lc *LightningChannel) extractPayDescs(commitHeight uint64, // persist state w.r.t to if forwarded or not, or can // inadvertently trigger replays + htlc := htlc + payDesc, err := lc.diskHtlcToPayDesc( feeRate, commitHeight, &htlc, localCommitKeys, remoteCommitKeys, diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index 1ea726c630..b52cd9f660 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -10112,3 +10112,101 @@ func testNewBreachRetribution(t *testing.T, chanType channeldb.ChannelType) { ) require.ErrorIs(t, err, channeldb.ErrLogEntryNotFound) } + +// TestExtractPayDescs asserts that `extractPayDescs` can correctly turn a +// slice of htlcs into two slices of PaymentDescriptors. +func TestExtractPayDescs(t *testing.T) { + t.Parallel() + + // Create a testing LightningChannel. + lnChan, _, err := CreateTestChannels( + t, channeldb.SingleFunderTweaklessBit, + ) + require.NoError(t, err) + + // Create two incoming HTLCs. + incomings := []channeldb.HTLC{ + createRandomHTLC(t, true), + createRandomHTLC(t, true), + } + + // Create two outgoing HTLCs. + outgoings := []channeldb.HTLC{ + createRandomHTLC(t, false), + createRandomHTLC(t, false), + } + + // Concatenate incomings and outgoings into a single slice. + htlcs := []channeldb.HTLC{} + htlcs = append(htlcs, incomings...) + htlcs = append(htlcs, outgoings...) + + // Run the method under test. + // + // NOTE: we use nil commitment key rings to avoid checking the htlc + // scripts(`genHtlcScript`) as it should be tested independently. + incomingPDs, outgoingPDs, err := lnChan.extractPayDescs( + 0, 0, htlcs, nil, nil, true, + ) + require.NoError(t, err) + + // Assert the incoming PaymentDescriptors are matched. + for i, pd := range incomingPDs { + htlc := incomings[i] + assertPayDescMatchHTLC(t, pd, htlc) + } + + // Assert the outgoing PaymentDescriptors are matched. + for i, pd := range outgoingPDs { + htlc := outgoings[i] + assertPayDescMatchHTLC(t, pd, htlc) + } +} + +// assertPayDescMatchHTLC compares a PaymentDescriptor to a channeldb.HTLC and +// asserts that the fields are matched. +func assertPayDescMatchHTLC(t *testing.T, pd PaymentDescriptor, + htlc channeldb.HTLC) { + + require := require.New(t) + + require.EqualValues(htlc.RHash, pd.RHash, "RHash") + require.Equal(htlc.RefundTimeout, pd.Timeout, "Timeout") + require.Equal(htlc.Amt, pd.Amount, "Amount") + require.Equal(htlc.HtlcIndex, pd.HtlcIndex, "HtlcIndex") + require.Equal(htlc.LogIndex, pd.LogIndex, "LogIndex") + require.EqualValues(htlc.OnionBlob[:], pd.OnionBlob, "OnionBlob") +} + +// createRandomHTLC creates an HTLC that has random value in every field except +// the `Incoming`. +func createRandomHTLC(t *testing.T, incoming bool) channeldb.HTLC { + var onionBlob [lnwire.OnionPacketSize]byte + _, err := rand.Read(onionBlob[:]) + require.NoError(t, err) + + var rHash [lntypes.HashSize]byte + _, err = rand.Read(rHash[:]) + require.NoError(t, err) + + sig := make([]byte, 64) + _, err = rand.Read(sig) + require.NoError(t, err) + + extra := make([]byte, 1000) + _, err = rand.Read(extra) + require.NoError(t, err) + + return channeldb.HTLC{ + Signature: sig, + RHash: rHash, + Amt: lnwire.MilliSatoshi(rand.Uint64()), + RefundTimeout: rand.Uint32(), + OutputIndex: rand.Int31n(1000), + Incoming: incoming, + OnionBlob: onionBlob, + HtlcIndex: rand.Uint64(), + LogIndex: rand.Uint64(), + ExtraData: extra, + } +} diff --git a/peer/brontide.go b/peer/brontide.go index 46954e9cd8..ecb3d4d590 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -1941,7 +1941,8 @@ func messageSummary(msg lnwire.Message) string { msg.ChanID, int64(msg.FeePerKw)) case *lnwire.ChannelReestablish: - return fmt.Sprintf("next_local_height=%v, remote_tail_height=%v", + return fmt.Sprintf("chan_id=%v, next_local_height=%v, "+ + "remote_tail_height=%v", msg.ChanID, msg.NextLocalCommitHeight, msg.RemoteCommitTailHeight) case *lnwire.ReplyShortChanIDsEnd: diff --git a/routing/router.go b/routing/router.go index 0b09003f19..5b52cfb8f0 100644 --- a/routing/router.go +++ b/routing/router.go @@ -1552,7 +1552,7 @@ func (r *ChannelRouter) processUpdate(msg interface{}, // to obtain the full funding outpoint that's encoded within // the channel ID. channelID := lnwire.NewShortChanIDFromInt(msg.ChannelID) - fundingTx, err := r.fetchFundingTx(&channelID) + fundingTx, err := r.fetchFundingTxWrapper(&channelID) if err != nil { // In order to ensure we don't erroneously mark a // channel as a zombie due to an RPC failure, we'll @@ -1762,6 +1762,36 @@ func (r *ChannelRouter) processUpdate(msg interface{}, return nil } +// fetchFundingTxWrapper is a wrapper around fetchFundingTx, except that it +// will exit if the router has stopped. +func (r *ChannelRouter) fetchFundingTxWrapper(chanID *lnwire.ShortChannelID) ( + *wire.MsgTx, error) { + + txChan := make(chan *wire.MsgTx, 1) + errChan := make(chan error, 1) + + go func() { + tx, err := r.fetchFundingTx(chanID) + if err != nil { + errChan <- err + return + } + + txChan <- tx + }() + + select { + case tx := <-txChan: + return tx, nil + + case err := <-errChan: + return nil, err + + case <-r.quit: + return nil, ErrRouterShuttingDown + } +} + // fetchFundingTx returns the funding transaction identified by the passed // short channel ID. //