From 774bfa740afef1c773985a926cfbc6487ba21223 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Mon, 7 Oct 2024 13:59:34 -0400 Subject: [PATCH] htlcswitch: relay experimental endorsement signal with update_add_htlc --- htlcswitch/link.go | 66 +++++++++++++++++++++++++++ htlcswitch/link_test.go | 4 ++ htlcswitch/test_utils.go | 2 + itest/lnd_forward_interceptor_test.go | 5 +- itest/lnd_invoice_acceptor_test.go | 12 +++-- lntest/utils.go | 12 +++++ lnwire/update_add_htlc.go | 28 ++++++++++-- peer/brontide.go | 5 ++ server.go | 11 +++++ 9 files changed, 134 insertions(+), 11 deletions(-) diff --git a/htlcswitch/link.go b/htlcswitch/link.go index 029198edf..344bf77a4 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -30,6 +30,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/queue" + "github.com/lightningnetwork/lnd/record" "github.com/lightningnetwork/lnd/ticker" "github.com/lightningnetwork/lnd/tlv" ) @@ -285,6 +286,10 @@ type ChannelLinkConfig struct { // MaxFeeExposure is the threshold in milli-satoshis after which we'll // restrict the flow of HTLCs and fee updates. MaxFeeExposure lnwire.MilliSatoshi + + // ShouldFwdExpEndorsement is a closure that indicates whether the link + // should forward experimental endorsement signals. + ShouldFwdExpEndorsement func() bool } // channelLink is the service which drives a channel's commitment update @@ -3651,6 +3656,13 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg) { continue } + endorseValue := l.experimentalEndorsement( + record.CustomSet(add.CustomRecords), + ) + endorseType := uint64( + lnwire.ExperimentalEndorsementType, + ) + switch fwdPkg.State { case channeldb.FwdStateProcessed: // This add was not forwarded on the previous @@ -3672,6 +3684,14 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg) { BlindingPoint: fwdInfo.NextBlinding, } + endorseValue.WhenSome(func(e byte) { + custRecords := map[uint64][]byte{ + endorseType: {e}, + } + + outgoingAdd.CustomRecords = custRecords + }) + // Finally, we'll encode the onion packet for // the _next_ hop using the hop iterator // decoded for the current hop. @@ -3722,6 +3742,12 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg) { BlindingPoint: fwdInfo.NextBlinding, } + endorseValue.WhenSome(func(e byte) { + addMsg.CustomRecords = map[uint64][]byte{ + endorseType: {e}, + } + }) + // Finally, we'll encode the onion packet for the // _next_ hop using the hop iterator decoded for the // current hop. @@ -3809,6 +3835,46 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg) { l.forwardBatch(replay, switchPackets...) } +// experimentalEndorsement returns the value to set for our outgoing +// experimental endorsement field, and a boolean indicating whether it should +// be populated on the outgoing htlc. +func (l *channelLink) experimentalEndorsement( + customUpdateAdd record.CustomSet) fn.Option[byte] { + + // Only relay experimental signal if we are within the experiment + // period. + if !l.cfg.ShouldFwdExpEndorsement() { + return fn.None[byte]() + } + + // If we don't have any custom records or the experimental field is + // not set, just forward a zero value. + if len(customUpdateAdd) == 0 { + return fn.Some[byte](lnwire.ExperimentalUnendorsed) + } + + t := uint64(lnwire.ExperimentalEndorsementType) + value, set := customUpdateAdd[t] + if !set { + return fn.Some[byte](lnwire.ExperimentalUnendorsed) + } + + // We expect at least one byte for this field, consider it invalid if + // it has no data and just forward a zero value. + if len(value) == 0 { + return fn.Some[byte](lnwire.ExperimentalUnendorsed) + } + + // Only forward endorsed if the incoming link is endorsed. + if value[0] == lnwire.ExperimentalEndorsed { + return fn.Some[byte](lnwire.ExperimentalEndorsed) + } + + // Forward as unendorsed otherwise, including cases where we've + // received an invalid value that uses more than 3 bits of information. + return fn.Some[byte](lnwire.ExperimentalUnendorsed) +} + // processExitHop handles an htlc for which this link is the exit hop. It // returns a boolean indicating whether the commitment tx needs an update. func (l *channelLink) processExitHop(add lnwire.UpdateAddHTLC, diff --git a/htlcswitch/link_test.go b/htlcswitch/link_test.go index 78bb99d04..c72a25538 100644 --- a/htlcswitch/link_test.go +++ b/htlcswitch/link_test.go @@ -2245,6 +2245,7 @@ func newSingleLinkTestHarness(t *testing.T, chanAmt, NotifyInactiveLinkEvent: func(wire.OutPoint) {}, HtlcNotifier: aliceSwitch.cfg.HtlcNotifier, GetAliases: getAliases, + ShouldFwdExpEndorsement: func() bool { return true }, } aliceLink := NewChannelLink(aliceCfg, aliceLc.channel) @@ -4888,6 +4889,8 @@ func (h *persistentLinkHarness) restartLink( // Instantiate with a long interval, so that we can precisely control // the firing via force feeding. bticker := ticker.NewForce(time.Hour) + + //nolint:lll aliceCfg := ChannelLinkConfig{ FwrdingPolicy: globalPolicy, Peer: alicePeer, @@ -4932,6 +4935,7 @@ func (h *persistentLinkHarness) restartLink( HtlcNotifier: h.hSwitch.cfg.HtlcNotifier, SyncStates: syncStates, GetAliases: getAliases, + ShouldFwdExpEndorsement: func() bool { return true }, } aliceLink := NewChannelLink(aliceCfg, aliceChannel) diff --git a/htlcswitch/test_utils.go b/htlcswitch/test_utils.go index 3719d7ae4..cdb4f1f4e 100644 --- a/htlcswitch/test_utils.go +++ b/htlcswitch/test_utils.go @@ -1154,6 +1154,7 @@ func (h *hopNetwork) createChannelLink(server, peer *mockServer, return server.htlcSwitch.ForwardPackets(linkQuit, packets...) } + //nolint:lll link := NewChannelLink( ChannelLinkConfig{ BestHeight: server.htlcSwitch.BestHeight, @@ -1193,6 +1194,7 @@ func (h *hopNetwork) createChannelLink(server, peer *mockServer, NotifyInactiveLinkEvent: func(wire.OutPoint) {}, HtlcNotifier: server.htlcSwitch.cfg.HtlcNotifier, GetAliases: getAliases, + ShouldFwdExpEndorsement: func() bool { return true }, }, channel, ) diff --git a/itest/lnd_forward_interceptor_test.go b/itest/lnd_forward_interceptor_test.go index 9bbecd31b..b7ddf5814 100644 --- a/itest/lnd_forward_interceptor_test.go +++ b/itest/lnd_forward_interceptor_test.go @@ -530,9 +530,10 @@ func testForwardInterceptorWireRecords(ht *lntest.HarnessTest) { require.NoError(ht, err, "failed to send request") // Assert that the Alice -> Bob custom records in update_add_htlc are - // not propagated on the Bob -> Carol link. + // not propagated on the Bob -> Carol link, just an endorsement signal. packet = ht.ReceiveHtlcInterceptor(carolInterceptor) - require.Len(ht, packet.InWireCustomRecords, 0) + require.Equal(ht, lntest.CustomRecordsWithUnendorsed(nil), + packet.InWireCustomRecords) // We're going to tell Carol to forward 5k sats less to Dave. We need to // set custom records on the HTLC as well, to make sure the HTLC isn't diff --git a/itest/lnd_invoice_acceptor_test.go b/itest/lnd_invoice_acceptor_test.go index 97d14650c..60f6615ce 100644 --- a/itest/lnd_invoice_acceptor_test.go +++ b/itest/lnd_invoice_acceptor_test.go @@ -102,9 +102,12 @@ func testInvoiceHtlcModifierBasic(ht *lntest.HarnessTest) { require.EqualValues( ht, tc.sendAmountMsat, modifierRequest.ExitHtlcAmt, ) + + // Expect custom records plus endorsement signal. require.Equal( - ht, tc.lastHopCustomRecords, - modifierRequest.ExitHtlcWireCustomRecords, + ht, lntest.CustomRecordsWithUnendorsed( + tc.lastHopCustomRecords, + ), modifierRequest.ExitHtlcWireCustomRecords, ) // For all other packets we resolve according to the test case. @@ -140,8 +143,9 @@ func testInvoiceHtlcModifierBasic(ht *lntest.HarnessTest) { require.Len(ht, updatedInvoice.Htlcs, 1) require.Equal( - ht, tc.lastHopCustomRecords, - updatedInvoice.Htlcs[0].CustomRecords, + ht, lntest.CustomRecordsWithUnendorsed( + tc.lastHopCustomRecords, + ), updatedInvoice.Htlcs[0].CustomRecords, ) // Make sure the custom channel data contains the encoded diff --git a/lntest/utils.go b/lntest/utils.go index d4ca705c3..feaae57e7 100644 --- a/lntest/utils.go +++ b/lntest/utils.go @@ -282,3 +282,15 @@ func CalcStaticFeeBuffer(c lnrpc.CommitmentType, numHTLCs int) btcutil.Amount { return feeBuffer.ToSatoshis() } + +// CustomRecordsWithUnendorsed copies the map of custom records and adds an +// endorsed signal (replacing in the case of conflict) for assertion in tests. +func CustomRecordsWithUnendorsed( + originalRecords lnwire.CustomRecords) map[uint64][]byte { + + return originalRecords.MergedCopy(map[uint64][]byte{ + uint64(lnwire.ExperimentalEndorsementType): { + lnwire.ExperimentalUnendorsed, + }}, + ) +} diff --git a/lnwire/update_add_htlc.go b/lnwire/update_add_htlc.go index 0a377e710..5251748f0 100644 --- a/lnwire/update_add_htlc.go +++ b/lnwire/update_add_htlc.go @@ -8,11 +8,29 @@ import ( "github.com/lightningnetwork/lnd/tlv" ) -// OnionPacketSize is the size of the serialized Sphinx onion packet included -// in each UpdateAddHTLC message. The breakdown of the onion packet is as -// follows: 1-byte version, 33-byte ephemeral public key (for ECDH), 1300-bytes -// of per-hop data, and a 32-byte HMAC over the entire packet. -const OnionPacketSize = 1366 +const ( + // OnionPacketSize is the size of the serialized Sphinx onion packet + // included in each UpdateAddHTLC message. The breakdown of the onion + // packet is as follows: 1-byte version, 33-byte ephemeral public key + // (for ECDH), 1300-bytes of per-hop data, and a 32-byte HMAC over the + // entire packet. + OnionPacketSize = 1366 + + // ExperimentalEndorsementType is the TLV type used for a custom + // record that sets an experimental endorsement value. + ExperimentalEndorsementType tlv.Type = 106823 + + // ExperimentalUnendorsed is the value that the experimental endorsement + // field contains when a htlc is not endorsed. + ExperimentalUnendorsed = 0 + + // ExperimentalEndorsed is the value that the experimental endorsement + // field contains when a htlc is endorsed. We're using a single byte + // to represent our endorsement value, but limit the value to using + // the first three bits (max value = 00000111). Interpreted as a uint8 + // (an alias for byte in go), we can just define this constant as 7. + ExperimentalEndorsed = 7 +) type ( // BlindingPointTlvType is the type for ephemeral pubkeys used in diff --git a/peer/brontide.go b/peer/brontide.go index 50d511101..17bef3234 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -424,6 +424,10 @@ type Config struct { // used to modify the way the co-op close transaction is constructed. AuxChanCloser fn.Option[chancloser.AuxChanCloser] + // ShouldFwdExpEndorsement is a closure that indicates whether + // experimental endorsement signals should be set. + ShouldFwdExpEndorsement func() bool + // Quit is the server's quit channel. If this is closed, we halt operation. Quit chan struct{} } @@ -1319,6 +1323,7 @@ func (p *Brontide) addLink(chanPoint *wire.OutPoint, PreviouslySentShutdown: shutdownMsg, DisallowRouteBlinding: p.cfg.DisallowRouteBlinding, MaxFeeExposure: p.cfg.MaxFeeExposure, + ShouldFwdExpEndorsement: p.cfg.ShouldFwdExpEndorsement, } // Before adding our new link, purge the switch of any pending or live diff --git a/server.go b/server.go index ec19d0913..a66609332 100644 --- a/server.go +++ b/server.go @@ -133,6 +133,12 @@ var ( // // TODO(roasbeef): add command line param to modify. MaxFundingAmount = funding.MaxBtcFundingAmount + + // EndorsementExperimentEnd is the time after which nodes should stop + // propagating experimental endorsement signals. + // + // Per blip04: January 1, 2026 12:00:00 AM UTC in unix seconds. + EndorsementExperimentEnd = time.Unix(1767225600, 0) ) // errPeerAlreadyConnected is an error returned by the server when we're @@ -4214,6 +4220,11 @@ func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq, MsgRouter: s.implCfg.MsgRouter, AuxChanCloser: s.implCfg.AuxChanCloser, AuxResolver: s.implCfg.AuxContractResolver, + ShouldFwdExpEndorsement: func() bool { + return clock.NewDefaultClock().Now().Before( + EndorsementExperimentEnd, + ) + }, } copy(pCfg.PubKeyBytes[:], peerAddr.IdentityKey.SerializeCompressed())