Merge pull request #11009 from lightningnetwork/backport-10942-to-v0.20.x-branch

[v0.20.x-branch] Backport #10942: htlcswitch: forward blinded payments addressed by node_id
This commit is contained in:
Olaoluwa Osuntokun 2026-08-03 13:46:04 -07:00 committed by GitHub
commit 97abb6a083
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 2001 additions and 332 deletions

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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(),
)
}

View file

@ -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

View file

@ -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,

View file

@ -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[:])
}

View file

@ -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,
},

View file

@ -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

View file

@ -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")
}
}

View file

@ -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,

View file

@ -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

View file

@ -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,

View file

@ -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) {

View file

@ -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,

View file

@ -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")
}
}

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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) {

View file

@ -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,

View file

@ -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.

View file

@ -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)
}

View file

@ -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 (

View file

@ -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<uint64, bytes> 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;
}
/**

View file

@ -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."
}
}
},

View file

@ -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

View file

@ -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++ {

View file

@ -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(),

View file

@ -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
}