From 774bfa740afef1c773985a926cfbc6487ba21223 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Mon, 7 Oct 2024 13:59:34 -0400 Subject: [PATCH 1/5] 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 029198edfb..344bf77a4a 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 78bb99d04e..c72a255384 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 3719d7ae4c..cdb4f1f4ea 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 9bbecd31b3..b7ddf58141 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 97d14650c1..60f6615ce2 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 d4ca705c31..feaae57e7b 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 0a377e710f..5251748f0b 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 50d5111016..17bef3234e 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 ec19d0913e..a666093321 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()) From 4bb5b0c27c729bd21c129a530b48d3324b6f69d1 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Thu, 31 Oct 2024 15:56:03 -0400 Subject: [PATCH 2/5] lnrpc: set a zero value endorsement signal on sender outgoing htlc Before we have sufficient signaling in the network to relay this signal, set a zero value experimental endorsement value on the sender's outgoing htlc. Once the network is relaying this signal and a flag day has been set, we'll be able to set a non-zero value here. --- itest/lnd_forward_interceptor_test.go | 34 ++++++++++++++------------- lnrpc/routerrpc/router_backend.go | 21 +++++++++++++++++ rpcserver.go | 6 +++++ 3 files changed, 45 insertions(+), 16 deletions(-) diff --git a/itest/lnd_forward_interceptor_test.go b/itest/lnd_forward_interceptor_test.go index b7ddf58141..0edf1e1098 100644 --- a/itest/lnd_forward_interceptor_test.go +++ b/itest/lnd_forward_interceptor_test.go @@ -515,12 +515,9 @@ func testForwardInterceptorWireRecords(ht *lntest.HarnessTest) { // all intercepted packets. These packets are held to simulate a // pending payment. packet := ht.ReceiveHtlcInterceptor(bobInterceptor) - - require.Len(ht, packet.InWireCustomRecords, 1) - - val, ok := packet.InWireCustomRecords[65537] - require.True(ht, ok, "expected custom record") - require.Equal(ht, []byte("test"), val) + require.Equal(ht, lntest.CustomRecordsWithUnendorsed( + customRecords, + ), packet.InWireCustomRecords) // Just resume the payment on Bob. err := bobInterceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{ @@ -569,7 +566,9 @@ func testForwardInterceptorWireRecords(ht *lntest.HarnessTest) { func(p *lnrpc.Payment) error { recordsEqual := reflect.DeepEqual( p.FirstHopCustomRecords, - sendReq.FirstHopCustomRecords, + lntest.CustomRecordsWithUnendorsed( + customRecords, + ), ) if !recordsEqual { return fmt.Errorf("expected custom records to "+ @@ -643,9 +642,9 @@ func testForwardInterceptorRestart(ht *lntest.HarnessTest) { // all intercepted packets. These packets are held to simulate a // pending payment. packet := ht.ReceiveHtlcInterceptor(bobInterceptor) - - require.Len(ht, packet.InWireCustomRecords, 1) - require.Equal(ht, customRecords, packet.InWireCustomRecords) + require.Equal(ht, lntest.CustomRecordsWithUnendorsed( + customRecords, + ), packet.InWireCustomRecords) // We accept the payment at Bob and resume it, so it gets to Carol. // This means the HTLC should now be fully locked in on Alice's side and @@ -681,8 +680,9 @@ func testForwardInterceptorRestart(ht *lntest.HarnessTest) { // We should get another notification about the held HTLC. packet = ht.ReceiveHtlcInterceptor(bobInterceptor) - require.Len(ht, packet.InWireCustomRecords, 1) - require.Equal(ht, customRecords, packet.InWireCustomRecords) + require.Len(ht, packet.InWireCustomRecords, 2) + require.Equal(ht, lntest.CustomRecordsWithUnendorsed(customRecords), + packet.InWireCustomRecords) err = carolInterceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{ IncomingCircuitKey: packet.IncomingCircuitKey, @@ -690,9 +690,10 @@ func testForwardInterceptorRestart(ht *lntest.HarnessTest) { }) require.NoError(ht, err, "failed to send request") - // And now we forward the payment at Carol. + // And now we forward the payment at Carol, expecting only an + // endorsement signal in our incoming custom records. packet = ht.ReceiveHtlcInterceptor(carolInterceptor) - require.Len(ht, packet.InWireCustomRecords, 0) + require.Len(ht, packet.InWireCustomRecords, 1) err = carolInterceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{ IncomingCircuitKey: packet.IncomingCircuitKey, Action: actionResume, @@ -704,8 +705,9 @@ func testForwardInterceptorRestart(ht *lntest.HarnessTest) { alice, preimage, lnrpc.Payment_SUCCEEDED, func(p *lnrpc.Payment) error { recordsEqual := reflect.DeepEqual( - p.FirstHopCustomRecords, - sendReq.FirstHopCustomRecords, + lntest.CustomRecordsWithUnendorsed( + sendReq.FirstHopCustomRecords, + ), p.FirstHopCustomRecords, ) if !recordsEqual { return fmt.Errorf("expected custom records to "+ diff --git a/lnrpc/routerrpc/router_backend.go b/lnrpc/routerrpc/router_backend.go index f2d21750ae..9421e991b6 100644 --- a/lnrpc/routerrpc/router_backend.go +++ b/lnrpc/routerrpc/router_backend.go @@ -110,6 +110,10 @@ type RouterBackend struct { // ParseCustomChannelData is a function that can be used to parse custom // channel data from the first hop of a route. ParseCustomChannelData func(message proto.Message) error + + // ShouldSetExpEndorsement returns a boolean indicating whether the + // experimental endorsement bit should be set. + ShouldSetExpEndorsement func() bool } // MissionControl defines the mission control dependencies of routerrpc. @@ -891,6 +895,23 @@ func (r *RouterBackend) extractIntentFromSendRequest( } payIntent.FirstHopCustomRecords = firstHopRecords + // If the experimental endorsement signal is not already set, propagate + // a zero value field if configured to set this signal. + if r.ShouldSetExpEndorsement() { + if payIntent.FirstHopCustomRecords == nil { + payIntent.FirstHopCustomRecords = make( + map[uint64][]byte, + ) + } + + t := uint64(lnwire.ExperimentalEndorsementType) + if _, set := payIntent.FirstHopCustomRecords[t]; !set { + payIntent.FirstHopCustomRecords[t] = []byte{ + lnwire.ExperimentalUnendorsed, + } + } + } + payIntent.PayAttemptTimeout = time.Second * time.Duration(rpcPayReq.TimeoutSeconds) diff --git a/rpcserver.go b/rpcserver.go index 7e016e056c..60433be249 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -44,6 +44,7 @@ import ( "github.com/lightningnetwork/lnd/channeldb/graphsession" "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/channelnotifier" + "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/contractcourt" "github.com/lightningnetwork/lnd/discovery" "github.com/lightningnetwork/lnd/feature" @@ -758,6 +759,11 @@ func (r *rpcServer) addDeps(s *server, macService *macaroons.Service, return nil }, + ShouldSetExpEndorsement: func() bool { + return clock.NewDefaultClock().Now().Before( + EndorsementExperimentEnd, + ) + }, } genInvoiceFeatures := func() *lnwire.FeatureVector { From f02bb58486c0f73589c04d192356a2b7ab167c79 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Wed, 22 May 2024 09:17:58 -0400 Subject: [PATCH 3/5] multi: add experimental endorsement feature bit and disable option --- feature/default_sets.go | 3 +++ feature/manager.go | 10 ++++++++++ lncfg/protocol.go | 9 +++++++++ lncfg/protocol_integration.go | 9 +++++++++ lnwire/features.go | 10 ++++++++++ rpcserver.go | 4 ++++ sample-lnd.conf | 3 +++ server.go | 31 ++++++++++++++++++------------- 8 files changed, 66 insertions(+), 13 deletions(-) diff --git a/feature/default_sets.go b/feature/default_sets.go index 4a9b2bf64d..616abc8ba3 100644 --- a/feature/default_sets.go +++ b/feature/default_sets.go @@ -96,4 +96,7 @@ var defaultSetDesc = setDesc{ SetInit: {}, // I SetNodeAnn: {}, // N }, + lnwire.ExperimentalEndorsementOptional: { + SetNodeAnn: {}, // N + }, } diff --git a/feature/manager.go b/feature/manager.go index 89f1d4b6bc..e0bcfc96bb 100644 --- a/feature/manager.go +++ b/feature/manager.go @@ -66,6 +66,10 @@ type Config struct { // NoTaprootOverlay unsets the taproot overlay channel feature bits. NoTaprootOverlay bool + // NoExperimentalEndorsement unsets any bits that signal support for + // forwarding experimental endorsement. + NoExperimentalEndorsement bool + // CustomFeatures is a set of custom features to advertise in each // set. CustomFeatures map[Set][]lnwire.FeatureBit @@ -199,6 +203,12 @@ func newManager(cfg Config, desc setDesc) (*Manager, error) { raw.Unset(lnwire.SimpleTaprootOverlayChansOptional) raw.Unset(lnwire.SimpleTaprootOverlayChansRequired) } + + if cfg.NoExperimentalEndorsement { + raw.Unset(lnwire.ExperimentalEndorsementOptional) + raw.Unset(lnwire.ExperimentalEndorsementRequired) + } + for _, custom := range cfg.CustomFeatures[set] { if custom > set.Maximum() { return nil, fmt.Errorf("feature bit: %v "+ diff --git a/lncfg/protocol.go b/lncfg/protocol.go index c670b18947..80809f49d6 100644 --- a/lncfg/protocol.go +++ b/lncfg/protocol.go @@ -67,6 +67,9 @@ type ProtocolOptions struct { // NoRouteBlindingOption disables forwarding of payments in blinded routes. NoRouteBlindingOption bool `long:"no-route-blinding" description:"do not forward payments that are a part of a blinded route"` + // NoExperimentalEndorsementOption disables experimental endorsement. + NoExperimentalEndorsementOption bool `long:"no-experimental-endorsement" description:"do not forward experimental endorsement signals"` + // CustomMessage allows the custom message APIs to handle messages with // the provided protocol numbers, which fall outside the custom message // number range. @@ -132,6 +135,12 @@ func (l *ProtocolOptions) NoRouteBlinding() bool { return l.NoRouteBlindingOption } +// NoExperimentalEndorsement returns true if experimental endorsement should +// be disabled. +func (l *ProtocolOptions) NoExperimentalEndorsement() bool { + return l.NoExperimentalEndorsementOption +} + // CustomMessageOverrides returns the set of protocol messages that we override // to allow custom handling. func (p ProtocolOptions) CustomMessageOverrides() []uint16 { diff --git a/lncfg/protocol_integration.go b/lncfg/protocol_integration.go index 5c5150a0ef..52cc658c3b 100644 --- a/lncfg/protocol_integration.go +++ b/lncfg/protocol_integration.go @@ -70,6 +70,9 @@ type ProtocolOptions struct { // NoRouteBlindingOption disables forwarding of payments in blinded routes. NoRouteBlindingOption bool `long:"no-route-blinding" description:"do not forward payments that are a part of a blinded route"` + // NoExperimentalEndorsementOption disables experimental endorsement. + NoExperimentalEndorsementOption bool `long:"no-experimental-endorsement" description:"do not forward experimental endorsement signals"` + // CustomMessage allows the custom message APIs to handle messages with // the provided protocol numbers, which fall outside the custom message // number range. @@ -127,6 +130,12 @@ func (l *ProtocolOptions) NoRouteBlinding() bool { return l.NoRouteBlindingOption } +// NoExperimentalEndorsement returns true if experimental endorsement should +// be disabled. +func (l *ProtocolOptions) NoExperimentalEndorsement() bool { + return l.NoExperimentalEndorsementOption +} + // CustomMessageOverrides returns the set of protocol messages that we override // to allow custom handling. func (l ProtocolOptions) CustomMessageOverrides() []uint16 { diff --git a/lnwire/features.go b/lnwire/features.go index c597b03988..bc6204f424 100644 --- a/lnwire/features.go +++ b/lnwire/features.go @@ -263,6 +263,14 @@ const ( // being finalized. SimpleTaprootChannelsOptionalStaging = 181 + // ExperimentalEndorsementRequired is a required feature bit that + // indicates that the node will relay experimental endorsement signals. + ExperimentalEndorsementRequired FeatureBit = 260 + + // ExperimentalEndorsementOptional is an optional feature bit that + // indicates that the node will relay experimental endorsement signals. + ExperimentalEndorsementOptional FeatureBit = 261 + // Bolt11BlindedPathsRequired is a required feature bit that indicates // that the node is able to understand the blinded path tagged field in // a BOLT 11 invoice. @@ -349,6 +357,8 @@ var Features = map[FeatureBit]string{ SimpleTaprootChannelsOptionalStaging: "simple-taproot-chans-x", SimpleTaprootOverlayChansOptional: "taproot-overlay-chans", SimpleTaprootOverlayChansRequired: "taproot-overlay-chans", + ExperimentalEndorsementRequired: "endorsement-x", + ExperimentalEndorsementOptional: "endorsement-x", Bolt11BlindedPathsOptional: "bolt-11-blinded-paths", Bolt11BlindedPathsRequired: "bolt-11-blinded-paths", } diff --git a/rpcserver.go b/rpcserver.go index 60433be249..4c593d8573 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -760,6 +760,10 @@ func (r *rpcServer) addDeps(s *server, macService *macaroons.Service, return nil }, ShouldSetExpEndorsement: func() bool { + if s.cfg.ProtocolOptions.NoExperimentalEndorsement() { + return false + } + return clock.NewDefaultClock().Now().Before( EndorsementExperimentEnd, ) diff --git a/sample-lnd.conf b/sample-lnd.conf index 6af5e4b578..4d3da37796 100644 --- a/sample-lnd.conf +++ b/sample-lnd.conf @@ -1399,6 +1399,9 @@ ; Set to disable blinded route forwarding. ; protocol.no-route-blinding=false +; Set to disable experimental endorsement signaling. +; protocol.no-experimental-endorsement=false + ; Set to handle messages of a particular type that falls outside of the ; custom message number range (i.e. 513 is onion messages). Note that you can ; set this option as many times as you want to support more than one custom diff --git a/server.go b/server.go index a666093321..6f263a4c21 100644 --- a/server.go +++ b/server.go @@ -573,19 +573,20 @@ func newServer(cfg *Config, listenAddrs []net.Addr, //nolint:lll featureMgr, err := feature.NewManager(feature.Config{ - NoTLVOnion: cfg.ProtocolOptions.LegacyOnion(), - NoStaticRemoteKey: cfg.ProtocolOptions.NoStaticRemoteKey(), - NoAnchors: cfg.ProtocolOptions.NoAnchorCommitments(), - NoWumbo: !cfg.ProtocolOptions.Wumbo(), - NoScriptEnforcementLease: cfg.ProtocolOptions.NoScriptEnforcementLease(), - NoKeysend: !cfg.AcceptKeySend, - NoOptionScidAlias: !cfg.ProtocolOptions.ScidAlias(), - NoZeroConf: !cfg.ProtocolOptions.ZeroConf(), - NoAnySegwit: cfg.ProtocolOptions.NoAnySegwit(), - CustomFeatures: cfg.ProtocolOptions.CustomFeatures(), - NoTaprootChans: !cfg.ProtocolOptions.TaprootChans, - NoTaprootOverlay: !cfg.ProtocolOptions.TaprootOverlayChans, - NoRouteBlinding: cfg.ProtocolOptions.NoRouteBlinding(), + NoTLVOnion: cfg.ProtocolOptions.LegacyOnion(), + NoStaticRemoteKey: cfg.ProtocolOptions.NoStaticRemoteKey(), + NoAnchors: cfg.ProtocolOptions.NoAnchorCommitments(), + NoWumbo: !cfg.ProtocolOptions.Wumbo(), + NoScriptEnforcementLease: cfg.ProtocolOptions.NoScriptEnforcementLease(), + NoKeysend: !cfg.AcceptKeySend, + NoOptionScidAlias: !cfg.ProtocolOptions.ScidAlias(), + NoZeroConf: !cfg.ProtocolOptions.ZeroConf(), + NoAnySegwit: cfg.ProtocolOptions.NoAnySegwit(), + CustomFeatures: cfg.ProtocolOptions.CustomFeatures(), + NoTaprootChans: !cfg.ProtocolOptions.TaprootChans, + NoTaprootOverlay: !cfg.ProtocolOptions.TaprootOverlayChans, + NoRouteBlinding: cfg.ProtocolOptions.NoRouteBlinding(), + NoExperimentalEndorsement: cfg.ProtocolOptions.NoExperimentalEndorsement(), }) if err != nil { return nil, err @@ -4221,6 +4222,10 @@ func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq, AuxChanCloser: s.implCfg.AuxChanCloser, AuxResolver: s.implCfg.AuxContractResolver, ShouldFwdExpEndorsement: func() bool { + if s.cfg.ProtocolOptions.NoExperimentalEndorsement() { + return false + } + return clock.NewDefaultClock().Now().Before( EndorsementExperimentEnd, ) From 7a876e8898c0270175c862dd7d78d7fbfed0e44d Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Thu, 31 Oct 2024 16:02:36 -0400 Subject: [PATCH 4/5] itest: add coverage for experimental endorsement --- itest/list_on_test.go | 4 + itest/lnd_experimental_endorsement.go | 124 ++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 itest/lnd_experimental_endorsement.go diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 68c923c3bb..7560fb16be 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -706,4 +706,8 @@ var allTestCases = []*lntest.TestCase{ Name: "debuglevel show", TestFunc: testDebuglevelShow, }, + { + Name: "experimental endorsement", + TestFunc: testExperimentalEndorsement, + }, } diff --git a/itest/lnd_experimental_endorsement.go b/itest/lnd_experimental_endorsement.go new file mode 100644 index 0000000000..6b89f1ada2 --- /dev/null +++ b/itest/lnd_experimental_endorsement.go @@ -0,0 +1,124 @@ +package itest + +import ( + "math" + + "github.com/btcsuite/btcd/btcutil" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" + "github.com/lightningnetwork/lnd/lntest" + "github.com/lightningnetwork/lnd/lntest/rpc" + "github.com/lightningnetwork/lnd/lntest/wait" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/stretchr/testify/require" +) + +// testExperimentalEndorsement tests setting of positive and negative +// experimental endorsement signals. +func testExperimentalEndorsement(ht *lntest.HarnessTest) { + testEndorsement(ht, true) + testEndorsement(ht, false) +} + +// testEndorsement sets up a 5 hop network and tests propagation of +// experimental endorsement signals. +func testEndorsement(ht *lntest.HarnessTest, aliceEndorse bool) { + alice, bob := ht.Alice, ht.Bob + carol := ht.NewNode( + "carol", []string{"--protocol.no-experimental-endorsement"}, + ) + dave := ht.NewNode("dave", nil) + eve := ht.NewNode("eve", nil) + + ht.EnsureConnected(alice, bob) + ht.EnsureConnected(bob, carol) + ht.EnsureConnected(carol, dave) + ht.EnsureConnected(dave, eve) + + ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) + ht.FundCoins(btcutil.SatoshiPerBitcoin, dave) + // Open and wait for channels. + const chanAmt = btcutil.Amount(300000) + p := lntest.OpenChannelParams{Amt: chanAmt} + reqs := []*lntest.OpenChannelRequest{ + {Local: alice, Remote: bob, Param: p}, + {Local: bob, Remote: carol, Param: p}, + {Local: carol, Remote: dave, Param: p}, + {Local: dave, Remote: eve, Param: p}, + } + resp := ht.OpenMultiChannelsAsync(reqs) + cpAB, cpBC, cpCD, cpDE := resp[0], resp[1], resp[2], resp[3] + + // Make sure Alice is aware of Bob=>Carol=>Dave=>Eve channels. + ht.AssertChannelInGraph(alice, cpBC) + ht.AssertChannelInGraph(alice, cpCD) + ht.AssertChannelInGraph(alice, cpDE) + + bobIntercept, cancelBob := bob.RPC.HtlcInterceptor() + defer cancelBob() + + carolIntercept, cancelCarol := carol.RPC.HtlcInterceptor() + defer cancelCarol() + + daveIntercept, cancelDave := dave.RPC.HtlcInterceptor() + defer cancelDave() + + req := &lnrpc.Invoice{ValueMsat: 1000} + addResponse := eve.RPC.AddInvoice(req) + invoice := eve.RPC.LookupInvoice(addResponse.RHash) + + sendReq := &routerrpc.SendPaymentRequest{ + PaymentRequest: invoice.PaymentRequest, + TimeoutSeconds: int32(wait.PaymentTimeout.Seconds()), + FeeLimitMsat: math.MaxInt64, + } + + expectedValue := []byte{lnwire.ExperimentalUnendorsed} + if aliceEndorse { + expectedValue = []byte{lnwire.ExperimentalEndorsed} + t := uint64(lnwire.ExperimentalEndorsementType) + sendReq.FirstHopCustomRecords = map[uint64][]byte{ + t: expectedValue, + } + } + + _ = alice.RPC.SendPayment(sendReq) + + // Validate that our signal (positive or zero) propagates until carol + // and then is dropped because she has disabled the feature. + validateEndorsedAndResume(ht, bobIntercept, true, expectedValue) + validateEndorsedAndResume(ht, carolIntercept, true, expectedValue) + validateEndorsedAndResume(ht, daveIntercept, false, nil) + + var preimage lntypes.Preimage + copy(preimage[:], invoice.RPreimage) + ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_SUCCEEDED) + + ht.CloseChannel(alice, cpAB) + ht.CloseChannel(bob, cpBC) + ht.CloseChannel(carol, cpCD) + ht.CloseChannel(dave, cpDE) +} + +func validateEndorsedAndResume(ht *lntest.HarnessTest, + interceptor rpc.InterceptorClient, hasEndorsement bool, + expectedValue []byte) { + + packet := ht.ReceiveHtlcInterceptor(interceptor) + + var expectedRecords map[uint64][]byte + if hasEndorsement { + u64Type := uint64(lnwire.ExperimentalEndorsementType) + expectedRecords = map[uint64][]byte{ + u64Type: expectedValue, + } + } + require.Equal(ht, expectedRecords, packet.InWireCustomRecords) + + err := interceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{ + IncomingCircuitKey: packet.IncomingCircuitKey, + Action: routerrpc.ResolveHoldForwardAction_RESUME, + }) + require.NoError(ht, err) +} From a8c159b1c71d60192bdf22560924b069c06a8df1 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Fri, 23 Aug 2024 10:05:58 -0400 Subject: [PATCH 5/5] docs: add experimental endorsement --- docs/release-notes/release-notes-0.19.0.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/release-notes/release-notes-0.19.0.md b/docs/release-notes/release-notes-0.19.0.md index 6fa3c0f063..8e43f0cd47 100644 --- a/docs/release-notes/release-notes-0.19.0.md +++ b/docs/release-notes/release-notes-0.19.0.md @@ -61,6 +61,12 @@ https://github.com/lightningnetwork/lnd/pull/9253) # New Features + +* [Support](https://github.com/lightningnetwork/lnd/pull/8390) for + [experimental endorsement](https://github.com/lightning/blips/pull/27) + signal relay was added. This signal has *no impact* on routing, and + is deployed experimentally to assist ongoing channel jamming research. + ## Functional Enhancements * [Add ability](https://github.com/lightningnetwork/lnd/pull/8998) to paginate wallet transactions. @@ -201,6 +207,7 @@ The underlying functionality between those two options remain the same. * Abdullahi Yunus * Animesh Bilthare * Boris Nagaev +* Carla Kirk-Cohen * CharlieZKSmith * Elle Mouton * George Tsagkarelis