From 30c4ddffcec761d639daf0d3d8fc2d5c52223e5e Mon Sep 17 00:00:00 2001 From: Yusuke Shimizu Date: Fri, 23 Jan 2026 10:07:33 +0900 Subject: [PATCH 1/7] feat: add 2-hop swaps and channel_adjacency hint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements 2-hop swap negotiation and pinned 2-hop payments (u→m→v). Adds poll capability extension for 2-hop candidate discovery via channel_adjacency (legacy neighbors_ad supported). Updates RPC/proto/docs and adds unit + integration tests. --- clightning/clightning.go | 111 ++ clightning/clightning_commands.go | 37 +- clightning/options.go | 4 +- cmd/peerswaplnd/pscli/main.go | 9 + docs/peer-protocol.md | 31 +- docs/usage.md | 84 +- lnd/client.go | 129 +++ lnd/connect.go | 2 +- lnd/peerlistener_test.go | 2 +- peerswaprpc/channel_adjacency.go | 53 + peerswaprpc/peerswaprpc.pb.go | 1272 ++++++++++++++-------- peerswaprpc/peerswaprpc.proto | 63 ++ peerswaprpc/peerswaprpc.swagger.json | 76 ++ peerswaprpc/server.go | 27 +- peersync/capability_snapshot.go | 22 + peersync/channel_adjacency.go | 206 ++++ peersync/cln_lightning_adapter.go | 9 + peersync/cln_lightning_adapter_test.go | 17 +- peersync/compatibility.go | 1 - peersync/lightning_adapter.go | 47 + peersync/lightning_adapter_test.go | 19 +- peersync/message_handler.go | 13 +- peersync/mocks/mock_glightning_client.go | 67 +- peersync/mocks/mock_lightning_client.go | 20 + peersync/peer.go | 44 +- peersync/peersync.go | 21 + peersync/ports.go | 15 + peersync/service_test.go | 77 ++ peersync/store.go | 28 + swap/actions.go | 69 +- swap/actions_twohop_test.go | 156 +++ swap/messages.go | 47 +- swap/messages_test.go | 197 ++++ swap/service.go | 157 ++- swap/service_test.go | 96 ++ swap/services.go | 2 + swap/swap.go | 12 + swap/swap_out_sender_test.go | 49 + test/grpc_retry_test.go | 32 +- test/swap_2hop_bitcoin_test.go | 286 +++++ test/swap_matrix_test.go | 12 +- 41 files changed, 2987 insertions(+), 634 deletions(-) create mode 100644 peerswaprpc/channel_adjacency.go create mode 100644 peersync/channel_adjacency.go create mode 100644 swap/actions_twohop_test.go create mode 100644 swap/messages_test.go create mode 100644 test/swap_2hop_bitcoin_test.go diff --git a/clightning/clightning.go b/clightning/clightning.go index 6d99c4b0..c9900409 100644 --- a/clightning/clightning.go +++ b/clightning/clightning.go @@ -8,6 +8,7 @@ import ( "fmt" log2 "log" "os" + "sort" "time" "github.com/btcsuite/btcd/chaincfg" @@ -198,6 +199,7 @@ type PeerChannel struct { PeerConnected bool `json:"peer_connected"` State string `json:"state"` ShortChannelId string `json:"short_channel_id,omitempty"` + Private bool `json:"private,omitempty"` TotalMsat glightning.Amount `json:"total_msat,omitempty"` ToUsMsat glightning.Amount `json:"to_us_msat,omitempty"` ReceivableMsat glightning.Amount `json:"receivable_msat,omitempty"` @@ -294,6 +296,55 @@ func (cl *ClightningClient) ReceivableMsat(scid string) (uint64, error) { return 0, fmt.Errorf("could not find a channel with scid: %s", scid) } +func (cl *ClightningClient) ChannelsToPeer(peerPubkey string) ([]string, error) { + var res ListPeerChannelsResponse + err := cl.glightning.Request(ListPeerChannelsRequest{Id: peerPubkey}, &res) + if err != nil { + return nil, err + } + + var scids []string + for _, ch := range res.Channels { + if ch.PeerId != peerPubkey || ch.ShortChannelId == "" { + continue + } + if err := cl.checkChannel(ch); err != nil { + continue + } + scids = append(scids, lightning.Scid(ch.ShortChannelId).ClnStyle()) + } + sort.Strings(scids) + return scids, nil +} + +func (cl *ClightningClient) ListChannels(ctx context.Context) ([]peersync.Channel, error) { + var res ListPeerChannelsResponse + if err := cl.glightning.Request(ListPeerChannelsRequest{}, &res); err != nil { + return nil, err + } + + channels := make([]peersync.Channel, 0, len(res.Channels)) + for _, ch := range res.Channels { + if ch.PeerId == "" || ch.ShortChannelId == "" { + continue + } + peerID, err := peersync.NewPeerID(ch.PeerId) + if err != nil { + continue + } + + channels = append(channels, peersync.Channel{ + Peer: peerID, + ChannelID: 0, + ShortChannelID: lightning.Scid(ch.ShortChannelId).ClnStyle(), + Active: ch.PeerConnected && ch.State == "CHANNELD_NORMAL", + Public: !ch.Private, + }) + } + + return channels, nil +} + // checkChannel performs a set of sanity checks id the channel is eligible for // a swap of amtSat func (cl *ClightningClient) checkChannel(ch PeerChannel) error { @@ -503,6 +554,66 @@ func (cl *ClightningClient) PayInvoiceViaChannel(payreq string, scid string) (pr return preimage, nil } +func (cl *ClightningClient) PayInvoiceVia2HopRoute(payreq string, outgoingScid string, incomingScid string, intermediaryPubkey string) (preimage string, err error) { + bolt11, err := cl.glightning.DecodeBolt11(payreq) + if err != nil { + return "", err + } + + if bolt11.AmountMsat.MSat() == 0 { + return "", fmt.Errorf("invoice has no amount") + } + + outgoingScid = lightning.Scid(outgoingScid).ClnStyle() + incomingScid = lightning.Scid(incomingScid).ClnStyle() + + route, err := cl.glightning.GetRoute( + bolt11.Payee, + bolt11.AmountMsat.MSat(), + 10.0, + uint(bolt11.MinFinalCltvExpiry+1), + "", + 0.0001, + nil, + 2, + ) + if err != nil { + return "", err + } + + if len(route) != 2 { + return "", fmt.Errorf("could not find 2-hop route") + } + if route[0].Id != intermediaryPubkey { + return "", fmt.Errorf("route does not use expected intermediary") + } + if route[0].ShortChannelId != outgoingScid || route[1].ShortChannelId != incomingScid { + return "", fmt.Errorf("route does not use expected channels") + } + + label := randomString() + _, err = cl.glightning.SendPay( + route, + bolt11.PaymentHash, + label, + bolt11.AmountMsat.MSat(), + payreq, + bolt11.PaymentSecret, + 0, + ) + if err != nil { + return "", err + } + + res, err := cl.glightning.WaitSendPay(bolt11.PaymentHash, 0) + if err != nil { + return "", err + } + + preimage = res.PaymentPreimage + return preimage, nil +} + // RebalancePayment handles the lightning payment that should re-balance the // channel. func (cl *ClightningClient) RebalancePayment(payreq string, channel string) (preimage string, err error) { diff --git a/clightning/clightning_commands.go b/clightning/clightning_commands.go index 25e33169..0dc0391c 100644 --- a/clightning/clightning_commands.go +++ b/clightning/clightning_commands.go @@ -180,6 +180,7 @@ type SwapOut struct { Asset string `json:"asset"` PremiumLimitRatePPM int64 `json:"premium_rate_limit_ppm"` Force bool `json:"force"` + PeerPubkey string `json:"peer_pubkey,omitempty"` cl *ClightningClient `json:"-"` } @@ -227,14 +228,20 @@ func (l *SwapOut) Call() (jrpc2.Result, error) { return nil, errors.New("fundingChannels is not connected") } + intermediaryPeerId := fundingChannels.Id + peerId := intermediaryPeerId + if l.PeerPubkey != "" { + peerId = l.PeerPubkey + } + // Skip this check when `force` is set. if !l.Force { - if l.cl.peerSync == nil || !l.cl.peerSync.HasCompatiblePeer(fundingChannels.Id) { + if l.cl.peerSync == nil || !l.cl.peerSync.HasCompatiblePeer(peerId) { return nil, fmt.Errorf("peer does not run peerswap") } } - if !l.cl.isPeerConnected(fundingChannels.Id) { + if !l.cl.isPeerConnected(peerId) { return nil, fmt.Errorf("peer is not connected") } @@ -255,7 +262,12 @@ func (l *SwapOut) Call() (jrpc2.Result, error) { } pk := l.cl.GetNodeId() - swapOut, err := l.cl.swaps.SwapOut(fundingChannels.Id, l.Asset, l.ShortChannelId, pk, l.SatAmt, l.PremiumLimitRatePPM) + var swapOut *swap.SwapStateMachine + if l.PeerPubkey != "" && l.PeerPubkey != intermediaryPeerId { + swapOut, err = l.cl.swaps.SwapOutTwoHop(peerId, l.Asset, l.ShortChannelId, pk, l.SatAmt, l.PremiumLimitRatePPM, intermediaryPeerId) + } else { + swapOut, err = l.cl.swaps.SwapOut(peerId, l.Asset, l.ShortChannelId, pk, l.SatAmt, l.PremiumLimitRatePPM) + } if err != nil { return nil, err } @@ -302,6 +314,7 @@ type SwapIn struct { Asset string `json:"asset"` PremiumLimitRatePPM int64 `json:"premium_limit_ppm"` Force bool `json:"force"` + PeerPubkey string `json:"peer_pubkey,omitempty"` cl *ClightningClient `json:"-"` } @@ -349,14 +362,20 @@ func (l *SwapIn) Call() (jrpc2.Result, error) { return nil, errors.New("fundingChannels is not connected") } + intermediaryPeerId := fundingChannels.Id + peerId := intermediaryPeerId + if l.PeerPubkey != "" { + peerId = l.PeerPubkey + } + // Skip this check when `force` is set. if !l.Force { - if l.cl.peerSync == nil || !l.cl.peerSync.HasCompatiblePeer(fundingChannels.Id) { + if l.cl.peerSync == nil || !l.cl.peerSync.HasCompatiblePeer(peerId) { return nil, fmt.Errorf("peer does not run peerswap") } } - if !l.cl.isPeerConnected(fundingChannels.Id) { + if !l.cl.isPeerConnected(peerId) { return nil, fmt.Errorf("peer is not connected") } switch l.Asset { @@ -373,7 +392,12 @@ func (l *SwapIn) Call() (jrpc2.Result, error) { } pk := l.cl.GetNodeId() - swapIn, err := l.cl.swaps.SwapIn(fundingChannels.Id, l.Asset, l.ShortChannelId, pk, l.SatAmt, l.PremiumLimitRatePPM) + var swapIn *swap.SwapStateMachine + if l.PeerPubkey != "" && l.PeerPubkey != intermediaryPeerId { + swapIn, err = l.cl.swaps.SwapInTwoHop(peerId, l.Asset, l.ShortChannelId, pk, l.SatAmt, l.PremiumLimitRatePPM, intermediaryPeerId) + } else { + swapIn, err = l.cl.swaps.SwapIn(peerId, l.Asset, l.ShortChannelId, pk, l.SatAmt, l.PremiumLimitRatePPM) + } if err != nil { return nil, err } @@ -586,6 +610,7 @@ func (l *ListPeers) Call() (jrpc2.Result, error) { view := format.BuildPeerView(peer.Id, capability, swaps) peerSwapPeer := peerswaprpc.NewPeerSwapPeerFromView(view) + peerSwapPeer.ChannelAdjacency = peerswaprpc.ChannelAdjacencyFromPeerState(peerState) channels, err := l.cl.glightning.ListChannelsBySource(peer.Id) if err != nil { return nil, err diff --git a/clightning/options.go b/clightning/options.go index e136f673..964e13ba 100644 --- a/clightning/options.go +++ b/clightning/options.go @@ -10,7 +10,7 @@ const ( liquidRpcUserOption = "peerswap-elementsd-rpcuser" liquidRpcPasswordOption = "peerswap-elementsd-rpcpassword" liquidRpcPasswordFilepathOption = "peerswap-elementsd-rpcpasswordfile" - liquidSwapsOption = "peerswap-elementsd-swaps" + liquidSwapsOption = "peerswap-elementsd-swaps" liquidRpcWalletOption = "peerswap-elementsd-rpcwallet" bitcoinRpcHostOption = "peerswap-bitcoin-rpchost" @@ -53,7 +53,7 @@ type PeerswapClightningConfig struct { LiquidRpcHost string `json:"liquid.rpchost"` LiquidRpcPort uint `json:"liquid.rpcport"` LiquidRpcWallet string `json:"liquid.rpcwallet"` - LiquidSwaps bool `json:"liquid.swaps"` + LiquidSwaps bool `json:"liquid.swaps"` LWKSignerName string `json:"lwksignername"` LWKWalletName string `json:"lwkwalletname"` diff --git a/cmd/peerswaplnd/pscli/main.go b/cmd/peerswaplnd/pscli/main.go index 4946db14..a074a882 100644 --- a/cmd/peerswaplnd/pscli/main.go +++ b/cmd/peerswaplnd/pscli/main.go @@ -75,6 +75,11 @@ var ( Name: "peer_pubkey", Required: true, } + swapPeerPubkeyFlag = cli.StringFlag{ + Name: "peer_pubkey", + Usage: "swap negotiation peer pubkey (optional; set for 2-hop swaps)", + Required: false, + } PremiumLimitRatePPMFlag = cli.Uint64Flag{ Name: "premium_limit_rate_ppm", Usage: "premium limit for a swap in parts per million", @@ -104,6 +109,7 @@ var ( channelIdFlag, assetFlag, PremiumLimitRatePPMFlag, + swapPeerPubkeyFlag, }, Action: swapOut, } @@ -116,6 +122,7 @@ var ( channelIdFlag, assetFlag, PremiumLimitRatePPMFlag, + swapPeerPubkeyFlag, }, Action: swapIn, } @@ -291,6 +298,7 @@ func swapIn(ctx *cli.Context) error { SwapAmount: ctx.Uint64(satAmountFlag.Name), Asset: ctx.String(assetFlag.Name), PremiumLimitRatePpm: ctx.Int64(PremiumLimitRatePPMFlag.Name), + PeerPubkey: ctx.String(swapPeerPubkeyFlag.Name), }) if err != nil { return err @@ -312,6 +320,7 @@ func swapOut(ctx *cli.Context) error { SwapAmount: ctx.Uint64(satAmountFlag.Name), Asset: ctx.String(assetFlag.Name), PremiumLimitRatePpm: ctx.Int64(PremiumLimitRatePPMFlag.Name), + PeerPubkey: ctx.String(swapPeerPubkeyFlag.Name), }) if err != nil { return err diff --git a/docs/peer-protocol.md b/docs/peer-protocol.md index bf633b20..7d48ebcc 100644 --- a/docs/peer-protocol.md +++ b/docs/peer-protocol.md @@ -54,6 +54,7 @@ The `protocol_version` is included to allow for possible changes in the future. PeerSwap utilizes custom messages as described in [BOLT#1](https://github.com/Lightning/bolts/blob/master/01-messaging.md). The types are in range `42069`-`42085`. The `payload` is JSON encoded. * Both nodes MUST ignore unexpected Messages. +* Both nodes MUST ignore unknown JSON fields in received payloads. * During a swap the involved peers MUST ensure, that there is only one active swap per channel. * Swaps are identified by a unique `swap_id` that MUST be mapped to the peers `pubkey` and MUST be checked on every message. @@ -116,7 +117,8 @@ This phase of the swap is used to agree on the swap conditions. network: string, scid: string, amount: uint64, - pubkey: string + pubkey: string, + twohop: object } ``` @@ -141,6 +143,10 @@ This phase of the swap is used to agree on the swap conditions. `pubkey` is a 33 byte compressed public key generated by the swap initiator. It is used for the spending paths in the [`opening_transaction`](#opening-transaction). +`twohop` is OPTIONAL and indicates a 2-hop swap negotiation. If present, it MUST contain: + +* `intermediary_pubkey`: the 33 byte compressed public key (hex) of the intermediary node. + ##### Requirements The sending node (swap [maker](#maker)/[initiator](#initiator)): @@ -179,7 +185,8 @@ The receiving node (swap [taker](#taker)/[responder](#responder)): protocol_version: uint64, swap_id: string, pubkey: string, - premium: int64 + premium: int64, + twohop: object } ``` @@ -191,6 +198,10 @@ The receiving node (swap [taker](#taker)/[responder](#responder)): `premium` is a compensation in Sats that the swap partner wants to be paid in order to participate in the swap. +`twohop` is OPTIONAL and indicates a 2-hop swap negotiation. If present, it MAY contain: + +* `incoming_scid`: the short channel id that the responder wants to be used as the incoming channel (2nd hop) for the 2-hop payment. + ##### Requirements The sending node (swap [taker](#taker)/[responder](#responder)): @@ -242,7 +253,8 @@ This phase of the swap is used to agree on the swap conditions. It involves one network: string, scid: string, amount: uint64, - pubkey: string + pubkey: string, + twohop: object } ``` `protocol_version` is the version of the PeerSwap peer protocol the sending node uses. @@ -266,6 +278,10 @@ This phase of the swap is used to agree on the swap conditions. It involves one `pubkey` is a 33 byte compressed public key generated by the initiator. It is used for the spending paths in the [`opening_transaction`](#opening-transaction). +`twohop` is OPTIONAL and indicates a 2-hop swap negotiation. If present, it MUST contain: + +* `intermediary_pubkey`: the 33 byte compressed public key (hex) of the intermediary node. + ##### Requirements The sending node (swap [taker](#taker)/[initiator](#initiator)): @@ -305,7 +321,8 @@ The receiving node (swap responder): swap_id: string, pubkey: string, payreq: string, - premium: uint64 + premium: uint64, + twohop: object } ``` @@ -319,6 +336,10 @@ The receiving node (swap responder): `premium` is a compensation in Sats that the swap partner wants to be paid in order to participate in the swap. +`twohop` is OPTIONAL and indicates a 2-hop swap negotiation. If present, it MAY contain: + +* `incoming_scid`: the short channel id that the responder wants to be used as the incoming channel (2nd hop) for the 2-hop payment. + ##### Requirements The sending node (swap [maker](#maker)/[responder](#responder)): @@ -569,4 +590,4 @@ This is the way to finish a swap if the invoice was not paid and the taker did n * for `btc` as asset: 0x3F0 corresponding to the CSV of 1008 * for `lbtc` as asset: 0x3C corresponding to the CSV of 60 * txin[0] script bytes: 0 - * txin[0] witness: ` ` \ No newline at end of file + * txin[0] witness: ` ` diff --git a/docs/usage.md b/docs/usage.md index 32547b2f..7d8af596 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -78,7 +78,7 @@ lightning-cli peerswap-swap-out [short channel id] [amount in sats] [asset: btc For LND: ```bash -pscli swapout --channel-id [chan_id] --sat_amt [amount in sats] --asset [btc or lbtc] --premium_limit_rate_ppm [premium limit in ppm] +pscli swapout --channel_id [chan_id] --sat_amt [amount in sats] --asset [btc or lbtc] --premium_limit_rate_ppm [premium limit in ppm] ``` ### Swap-In @@ -95,6 +95,33 @@ For LND: pscli swapin --channel_id [chan_id] --sat_amt [amount in sats] --asset [btc or lbtc] --premium_limit_rate_ppm [premium limit in ppm] ``` +### 2-hop swaps (u→m→v) + +PeerSwap can execute a swap even if the swap endpoints (`u` and `v`) do not share a direct channel, as long as there is exactly one intermediary node `m` with public channels `u–m` and `m–v`. + +Requirements (high-level): + +- `u` has a channel to the intermediary `m` (this is the channel you pass to the command). +- `m` has a public channel to `v` (for routing). +- `u` and `v` are connected as peers (TCP / Lightning peer connection), so they can exchange PeerSwap custom messages. +- `v` runs PeerSwap (the intermediary `m` does not need to). + +If an intermediary `m` also runs PeerSwap, it may advertise `channel_adjacency` in `listpeers`, which can be used as a hint for picking a reachable `v` behind `m`. + +To start a 2-hop swap, pass the channel to `m` as usual, and additionally set `peer_pubkey` to the *swap negotiation peer* `v`. + +For CLN (named parameters recommended for optional fields): +```bash +lightning-cli peerswap-swap-out short_channel_id= sat_amt= asset=btc premium_rate_limit_ppm= peer_pubkey= +lightning-cli peerswap-swap-in short_channel_id= sat_amt= asset=btc premium_rate_limit_ppm= peer_pubkey= +``` + +For LND: +```bash +pscli swapout --channel_id --sat_amt --asset btc --premium_limit_rate_ppm --peer_pubkey +pscli swapin --channel_id --sat_amt --asset btc --premium_limit_rate_ppm --peer_pubkey +``` + ## Premium The premium rate is the rate applied during a swap. There are default premium rates and peer-specific premium rates. @@ -173,34 +200,55 @@ pscli deletepeerpremiumrate --node_id [node_id] --asset [BTC|LBTC] --operation [ `listpeers` - A command that returns peers that support the PeerSwap protocol. It also gives statistics about received and sent swaps to a peer. +Note: peers may optionally advertise `channel_adjacency` as part of their regular poll messages. This can be used as an *advisory* hint for 2-hop discovery (e.g. find candidate swap peers behind an intermediary). The data is not trusted and may be stale; the actual swap still uses the 2-hop discovery step during negotiation. In `channel_adjacency`, `short_channel_id` is emitted in `"x"`-style (e.g. `"1x2x3"`). (Legacy name: `neighbors_ad`.) + Example output: ```bash [ { - "nodeid": "...", + "node_id": "", + "swaps_allowed": true, + "supported_assets": ["BTC", "LBTC"], + "channel_adjacency": { + "schema_version": 1, + "public_channels_only": true, + "max_neighbors": 20, + "truncated": false, + "neighbors": [ + { + "node_id": "", + "channels": [ + { + "channel_id": 1234567890, + "short_channel_id": "1x2x3", + "active": true + } + ] + } + ] + }, "channels": [ { - "short_channel_id": "...", + "channel_id": 1234567890, + "short_channel_id": "1:2:3", "local_balance": 7397932, "remote_balance": 2602068, - "balance": 0.7397932 + "active": true } ], - "sent": { - "total_swaps_out": 2, - "total_swaps_in": 1, - "total_sats_swapped_out": 5300000, - "total_sats_swapped_in": 302938 + "as_sender": { + "swaps_out": 2, + "swaps_in": 1, + "sats_out": 5300000, + "sats_in": 302938 }, - "received": { - "total_swaps_out": 1, - "total_swaps_in": 0, - "total_sats_swapped_out": 2400000, - "total_sats_swapped_in": 0 + "as_receiver": { + "swaps_out": 1, + "swaps_in": 0, + "sats_out": 2400000, + "sats_in": 0 }, - "total_fee_paid": 6082, - "swap_in_premium_rate": "100", - "swap_out_premium_rate": "100" + "paid_fee": 6082 } ] ``` @@ -278,4 +326,4 @@ Currently, it is not possible to set a label on the cln wallet For LND: To check the label attached to a transaction use `lncli listchaintxns`. -For LBTC transactions, elementsd `SetLabel` will attach a label to the associated address. \ No newline at end of file +For LBTC transactions, elementsd `SetLabel` will attach a label to the associated address. diff --git a/lnd/client.go b/lnd/client.go index b642bfdf..d142d48d 100644 --- a/lnd/client.go +++ b/lnd/client.go @@ -5,6 +5,8 @@ import ( "encoding/hex" "errors" "fmt" + "sort" + "strconv" "strings" "sync" "time" @@ -173,6 +175,32 @@ func (l *Client) ReceivableMsat(scid string) (uint64, error) { return 0, fmt.Errorf("could not find a channel with scid: %s", scid) } +func (l *Client) ChannelsToPeer(peerPubkey string) ([]string, error) { + r, err := l.lndClient.ListChannels(context.Background(), &lnrpc.ListChannelsRequest{ + ActiveOnly: true, + InactiveOnly: false, + PublicOnly: false, + PrivateOnly: false, + }) + if err != nil { + return nil, err + } + + var scids []string + for _, ch := range r.Channels { + if ch.RemotePubkey != peerPubkey { + continue + } + if err := l.checkChannel(ch); err != nil { + continue + } + shortID := lnwire.NewShortChanIDFromInt(ch.ChanId) + scids = append(scids, lightning.Scid(shortID.String()).ClnStyle()) + } + sort.Strings(scids) + return scids, nil +} + // checkChannel checks that a channel channel peer is connected and that the // channel is active. func (l *Client) checkChannel(ch *lnrpc.Channel) error { @@ -326,6 +354,83 @@ func (l *Client) PayInvoiceViaChannel(payreq, scid string) (preimage string, err } } +func (l *Client) PayInvoiceVia2HopRoute(payreq string, outgoingScid string, incomingScid string, intermediaryPubkey string) (preimage string, err error) { + decoded, err := l.lndClient.DecodePayReq(l.ctx, &lnrpc.PayReqString{PayReq: payreq}) + if err != nil { + return "", err + } + + outgoingChanID, err := lndScidToChanID(outgoingScid) + if err != nil { + return "", err + } + incomingChanID, err := lndScidToChanID(incomingScid) + if err != nil { + return "", err + } + lastHopPubkey, err := hex.DecodeString(intermediaryPubkey) + if err != nil { + return "", err + } + + routesRes, err := l.lndClient.QueryRoutes(context.Background(), &lnrpc.QueryRoutesRequest{ + PubKey: decoded.GetDestination(), + AmtMsat: decoded.GetNumMsat(), + FinalCltvDelta: int32(decoded.GetCltvExpiry() + int64(routing.BlockPadding) + 1), + OutgoingChanId: outgoingChanID, + LastHopPubkey: lastHopPubkey, + }) + if err != nil { + return "", err + } + + var selectedRoute *lnrpc.Route + for _, r := range routesRes.Routes { + if len(r.Hops) != 2 { + continue + } + if r.Hops[0].ChanId != outgoingChanID { + continue + } + if r.Hops[1].ChanId != incomingChanID { + continue + } + selectedRoute = r + break + } + if selectedRoute == nil { + return "", fmt.Errorf("could not find pinned 2-hop route (outgoing_scid=%s incoming_scid=%s)", outgoingScid, incomingScid) + } + + // Most invoices require a payment_addr (payment secret). QueryRoutes does + // not necessarily populate it for us, so we set it on the final hop. + if len(decoded.GetPaymentAddr()) > 0 && len(selectedRoute.Hops) > 0 { + selectedRoute.Hops[len(selectedRoute.Hops)-1].MppRecord = &lnrpc.MPPRecord{ + PaymentAddr: decoded.GetPaymentAddr(), + TotalAmtMsat: decoded.GetNumMsat(), + } + } + + paymentHash, err := hex.DecodeString(decoded.GetPaymentHash()) + if err != nil { + return "", err + } + res, err := l.routerClient.SendToRouteV2(context.Background(), &routerrpc.SendToRouteRequest{ + PaymentHash: paymentHash, + Route: selectedRoute, + }) + if err != nil { + return "", err + } + if res.Failure != nil { + return "", fmt.Errorf("payment failed: %v", res.Failure) + } + if len(res.Preimage) == 0 { + return "", errors.New("payment succeeded but preimage is missing") + } + return hex.EncodeToString(res.Preimage), nil +} + func (l *Client) RebalancePayment(payreq string, channelId string) (preimage string, err error) { return l.PayInvoiceViaChannel(payreq, channelId) } @@ -451,3 +556,27 @@ func (l *Client) probePayment(scid string, amountMsat uint64) (bool, string, err func LndShortChannelIdToCLShortChannelId(lndCI lnwire.ShortChannelID) string { return fmt.Sprintf("%dx%dx%d", lndCI.BlockHeight, lndCI.TxIndex, lndCI.TxPosition) } + +func lndScidToChanID(scid string) (uint64, error) { + scid = lightning.Scid(scid).LndStyle() + parts := strings.Split(scid, ":") + if len(parts) != 3 { + return 0, fmt.Errorf("invalid scid: %s", scid) + } + blockHeight, err := strconv.ParseUint(parts[0], 10, 32) + if err != nil { + return 0, fmt.Errorf("invalid scid: %s", scid) + } + txIndex, err := strconv.ParseUint(parts[1], 10, 32) + if err != nil { + return 0, fmt.Errorf("invalid scid: %s", scid) + } + txPosition, err := strconv.ParseUint(parts[2], 10, 16) + if err != nil { + return 0, fmt.Errorf("invalid scid: %s", scid) + } + if blockHeight > 0xFFFFFF || txIndex > 0xFFFFFF || txPosition > 0xFFFF { + return 0, fmt.Errorf("invalid scid: %s", scid) + } + return (blockHeight << 40) | (txIndex << 16) | txPosition, nil +} diff --git a/lnd/connect.go b/lnd/connect.go index ca006ccc..aa0e31e0 100644 --- a/lnd/connect.go +++ b/lnd/connect.go @@ -144,7 +144,7 @@ func GetClientConnection(ctx context.Context, cfg *peerswaplnd.LndConfig) (*grpc // tests that intentionally bounce the LND daemon. This keeps production // behavior unchanged while allowing tests to run faster without build tags. func GetClientConnectionShortBackoff(ctx context.Context, cfg *peerswaplnd.LndConfig) (*grpc.ClientConn, error) { - return getClientConnectionForTests(ctx, cfg) + return getClientConnectionForTests(ctx, cfg) } func getClientConnectionForTests(ctx context.Context, cfg *peerswaplnd.LndConfig) (*grpc.ClientConn, error) { diff --git a/lnd/peerlistener_test.go b/lnd/peerlistener_test.go index b8907539..fd99a29d 100644 --- a/lnd/peerlistener_test.go +++ b/lnd/peerlistener_test.go @@ -123,7 +123,7 @@ func TestPeerListener_Reconnect(t *testing.T) { assert.NoError(t, err) } -// TestPeerListener_Reconnect_OnGracefulStop tests that the PeerListener reconnects +// TestPeerListener_Reconnect_OnGracefulStop tests that the PeerListener reconnects // to the lnd node if the lnd node was gracefully shutdown and then restarted again. func TestPeerListener_Reconnect_OnGracefulStop(t *testing.T) { test.IsIntegrationTest(t) diff --git a/peerswaprpc/channel_adjacency.go b/peerswaprpc/channel_adjacency.go new file mode 100644 index 00000000..ce0b1443 --- /dev/null +++ b/peerswaprpc/channel_adjacency.go @@ -0,0 +1,53 @@ +package peerswaprpc + +import "github.com/elementsproject/peerswap/peersync" + +// ChannelAdjacencyFromPeerState converts the optional channel_adjacency metadata +// stored in peersync.Peer into the RPC representation. +func ChannelAdjacencyFromPeerState(peer *peersync.Peer) *ChannelAdjacency { + if peer == nil { + return nil + } + return ChannelAdjacencyFromPeerSync(peer.ChannelAdjacency()) +} + +// ChannelAdjacencyFromPeerSync converts peersync ChannelAdjacency hints into the +// peerswaprpc type used by API and CLI outputs. +func ChannelAdjacencyFromPeerSync(ad *peersync.ChannelAdjacency) *ChannelAdjacency { + if ad == nil { + return nil + } + + out := &ChannelAdjacency{ + SchemaVersion: int32(ad.SchemaVersion), + PublicChannelsOnly: ad.PublicChannelsOnly, + MaxNeighbors: int32(ad.MaxNeighbors), + Truncated: ad.Truncated, + } + + if len(ad.Neighbors) == 0 { + return out + } + + out.Neighbors = make([]*ChannelAdjacencyNeighbor, 0, len(ad.Neighbors)) + for _, neighbor := range ad.Neighbors { + rpcNeighbor := &ChannelAdjacencyNeighbor{ + NodeId: neighbor.NodeID, + } + + if len(neighbor.Channels) > 0 { + rpcNeighbor.Channels = make([]*ChannelAdjacencyChannel, 0, len(neighbor.Channels)) + for _, ch := range neighbor.Channels { + rpcNeighbor.Channels = append(rpcNeighbor.Channels, &ChannelAdjacencyChannel{ + ChannelId: ch.ChannelID, + ShortChannelId: ch.ShortChannelID, + Active: ch.Active, + }) + } + } + + out.Neighbors = append(out.Neighbors, rpcNeighbor) + } + + return out +} diff --git a/peerswaprpc/peerswaprpc.pb.go b/peerswaprpc/peerswaprpc.pb.go index fb91062c..8f2b6219 100644 --- a/peerswaprpc/peerswaprpc.pb.go +++ b/peerswaprpc/peerswaprpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.28.1 +// protoc-gen-go v1.33.0 // protoc (unknown) // source: peerswaprpc.proto @@ -448,6 +448,9 @@ type SwapOutRequest struct { Asset string `protobuf:"bytes,3,opt,name=asset,proto3" json:"asset,omitempty"` Force bool `protobuf:"varint,4,opt,name=force,proto3" json:"force,omitempty"` PremiumLimitRatePpm int64 `protobuf:"varint,5,opt,name=premium_limit_rate_ppm,json=premiumLimitRatePpm,proto3" json:"premium_limit_rate_ppm,omitempty"` + // Optional swap peer pubkey. If set, the swap negotiation messages are sent + // to this peer instead of the channel peer, enabling 2-hop swaps. + PeerPubkey string `protobuf:"bytes,6,opt,name=peer_pubkey,json=peerPubkey,proto3" json:"peer_pubkey,omitempty"` } func (x *SwapOutRequest) Reset() { @@ -517,6 +520,13 @@ func (x *SwapOutRequest) GetPremiumLimitRatePpm() int64 { return 0 } +func (x *SwapOutRequest) GetPeerPubkey() string { + if x != nil { + return x.PeerPubkey + } + return "" +} + type SwapOutResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -574,6 +584,9 @@ type SwapInRequest struct { Asset string `protobuf:"bytes,3,opt,name=asset,proto3" json:"asset,omitempty"` Force bool `protobuf:"varint,4,opt,name=force,proto3" json:"force,omitempty"` PremiumLimitRatePpm int64 `protobuf:"varint,5,opt,name=premium_limit_rate_ppm,json=premiumLimitRatePpm,proto3" json:"premium_limit_rate_ppm,omitempty"` + // Optional swap peer pubkey. If set, the swap negotiation messages are sent + // to this peer instead of the channel peer, enabling 2-hop swaps. + PeerPubkey string `protobuf:"bytes,6,opt,name=peer_pubkey,json=peerPubkey,proto3" json:"peer_pubkey,omitempty"` } func (x *SwapInRequest) Reset() { @@ -643,6 +656,13 @@ func (x *SwapInRequest) GetPremiumLimitRatePpm() int64 { return 0 } +func (x *SwapInRequest) GetPeerPubkey() string { + if x != nil { + return x.PeerPubkey + } + return "" +} + type SwapResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1414,6 +1434,16 @@ type PeerSwapPeer struct { AsReceiver *SwapStats `protobuf:"bytes,6,opt,name=as_receiver,json=asReceiver,proto3" json:"as_receiver,omitempty"` PaidFee uint64 `protobuf:"varint,7,opt,name=paid_fee,json=paidFee,proto3" json:"paid_fee,omitempty"` PeerPremium *PeerPremium `protobuf:"bytes,8,opt,name=peer_premium,json=peerPremium,proto3" json:"peer_premium,omitempty"` + // ChannelAdjacency is optional, advisory data advertised by the peer to help + // 2-hop discovery (u→m→v). + // + // Domain definition: + // - This describes the *sender's* adjacency list: LN channels from the sender + // to immediate neighbor nodes. + // - This data is a hint only. It may be stale or incorrect, and MUST NOT be + // treated as proof that a swap will succeed. The actual swap still relies + // on explicit 2-hop discovery during negotiation. + ChannelAdjacency *ChannelAdjacency `protobuf:"bytes,9,opt,name=channel_adjacency,json=channelAdjacency,proto3" json:"channel_adjacency,omitempty"` } func (x *PeerSwapPeer) Reset() { @@ -1504,6 +1534,13 @@ func (x *PeerSwapPeer) GetPeerPremium() *PeerPremium { return nil } +func (x *PeerSwapPeer) GetChannelAdjacency() *ChannelAdjacency { + if x != nil { + return x.ChannelAdjacency + } + return nil +} + type PeerSwapPeerChannel struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1583,6 +1620,224 @@ func (x *PeerSwapPeerChannel) GetShortChannelId() string { return "" } +type ChannelAdjacency struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Schema version of this nested message. This is NOT the PeerSwap protocol + // version from the outer poll message. + SchemaVersion int32 `protobuf:"varint,1,opt,name=schema_version,json=schemaVersion,proto3" json:"schema_version,omitempty"` + // Indicates whether only public channels were included. + // A "public" channel is one that is announced to the public LN graph. + PublicChannelsOnly bool `protobuf:"varint,2,opt,name=public_channels_only,json=publicChannelsOnly,proto3" json:"public_channels_only,omitempty"` + // Maximum number of neighbor nodes included in `neighbors`. + // If the sender has more neighbors, `neighbors` is truncated. + MaxNeighbors int32 `protobuf:"varint,3,opt,name=max_neighbors,json=maxNeighbors,proto3" json:"max_neighbors,omitempty"` + // Neighbor nodes adjacent to the sender (remote node pubkeys) and their + // connecting channels. + Neighbors []*ChannelAdjacencyNeighbor `protobuf:"bytes,4,rep,name=neighbors,proto3" json:"neighbors,omitempty"` + // True if the neighbor list was truncated due to `max_neighbors`. + Truncated bool `protobuf:"varint,5,opt,name=truncated,proto3" json:"truncated,omitempty"` +} + +func (x *ChannelAdjacency) Reset() { + *x = ChannelAdjacency{} + if protoimpl.UnsafeEnabled { + mi := &file_peerswaprpc_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ChannelAdjacency) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChannelAdjacency) ProtoMessage() {} + +func (x *ChannelAdjacency) ProtoReflect() protoreflect.Message { + mi := &file_peerswaprpc_proto_msgTypes[25] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChannelAdjacency.ProtoReflect.Descriptor instead. +func (*ChannelAdjacency) Descriptor() ([]byte, []int) { + return file_peerswaprpc_proto_rawDescGZIP(), []int{25} +} + +func (x *ChannelAdjacency) GetSchemaVersion() int32 { + if x != nil { + return x.SchemaVersion + } + return 0 +} + +func (x *ChannelAdjacency) GetPublicChannelsOnly() bool { + if x != nil { + return x.PublicChannelsOnly + } + return false +} + +func (x *ChannelAdjacency) GetMaxNeighbors() int32 { + if x != nil { + return x.MaxNeighbors + } + return 0 +} + +func (x *ChannelAdjacency) GetNeighbors() []*ChannelAdjacencyNeighbor { + if x != nil { + return x.Neighbors + } + return nil +} + +func (x *ChannelAdjacency) GetTruncated() bool { + if x != nil { + return x.Truncated + } + return false +} + +type ChannelAdjacencyNeighbor struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Node pubkey of the adjacent neighbor, as a 33-byte compressed secp256k1 + // public key, hex-encoded (66 hex characters). + NodeId string `protobuf:"bytes,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` + // One or more channels connecting the sender to `node_id`. + Channels []*ChannelAdjacencyChannel `protobuf:"bytes,2,rep,name=channels,proto3" json:"channels,omitempty"` +} + +func (x *ChannelAdjacencyNeighbor) Reset() { + *x = ChannelAdjacencyNeighbor{} + if protoimpl.UnsafeEnabled { + mi := &file_peerswaprpc_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ChannelAdjacencyNeighbor) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChannelAdjacencyNeighbor) ProtoMessage() {} + +func (x *ChannelAdjacencyNeighbor) ProtoReflect() protoreflect.Message { + mi := &file_peerswaprpc_proto_msgTypes[26] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChannelAdjacencyNeighbor.ProtoReflect.Descriptor instead. +func (*ChannelAdjacencyNeighbor) Descriptor() ([]byte, []int) { + return file_peerswaprpc_proto_rawDescGZIP(), []int{26} +} + +func (x *ChannelAdjacencyNeighbor) GetNodeId() string { + if x != nil { + return x.NodeId + } + return "" +} + +func (x *ChannelAdjacencyNeighbor) GetChannels() []*ChannelAdjacencyChannel { + if x != nil { + return x.Channels + } + return nil +} + +type ChannelAdjacencyChannel struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Optional numeric channel id, if available in the backend. + // + // For LND, this corresponds to `lnrpc.Channel.chan_id` (a packed BOLT-07 + // short_channel_id). + // For CLN, this is not available and is set to 0. + ChannelId uint64 `protobuf:"varint,1,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"` + // BOLT-07 short_channel_id formatted as "blockxtransactionxoutput", + // for example "1x2x3". + ShortChannelId string `protobuf:"bytes,2,opt,name=short_channel_id,json=shortChannelId,proto3" json:"short_channel_id,omitempty"` + // True if the channel is currently usable for forwarding (from the sender's + // local perspective). + Active bool `protobuf:"varint,3,opt,name=active,proto3" json:"active,omitempty"` +} + +func (x *ChannelAdjacencyChannel) Reset() { + *x = ChannelAdjacencyChannel{} + if protoimpl.UnsafeEnabled { + mi := &file_peerswaprpc_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ChannelAdjacencyChannel) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChannelAdjacencyChannel) ProtoMessage() {} + +func (x *ChannelAdjacencyChannel) ProtoReflect() protoreflect.Message { + mi := &file_peerswaprpc_proto_msgTypes[27] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChannelAdjacencyChannel.ProtoReflect.Descriptor instead. +func (*ChannelAdjacencyChannel) Descriptor() ([]byte, []int) { + return file_peerswaprpc_proto_rawDescGZIP(), []int{27} +} + +func (x *ChannelAdjacencyChannel) GetChannelId() uint64 { + if x != nil { + return x.ChannelId + } + return 0 +} + +func (x *ChannelAdjacencyChannel) GetShortChannelId() string { + if x != nil { + return x.ShortChannelId + } + return "" +} + +func (x *ChannelAdjacencyChannel) GetActive() bool { + if x != nil { + return x.Active + } + return false +} + type SwapStats struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1597,7 +1852,7 @@ type SwapStats struct { func (x *SwapStats) Reset() { *x = SwapStats{} if protoimpl.UnsafeEnabled { - mi := &file_peerswaprpc_proto_msgTypes[25] + mi := &file_peerswaprpc_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1610,7 +1865,7 @@ func (x *SwapStats) String() string { func (*SwapStats) ProtoMessage() {} func (x *SwapStats) ProtoReflect() protoreflect.Message { - mi := &file_peerswaprpc_proto_msgTypes[25] + mi := &file_peerswaprpc_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1623,7 +1878,7 @@ func (x *SwapStats) ProtoReflect() protoreflect.Message { // Deprecated: Use SwapStats.ProtoReflect.Descriptor instead. func (*SwapStats) Descriptor() ([]byte, []int) { - return file_peerswaprpc_proto_rawDescGZIP(), []int{25} + return file_peerswaprpc_proto_rawDescGZIP(), []int{28} } func (x *SwapStats) GetSwapsOut() uint64 { @@ -1670,7 +1925,7 @@ type Policy struct { func (x *Policy) Reset() { *x = Policy{} if protoimpl.UnsafeEnabled { - mi := &file_peerswaprpc_proto_msgTypes[26] + mi := &file_peerswaprpc_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1683,7 +1938,7 @@ func (x *Policy) String() string { func (*Policy) ProtoMessage() {} func (x *Policy) ProtoReflect() protoreflect.Message { - mi := &file_peerswaprpc_proto_msgTypes[26] + mi := &file_peerswaprpc_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1696,7 +1951,7 @@ func (x *Policy) ProtoReflect() protoreflect.Message { // Deprecated: Use Policy.ProtoReflect.Descriptor instead. func (*Policy) Descriptor() ([]byte, []int) { - return file_peerswaprpc_proto_rawDescGZIP(), []int{26} + return file_peerswaprpc_proto_rawDescGZIP(), []int{29} } func (x *Policy) GetReserveOnchainMsat() uint64 { @@ -1752,7 +2007,7 @@ type AllowSwapRequestsRequest struct { func (x *AllowSwapRequestsRequest) Reset() { *x = AllowSwapRequestsRequest{} if protoimpl.UnsafeEnabled { - mi := &file_peerswaprpc_proto_msgTypes[27] + mi := &file_peerswaprpc_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1765,7 +2020,7 @@ func (x *AllowSwapRequestsRequest) String() string { func (*AllowSwapRequestsRequest) ProtoMessage() {} func (x *AllowSwapRequestsRequest) ProtoReflect() protoreflect.Message { - mi := &file_peerswaprpc_proto_msgTypes[27] + mi := &file_peerswaprpc_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1778,7 +2033,7 @@ func (x *AllowSwapRequestsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AllowSwapRequestsRequest.ProtoReflect.Descriptor instead. func (*AllowSwapRequestsRequest) Descriptor() ([]byte, []int) { - return file_peerswaprpc_proto_rawDescGZIP(), []int{27} + return file_peerswaprpc_proto_rawDescGZIP(), []int{30} } func (x *AllowSwapRequestsRequest) GetAllow() bool { @@ -1799,7 +2054,7 @@ type AllowSwapRequestsResponse struct { func (x *AllowSwapRequestsResponse) Reset() { *x = AllowSwapRequestsResponse{} if protoimpl.UnsafeEnabled { - mi := &file_peerswaprpc_proto_msgTypes[28] + mi := &file_peerswaprpc_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1812,7 +2067,7 @@ func (x *AllowSwapRequestsResponse) String() string { func (*AllowSwapRequestsResponse) ProtoMessage() {} func (x *AllowSwapRequestsResponse) ProtoReflect() protoreflect.Message { - mi := &file_peerswaprpc_proto_msgTypes[28] + mi := &file_peerswaprpc_proto_msgTypes[31] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1825,7 +2080,7 @@ func (x *AllowSwapRequestsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AllowSwapRequestsResponse.ProtoReflect.Descriptor instead. func (*AllowSwapRequestsResponse) Descriptor() ([]byte, []int) { - return file_peerswaprpc_proto_rawDescGZIP(), []int{28} + return file_peerswaprpc_proto_rawDescGZIP(), []int{31} } func (x *AllowSwapRequestsResponse) GetAllow() bool { @@ -1844,7 +2099,7 @@ type Empty struct { func (x *Empty) Reset() { *x = Empty{} if protoimpl.UnsafeEnabled { - mi := &file_peerswaprpc_proto_msgTypes[29] + mi := &file_peerswaprpc_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1857,7 +2112,7 @@ func (x *Empty) String() string { func (*Empty) ProtoMessage() {} func (x *Empty) ProtoReflect() protoreflect.Message { - mi := &file_peerswaprpc_proto_msgTypes[29] + mi := &file_peerswaprpc_proto_msgTypes[32] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1870,7 +2125,7 @@ func (x *Empty) ProtoReflect() protoreflect.Message { // Deprecated: Use Empty.ProtoReflect.Descriptor instead. func (*Empty) Descriptor() ([]byte, []int) { - return file_peerswaprpc_proto_rawDescGZIP(), []int{29} + return file_peerswaprpc_proto_rawDescGZIP(), []int{32} } // PremiumRate defines the premium rate for a specific asset and operation. @@ -1887,7 +2142,7 @@ type PremiumRate struct { func (x *PremiumRate) Reset() { *x = PremiumRate{} if protoimpl.UnsafeEnabled { - mi := &file_peerswaprpc_proto_msgTypes[30] + mi := &file_peerswaprpc_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1900,7 +2155,7 @@ func (x *PremiumRate) String() string { func (*PremiumRate) ProtoMessage() {} func (x *PremiumRate) ProtoReflect() protoreflect.Message { - mi := &file_peerswaprpc_proto_msgTypes[30] + mi := &file_peerswaprpc_proto_msgTypes[33] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1913,7 +2168,7 @@ func (x *PremiumRate) ProtoReflect() protoreflect.Message { // Deprecated: Use PremiumRate.ProtoReflect.Descriptor instead. func (*PremiumRate) Descriptor() ([]byte, []int) { - return file_peerswaprpc_proto_rawDescGZIP(), []int{30} + return file_peerswaprpc_proto_rawDescGZIP(), []int{33} } func (x *PremiumRate) GetAsset() AssetType { @@ -1953,7 +2208,7 @@ type PeerPremium struct { func (x *PeerPremium) Reset() { *x = PeerPremium{} if protoimpl.UnsafeEnabled { - mi := &file_peerswaprpc_proto_msgTypes[31] + mi := &file_peerswaprpc_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1966,7 +2221,7 @@ func (x *PeerPremium) String() string { func (*PeerPremium) ProtoMessage() {} func (x *PeerPremium) ProtoReflect() protoreflect.Message { - mi := &file_peerswaprpc_proto_msgTypes[31] + mi := &file_peerswaprpc_proto_msgTypes[34] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1979,7 +2234,7 @@ func (x *PeerPremium) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerPremium.ProtoReflect.Descriptor instead. func (*PeerPremium) Descriptor() ([]byte, []int) { - return file_peerswaprpc_proto_rawDescGZIP(), []int{31} + return file_peerswaprpc_proto_rawDescGZIP(), []int{34} } func (x *PeerPremium) GetNodeId() string { @@ -2014,7 +2269,7 @@ type GetPremiumRateRequest struct { func (x *GetPremiumRateRequest) Reset() { *x = GetPremiumRateRequest{} if protoimpl.UnsafeEnabled { - mi := &file_peerswaprpc_proto_msgTypes[32] + mi := &file_peerswaprpc_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2027,7 +2282,7 @@ func (x *GetPremiumRateRequest) String() string { func (*GetPremiumRateRequest) ProtoMessage() {} func (x *GetPremiumRateRequest) ProtoReflect() protoreflect.Message { - mi := &file_peerswaprpc_proto_msgTypes[32] + mi := &file_peerswaprpc_proto_msgTypes[35] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2040,7 +2295,7 @@ func (x *GetPremiumRateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetPremiumRateRequest.ProtoReflect.Descriptor instead. func (*GetPremiumRateRequest) Descriptor() ([]byte, []int) { - return file_peerswaprpc_proto_rawDescGZIP(), []int{32} + return file_peerswaprpc_proto_rawDescGZIP(), []int{35} } func (x *GetPremiumRateRequest) GetNodeId() string { @@ -2082,7 +2337,7 @@ type DeletePremiumRateRequest struct { func (x *DeletePremiumRateRequest) Reset() { *x = DeletePremiumRateRequest{} if protoimpl.UnsafeEnabled { - mi := &file_peerswaprpc_proto_msgTypes[33] + mi := &file_peerswaprpc_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2095,7 +2350,7 @@ func (x *DeletePremiumRateRequest) String() string { func (*DeletePremiumRateRequest) ProtoMessage() {} func (x *DeletePremiumRateRequest) ProtoReflect() protoreflect.Message { - mi := &file_peerswaprpc_proto_msgTypes[33] + mi := &file_peerswaprpc_proto_msgTypes[36] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2108,7 +2363,7 @@ func (x *DeletePremiumRateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeletePremiumRateRequest.ProtoReflect.Descriptor instead. func (*DeletePremiumRateRequest) Descriptor() ([]byte, []int) { - return file_peerswaprpc_proto_rawDescGZIP(), []int{33} + return file_peerswaprpc_proto_rawDescGZIP(), []int{36} } func (x *DeletePremiumRateRequest) GetNodeId() string { @@ -2148,7 +2403,7 @@ type UpdatePremiumRateRequest struct { func (x *UpdatePremiumRateRequest) Reset() { *x = UpdatePremiumRateRequest{} if protoimpl.UnsafeEnabled { - mi := &file_peerswaprpc_proto_msgTypes[34] + mi := &file_peerswaprpc_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2161,7 +2416,7 @@ func (x *UpdatePremiumRateRequest) String() string { func (*UpdatePremiumRateRequest) ProtoMessage() {} func (x *UpdatePremiumRateRequest) ProtoReflect() protoreflect.Message { - mi := &file_peerswaprpc_proto_msgTypes[34] + mi := &file_peerswaprpc_proto_msgTypes[37] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2174,7 +2429,7 @@ func (x *UpdatePremiumRateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdatePremiumRateRequest.ProtoReflect.Descriptor instead. func (*UpdatePremiumRateRequest) Descriptor() ([]byte, []int) { - return file_peerswaprpc_proto_rawDescGZIP(), []int{34} + return file_peerswaprpc_proto_rawDescGZIP(), []int{37} } func (x *UpdatePremiumRateRequest) GetNodeId() string { @@ -2207,7 +2462,7 @@ type GetGlobalPremiumRateRequest struct { func (x *GetGlobalPremiumRateRequest) Reset() { *x = GetGlobalPremiumRateRequest{} if protoimpl.UnsafeEnabled { - mi := &file_peerswaprpc_proto_msgTypes[35] + mi := &file_peerswaprpc_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2220,7 +2475,7 @@ func (x *GetGlobalPremiumRateRequest) String() string { func (*GetGlobalPremiumRateRequest) ProtoMessage() {} func (x *GetGlobalPremiumRateRequest) ProtoReflect() protoreflect.Message { - mi := &file_peerswaprpc_proto_msgTypes[35] + mi := &file_peerswaprpc_proto_msgTypes[38] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2233,7 +2488,7 @@ func (x *GetGlobalPremiumRateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetGlobalPremiumRateRequest.ProtoReflect.Descriptor instead. func (*GetGlobalPremiumRateRequest) Descriptor() ([]byte, []int) { - return file_peerswaprpc_proto_rawDescGZIP(), []int{35} + return file_peerswaprpc_proto_rawDescGZIP(), []int{38} } func (x *GetGlobalPremiumRateRequest) GetAsset() AssetType { @@ -2265,7 +2520,7 @@ type UpdateGlobalPremiumRateRequest struct { func (x *UpdateGlobalPremiumRateRequest) Reset() { *x = UpdateGlobalPremiumRateRequest{} if protoimpl.UnsafeEnabled { - mi := &file_peerswaprpc_proto_msgTypes[36] + mi := &file_peerswaprpc_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2278,7 +2533,7 @@ func (x *UpdateGlobalPremiumRateRequest) String() string { func (*UpdateGlobalPremiumRateRequest) ProtoMessage() {} func (x *UpdateGlobalPremiumRateRequest) ProtoReflect() protoreflect.Message { - mi := &file_peerswaprpc_proto_msgTypes[36] + mi := &file_peerswaprpc_proto_msgTypes[39] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2291,7 +2546,7 @@ func (x *UpdateGlobalPremiumRateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateGlobalPremiumRateRequest.ProtoReflect.Descriptor instead. func (*UpdateGlobalPremiumRateRequest) Descriptor() ([]byte, []int) { - return file_peerswaprpc_proto_rawDescGZIP(), []int{36} + return file_peerswaprpc_proto_rawDescGZIP(), []int{39} } func (x *UpdateGlobalPremiumRateRequest) GetRate() *PremiumRate { @@ -2312,7 +2567,7 @@ type PeerSwapNodes struct { func (x *PeerSwapNodes) Reset() { *x = PeerSwapNodes{} if protoimpl.UnsafeEnabled { - mi := &file_peerswaprpc_proto_msgTypes[37] + mi := &file_peerswaprpc_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2325,7 +2580,7 @@ func (x *PeerSwapNodes) String() string { func (*PeerSwapNodes) ProtoMessage() {} func (x *PeerSwapNodes) ProtoReflect() protoreflect.Message { - mi := &file_peerswaprpc_proto_msgTypes[37] + mi := &file_peerswaprpc_proto_msgTypes[40] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2338,7 +2593,7 @@ func (x *PeerSwapNodes) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerSwapNodes.ProtoReflect.Descriptor instead. func (*PeerSwapNodes) Descriptor() ([]byte, []int) { - return file_peerswaprpc_proto_rawDescGZIP(), []int{37} + return file_peerswaprpc_proto_rawDescGZIP(), []int{40} } func (x *PeerSwapNodes) GetNodeId() string { @@ -2369,7 +2624,7 @@ var file_peerswaprpc_proto_rawDesc = []byte{ 0x28, 0x04, 0x52, 0x09, 0x73, 0x61, 0x74, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x2c, 0x0a, 0x15, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x13, 0x0a, 0x05, 0x74, 0x78, 0x5f, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x78, 0x49, 0x64, 0x22, 0xb1, 0x01, 0x0a, 0x0e, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x78, 0x49, 0x64, 0x22, 0xd2, 0x01, 0x0a, 0x0e, 0x53, 0x77, 0x61, 0x70, 0x4f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x64, 0x12, 0x1f, 0x0a, @@ -2380,346 +2635,385 @@ var file_peerswaprpc_proto_rawDesc = []byte{ 0x01, 0x28, 0x08, 0x52, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x12, 0x33, 0x0a, 0x16, 0x70, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x13, 0x70, 0x72, 0x65, 0x6d, - 0x69, 0x75, 0x6d, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x52, 0x61, 0x74, 0x65, 0x50, 0x70, 0x6d, 0x22, - 0x40, 0x0a, 0x0f, 0x53, 0x77, 0x61, 0x70, 0x4f, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x73, 0x77, 0x61, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x19, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x50, 0x72, 0x65, 0x74, - 0x74, 0x79, 0x50, 0x72, 0x69, 0x6e, 0x74, 0x53, 0x77, 0x61, 0x70, 0x52, 0x04, 0x73, 0x77, 0x61, - 0x70, 0x22, 0xb0, 0x01, 0x0a, 0x0d, 0x53, 0x77, 0x61, 0x70, 0x49, 0x6e, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, - 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, - 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x73, 0x77, 0x61, 0x70, 0x41, 0x6d, 0x6f, - 0x75, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x73, 0x73, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x61, 0x73, 0x73, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6f, 0x72, - 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x12, - 0x33, 0x0a, 0x16, 0x70, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, - 0x5f, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x13, 0x70, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x52, 0x61, 0x74, - 0x65, 0x50, 0x70, 0x6d, 0x22, 0x3d, 0x0a, 0x0c, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x73, 0x77, 0x61, 0x70, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x50, 0x72, - 0x65, 0x74, 0x74, 0x79, 0x50, 0x72, 0x69, 0x6e, 0x74, 0x53, 0x77, 0x61, 0x70, 0x52, 0x04, 0x73, - 0x77, 0x61, 0x70, 0x22, 0x29, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x77, 0x61, 0x70, 0x49, 0x64, 0x22, 0x12, - 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x22, 0x44, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x05, 0x73, 0x77, 0x61, 0x70, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, - 0x70, 0x2e, 0x50, 0x72, 0x65, 0x74, 0x74, 0x79, 0x50, 0x72, 0x69, 0x6e, 0x74, 0x53, 0x77, 0x61, - 0x70, 0x52, 0x05, 0x73, 0x77, 0x61, 0x70, 0x73, 0x22, 0x12, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, - 0x50, 0x65, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x41, 0x0a, 0x11, - 0x4c, 0x69, 0x73, 0x74, 0x50, 0x65, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x2c, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x16, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x50, 0x65, 0x65, 0x72, - 0x53, 0x77, 0x61, 0x70, 0x50, 0x65, 0x65, 0x72, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x22, - 0x19, 0x0a, 0x17, 0x52, 0x65, 0x6c, 0x6f, 0x61, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x46, - 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x31, 0x0a, 0x0e, 0x41, 0x64, - 0x64, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, - 0x70, 0x65, 0x65, 0x72, 0x5f, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x50, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x22, 0x34, 0x0a, - 0x11, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x70, 0x75, 0x62, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x50, 0x75, 0x62, - 0x6b, 0x65, 0x79, 0x22, 0x1b, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x65, 0x64, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x22, 0xdd, 0x01, 0x0a, 0x1a, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x65, 0x64, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x61, 0x0a, 0x0f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x73, 0x77, 0x61, - 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, - 0x77, 0x61, 0x70, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, - 0x64, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x53, 0x77, 0x61, 0x70, 0x73, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x52, 0x0e, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x53, 0x77, 0x61, - 0x70, 0x73, 0x1a, 0x5c, 0x0a, 0x13, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x53, - 0x77, 0x61, 0x70, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2f, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x65, 0x65, - 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x53, 0x77, 0x61, - 0x70, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, - 0x22, 0x53, 0x0a, 0x0f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x4c, - 0x69, 0x73, 0x74, 0x12, 0x40, 0x0a, 0x0f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, - 0x5f, 0x73, 0x77, 0x61, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, - 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, - 0x64, 0x53, 0x77, 0x61, 0x70, 0x52, 0x0e, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, - 0x53, 0x77, 0x61, 0x70, 0x73, 0x22, 0xd5, 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x65, 0x64, 0x53, 0x77, 0x61, 0x70, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x73, 0x73, 0x65, 0x74, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x73, 0x73, 0x65, 0x74, 0x12, 0x1d, 0x0a, - 0x0a, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x73, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x04, 0x52, 0x09, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x61, 0x74, 0x12, 0x3d, 0x0a, 0x09, - 0x73, 0x77, 0x61, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x20, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x65, 0x64, 0x53, 0x77, 0x61, 0x70, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x54, 0x79, 0x70, - 0x65, 0x52, 0x08, 0x73, 0x77, 0x61, 0x70, 0x54, 0x79, 0x70, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x72, - 0x65, 0x6a, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x72, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x25, 0x0a, 0x08, 0x53, 0x77, 0x61, 0x70, 0x54, 0x79, - 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x57, 0x41, 0x50, 0x5f, 0x49, 0x4e, 0x10, 0x00, 0x12, - 0x0c, 0x0a, 0x08, 0x53, 0x57, 0x41, 0x50, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x22, 0xcb, 0x03, - 0x0a, 0x0f, 0x50, 0x72, 0x65, 0x74, 0x74, 0x79, 0x50, 0x72, 0x69, 0x6e, 0x74, 0x53, 0x77, 0x61, - 0x70, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, - 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, - 0x12, 0x14, 0x0a, 0x05, 0x61, 0x73, 0x73, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x61, 0x73, 0x73, 0x65, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, - 0x6c, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x12, 0x14, - 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, - 0x74, 0x61, 0x74, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x6f, - 0x72, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0f, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x6f, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x64, - 0x12, 0x20, 0x0a, 0x0c, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, - 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, - 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x68, - 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, - 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x6f, 0x70, 0x65, - 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x74, 0x78, 0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0b, 0x6f, 0x70, 0x65, 0x6e, 0x69, 0x6e, 0x67, 0x54, 0x78, 0x49, 0x64, 0x12, 0x1e, 0x0a, - 0x0b, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x5f, 0x74, 0x78, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x54, 0x78, 0x49, 0x64, 0x12, 0x25, 0x0a, - 0x0e, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, - 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x4d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x12, 0x1e, 0x0a, 0x0b, 0x6c, 0x6e, 0x64, 0x5f, 0x63, 0x68, 0x61, 0x6e, - 0x5f, 0x69, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6c, 0x6e, 0x64, 0x43, 0x68, - 0x61, 0x6e, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x5f, - 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x70, 0x72, - 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xef, 0x02, 0x0a, 0x0c, - 0x50, 0x65, 0x65, 0x72, 0x53, 0x77, 0x61, 0x70, 0x50, 0x65, 0x65, 0x72, 0x12, 0x17, 0x0a, 0x07, + 0x69, 0x75, 0x6d, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x52, 0x61, 0x74, 0x65, 0x50, 0x70, 0x6d, 0x12, + 0x1f, 0x0a, 0x0b, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x50, 0x75, 0x62, 0x6b, 0x65, 0x79, + 0x22, 0x40, 0x0a, 0x0f, 0x53, 0x77, 0x61, 0x70, 0x4f, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x73, 0x77, 0x61, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x50, 0x72, 0x65, + 0x74, 0x74, 0x79, 0x50, 0x72, 0x69, 0x6e, 0x74, 0x53, 0x77, 0x61, 0x70, 0x52, 0x04, 0x73, 0x77, + 0x61, 0x70, 0x22, 0xd1, 0x01, 0x0a, 0x0d, 0x53, 0x77, 0x61, 0x70, 0x49, 0x6e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, + 0x6c, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x61, 0x6d, 0x6f, 0x75, + 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x73, 0x77, 0x61, 0x70, 0x41, 0x6d, + 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x73, 0x73, 0x65, 0x74, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x73, 0x73, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6f, + 0x72, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, + 0x12, 0x33, 0x0a, 0x16, 0x70, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x5f, 0x6c, 0x69, 0x6d, 0x69, + 0x74, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x13, 0x70, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x52, 0x61, + 0x74, 0x65, 0x50, 0x70, 0x6d, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x70, 0x75, + 0x62, 0x6b, 0x65, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, + 0x50, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x22, 0x3d, 0x0a, 0x0c, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x73, 0x77, 0x61, 0x70, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, + 0x50, 0x72, 0x65, 0x74, 0x74, 0x79, 0x50, 0x72, 0x69, 0x6e, 0x74, 0x53, 0x77, 0x61, 0x70, 0x52, + 0x04, 0x73, 0x77, 0x61, 0x70, 0x22, 0x29, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x53, 0x77, 0x61, 0x70, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x77, 0x61, 0x70, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x77, 0x61, 0x70, 0x49, 0x64, + 0x22, 0x12, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x22, 0x44, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x05, 0x73, 0x77, 0x61, + 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, + 0x77, 0x61, 0x70, 0x2e, 0x50, 0x72, 0x65, 0x74, 0x74, 0x79, 0x50, 0x72, 0x69, 0x6e, 0x74, 0x53, + 0x77, 0x61, 0x70, 0x52, 0x05, 0x73, 0x77, 0x61, 0x70, 0x73, 0x22, 0x12, 0x0a, 0x10, 0x4c, 0x69, + 0x73, 0x74, 0x50, 0x65, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x41, + 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x65, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x50, 0x65, + 0x65, 0x72, 0x53, 0x77, 0x61, 0x70, 0x50, 0x65, 0x65, 0x72, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, + 0x73, 0x22, 0x19, 0x0a, 0x17, 0x52, 0x65, 0x6c, 0x6f, 0x61, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x31, 0x0a, 0x0e, + 0x41, 0x64, 0x64, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, + 0x0a, 0x0b, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x50, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x22, + 0x34, 0x0a, 0x11, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x70, 0x75, 0x62, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x50, + 0x75, 0x62, 0x6b, 0x65, 0x79, 0x22, 0x1b, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x22, 0xdd, 0x01, 0x0a, 0x1a, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x65, 0x64, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x61, 0x0a, 0x0f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x73, + 0x77, 0x61, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x70, 0x65, 0x65, + 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x65, 0x64, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x53, 0x77, 0x61, 0x70, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0e, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x53, + 0x77, 0x61, 0x70, 0x73, 0x1a, 0x5c, 0x0a, 0x13, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, + 0x64, 0x53, 0x77, 0x61, 0x70, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, + 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2f, 0x0a, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, + 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x53, + 0x77, 0x61, 0x70, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x22, 0x53, 0x0a, 0x0f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x53, 0x77, 0x61, + 0x70, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x40, 0x0a, 0x0f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x65, 0x64, 0x5f, 0x73, 0x77, 0x61, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, + 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x65, 0x64, 0x53, 0x77, 0x61, 0x70, 0x52, 0x0e, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x65, 0x64, 0x53, 0x77, 0x61, 0x70, 0x73, 0x22, 0xd5, 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x65, 0x64, 0x53, 0x77, 0x61, 0x70, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x73, 0x73, + 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x73, 0x73, 0x65, 0x74, 0x12, + 0x1d, 0x0a, 0x0a, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x73, 0x61, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x09, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x61, 0x74, 0x12, 0x3d, + 0x0a, 0x09, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x20, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x53, 0x77, 0x61, 0x70, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x54, + 0x79, 0x70, 0x65, 0x52, 0x08, 0x73, 0x77, 0x61, 0x70, 0x54, 0x79, 0x70, 0x65, 0x12, 0x29, 0x0a, + 0x10, 0x72, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x72, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x25, 0x0a, 0x08, 0x53, 0x77, 0x61, 0x70, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x57, 0x41, 0x50, 0x5f, 0x49, 0x4e, 0x10, + 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x57, 0x41, 0x50, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x22, + 0xcb, 0x03, 0x0a, 0x0f, 0x50, 0x72, 0x65, 0x74, 0x74, 0x79, 0x50, 0x72, 0x69, 0x6e, 0x74, 0x53, + 0x77, 0x61, 0x70, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x02, 0x69, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, + 0x41, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x73, 0x73, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x61, 0x73, 0x73, 0x65, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, + 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, + 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, + 0x74, 0x6f, 0x72, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0f, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x6f, 0x72, 0x4e, 0x6f, 0x64, 0x65, + 0x49, 0x64, 0x12, 0x20, 0x0a, 0x0c, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, + 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x4e, 0x6f, + 0x64, 0x65, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x09, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, + 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x6f, + 0x70, 0x65, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x74, 0x78, 0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0b, 0x6f, 0x70, 0x65, 0x6e, 0x69, 0x6e, 0x67, 0x54, 0x78, 0x49, 0x64, 0x12, + 0x1e, 0x0a, 0x0b, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x5f, 0x74, 0x78, 0x5f, 0x69, 0x64, 0x18, 0x0c, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x54, 0x78, 0x49, 0x64, 0x12, + 0x25, 0x0a, 0x0e, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x4d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1e, 0x0a, 0x0b, 0x6c, 0x6e, 0x64, 0x5f, 0x63, 0x68, + 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6c, 0x6e, 0x64, + 0x43, 0x68, 0x61, 0x6e, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x72, 0x65, 0x6d, 0x69, 0x75, + 0x6d, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, + 0x70, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xb8, 0x03, + 0x0a, 0x0c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x77, 0x61, 0x70, 0x50, 0x65, 0x65, 0x72, 0x12, 0x17, + 0x0a, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x77, 0x61, 0x70, 0x73, + 0x5f, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, + 0x73, 0x77, 0x61, 0x70, 0x73, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x29, 0x0a, 0x10, + 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, + 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, + 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x73, 0x12, 0x39, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x6e, 0x6e, + 0x65, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x65, 0x65, 0x72, + 0x73, 0x77, 0x61, 0x70, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x77, 0x61, 0x70, 0x50, 0x65, 0x65, + 0x72, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x08, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, + 0x6c, 0x73, 0x12, 0x30, 0x0a, 0x09, 0x61, 0x73, 0x5f, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, + 0x2e, 0x53, 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x08, 0x61, 0x73, 0x53, 0x65, + 0x6e, 0x64, 0x65, 0x72, 0x12, 0x34, 0x0a, 0x0b, 0x61, 0x73, 0x5f, 0x72, 0x65, 0x63, 0x65, 0x69, + 0x76, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x65, 0x65, 0x72, + 0x73, 0x77, 0x61, 0x70, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x0a, + 0x61, 0x73, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x12, 0x19, 0x0a, 0x08, 0x70, 0x61, + 0x69, 0x64, 0x5f, 0x66, 0x65, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x70, 0x61, + 0x69, 0x64, 0x46, 0x65, 0x65, 0x12, 0x38, 0x0a, 0x0c, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x70, 0x72, + 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x65, + 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x50, 0x72, 0x65, 0x6d, 0x69, + 0x75, 0x6d, 0x52, 0x0b, 0x70, 0x65, 0x65, 0x72, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x12, + 0x47, 0x0a, 0x11, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x61, 0x64, 0x6a, 0x61, 0x63, + 0x65, 0x6e, 0x63, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x65, 0x65, + 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x41, 0x64, 0x6a, + 0x61, 0x63, 0x65, 0x6e, 0x63, 0x79, 0x52, 0x10, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x41, + 0x64, 0x6a, 0x61, 0x63, 0x65, 0x6e, 0x63, 0x79, 0x22, 0xc2, 0x01, 0x0a, 0x13, 0x50, 0x65, 0x65, + 0x72, 0x53, 0x77, 0x61, 0x70, 0x50, 0x65, 0x65, 0x72, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, + 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x64, 0x12, + 0x23, 0x0a, 0x0d, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x42, 0x61, 0x6c, + 0x61, 0x6e, 0x63, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x5f, 0x62, + 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x72, 0x65, + 0x6d, 0x6f, 0x74, 0x65, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x61, + 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x63, 0x74, + 0x69, 0x76, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x5f, 0x63, 0x68, 0x61, + 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, + 0x68, 0x6f, 0x72, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x64, 0x22, 0xf0, 0x01, + 0x0a, 0x10, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x41, 0x64, 0x6a, 0x61, 0x63, 0x65, 0x6e, + 0x63, 0x79, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x5f, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x73, 0x63, 0x68, 0x65, + 0x6d, 0x61, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x0a, 0x14, 0x70, 0x75, 0x62, + 0x6c, 0x69, 0x63, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x5f, 0x6f, 0x6e, 0x6c, + 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x43, + 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x6d, + 0x61, 0x78, 0x5f, 0x6e, 0x65, 0x69, 0x67, 0x68, 0x62, 0x6f, 0x72, 0x73, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x0c, 0x6d, 0x61, 0x78, 0x4e, 0x65, 0x69, 0x67, 0x68, 0x62, 0x6f, 0x72, 0x73, + 0x12, 0x40, 0x0a, 0x09, 0x6e, 0x65, 0x69, 0x67, 0x68, 0x62, 0x6f, 0x72, 0x73, 0x18, 0x04, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x43, + 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x41, 0x64, 0x6a, 0x61, 0x63, 0x65, 0x6e, 0x63, 0x79, 0x4e, + 0x65, 0x69, 0x67, 0x68, 0x62, 0x6f, 0x72, 0x52, 0x09, 0x6e, 0x65, 0x69, 0x67, 0x68, 0x62, 0x6f, + 0x72, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x64, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x74, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x64, + 0x22, 0x72, 0x0a, 0x18, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x41, 0x64, 0x6a, 0x61, 0x63, + 0x65, 0x6e, 0x63, 0x79, 0x4e, 0x65, 0x69, 0x67, 0x68, 0x62, 0x6f, 0x72, 0x12, 0x17, 0x0a, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6e, - 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x77, 0x61, 0x70, 0x73, 0x5f, 0x61, - 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x73, 0x77, - 0x61, 0x70, 0x73, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x29, 0x0a, 0x10, 0x73, 0x75, - 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x18, 0x03, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x41, - 0x73, 0x73, 0x65, 0x74, 0x73, 0x12, 0x39, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, - 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, - 0x61, 0x70, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x77, 0x61, 0x70, 0x50, 0x65, 0x65, 0x72, 0x43, - 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x08, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, - 0x12, 0x30, 0x0a, 0x09, 0x61, 0x73, 0x5f, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x53, - 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x08, 0x61, 0x73, 0x53, 0x65, 0x6e, 0x64, - 0x65, 0x72, 0x12, 0x34, 0x0a, 0x0b, 0x61, 0x73, 0x5f, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, - 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, - 0x61, 0x70, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x0a, 0x61, 0x73, - 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x12, 0x19, 0x0a, 0x08, 0x70, 0x61, 0x69, 0x64, - 0x5f, 0x66, 0x65, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x70, 0x61, 0x69, 0x64, - 0x46, 0x65, 0x65, 0x12, 0x38, 0x0a, 0x0c, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x70, 0x72, 0x65, 0x6d, - 0x69, 0x75, 0x6d, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x65, 0x65, 0x72, - 0x73, 0x77, 0x61, 0x70, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, - 0x52, 0x0b, 0x70, 0x65, 0x65, 0x72, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x22, 0xc2, 0x01, - 0x0a, 0x13, 0x50, 0x65, 0x65, 0x72, 0x53, 0x77, 0x61, 0x70, 0x50, 0x65, 0x65, 0x72, 0x43, 0x68, - 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, - 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x6e, - 0x65, 0x6c, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x62, 0x61, - 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x6c, 0x6f, 0x63, - 0x61, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x72, 0x65, 0x6d, - 0x6f, 0x74, 0x65, 0x5f, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x04, 0x52, 0x0d, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, - 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x73, 0x68, 0x6f, 0x72, - 0x74, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0e, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, - 0x49, 0x64, 0x22, 0x77, 0x0a, 0x09, 0x53, 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, - 0x1b, 0x0a, 0x09, 0x73, 0x77, 0x61, 0x70, 0x73, 0x5f, 0x6f, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x08, 0x73, 0x77, 0x61, 0x70, 0x73, 0x4f, 0x75, 0x74, 0x12, 0x19, 0x0a, 0x08, - 0x73, 0x77, 0x61, 0x70, 0x73, 0x5f, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, - 0x73, 0x77, 0x61, 0x70, 0x73, 0x49, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x61, 0x74, 0x73, 0x5f, - 0x6f, 0x75, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x61, 0x74, 0x73, 0x4f, - 0x75, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x61, 0x74, 0x73, 0x5f, 0x69, 0x6e, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x06, 0x73, 0x61, 0x74, 0x73, 0x49, 0x6e, 0x22, 0x9c, 0x02, 0x0a, 0x06, - 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x30, 0x0a, 0x14, 0x72, 0x65, 0x73, 0x65, 0x72, 0x76, - 0x65, 0x5f, 0x6f, 0x6e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x04, 0x52, 0x12, 0x72, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x4f, 0x6e, 0x63, - 0x68, 0x61, 0x69, 0x6e, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x2f, 0x0a, 0x14, 0x6d, 0x69, 0x6e, 0x5f, - 0x73, 0x77, 0x61, 0x70, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x6d, 0x73, 0x61, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x11, 0x6d, 0x69, 0x6e, 0x53, 0x77, 0x61, 0x70, 0x41, - 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x61, 0x63, 0x63, - 0x65, 0x70, 0x74, 0x5f, 0x61, 0x6c, 0x6c, 0x5f, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x0e, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6c, 0x6c, 0x50, 0x65, - 0x65, 0x72, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x6e, 0x65, 0x77, - 0x5f, 0x73, 0x77, 0x61, 0x70, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61, 0x6c, - 0x6c, 0x6f, 0x77, 0x4e, 0x65, 0x77, 0x53, 0x77, 0x61, 0x70, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x61, - 0x6c, 0x6c, 0x6f, 0x77, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x70, 0x65, 0x65, 0x72, 0x73, - 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x6c, 0x69, 0x73, - 0x74, 0x65, 0x64, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x30, 0x0a, 0x14, 0x73, 0x75, 0x73, 0x70, - 0x69, 0x63, 0x69, 0x6f, 0x75, 0x73, 0x5f, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x6c, 0x69, 0x73, 0x74, - 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x73, 0x75, 0x73, 0x70, 0x69, 0x63, 0x69, 0x6f, - 0x75, 0x73, 0x50, 0x65, 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x30, 0x0a, 0x18, 0x41, 0x6c, + 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x3d, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, + 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, + 0x61, 0x70, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x41, 0x64, 0x6a, 0x61, 0x63, 0x65, + 0x6e, 0x63, 0x79, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x08, 0x63, 0x68, 0x61, 0x6e, + 0x6e, 0x65, 0x6c, 0x73, 0x22, 0x7a, 0x0a, 0x17, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x41, + 0x64, 0x6a, 0x61, 0x63, 0x65, 0x6e, 0x63, 0x79, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, + 0x1d, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x64, 0x12, 0x28, + 0x0a, 0x10, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, + 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x43, + 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, + 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, + 0x22, 0x77, 0x0a, 0x09, 0x53, 0x77, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x1b, 0x0a, + 0x09, 0x73, 0x77, 0x61, 0x70, 0x73, 0x5f, 0x6f, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x08, 0x73, 0x77, 0x61, 0x70, 0x73, 0x4f, 0x75, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x77, + 0x61, 0x70, 0x73, 0x5f, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x77, + 0x61, 0x70, 0x73, 0x49, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x61, 0x74, 0x73, 0x5f, 0x6f, 0x75, + 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x61, 0x74, 0x73, 0x4f, 0x75, 0x74, + 0x12, 0x17, 0x0a, 0x07, 0x73, 0x61, 0x74, 0x73, 0x5f, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x06, 0x73, 0x61, 0x74, 0x73, 0x49, 0x6e, 0x22, 0x9c, 0x02, 0x0a, 0x06, 0x50, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x12, 0x30, 0x0a, 0x14, 0x72, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x5f, + 0x6f, 0x6e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x12, 0x72, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x4f, 0x6e, 0x63, 0x68, 0x61, + 0x69, 0x6e, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x2f, 0x0a, 0x14, 0x6d, 0x69, 0x6e, 0x5f, 0x73, 0x77, + 0x61, 0x70, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x11, 0x6d, 0x69, 0x6e, 0x53, 0x77, 0x61, 0x70, 0x41, 0x6d, 0x6f, + 0x75, 0x6e, 0x74, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x61, 0x63, 0x63, 0x65, 0x70, + 0x74, 0x5f, 0x61, 0x6c, 0x6c, 0x5f, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x0e, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x41, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, + 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x6e, 0x65, 0x77, 0x5f, 0x73, + 0x77, 0x61, 0x70, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61, 0x6c, 0x6c, 0x6f, + 0x77, 0x4e, 0x65, 0x77, 0x53, 0x77, 0x61, 0x70, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x61, 0x6c, 0x6c, + 0x6f, 0x77, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x05, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x6c, 0x69, 0x73, 0x74, 0x65, + 0x64, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x30, 0x0a, 0x14, 0x73, 0x75, 0x73, 0x70, 0x69, 0x63, + 0x69, 0x6f, 0x75, 0x73, 0x5f, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x06, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x73, 0x75, 0x73, 0x70, 0x69, 0x63, 0x69, 0x6f, 0x75, 0x73, + 0x50, 0x65, 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x30, 0x0a, 0x18, 0x41, 0x6c, 0x6c, 0x6f, + 0x77, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x05, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x22, 0x31, 0x0a, 0x19, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x22, 0x31, 0x0a, 0x19, - 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x6c, 0x6c, - 0x6f, 0x77, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x22, - 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x99, 0x01, 0x0a, 0x0b, 0x50, 0x72, 0x65, - 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x12, 0x29, 0x0a, 0x05, 0x61, 0x73, 0x73, 0x65, - 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, - 0x61, 0x70, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x05, 0x61, 0x73, - 0x73, 0x65, 0x74, 0x12, 0x35, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, - 0x70, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, - 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x28, 0x0a, 0x10, 0x70, 0x72, - 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x70, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, - 0x65, 0x50, 0x70, 0x6d, 0x22, 0x53, 0x0a, 0x0b, 0x50, 0x65, 0x65, 0x72, 0x50, 0x72, 0x65, 0x6d, - 0x69, 0x75, 0x6d, 0x12, 0x17, 0x0a, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x2b, 0x0a, 0x05, - 0x72, 0x61, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x65, - 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, - 0x74, 0x65, 0x52, 0x05, 0x72, 0x61, 0x74, 0x65, 0x73, 0x22, 0x92, 0x01, 0x0a, 0x15, 0x47, 0x65, - 0x74, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x29, 0x0a, 0x05, - 0x61, 0x73, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x70, 0x65, - 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, - 0x52, 0x05, 0x61, 0x73, 0x73, 0x65, 0x74, 0x12, 0x35, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x70, 0x65, 0x65, - 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, - 0x79, 0x70, 0x65, 0x52, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x95, - 0x01, 0x0a, 0x18, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, - 0x52, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x6e, - 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6e, 0x6f, - 0x64, 0x65, 0x49, 0x64, 0x12, 0x29, 0x0a, 0x05, 0x61, 0x73, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x41, - 0x73, 0x73, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x05, 0x61, 0x73, 0x73, 0x65, 0x74, 0x12, - 0x35, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x4f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x09, 0x6f, 0x70, 0x65, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x5e, 0x0a, 0x18, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x29, 0x0a, 0x04, 0x72, - 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x65, 0x65, 0x72, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x6c, 0x6c, 0x6f, 0x77, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x22, 0x07, 0x0a, + 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x99, 0x01, 0x0a, 0x0b, 0x50, 0x72, 0x65, 0x6d, 0x69, + 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x12, 0x29, 0x0a, 0x05, 0x61, 0x73, 0x73, 0x65, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, + 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x05, 0x61, 0x73, 0x73, 0x65, + 0x74, 0x12, 0x35, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, + 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x09, 0x6f, + 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x28, 0x0a, 0x10, 0x70, 0x72, 0x65, 0x6d, + 0x69, 0x75, 0x6d, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0e, 0x70, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x50, + 0x70, 0x6d, 0x22, 0x53, 0x0a, 0x0b, 0x50, 0x65, 0x65, 0x72, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, + 0x6d, 0x12, 0x17, 0x0a, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x2b, 0x0a, 0x05, 0x72, 0x61, + 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, - 0x52, 0x04, 0x72, 0x61, 0x74, 0x65, 0x22, 0x7f, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x47, 0x6c, 0x6f, - 0x62, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x05, 0x61, 0x73, 0x73, 0x65, 0x74, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, - 0x41, 0x73, 0x73, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x05, 0x61, 0x73, 0x73, 0x65, 0x74, - 0x12, 0x35, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x4f, - 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x09, 0x6f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x4b, 0x0a, 0x1e, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x04, 0x72, 0x61, 0x74, + 0x52, 0x05, 0x72, 0x61, 0x74, 0x65, 0x73, 0x22, 0x92, 0x01, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x50, + 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x17, 0x0a, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x29, 0x0a, 0x05, 0x61, 0x73, + 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x70, 0x65, 0x65, 0x72, + 0x73, 0x77, 0x61, 0x70, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x05, + 0x61, 0x73, 0x73, 0x65, 0x74, 0x12, 0x35, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, + 0x77, 0x61, 0x70, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, + 0x65, 0x52, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x95, 0x01, 0x0a, + 0x18, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x6e, 0x6f, 0x64, + 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6e, 0x6f, 0x64, 0x65, + 0x49, 0x64, 0x12, 0x29, 0x0a, 0x05, 0x61, 0x73, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x13, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x41, 0x73, 0x73, + 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x05, 0x61, 0x73, 0x73, 0x65, 0x74, 0x12, 0x35, 0x0a, + 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x17, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x4f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x5e, 0x0a, 0x18, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, + 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x17, 0x0a, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x29, 0x0a, 0x04, 0x72, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x52, 0x04, - 0x72, 0x61, 0x74, 0x65, 0x22, 0x28, 0x0a, 0x0d, 0x50, 0x65, 0x65, 0x72, 0x53, 0x77, 0x61, 0x70, - 0x4e, 0x6f, 0x64, 0x65, 0x73, 0x12, 0x17, 0x0a, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x2a, 0x35, - 0x0a, 0x09, 0x41, 0x73, 0x73, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x15, 0x0a, 0x11, 0x41, - 0x53, 0x53, 0x45, 0x54, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, - 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x42, 0x54, 0x43, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x4c, - 0x42, 0x54, 0x43, 0x10, 0x02, 0x2a, 0x45, 0x0a, 0x0d, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x19, 0x0a, 0x15, 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, - 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, - 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x57, 0x41, 0x50, 0x5f, 0x49, 0x4e, 0x10, 0x01, 0x12, 0x0c, - 0x0a, 0x08, 0x53, 0x57, 0x41, 0x50, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x02, 0x32, 0xb6, 0x0c, 0x0a, - 0x08, 0x50, 0x65, 0x65, 0x72, 0x53, 0x77, 0x61, 0x70, 0x12, 0x3b, 0x0a, 0x07, 0x53, 0x77, 0x61, - 0x70, 0x4f, 0x75, 0x74, 0x12, 0x18, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, - 0x53, 0x77, 0x61, 0x70, 0x4f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, - 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x77, 0x61, 0x70, 0x49, 0x6e, - 0x12, 0x17, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x53, 0x77, 0x61, 0x70, - 0x49, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x70, 0x65, 0x65, 0x72, - 0x73, 0x77, 0x61, 0x70, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x3b, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x53, 0x77, 0x61, 0x70, 0x12, 0x18, 0x2e, 0x70, - 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x77, 0x61, 0x70, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, - 0x70, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, - 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x12, 0x1a, 0x2e, 0x70, 0x65, - 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, - 0x61, 0x70, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x65, 0x65, 0x72, - 0x73, 0x12, 0x1a, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x4c, 0x69, 0x73, - 0x74, 0x50, 0x65, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, - 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x65, 0x65, - 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5f, 0x0a, 0x12, 0x4c, 0x69, - 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x53, 0x77, 0x61, 0x70, 0x73, - 0x12, 0x23, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, - 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x53, 0x77, - 0x61, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x0f, 0x4c, - 0x69, 0x73, 0x74, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x53, 0x77, 0x61, 0x70, 0x73, 0x12, 0x1a, - 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, - 0x61, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x65, 0x65, - 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x11, 0x41, 0x6c, 0x6c, 0x6f, 0x77, - 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x70, - 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x53, 0x77, 0x61, - 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x10, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x50, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x12, 0x47, 0x0a, 0x10, 0x52, 0x65, 0x6c, 0x6f, 0x61, 0x64, 0x50, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x21, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, - 0x70, 0x2e, 0x52, 0x65, 0x6c, 0x6f, 0x61, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x46, 0x69, - 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x70, 0x65, 0x65, 0x72, - 0x73, 0x77, 0x61, 0x70, 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x35, 0x0a, 0x07, 0x41, - 0x64, 0x64, 0x50, 0x65, 0x65, 0x72, 0x12, 0x18, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, - 0x70, 0x2e, 0x41, 0x64, 0x64, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x10, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x50, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x12, 0x3b, 0x0a, 0x0a, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x65, 0x65, 0x72, - 0x12, 0x1b, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x52, 0x65, 0x6d, 0x6f, - 0x76, 0x65, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, - 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, - 0x38, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x53, 0x75, 0x73, 0x50, 0x65, 0x65, 0x72, 0x12, 0x18, 0x2e, - 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x41, 0x64, 0x64, 0x50, 0x65, 0x65, 0x72, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, - 0x61, 0x70, 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x3e, 0x0a, 0x0d, 0x52, 0x65, 0x6d, - 0x6f, 0x76, 0x65, 0x53, 0x75, 0x73, 0x50, 0x65, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x70, 0x65, 0x65, - 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x65, 0x65, 0x72, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, - 0x61, 0x70, 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x4d, 0x0a, 0x10, 0x4c, 0x69, 0x71, - 0x75, 0x69, 0x64, 0x47, 0x65, 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x1b, 0x2e, - 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x70, 0x65, 0x65, - 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x10, 0x4c, 0x69, 0x71, 0x75, - 0x69, 0x64, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x1b, 0x2e, 0x70, - 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, - 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x70, 0x65, 0x65, 0x72, - 0x73, 0x77, 0x61, 0x70, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x13, 0x4c, 0x69, 0x71, 0x75, 0x69, - 0x64, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x1e, - 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, - 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, - 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, - 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x54, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x6d, - 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x12, 0x25, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, - 0x61, 0x70, 0x2e, 0x47, 0x65, 0x74, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x6d, - 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, - 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, - 0x6d, 0x52, 0x61, 0x74, 0x65, 0x12, 0x5a, 0x0a, 0x17, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x47, + 0x72, 0x61, 0x74, 0x65, 0x22, 0x7f, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x47, 0x6c, 0x6f, 0x62, 0x61, + 0x6c, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x05, 0x61, 0x73, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x41, 0x73, + 0x73, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x05, 0x61, 0x73, 0x73, 0x65, 0x74, 0x12, 0x35, + 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x17, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x4f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x09, 0x6f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x4b, 0x0a, 0x1e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, - 0x12, 0x28, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x70, 0x65, 0x65, - 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, - 0x65, 0x12, 0x48, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, - 0x61, 0x74, 0x65, 0x12, 0x1f, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x47, - 0x65, 0x74, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, - 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x12, 0x4e, 0x0a, 0x11, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, - 0x12, 0x22, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, - 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x12, 0x4e, 0x0a, 0x11, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, - 0x12, 0x22, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, - 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x12, 0x28, 0x0a, 0x04, 0x53, - 0x74, 0x6f, 0x70, 0x12, 0x0f, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0f, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x70, 0x72, 0x6f, 0x6a, - 0x65, 0x63, 0x74, 0x2f, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2f, 0x70, 0x65, 0x65, - 0x72, 0x73, 0x77, 0x61, 0x70, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x04, 0x72, 0x61, 0x74, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, + 0x2e, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x52, 0x04, 0x72, 0x61, + 0x74, 0x65, 0x22, 0x28, 0x0a, 0x0d, 0x50, 0x65, 0x65, 0x72, 0x53, 0x77, 0x61, 0x70, 0x4e, 0x6f, + 0x64, 0x65, 0x73, 0x12, 0x17, 0x0a, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x2a, 0x35, 0x0a, 0x09, + 0x41, 0x73, 0x73, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x15, 0x0a, 0x11, 0x41, 0x53, 0x53, + 0x45, 0x54, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, + 0x12, 0x07, 0x0a, 0x03, 0x42, 0x54, 0x43, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x4c, 0x42, 0x54, + 0x43, 0x10, 0x02, 0x2a, 0x45, 0x0a, 0x0d, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x19, 0x0a, 0x15, 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, 0x49, 0x4f, + 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, + 0x0b, 0x0a, 0x07, 0x53, 0x57, 0x41, 0x50, 0x5f, 0x49, 0x4e, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, + 0x53, 0x57, 0x41, 0x50, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x02, 0x32, 0xb6, 0x0c, 0x0a, 0x08, 0x50, + 0x65, 0x65, 0x72, 0x53, 0x77, 0x61, 0x70, 0x12, 0x3b, 0x0a, 0x07, 0x53, 0x77, 0x61, 0x70, 0x4f, + 0x75, 0x74, 0x12, 0x18, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x53, 0x77, + 0x61, 0x70, 0x4f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x70, + 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x77, 0x61, 0x70, 0x49, 0x6e, 0x12, 0x17, + 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x49, 0x6e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, + 0x61, 0x70, 0x2e, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x3b, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x53, 0x77, 0x61, 0x70, 0x12, 0x18, 0x2e, 0x70, 0x65, 0x65, + 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, + 0x53, 0x77, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x09, + 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x12, 0x1a, 0x2e, 0x70, 0x65, 0x65, 0x72, + 0x73, 0x77, 0x61, 0x70, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, + 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x44, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, + 0x1a, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, + 0x65, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x65, + 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x65, 0x65, 0x72, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5f, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x53, 0x77, 0x61, 0x70, 0x73, 0x12, 0x23, + 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x53, 0x77, 0x61, 0x70, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x0f, 0x4c, 0x69, 0x73, + 0x74, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x53, 0x77, 0x61, 0x70, 0x73, 0x12, 0x1a, 0x2e, 0x70, + 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, + 0x77, 0x61, 0x70, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x61, 0x70, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x11, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x53, 0x77, + 0x61, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x70, 0x65, 0x65, + 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x53, 0x77, 0x61, 0x70, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, + 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x12, 0x47, 0x0a, 0x10, 0x52, 0x65, 0x6c, 0x6f, 0x61, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x46, 0x69, 0x6c, 0x65, 0x12, 0x21, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, + 0x52, 0x65, 0x6c, 0x6f, 0x61, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x46, 0x69, 0x6c, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, + 0x61, 0x70, 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x35, 0x0a, 0x07, 0x41, 0x64, 0x64, + 0x50, 0x65, 0x65, 0x72, 0x12, 0x18, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, + 0x41, 0x64, 0x64, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, + 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x12, 0x3b, 0x0a, 0x0a, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x65, 0x65, 0x72, 0x12, 0x1b, + 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, + 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x70, 0x65, + 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x38, 0x0a, + 0x0a, 0x41, 0x64, 0x64, 0x53, 0x75, 0x73, 0x50, 0x65, 0x65, 0x72, 0x12, 0x18, 0x2e, 0x70, 0x65, + 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x41, 0x64, 0x64, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, + 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x3e, 0x0a, 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x76, + 0x65, 0x53, 0x75, 0x73, 0x50, 0x65, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, + 0x77, 0x61, 0x70, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, + 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x4d, 0x0a, 0x10, 0x4c, 0x69, 0x71, 0x75, 0x69, + 0x64, 0x47, 0x65, 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x1b, 0x2e, 0x70, 0x65, + 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, + 0x77, 0x61, 0x70, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x10, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, + 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x1b, 0x2e, 0x70, 0x65, 0x65, + 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, + 0x61, 0x70, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x13, 0x4c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x53, + 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x1e, 0x2e, 0x70, + 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x41, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, + 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x54, 0x6f, 0x41, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, + 0x14, 0x47, 0x65, 0x74, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, + 0x6d, 0x52, 0x61, 0x74, 0x65, 0x12, 0x25, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, + 0x2e, 0x47, 0x65, 0x74, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, + 0x6d, 0x52, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x70, + 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, + 0x61, 0x74, 0x65, 0x12, 0x5a, 0x0a, 0x17, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x47, 0x6c, 0x6f, + 0x62, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x12, 0x28, + 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, + 0x77, 0x61, 0x70, 0x2e, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x12, + 0x48, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, + 0x65, 0x12, 0x1f, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x47, 0x65, 0x74, + 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x50, 0x72, + 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x12, 0x4e, 0x0a, 0x11, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x12, 0x22, + 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x50, 0x72, + 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x12, 0x4e, 0x0a, 0x11, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x12, 0x22, + 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x50, 0x72, 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x50, 0x72, + 0x65, 0x6d, 0x69, 0x75, 0x6d, 0x52, 0x61, 0x74, 0x65, 0x12, 0x28, 0x0a, 0x04, 0x53, 0x74, 0x6f, + 0x70, 0x12, 0x0f, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x1a, 0x0f, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, + 0x74, 0x2f, 0x70, 0x65, 0x65, 0x72, 0x73, 0x77, 0x61, 0x70, 0x2f, 0x70, 0x65, 0x65, 0x72, 0x73, + 0x77, 0x61, 0x70, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2735,7 +3029,7 @@ func file_peerswaprpc_proto_rawDescGZIP() []byte { } var file_peerswaprpc_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_peerswaprpc_proto_msgTypes = make([]protoimpl.MessageInfo, 39) +var file_peerswaprpc_proto_msgTypes = make([]protoimpl.MessageInfo, 42) var file_peerswaprpc_proto_goTypes = []interface{}{ (AssetType)(0), // 0: peerswap.AssetType (OperationType)(0), // 1: peerswap.OperationType @@ -2765,94 +3059,100 @@ var file_peerswaprpc_proto_goTypes = []interface{}{ (*PrettyPrintSwap)(nil), // 25: peerswap.PrettyPrintSwap (*PeerSwapPeer)(nil), // 26: peerswap.PeerSwapPeer (*PeerSwapPeerChannel)(nil), // 27: peerswap.PeerSwapPeerChannel - (*SwapStats)(nil), // 28: peerswap.SwapStats - (*Policy)(nil), // 29: peerswap.Policy - (*AllowSwapRequestsRequest)(nil), // 30: peerswap.AllowSwapRequestsRequest - (*AllowSwapRequestsResponse)(nil), // 31: peerswap.AllowSwapRequestsResponse - (*Empty)(nil), // 32: peerswap.Empty - (*PremiumRate)(nil), // 33: peerswap.PremiumRate - (*PeerPremium)(nil), // 34: peerswap.PeerPremium - (*GetPremiumRateRequest)(nil), // 35: peerswap.GetPremiumRateRequest - (*DeletePremiumRateRequest)(nil), // 36: peerswap.DeletePremiumRateRequest - (*UpdatePremiumRateRequest)(nil), // 37: peerswap.UpdatePremiumRateRequest - (*GetGlobalPremiumRateRequest)(nil), // 38: peerswap.GetGlobalPremiumRateRequest - (*UpdateGlobalPremiumRateRequest)(nil), // 39: peerswap.UpdateGlobalPremiumRateRequest - (*PeerSwapNodes)(nil), // 40: peerswap.PeerSwapNodes - nil, // 41: peerswap.ListRequestedSwapsResponse.RequestedSwapsEntry + (*ChannelAdjacency)(nil), // 28: peerswap.ChannelAdjacency + (*ChannelAdjacencyNeighbor)(nil), // 29: peerswap.ChannelAdjacencyNeighbor + (*ChannelAdjacencyChannel)(nil), // 30: peerswap.ChannelAdjacencyChannel + (*SwapStats)(nil), // 31: peerswap.SwapStats + (*Policy)(nil), // 32: peerswap.Policy + (*AllowSwapRequestsRequest)(nil), // 33: peerswap.AllowSwapRequestsRequest + (*AllowSwapRequestsResponse)(nil), // 34: peerswap.AllowSwapRequestsResponse + (*Empty)(nil), // 35: peerswap.Empty + (*PremiumRate)(nil), // 36: peerswap.PremiumRate + (*PeerPremium)(nil), // 37: peerswap.PeerPremium + (*GetPremiumRateRequest)(nil), // 38: peerswap.GetPremiumRateRequest + (*DeletePremiumRateRequest)(nil), // 39: peerswap.DeletePremiumRateRequest + (*UpdatePremiumRateRequest)(nil), // 40: peerswap.UpdatePremiumRateRequest + (*GetGlobalPremiumRateRequest)(nil), // 41: peerswap.GetGlobalPremiumRateRequest + (*UpdateGlobalPremiumRateRequest)(nil), // 42: peerswap.UpdateGlobalPremiumRateRequest + (*PeerSwapNodes)(nil), // 43: peerswap.PeerSwapNodes + nil, // 44: peerswap.ListRequestedSwapsResponse.RequestedSwapsEntry } var file_peerswaprpc_proto_depIdxs = []int32{ 25, // 0: peerswap.SwapOutResponse.swap:type_name -> peerswap.PrettyPrintSwap 25, // 1: peerswap.SwapResponse.swap:type_name -> peerswap.PrettyPrintSwap 25, // 2: peerswap.ListSwapsResponse.swaps:type_name -> peerswap.PrettyPrintSwap 26, // 3: peerswap.ListPeersResponse.peers:type_name -> peerswap.PeerSwapPeer - 41, // 4: peerswap.ListRequestedSwapsResponse.requested_swaps:type_name -> peerswap.ListRequestedSwapsResponse.RequestedSwapsEntry + 44, // 4: peerswap.ListRequestedSwapsResponse.requested_swaps:type_name -> peerswap.ListRequestedSwapsResponse.RequestedSwapsEntry 24, // 5: peerswap.RequestSwapList.requested_swaps:type_name -> peerswap.RequestedSwap 2, // 6: peerswap.RequestedSwap.swap_type:type_name -> peerswap.RequestedSwap.SwapType 27, // 7: peerswap.PeerSwapPeer.channels:type_name -> peerswap.PeerSwapPeerChannel - 28, // 8: peerswap.PeerSwapPeer.as_sender:type_name -> peerswap.SwapStats - 28, // 9: peerswap.PeerSwapPeer.as_receiver:type_name -> peerswap.SwapStats - 34, // 10: peerswap.PeerSwapPeer.peer_premium:type_name -> peerswap.PeerPremium - 0, // 11: peerswap.PremiumRate.asset:type_name -> peerswap.AssetType - 1, // 12: peerswap.PremiumRate.operation:type_name -> peerswap.OperationType - 33, // 13: peerswap.PeerPremium.rates:type_name -> peerswap.PremiumRate - 0, // 14: peerswap.GetPremiumRateRequest.asset:type_name -> peerswap.AssetType - 1, // 15: peerswap.GetPremiumRateRequest.operation:type_name -> peerswap.OperationType - 0, // 16: peerswap.DeletePremiumRateRequest.asset:type_name -> peerswap.AssetType - 1, // 17: peerswap.DeletePremiumRateRequest.operation:type_name -> peerswap.OperationType - 33, // 18: peerswap.UpdatePremiumRateRequest.rate:type_name -> peerswap.PremiumRate - 0, // 19: peerswap.GetGlobalPremiumRateRequest.asset:type_name -> peerswap.AssetType - 1, // 20: peerswap.GetGlobalPremiumRateRequest.operation:type_name -> peerswap.OperationType - 33, // 21: peerswap.UpdateGlobalPremiumRateRequest.rate:type_name -> peerswap.PremiumRate - 23, // 22: peerswap.ListRequestedSwapsResponse.RequestedSwapsEntry.value:type_name -> peerswap.RequestSwapList - 9, // 23: peerswap.PeerSwap.SwapOut:input_type -> peerswap.SwapOutRequest - 11, // 24: peerswap.PeerSwap.SwapIn:input_type -> peerswap.SwapInRequest - 13, // 25: peerswap.PeerSwap.GetSwap:input_type -> peerswap.GetSwapRequest - 14, // 26: peerswap.PeerSwap.ListSwaps:input_type -> peerswap.ListSwapsRequest - 16, // 27: peerswap.PeerSwap.ListPeers:input_type -> peerswap.ListPeersRequest - 21, // 28: peerswap.PeerSwap.ListRequestedSwaps:input_type -> peerswap.ListRequestedSwapsRequest - 14, // 29: peerswap.PeerSwap.ListActiveSwaps:input_type -> peerswap.ListSwapsRequest - 30, // 30: peerswap.PeerSwap.AllowSwapRequests:input_type -> peerswap.AllowSwapRequestsRequest - 18, // 31: peerswap.PeerSwap.ReloadPolicyFile:input_type -> peerswap.ReloadPolicyFileRequest - 19, // 32: peerswap.PeerSwap.AddPeer:input_type -> peerswap.AddPeerRequest - 20, // 33: peerswap.PeerSwap.RemovePeer:input_type -> peerswap.RemovePeerRequest - 19, // 34: peerswap.PeerSwap.AddSusPeer:input_type -> peerswap.AddPeerRequest - 20, // 35: peerswap.PeerSwap.RemoveSusPeer:input_type -> peerswap.RemovePeerRequest - 3, // 36: peerswap.PeerSwap.LiquidGetAddress:input_type -> peerswap.GetAddressRequest - 5, // 37: peerswap.PeerSwap.LiquidGetBalance:input_type -> peerswap.GetBalanceRequest - 7, // 38: peerswap.PeerSwap.LiquidSendToAddress:input_type -> peerswap.SendToAddressRequest - 38, // 39: peerswap.PeerSwap.GetGlobalPremiumRate:input_type -> peerswap.GetGlobalPremiumRateRequest - 39, // 40: peerswap.PeerSwap.UpdateGlobalPremiumRate:input_type -> peerswap.UpdateGlobalPremiumRateRequest - 35, // 41: peerswap.PeerSwap.GetPremiumRate:input_type -> peerswap.GetPremiumRateRequest - 37, // 42: peerswap.PeerSwap.UpdatePremiumRate:input_type -> peerswap.UpdatePremiumRateRequest - 36, // 43: peerswap.PeerSwap.DeletePremiumRate:input_type -> peerswap.DeletePremiumRateRequest - 32, // 44: peerswap.PeerSwap.Stop:input_type -> peerswap.Empty - 12, // 45: peerswap.PeerSwap.SwapOut:output_type -> peerswap.SwapResponse - 12, // 46: peerswap.PeerSwap.SwapIn:output_type -> peerswap.SwapResponse - 12, // 47: peerswap.PeerSwap.GetSwap:output_type -> peerswap.SwapResponse - 15, // 48: peerswap.PeerSwap.ListSwaps:output_type -> peerswap.ListSwapsResponse - 17, // 49: peerswap.PeerSwap.ListPeers:output_type -> peerswap.ListPeersResponse - 22, // 50: peerswap.PeerSwap.ListRequestedSwaps:output_type -> peerswap.ListRequestedSwapsResponse - 15, // 51: peerswap.PeerSwap.ListActiveSwaps:output_type -> peerswap.ListSwapsResponse - 29, // 52: peerswap.PeerSwap.AllowSwapRequests:output_type -> peerswap.Policy - 29, // 53: peerswap.PeerSwap.ReloadPolicyFile:output_type -> peerswap.Policy - 29, // 54: peerswap.PeerSwap.AddPeer:output_type -> peerswap.Policy - 29, // 55: peerswap.PeerSwap.RemovePeer:output_type -> peerswap.Policy - 29, // 56: peerswap.PeerSwap.AddSusPeer:output_type -> peerswap.Policy - 29, // 57: peerswap.PeerSwap.RemoveSusPeer:output_type -> peerswap.Policy - 4, // 58: peerswap.PeerSwap.LiquidGetAddress:output_type -> peerswap.GetAddressResponse - 6, // 59: peerswap.PeerSwap.LiquidGetBalance:output_type -> peerswap.GetBalanceResponse - 8, // 60: peerswap.PeerSwap.LiquidSendToAddress:output_type -> peerswap.SendToAddressResponse - 33, // 61: peerswap.PeerSwap.GetGlobalPremiumRate:output_type -> peerswap.PremiumRate - 33, // 62: peerswap.PeerSwap.UpdateGlobalPremiumRate:output_type -> peerswap.PremiumRate - 33, // 63: peerswap.PeerSwap.GetPremiumRate:output_type -> peerswap.PremiumRate - 33, // 64: peerswap.PeerSwap.UpdatePremiumRate:output_type -> peerswap.PremiumRate - 33, // 65: peerswap.PeerSwap.DeletePremiumRate:output_type -> peerswap.PremiumRate - 32, // 66: peerswap.PeerSwap.Stop:output_type -> peerswap.Empty - 45, // [45:67] is the sub-list for method output_type - 23, // [23:45] is the sub-list for method input_type - 23, // [23:23] is the sub-list for extension type_name - 23, // [23:23] is the sub-list for extension extendee - 0, // [0:23] is the sub-list for field type_name + 31, // 8: peerswap.PeerSwapPeer.as_sender:type_name -> peerswap.SwapStats + 31, // 9: peerswap.PeerSwapPeer.as_receiver:type_name -> peerswap.SwapStats + 37, // 10: peerswap.PeerSwapPeer.peer_premium:type_name -> peerswap.PeerPremium + 28, // 11: peerswap.PeerSwapPeer.channel_adjacency:type_name -> peerswap.ChannelAdjacency + 29, // 12: peerswap.ChannelAdjacency.neighbors:type_name -> peerswap.ChannelAdjacencyNeighbor + 30, // 13: peerswap.ChannelAdjacencyNeighbor.channels:type_name -> peerswap.ChannelAdjacencyChannel + 0, // 14: peerswap.PremiumRate.asset:type_name -> peerswap.AssetType + 1, // 15: peerswap.PremiumRate.operation:type_name -> peerswap.OperationType + 36, // 16: peerswap.PeerPremium.rates:type_name -> peerswap.PremiumRate + 0, // 17: peerswap.GetPremiumRateRequest.asset:type_name -> peerswap.AssetType + 1, // 18: peerswap.GetPremiumRateRequest.operation:type_name -> peerswap.OperationType + 0, // 19: peerswap.DeletePremiumRateRequest.asset:type_name -> peerswap.AssetType + 1, // 20: peerswap.DeletePremiumRateRequest.operation:type_name -> peerswap.OperationType + 36, // 21: peerswap.UpdatePremiumRateRequest.rate:type_name -> peerswap.PremiumRate + 0, // 22: peerswap.GetGlobalPremiumRateRequest.asset:type_name -> peerswap.AssetType + 1, // 23: peerswap.GetGlobalPremiumRateRequest.operation:type_name -> peerswap.OperationType + 36, // 24: peerswap.UpdateGlobalPremiumRateRequest.rate:type_name -> peerswap.PremiumRate + 23, // 25: peerswap.ListRequestedSwapsResponse.RequestedSwapsEntry.value:type_name -> peerswap.RequestSwapList + 9, // 26: peerswap.PeerSwap.SwapOut:input_type -> peerswap.SwapOutRequest + 11, // 27: peerswap.PeerSwap.SwapIn:input_type -> peerswap.SwapInRequest + 13, // 28: peerswap.PeerSwap.GetSwap:input_type -> peerswap.GetSwapRequest + 14, // 29: peerswap.PeerSwap.ListSwaps:input_type -> peerswap.ListSwapsRequest + 16, // 30: peerswap.PeerSwap.ListPeers:input_type -> peerswap.ListPeersRequest + 21, // 31: peerswap.PeerSwap.ListRequestedSwaps:input_type -> peerswap.ListRequestedSwapsRequest + 14, // 32: peerswap.PeerSwap.ListActiveSwaps:input_type -> peerswap.ListSwapsRequest + 33, // 33: peerswap.PeerSwap.AllowSwapRequests:input_type -> peerswap.AllowSwapRequestsRequest + 18, // 34: peerswap.PeerSwap.ReloadPolicyFile:input_type -> peerswap.ReloadPolicyFileRequest + 19, // 35: peerswap.PeerSwap.AddPeer:input_type -> peerswap.AddPeerRequest + 20, // 36: peerswap.PeerSwap.RemovePeer:input_type -> peerswap.RemovePeerRequest + 19, // 37: peerswap.PeerSwap.AddSusPeer:input_type -> peerswap.AddPeerRequest + 20, // 38: peerswap.PeerSwap.RemoveSusPeer:input_type -> peerswap.RemovePeerRequest + 3, // 39: peerswap.PeerSwap.LiquidGetAddress:input_type -> peerswap.GetAddressRequest + 5, // 40: peerswap.PeerSwap.LiquidGetBalance:input_type -> peerswap.GetBalanceRequest + 7, // 41: peerswap.PeerSwap.LiquidSendToAddress:input_type -> peerswap.SendToAddressRequest + 41, // 42: peerswap.PeerSwap.GetGlobalPremiumRate:input_type -> peerswap.GetGlobalPremiumRateRequest + 42, // 43: peerswap.PeerSwap.UpdateGlobalPremiumRate:input_type -> peerswap.UpdateGlobalPremiumRateRequest + 38, // 44: peerswap.PeerSwap.GetPremiumRate:input_type -> peerswap.GetPremiumRateRequest + 40, // 45: peerswap.PeerSwap.UpdatePremiumRate:input_type -> peerswap.UpdatePremiumRateRequest + 39, // 46: peerswap.PeerSwap.DeletePremiumRate:input_type -> peerswap.DeletePremiumRateRequest + 35, // 47: peerswap.PeerSwap.Stop:input_type -> peerswap.Empty + 12, // 48: peerswap.PeerSwap.SwapOut:output_type -> peerswap.SwapResponse + 12, // 49: peerswap.PeerSwap.SwapIn:output_type -> peerswap.SwapResponse + 12, // 50: peerswap.PeerSwap.GetSwap:output_type -> peerswap.SwapResponse + 15, // 51: peerswap.PeerSwap.ListSwaps:output_type -> peerswap.ListSwapsResponse + 17, // 52: peerswap.PeerSwap.ListPeers:output_type -> peerswap.ListPeersResponse + 22, // 53: peerswap.PeerSwap.ListRequestedSwaps:output_type -> peerswap.ListRequestedSwapsResponse + 15, // 54: peerswap.PeerSwap.ListActiveSwaps:output_type -> peerswap.ListSwapsResponse + 32, // 55: peerswap.PeerSwap.AllowSwapRequests:output_type -> peerswap.Policy + 32, // 56: peerswap.PeerSwap.ReloadPolicyFile:output_type -> peerswap.Policy + 32, // 57: peerswap.PeerSwap.AddPeer:output_type -> peerswap.Policy + 32, // 58: peerswap.PeerSwap.RemovePeer:output_type -> peerswap.Policy + 32, // 59: peerswap.PeerSwap.AddSusPeer:output_type -> peerswap.Policy + 32, // 60: peerswap.PeerSwap.RemoveSusPeer:output_type -> peerswap.Policy + 4, // 61: peerswap.PeerSwap.LiquidGetAddress:output_type -> peerswap.GetAddressResponse + 6, // 62: peerswap.PeerSwap.LiquidGetBalance:output_type -> peerswap.GetBalanceResponse + 8, // 63: peerswap.PeerSwap.LiquidSendToAddress:output_type -> peerswap.SendToAddressResponse + 36, // 64: peerswap.PeerSwap.GetGlobalPremiumRate:output_type -> peerswap.PremiumRate + 36, // 65: peerswap.PeerSwap.UpdateGlobalPremiumRate:output_type -> peerswap.PremiumRate + 36, // 66: peerswap.PeerSwap.GetPremiumRate:output_type -> peerswap.PremiumRate + 36, // 67: peerswap.PeerSwap.UpdatePremiumRate:output_type -> peerswap.PremiumRate + 36, // 68: peerswap.PeerSwap.DeletePremiumRate:output_type -> peerswap.PremiumRate + 35, // 69: peerswap.PeerSwap.Stop:output_type -> peerswap.Empty + 48, // [48:70] is the sub-list for method output_type + 26, // [26:48] is the sub-list for method input_type + 26, // [26:26] is the sub-list for extension type_name + 26, // [26:26] is the sub-list for extension extendee + 0, // [0:26] is the sub-list for field type_name } func init() { file_peerswaprpc_proto_init() } @@ -3162,7 +3462,7 @@ func file_peerswaprpc_proto_init() { } } file_peerswaprpc_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SwapStats); i { + switch v := v.(*ChannelAdjacency); i { case 0: return &v.state case 1: @@ -3174,7 +3474,7 @@ func file_peerswaprpc_proto_init() { } } file_peerswaprpc_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Policy); i { + switch v := v.(*ChannelAdjacencyNeighbor); i { case 0: return &v.state case 1: @@ -3186,7 +3486,7 @@ func file_peerswaprpc_proto_init() { } } file_peerswaprpc_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AllowSwapRequestsRequest); i { + switch v := v.(*ChannelAdjacencyChannel); i { case 0: return &v.state case 1: @@ -3198,7 +3498,7 @@ func file_peerswaprpc_proto_init() { } } file_peerswaprpc_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AllowSwapRequestsResponse); i { + switch v := v.(*SwapStats); i { case 0: return &v.state case 1: @@ -3210,7 +3510,7 @@ func file_peerswaprpc_proto_init() { } } file_peerswaprpc_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Empty); i { + switch v := v.(*Policy); i { case 0: return &v.state case 1: @@ -3222,7 +3522,7 @@ func file_peerswaprpc_proto_init() { } } file_peerswaprpc_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PremiumRate); i { + switch v := v.(*AllowSwapRequestsRequest); i { case 0: return &v.state case 1: @@ -3234,7 +3534,7 @@ func file_peerswaprpc_proto_init() { } } file_peerswaprpc_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeerPremium); i { + switch v := v.(*AllowSwapRequestsResponse); i { case 0: return &v.state case 1: @@ -3246,7 +3546,7 @@ func file_peerswaprpc_proto_init() { } } file_peerswaprpc_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetPremiumRateRequest); i { + switch v := v.(*Empty); i { case 0: return &v.state case 1: @@ -3258,7 +3558,7 @@ func file_peerswaprpc_proto_init() { } } file_peerswaprpc_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeletePremiumRateRequest); i { + switch v := v.(*PremiumRate); i { case 0: return &v.state case 1: @@ -3270,7 +3570,7 @@ func file_peerswaprpc_proto_init() { } } file_peerswaprpc_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdatePremiumRateRequest); i { + switch v := v.(*PeerPremium); i { case 0: return &v.state case 1: @@ -3282,7 +3582,7 @@ func file_peerswaprpc_proto_init() { } } file_peerswaprpc_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetGlobalPremiumRateRequest); i { + switch v := v.(*GetPremiumRateRequest); i { case 0: return &v.state case 1: @@ -3294,7 +3594,7 @@ func file_peerswaprpc_proto_init() { } } file_peerswaprpc_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateGlobalPremiumRateRequest); i { + switch v := v.(*DeletePremiumRateRequest); i { case 0: return &v.state case 1: @@ -3306,6 +3606,42 @@ func file_peerswaprpc_proto_init() { } } file_peerswaprpc_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UpdatePremiumRateRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_peerswaprpc_proto_msgTypes[38].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetGlobalPremiumRateRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_peerswaprpc_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UpdateGlobalPremiumRateRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_peerswaprpc_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PeerSwapNodes); i { case 0: return &v.state @@ -3324,7 +3660,7 @@ func file_peerswaprpc_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_peerswaprpc_proto_rawDesc, NumEnums: 3, - NumMessages: 39, + NumMessages: 42, NumExtensions: 0, NumServices: 1, }, diff --git a/peerswaprpc/peerswaprpc.proto b/peerswaprpc/peerswaprpc.proto index d1a1cace..732ceb33 100644 --- a/peerswaprpc/peerswaprpc.proto +++ b/peerswaprpc/peerswaprpc.proto @@ -72,6 +72,9 @@ message SwapOutRequest { string asset = 3; bool force = 4; int64 premium_limit_rate_ppm = 5; + // Optional swap peer pubkey. If set, the swap negotiation messages are sent + // to this peer instead of the channel peer, enabling 2-hop swaps. + string peer_pubkey = 6; } message SwapOutResponse { @@ -84,6 +87,9 @@ message SwapInRequest { string asset = 3; bool force = 4; int64 premium_limit_rate_ppm = 5; + // Optional swap peer pubkey. If set, the swap negotiation messages are sent + // to this peer instead of the channel peer, enabling 2-hop swaps. + string peer_pubkey = 6; } message SwapResponse { @@ -165,6 +171,16 @@ message PeerSwapPeer { SwapStats as_receiver = 6; uint64 paid_fee = 7; PeerPremium peer_premium = 8; + // ChannelAdjacency is optional, advisory data advertised by the peer to help + // 2-hop discovery (u→m→v). + // + // Domain definition: + // - This describes the *sender's* adjacency list: LN channels from the sender + // to immediate neighbor nodes. + // - This data is a hint only. It may be stale or incorrect, and MUST NOT be + // treated as proof that a swap will succeed. The actual swap still relies + // on explicit 2-hop discovery during negotiation. + ChannelAdjacency channel_adjacency = 9; } message PeerSwapPeerChannel { @@ -175,6 +191,53 @@ message PeerSwapPeerChannel { string short_channel_id = 6; } +message ChannelAdjacency { + // Schema version of this nested message. This is NOT the PeerSwap protocol + // version from the outer poll message. + int32 schema_version = 1; + + // Indicates whether only public channels were included. + // A "public" channel is one that is announced to the public LN graph. + bool public_channels_only = 2; + + // Maximum number of neighbor nodes included in `neighbors`. + // If the sender has more neighbors, `neighbors` is truncated. + int32 max_neighbors = 3; + + // Neighbor nodes adjacent to the sender (remote node pubkeys) and their + // connecting channels. + repeated ChannelAdjacencyNeighbor neighbors = 4; + + // True if the neighbor list was truncated due to `max_neighbors`. + bool truncated = 5; +} + +message ChannelAdjacencyNeighbor { + // Node pubkey of the adjacent neighbor, as a 33-byte compressed secp256k1 + // public key, hex-encoded (66 hex characters). + string node_id = 1; + + // One or more channels connecting the sender to `node_id`. + repeated ChannelAdjacencyChannel channels = 2; +} + +message ChannelAdjacencyChannel { + // Optional numeric channel id, if available in the backend. + // + // For LND, this corresponds to `lnrpc.Channel.chan_id` (a packed BOLT-07 + // short_channel_id). + // For CLN, this is not available and is set to 0. + uint64 channel_id = 1; + + // BOLT-07 short_channel_id formatted as "blockxtransactionxoutput", + // for example "1x2x3". + string short_channel_id = 2; + + // True if the channel is currently usable for forwarding (from the sender's + // local perspective). + bool active = 3; +} + message SwapStats { uint64 swaps_out = 1; uint64 swaps_in = 2; diff --git a/peerswaprpc/peerswaprpc.swagger.json b/peerswaprpc/peerswaprpc.swagger.json index 501c0774..f13f7be2 100644 --- a/peerswaprpc/peerswaprpc.swagger.json +++ b/peerswaprpc/peerswaprpc.swagger.json @@ -526,6 +526,70 @@ "default": "ASSET_UNSPECIFIED", "description": "Enum for supported asset types." }, + "peerswapChannelAdjacency": { + "type": "object", + "properties": { + "schemaVersion": { + "type": "integer", + "format": "int32", + "description": "Schema version of this nested message. This is NOT the PeerSwap protocol\nversion from the outer poll message." + }, + "publicChannelsOnly": { + "type": "boolean", + "description": "Indicates whether only public channels were included.\nA \"public\" channel is one that is announced to the public LN graph." + }, + "maxNeighbors": { + "type": "integer", + "format": "int32", + "description": "Maximum number of neighbor nodes included in `neighbors`.\nIf the sender has more neighbors, `neighbors` is truncated." + }, + "neighbors": { + "type": "array", + "items": { + "$ref": "#/definitions/peerswapChannelAdjacencyNeighbor" + }, + "description": "Neighbor nodes adjacent to the sender (remote node pubkeys) and their\nconnecting channels." + }, + "truncated": { + "type": "boolean", + "description": "True if the neighbor list was truncated due to `max_neighbors`." + } + } + }, + "peerswapChannelAdjacencyChannel": { + "type": "object", + "properties": { + "channelId": { + "type": "string", + "format": "uint64", + "description": "Optional numeric channel id, if available in the backend.\n\nFor LND, this corresponds to `lnrpc.Channel.chan_id` (a packed BOLT-07\nshort_channel_id).\nFor CLN, this is not available and is set to 0." + }, + "shortChannelId": { + "type": "string", + "description": "BOLT-07 short_channel_id formatted as \"blockxtransactionxoutput\",\nfor example \"1x2x3\"." + }, + "active": { + "type": "boolean", + "description": "True if the channel is currently usable for forwarding (from the sender's\nlocal perspective)." + } + } + }, + "peerswapChannelAdjacencyNeighbor": { + "type": "object", + "properties": { + "nodeId": { + "type": "string", + "description": "Node pubkey of the adjacent neighbor, as a 33-byte compressed secp256k1\npublic key, hex-encoded (66 hex characters)." + }, + "channels": { + "type": "array", + "items": { + "$ref": "#/definitions/peerswapChannelAdjacencyChannel" + }, + "description": "One or more channels connecting the sender to `node_id`." + } + } + }, "peerswapEmpty": { "type": "object" }, @@ -637,6 +701,10 @@ }, "peerPremium": { "$ref": "#/definitions/peerswapPeerPremium" + }, + "channelAdjacency": { + "$ref": "#/definitions/peerswapChannelAdjacency", + "description": "ChannelAdjacency is optional, advisory data advertised by the peer to help\n2-hop discovery (u→m→v).\n\nDomain definition:\n- This describes the *sender's* adjacency list: LN channels from the sender\n to immediate neighbor nodes.\n- This data is a hint only. It may be stale or incorrect, and MUST NOT be\n treated as proof that a swap will succeed. The actual swap still relies\n on explicit 2-hop discovery during negotiation." } } }, @@ -841,6 +909,10 @@ "premiumLimitRatePpm": { "type": "string", "format": "int64" + }, + "peerPubkey": { + "type": "string", + "description": "Optional swap peer pubkey. If set, the swap negotiation messages are sent\nto this peer instead of the channel peer, enabling 2-hop swaps." } } }, @@ -864,6 +936,10 @@ "premiumLimitRatePpm": { "type": "string", "format": "int64" + }, + "peerPubkey": { + "type": "string", + "description": "Optional swap peer pubkey. If set, the swap negotiation messages are sent\nto this peer instead of the channel peer, enabling 2-hop swaps." } } }, diff --git a/peerswaprpc/server.go b/peerswaprpc/server.go index 3f546642..4643c3d1 100644 --- a/peerswaprpc/server.go +++ b/peerswaprpc/server.go @@ -158,7 +158,11 @@ func (p *PeerswapServer) SwapOut(ctx context.Context, request *SwapOutRequest) ( return nil, err } pk := gi.IdentityPubkey - peerId := swapchan.RemotePubkey + intermediaryPeerId := swapchan.RemotePubkey + peerId := intermediaryPeerId + if request.GetPeerPubkey() != "" { + peerId = request.GetPeerPubkey() + } shortId := lnwire.NewShortChanIDFromInt(swapchan.ChanId) @@ -173,7 +177,12 @@ func (p *PeerswapServer) SwapOut(ctx context.Context, request *SwapOutRequest) ( return nil, fmt.Errorf("peer is not connected") } - swapOut, err := p.swaps.SwapOut(peerId, request.Asset, shortId.String(), pk, request.SwapAmount, request.GetPremiumLimitRatePpm()) + var swapOut *swap.SwapStateMachine + if request.GetPeerPubkey() != "" && request.GetPeerPubkey() != intermediaryPeerId { + swapOut, err = p.swaps.SwapOutTwoHop(peerId, request.Asset, shortId.String(), pk, request.SwapAmount, request.GetPremiumLimitRatePpm(), intermediaryPeerId) + } else { + swapOut, err = p.swaps.SwapOut(peerId, request.Asset, shortId.String(), pk, request.SwapAmount, request.GetPremiumLimitRatePpm()) + } if err != nil { return nil, err } @@ -257,7 +266,11 @@ func (p *PeerswapServer) SwapIn(ctx context.Context, request *SwapInRequest) (*S return nil, err } pk := gi.IdentityPubkey - peerId := swapchan.RemotePubkey + intermediaryPeerId := swapchan.RemotePubkey + peerId := intermediaryPeerId + if request.GetPeerPubkey() != "" { + peerId = request.GetPeerPubkey() + } shortId := lnwire.NewShortChanIDFromInt(swapchan.ChanId) @@ -272,7 +285,12 @@ func (p *PeerswapServer) SwapIn(ctx context.Context, request *SwapInRequest) (*S return nil, fmt.Errorf("peer is not connected") } - swapIn, err := p.swaps.SwapIn(peerId, request.Asset, shortId.String(), pk, request.SwapAmount, request.GetPremiumLimitRatePpm()) + var swapIn *swap.SwapStateMachine + if request.GetPeerPubkey() != "" && request.GetPeerPubkey() != intermediaryPeerId { + swapIn, err = p.swaps.SwapInTwoHop(peerId, request.Asset, shortId.String(), pk, request.SwapAmount, request.GetPremiumLimitRatePpm(), intermediaryPeerId) + } else { + swapIn, err = p.swaps.SwapIn(peerId, request.Asset, shortId.String(), pk, request.SwapAmount, request.GetPremiumLimitRatePpm()) + } if err != nil { return nil, err } @@ -358,6 +376,7 @@ func (p *PeerswapServer) ListPeers(ctx context.Context, request *ListPeersReques view := format.BuildPeerView(v.PubKey, capability, swaps) peer := NewPeerSwapPeerFromView(view) + peer.ChannelAdjacency = ChannelAdjacencyFromPeerState(peerState) peer.Channels = getPeerSwapChannels(v.PubKey, channelRes.Channels) peerSwapPeers = append(peerSwapPeers, peer) } diff --git a/peersync/capability_snapshot.go b/peersync/capability_snapshot.go index 3eef6fc7..10087b1b 100644 --- a/peersync/capability_snapshot.go +++ b/peersync/capability_snapshot.go @@ -1,6 +1,8 @@ package peersync import ( + "encoding/json" + "github.com/elementsproject/peerswap/premium" ) @@ -15,6 +17,26 @@ type PeerCapabilitySnapshot struct { BTCSwapOutPremiumRatePPM int64 `json:"btc_swap_out_premium_rate_ppm,omitempty"` LBTCSwapInPremiumRatePPM int64 `json:"lbtc_swap_in_premium_rate_ppm,omitempty"` LBTCSwapOutPremiumRatePPM int64 `json:"lbtc_swap_out_premium_rate_ppm,omitempty"` + // ChannelAdjacency is an optional hint used for 2-hop discovery. Nodes that + // do not understand the field ignore it. + ChannelAdjacency *ChannelAdjacency `json:"channel_adjacency,omitempty"` +} + +// UnmarshalJSON keeps backwards-compatibility for legacy field names. +func (s *PeerCapabilitySnapshot) UnmarshalJSON(data []byte) error { + type alias PeerCapabilitySnapshot + var tmp struct { + alias + LegacyNeighborsAd *ChannelAdjacency `json:"neighbors_ad,omitempty"` + } + if err := json.Unmarshal(data, &tmp); err != nil { + return err + } + *s = PeerCapabilitySnapshot(tmp.alias) + if s.ChannelAdjacency == nil && tmp.LegacyNeighborsAd != nil { + s.ChannelAdjacency = tmp.LegacyNeighborsAd + } + return nil } // SnapshotFromCapability builds a snapshot from the given capability. diff --git a/peersync/channel_adjacency.go b/peersync/channel_adjacency.go new file mode 100644 index 00000000..56d0b21d --- /dev/null +++ b/peersync/channel_adjacency.go @@ -0,0 +1,206 @@ +package peersync + +import ( + "encoding/json" + "sort" +) + +const ( + channelAdjacencySchemaVersion = 1 + defaultChannelAdjacencyLimit = 20 +) + +// ChannelAdjacency is an optional, advisory advertisement embedded in peersync +// poll messages. +// +// Domain definition: +// - It represents a (possibly truncated) adjacency list of the sender's LN +// channels to immediate neighbors. +// - It is intended only as a *hint* for 2-hop discovery (u→m→v). It must not +// be trusted: data may be stale or incorrect, and swaps still use explicit +// 2-hop discovery during negotiation. +// - By default we only advertise public + active channels and we do not +// include balance information for privacy. +type ChannelAdjacency struct { + // SchemaVersion is the version of this nested schema. This is not the + // PeerSwap protocol version from the outer poll message. + SchemaVersion int `json:"schema_version,omitempty"` + + // PublicChannelsOnly indicates that only public channels were included. + // Public channels are those that are announced in the public LN graph. + PublicChannelsOnly bool `json:"public_channels_only,omitempty"` + + // MaxNeighbors is the maximum number of neighbor nodes included in Neighbors. + // If the sender has more neighbors, Neighbors will be truncated. + MaxNeighbors int `json:"max_neighbors,omitempty"` + + // Truncated indicates whether Neighbors was truncated due to MaxNeighbors. + Truncated bool `json:"truncated,omitempty"` + + // Neighbors is the set of adjacent neighbor nodes (remote node pubkeys). + Neighbors []ChannelAdjacencyNeighbor `json:"neighbors,omitempty"` +} + +// ChannelAdjacencyNeighbor represents a single adjacent node and the channels +// connecting the sender to that node. +type ChannelAdjacencyNeighbor struct { + // NodeID is the neighbor node's pubkey as a 33-byte compressed secp256k1 + // public key, hex-encoded (66 hex characters). + NodeID string `json:"node_id,omitempty"` + + // Channels describes one or more channels between the sender and NodeID. + Channels []ChannelAdjacencyChannel `json:"channels,omitempty"` +} + +// ChannelAdjacencyChannel describes a single channel edge from the sender to +// a neighbor. +type ChannelAdjacencyChannel struct { + // ChannelID is an implementation-specific numeric channel id, if available. + // For LND, this corresponds to lnrpc.Channel.chan_id (a packed short_channel_id). + // For backends that do not expose a stable numeric id, this may be 0. + ChannelID uint64 `json:"channel_id,omitempty"` + + // ShortChannelID is the BOLT-07 short_channel_id formatted as "blockxtransactionxoutput", + // for example "1x2x3". + ShortChannelID string `json:"short_channel_id,omitempty"` + + // Active indicates whether the channel is currently usable for forwarding + // (from the sender's local perspective). + Active bool `json:"active,omitempty"` +} + +func buildChannelAdjacency(channels []Channel) *ChannelAdjacency { + byNode := make(map[string][]ChannelAdjacencyChannel) + for _, ch := range channels { + if !ch.Public || !ch.Active { + continue + } + if ch.ShortChannelID == "" { + continue + } + nodeID := ch.Peer.String() + if nodeID == "" { + continue + } + byNode[nodeID] = append(byNode[nodeID], ChannelAdjacencyChannel{ + ChannelID: ch.ChannelID, + ShortChannelID: ch.ShortChannelID, + Active: ch.Active, + }) + } + + if len(byNode) == 0 { + return nil + } + + keys := make([]string, 0, len(byNode)) + for k := range byNode { + keys = append(keys, k) + } + sort.Strings(keys) + + maxNeighbors := defaultChannelAdjacencyLimit + truncated := maxNeighbors > 0 && len(keys) > maxNeighbors + if truncated { + keys = keys[:maxNeighbors] + } + + neighbors := make([]ChannelAdjacencyNeighbor, 0, len(keys)) + for _, nodeID := range keys { + chans := byNode[nodeID] + sort.Slice(chans, func(i, j int) bool { + return chans[i].ShortChannelID < chans[j].ShortChannelID + }) + neighbors = append(neighbors, ChannelAdjacencyNeighbor{ + NodeID: nodeID, + Channels: chans, + }) + } + + if len(neighbors) == 0 { + return nil + } + + return &ChannelAdjacency{ + SchemaVersion: channelAdjacencySchemaVersion, + PublicChannelsOnly: true, + MaxNeighbors: maxNeighbors, + Truncated: truncated, + Neighbors: neighbors, + } +} + +func cloneChannelAdjacency(ad *ChannelAdjacency) *ChannelAdjacency { + if ad == nil { + return nil + } + + out := &ChannelAdjacency{ + SchemaVersion: ad.SchemaVersion, + PublicChannelsOnly: ad.PublicChannelsOnly, + MaxNeighbors: ad.MaxNeighbors, + Truncated: ad.Truncated, + } + if len(ad.Neighbors) == 0 { + return out + } + + out.Neighbors = make([]ChannelAdjacencyNeighbor, len(ad.Neighbors)) + for i, neighbor := range ad.Neighbors { + out.Neighbors[i].NodeID = neighbor.NodeID + if len(neighbor.Channels) == 0 { + continue + } + out.Neighbors[i].Channels = make([]ChannelAdjacencyChannel, len(neighbor.Channels)) + copy(out.Neighbors[i].Channels, neighbor.Channels) + } + + return out +} + +// UnmarshalJSON supports legacy field names from earlier iterations of this +// optional extension. +func (a *ChannelAdjacency) UnmarshalJSON(data []byte) error { + // New schema keys. + type schemaV1 struct { + SchemaVersion int `json:"schema_version,omitempty"` + PublicChannelsOnly bool `json:"public_channels_only,omitempty"` + MaxNeighbors int `json:"max_neighbors,omitempty"` + Truncated bool `json:"truncated,omitempty"` + Neighbors []ChannelAdjacencyNeighbor `json:"neighbors,omitempty"` + } + + // Legacy schema keys (2hop.md initial draft). + type legacySchema struct { + V int `json:"v,omitempty"` + PublicOnly bool `json:"public_only,omitempty"` + Limit int `json:"limit,omitempty"` + Entries []ChannelAdjacencyNeighbor `json:"entries,omitempty"` + } + + var v1 schemaV1 + if err := json.Unmarshal(data, &v1); err != nil { + return err + } + + if v1.SchemaVersion != 0 || v1.MaxNeighbors != 0 || v1.Truncated || v1.PublicChannelsOnly || len(v1.Neighbors) > 0 { + a.SchemaVersion = v1.SchemaVersion + a.PublicChannelsOnly = v1.PublicChannelsOnly + a.MaxNeighbors = v1.MaxNeighbors + a.Truncated = v1.Truncated + a.Neighbors = v1.Neighbors + return nil + } + + var legacy legacySchema + if err := json.Unmarshal(data, &legacy); err != nil { + return err + } + + a.SchemaVersion = legacy.V + a.PublicChannelsOnly = legacy.PublicOnly + a.MaxNeighbors = legacy.Limit + a.Neighbors = legacy.Entries + a.Truncated = false + return nil +} diff --git a/peersync/cln_lightning_adapter.go b/peersync/cln_lightning_adapter.go index acb0cb1e..4f25b5d5 100644 --- a/peersync/cln_lightning_adapter.go +++ b/peersync/cln_lightning_adapter.go @@ -19,6 +19,8 @@ type GlightningClient interface { // AddMessageHandler registers a callback for inbound custom messages. // The callback receives (peerID, msgTypeHex, payloadBytes). AddMessageHandler(func(peerID, msgType string, payload []byte) error) + // ListChannels returns the node's channels in peersync-compatible form. + ListChannels(ctx context.Context) ([]Channel, error) } // ClnLightningAdapter bridges a GlightningClient to the peersync Lightning interface. @@ -103,3 +105,10 @@ func (a *ClnLightningAdapter) ListPeers(ctx context.Context) ([]PeerID, error) { } return res, nil } + +func (a *ClnLightningAdapter) ListChannels(ctx context.Context) ([]Channel, error) { + if a.client == nil { + return nil, errors.New("peersync cln adapter: client not configured") + } + return a.client.ListChannels(ctx) +} diff --git a/peersync/cln_lightning_adapter_test.go b/peersync/cln_lightning_adapter_test.go index 64d32930..1dba69e8 100644 --- a/peersync/cln_lightning_adapter_test.go +++ b/peersync/cln_lightning_adapter_test.go @@ -1,4 +1,4 @@ -package peersync +package peersync_test import ( "context" @@ -8,6 +8,7 @@ import ( "time" "github.com/elementsproject/peerswap/messages" + "github.com/elementsproject/peerswap/peersync" psmocks "github.com/elementsproject/peerswap/peersync/mocks" "go.uber.org/mock/gomock" ) @@ -18,11 +19,11 @@ func TestClnLightningAdapter_SendCustomMessage(t *testing.T) { client := psmocks.NewMockGlightningClient(ctrl) client.EXPECT().AddMessageHandler(gomock.Any()) - adapter := NewClnLightningAdapter(client) + adapter := peersync.NewClnLightningAdapter(client) // Given a peer id and payload peerIDStr := "022222222222222222222222222222222222222222222222222222222222222222" - peerID, err := NewPeerID(peerIDStr) + peerID, err := peersync.NewPeerID(peerIDStr) if err != nil { t.Fatalf("unexpected error creating peer id: %v", err) } @@ -45,10 +46,10 @@ func TestClnLightningAdapter_SendCustomMessage_Error(t *testing.T) { client := psmocks.NewMockGlightningClient(ctrl) client.EXPECT().AddMessageHandler(gomock.Any()) - adapter := NewClnLightningAdapter(client) + adapter := peersync.NewClnLightningAdapter(client) peerIDStr := "03aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - peerID, _ := NewPeerID(peerIDStr) + peerID, _ := peersync.NewPeerID(peerIDStr) client. EXPECT(). @@ -67,7 +68,7 @@ func TestClnLightningAdapter_SubscribeAndPush(t *testing.T) { client := psmocks.NewMockGlightningClient(ctrl) // ListPeers should not be called here; no expectations are needed. client.EXPECT().AddMessageHandler(gomock.Any()) - adapter := NewClnLightningAdapter(client) + adapter := peersync.NewClnLightningAdapter(client) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -109,7 +110,7 @@ func TestClnLightningAdapter_ListPeers(t *testing.T) { "invalid", }) - adapter := NewClnLightningAdapter(client) + adapter := peersync.NewClnLightningAdapter(client) peers, err := adapter.ListPeers(context.Background()) if err != nil { @@ -131,7 +132,7 @@ func TestClnLightningAdapter_ListPeers_Empty(t *testing.T) { client.EXPECT().AddMessageHandler(gomock.Any()) client.EXPECT().GetPeers().Return(nil) - adapter := NewClnLightningAdapter(client) + adapter := peersync.NewClnLightningAdapter(client) peers, err := adapter.ListPeers(context.Background()) if err != nil { t.Fatalf("ListPeers returned error: %v", err) diff --git a/peersync/compatibility.go b/peersync/compatibility.go index 279d4237..803777c6 100644 --- a/peersync/compatibility.go +++ b/peersync/compatibility.go @@ -41,4 +41,3 @@ func (ps *PeerSync) CompatiblePeers() (map[string]*Peer, error) { return result, nil } - diff --git a/peersync/lightning_adapter.go b/peersync/lightning_adapter.go index f48d9762..364b6af4 100644 --- a/peersync/lightning_adapter.go +++ b/peersync/lightning_adapter.go @@ -10,8 +10,10 @@ import ( "strconv" "sync" + "github.com/elementsproject/peerswap/lightning" "github.com/elementsproject/peerswap/messages" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnwire" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -32,6 +34,11 @@ type LightningClient interface { in *lnrpc.ListPeersRequest, opts ...grpc.CallOption, ) (*lnrpc.ListPeersResponse, error) + ListChannels( + ctx context.Context, + in *lnrpc.ListChannelsRequest, + opts ...grpc.CallOption, + ) (*lnrpc.ListChannelsResponse, error) SubscribeCustomMessages( ctx context.Context, in *lnrpc.SubscribeCustomMessagesRequest, @@ -216,3 +223,43 @@ func (a *LightningAdapter) ListPeers(ctx context.Context) ([]PeerID, error) { } return result, nil } + +func (a *LightningAdapter) ListChannels(ctx context.Context) ([]Channel, error) { + if a.client == nil { + return nil, errors.New("peersync adapter: client not configured") + } + + // We need both active and public channels to build optional 2-hop discovery + // hints such as ChannelAdjacency. + // We fetch all channels and filter in the caller so that future extensions + // can adjust filtering without changing this adapter signature. + resp, err := a.client.ListChannels(ctx, &lnrpc.ListChannelsRequest{ + ActiveOnly: false, + InactiveOnly: false, + PublicOnly: false, + PrivateOnly: false, + }) + if err != nil { + return nil, err + } + + channels := make([]Channel, 0, len(resp.Channels)) + for _, ch := range resp.Channels { + peer, err := NewPeerID(ch.RemotePubkey) + if err != nil { + continue + } + + shortID := lnwire.NewShortChanIDFromInt(ch.ChanId) + shortChannelID := lightning.Scid(shortID.String()).ClnStyle() + + channels = append(channels, Channel{ + Peer: peer, + ChannelID: ch.ChanId, + ShortChannelID: shortChannelID, + Active: ch.Active, + Public: !ch.Private, + }) + } + return channels, nil +} diff --git a/peersync/lightning_adapter_test.go b/peersync/lightning_adapter_test.go index d64b7c4c..46fa25f3 100644 --- a/peersync/lightning_adapter_test.go +++ b/peersync/lightning_adapter_test.go @@ -1,4 +1,4 @@ -package peersync +package peersync_test import ( "bytes" @@ -10,6 +10,7 @@ import ( "time" "github.com/elementsproject/peerswap/messages" + "github.com/elementsproject/peerswap/peersync" psmocks "github.com/elementsproject/peerswap/peersync/mocks" "github.com/lightningnetwork/lnd/lnrpc" "go.uber.org/mock/gomock" @@ -81,10 +82,10 @@ func TestLightningAdapterSendCustomMessage(t *testing.T) { t.Cleanup(ctrl.Finish) client := psmocks.NewMockLightningClient(ctrl) - adapter := NewLightningAdapter(client) + adapter := peersync.NewLightningAdapter(client) peerIDValue := "022222222222222222222222222222222222222222222222222222222222222222" - peerID, err := NewPeerID(peerIDValue) + peerID, err := peersync.NewPeerID(peerIDValue) if err != nil { t.Fatalf("unexpected error creating peer id: %v", err) } @@ -115,9 +116,9 @@ func TestLightningAdapterSendCustomMessageInvalidPeer(t *testing.T) { t.Cleanup(ctrl.Finish) client := psmocks.NewMockLightningClient(ctrl) - adapter := NewLightningAdapter(client) + adapter := peersync.NewLightningAdapter(client) - invalidPeer, err := NewPeerID("not-hex") + invalidPeer, err := peersync.NewPeerID("not-hex") if err != nil { t.Fatalf("unexpected error creating peer id: %v", err) } @@ -141,7 +142,7 @@ func TestLightningAdapterSubscribeCustomMessages(t *testing.T) { return stream, nil }) - adapter := NewLightningAdapter(client) + adapter := peersync.NewLightningAdapter(client) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -165,7 +166,7 @@ func TestLightningAdapterSubscribeCustomMessages(t *testing.T) { Data: payload, }) - var msg CustomMessage + var msg peersync.CustomMessage select { case msg = <-ch: if msg.From.String() != peerID { @@ -198,7 +199,7 @@ func TestLightningAdapterListPeers(t *testing.T) { }, }, nil) - adapter := NewLightningAdapter(client) + adapter := peersync.NewLightningAdapter(client) peers, err := adapter.ListPeers(context.Background()) if err != nil { @@ -223,7 +224,7 @@ func TestLightningAdapterListPeersError(t *testing.T) { ListPeers(gomock.Any(), gomock.AssignableToTypeOf(&lnrpc.ListPeersRequest{})). Return(nil, errors.New("boom")) - adapter := NewLightningAdapter(client) + adapter := peersync.NewLightningAdapter(client) if _, err := adapter.ListPeers(context.Background()); err == nil { t.Fatalf("expected error from ListPeers") diff --git a/peersync/message_handler.go b/peersync/message_handler.go index 62f5922d..fba5035e 100644 --- a/peersync/message_handler.go +++ b/peersync/message_handler.go @@ -65,7 +65,7 @@ func (h *messageHandler) handlePollMessage(ctx context.Context, msg CustomMessag return } - peerID, capability, err := h.parsePollMessage(msg) + peerID, snapshot, capability, err := h.parsePollMessage(msg) if err != nil { log.Printf("failed to parse poll message: %v", err) return @@ -86,6 +86,9 @@ func (h *messageHandler) handlePollMessage(ctx context.Context, msg CustomMessag } peer.UpdateCapability(capability) + if snapshot != nil && snapshot.ChannelAdjacency != nil { + peer.UpdateChannelAdjacency(snapshot.ChannelAdjacency) + } if err := h.store.SavePeerState(peer); err != nil { log.Printf("failed to store peer state: %v", err) @@ -117,19 +120,19 @@ func (h *messageHandler) handleRequestPollMessage(ctx context.Context, msg Custo } } -func (h *messageHandler) parsePollMessage(msg CustomMessage) (PeerID, *PeerCapability, error) { +func (h *messageHandler) parsePollMessage(msg CustomMessage) (PeerID, *PollMessageDTO, *PeerCapability, error) { peerID := msg.From var payload PollMessageDTO if err := json.Unmarshal(msg.Payload, &payload); err != nil { - return peerID, nil, fmt.Errorf("decode poll message: %w", err) + return peerID, nil, nil, fmt.Errorf("decode poll message: %w", err) } capability, err := payload.ToCapability() if err != nil { - return peerID, nil, fmt.Errorf("invalid poll payload: %w", err) + return peerID, nil, nil, fmt.Errorf("invalid poll payload: %w", err) } - return peerID, capability, nil + return peerID, &payload, capability, nil } func (h *messageHandler) findPeer(peerID PeerID) (*Peer, error) { diff --git a/peersync/mocks/mock_glightning_client.go b/peersync/mocks/mock_glightning_client.go index 05b8cc30..3bbb0594 100644 --- a/peersync/mocks/mock_glightning_client.go +++ b/peersync/mocks/mock_glightning_client.go @@ -10,70 +10,87 @@ package mocks import ( - reflect "reflect" + context "context" + reflect "reflect" - gomock "go.uber.org/mock/gomock" + peersync "github.com/elementsproject/peerswap/peersync" + gomock "go.uber.org/mock/gomock" ) // MockGlightningClient is a mock of GlightningClient interface. type MockGlightningClient struct { - ctrl *gomock.Controller - recorder *MockGlightningClientMockRecorder + ctrl *gomock.Controller + recorder *MockGlightningClientMockRecorder } // MockGlightningClientMockRecorder is the mock recorder for MockGlightningClient. type MockGlightningClientMockRecorder struct { - mock *MockGlightningClient + mock *MockGlightningClient } // NewMockGlightningClient creates a new mock instance. func NewMockGlightningClient(ctrl *gomock.Controller) *MockGlightningClient { - mock := &MockGlightningClient{ctrl: ctrl} - mock.recorder = &MockGlightningClientMockRecorder{mock} - return mock + mock := &MockGlightningClient{ctrl: ctrl} + mock.recorder = &MockGlightningClientMockRecorder{mock} + return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockGlightningClient) EXPECT() *MockGlightningClientMockRecorder { - return m.recorder + return m.recorder } // AddMessageHandler mocks base method. func (m *MockGlightningClient) AddMessageHandler(arg0 func(string, string, []byte) error) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "AddMessageHandler", arg0) + m.ctrl.T.Helper() + m.ctrl.Call(m, "AddMessageHandler", arg0) } // AddMessageHandler indicates an expected call of AddMessageHandler. func (mr *MockGlightningClientMockRecorder) AddMessageHandler(arg0 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMessageHandler", reflect.TypeOf((*MockGlightningClient)(nil).AddMessageHandler), arg0) + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMessageHandler", reflect.TypeOf((*MockGlightningClient)(nil).AddMessageHandler), arg0) } // GetPeers mocks base method. func (m *MockGlightningClient) GetPeers() []string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPeers") - ret0, _ := ret[0].([]string) - return ret0 + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPeers") + ret0, _ := ret[0].([]string) + return ret0 } // GetPeers indicates an expected call of GetPeers. func (mr *MockGlightningClientMockRecorder) GetPeers() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeers", reflect.TypeOf((*MockGlightningClient)(nil).GetPeers)) + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeers", reflect.TypeOf((*MockGlightningClient)(nil).GetPeers)) +} + +// ListChannels mocks base method. +func (m *MockGlightningClient) ListChannels(arg0 context.Context) ([]peersync.Channel, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListChannels", arg0) + ret0, _ := ret[0].([]peersync.Channel) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListChannels indicates an expected call of ListChannels. +func (mr *MockGlightningClientMockRecorder) ListChannels(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListChannels", reflect.TypeOf((*MockGlightningClient)(nil).ListChannels), arg0) } // SendMessage mocks base method. func (m *MockGlightningClient) SendMessage(arg0 string, arg1 []byte, arg2 int) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SendMessage", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendMessage", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 } // SendMessage indicates an expected call of SendMessage. func (mr *MockGlightningClientMockRecorder) SendMessage(arg0, arg1, arg2 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendMessage", reflect.TypeOf((*MockGlightningClient)(nil).SendMessage), arg0, arg1, arg2) + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendMessage", reflect.TypeOf((*MockGlightningClient)(nil).SendMessage), arg0, arg1, arg2) } diff --git a/peersync/mocks/mock_lightning_client.go b/peersync/mocks/mock_lightning_client.go index 2798cf05..41f81e1c 100644 --- a/peersync/mocks/mock_lightning_client.go +++ b/peersync/mocks/mock_lightning_client.go @@ -61,6 +61,26 @@ func (mr *MockLightningClientMockRecorder) ListPeers(arg0, arg1 any, arg2 ...any return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPeers", reflect.TypeOf((*MockLightningClient)(nil).ListPeers), varargs...) } +// ListChannels mocks base method. +func (m *MockLightningClient) ListChannels(arg0 context.Context, arg1 *lnrpc.ListChannelsRequest, arg2 ...grpc.CallOption) (*lnrpc.ListChannelsResponse, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ListChannels", varargs...) + ret0, _ := ret[0].(*lnrpc.ListChannelsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListChannels indicates an expected call of ListChannels. +func (mr *MockLightningClientMockRecorder) ListChannels(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListChannels", reflect.TypeOf((*MockLightningClient)(nil).ListChannels), varargs...) +} + // SendCustomMessage mocks base method. func (m *MockLightningClient) SendCustomMessage(arg0 context.Context, arg1 *lnrpc.SendCustomMessageRequest, arg2 ...grpc.CallOption) (*lnrpc.SendCustomMessageResponse, error) { m.ctrl.T.Helper() diff --git a/peersync/peer.go b/peersync/peer.go index c1dc1933..01236ccb 100644 --- a/peersync/peer.go +++ b/peersync/peer.go @@ -255,23 +255,25 @@ const ( // Peer represents a remote node tracked by peersync. type Peer struct { - id PeerID - address string - capability *PeerCapability - status PeerStatus - lastPollAt time.Time - lastObservedAt time.Time + id PeerID + address string + capability *PeerCapability + channelAdjacency *ChannelAdjacency + status PeerStatus + lastPollAt time.Time + lastObservedAt time.Time } // NewPeer constructs a peer model with an unknown initial status. func NewPeer(id PeerID, address string) *Peer { return &Peer{ - id: id, - address: address, - capability: nil, - status: StatusUnknown, - lastPollAt: time.Time{}, - lastObservedAt: time.Time{}, + id: id, + address: address, + capability: nil, + channelAdjacency: nil, + status: StatusUnknown, + lastPollAt: time.Time{}, + lastObservedAt: time.Time{}, } } @@ -290,6 +292,24 @@ func (p *Peer) Capability() *PeerCapability { return p.capability } +// ChannelAdjacency returns the last received 2-hop discovery hint (if any). +// +// See ChannelAdjacency for the exact domain definition and trust model. +func (p *Peer) ChannelAdjacency() *ChannelAdjacency { + if p == nil { + return nil + } + return cloneChannelAdjacency(p.channelAdjacency) +} + +// UpdateChannelAdjacency stores the advertised 2-hop discovery hint. +func (p *Peer) UpdateChannelAdjacency(ad *ChannelAdjacency) { + if p == nil { + return + } + p.channelAdjacency = cloneChannelAdjacency(ad) +} + // UpdateCapability refreshes the peer capability and marks it active. func (p *Peer) UpdateCapability(capability *PeerCapability) { p.capability = capability diff --git a/peersync/peersync.go b/peersync/peersync.go index c32bf2c8..13d1f246 100644 --- a/peersync/peersync.go +++ b/peersync/peersync.go @@ -219,6 +219,9 @@ func (ps *PeerSync) sendCapability(ctx context.Context, peer PeerID, msgType mes capability := ps.localCapabilityForPeer(peer) msg := ps.capabilityToDTO(capability, peer) + if msgType == messages.MESSAGETYPE_POLL { + ps.attachChannelAdjacency(ctx, msg) + } payload, err := json.Marshal(msg) if err != nil { @@ -231,6 +234,24 @@ func (ps *PeerSync) sendCapability(ctx context.Context, peer PeerID, msgType mes return ps.lightning.SendCustomMessage(cctx, peer, msgType, payload) } +func (ps *PeerSync) attachChannelAdjacency(ctx context.Context, msg *PollMessageDTO) { + if ps == nil || msg == nil || ps.lightning == nil { + return + } + + channels, err := ps.lightning.ListChannels(ctx) + if err != nil { + log.Printf("peersync: failed to list channels for channel_adjacency: %v", err) + return + } + + ad := buildChannelAdjacency(channels) + if ad == nil { + return + } + msg.ChannelAdjacency = ad +} + // Stop terminates the underlying receiver. func (ps *PeerSync) Stop() error { if ps.lightning == nil { diff --git a/peersync/ports.go b/peersync/ports.go index 05d77b64..e386bc8b 100644 --- a/peersync/ports.go +++ b/peersync/ports.go @@ -28,4 +28,19 @@ type Lightning interface { // ListPeers returns currently connected peer IDs (node pubkeys). ListPeers(ctx context.Context) ([]PeerID, error) + + // ListChannels returns channels known to the local node. + // Implementations should include at least peer id, short channel id, and + // whether the channel is public/active. + ListChannels(ctx context.Context) ([]Channel, error) +} + +// Channel describes a local channel in a transport-friendly way. +// It is used to build optional metadata such as ChannelAdjacency. +type Channel struct { + Peer PeerID + ChannelID uint64 + ShortChannelID string + Active bool + Public bool } diff --git a/peersync/service_test.go b/peersync/service_test.go index af2231bc..09320df2 100644 --- a/peersync/service_test.go +++ b/peersync/service_test.go @@ -55,6 +55,8 @@ func (l *stubLightning) Stop() error { return l.stopErr } func (l *stubLightning) ListPeers(ctx context.Context) ([]PeerID, error) { return l.peers, nil } +func (l *stubLightning) ListChannels(ctx context.Context) ([]Channel, error) { return nil, nil } + func (l *stubLightning) recordSend(call sentCall) { l.mu.Lock() defer l.mu.Unlock() @@ -200,6 +202,19 @@ func TestHandlePollMessage(t *testing.T) { BTCSwapOutPremiumRatePPM: 200, LBTCSwapInPremiumRatePPM: 300, LBTCSwapOutPremiumRatePPM: 400, + ChannelAdjacency: &ChannelAdjacency{ + SchemaVersion: 1, + PublicChannelsOnly: true, + MaxNeighbors: 20, + Neighbors: []ChannelAdjacencyNeighbor{ + { + NodeID: "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + Channels: []ChannelAdjacencyChannel{ + {ShortChannelID: "1x2x3", Active: true}, + }, + }, + }, + }, } data, err := json.Marshal(payload) if err != nil { @@ -219,6 +234,68 @@ func TestHandlePollMessage(t *testing.T) { if stored.Capability().Version().Value() != NewVersion(2).Value() { t.Fatalf("unexpected capability version") } + + ad := stored.ChannelAdjacency() + if ad == nil || len(ad.Neighbors) != 1 || ad.Neighbors[0].NodeID == "" { + t.Fatalf("expected channel_adjacency to be persisted on peer state") + } + if ad.PublicChannelsOnly != true || ad.SchemaVersion != 1 { + t.Fatalf("unexpected channel_adjacency metadata") + } +} + +func TestHandlePollMessage_LegacyNeighborsAd(t *testing.T) { + syncer, deps := newTestPeerSync(t) + + ctx := context.Background() + + peerID, err := NewPeerID("peer-remote") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + peer := NewPeer(peerID, "addr-remote") + peer.SetStatus(StatusActive) + if err := deps.store.SavePeerState(peer); err != nil { + t.Fatalf("failed to seed peer state: %v", err) + } + + legacy := `{ + "version": 2, + "assets": ["BTC"], + "peer_allowed": true, + "btc_swap_in_premium_rate_ppm": 100, + "btc_swap_out_premium_rate_ppm": 200, + "lbtc_swap_in_premium_rate_ppm": 300, + "lbtc_swap_out_premium_rate_ppm": 400, + "neighbors_ad": { + "v": 1, + "public_only": true, + "limit": 20, + "entries": [ + { + "node_id": "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "channels": [ + { "short_channel_id": "1x2x3", "active": true } + ] + } + ] + } +}` + + syncer.handler.handlePollMessage(ctx, CustomMessage{From: peerID, Type: messages.MESSAGETYPE_POLL, Payload: []byte(legacy)}) + + stored, err := deps.store.GetPeerState(peerID) + if err != nil { + t.Fatalf("failed to load peer state: %v", err) + } + + ad := stored.ChannelAdjacency() + if ad == nil || len(ad.Neighbors) != 1 || ad.Neighbors[0].NodeID == "" { + t.Fatalf("expected legacy neighbors_ad to be persisted as channel_adjacency") + } + if ad.SchemaVersion != 1 || ad.PublicChannelsOnly != true || ad.MaxNeighbors != 20 || ad.Truncated != false { + t.Fatalf("unexpected legacy channel_adjacency mapping") + } } func TestHandleRequestPollMessage(t *testing.T) { diff --git a/peersync/store.go b/peersync/store.go index 8e2ce931..fdb4d0e7 100644 --- a/peersync/store.go +++ b/peersync/store.go @@ -257,6 +257,26 @@ type peerRecord struct { BTCSwapOutPremiumRatePPM int64 `json:"btc_swap_out_premium_rate_ppm,omitempty"` LBTCSwapInPremiumRatePPM int64 `json:"lbtc_swap_in_premium_rate_ppm,omitempty"` LBTCSwapOutPremiumRatePPM int64 `json:"lbtc_swap_out_premium_rate_ppm,omitempty"` + // ChannelAdjacency is optional advisory data used for 2-hop discovery. + ChannelAdjacency *ChannelAdjacency `json:"channel_adjacency,omitempty"` +} + +// UnmarshalJSON keeps backwards-compatibility for legacy field names that may +// exist in persisted peer records. +func (r *peerRecord) UnmarshalJSON(data []byte) error { + type alias peerRecord + var tmp struct { + alias + LegacyNeighborsAd *ChannelAdjacency `json:"neighbors_ad,omitempty"` + } + if err := json.Unmarshal(data, &tmp); err != nil { + return err + } + *r = peerRecord(tmp.alias) + if r.ChannelAdjacency == nil && tmp.LegacyNeighborsAd != nil { + r.ChannelAdjacency = tmp.LegacyNeighborsAd + } + return nil } func (r *peerRecord) snapshot() *PeerCapabilitySnapshot { @@ -298,6 +318,12 @@ func peerToRecord(peer *Peer) *peerRecord { Status: peer.Status(), LastPollAt: peer.LastPollAt(), LastSeen: peer.LastObservedAt(), + ChannelAdjacency: func() *ChannelAdjacency { + if peer == nil { + return nil + } + return cloneChannelAdjacency(peer.channelAdjacency) + }(), } if capability := peer.Capability(); capability != nil { @@ -325,6 +351,8 @@ func (r *peerRecord) toPeer(key string) (*Peer, error) { peer.capability = capability } + peer.channelAdjacency = cloneChannelAdjacency(r.ChannelAdjacency) + return peer, nil } diff --git a/swap/actions.go b/swap/actions.go index 3c417602..8bf30902 100644 --- a/swap/actions.go +++ b/swap/actions.go @@ -455,6 +455,11 @@ func (c *CreateSwapOutFromRequestAction) Execute(services *SwapServices, swap *S Payreq: feeInvoice, Premium: premiumValue, } + if swap.SwapOutRequest != nil && swap.SwapOutRequest.TwoHop != nil { + message.TwoHop = &TwoHop{ + IncomingScid: swap.GetLocalScid(), + } + } swap.SwapOutAgreement = message nextMessage, nextMessageType, err := MarshalPeerswapMessage(message) @@ -613,13 +618,13 @@ func (s *SendMessageWithRetryAction) Execute(services *SwapServices, swap *SwapD return swap.HandleError(errors.New("swap.NextMessage is nil")) } - // Send message repeated as we really want the message to be received at some point! - // In fast_test builds, lower the retry interval to speed up tests. - retryDur := 10 * time.Second - if isdev.FastTests() { - retryDur = 1 * time.Second - } - rm := messages.NewRedundantMessenger(services.messenger, retryDur) + // Send message repeated as we really want the message to be received at some point! + // In fast_test builds, lower the retry interval to speed up tests. + retryDur := 10 * time.Second + if isdev.FastTests() { + retryDur = 1 * time.Second + } + rm := messages.NewRedundantMessenger(services.messenger, retryDur) err := services.messengerManager.AddSender(swap.GetId().String(), rm) if err != nil { return swap.HandleError(err) @@ -661,12 +666,14 @@ func (r *PayFeeInvoiceAction) Execute(services *SwapServices, swap *SwapData) Ev } // Probe the payment to check if it's possible - success, failureReason, err := ll.ProbePayment(swap.SwapOutRequest.Scid, requiredBalance) - if err != nil { - return swap.HandleError(err) - } - if !success { - return swap.HandleError(fmt.Errorf("the prepayment probe was unsuccessful: %s", failureReason)) + if swap.SwapOutRequest.TwoHop == nil { + success, failureReason, err := ll.ProbePayment(swap.GetLocalScid(), requiredBalance) + if err != nil { + return swap.HandleError(err) + } + if !success { + return swap.HandleError(fmt.Errorf("the prepayment probe was unsuccessful: %s", failureReason)) + } } swap.OpeningTxFee = msatAmt / 1000 @@ -684,7 +691,20 @@ func (r *PayFeeInvoiceAction) Execute(services *SwapServices, swap *SwapData) Ev return swap.HandleError(errors.New(fmt.Sprintf("Fee is too damn high. Max expected: %v Received %v", maxExpected, swap.OpeningTxFee))) } - preimage, err := ll.PayInvoiceViaChannel(swap.SwapOutAgreement.Payreq, swap.GetScid()) + var preimage string + if swap.SwapOutRequest.TwoHop != nil { + if swap.SwapOutAgreement == nil || swap.SwapOutAgreement.TwoHop == nil || swap.SwapOutAgreement.TwoHop.IncomingScid == "" { + return swap.HandleError(errors.New("missing twohop incoming scid in swap_out_agreement")) + } + preimage, err = ll.PayInvoiceVia2HopRoute( + swap.SwapOutAgreement.Payreq, + swap.GetLocalScid(), + swap.SwapOutAgreement.TwoHop.IncomingScid, + swap.SwapOutRequest.TwoHop.IntermediaryPubkey, + ) + } else { + preimage, err = ll.PayInvoiceViaChannel(swap.SwapOutAgreement.Payreq, swap.GetLocalScid()) + } if err != nil { return swap.HandleError(err) } @@ -827,7 +847,26 @@ func (p *ValidateTxAndPayClaimInvoiceAction) Execute(services *SwapServices, swa swap.LastErr = err return swap.HandleError(err) } - preimage, err = lc.RebalancePayment(swap.OpeningTxBroadcasted.Payreq, swap.GetScid()) + if swap.SwapOutRequest != nil && swap.SwapOutRequest.TwoHop != nil { + if swap.SwapOutAgreement == nil || swap.SwapOutAgreement.TwoHop == nil || swap.SwapOutAgreement.TwoHop.IncomingScid == "" { + return swap.HandleError(errors.New("missing twohop incoming scid in swap_out_agreement")) + } + preimage, err = lc.PayInvoiceVia2HopRoute( + swap.OpeningTxBroadcasted.Payreq, + swap.GetLocalScid(), + swap.SwapOutAgreement.TwoHop.IncomingScid, + swap.SwapOutRequest.TwoHop.IntermediaryPubkey, + ) + } else if swap.SwapInRequest != nil && swap.SwapInRequest.TwoHop != nil { + preimage, err = lc.PayInvoiceVia2HopRoute( + swap.OpeningTxBroadcasted.Payreq, + swap.GetLocalScid(), + swap.SwapInRequest.Scid, + swap.SwapInRequest.TwoHop.IntermediaryPubkey, + ) + } else { + preimage, err = lc.RebalancePayment(swap.OpeningTxBroadcasted.Payreq, swap.GetLocalScid()) + } if err != nil { log.Infof("error trying to pay invoice: %v, retry...", err) payErr = err diff --git a/swap/actions_twohop_test.go b/swap/actions_twohop_test.go new file mode 100644 index 00000000..1c0138ef --- /dev/null +++ b/swap/actions_twohop_test.go @@ -0,0 +1,156 @@ +package swap + +import ( + "context" + "os" + "path" + "testing" + + "github.com/elementsproject/peerswap/policy" + "github.com/elementsproject/peerswap/premium" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.etcd.io/bbolt" +) + +func newTestPremiumSetting(t *testing.T) *premium.Setting { + t.Helper() + + dir := t.TempDir() + db, err := bbolt.Open(path.Join(dir, "premium-db"), os.ModePerm, nil) + require.NoError(t, err) + t.Cleanup(func() { _ = db.Close() }) + + premiumSetting, err := premium.NewSetting(db) + require.NoError(t, err) + ctx := context.Background() + require.NoError(t, + premiumSetting.SetDefaultRate(ctx, + lo.Must(premium.NewPremiumRate(premium.BTC, premium.SwapIn, premium.NewPPM(10000))))) + require.NoError(t, + premiumSetting.SetDefaultRate(ctx, + lo.Must(premium.NewPremiumRate(premium.BTC, premium.SwapOut, premium.NewPPM(10000))))) + require.NoError(t, + premiumSetting.SetDefaultRate(ctx, + lo.Must(premium.NewPremiumRate(premium.LBTC, premium.SwapIn, premium.NewPPM(10000))))) + require.NoError(t, + premiumSetting.SetDefaultRate(ctx, + lo.Must(premium.NewPremiumRate(premium.LBTC, premium.SwapOut, premium.NewPPM(10000))))) + + return premiumSetting +} + +func TestCreateSwapOutFromRequestAction_SetsTwoHopIncomingScid(t *testing.T) { + t.Parallel() + + swapID := NewSwapId() + localScid := "22x2x2" + + swapData := &SwapData{ + PeerNodeId: getRandom33ByteHexString(), + PrivkeyBytes: getRandomPrivkey().Serialize(), + LocalScid: localScid, + SwapOutRequest: &SwapOutRequestMessage{ + ProtocolVersion: PEERSWAP_PROTOCOL_VERSION, + SwapId: swapID, + Asset: "", + Network: "mainnet", + Scid: "100x2x3", + Amount: 100000, + Pubkey: getRandom33ByteHexString(), + PremiumLimit: 999999999, + TwoHop: &TwoHop{ + IntermediaryPubkey: getRandom33ByteHexString(), + }, + }, + } + + chain := &dummyChain{returnGetCSVHeight: 1008} + chain.SetBalance(10_000_000) + lc := &dummyLightningClient{preimage: ""} + + services := &SwapServices{ + lightning: lc, + bitcoinTxWatcher: chain, + bitcoinWallet: chain, + bitcoinValidator: chain, + bitcoinEnabled: true, + liquidTxWatcher: chain, + liquidWallet: chain, + liquidValidator: chain, + liquidEnabled: true, + messenger: &dummyMessenger{}, + messengerManager: &MessengerManagerStub{}, + policy: &dummyPolicy{ + getMinSwapAmountMsatReturn: policy.DefaultPolicy().MinSwapAmountMsat, + newSwapsAllowedReturn: policy.DefaultPolicy().AllowNewSwaps, + }, + toService: &timeOutDummy{}, + ps: newTestPremiumSetting(t), + } + + a := &CreateSwapOutFromRequestAction{} + got := a.Execute(services, swapData) + assert.Equal(t, Event_ActionSucceeded, got) + require.NotNil(t, swapData.SwapOutAgreement) + require.NotNil(t, swapData.SwapOutAgreement.TwoHop) + assert.Equal(t, localScid, swapData.SwapOutAgreement.TwoHop.IncomingScid) +} + +func TestPayFeeInvoiceAction_Uses2HopPaymentWhenRequested(t *testing.T) { + t.Parallel() + + swapID := NewSwapId() + intermediary := getRandom33ByteHexString() + outgoingScid := "111x1x1" + incomingScid := "222x2x2" + + swapData := &SwapData{ + LocalScid: outgoingScid, + SwapOutRequest: &SwapOutRequestMessage{ + ProtocolVersion: PEERSWAP_PROTOCOL_VERSION, + SwapId: swapID, + Asset: "", + Network: "mainnet", + Scid: outgoingScid, + Amount: 100000, + Pubkey: getRandom33ByteHexString(), + PremiumLimit: 999999999, + TwoHop: &TwoHop{ + IntermediaryPubkey: intermediary, + }, + }, + SwapOutAgreement: &SwapOutAgreementMessage{ + ProtocolVersion: PEERSWAP_PROTOCOL_VERSION, + SwapId: swapID, + Pubkey: getRandom33ByteHexString(), + Payreq: "fee", + Premium: 0, + TwoHop: &TwoHop{ + IncomingScid: incomingScid, + }, + }, + } + + chain := &dummyChain{returnGetCSVHeight: 1008} + lc := &dummyLightningClient{preimage: ""} + + services := &SwapServices{ + lightning: lc, + bitcoinTxWatcher: chain, + bitcoinWallet: chain, + bitcoinValidator: chain, + bitcoinEnabled: true, + } + + a := &PayFeeInvoiceAction{} + got := a.Execute(services, swapData) + assert.Equal(t, Event_ActionSucceeded, got) + assert.Equal(t, 1, lc.payInvoiceVia2HopRouteCalled) + assert.Equal(t, "fee", lc.payInvoiceVia2HopRouteParams.payreq) + assert.Equal(t, outgoingScid, lc.payInvoiceVia2HopRouteParams.outgoingScid) + assert.Equal(t, incomingScid, lc.payInvoiceVia2HopRouteParams.incomingScid) + assert.Equal(t, intermediary, lc.payInvoiceVia2HopRouteParams.intermediaryPubkey) + assert.NotEmpty(t, swapData.FeePreimage) +} diff --git a/swap/messages.go b/swap/messages.go index b5c57db3..b7e19fb1 100644 --- a/swap/messages.go +++ b/swap/messages.go @@ -23,6 +23,11 @@ func NewInvalidLengthError(paramName string, expected, actual int) error { return fmt.Errorf("Param %s is of invalid length expected: %v, actual %v", paramName, expected, actual) } +type TwoHop struct { + IntermediaryPubkey string `json:"intermediary_pubkey,omitempty"` + IncomingScid string `json:"incoming_scid,omitempty"` +} + type SwapInRequestMessage struct { // ProtocolVersion is the version of the PeerSwap peer protocol the sending // node uses. @@ -43,9 +48,10 @@ type SwapInRequestMessage struct { // with x as separator, e.g. 539268x845x1. Scid string `json:"scid"` // Amount is The amount in Sats that is asked for. - Amount uint64 `json:"amount"` - Pubkey string `json:"pubkey"` - PremiumLimit int64 `json:"acceptable_premium"` + Amount uint64 `json:"amount"` + Pubkey string `json:"pubkey"` + PremiumLimit int64 `json:"acceptable_premium"` + TwoHop *TwoHop `json:"twohop,omitempty"` } func (s SwapInRequestMessage) MessageType() messages.MessageType { @@ -57,6 +63,12 @@ func (s SwapInRequestMessage) Validate(swap *SwapData) error { if err != nil { return err } + if s.TwoHop != nil { + err = validateHexString("twohop.intermediary_pubkey", s.TwoHop.IntermediaryPubkey, 33) + if err != nil { + return err + } + } err = validateAssetAndNetwork(s.Asset, s.Network) if err != nil { return err @@ -171,7 +183,8 @@ type SwapInAgreementMessage struct { Pubkey string `json:"pubkey"` // Premium is a compensation in Sats that the swap partner wants to be paid // in order to participate in the swap. - Premium int64 `json:"premium"` + Premium int64 `json:"premium"` + TwoHop *TwoHop `json:"twohop,omitempty"` } func (s SwapInAgreementMessage) Validate(swap *SwapData) error { @@ -179,6 +192,12 @@ func (s SwapInAgreementMessage) Validate(swap *SwapData) error { if err != nil { return err } + if s.TwoHop != nil && s.TwoHop.IncomingScid != "" { + err = validateScid(s.TwoHop.IncomingScid) + if err != nil { + return err + } + } return nil } @@ -219,8 +238,9 @@ type SwapOutRequestMessage struct { Amount uint64 `json:"amount"` // Pubkey is a 33 byte compressed public key used for the spending paths in // the opening_transaction. - Pubkey string `json:"pubkey"` - PremiumLimit int64 `json:"acceptable_premium"` + Pubkey string `json:"pubkey"` + PremiumLimit int64 `json:"acceptable_premium"` + TwoHop *TwoHop `json:"twohop,omitempty"` } func (s SwapOutRequestMessage) Validate(swap *SwapData) error { @@ -228,6 +248,12 @@ func (s SwapOutRequestMessage) Validate(swap *SwapData) error { if err != nil { return err } + if s.TwoHop != nil { + err = validateHexString("twohop.intermediary_pubkey", s.TwoHop.IntermediaryPubkey, 33) + if err != nil { + return err + } + } err = validateAssetAndNetwork(s.Asset, s.Network) if err != nil { return err @@ -269,7 +295,8 @@ type SwapOutAgreementMessage struct { Payreq string // Premium is a compensation in Sats that the swap partner wants to be paid // in order to participate in the swap. - Premium int64 `json:"premium"` + Premium int64 `json:"premium"` + TwoHop *TwoHop `json:"twohop,omitempty"` } func (s SwapOutAgreementMessage) Validate(swap *SwapData) error { @@ -277,6 +304,12 @@ func (s SwapOutAgreementMessage) Validate(swap *SwapData) error { if err != nil { return err } + if s.TwoHop != nil && s.TwoHop.IncomingScid != "" { + err = validateScid(s.TwoHop.IncomingScid) + if err != nil { + return err + } + } return nil } diff --git a/swap/messages_test.go b/swap/messages_test.go new file mode 100644 index 00000000..88d49f84 --- /dev/null +++ b/swap/messages_test.go @@ -0,0 +1,197 @@ +package swap + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPeerswapMessages_TwoHopJSONIsOptional(t *testing.T) { + t.Parallel() + + swapID := strings.Repeat("00", 32) + pubkey := "02" + strings.Repeat("00", 32) + intermediaryPubkey := "03" + strings.Repeat("11", 32) + + t.Run("swap_in_request without twohop", func(t *testing.T) { + t.Parallel() + + raw := `{ + "protocol_version": 5, + "swap_id": "` + swapID + `", + "asset": "", + "network": "regtest", + "scid": "1x2x3", + "amount": 100, + "pubkey": "` + pubkey + `", + "acceptable_premium": 0 + }` + + var msg SwapInRequestMessage + require.NoError(t, json.Unmarshal([]byte(raw), &msg)) + assert.Nil(t, msg.TwoHop) + require.NoError(t, msg.Validate(&SwapData{})) + }) + + t.Run("swap_in_request with twohop", func(t *testing.T) { + t.Parallel() + + raw := `{ + "protocol_version": 5, + "swap_id": "` + swapID + `", + "asset": "", + "network": "regtest", + "scid": "1x2x3", + "amount": 100, + "pubkey": "` + pubkey + `", + "acceptable_premium": 0, + "twohop": { + "intermediary_pubkey": "` + intermediaryPubkey + `" + } + }` + + var msg SwapInRequestMessage + require.NoError(t, json.Unmarshal([]byte(raw), &msg)) + require.NotNil(t, msg.TwoHop) + assert.Equal(t, intermediaryPubkey, msg.TwoHop.IntermediaryPubkey) + require.NoError(t, msg.Validate(&SwapData{})) + }) + + t.Run("swap_out_request without twohop", func(t *testing.T) { + t.Parallel() + + raw := `{ + "protocol_version": 5, + "swap_id": "` + swapID + `", + "asset": "", + "network": "regtest", + "scid": "1x2x3", + "amount": 100, + "pubkey": "` + pubkey + `", + "acceptable_premium": 0 + }` + + var msg SwapOutRequestMessage + require.NoError(t, json.Unmarshal([]byte(raw), &msg)) + assert.Nil(t, msg.TwoHop) + require.NoError(t, msg.Validate(&SwapData{})) + }) + + t.Run("swap_out_request with twohop", func(t *testing.T) { + t.Parallel() + + raw := `{ + "protocol_version": 5, + "swap_id": "` + swapID + `", + "asset": "", + "network": "regtest", + "scid": "1x2x3", + "amount": 100, + "pubkey": "` + pubkey + `", + "acceptable_premium": 0, + "twohop": { + "intermediary_pubkey": "` + intermediaryPubkey + `" + } + }` + + var msg SwapOutRequestMessage + require.NoError(t, json.Unmarshal([]byte(raw), &msg)) + require.NotNil(t, msg.TwoHop) + assert.Equal(t, intermediaryPubkey, msg.TwoHop.IntermediaryPubkey) + require.NoError(t, msg.Validate(&SwapData{})) + }) + + t.Run("swap_in_agreement with twohop.incoming_scid", func(t *testing.T) { + t.Parallel() + + raw := `{ + "protocol_version": 5, + "swap_id": "` + swapID + `", + "pubkey": "` + pubkey + `", + "premium": 10, + "twohop": { + "incoming_scid": "4x5x6" + } + }` + + var msg SwapInAgreementMessage + require.NoError(t, json.Unmarshal([]byte(raw), &msg)) + require.NotNil(t, msg.TwoHop) + assert.Equal(t, "4x5x6", msg.TwoHop.IncomingScid) + require.NoError(t, msg.Validate(&SwapData{})) + }) + + t.Run("swap_out_agreement with twohop.incoming_scid", func(t *testing.T) { + t.Parallel() + + raw := `{ + "protocol_version": 5, + "swap_id": "` + swapID + `", + "pubkey": "` + pubkey + `", + "Payreq": "lnbc1...", + "premium": 10, + "twohop": { + "incoming_scid": "4x5x6" + } + }` + + var msg SwapOutAgreementMessage + require.NoError(t, json.Unmarshal([]byte(raw), &msg)) + require.NotNil(t, msg.TwoHop) + assert.Equal(t, "4x5x6", msg.TwoHop.IncomingScid) + require.NoError(t, msg.Validate(&SwapData{})) + }) + + t.Run("marshal omits twohop when nil", func(t *testing.T) { + t.Parallel() + + msg := SwapOutRequestMessage{TwoHop: nil} + b, err := json.Marshal(msg) + require.NoError(t, err) + assert.NotContains(t, string(b), "twohop") + }) +} + +func TestPeerswapMessages_TwoHopValidation(t *testing.T) { + t.Parallel() + + swapID, err := ParseSwapIdFromString(strings.Repeat("00", 32)) + require.NoError(t, err) + + pubkey := "02" + strings.Repeat("00", 32) + + t.Run("request rejects invalid intermediary_pubkey", func(t *testing.T) { + t.Parallel() + + msg := SwapOutRequestMessage{ + ProtocolVersion: 5, + SwapId: swapID, + Asset: "", + Network: "regtest", + Scid: "1x2x3", + Amount: 1, + Pubkey: pubkey, + TwoHop: &TwoHop{ + IntermediaryPubkey: "00", + }, + } + require.Error(t, msg.Validate(&SwapData{})) + }) + + t.Run("agreement rejects invalid incoming_scid", func(t *testing.T) { + t.Parallel() + + msg := SwapOutAgreementMessage{ + ProtocolVersion: 5, + SwapId: swapID, + Pubkey: pubkey, + TwoHop: &TwoHop{ + IncomingScid: "not-a-scid", + }, + } + require.Error(t, msg.Validate(&SwapData{})) + }) +} diff --git a/swap/service.go b/swap/service.go index b0035ae7..d899600f 100644 --- a/swap/service.go +++ b/swap/service.go @@ -132,7 +132,7 @@ func (s *SwapService) RecoverSwaps() error { } swap.stateChange = sync.NewCond(&swap.stateMutex) - err := s.lockSwap(swap.SwapId.String(), swap.Data.GetScid(), swap) + err := s.lockSwap(swap.SwapId.String(), swap.Data.GetLocalScid(), swap) if err != nil { log.Infof("[%s]: error recovering swap: %v", swap.SwapId.String(), err) return @@ -375,6 +375,19 @@ func (s *SwapService) OnCsvPassed(swapId string) error { // todo move wallet and chain / channel validation logic here // SwapOut starts a new swap out process func (s *SwapService) SwapOut(peer string, chain string, channelId string, initiator string, amtSat uint64, premiumLimitRatePpm int64) (*SwapStateMachine, error) { + return s.swapOut(peer, chain, channelId, initiator, amtSat, premiumLimitRatePpm, nil) +} + +// SwapOutTwoHop starts a new swap out with a 2-hop payment route. The swap +// negotiation messages are sent to `peer`, while `channelId` denotes the local +// channel to the intermediary identified by `intermediaryPubkey`. +func (s *SwapService) SwapOutTwoHop(peer string, chain string, channelId string, initiator string, amtSat uint64, premiumLimitRatePpm int64, intermediaryPubkey string) (*SwapStateMachine, error) { + return s.swapOut(peer, chain, channelId, initiator, amtSat, premiumLimitRatePpm, &TwoHop{ + IntermediaryPubkey: intermediaryPubkey, + }) +} + +func (s *SwapService) swapOut(peer string, chain string, channelId string, initiator string, amtSat uint64, premiumLimitRatePpm int64, twoHop *TwoHop) (*SwapStateMachine, error) { if !s.swapServices.policy.NewSwapsAllowed() { return nil, fmt.Errorf("swaps are disabled") } @@ -401,6 +414,7 @@ func (s *SwapService) SwapOut(peer string, chain string, channelId string, initi } swap := newSwapOutSenderFSM(s.swapServices, initiator, peer) + swap.Data.LocalScid = channelId err = s.lockSwap(swap.SwapId.String(), channelId, swap) if err != nil { return nil, err @@ -424,6 +438,7 @@ func (s *SwapService) SwapOut(peer string, chain string, channelId string, initi Amount: amtSat, Pubkey: hex.EncodeToString(swap.Data.GetPrivkey().PubKey().SerializeCompressed()), PremiumLimit: premium.NewPPM(premiumLimitRatePpm).Compute(amtSat), + TwoHop: twoHop, } done, err := swap.SendEvent(Event_OnSwapOutStarted, request) @@ -440,6 +455,19 @@ func (s *SwapService) SwapOut(peer string, chain string, channelId string, initi // todo check prerequisites // SwapIn starts a new swap in process func (s *SwapService) SwapIn(peer string, chain string, channelId string, initiator string, amtSat uint64, premiumLimitRatePPM int64) (*SwapStateMachine, error) { + return s.swapIn(peer, chain, channelId, initiator, amtSat, premiumLimitRatePPM, nil) +} + +// SwapInTwoHop starts a new swap in with a 2-hop payment route. The swap +// negotiation messages are sent to `peer`, while `channelId` denotes the local +// channel to the intermediary identified by `intermediaryPubkey`. +func (s *SwapService) SwapInTwoHop(peer string, chain string, channelId string, initiator string, amtSat uint64, premiumLimitRatePPM int64, intermediaryPubkey string) (*SwapStateMachine, error) { + return s.swapIn(peer, chain, channelId, initiator, amtSat, premiumLimitRatePPM, &TwoHop{ + IntermediaryPubkey: intermediaryPubkey, + }) +} + +func (s *SwapService) swapIn(peer string, chain string, channelId string, initiator string, amtSat uint64, premiumLimitRatePPM int64, twoHop *TwoHop) (*SwapStateMachine, error) { if !s.swapServices.policy.NewSwapsAllowed() { return nil, fmt.Errorf("swaps are disabled") } @@ -480,6 +508,7 @@ func (s *SwapService) SwapIn(peer string, chain string, channelId string, initia return nil, errors.New("invalid chain") } swap := newSwapInSenderFSM(s.swapServices, initiator, peer) + swap.Data.LocalScid = channelId err = s.lockSwap(swap.SwapId.String(), channelId, swap) if err != nil { return nil, err @@ -494,6 +523,7 @@ func (s *SwapService) SwapIn(peer string, chain string, channelId string, initia Amount: amtSat, Pubkey: hex.EncodeToString(swap.Data.GetPrivkey().PubKey().SerializeCompressed()), PremiumLimit: premium.NewPPM(premiumLimitRatePPM).Compute(amtSat), + TwoHop: twoHop, } done, err := swap.SendEvent(Event_SwapInSender_OnSwapInRequested, request) @@ -585,7 +615,43 @@ func (s *SwapService) OnSwapInRequestReceived(swapId *SwapId, peerId string, mes return err } - sp, err := s.swapServices.lightning.SpendableMsat(message.Scid) + localScid := message.Scid + if message.TwoHop != nil { + channels, err := s.swapServices.lightning.ChannelsToPeer(message.TwoHop.IntermediaryPubkey) + if err != nil { + msg := fmt.Sprintf("from the %s peer: %s", s.swapServices.lightning.Implementation(), err.Error()) + msgBytes, msgType, err := MarshalPeerswapMessage(&CancelMessage{ + SwapId: swapId, + Message: msg, + }) + s.swapServices.messenger.SendMessage(peerId, msgBytes, msgType) + return err + } + var found bool + for _, scid := range channels { + sp, err := s.swapServices.lightning.SpendableMsat(scid) + if err != nil { + continue + } + if sp >= message.Amount*1000 { + localScid = scid + found = true + break + } + } + if !found { + err = fmt.Errorf("no suitable channel to intermediary_pubkey: %s", message.TwoHop.IntermediaryPubkey) + msg := fmt.Sprintf("from the %s peer: %s", s.swapServices.lightning.Implementation(), err.Error()) + msgBytes, msgType, err := MarshalPeerswapMessage(&CancelMessage{ + SwapId: swapId, + Message: msg, + }) + s.swapServices.messenger.SendMessage(peerId, msgBytes, msgType) + return err + } + } + + sp, err := s.swapServices.lightning.SpendableMsat(localScid) if err != nil { msg := fmt.Sprintf("from the %s peer: %s", s.swapServices.lightning.Implementation(), err.Error()) // We want to tell our peer why we can not do this swap. @@ -609,30 +675,33 @@ func (s *SwapService) OnSwapInRequestReceived(swapId *SwapId, peerId string, mes return err } - success, failureReason, err := s.swapServices.lightning.ProbePayment(message.Scid, message.Amount*1000) - if err != nil { - msg := fmt.Sprintf("from the %s peer: %s", s.swapServices.lightning.Implementation(), err.Error()) - // We want to tell our peer why we can not do this swap. - msgBytes, msgType, err := MarshalPeerswapMessage(&CancelMessage{ - SwapId: swapId, - Message: msg, - }) - s.swapServices.messenger.SendMessage(peerId, msgBytes, msgType) - return err - } - if !success { - // We want to tell our peer why we can not do this swap. - msgBytes, msgType, err := MarshalPeerswapMessage(&CancelMessage{ - SwapId: swapId, - Message: "The prepayment probe was unsuccessful." + failureReason, - }) - s.swapServices.messenger.SendMessage(peerId, msgBytes, msgType) - return err + if message.TwoHop == nil { + success, failureReason, err := s.swapServices.lightning.ProbePayment(message.Scid, message.Amount*1000) + if err != nil { + msg := fmt.Sprintf("from the %s peer: %s", s.swapServices.lightning.Implementation(), err.Error()) + // We want to tell our peer why we can not do this swap. + msgBytes, msgType, err := MarshalPeerswapMessage(&CancelMessage{ + SwapId: swapId, + Message: msg, + }) + s.swapServices.messenger.SendMessage(peerId, msgBytes, msgType) + return err + } + if !success { + // We want to tell our peer why we can not do this swap. + msgBytes, msgType, err := MarshalPeerswapMessage(&CancelMessage{ + SwapId: swapId, + Message: "The prepayment probe was unsuccessful." + failureReason, + }) + s.swapServices.messenger.SendMessage(peerId, msgBytes, msgType) + return err + } } swap := newSwapInReceiverFSM(swapId, s.swapServices, peerId) + swap.Data.LocalScid = localScid - err = s.lockSwap(swap.SwapId.String(), message.Scid, swap) + err = s.lockSwap(swap.SwapId.String(), localScid, swap) if err != nil { // If we already have an active swap on the same channel or can not lock // in a new swap we want to tell it our peer. @@ -682,7 +751,44 @@ func (s *SwapService) OnSwapOutRequestReceived(swapId *SwapId, peerId string, me s.swapServices.messenger.SendMessage(peerId, msgBytes, msgType) return err } - rs, err := s.swapServices.lightning.ReceivableMsat(message.Scid) + + localScid := message.Scid + if message.TwoHop != nil { + channels, err := s.swapServices.lightning.ChannelsToPeer(message.TwoHop.IntermediaryPubkey) + if err != nil { + msg := fmt.Sprintf("from the %s peer: %s", s.swapServices.lightning.Implementation(), err.Error()) + msgBytes, msgType, err := MarshalPeerswapMessage(&CancelMessage{ + SwapId: swapId, + Message: msg, + }) + s.swapServices.messenger.SendMessage(peerId, msgBytes, msgType) + return err + } + var found bool + for _, scid := range channels { + rs, err := s.swapServices.lightning.ReceivableMsat(scid) + if err != nil { + continue + } + if rs >= message.Amount*1000 { + localScid = scid + found = true + break + } + } + if !found { + err = fmt.Errorf("no suitable channel to intermediary_pubkey: %s", message.TwoHop.IntermediaryPubkey) + msg := fmt.Sprintf("from the %s peer: %s", s.swapServices.lightning.Implementation(), err.Error()) + msgBytes, msgType, err := MarshalPeerswapMessage(&CancelMessage{ + SwapId: swapId, + Message: msg, + }) + s.swapServices.messenger.SendMessage(peerId, msgBytes, msgType) + return err + } + } + + rs, err := s.swapServices.lightning.ReceivableMsat(localScid) if err != nil { msg := fmt.Sprintf("from the %s peer: %s", s.swapServices.lightning.Implementation(), err.Error()) // We want to tell our peer why we can not do this swap. @@ -707,7 +813,8 @@ func (s *SwapService) OnSwapOutRequestReceived(swapId *SwapId, peerId string, me } swap := newSwapOutReceiverFSM(swapId, s.swapServices, peerId) - err = s.lockSwap(swap.SwapId.String(), message.Scid, swap) + swap.Data.LocalScid = localScid + err = s.lockSwap(swap.SwapId.String(), localScid, swap) if err != nil { // If we already have an active swap on the same channel or can not lock // in a new swap we want to tell it our peer. @@ -963,7 +1070,7 @@ func (s *SwapService) lockSwap(swapId, channelId string, fsm *SwapStateMachine) // Check if we already have an active swap on the same channel for id, swap := range s.activeSwaps { - if swap.Data.GetScid() == channelId { + if swap.Data.GetLocalScid() == channelId { return ActiveSwapError{channelId: channelId, swapId: id} } } diff --git a/swap/service_test.go b/swap/service_test.go index 73a93706..3f63732a 100644 --- a/swap/service_test.go +++ b/swap/service_test.go @@ -512,3 +512,99 @@ func getRandom32ByteHexString() string { privkey, _ := btcec.NewPrivateKey() return hex.EncodeToString(privkey.Serialize()) } + +func Test_SwapService_TwoHopSelectsLocalScid_SwapOutReceiver(t *testing.T) { + t.Parallel() + + initiator, receiver, _, _, protocolScid := getTestParams() + intermediary := getRandom33ByteHexString() + + swapService := getTestSetup(t, receiver) + swapService.swapServices.messenger.(*ConnectedMessenger).other = &ConnectedMessenger{ + thisPeerId: "stub", + OnMessage: func(string, string, []byte) error { return nil }, + } + require.NoError(t, swapService.Start()) + + localScidNotEnough := "11x1x1" + localScidEnough := "22x2x2" + + lc := swapService.swapServices.lightning.(*dummyLightningClient) + lc.channelsToPeer = map[string][]string{ + intermediary: {localScidNotEnough, localScidEnough}, + } + lc.receivableMsat = map[string]uint64{ + localScidNotEnough: 0, + localScidEnough: 200_000_000, + } + + swapID := NewSwapId() + msg := &SwapOutRequestMessage{ + ProtocolVersion: PEERSWAP_PROTOCOL_VERSION, + SwapId: swapID, + Asset: "", + Network: "mainnet", + Scid: protocolScid, + Amount: 100000, + Pubkey: initiator, + PremiumLimit: 999999999, + TwoHop: &TwoHop{ + IntermediaryPubkey: intermediary, + }, + } + + require.NoError(t, swapService.OnSwapOutRequestReceived(swapID, initiator, msg)) + + fsm, err := swapService.GetActiveSwap(swapID.String()) + require.NoError(t, err) + assert.Equal(t, localScidEnough, fsm.Data.LocalScid) + assert.Equal(t, protocolScid, fsm.Data.GetScid()) +} + +func Test_SwapService_TwoHopSelectsLocalScid_SwapInReceiver(t *testing.T) { + t.Parallel() + + initiator, receiver, _, _, protocolScid := getTestParams() + intermediary := getRandom33ByteHexString() + + swapService := getTestSetup(t, receiver) + swapService.swapServices.messenger.(*ConnectedMessenger).other = &ConnectedMessenger{ + thisPeerId: "stub", + OnMessage: func(string, string, []byte) error { return nil }, + } + require.NoError(t, swapService.Start()) + + localScidNotEnough := "11x1x1" + localScidEnough := "22x2x2" + + lc := swapService.swapServices.lightning.(*dummyLightningClient) + lc.channelsToPeer = map[string][]string{ + intermediary: {localScidNotEnough, localScidEnough}, + } + lc.spendableMsat = map[string]uint64{ + localScidNotEnough: 0, + localScidEnough: 200_000_000, + } + + swapID := NewSwapId() + msg := &SwapInRequestMessage{ + ProtocolVersion: PEERSWAP_PROTOCOL_VERSION, + SwapId: swapID, + Asset: "", + Network: "mainnet", + Scid: protocolScid, + Amount: 100000, + Pubkey: initiator, + PremiumLimit: 999999999, + TwoHop: &TwoHop{ + IntermediaryPubkey: intermediary, + }, + } + + require.NoError(t, swapService.OnSwapInRequestReceived(swapID, initiator, msg)) + + fsm, err := swapService.GetActiveSwap(swapID.String()) + require.NoError(t, err) + assert.Equal(t, localScidEnough, fsm.Data.LocalScid) + assert.Equal(t, protocolScid, fsm.Data.GetScid()) +} diff --git a/swap/services.go b/swap/services.go index 236a9733..5b875f3f 100644 --- a/swap/services.go +++ b/swap/services.go @@ -45,6 +45,7 @@ type LightningClient interface { PayInvoice(payreq string) (preImage string, err error) GetPayreq(msatAmount uint64, preimage string, swapId string, memo string, invoiceType InvoiceType, expirySeconds, expiryCltv uint64) (string, error) PayInvoiceViaChannel(payreq string, channel string) (preimage string, err error) + PayInvoiceVia2HopRoute(payreq string, outgoingScid string, incomingScid string, intermediaryPubkey string) (preimage string, err error) AddPaymentCallback(f func(swapId string, invoiceType InvoiceType)) AddPaymentNotifier(swapId string, payreq string, invoiceType InvoiceType) RebalancePayment(payreq string, channel string) (preimage string, err error) @@ -52,6 +53,7 @@ type LightningClient interface { Implementation() string SpendableMsat(scid string) (uint64, error) ReceivableMsat(scid string) (uint64, error) + ChannelsToPeer(peerPubkey string) ([]string, error) ProbePayment(scid string, amountMsat uint64) (bool, string, error) } diff --git a/swap/swap.go b/swap/swap.go index dae0979d..91c28a7b 100644 --- a/swap/swap.go +++ b/swap/swap.go @@ -104,6 +104,11 @@ type SwapData struct { // cancel message CancelMessage string `json:"cancel_message"` + // LocalScid is the local channel used for this swap. For 2-hop swaps, the + // protocol Scid can refer to a channel that does not exist locally, so we + // persist the local channel scid separately. + LocalScid string `json:"local_scid,omitempty"` + PeerNodeId string `json:"peer_node_id"` InitiatorNodeId string `json:"initiator_node_id"` CreatedAt int64 `json:"created_at"` @@ -185,6 +190,13 @@ func (s *SwapData) GetScid() string { return "" } +func (s *SwapData) GetLocalScid() string { + if s.LocalScid != "" { + return s.LocalScid + } + return s.GetScid() +} + func (s *SwapData) GetScidInBoltFormat() string { if s.SwapInRequest != nil { return strings.ReplaceAll(s.SwapInRequest.Scid, ":", "x") diff --git a/swap/swap_out_sender_test.go b/swap/swap_out_sender_test.go index 7de0f656..74def869 100644 --- a/swap/swap_out_sender_test.go +++ b/swap/swap_out_sender_test.go @@ -262,6 +262,18 @@ type dummyLightningClient struct { spendableMsatCalled int receivableMsatCalled int + + channelsToPeer map[string][]string + spendableMsat map[string]uint64 + receivableMsat map[string]uint64 + + payInvoiceVia2HopRouteCalled int + payInvoiceVia2HopRouteParams struct { + payreq string + outgoingScid string + incomingScid string + intermediaryPubkey string + } } func (d *dummyLightningClient) Implementation() string { @@ -270,14 +282,31 @@ func (d *dummyLightningClient) Implementation() string { func (d *dummyLightningClient) SpendableMsat(scid string) (uint64, error) { d.spendableMsatCalled++ + if d.spendableMsat != nil { + if msat, ok := d.spendableMsat[scid]; ok { + return msat, nil + } + } return math.MaxUint64, nil } func (d *dummyLightningClient) ReceivableMsat(scid string) (uint64, error) { d.receivableMsatCalled++ + if d.receivableMsat != nil { + if msat, ok := d.receivableMsat[scid]; ok { + return msat, nil + } + } return math.MaxUint64, nil } +func (d *dummyLightningClient) ChannelsToPeer(peerPubkey string) ([]string, error) { + if d.channelsToPeer == nil { + return nil, nil + } + return d.channelsToPeer[peerPubkey], nil +} + func (d *dummyLightningClient) CanSpend(amtMsat uint64) error { d.canSpendCalled++ return d.canSpendError @@ -359,6 +388,26 @@ func (d *dummyLightningClient) PayInvoiceViaChannel(payreq, scid string) (preima return pi.String(), nil } +func (d *dummyLightningClient) PayInvoiceVia2HopRoute(payreq string, outgoingScid string, incomingScid string, intermediaryPubkey string) (preimage string, err error) { + d.payInvoiceVia2HopRouteCalled++ + d.payInvoiceVia2HopRouteParams.payreq = payreq + d.payInvoiceVia2HopRouteParams.outgoingScid = outgoingScid + d.payInvoiceVia2HopRouteParams.incomingScid = incomingScid + d.payInvoiceVia2HopRouteParams.intermediaryPubkey = intermediaryPubkey + + if d.failpayment { + return "", errors.New("payment failed") + } + if payreq == "err" { + return "", errors.New("error paying invoice") + } + pi, err := lightning.GetPreimage() + if err != nil { + return "", err + } + return pi.String(), nil +} + func (d *dummyLightningClient) ProbePayment(scid string, amountMsat uint64) (bool, string, error) { return true, "", nil } diff --git a/test/grpc_retry_test.go b/test/grpc_retry_test.go index 38431b0d..46ef9b82 100644 --- a/test/grpc_retry_test.go +++ b/test/grpc_retry_test.go @@ -45,14 +45,14 @@ func Test_GrpcRetryRequest(t *testing.T) { t.Cleanup(lnd.Kill) // Create a client connection to the lnd node. And a new lnd client. - cc, err := peerswaplndinternal.GetClientConnectionShortBackoff( - context.Background(), - &peerswaplnd.LndConfig{ - LndHost: fmt.Sprintf("localhost:%d", lnd.RpcPort), - TlsCertPath: lnd.TlsPath, - MacaroonPath: lnd.MacaroonPath, - }, - ) + cc, err := peerswaplndinternal.GetClientConnectionShortBackoff( + context.Background(), + &peerswaplnd.LndConfig{ + LndHost: fmt.Sprintf("localhost:%d", lnd.RpcPort), + TlsCertPath: lnd.TlsPath, + MacaroonPath: lnd.MacaroonPath, + }, + ) if err != nil { t.Fatalf("Could not create lnd client connection: %v", err) } @@ -116,14 +116,14 @@ func Test_GrpcReconnectStream(t *testing.T) { t.Cleanup(lnd.Kill) // Create a client connection to the lnd node. And a new lnd client. - cc, err := peerswaplndinternal.GetClientConnectionShortBackoff( - context.Background(), - &peerswaplnd.LndConfig{ - LndHost: fmt.Sprintf("localhost:%d", lnd.RpcPort), - TlsCertPath: lnd.TlsPath, - MacaroonPath: lnd.MacaroonPath, - }, - ) + cc, err := peerswaplndinternal.GetClientConnectionShortBackoff( + context.Background(), + &peerswaplnd.LndConfig{ + LndHost: fmt.Sprintf("localhost:%d", lnd.RpcPort), + TlsCertPath: lnd.TlsPath, + MacaroonPath: lnd.MacaroonPath, + }, + ) if err != nil { t.Fatalf("Could not create lnd client connection: %v", err) } diff --git a/test/swap_2hop_bitcoin_test.go b/test/swap_2hop_bitcoin_test.go new file mode 100644 index 00000000..a820dba8 --- /dev/null +++ b/test/swap_2hop_bitcoin_test.go @@ -0,0 +1,286 @@ +package test + +import ( + "context" + "encoding/hex" + "testing" + "time" + + "github.com/elementsproject/peerswap/peerswaprpc" + "github.com/elementsproject/peerswap/swap" + "github.com/elementsproject/peerswap/test/scenario" + "github.com/elementsproject/peerswap/testframework" + "github.com/lightningnetwork/lnd/lnrpc" +) + +const ( + twoHopChanAmountSat = uint64(10_000_000) + twoHopSwapAmountSat = uint64(1_000_000) +) + +func lnd3HopSetup( + t *testing.T, + channelCapacitySat uint64, + pushAmtSat uint64, +) (*testframework.BitcoinNode, *testframework.LndNode, *testframework.LndNode, *testframework.LndNode, *PeerSwapd, *PeerSwapd, string, string) { + t.Helper() + + builder := NewHarnessBuilder(t) + bitcoind := builder.Bitcoind() + + extraConfig := map[string]string{ + "protocol.wumbo-channels": "true", + // Speed up gossip propagation for multihop routes. + "trickledelay": "50", + } + + u := builder.AddLndNode(1, WithLndExtraConfig(extraConfig), WithLndFailurePrinter(printFailedFiltered)) + m := builder.AddLndNode(2, WithLndExtraConfig(extraConfig), WithLndFailurePrinter(printFailedFiltered)) + v := builder.AddLndNode(3, WithLndExtraConfig(extraConfig), WithLndFailurePrinter(printFailedFiltered)) + + uPeerswapd := builder.AddPeerSwapd(1, u, nil, WithPeerSwapdFailurePrinter(printFailedFiltered)) + // Note: Do not start peerswapd on m, to prove intermediary does not need PeerSwap. + vPeerswapd := builder.AddPeerSwapd(2, v, nil, WithPeerSwapdFailurePrinter(printFailedFiltered)) + + builder.Start() + + scidUM, err := u.OpenChannel(m, channelCapacitySat, pushAmtSat, true, true, true) + requireNoError(t, err) + + scidMV, err := m.OpenChannel(v, channelCapacitySat, pushAmtSat, true, true, true) + requireNoError(t, err) + + // Fund u/v wallets so both swap directions can run without relying on + // channel open residual wallet balances. + _, err = u.FundWallet(10*channelCapacitySat, true) + requireNoError(t, err) + _, err = v.FundWallet(10*channelCapacitySat, true) + requireNoError(t, err) + + // Connect u <-> v as peers (no channel). + err = u.Connect(v, true) + requireNoError(t, err) + + // Ensure compatibility snapshot is exchanged between u and v. + err = syncPoll( + &peerswapPollableNode{uPeerswapd, u.Id()}, + &peerswapPollableNode{vPeerswapd, v.Id()}, + ) + requireNoError(t, err) + + return bitcoind, u, m, v, uPeerswapd, vPeerswapd, scidUM, scidMV +} + +func waitForPinned2HopRoute( + t *testing.T, + payer *testframework.LndNode, + destinationPubkey string, + outgoingChanID uint64, + incomingChanID uint64, + lastHopPubkey string, +) { + t.Helper() + + lastHopBytes, err := hex.DecodeString(lastHopPubkey) + requireNoError(t, err) + + err = testframework.WaitFor(func() bool { + // Note: QueryRoutes returns an error when no route is known. We retry + // until the route graph catches up. + res, err := payer.Rpc.QueryRoutes(context.Background(), &lnrpc.QueryRoutesRequest{ + PubKey: destinationPubkey, + AmtMsat: 1000, + FinalCltvDelta: 40, + OutgoingChanId: outgoingChanID, + LastHopPubkey: lastHopBytes, + }) + if err != nil { + return false + } + for _, r := range res.Routes { + if len(r.Hops) != 2 { + continue + } + if r.Hops[0].ChanId != outgoingChanID { + continue + } + if r.Hops[1].ChanId != incomingChanID { + continue + } + return true + } + return false + }, testframework.TIMEOUT) + requireNoError(t, err, "did not find pinned 2-hop route (dest=%s outgoing=%d incoming=%d last_hop=%s)", destinationPubkey, outgoingChanID, incomingChanID, lastHopPubkey) +} + +func startLnd2HopSwap(t *testing.T, requester *PeerSwapd, channelID uint64, peerPubkey string, swapAmt uint64, swapType swap.SwapType) { + t.Helper() + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + asset := "btc" + + switch swapType { + case swap.SWAPTYPE_IN: + go func() { + _, _ = requester.PeerswapClient.SwapIn(ctx, &peerswaprpc.SwapInRequest{ + ChannelId: channelID, + SwapAmount: swapAmt, + Asset: asset, + PeerPubkey: peerPubkey, + PremiumLimitRatePpm: 100000, + }) + }() + case swap.SWAPTYPE_OUT: + go func() { + _, _ = requester.PeerswapClient.SwapOut(ctx, &peerswaprpc.SwapOutRequest{ + ChannelId: channelID, + SwapAmount: swapAmt, + Asset: asset, + PeerPubkey: peerPubkey, + PremiumLimitRatePpm: 100000, + }) + }() + default: + t.Fatalf("unknown swap type %v", swapType) + } +} + +func await2HopPreimageClaim( + t *testing.T, + bitcoind *testframework.BitcoinNode, + u, m, v *testframework.LndNode, + uPeerswapd, vPeerswapd *PeerSwapd, + swapType swap.SwapType, +) { + t.Helper() + + require := requireNew(t) + + logs := scenario.NewLogBook() + + // Taker waits for the opening tx confirmation and later claims. + awaitOpening := scenario.LogExpectation{ + Action: "await-opening-confirmation", + Message: "Await confirmation for tx", + Timeout: testframework.TIMEOUT, + } + claimLog := scenario.LogExpectation{ + Action: "claim-success", + Timeout: testframework.TIMEOUT, + } + + // Maker logs when the claim invoice is paid. + invoiceLog := scenario.LogExpectation{ + Action: "invoice-paid", + Timeout: testframework.TIMEOUT, + } + + switch swapType { + case swap.SWAPTYPE_OUT: + awaitOpening.Waiter = uPeerswapd.DaemonProcess + invoiceLog.Waiter = vPeerswapd.DaemonProcess + invoiceLog.Message = "Event_OnClaimInvoicePaid on State_SwapOutReceiver_AwaitClaimInvoicePayment" + claimLog.Waiter = uPeerswapd.DaemonProcess + claimLog.Message = "Event_ActionSucceeded on State_SwapOutSender_ClaimSwap" + case swap.SWAPTYPE_IN: + awaitOpening.Waiter = vPeerswapd.DaemonProcess + invoiceLog.Waiter = uPeerswapd.DaemonProcess + invoiceLog.Message = "Event_OnClaimInvoicePaid on State_SwapInSender_AwaitClaimPayment" + claimLog.Waiter = vPeerswapd.DaemonProcess + claimLog.Message = "Event_ActionSucceeded on State_SwapInReceiver_ClaimSwap" + default: + t.Fatalf("unknown swap type %v", swapType) + } + + logs.Register(awaitOpening) + logs.Register(invoiceLog) + logs.Register(claimLog) + + // Wait for opening tx, then confirm it. + require.NoError(logs.Await("await-opening-confirmation")) + requireNoError(t, bitcoind.GenerateBlocks(BitcoinConfirms)) + waitForBlockheightSync(t, testframework.TIMEOUT, u, m, v) + + // Wait for claim invoice to be paid, then for claim tx broadcast. + require.NoError(logs.Await("invoice-paid")) + _, err := waitForTxInMempool(t, bitcoind.RpcProxy, testframework.TIMEOUT) + require.NoError(err) + + // Confirm claim tx. + requireNoError(t, bitcoind.GenerateBlocks(BitcoinConfirms)) + waitForBlockheightSync(t, testframework.TIMEOUT, u, m, v) + + // Swap done. + require.NoError(logs.Await("claim-success")) +} + +func Test_2Hop_Bitcoin_LND(t *testing.T) { + IsIntegrationTest(t) + + bitcoind, u, m, v, uPeerswapd, vPeerswapd, scidUM, scidMV := lnd3HopSetup( + t, + twoHopChanAmountSat, + twoHopChanAmountSat/2, + ) + DumpOnFailure(t, WithBitcoin(bitcoind), WithLnds([]*testframework.LndNode{u, m, v}), WithPeerSwapds(uPeerswapd, vPeerswapd)) + + // Ensure u and v are peers but do not share a channel. + _, err := u.GetScid(v) + assertError(t, err, "expected no direct channel between u and v") + + channelIDUM, err := u.ChanIdFromScid(scidUM) + requireNoError(t, err) + channelIDMV, err := m.ChanIdFromScid(scidMV) + requireNoError(t, err) + + t.Run("swapout", func(t *testing.T) { + startBalance, err := u.GetChannelBalanceSat(scidUM) + requireNoError(t, err) + + // Ensure the route graph knows u -> m -> v. + waitForPinned2HopRoute(t, u, v.Id(), channelIDUM, channelIDMV, m.Id()) + + startLnd2HopSwap(t, uPeerswapd, channelIDUM, v.Id(), twoHopSwapAmountSat, swap.SWAPTYPE_OUT) + await2HopPreimageClaim(t, bitcoind, u, m, v, uPeerswapd, vPeerswapd, swap.SWAPTYPE_OUT) + + var endBalance uint64 + err = testframework.WaitFor(func() bool { + endBalance, err = u.GetChannelBalanceSat(scidUM) + requireNoError(t, err) + return endBalance < startBalance + }, testframework.TIMEOUT) + requireNoError(t, err) + t.Logf("u-m local balance changed: before=%d after=%d (swap-out)", startBalance, endBalance) + }) + + t.Run("swapin", func(t *testing.T) { + startBalance, err := u.GetChannelBalanceSat(scidUM) + requireNoError(t, err) + + channelIDVM, err := v.ChanIdFromScid(scidMV) + requireNoError(t, err) + channelIDMU, err := m.ChanIdFromScid(scidUM) + requireNoError(t, err) + + // Ensure the route graph knows v -> m -> u. + waitForPinned2HopRoute(t, v, u.Id(), channelIDVM, channelIDMU, m.Id()) + + startLnd2HopSwap(t, uPeerswapd, channelIDUM, v.Id(), twoHopSwapAmountSat, swap.SWAPTYPE_IN) + await2HopPreimageClaim(t, bitcoind, u, m, v, uPeerswapd, vPeerswapd, swap.SWAPTYPE_IN) + + var endBalance uint64 + err = testframework.WaitFor(func() bool { + endBalance, err = u.GetChannelBalanceSat(scidUM) + requireNoError(t, err) + return endBalance > startBalance + }, testframework.TIMEOUT) + requireNoError(t, err) + t.Logf("u-m local balance changed: before=%d after=%d (swap-in)", startBalance, endBalance) + }) + + // Give LND some time to settle pending HTLCs before cleanup tears down nodes. + time.Sleep(1 * time.Second) +} diff --git a/test/swap_matrix_test.go b/test/swap_matrix_test.go index 22da1241..504acde5 100644 --- a/test/swap_matrix_test.go +++ b/test/swap_matrix_test.go @@ -9,9 +9,9 @@ import ( ) func Test_SwapInMatrix(t *testing.T) { - t.Helper() - // Run this top-level matrix in parallel with others to reduce CI wall time. - t.Parallel() + t.Helper() + // Run this top-level matrix in parallel with others to reduce CI wall time. + t.Parallel() cases := []struct { name string @@ -343,9 +343,9 @@ func runLwkLndLndSwapIn(t *testing.T) { } func Test_SwapOutMatrix(t *testing.T) { - t.Helper() - // Run this top-level matrix in parallel with others to reduce CI wall time. - t.Parallel() + t.Helper() + // Run this top-level matrix in parallel with others to reduce CI wall time. + t.Parallel() cases := []struct { name string From 5263a366197bac50910bc8e292b2856ca8bfda36 Mon Sep 17 00:00:00 2001 From: Yusuke Shimizu Date: Fri, 23 Jan 2026 10:27:24 +0900 Subject: [PATCH 2/7] ci: run Test_2Hop_Bitcoin_LND in bitcoin_lndlnd matrix --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 9716f0fa..224976e8 100644 --- a/Makefile +++ b/Makefile @@ -177,7 +177,7 @@ test-matrix-bitcoin_mixed: test-bins .PHONY: test-matrix-bitcoin_lndlnd test-matrix-bitcoin_lndlnd: test-bins - ${INTEGRATION_TEST_ENV} go test ${INTEGRATION_TEST_OPTS} -run '^Test_Swap(In|Out)Matrix/(bitcoin_lndlnd).*$$' ./test + ${INTEGRATION_TEST_ENV} go test ${INTEGRATION_TEST_OPTS} -run '^(Test_Swap(In|Out)Matrix/(bitcoin_lndlnd).*|Test_2Hop_Bitcoin_LND.*)$$' ./test .PHONY: test-matrix-liquid_clncln test-matrix-liquid_clncln: test-bins From 98ca5ccf9be5d507eb1fcf6632b2ab82546d62b6 Mon Sep 17 00:00:00 2001 From: Yusuke Shimizu Date: Fri, 23 Jan 2026 10:41:46 +0900 Subject: [PATCH 3/7] test: mine extra blocks so 2-hop route is announced --- test/swap_2hop_bitcoin_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/swap_2hop_bitcoin_test.go b/test/swap_2hop_bitcoin_test.go index a820dba8..5216673b 100644 --- a/test/swap_2hop_bitcoin_test.go +++ b/test/swap_2hop_bitcoin_test.go @@ -50,6 +50,13 @@ func lnd3HopSetup( scidMV, err := m.OpenChannel(v, channelCapacitySat, pushAmtSat, true, true, true) requireNoError(t, err) + // LND may require additional confirmations before a channel is announced to + // the public graph. Since we open two channels sequentially, the second + // channel can otherwise remain unannounced long enough for QueryRoutes to + // not find the required 2-hop route on slow CI machines. + requireNoError(t, bitcoind.GenerateBlocks(BitcoinConfirms)) + waitForBlockheightSync(t, testframework.TIMEOUT, u, m, v) + // Fund u/v wallets so both swap directions can run without relying on // channel open residual wallet balances. _, err = u.FundWallet(10*channelCapacitySat, true) From 681709224867958e808d50e7d2379ae3a732188e Mon Sep 17 00:00:00 2001 From: Yusuke Shimizu Date: Fri, 23 Jan 2026 12:04:29 +0900 Subject: [PATCH 4/7] test: strengthen 2-hop preimage claim assertions --- test/swap_2hop_bitcoin_test.go | 267 +++++++++++++++++++++++++++++---- 1 file changed, 234 insertions(+), 33 deletions(-) diff --git a/test/swap_2hop_bitcoin_test.go b/test/swap_2hop_bitcoin_test.go index 5216673b..92227a45 100644 --- a/test/swap_2hop_bitcoin_test.go +++ b/test/swap_2hop_bitcoin_test.go @@ -3,9 +3,12 @@ package test import ( "context" "encoding/hex" + "fmt" + "strings" "testing" "time" + "github.com/elementsproject/peerswap/premium" "github.com/elementsproject/peerswap/peerswaprpc" "github.com/elementsproject/peerswap/swap" "github.com/elementsproject/peerswap/test/scenario" @@ -155,20 +158,104 @@ func startLnd2HopSwap(t *testing.T, requester *PeerSwapd, channelID uint64, peer } } -func await2HopPreimageClaim( +func waitForLndInvoiceByMemoContains( + t *testing.T, + node *testframework.LndNode, + memoSubstring string, +) *lnrpc.Invoice { + t.Helper() + + require := requireNew(t) + + var invoice *lnrpc.Invoice + err := testframework.WaitFor(func() bool { + resp, err := node.Rpc.ListInvoices(context.Background(), &lnrpc.ListInvoiceRequest{}) + require.NoError(err) + + var newest *lnrpc.Invoice + for _, candidate := range resp.Invoices { + if !strings.Contains(candidate.Memo, memoSubstring) { + continue + } + if candidate.PaymentRequest == "" { + continue + } + if newest == nil || candidate.AddIndex > newest.AddIndex { + newest = candidate + } + } + invoice = newest + return invoice != nil + }, testframework.TIMEOUT) + require.NoError(err) + return invoice +} + +func waitForLndPaymentByPayreq( + t *testing.T, + node *testframework.LndNode, + payreq string, +) *lnrpc.Payment { + t.Helper() + + require := requireNew(t) + + var payment *lnrpc.Payment + err := testframework.WaitFor(func() bool { + resp, err := node.Rpc.ListPayments(context.Background(), &lnrpc.ListPaymentsRequest{}) + require.NoError(err) + + for _, candidate := range resp.Payments { + if candidate.PaymentRequest != payreq { + continue + } + payment = candidate + return payment.Status == lnrpc.Payment_SUCCEEDED + } + return false + }, testframework.TIMEOUT) + require.NoError(err) + return payment +} + +func preimageClaim2HopTest( t *testing.T, bitcoind *testframework.BitcoinNode, u, m, v *testframework.LndNode, uPeerswapd, vPeerswapd *PeerSwapd, + scidUM, scidMV string, + swapAmt uint64, swapType swap.SwapType, ) { t.Helper() require := requireNew(t) + const asset = "btc" + expectedClaimMemoPrefix := fmt.Sprintf("peerswap %s claim %s", asset, scidUM) + expectedFeeMemoPrefix := fmt.Sprintf("peerswap %s fee %s", asset, scidUM) + parseSwapID := func(memo string) string { + t.Helper() + fields := strings.Fields(memo) + if len(fields) < 5 { + t.Fatalf("unexpected peerswap memo format: %q", memo) + } + return fields[len(fields)-1] + } + + // Snapshot balances up-front (preimageClaimTest parity). + origWalletU, err := u.GetBtcBalanceSat() + require.NoError(err) + origWalletV, err := v.GetBtcBalanceSat() + require.NoError(err) + + origChanUM, err := u.GetChannelBalanceSat(scidUM) + require.NoError(err) + origChanMV, err := v.GetChannelBalanceSat(scidMV) + require.NoError(err) + logs := scenario.NewLogBook() - // Taker waits for the opening tx confirmation and later claims. awaitOpening := scenario.LogExpectation{ Action: "await-opening-confirmation", Message: "Await confirmation for tx", @@ -178,26 +265,40 @@ func await2HopPreimageClaim( Action: "claim-success", Timeout: testframework.TIMEOUT, } - - // Maker logs when the claim invoice is paid. invoiceLog := scenario.LogExpectation{ Action: "invoice-paid", Timeout: testframework.TIMEOUT, } + var claimInvoiceCreator *testframework.LndNode + var claimInvoicePayer *testframework.LndNode + var feeInvoiceCreator *testframework.LndNode + var feeInvoicePayer *testframework.LndNode + switch swapType { case swap.SWAPTYPE_OUT: + // u initiates swap-out with v; v creates opening tx + claim invoice; u claims on-chain. awaitOpening.Waiter = uPeerswapd.DaemonProcess invoiceLog.Waiter = vPeerswapd.DaemonProcess invoiceLog.Message = "Event_OnClaimInvoicePaid on State_SwapOutReceiver_AwaitClaimInvoicePayment" claimLog.Waiter = uPeerswapd.DaemonProcess claimLog.Message = "Event_ActionSucceeded on State_SwapOutSender_ClaimSwap" + + claimInvoiceCreator = v + claimInvoicePayer = u + + feeInvoiceCreator = v + feeInvoicePayer = u case swap.SWAPTYPE_IN: + // u initiates swap-in with v; u creates opening tx + claim invoice; v claims on-chain. awaitOpening.Waiter = vPeerswapd.DaemonProcess invoiceLog.Waiter = uPeerswapd.DaemonProcess invoiceLog.Message = "Event_OnClaimInvoicePaid on State_SwapInSender_AwaitClaimPayment" claimLog.Waiter = vPeerswapd.DaemonProcess claimLog.Message = "Event_ActionSucceeded on State_SwapInReceiver_ClaimSwap" + + claimInvoiceCreator = u + claimInvoicePayer = v default: t.Fatalf("unknown swap type %v", swapType) } @@ -206,14 +307,103 @@ func await2HopPreimageClaim( logs.Register(invoiceLog) logs.Register(claimLog) - // Wait for opening tx, then confirm it. + // swap-out pays an initial fee invoice before the opening tx is broadcast. + var feePayment *lnrpc.Payment + if swapType == swap.SWAPTYPE_OUT { + feeInvoice := waitForLndInvoiceByMemoContains(t, feeInvoiceCreator, expectedFeeMemoPrefix) + feePayment = waitForLndPaymentByPayreq(t, feeInvoicePayer, feeInvoice.PaymentRequest) + var invoiceValueSat int64 + if feeInvoice.ValueMsat != 0 { + invoiceValueSat = feeInvoice.ValueMsat / 1000 + } else { + invoiceValueSat = feeInvoice.Value + } + require.Equal(invoiceValueSat, feePayment.ValueSat) + + awaitOpening.Message = fmt.Sprintf("Await confirmation for tx with id: .* on swap %s", parseSwapID(feeInvoice.Memo)) + logs.Register(awaitOpening) + } + + // Wait for opening tx to hit the mempool and capture its fee (commitFee). + commitFee, err := waitForTxInMempool(t, bitcoind.RpcProxy, testframework.TIMEOUT) + require.NoError(err) + + // For swap-in, we can make the await-opening log expectation swap-specific by + // parsing the swap id from the claim invoice memo (created with the opening tx). + if swapType == swap.SWAPTYPE_IN { + claimInvoice := waitForLndInvoiceByMemoContains(t, claimInvoiceCreator, expectedClaimMemoPrefix) + awaitOpening.Message = fmt.Sprintf("Await confirmation for tx with id: .* on swap %s", parseSwapID(claimInvoice.Memo)) + logs.Register(awaitOpening) + } + + // Confirm opening tx. require.NoError(logs.Await("await-opening-confirmation")) requireNoError(t, bitcoind.GenerateBlocks(BitcoinConfirms)) waitForBlockheightSync(t, testframework.TIMEOUT, u, m, v) - // Wait for claim invoice to be paid, then for claim tx broadcast. + // Wait for claim invoice to be paid. require.NoError(logs.Await("invoice-paid")) - _, err := waitForTxInMempool(t, bitcoind.RpcProxy, testframework.TIMEOUT) + + claimInvoice := waitForLndInvoiceByMemoContains(t, claimInvoiceCreator, expectedClaimMemoPrefix) + claimPayment := waitForLndPaymentByPayreq(t, claimInvoicePayer, claimInvoice.PaymentRequest) + + // Channel balances should match once the invoice is paid (preimageClaimTest parity). + switch swapType { + case swap.SWAPTYPE_OUT: + swapOutPremium := premium.NewPPM(premium.DefaultBTCSwapOutPremiumRatePPM).Compute(swapAmt) + if swapOutPremium < 0 { + t.Fatalf("unexpected negative swap-out premium: %d", swapOutPremium) + } + expectedClaimInvoiceAmt := int64(swapAmt) + swapOutPremium + require.Equal(expectedClaimInvoiceAmt, claimPayment.ValueSat) + + totalInvoiceValue := claimPayment.ValueSat + totalRoutingFee := claimPayment.FeeSat + if feePayment != nil { + totalInvoiceValue += feePayment.ValueSat + totalRoutingFee += feePayment.FeeSat + } + require.True(totalInvoiceValue > 0) + + require.True(testframework.AssertWaitForChannelBalance( + t, + u, + scidUM, + float64(int64(origChanUM)-totalInvoiceValue-totalRoutingFee), + 1., + testframework.TIMEOUT, + )) + require.True(testframework.AssertWaitForChannelBalance( + t, + v, + scidMV, + float64(int64(origChanMV)+totalInvoiceValue), + 1., + testframework.TIMEOUT, + )) + case swap.SWAPTYPE_IN: + require.Equal(int64(swapAmt), claimPayment.ValueSat) + + require.True(testframework.AssertWaitForChannelBalance( + t, + u, + scidUM, + float64(int64(origChanUM)+claimPayment.ValueSat), + 1., + testframework.TIMEOUT, + )) + require.True(testframework.AssertWaitForChannelBalance( + t, + v, + scidMV, + float64(int64(origChanMV)-claimPayment.ValueSat-claimPayment.FeeSat), + 1., + testframework.TIMEOUT, + )) + } + + // Wait for claim tx being broadcasted and capture its fee (claimFee). + claimFee, err := waitForTxInMempool(t, bitcoind.RpcProxy, testframework.TIMEOUT) require.NoError(err) // Confirm claim tx. @@ -222,6 +412,41 @@ func await2HopPreimageClaim( // Swap done. require.NoError(logs.Await("claim-success")) + + // Wallet balance assertions (preimageClaimTest parity). + switch swapType { + case swap.SWAPTYPE_OUT: + // u receives swapAmt on-chain and pays the claim tx fee. + testframework.AssertOnchainBalanceInDelta(t, + u, origWalletU+swapAmt-claimFee, 1, time.Second*10) + // v funds the opening tx output and pays the opening tx fee. + testframework.AssertOnchainBalanceInDelta(t, + v, origWalletV-swapAmt-commitFee, 1, time.Second*10) + case swap.SWAPTYPE_IN: + swapInPremium := premium.NewPPM(premium.DefaultBTCSwapInPremiumRatePPM).Compute(swapAmt) + if swapInPremium < 0 { + t.Fatalf("unexpected negative swap-in premium: %d", swapInPremium) + } + + onchainAmt := swapAmt + uint64(swapInPremium) + // u funds the opening tx output and pays the opening tx fee. + testframework.AssertOnchainBalanceInDelta(t, + u, origWalletU-onchainAmt-commitFee, 1, time.Second*10) + // v receives on-chain and pays the claim tx fee. + testframework.AssertOnchainBalanceInDelta(t, + v, origWalletV+onchainAmt-claimFee, 1, time.Second*10) + } + + // Check latest claim invoice memo and payer payreq (preimageClaimTest parity). + require.True( + strings.Contains(claimInvoice.Memo, expectedClaimMemoPrefix), + "expected memo to contain: %s, got: %s", + expectedClaimMemoPrefix, + claimInvoice.Memo, + ) + latestPayreq, err := claimInvoicePayer.GetLatestPayReqOfPayment() + require.NoError(err) + require.Equal(claimInvoice.PaymentRequest, latestPayreq) } func Test_2Hop_Bitcoin_LND(t *testing.T) { @@ -244,29 +469,14 @@ func Test_2Hop_Bitcoin_LND(t *testing.T) { requireNoError(t, err) t.Run("swapout", func(t *testing.T) { - startBalance, err := u.GetChannelBalanceSat(scidUM) - requireNoError(t, err) - // Ensure the route graph knows u -> m -> v. waitForPinned2HopRoute(t, u, v.Id(), channelIDUM, channelIDMV, m.Id()) startLnd2HopSwap(t, uPeerswapd, channelIDUM, v.Id(), twoHopSwapAmountSat, swap.SWAPTYPE_OUT) - await2HopPreimageClaim(t, bitcoind, u, m, v, uPeerswapd, vPeerswapd, swap.SWAPTYPE_OUT) - - var endBalance uint64 - err = testframework.WaitFor(func() bool { - endBalance, err = u.GetChannelBalanceSat(scidUM) - requireNoError(t, err) - return endBalance < startBalance - }, testframework.TIMEOUT) - requireNoError(t, err) - t.Logf("u-m local balance changed: before=%d after=%d (swap-out)", startBalance, endBalance) + preimageClaim2HopTest(t, bitcoind, u, m, v, uPeerswapd, vPeerswapd, scidUM, scidMV, twoHopSwapAmountSat, swap.SWAPTYPE_OUT) }) t.Run("swapin", func(t *testing.T) { - startBalance, err := u.GetChannelBalanceSat(scidUM) - requireNoError(t, err) - channelIDVM, err := v.ChanIdFromScid(scidMV) requireNoError(t, err) channelIDMU, err := m.ChanIdFromScid(scidUM) @@ -276,16 +486,7 @@ func Test_2Hop_Bitcoin_LND(t *testing.T) { waitForPinned2HopRoute(t, v, u.Id(), channelIDVM, channelIDMU, m.Id()) startLnd2HopSwap(t, uPeerswapd, channelIDUM, v.Id(), twoHopSwapAmountSat, swap.SWAPTYPE_IN) - await2HopPreimageClaim(t, bitcoind, u, m, v, uPeerswapd, vPeerswapd, swap.SWAPTYPE_IN) - - var endBalance uint64 - err = testframework.WaitFor(func() bool { - endBalance, err = u.GetChannelBalanceSat(scidUM) - requireNoError(t, err) - return endBalance > startBalance - }, testframework.TIMEOUT) - requireNoError(t, err) - t.Logf("u-m local balance changed: before=%d after=%d (swap-in)", startBalance, endBalance) + preimageClaim2HopTest(t, bitcoind, u, m, v, uPeerswapd, vPeerswapd, scidUM, scidMV, twoHopSwapAmountSat, swap.SWAPTYPE_IN) }) // Give LND some time to settle pending HTLCs before cleanup tears down nodes. From 56f5054f88ab947df11d4534f1bfdafecceda4cc Mon Sep 17 00:00:00 2001 From: Yusuke Shimizu Date: Fri, 23 Jan 2026 12:25:02 +0900 Subject: [PATCH 5/7] test: match 2-hop payments by hash --- test/swap_2hop_bitcoin_test.go | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/test/swap_2hop_bitcoin_test.go b/test/swap_2hop_bitcoin_test.go index 92227a45..820575ec 100644 --- a/test/swap_2hop_bitcoin_test.go +++ b/test/swap_2hop_bitcoin_test.go @@ -200,17 +200,28 @@ func waitForLndPaymentByPayreq( require := requireNew(t) + decoded, err := node.Rpc.DecodePayReq(context.Background(), &lnrpc.PayReqString{PayReq: payreq}) + require.NoError(err) + paymentHash := decoded.GetPaymentHash() + expectedValueSat := decoded.GetNumSatoshis() + var payment *lnrpc.Payment - err := testframework.WaitFor(func() bool { + err = testframework.WaitFor(func() bool { resp, err := node.Rpc.ListPayments(context.Background(), &lnrpc.ListPaymentsRequest{}) require.NoError(err) for _, candidate := range resp.Payments { - if candidate.PaymentRequest != payreq { + // 2-hop payments use SendToRouteV2, which does not reliably persist + // the original payment request. Matching on payment hash works for + // both SendPaymentV2 and SendToRouteV2. + if candidate.PaymentHash != paymentHash { continue } payment = candidate - return payment.Status == lnrpc.Payment_SUCCEEDED + if payment.Status != lnrpc.Payment_SUCCEEDED { + return false + } + return payment.ValueSat == expectedValueSat } return false }, testframework.TIMEOUT) @@ -444,9 +455,17 @@ func preimageClaim2HopTest( expectedClaimMemoPrefix, claimInvoice.Memo, ) - latestPayreq, err := claimInvoicePayer.GetLatestPayReqOfPayment() + // `SendToRouteV2` payments do not always retain the original payment request, + // so assert via payment hash instead of comparing payreq strings. + decodedClaimInvoice, err := claimInvoicePayer.Rpc.DecodePayReq(context.Background(), &lnrpc.PayReqString{PayReq: claimInvoice.PaymentRequest}) + require.NoError(err) + require.Equal(decodedClaimInvoice.GetPaymentHash(), claimPayment.PaymentHash) + + latestPayments, err := claimInvoicePayer.Rpc.ListPayments(context.Background(), &lnrpc.ListPaymentsRequest{}) require.NoError(err) - require.Equal(claimInvoice.PaymentRequest, latestPayreq) + require.True(latestPayments.Payments != nil) + require.True(len(latestPayments.Payments) > 0) + require.Equal(claimPayment.PaymentHash, latestPayments.Payments[len(latestPayments.Payments)-1].PaymentHash) } func Test_2Hop_Bitcoin_LND(t *testing.T) { From 7a26772098655c642b4fb96c843e31fb49be8da5 Mon Sep 17 00:00:00 2001 From: Yusuke Shimizu Date: Fri, 23 Jan 2026 12:25:08 +0900 Subject: [PATCH 6/7] lnd: wait for channel active after restart --- lnd/paymentwatcher_test.go | 52 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/lnd/paymentwatcher_test.go b/lnd/paymentwatcher_test.go index 556d0c98..ab0e3c00 100644 --- a/lnd/paymentwatcher_test.go +++ b/lnd/paymentwatcher_test.go @@ -96,7 +96,8 @@ func TestPaymentWatcher_WatchPayment_Reconnect(t *testing.T) { t.Fatalf("Could not create tx watcher: %v", err) } - payreq, err := payee.AddInvoice(100000, "testpaymentwatcher_payinvoice", "") + invoiceAmtSat := uint64(100000) + payreq, err := payee.AddInvoice(invoiceAmtSat, "testpaymentwatcher_payinvoice", "") if err != nil { t.Fatalf("Could not add invoice: %v", err) } @@ -127,6 +128,29 @@ func TestPaymentWatcher_WatchPayment_Reconnect(t *testing.T) { // We have to reconnect in order to pay the invoice. payer.Connect(payee, true) + // Ensure the channel is fully re-established before paying. + scid, err := payer.GetScid(payee) + if err != nil { + t.Fatalf("GetScid() %v", err) + } + err = testframework.WaitForWithErr(func() (bool, error) { + active, err := payer.IsChannelActive(scid) + return active, err + }, testframework.TIMEOUT) + if err != nil { + t.Fatalf("channel did not become active: %v", err) + } + err = testframework.WaitForWithErr(func() (bool, error) { + bal, err := payer.GetChannelBalanceSat(scid) + if err != nil { + return false, err + } + return bal >= invoiceAmtSat, nil + }, testframework.TIMEOUT) + if err != nil { + t.Fatalf("payer has insufficient channel balance: %v", err) + } + // Now we pay the invoice and check if the watcher is still active. If the // watcher is still active, the callback should be called. err = payer.PayInvoice(payreq) @@ -165,7 +189,8 @@ func TestPaymentWatcher_WatchPayment_Reconnect_OnGracefulStop(t *testing.T) { t.Fatalf("Could not create tx watcher: %v", err) } - payreq, err := payee.AddInvoice(100000, "testpaymentwatcher_payinvoice", "") + invoiceAmtSat := uint64(100000) + payreq, err := payee.AddInvoice(invoiceAmtSat, "testpaymentwatcher_payinvoice", "") if err != nil { t.Fatalf("Could not add invoice: %v", err) } @@ -199,6 +224,29 @@ func TestPaymentWatcher_WatchPayment_Reconnect_OnGracefulStop(t *testing.T) { // We have to reconnect in order to pay the invoice. payer.Connect(payee, true) + // Ensure the channel is fully re-established before paying. + scid, err := payer.GetScid(payee) + if err != nil { + t.Fatalf("GetScid() %v", err) + } + err = testframework.WaitForWithErr(func() (bool, error) { + active, err := payer.IsChannelActive(scid) + return active, err + }, testframework.TIMEOUT) + if err != nil { + t.Fatalf("channel did not become active: %v", err) + } + err = testframework.WaitForWithErr(func() (bool, error) { + bal, err := payer.GetChannelBalanceSat(scid) + if err != nil { + return false, err + } + return bal >= invoiceAmtSat, nil + }, testframework.TIMEOUT) + if err != nil { + t.Fatalf("payer has insufficient channel balance: %v", err) + } + // Now we pay the invoice and check if the watcher is still active. If the // watcher is still active, the callback should be called. err = payer.PayInvoice(payreq) From 4e8525550ad0dfb7f2c8a40040caf3acb612b2e7 Mon Sep 17 00:00:00 2001 From: Yusuke Shimizu Date: Fri, 23 Jan 2026 15:50:14 +0900 Subject: [PATCH 7/7] docs: Update protocol version to 6 - Increment protocol version in documentation and tests - Ensure consistency across related test cases Signed-off-by: Yusuke Shimizu --- docs/peer-protocol.md | 2 +- swap/messages_test.go | 18 ++++++++++-------- swap/service.go | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/peer-protocol.md b/docs/peer-protocol.md index 7d48ebcc..7304e64a 100644 --- a/docs/peer-protocol.md +++ b/docs/peer-protocol.md @@ -49,7 +49,7 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S - [The `claim_by_csv` path](#the-claim_by_csv-path) ## General -The `protocol_version` is included to allow for possible changes in the future. The `protocol_version` of this document is `5`. +The `protocol_version` is included to allow for possible changes in the future. The `protocol_version` of this document is `6`. PeerSwap utilizes custom messages as described in [BOLT#1](https://github.com/Lightning/bolts/blob/master/01-messaging.md). The types are in range `42069`-`42085`. The `payload` is JSON encoded. diff --git a/swap/messages_test.go b/swap/messages_test.go index 88d49f84..f8d78f06 100644 --- a/swap/messages_test.go +++ b/swap/messages_test.go @@ -2,6 +2,7 @@ package swap import ( "encoding/json" + "strconv" "strings" "testing" @@ -12,6 +13,7 @@ import ( func TestPeerswapMessages_TwoHopJSONIsOptional(t *testing.T) { t.Parallel() + protocolVersion := strconv.Itoa(PEERSWAP_PROTOCOL_VERSION) swapID := strings.Repeat("00", 32) pubkey := "02" + strings.Repeat("00", 32) intermediaryPubkey := "03" + strings.Repeat("11", 32) @@ -20,7 +22,7 @@ func TestPeerswapMessages_TwoHopJSONIsOptional(t *testing.T) { t.Parallel() raw := `{ - "protocol_version": 5, + "protocol_version": ` + protocolVersion + `, "swap_id": "` + swapID + `", "asset": "", "network": "regtest", @@ -40,7 +42,7 @@ func TestPeerswapMessages_TwoHopJSONIsOptional(t *testing.T) { t.Parallel() raw := `{ - "protocol_version": 5, + "protocol_version": ` + protocolVersion + `, "swap_id": "` + swapID + `", "asset": "", "network": "regtest", @@ -64,7 +66,7 @@ func TestPeerswapMessages_TwoHopJSONIsOptional(t *testing.T) { t.Parallel() raw := `{ - "protocol_version": 5, + "protocol_version": ` + protocolVersion + `, "swap_id": "` + swapID + `", "asset": "", "network": "regtest", @@ -84,7 +86,7 @@ func TestPeerswapMessages_TwoHopJSONIsOptional(t *testing.T) { t.Parallel() raw := `{ - "protocol_version": 5, + "protocol_version": ` + protocolVersion + `, "swap_id": "` + swapID + `", "asset": "", "network": "regtest", @@ -108,7 +110,7 @@ func TestPeerswapMessages_TwoHopJSONIsOptional(t *testing.T) { t.Parallel() raw := `{ - "protocol_version": 5, + "protocol_version": ` + protocolVersion + `, "swap_id": "` + swapID + `", "pubkey": "` + pubkey + `", "premium": 10, @@ -128,7 +130,7 @@ func TestPeerswapMessages_TwoHopJSONIsOptional(t *testing.T) { t.Parallel() raw := `{ - "protocol_version": 5, + "protocol_version": ` + protocolVersion + `, "swap_id": "` + swapID + `", "pubkey": "` + pubkey + `", "Payreq": "lnbc1...", @@ -167,7 +169,7 @@ func TestPeerswapMessages_TwoHopValidation(t *testing.T) { t.Parallel() msg := SwapOutRequestMessage{ - ProtocolVersion: 5, + ProtocolVersion: PEERSWAP_PROTOCOL_VERSION, SwapId: swapID, Asset: "", Network: "regtest", @@ -185,7 +187,7 @@ func TestPeerswapMessages_TwoHopValidation(t *testing.T) { t.Parallel() msg := SwapOutAgreementMessage{ - ProtocolVersion: 5, + ProtocolVersion: PEERSWAP_PROTOCOL_VERSION, SwapId: swapID, Pubkey: pubkey, TwoHop: &TwoHop{ diff --git a/swap/service.go b/swap/service.go index d899600f..3dd35583 100644 --- a/swap/service.go +++ b/swap/service.go @@ -14,7 +14,7 @@ import ( ) const ( - PEERSWAP_PROTOCOL_VERSION = 5 + PEERSWAP_PROTOCOL_VERSION = 6 ) var (