diff --git a/contractcourt/htlc_incoming_contest_resolver.go b/contractcourt/htlc_incoming_contest_resolver.go index d075166c1..b452e0e5b 100644 --- a/contractcourt/htlc_incoming_contest_resolver.go +++ b/contractcourt/htlc_incoming_contest_resolver.go @@ -83,7 +83,7 @@ func (h *htlcIncomingContestResolver) processFinalHtlcFail() error { func (h *htlcIncomingContestResolver) invalidFinalHtlc( payload *hop.Payload, height uint32) bool { - if payload.FwdInfo.NextHop != hop.Exit { + if !payload.FwdInfo.IsExit() { return false } @@ -311,7 +311,7 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) { hodlChan <-chan interface{} witnessUpdates <-chan lntypes.Preimage ) - if payload.FwdInfo.NextHop == hop.Exit { + if payload.FwdInfo.IsExit() { // Create a buffered hodl chan to prevent deadlock. hodlQueue := queue.NewConcurrentQueue(10) hodlQueue.Start() @@ -700,7 +700,7 @@ func (h *htlcIncomingContestResolver) findAndapplyPreimage() (bool, error) { // Exit early if this is not the exit hop, which means we are not the // payment receiver and don't have the preimage. - if payload.FwdInfo.NextHop != hop.Exit { + if !payload.FwdInfo.IsExit() { return false, nil } diff --git a/docs/release-notes/release-notes-0.20.2.md b/docs/release-notes/release-notes-0.20.2.md index f7463c95f..e426b35df 100644 --- a/docs/release-notes/release-notes-0.20.2.md +++ b/docs/release-notes/release-notes-0.20.2.md @@ -42,6 +42,10 @@ ## RPC Additions +* The [HTLC interceptor](https://github.com/lightningnetwork/lnd/pull/10942) now + exposes the next hop of a blinded route that identifies it by node ID + (`next_node_id`) rather than by channel. + ## lncli Additions # Improvements @@ -60,6 +64,15 @@ ## RPC Updates +* `ForwardHtlcInterceptRequest.outgoing_requested_chan_id` now holds a reserved + sentinel value (`18446744073709551615`, all bits set) when the + [HTLC interceptor](https://github.com/lightningnetwork/lnd/pull/10942) reports + a blinded forward that identifies the next hop by node ID. The sender of such + a forward requests no channel, so a zero value here would make a client that + detects the exit hop by a zero channel ID classify the forward as a final + receive. Clients that switch on this field must handle the sentinel and read + `outgoing_requested_node_id` for the next hop. + ## lncli Updates ## Breaking Changes @@ -71,6 +84,13 @@ # Technical and Architectural Updates ## BOLT Spec Updates +* [Fixed an issue](https://github.com/lightningnetwork/lnd/pull/10942) where an + lnd node acting as a relaying node (including the introduction node) in a + blinded path failed to forward the payment when the next hop was identified by + node ID (`next_node_id`) rather than a short channel ID. The next hop's public + key is now resolved to one of our channels with that peer using non-strict + forwarding. + ## Testing ## Database diff --git a/htlcswitch/hop/forwarding_info.go b/htlcswitch/hop/forwarding_info.go index 539e0db1f..e550035a1 100644 --- a/htlcswitch/hop/forwarding_info.go +++ b/htlcswitch/hop/forwarding_info.go @@ -2,6 +2,7 @@ package hop import ( "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/lnwire" ) @@ -11,10 +12,14 @@ import ( // received within the incoming HTLC, to ensure that the prior hop didn't // tamper with the end-to-end routing information at all. type ForwardingInfo struct { - // NextHop is the channel ID of the next hop. The received HTLC should - // be forwarded to this particular channel in order to continue the - // end-to-end route. - NextHop lnwire.ShortChannelID + // NextHop identifies the next hop the HTLC should be forwarded to. In + // the common case it is a Left holding the short channel ID of the + // outgoing channel. For a blinded route whose recipient identifies the + // next hop by node ID (next_node_id) it is a Right holding the next + // node's compressed public key, which the switch's non-strict + // forwarding logic resolves to one of our channels with that peer. The + // zero value is a Left equal to hop.Exit, which denotes the exit hop. + NextHop fn.Either[lnwire.ShortChannelID, [33]byte] // AmountToForward is the amount of milli-satoshis that the receiving // node should forward to the next hop. @@ -35,6 +40,51 @@ type ForwardingInfo struct { PathID *chainhash.Hash } +// NewChannelNextHop returns a next-hop value that identifies the outgoing +// channel by its short channel ID, which is the common case. +func NewChannelNextHop( + scid lnwire.ShortChannelID) fn.Either[lnwire.ShortChannelID, [33]byte] { + + return fn.NewLeft[lnwire.ShortChannelID, [33]byte](scid) +} + +// NewNodeNextHop returns a next-hop value that identifies the next hop by the +// next node's compressed public key, as used by blinded routes that set +// next_node_id instead of a short channel ID. +func NewNodeNextHop( + nodeID [33]byte) fn.Either[lnwire.ShortChannelID, [33]byte] { + + return fn.NewRight[lnwire.ShortChannelID, [33]byte](nodeID) +} + +// IsExit returns true if this forwarding info denotes the exit hop, i.e. we are +// the final recipient of the HTLC. This is the case when the next hop is a +// short channel ID equal to hop.Exit. A node-ID next hop (used by some blinded +// routes) is always a forward, never the exit hop. +func (f ForwardingInfo) IsExit() bool { + var isExit bool + f.NextHop.WhenLeft(func(scid lnwire.ShortChannelID) { + isExit = scid == Exit + }) + + return isExit +} + +// NextHopChannel returns the short channel ID of the outgoing channel when the +// next hop is identified by channel ID (the common case). It returns None when +// the next hop is identified by node ID instead, in which case the outgoing +// channel is selected by the switch's non-strict forwarding. +func (f ForwardingInfo) NextHopChannel() fn.Option[lnwire.ShortChannelID] { + return f.NextHop.LeftToSome() +} + +// NextHopNode returns the next hop's compressed pubkey when it is identified by +// node ID (blinded routes via next_node_id), or None when identified by +// channel. +func (f ForwardingInfo) NextHopNode() fn.Option[[33]byte] { + return f.NextHop.RightToSome() +} + // FinalHtlcValidationResult describes the result of checking a final-hop // HTLC against the onion payload and supported final-hop CLTV range. type FinalHtlcValidationResult uint8 diff --git a/htlcswitch/hop/forwarding_info_test.go b/htlcswitch/hop/forwarding_info_test.go index 82a5ad0c6..3ca5fbe3d 100644 --- a/htlcswitch/hop/forwarding_info_test.go +++ b/htlcswitch/hop/forwarding_info_test.go @@ -3,6 +3,7 @@ package hop import ( "testing" + "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/lnwire" "github.com/stretchr/testify/require" ) @@ -21,7 +22,7 @@ func TestValidateFinalHtlc(t *testing.T) { fwdInfo := ForwardingInfo{ AmountToForward: amount, OutgoingCLTV: expiry, - NextHop: Exit, + NextHop: NewChannelNextHop(Exit), } testCases := []struct { @@ -115,7 +116,7 @@ func TestValidateFinalHtlc(t *testing.T) { fwdInfo: ForwardingInfo{ AmountToForward: amount, OutgoingCLTV: expiry + maxCltvDelta + 2, - NextHop: Exit, + NextHop: NewChannelNextHop(Exit), }, validateAmount: true, expected: FinalHtlcInvalidCltv, @@ -135,3 +136,41 @@ func TestValidateFinalHtlc(t *testing.T) { }) } } + +// TestForwardingInfoNextHop asserts the next-hop accessors for both the short +// channel ID (Left) and node ID (Right) representations, including the +// invariant that the zero-value ForwardingInfo denotes the exit hop. +func TestForwardingInfoNextHop(t *testing.T) { + t.Parallel() + + scid := lnwire.NewShortChanIDFromInt(12345) + nodeID := [33]byte{0x02} + + // The zero-value ForwardingInfo must denote the exit hop, since its + // NextHop is a Left equal to hop.Exit. Callers rely on this to detect + // that we are the final recipient. + zero := ForwardingInfo{} + require.True(t, zero.IsExit(), "zero value must be the exit hop") + require.Equal( + t, fn.Some(Exit), zero.NextHopChannel(), + "zero value must expose the Exit channel", + ) + + // An explicit channel next hop equal to Exit is likewise the exit hop. + exit := ForwardingInfo{NextHop: NewChannelNextHop(Exit)} + require.True(t, exit.IsExit()) + + // A channel next hop with a real SCID is a forward, and exposes that + // SCID through NextHopChannel. + channel := ForwardingInfo{NextHop: NewChannelNextHop(scid)} + require.False(t, channel.IsExit()) + require.Equal(t, fn.Some(scid), channel.NextHopChannel()) + + // A node-ID next hop is always a forward and never exposes an outgoing + // channel, since the switch selects one via non-strict forwarding. + node := ForwardingInfo{NextHop: NewNodeNextHop(nodeID)} + require.False(t, node.IsExit()) + require.Equal( + t, fn.None[lnwire.ShortChannelID](), node.NextHopChannel(), + ) +} diff --git a/htlcswitch/hop/fuzz_test.go b/htlcswitch/hop/fuzz_test.go index 525194c38..853292bac 100644 --- a/htlcswitch/hop/fuzz_test.go +++ b/htlcswitch/hop/fuzz_test.go @@ -92,7 +92,7 @@ func hopFromPayload(p *Payload) (*route.Hop, uint64) { BlindingPoint: p.blindingPoint, CustomRecords: p.customRecords, TotalAmtMsat: p.totalAmtMsat, - }, p.FwdInfo.NextHop.ToUint64() + }, p.FwdInfo.NextHop.UnwrapLeftOr(Exit).ToUint64() } // FuzzPayloadFinal fuzzes final hop payloads, providing the additional context diff --git a/htlcswitch/hop/iterator.go b/htlcswitch/hop/iterator.go index cf04b88a1..6ecd998fc 100644 --- a/htlcswitch/hop/iterator.go +++ b/htlcswitch/hop/iterator.go @@ -10,6 +10,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/chaincfg/chainhash" sphinx "github.com/lightningnetwork/lightning-onion" + "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/record" "github.com/lightningnetwork/lnd/tlv" @@ -231,6 +232,13 @@ func parseAndValidateRecipientData(r *sphinxHopIterator, payload *Payload, return nil, routeRole, err } + // BOLT 4 requires a blinded hop to set exactly one of short_channel_id + // or next_node_id. Reject a hop that sets both here. + if routeData.ShortChannelID.IsSome() && routeData.NextNodeID.IsSome() { + return nil, routeRole, fmt.Errorf("blinded hop sets both " + + "short channel ID and next node ID") + } + // This is the final node in the blinded route. if isFinal { return deriveBlindedRouteFinalHopForwardingInfo( @@ -318,14 +326,35 @@ func deriveBlindedRouteForwardingInfo(r *sphinxHopIterator, ) } - nextSCID, err := routeData.ShortChannelID.UnwrapOrErr( - fmt.Errorf("next SCID not set for non-final blinded hop"), - ) - if err != nil { - return nil, routeRole, err + // Determine the next hop. The recipient identifies it either by a short + // channel ID (the common case) or, as some implementations do for + // blinded routes, by the next node's ID (next_node_id). Setting both is + // already rejected upstream, and the dummy hop check above has handled + // a next_node_id that points at us. + var nextHop fn.Either[lnwire.ShortChannelID, [33]byte] + switch { + case routeData.ShortChannelID.IsSome(): + scid := routeData.ShortChannelID.UnwrapOr( + routeData.ShortChannelID.Zero(), + ) + nextHop = NewChannelNextHop(scid.Val) + + case routeData.NextNodeID.IsSome(): + nodeID := routeData.NextNodeID.UnwrapOr( + routeData.NextNodeID.Zero(), + ) + var pubKey [33]byte + copy(pubKey[:], nodeID.Val.SerializeCompressed()) + + nextHop = NewNodeNextHop(pubKey) + + default: + return nil, routeRole, fmt.Errorf("next hop not set for " + + "non-final blinded hop") } + payload.FwdInfo = ForwardingInfo{ - NextHop: nextSCID.Val, + NextHop: nextHop, AmountToForward: fwdAmt, OutgoingCLTV: r.blindingKit.IncomingCltv - uint32( relayInfo.Val.CltvExpiryDelta, diff --git a/htlcswitch/hop/iterator_test.go b/htlcswitch/hop/iterator_test.go index b132a046d..3d30faefc 100644 --- a/htlcswitch/hop/iterator_test.go +++ b/htlcswitch/hop/iterator_test.go @@ -9,6 +9,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/davecgh/go-spew/spew" sphinx "github.com/lightningnetwork/lightning-onion" + "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/record" "github.com/lightningnetwork/lnd/tlv" @@ -33,7 +34,9 @@ func TestSphinxHopIteratorForwardingInstructions(t *testing.T) { // extract each type, no matter the payload type. nextAddrInt := binary.BigEndian.Uint64(hopData.NextAddress[:]) expectedFwdInfo := ForwardingInfo{ - NextHop: lnwire.NewShortChanIDFromInt(nextAddrInt), + NextHop: NewChannelNextHop( + lnwire.NewShortChanIDFromInt(nextAddrInt), + ), AmountToForward: lnwire.MilliSatoshi(hopData.ForwardAmount), OutgoingCLTV: hopData.OutgoingCltv, } @@ -303,3 +306,378 @@ func TestParseAndValidateRecipientData(t *testing.T) { }) } } + +// TestDeriveBlindedRouteNextHop asserts how a non-final blinded hop's next hop +// is derived from the recipient data: a short channel ID becomes a Left, a +// next_node_id becomes a Right, having both set is rejected with an error, and +// the absence of both is also an error. +func TestDeriveBlindedRouteNextHop(t *testing.T) { + t.Parallel() + + nodeKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + nextNodeKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + nextNodePub := nextNodeKey.PubKey() + + var nextNodeRaw [33]byte + copy(nextNodeRaw[:], nextNodePub.SerializeCompressed()) + + scid := lnwire.NewShortChanIDFromInt(1500) + + relayInfo := tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType10]( + record.PaymentRelayInfo{ + CltvExpiryDelta: 10, + BaseFee: 100, + FeeRate: 0, + }, + )) + constraints := tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType12]( + record.PaymentConstraints{ + MaxCltvExpiry: 1000, + HtlcMinimumMsat: lnwire.MilliSatoshi(1), + }, + )) + scidRecord := tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType2](scid)) + nodeIDRecord := tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType4](nextNodePub), + ) + + tests := []struct { + name string + data *record.BlindedRouteData + expectedHop fn.Either[lnwire.ShortChannelID, [33]byte] + expectedErr string + }{ + { + name: "short channel id only", + data: &record.BlindedRouteData{ + ShortChannelID: scidRecord, + RelayInfo: relayInfo, + Constraints: constraints, + }, + expectedHop: NewChannelNextHop(scid), + }, + { + name: "next node id only", + data: &record.BlindedRouteData{ + NextNodeID: nodeIDRecord, + RelayInfo: relayInfo, + Constraints: constraints, + }, + expectedHop: NewNodeNextHop(nextNodeRaw), + }, + { + // BOLT 4 requires a non-final blinded hop to set + // exactly one of short_channel_id or next_node_id, so + // setting both must be rejected. + name: "both present is an error", + data: &record.BlindedRouteData{ + ShortChannelID: scidRecord, + NextNodeID: nodeIDRecord, + RelayInfo: relayInfo, + Constraints: constraints, + }, + expectedErr: "both short channel ID and next node ID", + }, + { + name: "neither present", + data: &record.BlindedRouteData{ + RelayInfo: relayInfo, + Constraints: constraints, + }, + expectedErr: "next hop not set", + }, + } + + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + data, err := record.EncodeBlindedRouteData( + testCase.data, + ) + require.NoError(t, err) + + kit := BlindingKit{ + Processor: &mockProcessor{}, + IncomingAmount: 10000, + IncomingCltv: 500, + UpdateAddBlinding: tlv.SomeRecordT( + //nolint:ll + tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](&btcec.PublicKey{}), + ), + } + iterator := &sphinxHopIterator{ + blindingKit: kit, + router: sphinx.NewRouter( + &sphinx.PrivKeyECDH{PrivKey: nodeKey}, + sphinx.NewMemoryReplayLog(), + ), + } + + payload, _, err := parseAndValidateRecipientData( + iterator, &Payload{encryptedData: data}, + false, RouteRoleCleartext, + ) + + if testCase.expectedErr != "" { + require.ErrorContains( + t, err, testCase.expectedErr, + ) + + return + } + + require.NoError(t, err) + require.Equal( + t, testCase.expectedHop, + payload.FwdInfo.NextHop, + ) + }) + } +} + +// TestBlindedHopBothNextHopFieldsRejected asserts that a blinded hop setting +// both short_channel_id and next_node_id is rejected for a final hop and for a +// dummy hop (next_node_id == our own pubkey), not just an intermediate hop. The +// mutual-exclusivity check runs before the final-hop and dummy-hop branches, so +// none of them accept a hop that violates BOLT 4. The intermediate case is +// already covered by TestDeriveBlindedRouteNextHop. +func TestBlindedHopBothNextHopFieldsRejected(t *testing.T) { + t.Parallel() + + nodeKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + nodePub := nodeKey.PubKey() + + // Route data that sets both short_channel_id and next_node_id. The node + // ID is our own pubkey, which for a non-final hop would otherwise + // signal a dummy hop; the both-set check must still fire first. + bothData := &record.BlindedRouteData{ + ShortChannelID: tlv.SomeRecordT( + tlv.NewRecordT[tlv.TlvType2]( + lnwire.NewShortChanIDFromInt(1500), + ), + ), + NextNodeID: tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType4](nodePub), + ), + RelayInfo: tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType10]( + record.PaymentRelayInfo{ + CltvExpiryDelta: 10, + BaseFee: 100, + FeeRate: 0, + }, + )), + Constraints: tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType12]( + record.PaymentConstraints{ + MaxCltvExpiry: 1000, + HtlcMinimumMsat: lnwire.MilliSatoshi(1), + }, + )), + } + data, err := record.EncodeBlindedRouteData(bothData) + require.NoError(t, err) + + // Both the dummy/forwarding path (isFinal=false, next_node_id points at + // us) and the final path (isFinal=true) must reject the hop. + for _, isFinal := range []bool{false, true} { + name := "forwarding hop" + if isFinal { + name = "final hop" + } + + t.Run(name, func(t *testing.T) { + kit := BlindingKit{ + Processor: &mockProcessor{}, + IncomingAmount: 10000, + IncomingCltv: 500, + UpdateAddBlinding: tlv.SomeRecordT( + //nolint:ll + tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](&btcec.PublicKey{}), + ), + } + iterator := &sphinxHopIterator{ + blindingKit: kit, + router: sphinx.NewRouter( + &sphinx.PrivKeyECDH{PrivKey: nodeKey}, + sphinx.NewMemoryReplayLog(), + ), + } + + _, _, err := parseAndValidateRecipientData( + iterator, &Payload{encryptedData: data}, + isFinal, RouteRoleCleartext, + ) + require.ErrorContains( + t, err, + "both short channel ID and next node ID", + ) + }) + } +} + +// TestBlindedRouteDummyHopPeeledLocally asserts that a blinded route hop where +// next_node_id is our own public key is recognized as a dummy hop and is peeled +// locally rather than falling through to the generic next_node_id forwarding +// branch. +func TestBlindedRouteDummyHopPeeledLocally(t *testing.T) { + t.Parallel() + + // Construct a realistic onion packet that contains a blinded final hop. + // We'll use this to test that we can peel a dummy hop locally and + // extract the forwarding information from the decrypted final hop's + // payload. + nodeKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + nodePub := nodeKey.PubKey() + + relayInfo := tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType10]( + record.PaymentRelayInfo{ + CltvExpiryDelta: 10, + BaseFee: 100, + FeeRate: 0, + }, + )) + constraints := tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType12]( + record.PaymentConstraints{ + MaxCltvExpiry: 1000, + HtlcMinimumMsat: lnwire.MilliSatoshi(1), + }, + )) + + // Set next_node_id to our own public key. This signals a dummy hop. + nodeIDRecord := tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType4](nodePub), + ) + + // We'll generate a valid, cryptographically blinded final hop's payload + // using sphinx.BuildBlindedPath. This contains the PathID. + secret := make([]byte, 32) + secret[0] = 2 + finalHopData := &record.BlindedRouteData{ + PathID: tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType6](secret), + ), + } + finalHopDataBytes, err := record.EncodeBlindedRouteData(finalHopData) + require.NoError(t, err) + + hopInfo := &sphinx.HopInfo{ + NodePub: nodePub, + PlainText: finalHopDataBytes, + } + + blindingSessionKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + blindedPathInfo, err := sphinx.BuildBlindedPath( + blindingSessionKey, []*sphinx.HopInfo{hopInfo}, + ) + require.NoError(t, err) + + // Since we are peeling a dummy hop locally, we want the next blinding + // override to be the blinding point generated for our blinded final + // hop. + dummyHopData := &record.BlindedRouteData{ + NextNodeID: nodeIDRecord, + RelayInfo: relayInfo, + Constraints: constraints, + NextBlindingOverride: tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType8]( + blindedPathInfo.Path.BlindingPoint, + ), + ), + } + + data, err := record.EncodeBlindedRouteData(dummyHopData) + require.NoError(t, err) + + // Encode a valid TLV payload for the next hop (which we will peel). + var hop2Buffer bytes.Buffer + amt := uint64(10000) + cltv := uint32(500) + encryptedDataRecord := record.NewEncryptedDataRecord( + &blindedPathInfo.Path.BlindedHops[0].CipherText, + ) + tlvRecords := []tlv.Record{ + record.NewAmtToFwdRecord(&amt), + record.NewLockTimeRecord(&cltv), + encryptedDataRecord, + } + tlvStream, err := tlv.NewStream(tlvRecords...) + require.NoError(t, err) + err = tlvStream.Encode(&hop2Buffer) + require.NoError(t, err) + + hopPayload, err := sphinx.NewTLVHopPayload(hop2Buffer.Bytes()) + require.NoError(t, err) + + // Create a valid 1-hop onion path using our blinded public key. + var paymentPath sphinx.PaymentPath + paymentPath[0] = sphinx.OnionHop{ + NodePub: *blindedPathInfo.Path.BlindedHops[0].BlindedNodePub, + HopPayload: hopPayload, + } + + sessionKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + rHash := [32]byte{1} + + // Generate a cryptographically valid onion packet for this path. + onionPacket, err := sphinx.NewOnionPacket( + &paymentPath, sessionKey, rHash[:], + sphinx.DeterministicPacketFiller, + ) + require.NoError(t, err) + + // Simulate an incoming HTLC with a blinding point and a valid onion + // packet. The blinding point is used to decrypt the dummy hop's + // payload, which contains the blinding point for the next hop (the + // blinded final hop). + kit := BlindingKit{ + Processor: &mockProcessor{}, + IncomingAmount: 12000, + IncomingCltv: 510, + UpdateAddBlinding: tlv.SomeRecordT( + tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType]( + nodePub, + ), + ), + } + + iterator := &sphinxHopIterator{ + blindingKit: kit, + rHash: rHash[:], + router: sphinx.NewRouter( + &sphinx.PrivKeyECDH{PrivKey: nodeKey}, + sphinx.NewMemoryReplayLog(), + ), + // Set our valid onion packet to be peeled. + processedPacket: &sphinx.ProcessedPacket{ + NextPacket: onionPacket, + }, + } + + // When we parse and validate the recipient data, it should enter the + // dummy-hop peeling path. Since our onion packet is valid and matches + // our private key, it should be successfully peeled and parsed. + pld, _, err := parseAndValidateRecipientData( + iterator, &Payload{encryptedData: data}, + false, RouteRoleCleartext, + ) + + // Assert that we successfully peeled the dummy hop and extracted the + // decrypted final payload. + require.NoError(t, err) + require.NotNil(t, pld) + + fwdInfo := pld.ForwardingInfo() + require.Equal(t, lnwire.MilliSatoshi(0), fwdInfo.AmountToForward) + require.Equal(t, uint32(0), fwdInfo.OutgoingCLTV) + require.NotNil(t, fwdInfo.PathID) + require.Equal(t, secret, fwdInfo.PathID[:]) +} diff --git a/htlcswitch/hop/payload.go b/htlcswitch/hop/payload.go index 14a0813e8..c84f1d2a8 100644 --- a/htlcswitch/hop/payload.go +++ b/htlcswitch/hop/payload.go @@ -126,7 +126,9 @@ func NewLegacyPayload(f *sphinx.HopData) *Payload { return &Payload{ FwdInfo: ForwardingInfo{ - NextHop: lnwire.NewShortChanIDFromInt(nextHop), + NextHop: NewChannelNextHop( + lnwire.NewShortChanIDFromInt(nextHop), + ), AmountToForward: lnwire.MilliSatoshi(f.ForwardAmount), OutgoingCLTV: f.OutgoingCltv, }, @@ -201,7 +203,9 @@ func ParseTLVPayload(r io.Reader) (*Payload, map[tlv.Type][]byte, error) { return &Payload{ FwdInfo: ForwardingInfo{ - NextHop: lnwire.NewShortChanIDFromInt(cid), + NextHop: NewChannelNextHop( + lnwire.NewShortChanIDFromInt(cid), + ), AmountToForward: lnwire.MilliSatoshi(amt), OutgoingCLTV: cltv, }, diff --git a/htlcswitch/htlcnotifier.go b/htlcswitch/htlcnotifier.go index 4d4d33374..ac9bb3b06 100644 --- a/htlcswitch/htlcnotifier.go +++ b/htlcswitch/htlcnotifier.go @@ -466,6 +466,14 @@ func getEventType(pkt *htlcPacket) HtlcEventType { case pkt.incomingChanID == hop.Source: return HtlcEventTypeSend + // A node-ID (pubkey) next hop has no outgoing SCID until the switch + // selects one, so outgoingChanID may still be hop.Exit on an early + // failure. Such a hop is always a forward, never the exit, so classify + // it before the hop.Exit check to avoid reporting a forward as a + // receive. + case pkt.outgoingHop.IsRight(): + return HtlcEventTypeForward + case pkt.outgoingChanID == hop.Exit: return HtlcEventTypeReceive diff --git a/htlcswitch/htlcnotifier_test.go b/htlcswitch/htlcnotifier_test.go new file mode 100644 index 000000000..f1f07225e --- /dev/null +++ b/htlcswitch/htlcnotifier_test.go @@ -0,0 +1,139 @@ +package htlcswitch + +import ( + "testing" + "time" + + "github.com/lightningnetwork/lnd/htlcswitch/hop" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/stretchr/testify/require" +) + +// TestGetEventType asserts how getEventType classifies an htlcPacket as a send, +// receive or forward event. +func TestGetEventType(t *testing.T) { + t.Parallel() + + var nodeID [33]byte + nodeID[0] = 0x02 + + tests := []struct { + name string + pkt *htlcPacket + want HtlcEventType + }{ + { + name: "send", + pkt: &htlcPacket{incomingChanID: hop.Source}, + want: HtlcEventTypeSend, + }, + { + name: "receive at exit hop", + pkt: &htlcPacket{ + incomingChanID: lnwire.NewShortChanIDFromInt(1), + outgoingChanID: hop.Exit, + }, + want: HtlcEventTypeReceive, + }, + { + name: "forward by channel ID", + pkt: &htlcPacket{ + incomingChanID: lnwire.NewShortChanIDFromInt(1), + outgoingChanID: lnwire.NewShortChanIDFromInt(2), + }, + want: HtlcEventTypeForward, + }, + { + // A node-ID forward that failed before channel + // selection has outgoingChanID == hop.Exit but a Right + // (pubkey) next hop, so it must classify as a forward. + name: "forward by node ID before selection", + pkt: &htlcPacket{ + incomingChanID: lnwire.NewShortChanIDFromInt(1), + outgoingChanID: hop.Exit, + outgoingHop: hop.NewNodeNextHop(nodeID), + }, + want: HtlcEventTypeForward, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + require.Equal(t, tc.want, getEventType(tc.pkt)) + }) + } +} + +// TestGetEventTypeNodeIDReconstructedPackets asserts that node-ID forward +// packets reconstructed via failAddPacket and interceptedForward.resolve +// preserve outgoingHop and are correctly classified as HtlcEventTypeForward by +// getEventType. +func TestGetEventTypeNodeIDReconstructedPackets(t *testing.T) { + t.Parallel() + + var nodeID [33]byte + nodeID[0] = 0x02 + + inChanID := lnwire.NewShortChanIDFromInt(1) + chanID := lnwire.ChannelID{1} + + // Create a switch with a mailOrchestrator and mailbox. + s := &Switch{ + mailOrchestrator: newMailOrchestrator(&mailOrchConfig{}), + } + mailbox := s.mailOrchestrator.GetOrCreateMailBox(chanID, inChanID) + s.mailOrchestrator.BindLiveShortChanID(mailbox, chanID, inChanID) + + // 1. Verify failAddPacket reconstruction. + origPkt := &htlcPacket{ + incomingChanID: inChanID, + incomingHTLCID: 42, + outgoingChanID: hop.Exit, + outgoingHop: hop.NewNodeNextHop(nodeID), + obfuscator: NewMockObfuscator(), + } + linkErr := NewLinkError(&lnwire.FailUnknownNextPeer{}) + + err := s.failAddPacket(origPkt, linkErr) + require.Equal(t, linkErr, err) + + select { + case failPkt := <-mailbox.PacketOutBox(): + require.True(t, failPkt.outgoingHop.IsRight()) + require.Equal( + t, HtlcEventTypeForward, getEventType(failPkt), + "failAddPacket must classify as forward", + ) + case <-time.After(time.Second): + t.Fatal("failAddPacket did not deliver packet to mailbox") + } + + // 2. Verify interceptedForward.resolve reconstruction. + resolvePkt := &htlcPacket{ + incomingChanID: inChanID, + incomingHTLCID: 43, + outgoingChanID: hop.Exit, + outgoingHop: hop.NewNodeNextHop(nodeID), + obfuscator: NewMockObfuscator(), + } + fwd := &interceptedForward{ + htlcSwitch: s, + packet: resolvePkt, + } + + err = fwd.resolve(&lnwire.UpdateFailHTLC{}) + require.NoError(t, err) + + select { + case resPkt := <-mailbox.PacketOutBox(): + require.True(t, resPkt.outgoingHop.IsRight()) + require.Equal( + t, HtlcEventTypeForward, getEventType(resPkt), + "interceptedForward.resolve must classify as forward", + ) + case <-time.After(time.Second): + t.Fatal("resolve did not deliver packet to mailbox") + } +} diff --git a/htlcswitch/interceptable_switch.go b/htlcswitch/interceptable_switch.go index ac2d24ccc..9ef686a65 100644 --- a/htlcswitch/interceptable_switch.go +++ b/htlcswitch/interceptable_switch.go @@ -705,6 +705,7 @@ func (f *interceptedForward) Packet() InterceptedPacket { HtlcID: f.packet.incomingHTLCID, }, OutgoingChanID: f.packet.outgoingChanID, + OutgoingNodeID: f.packet.outgoingHop.RightToSome(), Hash: f.htlc.PaymentHash, OutgoingExpiry: f.htlc.Expiry, OutgoingAmount: f.htlc.Amount, @@ -891,6 +892,7 @@ func (f *interceptedForward) resolve(message lnwire.Message) error { incomingChanID: f.packet.incomingChanID, incomingHTLCID: f.packet.incomingHTLCID, outgoingChanID: f.packet.outgoingChanID, + outgoingHop: f.packet.outgoingHop, outgoingHTLCID: f.packet.outgoingHTLCID, isResolution: true, circuit: f.packet.circuit, diff --git a/htlcswitch/interfaces.go b/htlcswitch/interfaces.go index 6a56b181e..f373aea8b 100644 --- a/htlcswitch/interfaces.go +++ b/htlcswitch/interfaces.go @@ -381,6 +381,14 @@ type InterceptableHtlcForwarder interface { // and resolve it later or let the switch execute its default behavior. type ForwardInterceptor func(InterceptedPacket) error +// NodeIDForwardSCID is the sentinel outgoing SCID reported to HTLC interceptor +// clients (at the RPC boundary) for a next hop identified by node ID (BOLT 4 +// next_node_id) rather than by channel. All bits are set, an out-of-range value +// that can never match a real or alias channel, so a client switching on a zero +// SCID to detect the exit hop does not read the forward as a final receive. The +// pubkey is in InterceptedPacket.OutgoingNodeID. +const NodeIDForwardSCID uint64 = ^uint64(0) + // InterceptedPacket contains the relevant information for the interceptor about // an HTLC. type InterceptedPacket struct { @@ -388,9 +396,17 @@ type InterceptedPacket struct { // packet. IncomingCircuit models.CircuitKey - // OutgoingChanID is the destination channel for this packet. + // OutgoingChanID is the destination channel for this packet. For a + // node-ID next hop with no concrete channel known yet it is hop.Exit + // and OutgoingNodeID holds the pubkey; the RPC layer maps that to the + // NodeIDForwardSCID sentinel before reporting it to a client. OutgoingChanID lnwire.ShortChannelID + // OutgoingNodeID is the next hop's compressed pubkey for a blinded + // route that identifies it by node ID (next_node_id). None in the + // common channel-ID case. + OutgoingNodeID fn.Option[[33]byte] + // Hash is the payment hash of the htlc. Hash lntypes.Hash diff --git a/htlcswitch/link.go b/htlcswitch/link.go index 056403cb3..437127919 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -2635,7 +2635,10 @@ func (l *channelLink) canSendHtlc(policy models.ForwardingPolicy, htlcBlob = fn.Some(blob) } - return l.AuxBandwidth(amt, originalScid, htlcBlob, ts) + // Check if this link can handle the traffic. + return l.AuxBandwidth( + amt, l.ShortChanID(), htlcBlob, ts, + ) }, ).Unpack() if externalErr != nil { @@ -3153,8 +3156,8 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg) { continue } - switch fwdInfo.NextHop { - case hop.Exit: + switch { + case fwdInfo.IsExit(): err := l.processExitHop( add, sourceRef, obfuscator, fwdInfo, heightNow, pld, @@ -3232,7 +3235,8 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg) { updatePacket := &htlcPacket{ incomingChanID: l.ShortChanID(), incomingHTLCID: add.ID, - outgoingChanID: fwdInfo.NextHop, + outgoingChanID: fwdInfo.NextHopChannel().UnwrapOr(hop.Exit), + outgoingHop: fwdInfo.NextHop, sourceRef: &sourceRef, incomingAmount: add.Amount, amount: outgoingAdd.Amount, @@ -3309,7 +3313,8 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg) { updatePacket := &htlcPacket{ incomingChanID: l.ShortChanID(), incomingHTLCID: add.ID, - outgoingChanID: fwdInfo.NextHop, + outgoingChanID: fwdInfo.NextHopChannel().UnwrapOr(hop.Exit), + outgoingHop: fwdInfo.NextHop, sourceRef: &sourceRef, incomingAmount: add.Amount, amount: addMsg.Amount, diff --git a/htlcswitch/link_test.go b/htlcswitch/link_test.go index a64942d5c..59b3acd07 100644 --- a/htlcswitch/link_test.go +++ b/htlcswitch/link_test.go @@ -40,6 +40,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/ticker" + "github.com/lightningnetwork/lnd/tlv" "github.com/stretchr/testify/require" ) @@ -776,8 +777,9 @@ func testChannelLinkInboundFee(t *testing.T, //nolint:thelper hops := []*hop.Payload{ { FwdInfo: hop.ForwardingInfo{ - NextHop: n.carolChannelLink. - ShortChanID(), + NextHop: hop.NewChannelNextHop( + n.carolChannelLink.ShortChanID(), + ), AmountToForward: 1_000_000, OutgoingCLTV: 106, }, @@ -6394,6 +6396,134 @@ func TestCheckHtlcForward(t *testing.T) { }) } +// recordingAuxShaper is a minimal AuxTrafficShaper that records the channel id +// it is asked about and declines to handle the traffic, so the normal +// forwarding path proceeds. Only the methods reached by CheckHtlcForward are +// implemented; the rest are inherited from the embedded (nil) interface and +// must never be called. +type recordingAuxShaper struct { + AuxTrafficShaper + + gotCID lnwire.ShortChannelID +} + +// ShouldHandleTraffic records the short channel ID passed to the shaper. +func (a *recordingAuxShaper) ShouldHandleTraffic(cid lnwire.ShortChannelID, + _, _ fn.Option[tlv.Blob]) (bool, error) { + + a.gotCID = cid + + return false, nil +} + +// IsCustomHTLC returns false as recordingAuxShaper handles standard HTLCs. +func (a *recordingAuxShaper) IsCustomHTLC(_ lnwire.CustomRecords) bool { + return false +} + +// TestCheckHtlcForwardAuxShaperChannel asserts that during non-strict +// forwarding the aux traffic shaper is keyed on the channel actually being +// evaluated (the link's own SCID), not the sender-requested SCID, which fixes +// both the node-ID/blinded path (where no SCID is requested) and pre-existing +// parallel-channel forwarding. It also asserts the real SCID handed to the +// shaper never leaks into the sender-facing channel_update, which continues to +// reference the requested (alias) SCID. +func TestCheckHtlcForwardAuxShaperChannel(t *testing.T) { + t.Parallel() + + const ( + chanScid = 42 + requestedScid = 99 + ) + + fetchLastChannelUpdate := func(lnwire.ShortChannelID) ( + *lnwire.ChannelUpdate1, error) { + + return &lnwire.ChannelUpdate1{}, nil + } + + // Record the SCID used to build the returned channel_update on failure. + var updateScid lnwire.ShortChannelID + failAliasUpdate := func(sid lnwire.ShortChannelID, + incoming bool) *lnwire.ChannelUpdate1 { + + updateScid = sid + + return &lnwire.ChannelUpdate1{ + ShortChannelID: sid, + } + } + + testChannel, _, err := createTestChannel( + t, alicePrivKey, bobPrivKey, 100000, 100000, 1000, 1000, + lnwire.NewShortChanIDFromInt(chanScid), + ) + require.NoError(t, err) + + shaper := &recordingAuxShaper{} + link := channelLink{ + cfg: ChannelLinkConfig{ + FwrdingPolicy: models.ForwardingPolicy{ + TimeLockDelta: 20, + MinHTLCOut: 500, + MaxHTLC: 1000, + BaseFee: 10, + }, + FetchLastChannelUpdate: fetchLastChannelUpdate, + MaxOutgoingCltvExpiry: DefaultMaxOutgoingCltvExpiry, + HtlcNotifier: &mockHTLCNotifier{}, + }, + log: log, + channel: testChannel.channel, + } + link.cfg.AuxTrafficShaper = fn.Some[AuxTrafficShaper](shaper) + link.attachFailAliasUpdate(failAliasUpdate) + + require.Equal( + t, lnwire.NewShortChanIDFromInt(chanScid), link.ShortChanID(), + ) + + var hash [32]byte + requested := lnwire.NewShortChanIDFromInt(requestedScid) + + // A satisfiable forward: the shaper must be queried about the channel + // being evaluated (the link's own SCID), not the requested SCID. + result := link.CheckHtlcForward( + hash, 1500, 1000, 200, 150, models.InboundFee{}, 0, requested, + nil, + ) + require.Nil(t, result, "expected policy to be satisfied") + require.Equal( + t, link.ShortChanID(), shaper.gotCID, + "aux shaper must be keyed on the evaluated channel", + ) + require.NotEqual( + t, requested, shaper.gotCID, + "aux shaper must not be keyed on the requested SCID", + ) + + // A failing forward: the returned channel_update must reference the + // requested (alias) SCID, never the real channel SCID handed to the + // shaper. + result = link.CheckHtlcForward( + hash, 100, 50, 200, 150, models.InboundFee{}, 0, requested, nil, + ) + require.NotNil(t, result) + require.Equal( + t, requested, updateScid, + "channel_update must reference the requested SCID, not the "+ + "real channel SCID", + ) + + wireErr := result.WireMessage() + failAmt, ok := wireErr.(*lnwire.FailAmountBelowMinimum) + require.True(t, ok, "expected FailAmountBelowMinimum failure") + require.Equal( + t, requested, failAmt.Update.ShortChannelID, + "failure update must carry the requested SCID", + ) +} + // TestChannelLinkCanceledInvoice in this test checks the interaction // between Alice and Bob for a canceled invoice. func TestChannelLinkCanceledInvoice(t *testing.T) { diff --git a/htlcswitch/mailbox.go b/htlcswitch/mailbox.go index b283825dd..2a0796855 100644 --- a/htlcswitch/mailbox.go +++ b/htlcswitch/mailbox.go @@ -699,12 +699,18 @@ func (m *memoryMailBox) FailAdd(pkt *htlcPacket) { reason lnwire.OpaqueReason ) - // Create a temporary channel failure which we will send back to our - // peer if this is a forward, or report to the user if the failed - // payment was locally initiated. - failure := m.cfg.failMailboxUpdate( - pkt.originalOutgoingChanID, m.cfg.shortChanID, - ) + var failure lnwire.FailureMessage + if pkt.outgoingHop.IsRight() { + // A node-ID next hop has no requested outgoing channel. + // Returning a channel_update could leak a private channel's + // SCID if the failure reason is persisted before blinding + // error processing or replayed during channel reestablishment. + failure = &lnwire.FailUnknownNextPeer{} + } else { + failure = m.cfg.failMailboxUpdate( + pkt.originalOutgoingChanID, m.cfg.shortChanID, + ) + } // If the payment was locally initiated (which is indicated by a nil // obfuscator), we do not need to encrypt it back to the sender. @@ -737,6 +743,8 @@ func (m *memoryMailBox) FailAdd(pkt *htlcPacket) { failPkt := &htlcPacket{ incomingChanID: pkt.incomingChanID, incomingHTLCID: pkt.incomingHTLCID, + outgoingChanID: pkt.outgoingChanID, + outgoingHop: pkt.outgoingHop, circuit: pkt.circuit, sourceRef: pkt.sourceRef, hasSource: true, diff --git a/htlcswitch/mailbox_test.go b/htlcswitch/mailbox_test.go index 57a581c4b..8b0967a1a 100644 --- a/htlcswitch/mailbox_test.go +++ b/htlcswitch/mailbox_test.go @@ -10,6 +10,7 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/clock" + "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/lnmock" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" @@ -276,6 +277,17 @@ func (c *mailboxContext) sendAdds(start, num int) []*htlcPacket { ID: uint64(start + i), }, } + if i%2 == 0 { + pkt.outgoingHop = fn.NewLeft[ + lnwire.ShortChannelID, [33]byte, + ](pkt.outgoingChanID) + } else { + var nodeID [33]byte + prand.Read(nodeID[:]) + pkt.outgoingHop = fn.NewRight[ + lnwire.ShortChannelID, [33]byte, + ](nodeID) + } sentPackets[i] = pkt err := c.mailbox.AddPacket(pkt) @@ -313,6 +325,14 @@ func (c *mailboxContext) checkFails(adds []*htlcPacket) { select { case fail := <-c.forwards: if add.inKey() == fail.inKey() { + require.Equal( + c.t, add.outgoingChanID, + fail.outgoingChanID, + ) + require.Equal( + c.t, add.outgoingHop, + fail.outgoingHop, + ) continue } c.t.Fatalf("inkey mismatch #%d, add: %v vs fail: %v", @@ -828,3 +848,54 @@ func TestMailOrchestrator(t *testing.T) { spew.Sdump(sentPackets), spew.Sdump(recvdPackets)) } } + +// TestMailBoxFailAddNodeID asserts that FailAdd for a node-ID hop returns a +// FailUnknownNextPeer failure without a channel update. +func TestMailBoxFailAddNodeID(t *testing.T) { + ctx := newMailboxContext(t, time.Now(), time.Minute) + + var nodeID [33]byte + nodeID[0] = 0x02 + + pkt := &htlcPacket{ + incomingChanID: lnwire.NewShortChanIDFromInt(1), + incomingHTLCID: 1, + outgoingHop: fn.NewRight[lnwire.ShortChannelID, [33]byte]( + nodeID, + ), + htlc: &lnwire.UpdateAddHTLC{ + ID: 1, + }, + } + + require.NoError(t, ctx.mailbox.AddPacket(pkt)) + + // Pull packet from mailbox to simulate link delivery. + select { + case <-ctx.mailbox.PacketOutBox(): + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for packet outbox") + } + + // Fail the packet via FailAdd. + ctx.mailbox.FailAdd(pkt) + + select { + case pktResponse := <-ctx.forwards: + require.Equal(t, pkt.incomingChanID, pktResponse.incomingChanID) + require.Equal(t, pkt.incomingHTLCID, pktResponse.incomingHTLCID) + require.Equal(t, pkt.outgoingChanID, pktResponse.outgoingChanID) + require.Equal(t, pkt.outgoingHop, pktResponse.outgoingHop) + require.NotNil(t, pktResponse.linkFailure) + + var unknownNextPeer *lnwire.FailUnknownNextPeer + require.ErrorAs( + t, pktResponse.linkFailure.WireMessage(), + &unknownNextPeer, + "expected FailUnknownNextPeer for node-ID FailAdd", + ) + + case <-time.After(50 * time.Millisecond): + t.Fatal("timeout waiting for packet response") + } +} diff --git a/htlcswitch/mock.go b/htlcswitch/mock.go index dbab96727..62cbb8822 100644 --- a/htlcswitch/mock.go +++ b/htlcswitch/mock.go @@ -367,7 +367,13 @@ func (r *mockHopIterator) EncodeNextHop(w io.Writer) error { } func encodeFwdInfo(w io.Writer, f *hop.ForwardingInfo) error { - if err := binary.Write(w, binary.BigEndian, f.NextHop); err != nil { + if f.NextHop.IsRight() { + return fmt.Errorf("mock serialization does not support " + + "node-ID next hop") + } + + nextHop := f.NextHopChannel().UnwrapOr(hop.Exit) + if err := binary.Write(w, binary.BigEndian, nextHop); err != nil { return err } @@ -509,7 +515,8 @@ func (p *mockIteratorDecoder) DecodeHopIterator(r io.Reader, rHash []byte, } var nextHopBytes [8]byte - binary.BigEndian.PutUint64(nextHopBytes[:], f.NextHop.ToUint64()) + scid := f.NextHopChannel().UnwrapOr(hop.Exit) + binary.BigEndian.PutUint64(nextHopBytes[:], scid.ToUint64()) hops[i] = hop.NewLegacyPayload(&sphinx.HopData{ Realm: [1]byte{}, // hop.BitcoinNetwork @@ -562,9 +569,11 @@ func (p *mockIteratorDecoder) DecodeHopIterators(id []byte, } func decodeFwdInfo(r io.Reader, f *hop.ForwardingInfo) error { - if err := binary.Read(r, binary.BigEndian, &f.NextHop); err != nil { + var nextHop lnwire.ShortChannelID + if err := binary.Read(r, binary.BigEndian, &nextHop); err != nil { return err } + f.NextHop = hop.NewChannelNextHop(nextHop) if err := binary.Read(r, binary.BigEndian, &f.AmountToForward); err != nil { return err diff --git a/htlcswitch/packet.go b/htlcswitch/packet.go index ed5f82588..9af7e3432 100644 --- a/htlcswitch/packet.go +++ b/htlcswitch/packet.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/htlcswitch/hop" "github.com/lightningnetwork/lnd/lnwire" @@ -18,9 +19,23 @@ type htlcPacket struct { incomingChanID lnwire.ShortChannelID // outgoingChanID is the ID of the channel that we have offered or will - // offer an outgoing HTLC on. + // offer an outgoing HTLC on. It is mutable and may remain zero + // (hop.Exit) until non-strict forwarding resolves a node-ID next hop to + // a concrete channel, or may differ from the requested SCID after + // non-strict load-balancing. A zero outgoingChanID alone does not imply + // an exit hop: if outgoingHop is a Right (node ID), the HTLC is a + // forward whose outgoing channel has not yet been selected. outgoingChanID lnwire.ShortChannelID + // outgoingHop carries the immutable next-hop instruction decoded from + // the onion payload, following the same encoding as + // hop.ForwardingInfo.NextHop. The three possible cases are: + // 1. Left(scid) where scid != Exit: a channel-addressed forward. + // 2. Right(pubkey): a node-addressed forward for a blinded route, + // resolved to an active link via non-strict forwarding. + // 3. Left(Exit): a final receive at the destination/receiver node. + outgoingHop fn.Either[lnwire.ShortChannelID, [33]byte] + // incomingHTLCID is the ID of the HTLC that we have received from the peer // on the incoming channel. incomingHTLCID uint64 @@ -104,11 +119,10 @@ type htlcPacket struct { // in the incoming update_add_htlc wire message. inWireCustomRecords lnwire.CustomRecords - // originalOutgoingChanID is used when sending back failure messages. - // It is only used for forwarded Adds on option_scid_alias channels. - // This is to avoid possible confusion if a payer uses the public SCID - // but receives a channel_update with the alias SCID. Instead, the - // payer should receive a channel_update with the public SCID. + // originalOutgoingChanID is used when sending back failure messages. It + // retains the original sender-facing requested SCID for forwarded Adds, + // including option_scid_alias channels. This prevents exposing the + // evaluated link's concrete SCID or alias in channel_update failures. originalOutgoingChanID lnwire.ShortChannelID // inboundFee is the fee schedule of the incoming channel. diff --git a/htlcswitch/switch.go b/htlcswitch/switch.go index a3aae809b..23a83a3b1 100644 --- a/htlcswitch/switch.go +++ b/htlcswitch/switch.go @@ -1250,6 +1250,7 @@ func (s *Switch) failAddPacket(packet *htlcPacket, failure *LinkError) error { incomingChanID: packet.incomingChanID, incomingHTLCID: packet.incomingHTLCID, outgoingChanID: packet.outgoingChanID, + outgoingHop: packet.outgoingHop, outgoingHTLCID: packet.outgoingHTLCID, incomingAmount: packet.incomingAmount, amount: packet.amount, @@ -2862,41 +2863,94 @@ func (s *Switch) handlePacketAdd(packet *htlcPacket, return s.failAddPacket(packet, failure) } - // Before we attempt to find a non-strict forwarding path for this - // htlc, check whether the htlc is being routed over the same incoming - // and outgoing channel. If our node does not allow forwards of this - // nature, we fail the htlc early. This check is in place to disallow - // inefficiently routed htlcs from locking up our balance. With - // channels where the option-scid-alias feature was negotiated, we also - // have to be sure that the IDs aren't the same since one or both could - // be an alias. - linkErr := s.checkCircularForward( - packet.incomingChanID, packet.outgoingChanID, - s.cfg.AllowCircularRoute, htlc.PaymentHash, - ) - if linkErr != nil { - return s.failAddPacket(packet, linkErr) - } + // Collect the links that could carry this HTLC to the next hop. + // Non-strict forwarding then load-balances across our channels to that + // peer. A short channel ID maps to a link and its peer, while a blinded + // node-ID next hop resolves the peer directly. A node-ID hop has no + // sender-specified channel, so outgoingChanID stays hop.Exit until + // selection. + var interfaceLinks []ChannelLink + if packet.outgoingHop.IsLeft() { + // Before we attempt to find a non-strict forwarding path for + // this htlc, check whether the htlc is being routed over the + // same incoming and outgoing channel. If our node does not + // allow forwards of this nature, we fail the htlc early. This + // check is in place to disallow inefficiently routed htlcs from + // locking up our balance. With channels where the + // option-scid-alias feature was negotiated, we also have to be + // sure that the IDs aren't the same since one or both could be + // an alias. + linkErr := s.checkCircularForward( + packet.incomingChanID, packet.outgoingChanID, + s.cfg.AllowCircularRoute, htlc.PaymentHash, + ) + if linkErr != nil { + return s.failAddPacket(packet, linkErr) + } - s.indexMtx.RLock() - targetLink, err := s.getLinkByMapping(packet) - if err != nil { + s.indexMtx.RLock() + targetLink, err := s.getLinkByMapping(packet) + if err != nil { + s.indexMtx.RUnlock() + + log.Debugf("unable to find link with "+ + "destination %v", packet.outgoingChanID) + + // If packet was forwarded from another channel link + // then we should notify this link that some error + // occurred. + linkError := NewLinkError( + &lnwire.FailUnknownNextPeer{}, + ) + + return s.failAddPacket(packet, linkError) + } + + // NOTE: for the SCID path, we fetch all links to the target + // peer. If parallel channels exist to the incoming peer, the + // candidate set may include the incoming channel even when a + // different SCID was requested. + targetPeer := targetLink.PeerPubKey() + interfaceLinks, _ = s.getLinks(targetPeer) + s.indexMtx.RUnlock() + } else { + // A blinded node-ID next hop identifies the peer directly, so + // resolve its links and let non-strict forwarding load-balance + // across our channels to that peer. + peerKey := packet.outgoingHop.UnwrapRightOr([33]byte{}) + + s.indexMtx.RLock() + interfaceLinks, _ = s.getLinks(peerKey) s.indexMtx.RUnlock() - log.Debugf("unable to find link with "+ - "destination %v", packet.outgoingChanID) + // Drop links that would form a disallowed circular route, so + // selection can't later land on the incoming channel. + var nonCircularLinks []ChannelLink + for _, link := range interfaceLinks { + linkErr := s.checkCircularForward( + packet.incomingChanID, link.ShortChanID(), + s.cfg.AllowCircularRoute, htlc.PaymentHash, + ) + if linkErr == nil { + nonCircularLinks = append( + nonCircularLinks, link, + ) + } + } + interfaceLinks = nonCircularLinks - // If packet was forwarded from another channel link than we - // should notify this link that some error occurred. - linkError := NewLinkError( - &lnwire.FailUnknownNextPeer{}, - ) + // Without a usable link to the peer (none exist, or all would + // be circular) we cannot forward. Fail as unknown next peer + // rather than attributing it to a specific channel. + if len(interfaceLinks) == 0 { + log.Debugf("no usable link to peer %x for blinded "+ + "next hop", peerKey) - return s.failAddPacket(packet, linkError) + return s.failAddPacket(packet, NewLinkError( + &lnwire.FailUnknownNextPeer{}, + )) + } } - targetPeerKey := targetLink.PeerPubKey() - interfaceLinks, _ := s.getLinks(targetPeerKey) - s.indexMtx.RUnlock() // We'll keep track of any HTLC failures during the link selection // process. This way we can return the error for precise link that the @@ -2943,6 +2997,18 @@ func (s *Switch) handlePacketAdd(packet *htlcPacket, // current policy, then we'll send back an error, but ensure we send // back the error sourced at the *target* link. if len(destinations) == 0 { + // A node-ID next hop has no requested outgoing channel. + // Returning a per-candidate failure could leak a private + // channel via its channel_update (a probing vector), so fail + // generically. Later errors don't include private data. Defense + // in depth: route blinding error handling hides it too via + // error conversion. + if packet.outgoingHop.IsRight() { + return s.failAddPacket(packet, NewLinkError( + &lnwire.FailUnknownNextPeer{}, + )) + } + // At this point, some or all of the links rejected the HTLC so // we couldn't forward it. So we'll try to look up the error // that came from the source. diff --git a/htlcswitch/switch_test.go b/htlcswitch/switch_test.go index 13563916e..3c8b9fea8 100644 --- a/htlcswitch/switch_test.go +++ b/htlcswitch/switch_test.go @@ -1991,6 +1991,139 @@ func TestCircularForwards(t *testing.T) { } } +// TestNodeIDNonStrictRouting ensures that when a blinded route identifies the +// next hop by node ID, non-strict forwarding deterministically selects a valid +// outgoing channel to that peer and never fails the HTLC by landing on the +// incoming channel. +func TestNodeIDNonStrictRouting(t *testing.T) { + t.Parallel() + + // bob is both the source of the incoming HTLC and the next hop + // identified by node ID, so we have two channels with bob: the channel + // the HTLC arrives on and a second, valid outgoing channel. + bobPeer, err := newMockServer( + t, "bob", testStartingHeight, nil, testDefaultDelta, + ) + require.NoError(t, err, "unable to create bob server") + + s, err := initSwitchWithTempDB(t, testStartingHeight) + require.NoError(t, err, "unable to init switch") + require.NoError(t, s.Start(), "unable to start switch") + defer func() { _ = s.Stop() }() + + // Disallow circular routes so that forwarding back over the incoming + // channel is rejected. + s.cfg.AllowCircularRoute = false + + incomingChanID, incomingScid := genID() + outgoingChanID, outgoingScid := genID() + + incomingLink := newMockChannelLink( + s, incomingChanID, incomingScid, emptyScid, bobPeer, + true, false, false, false, + ) + outgoingLink := newMockChannelLink( + s, outgoingChanID, outgoingScid, emptyScid, bobPeer, + true, false, false, false, + ) + require.NoError(t, s.AddLink(incomingLink), "unable to add incoming") + require.NoError(t, s.AddLink(outgoingLink), "unable to add outgoing") + + // Forward many HTLCs so that random selection would almost certainly + // land on the incoming channel, which will be sorted out by the switch. + const numHTLCs = 20 + for i := 0; i < numHTLCs; i++ { + var hash [sha256.Size]byte + hash[0] = byte(i) + + packet := &htlcPacket{ + incomingChanID: incomingLink.ShortChanID(), + incomingHTLCID: uint64(i), + outgoingHop: hop.NewNodeNextHop(bobPeer.PubKey()), + htlc: &lnwire.UpdateAddHTLC{ + PaymentHash: hash, + Amount: 1, + }, + obfuscator: NewMockObfuscator(), + } + + require.NoError(t, s.ForwardPackets(nil, packet)) + + select { + case p := <-outgoingLink.packets: + require.Nil(t, p.linkFailure, "unexpected link failure") + require.Equal( + t, outgoingLink.ShortChanID(), + p.outgoingChanID, + "forwarded over wrong channel", + ) + + case <-incomingLink.packets: + t.Fatal("HTLC forwarded over incoming (circular) " + + "channel") + + case <-time.After(time.Second): + t.Fatal("no timely reply from switch") + } + } +} + +// TestNodeIDNonStrictRoutingAllLinksCircular ensures that when a blinded route +// identifies the next hop by node ID, and the only channel we have with that +// peer is the incoming channel (forming a circular route), the switch fails the +// HTLC early upfront. +func TestNodeIDNonStrictRoutingAllLinksCircular(t *testing.T) { + t.Parallel() + + bobPeer, err := newMockServer( + t, "bob", testStartingHeight, nil, testDefaultDelta, + ) + require.NoError(t, err, "unable to create bob server") + + s, err := initSwitchWithTempDB(t, testStartingHeight) + require.NoError(t, err, "unable to init switch") + require.NoError(t, s.Start(), "unable to start switch") + defer func() { _ = s.Stop() }() + + // Disallow circular routes. + s.cfg.AllowCircularRoute = false + + incomingChanID, incomingScid := genID() + incomingLink := newMockChannelLink( + s, incomingChanID, incomingScid, emptyScid, bobPeer, + true, false, false, false, + ) + require.NoError(t, s.AddLink(incomingLink), "unable to add incoming") + + packet := &htlcPacket{ + incomingChanID: incomingLink.ShortChanID(), + incomingHTLCID: 1, + outgoingHop: hop.NewNodeNextHop(bobPeer.PubKey()), + htlc: &lnwire.UpdateAddHTLC{ + PaymentHash: [32]byte{1}, + Amount: 1, + }, + obfuscator: NewMockObfuscator(), + } + + err = s.ForwardPackets(nil, packet) + require.NoError(t, err, "unable to forward packets") + + select { + case p := <-incomingLink.packets: + require.NotNil(t, p.linkFailure, "expected early link failure") + wireErr := p.linkFailure.WireMessage() + var unknownNextPeer *lnwire.FailUnknownNextPeer + require.ErrorAs( + t, wireErr, &unknownNextPeer, + "expected FailUnknownNextPeer", + ) + + case <-time.After(time.Second): + t.Fatal("no timely reply from switch") + } +} + // TestCheckCircularForward tests the error returned by checkCircularForward // in cases where we allow and disallow same channel circular forwards. func TestCheckCircularForward(t *testing.T) { diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 02fd01218..6c9feacbe 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -591,6 +591,18 @@ var allTestCases = []*lntest.TestCase{ Name: "blinded payment htlc re-forward", TestFunc: testBlindedPaymentHTLCReForward, }, + { + Name: "blinded route next node id", + TestFunc: testBlindedRouteNextNodeID, + }, + { + Name: "blinded route next node id private channel", + TestFunc: testBlindedRouteNextNodeIDPrivateChannel, + }, + { + Name: "blinded route next node id restart", + TestFunc: testBlindedRouteNextNodeIDRestart, + }, { Name: "query blinded route", TestFunc: testQueryBlindedRoutes, diff --git a/itest/lnd_route_blinding_test.go b/itest/lnd_route_blinding_test.go index af2612d24..a387e907a 100644 --- a/itest/lnd_route_blinding_test.go +++ b/itest/lnd_route_blinding_test.go @@ -1,6 +1,7 @@ package itest import ( + "bytes" "context" "crypto/sha256" "encoding/hex" @@ -10,12 +11,16 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" + sphinx "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/chainreg" + "github.com/lightningnetwork/lnd/htlcswitch" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest/node" "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/record" + "github.com/lightningnetwork/lnd/tlv" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -383,6 +388,78 @@ func (b *blindedForwardTest) setupNetwork(ctx context.Context, } } +// setupNetworkPrivateMiddle sets up the same Alice -> Bob -> Carol -> Dave +// network as setupNetwork (with an interceptor on Carol), except that the +// Bob -> Carol channel is private. This is the channel the introduction node +// (Bob) must resolve to from Carol's node ID, exercising resolution to an SCID +// alias of an unadvertised channel. +func (b *blindedForwardTest) setupNetworkPrivateMiddle(ctx context.Context) { + carolArgs := []string{ + "--bitcoin.timelockdelta=24", + fmt.Sprintf("--bitcoin.defaultremotedelay=%v", toLocalCSV), + "--requireinterceptor", + } + daveArgs := []string{ + "--bitcoin.timelockdelta=24", + fmt.Sprintf("--bitcoin.defaultremotedelay=%v", toLocalCSV), + } + + alice := b.ht.NewNode("Alice", nil) + bob := b.ht.NewNode("Bob", nil) + carol := b.ht.NewNode("Carol", carolArgs) + dave := b.ht.NewNode("Dave", daveArgs) + b.alice, b.bob, b.carol, b.dave = alice, bob, carol, dave + + b.ht.EnsureConnected(alice, bob) + b.ht.EnsureConnected(bob, carol) + b.ht.EnsureConnected(carol, dave) + + // Fund every node that opens a channel. + const chanAmt = btcutil.Amount(100_000) + b.ht.FundCoins(btcutil.SatoshiPerBitcoin, alice) + b.ht.FundCoins(btcutil.SatoshiPerBitcoin, bob) + b.ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) + + // Open Alice -> Bob and Carol -> Dave as public channels, but Bob -> + // Carol (the hop the introduction node must resolve by node ID) as a + // private channel, so it is only reachable via an SCID alias. + reqs := []*lntest.OpenChannelRequest{ + { + Local: alice, + Remote: bob, + Param: lntest.OpenChannelParams{Amt: chanAmt}, + }, + { + Local: bob, + Remote: carol, + Param: lntest.OpenChannelParams{ + Amt: chanAmt, + Private: true, + }, + }, + { + Local: carol, + Remote: dave, + Param: lntest.OpenChannelParams{Amt: chanAmt}, + }, + } + b.channels = b.ht.OpenMultiChannelsAsync(reqs) + + // Alice must know the public Alice -> Bob channel to build a route to + // the introduction node, and Bob and Carol must both know the private + // Bob -> Carol channel used for forwarding. + b.ht.AssertChannelInGraph(alice, b.channels[0]) + b.ht.AssertChannelInGraph(bob, b.channels[0]) + b.ht.AssertChannelInGraph(bob, b.channels[1]) + b.ht.AssertChannelInGraph(carol, b.channels[1]) + b.ht.AssertChannelInGraph(carol, b.channels[2]) + b.ht.AssertChannelInGraph(dave, b.channels[2]) + + var err error + b.carolInterceptor, err = b.carol.RPC.Router.HtlcInterceptor(ctx) + require.NoError(b.ht, err, "interceptor") +} + // buildBlindedPath returns a blinded route from Bob -> Carol -> Dave, with Bob // acting as the introduction point. func (b *blindedForwardTest) buildBlindedPath() *lnrpc.BlindedPaymentPath { @@ -1421,6 +1498,349 @@ func testBlindedPaymentHTLCReForward(ht *lntest.HarnessTest) { } } +// nextNodeIDRouteData builds the recipient data for a non-final blinded hop +// that identifies the next hop by its node ID (next_node_id) rather than a +// short channel ID. This is the form of recipient data that a non-lnd +// implementation may produce and that the forwarding node must resolve to one +// of its active channels. +func nextNodeIDRouteData(nextNode *btcec.PublicKey, + relayInfo record.PaymentRelayInfo, + constraints *record.PaymentConstraints) *record.BlindedRouteData { + + return &record.BlindedRouteData{ + NextNodeID: tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType4](nextNode), + ), + RelayInfo: tlv.SomeRecordT( + tlv.NewRecordT[tlv.TlvType10](relayInfo), + ), + Constraints: tlv.SomeRecordT( + tlv.NewRecordT[tlv.TlvType12](*constraints), + ), + } +} + +// buildBlindedPathWithNextNodeID constructs a Bob -> Carol -> Dave blinded path +// in which the non-final hops (Bob and Carol) identify their next hop by node +// ID instead of a short channel ID. Bob is the introduction node. The returned +// path can be used to exercise an lnd forwarding node's ability to resolve a +// next_node_id to one of its active channels. +func (b *blindedForwardTest) buildBlindedPathWithNextNodeID( + paymentAmt int64) *lnrpc.BlindedPaymentPath { + + bobPub, err := btcec.ParsePubKey(b.bob.PubKey[:]) + require.NoError(b.ht, err) + + carolPub, err := btcec.ParsePubKey(b.carol.PubKey[:]) + require.NoError(b.ht, err) + + davePub, err := btcec.ParsePubKey(b.dave.PubKey[:]) + require.NoError(b.ht, err) + + // Use zero fees so that the forwarded amount remains constant along the + // path, keeping the route math trivial. + const ( + hopCltvDelta uint16 = 144 + finalCltvDelta uint32 = 24 + ) + + // Set a generous max CLTV constraint so that the incoming expiry at + // each hop never trips the payment constraints check. + info := b.alice.RPC.GetInfo() + constraints := &record.PaymentConstraints{ + MaxCltvExpiry: info.BlockHeight + 10_000, + HtlcMinimumMsat: 0, + } + relayInfo := record.PaymentRelayInfo{ + CltvExpiryDelta: hopCltvDelta, + FeeRate: 0, + BaseFee: 0, + } + + // Bob (the introduction node) forwards to Carol and Carol forwards to + // Dave, each identified purely by node ID. Dave is the final hop; its + // path ID is arbitrary because the payment is settled at Carol via the + // interceptor before it ever reaches Dave. + hopData := []struct { + pub *btcec.PublicKey + data *record.BlindedRouteData + }{ + { + pub: bobPub, + data: nextNodeIDRouteData( + carolPub, relayInfo, constraints, + ), + }, + { + pub: carolPub, + data: nextNodeIDRouteData( + davePub, relayInfo, constraints, + ), + }, + { + pub: davePub, + data: record.NewFinalHopBlindedRouteData( + constraints, bytes.Repeat([]byte{1}, 32), + ), + }, + } + + paymentPath := make([]*sphinx.HopInfo, len(hopData)) + for i, hop := range hopData { + plainText, err := record.EncodeBlindedRouteData(hop.data) + require.NoError(b.ht, err) + + paymentPath[i] = &sphinx.HopInfo{ + NodePub: hop.pub, + PlainText: plainText, + } + } + + // Encrypt the per-hop data into a blinded path using a fresh session + // key. + sessionKey, err := btcec.NewPrivateKey() + require.NoError(b.ht, err) + + blindedPathInfo, err := sphinx.BuildBlindedPath(sessionKey, paymentPath) + require.NoError(b.ht, err) + blindedPath := blindedPathInfo.Path + + // The introduction node is communicated in plaintext, so overwrite the + // first hop's blinded pub key with the real introduction point. + blindedPath.BlindedHops[0].BlindedNodePub = + blindedPath.IntroductionPoint + + blindedHops := make( + []*lnrpc.BlindedHop, len(blindedPath.BlindedHops), + ) + for i, hop := range blindedPath.BlindedHops { + blindedHops[i] = &lnrpc.BlindedHop{ + BlindedNode: hop.BlindedNodePub.SerializeCompressed(), + EncryptedData: hop.CipherText, + } + } + + return &lnrpc.BlindedPaymentPath{ + BlindedPath: &lnrpc.BlindedPath{ + IntroductionNode: b.bob.PubKey[:], + BlindingPoint: blindedPath.BlindingPoint. + SerializeCompressed(), + BlindedHops: blindedHops, + }, + BaseFeeMsat: 0, + TotalCltvDelta: 2*uint32(hopCltvDelta) + finalCltvDelta, + HtlcMinMsat: 0, + HtlcMaxMsat: uint64(paymentAmt) * 2, + } +} + +// testBlindedRouteNextNodeID tests that an lnd node acting as the introduction +// node of a blinded path can forward a payment when the recipient identifies +// the next hop by its node ID (next_node_id) rather than a short channel ID. +// The introduction node must resolve the node ID to one of its active channels +// with that peer. +func testBlindedRouteNextNodeID(ht *lntest.HarnessTest) { + ctx, testCase := newBlindedForwardTest(ht) + defer testCase.cleanup() + + // Set up the Alice -> Bob -> Carol -> Dave network with an interceptor + // on Carol. Bob is the introduction node whose node ID resolution we + // want to exercise, and Carol's interceptor lets us deterministically + // observe that Bob successfully resolved and forwarded the HTLC. + testCase.setupNetwork(ctx, true) + + testCase.runNextNodeIDForward(ctx, nil) +} + +// testBlindedRouteNextNodeIDPrivateChannel is like testBlindedRouteNextNodeID, +// but the Bob -> Carol channel that the introduction node must resolve by node +// ID is private. This exercises the introduction node's ability to resolve the +// next node's ID to an SCID alias of an unadvertised channel (option-scid-alias +// channels are not forwardable by their confirmed SCID). +func testBlindedRouteNextNodeIDPrivateChannel(ht *lntest.HarnessTest) { + ctx, testCase := newBlindedForwardTest(ht) + defer testCase.cleanup() + + // Set up Alice -> Bob -> Carol -> Dave where the Bob -> Carol channel + // is private, so Bob must resolve Carol's node ID to that channel's + // alias. + testCase.setupNetworkPrivateMiddle(ctx) + + testCase.runNextNodeIDForward(ctx, nil) +} + +// testBlindedRouteNextNodeIDRestart tests that a blinded payment forwarded by +// node ID survives a restart of the introduction node. The HTLC is held at the +// receiver's interceptor after the introduction node (Bob) has resolved the +// next node's ID and forwarded it. Bob is then restarted, forcing it to replay +// its forwarding package and re-decode the node-ID blinded hop, after which the +// in-flight HTLC must remain intact and the payment must still settle. +func testBlindedRouteNextNodeIDRestart(ht *lntest.HarnessTest) { + ctx, testCase := newBlindedForwardTest(ht) + defer testCase.cleanup() + + testCase.setupNetwork(ctx, true) + + // Open a second, parallel Bob -> Carol channel with zero fees, matching + // the zero-fee policy runNextNodeIDForward sets on channels[1]. The + // blinded path identifies the hop by Carol's node ID, so both Bob -> + // Carol channels are valid candidates and Bob's non-strict forwarding + // picks one at random. We use this to prove that replaying the + // forwarding package after a restart re-pins the same randomly selected + // channel and does not duplicate the HTLC onto the other one. + ht.FundCoins(btcutil.SatoshiPerBitcoin, testCase.bob) + parallel := ht.OpenChannel( + testCase.bob, testCase.carol, + lntest.OpenChannelParams{Amt: chanAmt}, + ) + testCase.bob.RPC.UpdateChannelPolicy(&lnrpc.PolicyUpdateRequest{ + Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: parallel, + }, + BaseFeeMsat: 0, + FeeRatePpm: 0, + TimeLockDelta: 80, + }) + + testCase.runNextNodeIDForward(ctx, func() { + hash := sha256.Sum256(testCase.preimage[:]) + + // Non-strict forwarding picked one of the two Bob -> Carol + // channels at random. Find which one currently carries the + // outgoing HTLC so we can assert it stays there across the + // restart. + chosen, other := testCase.channels[1], parallel + if channelHasHTLC(ht, testCase.bob, parallel, hash[:]) { + chosen, other = parallel, testCase.channels[1] + } + + // Restart the introduction node while the HTLC is held at + // Carol's interceptor. On startup Bob replays its forwarding + // package and must re-decode the node-ID blinded hop without + // disturbing the already forwarded HTLC. + ht.RestartNode(testCase.bob) + ht.EnsureConnected(testCase.alice, testCase.bob) + ht.EnsureConnected(testCase.bob, testCase.carol) + + // After replaying its forwarding package, the in-flight HTLC + // must still be on the originally selected channel and must not + // have been duplicated onto the other Bob -> Carol channel. Bob + // therefore holds exactly two active HTLCs: the incoming one + // from Alice and the single outgoing one to Carol. + ht.AssertOutgoingHTLCActive(testCase.bob, chosen, hash[:]) + ht.AssertHTLCNotActive(testCase.bob, other, hash[:]) + ht.AssertNumActiveHtlcs(testCase.bob, 2) + }) +} + +// channelHasHTLC reports whether the given channel currently has a pending +// HTLC locked in for the provided payment hash. +func channelHasHTLC(ht *lntest.HarnessTest, hn *node.HarnessNode, + cp *lnrpc.ChannelPoint, hash []byte) bool { + + channel := ht.GetChannelByChanPoint(hn, cp) + for _, htlc := range channel.PendingHtlcs { + if bytes.Equal(htlc.HashLock, hash) { + return true + } + } + + return false +} + +// runNextNodeIDForward drives a payment along a blinded path whose non-final +// hops identify the next hop by node ID, asserting that the lnd introduction +// node (Bob) resolves the node ID to one of its channels and forwards the HTLC +// to Carol, who settles it via her interceptor. If midFlight is non-nil it is +// invoked while the HTLC is held at Carol's interceptor, before it is settled, +// letting callers exercise behaviour such as restarting the introduction node. +func (b *blindedForwardTest) runNextNodeIDForward(ctx context.Context, + midFlight func()) { + + ht := b.ht + + // Since buildBlindedPathWithNextNodeID constructs a path with zero + // fees to keep routing math trivial, we must update Bob's outgoing + // channel policy to have zero fees so that forwarding is not rejected + // with FeeInsufficient. + bobUpdateReq := &lnrpc.PolicyUpdateRequest{ + Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{ + ChanPoint: b.channels[1], + }, + BaseFeeMsat: 0, + FeeRatePpm: 0, + TimeLockDelta: 80, + } + b.bob.RPC.UpdateChannelPolicy(bobUpdateReq) + + const paymentAmt = 10_000_000 + blindedPath := b.buildBlindedPathWithNextNodeID(paymentAmt) + route := b.createRouteToBlinded(paymentAmt, blindedPath) + + hash := sha256.Sum256(b.preimage[:]) + sendReq := &routerrpc.SendToRouteRequest{ + PaymentHash: hash[:], + Route: route, + } + + // Dispatch the payment in the background since the HTLC will be held by + // Carol's interceptor until we resolve it. + done := make(chan struct{}) + go func() { + defer close(done) + + htlcAttempt, err := b.alice.RPC.Router.SendToRouteV2( + ctx, sendReq, + ) + require.NoError(ht, err) + require.Equal( + ht, lnrpc.HTLCAttempt_SUCCEEDED, htlcAttempt.Status, + ) + }() + + // Bob holding two active HTLCs (one incoming from Alice, one outgoing + // to Carol) demonstrates that Bob (the lnd introduction node) resolved + // Carol's node ID and forwarded the HTLC onwards. We assert on the + // count rather than a specific Bob -> Carol channel because non-strict + // forwarding may pick any of Bob's channels to Carol. + ht.AssertOutgoingHTLCActive(b.alice, b.channels[0], hash[:]) + ht.AssertNumActiveHtlcs(b.bob, 2) + + // Carol intercepts the forwarded HTLC, confirming that the introduction + // node's resolution and forwarding succeeded. Settle it with the + // preimage so that Alice's payment completes successfully. + interceptor := b.carolInterceptor + carolHTLC, err := interceptor.Recv() + require.NoError(ht, err) + + // Carol's own onward hop to Dave is also identified by node ID, so her + // intercept request must expose Dave's pubkey and flag the node-ID + // forward with the sentinel outgoing channel rather than a zero SCID. + require.Equal( + ht, htlcswitch.NodeIDForwardSCID, + carolHTLC.OutgoingRequestedChanId, + ) + require.Equal(ht, b.dave.PubKey[:], carolHTLC.OutgoingRequestedNodeId) + + // Run any caller-supplied step while the HTLC is held mid-flight. + if midFlight != nil { + midFlight() + } + + err = interceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{ + IncomingCircuitKey: carolHTLC.IncomingCircuitKey, + Action: routerrpc.ResolveHoldForwardAction_SETTLE, + Preimage: b.preimage[:], + }) + require.NoError(ht, err) + + select { + case <-done: + case <-time.After(defaultTimeout): + require.Fail(ht, "timeout waiting for payment to complete") + } +} + // testPartiallySpecifiedBlindedPath tests lnd's ability to: // - Assert the error when attempting to create a blinded payment with an // invalid partially specified path. diff --git a/lnrpc/routerrpc/forward_interceptor.go b/lnrpc/routerrpc/forward_interceptor.go index 61adf8f2b..a1a065ff5 100644 --- a/lnrpc/routerrpc/forward_interceptor.go +++ b/lnrpc/routerrpc/forward_interceptor.go @@ -100,6 +100,17 @@ func (r *forwardInterceptor) onIntercept( InWireCustomRecords: htlc.InWireCustomRecords, } + // A node-ID forward has no requested outgoing channel. Expose the + // requested pubkey and report the reserved NodeIDForwardSCID sentinel + // rather than a zero SCID. Older un-upgraded protobuf clients do not + // know about outgoing_requested_node_id and would otherwise interpret + // a zero SCID as an exit hop. + htlc.OutgoingNodeID.WhenSome(func(nodeID [33]byte) { + interceptionRequest.OutgoingRequestedNodeId = nodeID[:] + interceptionRequest.OutgoingRequestedChanId = + htlcswitch.NodeIDForwardSCID + }) + return r.stream.Send(interceptionRequest) } diff --git a/lnrpc/routerrpc/router.pb.go b/lnrpc/routerrpc/router.pb.go index 2a7b2deae..1496cdd14 100644 --- a/lnrpc/routerrpc/router.pb.go +++ b/lnrpc/routerrpc/router.pb.go @@ -3088,7 +3088,8 @@ type ForwardHtlcInterceptRequest struct { // The requested outgoing channel id for this forwarded htlc. Because of // non-strict forwarding, this isn't necessarily the channel over which the // packet will be forwarded eventually. A different channel to the same peer - // may be selected as well. + // may be selected as well. This is set to a sentinel value (all bits set) + // if the outgoing_requested_node_id is specified for blinded routes. OutgoingRequestedChanId uint64 `protobuf:"varint,7,opt,name=outgoing_requested_chan_id,json=outgoingRequestedChanId,proto3" json:"outgoing_requested_chan_id,omitempty"` // The outgoing htlc amount. OutgoingAmountMsat uint64 `protobuf:"varint,3,opt,name=outgoing_amount_msat,json=outgoingAmountMsat,proto3" json:"outgoing_amount_msat,omitempty"` @@ -3104,6 +3105,19 @@ type ForwardHtlcInterceptRequest struct { AutoFailHeight int32 `protobuf:"varint,10,opt,name=auto_fail_height,json=autoFailHeight,proto3" json:"auto_fail_height,omitempty"` // The custom records of the peer's incoming p2p wire message. InWireCustomRecords map[uint64][]byte `protobuf:"bytes,11,rep,name=in_wire_custom_records,json=inWireCustomRecords,proto3" json:"in_wire_custom_records,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + // The requested outgoing node for a blinded forward. When non-empty, this + // field contains exactly one 33-byte compressed public key and + // outgoing_requested_chan_id is set to 18446744073709551615 + // (0xffffffffffffffff). Clients MUST NOT interpret that value as an actual + // channel ID; the presence of this field identifies a node-addressed + // forward. + // + // The possible next-hop representations are: + // + // node ID empty, channel ID 0: final receive; + // node ID empty, ordinary channel ID: channel-addressed forward; + // node ID present, channel ID MaxUint64: node-addressed forward. + OutgoingRequestedNodeId []byte `protobuf:"bytes,12,opt,name=outgoing_requested_node_id,json=outgoingRequestedNodeId,proto3" json:"outgoing_requested_node_id,omitempty"` } func (x *ForwardHtlcInterceptRequest) Reset() { @@ -3215,6 +3229,13 @@ func (x *ForwardHtlcInterceptRequest) GetInWireCustomRecords() map[uint64][]byte return nil } +func (x *ForwardHtlcInterceptRequest) GetOutgoingRequestedNodeId() []byte { + if x != nil { + return x.OutgoingRequestedNodeId + } + return nil +} + // * // ForwardHtlcInterceptResponse enables the caller to resolve a previously hold // forward. The caller can choose either to: @@ -4132,7 +4153,7 @@ var file_routerrpc_router_proto_rawDesc = []byte{ 0x0a, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x68, 0x74, 0x6c, 0x63, 0x49, 0x64, - 0x22, 0xa7, 0x06, 0x0a, 0x1b, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x48, 0x74, 0x6c, 0x63, + 0x22, 0xe4, 0x06, 0x0a, 0x1b, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x48, 0x74, 0x6c, 0x63, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x47, 0x0a, 0x14, 0x69, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, @@ -4174,257 +4195,260 @@ var file_routerrpc_router_proto_rawDesc = []byte{ 0x73, 0x74, 0x2e, 0x49, 0x6e, 0x57, 0x69, 0x72, 0x65, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x13, 0x69, 0x6e, 0x57, 0x69, 0x72, 0x65, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, - 0x1a, 0x40, 0x0a, 0x12, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, - 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x1a, 0x46, 0x0a, 0x18, 0x49, 0x6e, 0x57, 0x69, 0x72, 0x65, 0x43, 0x75, 0x73, 0x74, - 0x6f, 0x6d, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, - 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x6b, 0x65, 0x79, - 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xb9, 0x04, 0x0a, 0x1c, 0x46, - 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x48, 0x74, 0x6c, 0x63, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, - 0x65, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x14, 0x69, - 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x5f, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x6f, 0x75, 0x74, - 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x4b, 0x65, 0x79, - 0x52, 0x12, 0x69, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, - 0x74, 0x4b, 0x65, 0x79, 0x12, 0x3b, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, - 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x48, 0x6f, 0x6c, 0x64, 0x46, 0x6f, 0x72, 0x77, - 0x61, 0x72, 0x64, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x08, 0x70, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x27, 0x0a, - 0x0f, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x4d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x3d, 0x0a, 0x0c, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, - 0x65, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x6c, - 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x2e, 0x46, 0x61, 0x69, - 0x6c, 0x75, 0x72, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x0b, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, - 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x69, 0x6e, 0x5f, 0x61, 0x6d, 0x6f, 0x75, - 0x6e, 0x74, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x69, - 0x6e, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6f, - 0x75, 0x74, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x07, - 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x6f, 0x75, 0x74, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x4d, - 0x73, 0x61, 0x74, 0x12, 0x78, 0x0a, 0x17, 0x6f, 0x75, 0x74, 0x5f, 0x77, 0x69, 0x72, 0x65, 0x5f, - 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x08, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x41, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, - 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x48, 0x74, 0x6c, 0x63, 0x49, 0x6e, 0x74, 0x65, - 0x72, 0x63, 0x65, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x4f, 0x75, + 0x12, 0x3b, 0x0a, 0x1a, 0x6f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x5f, 0x72, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0c, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x17, 0x6f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x1a, 0x40, 0x0a, + 0x12, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, + 0x46, 0x0a, 0x18, 0x49, 0x6e, 0x57, 0x69, 0x72, 0x65, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, + 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, + 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xb9, 0x04, 0x0a, 0x1c, 0x46, 0x6f, 0x72, 0x77, + 0x61, 0x72, 0x64, 0x48, 0x74, 0x6c, 0x63, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x14, 0x69, 0x6e, 0x63, 0x6f, + 0x6d, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x5f, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, + 0x70, 0x63, 0x2e, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x12, 0x69, + 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x4b, 0x65, + 0x79, 0x12, 0x3b, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x23, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, + 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x48, 0x6f, 0x6c, 0x64, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, + 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, + 0x0a, 0x08, 0x70, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x08, 0x70, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x66, 0x61, + 0x69, 0x6c, 0x75, 0x72, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x4d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x12, 0x3d, 0x0a, 0x0c, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x5f, 0x63, + 0x6f, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x6c, 0x6e, 0x72, 0x70, + 0x63, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, + 0x65, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x0b, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x43, 0x6f, + 0x64, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x69, 0x6e, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, + 0x6d, 0x73, 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x69, 0x6e, 0x41, 0x6d, + 0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6f, 0x75, 0x74, 0x5f, + 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x0d, 0x6f, 0x75, 0x74, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x73, 0x61, 0x74, + 0x12, 0x78, 0x0a, 0x17, 0x6f, 0x75, 0x74, 0x5f, 0x77, 0x69, 0x72, 0x65, 0x5f, 0x63, 0x75, 0x73, + 0x74, 0x6f, 0x6d, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x41, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x6f, + 0x72, 0x77, 0x61, 0x72, 0x64, 0x48, 0x74, 0x6c, 0x63, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, + 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x4f, 0x75, 0x74, 0x57, 0x69, + 0x72, 0x65, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x14, 0x6f, 0x75, 0x74, 0x57, 0x69, 0x72, 0x65, 0x43, 0x75, 0x73, + 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x1a, 0x47, 0x0a, 0x19, 0x4f, 0x75, 0x74, 0x57, 0x69, 0x72, 0x65, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x63, 0x6f, 0x72, - 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x14, 0x6f, 0x75, 0x74, 0x57, 0x69, 0x72, 0x65, - 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x1a, 0x47, 0x0a, - 0x19, 0x4f, 0x75, 0x74, 0x57, 0x69, 0x72, 0x65, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, - 0x63, 0x6f, 0x72, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x82, 0x01, 0x0a, 0x17, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x43, 0x68, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x32, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x70, 0x6f, 0x69, 0x6e, 0x74, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, - 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x09, 0x63, 0x68, 0x61, - 0x6e, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x33, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, - 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x41, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x1a, 0x0a, 0x18, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x43, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x41, 0x6c, - 0x69, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x0a, - 0x61, 0x6c, 0x69, 0x61, 0x73, 0x5f, 0x6d, 0x61, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x0f, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x4d, 0x61, - 0x70, 0x52, 0x09, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x4d, 0x61, 0x70, 0x73, 0x22, 0x44, 0x0a, 0x12, - 0x41, 0x64, 0x64, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x0a, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x5f, 0x6d, 0x61, 0x70, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x41, - 0x6c, 0x69, 0x61, 0x73, 0x4d, 0x61, 0x70, 0x52, 0x09, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x4d, 0x61, - 0x70, 0x73, 0x22, 0x46, 0x0a, 0x14, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x69, 0x61, - 0x73, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x0a, 0x61, 0x6c, - 0x69, 0x61, 0x73, 0x5f, 0x6d, 0x61, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, - 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x4d, 0x61, 0x70, 0x52, - 0x09, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x4d, 0x61, 0x70, 0x73, 0x22, 0x47, 0x0a, 0x15, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x0a, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x5f, 0x6d, 0x61, 0x70, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, - 0x41, 0x6c, 0x69, 0x61, 0x73, 0x4d, 0x61, 0x70, 0x52, 0x09, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x4d, - 0x61, 0x70, 0x73, 0x22, 0x2c, 0x0a, 0x14, 0x46, 0x69, 0x6e, 0x64, 0x42, 0x61, 0x73, 0x65, 0x41, - 0x6c, 0x69, 0x61, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x61, - 0x6c, 0x69, 0x61, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x61, 0x6c, 0x69, 0x61, - 0x73, 0x22, 0x2b, 0x0a, 0x15, 0x46, 0x69, 0x6e, 0x64, 0x42, 0x61, 0x73, 0x65, 0x41, 0x6c, 0x69, - 0x61, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x61, - 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x62, 0x61, 0x73, 0x65, 0x2a, 0x81, - 0x04, 0x0a, 0x0d, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, - 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0d, 0x0a, - 0x09, 0x4e, 0x4f, 0x5f, 0x44, 0x45, 0x54, 0x41, 0x49, 0x4c, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, - 0x4f, 0x4e, 0x49, 0x4f, 0x4e, 0x5f, 0x44, 0x45, 0x43, 0x4f, 0x44, 0x45, 0x10, 0x02, 0x12, 0x15, - 0x0a, 0x11, 0x4c, 0x49, 0x4e, 0x4b, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x45, 0x4c, 0x49, 0x47, 0x49, - 0x42, 0x4c, 0x45, 0x10, 0x03, 0x12, 0x14, 0x0a, 0x10, 0x4f, 0x4e, 0x5f, 0x43, 0x48, 0x41, 0x49, - 0x4e, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x04, 0x12, 0x14, 0x0a, 0x10, 0x48, - 0x54, 0x4c, 0x43, 0x5f, 0x45, 0x58, 0x43, 0x45, 0x45, 0x44, 0x53, 0x5f, 0x4d, 0x41, 0x58, 0x10, - 0x05, 0x12, 0x18, 0x0a, 0x14, 0x49, 0x4e, 0x53, 0x55, 0x46, 0x46, 0x49, 0x43, 0x49, 0x45, 0x4e, - 0x54, 0x5f, 0x42, 0x41, 0x4c, 0x41, 0x4e, 0x43, 0x45, 0x10, 0x06, 0x12, 0x16, 0x0a, 0x12, 0x49, - 0x4e, 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x46, 0x4f, 0x52, 0x57, 0x41, 0x52, - 0x44, 0x10, 0x07, 0x12, 0x13, 0x0a, 0x0f, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x41, 0x44, 0x44, 0x5f, - 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x08, 0x12, 0x15, 0x0a, 0x11, 0x46, 0x4f, 0x52, 0x57, - 0x41, 0x52, 0x44, 0x53, 0x5f, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x09, 0x12, - 0x14, 0x0a, 0x10, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, - 0x4c, 0x45, 0x44, 0x10, 0x0a, 0x12, 0x15, 0x0a, 0x11, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, - 0x5f, 0x55, 0x4e, 0x44, 0x45, 0x52, 0x50, 0x41, 0x49, 0x44, 0x10, 0x0b, 0x12, 0x1b, 0x0a, 0x17, - 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x45, 0x58, 0x50, 0x49, 0x52, 0x59, 0x5f, 0x54, - 0x4f, 0x4f, 0x5f, 0x53, 0x4f, 0x4f, 0x4e, 0x10, 0x0c, 0x12, 0x14, 0x0a, 0x10, 0x49, 0x4e, 0x56, - 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x0d, 0x12, - 0x17, 0x0a, 0x13, 0x4d, 0x50, 0x50, 0x5f, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x54, - 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x0e, 0x12, 0x14, 0x0a, 0x10, 0x41, 0x44, 0x44, 0x52, - 0x45, 0x53, 0x53, 0x5f, 0x4d, 0x49, 0x53, 0x4d, 0x41, 0x54, 0x43, 0x48, 0x10, 0x0f, 0x12, 0x16, - 0x0a, 0x12, 0x53, 0x45, 0x54, 0x5f, 0x54, 0x4f, 0x54, 0x41, 0x4c, 0x5f, 0x4d, 0x49, 0x53, 0x4d, - 0x41, 0x54, 0x43, 0x48, 0x10, 0x10, 0x12, 0x15, 0x0a, 0x11, 0x53, 0x45, 0x54, 0x5f, 0x54, 0x4f, - 0x54, 0x41, 0x4c, 0x5f, 0x54, 0x4f, 0x4f, 0x5f, 0x4c, 0x4f, 0x57, 0x10, 0x11, 0x12, 0x10, 0x0a, - 0x0c, 0x53, 0x45, 0x54, 0x5f, 0x4f, 0x56, 0x45, 0x52, 0x50, 0x41, 0x49, 0x44, 0x10, 0x12, 0x12, - 0x13, 0x0a, 0x0f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x49, 0x4e, 0x56, 0x4f, 0x49, - 0x43, 0x45, 0x10, 0x13, 0x12, 0x13, 0x0a, 0x0f, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, - 0x4b, 0x45, 0x59, 0x53, 0x45, 0x4e, 0x44, 0x10, 0x14, 0x12, 0x13, 0x0a, 0x0f, 0x4d, 0x50, 0x50, - 0x5f, 0x49, 0x4e, 0x5f, 0x50, 0x52, 0x4f, 0x47, 0x52, 0x45, 0x53, 0x53, 0x10, 0x15, 0x12, 0x12, - 0x0a, 0x0e, 0x43, 0x49, 0x52, 0x43, 0x55, 0x4c, 0x41, 0x52, 0x5f, 0x52, 0x4f, 0x55, 0x54, 0x45, - 0x10, 0x16, 0x2a, 0xae, 0x01, 0x0a, 0x0c, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x12, 0x0d, 0x0a, 0x09, 0x49, 0x4e, 0x5f, 0x46, 0x4c, 0x49, 0x47, 0x48, 0x54, - 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x55, 0x43, 0x43, 0x45, 0x45, 0x44, 0x45, 0x44, 0x10, - 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x54, 0x49, 0x4d, 0x45, - 0x4f, 0x55, 0x54, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, - 0x4e, 0x4f, 0x5f, 0x52, 0x4f, 0x55, 0x54, 0x45, 0x10, 0x03, 0x12, 0x10, 0x0a, 0x0c, 0x46, 0x41, - 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x12, 0x24, 0x0a, 0x20, - 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x49, 0x4e, 0x43, 0x4f, 0x52, 0x52, 0x45, 0x43, 0x54, - 0x5f, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x44, 0x45, 0x54, 0x41, 0x49, 0x4c, 0x53, - 0x10, 0x05, 0x12, 0x1f, 0x0a, 0x1b, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x49, 0x4e, 0x53, - 0x55, 0x46, 0x46, 0x49, 0x43, 0x49, 0x45, 0x4e, 0x54, 0x5f, 0x42, 0x41, 0x4c, 0x41, 0x4e, 0x43, - 0x45, 0x10, 0x06, 0x2a, 0x51, 0x0a, 0x18, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x48, 0x6f, - 0x6c, 0x64, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, - 0x0a, 0x0a, 0x06, 0x53, 0x45, 0x54, 0x54, 0x4c, 0x45, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x46, - 0x41, 0x49, 0x4c, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x52, 0x45, 0x53, 0x55, 0x4d, 0x45, 0x10, - 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x52, 0x45, 0x53, 0x55, 0x4d, 0x45, 0x5f, 0x4d, 0x4f, 0x44, 0x49, - 0x46, 0x49, 0x45, 0x44, 0x10, 0x03, 0x2a, 0x35, 0x0a, 0x10, 0x43, 0x68, 0x61, 0x6e, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x45, 0x4e, - 0x41, 0x42, 0x4c, 0x45, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, - 0x45, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x55, 0x54, 0x4f, 0x10, 0x02, 0x32, 0xc6, 0x0e, - 0x0a, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x12, 0x40, 0x0a, 0x0d, 0x53, 0x65, 0x6e, 0x64, - 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x56, 0x32, 0x12, 0x1d, 0x2e, 0x72, 0x6f, 0x75, 0x74, - 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, - 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0e, 0x54, 0x72, - 0x61, 0x63, 0x6b, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x56, 0x32, 0x12, 0x1e, 0x2e, 0x72, + 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, + 0x02, 0x38, 0x01, 0x22, 0x82, 0x01, 0x0a, 0x17, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x68, + 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x32, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, + 0x6e, 0x65, 0x6c, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x50, 0x6f, + 0x69, 0x6e, 0x74, 0x12, 0x33, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, + 0x43, 0x68, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x1a, 0x0a, 0x18, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x43, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x41, 0x6c, 0x69, 0x61, 0x73, + 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x0a, 0x61, 0x6c, 0x69, + 0x61, 0x73, 0x5f, 0x6d, 0x61, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, + 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x4d, 0x61, 0x70, 0x52, 0x09, + 0x61, 0x6c, 0x69, 0x61, 0x73, 0x4d, 0x61, 0x70, 0x73, 0x22, 0x44, 0x0a, 0x12, 0x41, 0x64, 0x64, + 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x2e, 0x0a, 0x0a, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x5f, 0x6d, 0x61, 0x70, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x6c, 0x69, 0x61, + 0x73, 0x4d, 0x61, 0x70, 0x52, 0x09, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x4d, 0x61, 0x70, 0x73, 0x22, + 0x46, 0x0a, 0x14, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x0a, 0x61, 0x6c, 0x69, 0x61, 0x73, + 0x5f, 0x6d, 0x61, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6c, 0x6e, + 0x72, 0x70, 0x63, 0x2e, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x4d, 0x61, 0x70, 0x52, 0x09, 0x61, 0x6c, + 0x69, 0x61, 0x73, 0x4d, 0x61, 0x70, 0x73, 0x22, 0x47, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x2e, 0x0a, 0x0a, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x5f, 0x6d, 0x61, 0x70, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x6c, 0x69, + 0x61, 0x73, 0x4d, 0x61, 0x70, 0x52, 0x09, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x4d, 0x61, 0x70, 0x73, + 0x22, 0x2c, 0x0a, 0x14, 0x46, 0x69, 0x6e, 0x64, 0x42, 0x61, 0x73, 0x65, 0x41, 0x6c, 0x69, 0x61, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x6c, 0x69, 0x61, + 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x22, 0x2b, + 0x0a, 0x15, 0x46, 0x69, 0x6e, 0x64, 0x42, 0x61, 0x73, 0x65, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x61, 0x73, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x62, 0x61, 0x73, 0x65, 0x2a, 0x81, 0x04, 0x0a, 0x0d, + 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x12, 0x0b, 0x0a, + 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x4e, 0x4f, + 0x5f, 0x44, 0x45, 0x54, 0x41, 0x49, 0x4c, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x4f, 0x4e, 0x49, + 0x4f, 0x4e, 0x5f, 0x44, 0x45, 0x43, 0x4f, 0x44, 0x45, 0x10, 0x02, 0x12, 0x15, 0x0a, 0x11, 0x4c, + 0x49, 0x4e, 0x4b, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x45, 0x4c, 0x49, 0x47, 0x49, 0x42, 0x4c, 0x45, + 0x10, 0x03, 0x12, 0x14, 0x0a, 0x10, 0x4f, 0x4e, 0x5f, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x54, + 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x04, 0x12, 0x14, 0x0a, 0x10, 0x48, 0x54, 0x4c, 0x43, + 0x5f, 0x45, 0x58, 0x43, 0x45, 0x45, 0x44, 0x53, 0x5f, 0x4d, 0x41, 0x58, 0x10, 0x05, 0x12, 0x18, + 0x0a, 0x14, 0x49, 0x4e, 0x53, 0x55, 0x46, 0x46, 0x49, 0x43, 0x49, 0x45, 0x4e, 0x54, 0x5f, 0x42, + 0x41, 0x4c, 0x41, 0x4e, 0x43, 0x45, 0x10, 0x06, 0x12, 0x16, 0x0a, 0x12, 0x49, 0x4e, 0x43, 0x4f, + 0x4d, 0x50, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x46, 0x4f, 0x52, 0x57, 0x41, 0x52, 0x44, 0x10, 0x07, + 0x12, 0x13, 0x0a, 0x0f, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x41, 0x44, 0x44, 0x5f, 0x46, 0x41, 0x49, + 0x4c, 0x45, 0x44, 0x10, 0x08, 0x12, 0x15, 0x0a, 0x11, 0x46, 0x4f, 0x52, 0x57, 0x41, 0x52, 0x44, + 0x53, 0x5f, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x09, 0x12, 0x14, 0x0a, 0x10, + 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x45, 0x44, + 0x10, 0x0a, 0x12, 0x15, 0x0a, 0x11, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x55, 0x4e, + 0x44, 0x45, 0x52, 0x50, 0x41, 0x49, 0x44, 0x10, 0x0b, 0x12, 0x1b, 0x0a, 0x17, 0x49, 0x4e, 0x56, + 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x45, 0x58, 0x50, 0x49, 0x52, 0x59, 0x5f, 0x54, 0x4f, 0x4f, 0x5f, + 0x53, 0x4f, 0x4f, 0x4e, 0x10, 0x0c, 0x12, 0x14, 0x0a, 0x10, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, + 0x45, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x0d, 0x12, 0x17, 0x0a, 0x13, + 0x4d, 0x50, 0x50, 0x5f, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x54, 0x49, 0x4d, 0x45, + 0x4f, 0x55, 0x54, 0x10, 0x0e, 0x12, 0x14, 0x0a, 0x10, 0x41, 0x44, 0x44, 0x52, 0x45, 0x53, 0x53, + 0x5f, 0x4d, 0x49, 0x53, 0x4d, 0x41, 0x54, 0x43, 0x48, 0x10, 0x0f, 0x12, 0x16, 0x0a, 0x12, 0x53, + 0x45, 0x54, 0x5f, 0x54, 0x4f, 0x54, 0x41, 0x4c, 0x5f, 0x4d, 0x49, 0x53, 0x4d, 0x41, 0x54, 0x43, + 0x48, 0x10, 0x10, 0x12, 0x15, 0x0a, 0x11, 0x53, 0x45, 0x54, 0x5f, 0x54, 0x4f, 0x54, 0x41, 0x4c, + 0x5f, 0x54, 0x4f, 0x4f, 0x5f, 0x4c, 0x4f, 0x57, 0x10, 0x11, 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x45, + 0x54, 0x5f, 0x4f, 0x56, 0x45, 0x52, 0x50, 0x41, 0x49, 0x44, 0x10, 0x12, 0x12, 0x13, 0x0a, 0x0f, + 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x10, + 0x13, 0x12, 0x13, 0x0a, 0x0f, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x4b, 0x45, 0x59, + 0x53, 0x45, 0x4e, 0x44, 0x10, 0x14, 0x12, 0x13, 0x0a, 0x0f, 0x4d, 0x50, 0x50, 0x5f, 0x49, 0x4e, + 0x5f, 0x50, 0x52, 0x4f, 0x47, 0x52, 0x45, 0x53, 0x53, 0x10, 0x15, 0x12, 0x12, 0x0a, 0x0e, 0x43, + 0x49, 0x52, 0x43, 0x55, 0x4c, 0x41, 0x52, 0x5f, 0x52, 0x4f, 0x55, 0x54, 0x45, 0x10, 0x16, 0x2a, + 0xae, 0x01, 0x0a, 0x0c, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x12, 0x0d, 0x0a, 0x09, 0x49, 0x4e, 0x5f, 0x46, 0x4c, 0x49, 0x47, 0x48, 0x54, 0x10, 0x00, 0x12, + 0x0d, 0x0a, 0x09, 0x53, 0x55, 0x43, 0x43, 0x45, 0x45, 0x44, 0x45, 0x44, 0x10, 0x01, 0x12, 0x12, + 0x0a, 0x0e, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, + 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x4e, 0x4f, 0x5f, + 0x52, 0x4f, 0x55, 0x54, 0x45, 0x10, 0x03, 0x12, 0x10, 0x0a, 0x0c, 0x46, 0x41, 0x49, 0x4c, 0x45, + 0x44, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x12, 0x24, 0x0a, 0x20, 0x46, 0x41, 0x49, + 0x4c, 0x45, 0x44, 0x5f, 0x49, 0x4e, 0x43, 0x4f, 0x52, 0x52, 0x45, 0x43, 0x54, 0x5f, 0x50, 0x41, + 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x44, 0x45, 0x54, 0x41, 0x49, 0x4c, 0x53, 0x10, 0x05, 0x12, + 0x1f, 0x0a, 0x1b, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x49, 0x4e, 0x53, 0x55, 0x46, 0x46, + 0x49, 0x43, 0x49, 0x45, 0x4e, 0x54, 0x5f, 0x42, 0x41, 0x4c, 0x41, 0x4e, 0x43, 0x45, 0x10, 0x06, + 0x2a, 0x51, 0x0a, 0x18, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x48, 0x6f, 0x6c, 0x64, 0x46, + 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, + 0x53, 0x45, 0x54, 0x54, 0x4c, 0x45, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x46, 0x41, 0x49, 0x4c, + 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x52, 0x45, 0x53, 0x55, 0x4d, 0x45, 0x10, 0x02, 0x12, 0x13, + 0x0a, 0x0f, 0x52, 0x45, 0x53, 0x55, 0x4d, 0x45, 0x5f, 0x4d, 0x4f, 0x44, 0x49, 0x46, 0x49, 0x45, + 0x44, 0x10, 0x03, 0x2a, 0x35, 0x0a, 0x10, 0x43, 0x68, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x45, 0x4e, 0x41, 0x42, 0x4c, + 0x45, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x10, 0x01, + 0x12, 0x08, 0x0a, 0x04, 0x41, 0x55, 0x54, 0x4f, 0x10, 0x02, 0x32, 0xc6, 0x0e, 0x0a, 0x06, 0x52, + 0x6f, 0x75, 0x74, 0x65, 0x72, 0x12, 0x40, 0x0a, 0x0d, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x56, 0x32, 0x12, 0x1d, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, + 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0e, 0x54, 0x72, 0x61, 0x63, 0x6b, + 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x56, 0x32, 0x12, 0x1e, 0x2e, 0x72, 0x6f, 0x75, 0x74, + 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x50, 0x61, 0x79, 0x6d, 0x65, + 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, + 0x63, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0d, 0x54, + 0x72, 0x61, 0x63, 0x6b, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1f, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x50, 0x61, - 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x6c, - 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x12, 0x42, - 0x0a, 0x0d, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, - 0x1f, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x72, 0x61, 0x63, - 0x6b, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x0e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, - 0x30, 0x01, 0x12, 0x4b, 0x0a, 0x10, 0x45, 0x73, 0x74, 0x69, 0x6d, 0x61, 0x74, 0x65, 0x52, 0x6f, - 0x75, 0x74, 0x65, 0x46, 0x65, 0x65, 0x12, 0x1a, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, - 0x70, 0x63, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x65, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x52, - 0x6f, 0x75, 0x74, 0x65, 0x46, 0x65, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x51, 0x0a, 0x0b, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x1d, - 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x54, - 0x6f, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, - 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, - 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x88, - 0x02, 0x01, 0x12, 0x42, 0x0a, 0x0d, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x52, 0x6f, 0x75, 0x74, - 0x65, 0x56, 0x32, 0x12, 0x1d, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, - 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x48, 0x54, 0x4c, 0x43, 0x41, - 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x12, 0x64, 0x0a, 0x13, 0x52, 0x65, 0x73, 0x65, 0x74, 0x4d, - 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x12, 0x25, 0x2e, - 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x74, 0x4d, - 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, - 0x2e, 0x52, 0x65, 0x73, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, - 0x74, 0x72, 0x6f, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x64, 0x0a, 0x13, - 0x51, 0x75, 0x65, 0x72, 0x79, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, - 0x72, 0x6f, 0x6c, 0x12, 0x25, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, - 0x51, 0x75, 0x65, 0x72, 0x79, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, - 0x72, 0x6f, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x72, 0x6f, 0x75, - 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x4d, 0x69, 0x73, 0x73, - 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x6a, 0x0a, 0x15, 0x58, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x4d, 0x69, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x12, 0x27, 0x2e, 0x72, 0x6f, - 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x58, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x4d, - 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, - 0x2e, 0x58, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, - 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x70, - 0x0a, 0x17, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, - 0x72, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x29, 0x2e, 0x72, 0x6f, 0x75, 0x74, - 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, - 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, - 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x70, 0x0a, 0x17, 0x53, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, - 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x29, 0x2e, 0x72, 0x6f, - 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, - 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, - 0x70, 0x63, 0x2e, 0x53, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, - 0x74, 0x72, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x5b, 0x0a, 0x10, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x72, 0x6f, 0x62, 0x61, - 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x12, 0x22, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, - 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x72, 0x6f, 0x62, 0x61, 0x62, 0x69, 0x6c, - 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x72, 0x6f, 0x75, - 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x72, 0x6f, 0x62, - 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x49, 0x0a, 0x0a, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x1c, 0x2e, - 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x52, - 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x6f, - 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x52, 0x6f, 0x75, - 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x13, 0x53, 0x75, - 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x48, 0x74, 0x6c, 0x63, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x73, 0x12, 0x25, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, - 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x48, 0x74, 0x6c, 0x63, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, - 0x72, 0x72, 0x70, 0x63, 0x2e, 0x48, 0x74, 0x6c, 0x63, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, - 0x12, 0x4d, 0x0a, 0x0b, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, - 0x1d, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, - 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, - 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, - 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x03, 0x88, 0x02, 0x01, 0x30, 0x01, 0x12, - 0x4f, 0x0a, 0x0c, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, - 0x1e, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x72, 0x61, 0x63, - 0x6b, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x18, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x6d, - 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x03, 0x88, 0x02, 0x01, 0x30, 0x01, - 0x12, 0x66, 0x0a, 0x0f, 0x48, 0x74, 0x6c, 0x63, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, - 0x74, 0x6f, 0x72, 0x12, 0x27, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, - 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x48, 0x74, 0x6c, 0x63, 0x49, 0x6e, 0x74, 0x65, 0x72, - 0x63, 0x65, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x26, 0x2e, 0x72, - 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, - 0x48, 0x74, 0x6c, 0x63, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x28, 0x01, 0x30, 0x01, 0x12, 0x5b, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x22, 0x2e, 0x72, - 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, - 0x68, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x23, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x14, 0x58, 0x41, 0x64, 0x64, 0x4c, 0x6f, 0x63, - 0x61, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x12, 0x1c, 0x2e, - 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x6c, 0x69, - 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x6f, - 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x6c, 0x69, 0x61, 0x73, - 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5c, 0x0a, 0x17, 0x58, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x41, 0x6c, - 0x69, 0x61, 0x73, 0x65, 0x73, 0x12, 0x1f, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, - 0x63, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, - 0x70, 0x63, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5c, 0x0a, 0x17, 0x58, 0x46, 0x69, 0x6e, - 0x64, 0x42, 0x61, 0x73, 0x65, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x41, 0x6c, - 0x69, 0x61, 0x73, 0x12, 0x1f, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, - 0x46, 0x69, 0x6e, 0x64, 0x42, 0x61, 0x73, 0x65, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, - 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x42, 0x61, 0x73, 0x65, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6e, 0x64, 0x2f, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2f, - 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, + 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x12, + 0x4b, 0x0a, 0x10, 0x45, 0x73, 0x74, 0x69, 0x6d, 0x61, 0x74, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, + 0x46, 0x65, 0x65, 0x12, 0x1a, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, + 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x65, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1b, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x6f, 0x75, 0x74, + 0x65, 0x46, 0x65, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x51, 0x0a, 0x0b, + 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x1d, 0x2e, 0x72, 0x6f, + 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x52, 0x6f, + 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x72, 0x6f, 0x75, + 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x52, 0x6f, 0x75, + 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x88, 0x02, 0x01, 0x12, + 0x42, 0x0a, 0x0d, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x56, 0x32, + 0x12, 0x1d, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, + 0x64, 0x54, 0x6f, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x12, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x48, 0x54, 0x4c, 0x43, 0x41, 0x74, 0x74, 0x65, + 0x6d, 0x70, 0x74, 0x12, 0x64, 0x0a, 0x13, 0x52, 0x65, 0x73, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x12, 0x25, 0x2e, 0x72, 0x6f, 0x75, + 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x26, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, + 0x73, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x64, 0x0a, 0x13, 0x51, 0x75, 0x65, + 0x72, 0x79, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x12, 0x25, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, + 0x72, 0x79, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, + 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x6a, 0x0a, 0x15, 0x58, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x12, 0x27, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, + 0x72, 0x72, 0x70, 0x63, 0x2e, 0x58, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x4d, 0x69, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x28, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x58, 0x49, + 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, + 0x72, 0x6f, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x70, 0x0a, 0x17, 0x47, + 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x29, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, + 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x2a, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, + 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x70, 0x0a, + 0x17, 0x53, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x29, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, + 0x72, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, + 0x53, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x5b, 0x0a, 0x10, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x72, 0x6f, 0x62, 0x61, 0x62, 0x69, 0x6c, + 0x69, 0x74, 0x79, 0x12, 0x22, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, + 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x72, 0x6f, 0x62, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, + 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x72, 0x6f, 0x62, 0x61, 0x62, 0x69, + 0x6c, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x0a, + 0x42, 0x75, 0x69, 0x6c, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x1c, 0x2e, 0x72, 0x6f, 0x75, + 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x52, 0x6f, 0x75, 0x74, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, + 0x72, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x13, 0x53, 0x75, 0x62, 0x73, 0x63, + 0x72, 0x69, 0x62, 0x65, 0x48, 0x74, 0x6c, 0x63, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x25, + 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, + 0x72, 0x69, 0x62, 0x65, 0x48, 0x74, 0x6c, 0x63, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, + 0x63, 0x2e, 0x48, 0x74, 0x6c, 0x63, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x12, 0x4d, 0x0a, + 0x0b, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1d, 0x2e, 0x72, + 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x6f, + 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x03, 0x88, 0x02, 0x01, 0x30, 0x01, 0x12, 0x4f, 0x0a, 0x0c, + 0x54, 0x72, 0x61, 0x63, 0x6b, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1e, 0x2e, 0x72, + 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x50, 0x61, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, + 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x03, 0x88, 0x02, 0x01, 0x30, 0x01, 0x12, 0x66, 0x0a, + 0x0f, 0x48, 0x74, 0x6c, 0x63, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x6f, 0x72, + 0x12, 0x27, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x6f, 0x72, + 0x77, 0x61, 0x72, 0x64, 0x48, 0x74, 0x6c, 0x63, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x26, 0x2e, 0x72, 0x6f, 0x75, 0x74, + 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x48, 0x74, 0x6c, + 0x63, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x28, 0x01, 0x30, 0x01, 0x12, 0x5b, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, + 0x68, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x22, 0x2e, 0x72, 0x6f, 0x75, 0x74, + 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x68, 0x61, 0x6e, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, + 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x43, 0x68, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x53, 0x0a, 0x14, 0x58, 0x41, 0x64, 0x64, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x43, + 0x68, 0x61, 0x6e, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x12, 0x1c, 0x2e, 0x72, 0x6f, 0x75, + 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, + 0x72, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5c, 0x0a, 0x17, 0x58, 0x44, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x41, 0x6c, 0x69, 0x61, 0x73, + 0x65, 0x73, 0x12, 0x1f, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5c, 0x0a, 0x17, 0x58, 0x46, 0x69, 0x6e, 0x64, 0x42, 0x61, + 0x73, 0x65, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x41, 0x6c, 0x69, 0x61, 0x73, + 0x12, 0x1f, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x69, 0x6e, + 0x64, 0x42, 0x61, 0x73, 0x65, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x69, + 0x6e, 0x64, 0x42, 0x61, 0x73, 0x65, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x2f, 0x6c, 0x6e, 0x64, 0x2f, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2f, 0x72, 0x6f, 0x75, + 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/lnrpc/routerrpc/router.proto b/lnrpc/routerrpc/router.proto index 8f5502675..b6b3a906e 100644 --- a/lnrpc/routerrpc/router.proto +++ b/lnrpc/routerrpc/router.proto @@ -1003,7 +1003,8 @@ message ForwardHtlcInterceptRequest { // The requested outgoing channel id for this forwarded htlc. Because of // non-strict forwarding, this isn't necessarily the channel over which the // packet will be forwarded eventually. A different channel to the same peer - // may be selected as well. + // may be selected as well. This is set to a sentinel value (all bits set) + // if the outgoing_requested_node_id is specified for blinded routes. uint64 outgoing_requested_chan_id = 7; // The outgoing htlc amount. @@ -1025,6 +1026,19 @@ message ForwardHtlcInterceptRequest { // The custom records of the peer's incoming p2p wire message. map in_wire_custom_records = 11; + + // The requested outgoing node for a blinded forward. When non-empty, this + // field contains exactly one 33-byte compressed public key and + // outgoing_requested_chan_id is set to 18446744073709551615 + // (0xffffffffffffffff). Clients MUST NOT interpret that value as an actual + // channel ID; the presence of this field identifies a node-addressed + // forward. + // + // The possible next-hop representations are: + // node ID empty, channel ID 0: final receive; + // node ID empty, ordinary channel ID: channel-addressed forward; + // node ID present, channel ID MaxUint64: node-addressed forward. + bytes outgoing_requested_node_id = 12; } /** diff --git a/lnrpc/routerrpc/router.swagger.json b/lnrpc/routerrpc/router.swagger.json index 4fdf61663..766ea591a 100644 --- a/lnrpc/routerrpc/router.swagger.json +++ b/lnrpc/routerrpc/router.swagger.json @@ -1501,7 +1501,7 @@ "outgoing_requested_chan_id": { "type": "string", "format": "uint64", - "description": "The requested outgoing channel id for this forwarded htlc. Because of\nnon-strict forwarding, this isn't necessarily the channel over which the\npacket will be forwarded eventually. A different channel to the same peer\nmay be selected as well." + "description": "The requested outgoing channel id for this forwarded htlc. Because of\nnon-strict forwarding, this isn't necessarily the channel over which the\npacket will be forwarded eventually. A different channel to the same peer\nmay be selected as well. This is set to a sentinel value (all bits set)\nif the outgoing_requested_node_id is specified for blinded routes." }, "outgoing_amount_msat": { "type": "string", @@ -1538,6 +1538,11 @@ "format": "byte" }, "description": "The custom records of the peer's incoming p2p wire message." + }, + "outgoing_requested_node_id": { + "type": "string", + "format": "byte", + "description": "The requested outgoing node for a blinded forward. When non-empty, this\nfield contains exactly one 33-byte compressed public key and\noutgoing_requested_chan_id is set to 18446744073709551615\n(0xffffffffffffffff). Clients MUST NOT interpret that value as an actual\nchannel ID; the presence of this field identifies a node-addressed\nforward.\n\nThe possible next-hop representations are:\n node ID empty, channel ID 0: final receive;\n node ID empty, ordinary channel ID: channel-addressed forward;\n node ID present, channel ID MaxUint64: node-addressed forward." } } }, diff --git a/record/blinded_data.go b/record/blinded_data.go index 3d9b17c27..31e5e9ad7 100644 --- a/record/blinded_data.go +++ b/record/blinded_data.go @@ -31,7 +31,9 @@ type BlindedRouteData struct { // NextNodeID is the node ID of the next node on the path. In the // context of blinded path payments, this is used to indicate the - // presence of dummy hops that need to be peeled from the onion. + // presence of dummy hops that need to be peeled from the onion, or to + // identify a real next-node forwarding target when the public key is + // not ours. NextNodeID tlv.OptionalRecordT[tlv.TlvType4, *btcec.PublicKey] // PathID is a secret set of bytes that the blinded path creator will diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index 77bad02e3..a8da3f660 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -1170,7 +1170,9 @@ func testBasicGraphPathFindingCase(t *testing.T, graphInstance *testGraphInstanc require.Equal( t, route.Hops[i+1].ChannelID, - payload.FwdInfo.NextHop.ToUint64(), + payload.FwdInfo.NextHopChannel().UnwrapOr( + switchhop.Exit, + ).ToUint64(), ) } @@ -1183,7 +1185,11 @@ func testBasicGraphPathFindingCase(t *testing.T, graphInstance *testGraphInstanc // The final hop should have a next hop value of all zeroes in order // to indicate it's the exit hop. - require.Zero(t, payload.FwdInfo.NextHop.ToUint64()) + require.Zero( + t, payload.FwdInfo.NextHopChannel().UnwrapOr( + switchhop.Exit, + ).ToUint64(), + ) var expectedTotalFee lnwire.MilliSatoshi for i := 0; i < expectedHopCount; i++ { diff --git a/witness_beacon.go b/witness_beacon.go index 68c096a85..45799da33 100644 --- a/witness_beacon.go +++ b/witness_beacon.go @@ -106,14 +106,26 @@ func (p *preimageBeacon) SubscribeUpdates( }, } + // Report the forwarding next hop to the interceptor. A channel-ID next + // hop is reported directly; a node-ID next hop has no outgoing channel + // of its own, so outgoingChanID is hop.Exit and the requested node ID + // is exposed separately, exactly as the off-chain interceptor does. + // This is the requested next hop, not the channel that non-strict + // forwarding eventually selects, so we deliberately do not resolve it + // against the circuit map. The RPC boundary maps a node-ID hop to the + // NodeIDForwardSCID sentinel for the client. + // // Notify the htlc interceptor. There may be a client connected // and willing to supply a preimage. packet := &htlcswitch.InterceptedPacket{ - Hash: htlc.RHash, - IncomingExpiry: htlc.RefundTimeout, - IncomingAmount: htlc.Amt, - IncomingCircuit: inKey, - OutgoingChanID: payload.FwdInfo.NextHop, + Hash: htlc.RHash, + IncomingExpiry: htlc.RefundTimeout, + IncomingAmount: htlc.Amt, + IncomingCircuit: inKey, + OutgoingChanID: payload.FwdInfo.NextHopChannel().UnwrapOr( + hop.Exit, + ), + OutgoingNodeID: payload.FwdInfo.NextHopNode(), OutgoingExpiry: payload.FwdInfo.OutgoingCLTV, OutgoingAmount: payload.FwdInfo.AmountToForward, InOnionCustomRecords: payload.CustomRecords(), diff --git a/witness_beacon_test.go b/witness_beacon_test.go index 1edbada93..9c7cf5352 100644 --- a/witness_beacon_test.go +++ b/witness_beacon_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/htlcswitch" "github.com/lightningnetwork/lnd/htlcswitch/hop" @@ -97,6 +98,47 @@ func TestWitnessBeaconInterceptErrorCancels(t *testing.T) { p.RUnlock() } +// TestWitnessBeaconInterceptNodeID asserts that for a node-ID next hop the +// on-chain interceptor reports the exit-hop SCID (hop.Exit) together with the +// requested next node's public key, matching the off-chain interceptor. The +// next hop is not resolved against the circuit map; the RPC boundary maps +// hop.Exit to the sentinel. +func TestWitnessBeaconInterceptNodeID(t *testing.T) { + var interceptedFwd htlcswitch.InterceptedForward + interceptor := func(fwd htlcswitch.InterceptedForward) error { + interceptedFwd = fwd + + return nil + } + + p := newPreimageBeacon( + &mockWitnessCache{}, interceptor, + func(models.CircuitKey) error { + return nil + }, + ) + + var nodeID [33]byte + nodeID[0] = 0x02 + + payload := &hop.Payload{ + FwdInfo: hop.ForwardingInfo{ + NextHop: hop.NewNodeNextHop(nodeID), + }, + } + + _, err := p.SubscribeUpdates( + lnwire.NewShortChanIDFromInt(1), + &channeldb.HTLC{RHash: lntypes.Hash{1}}, + payload, []byte{2}, + ) + require.NoError(t, err) + + packet := interceptedFwd.Packet() + require.Equal(t, hop.Exit, packet.OutgoingChanID) + require.Equal(t, fn.Some(nodeID), packet.OutgoingNodeID) +} + type mockWitnessCache struct { witnessCache }