Merge pull request #8239 from ellemouton/loadSessionByActiveTower

wtclient: add DeactivateTower and TerminateSession commands
This commit is contained in:
Elle 2024-02-21 11:28:25 +02:00 committed by GitHub
commit c398b0cc69
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 2731 additions and 762 deletions

View File

@ -10,7 +10,8 @@ import (
"github.com/urfave/cli"
)
// wtclientCommands will return nil for non-wtclientrpc builds.
// wtclientCommands is a list of commands that can be used to interact with the
// watchtower client.
func wtclientCommands() []cli.Command {
return []cli.Command{
{
@ -20,10 +21,12 @@ func wtclientCommands() []cli.Command {
Subcommands: []cli.Command{
addTowerCommand,
removeTowerCommand,
deactivateTowerCommand,
listTowersCommand,
getTowerCommand,
statsCommand,
policyCommand,
sessionCommands,
},
},
}
@ -84,6 +87,44 @@ func addTower(ctx *cli.Context) error {
return nil
}
var deactivateTowerCommand = cli.Command{
Name: "deactivate",
Usage: "Deactivate a watchtower to temporarily prevent its use for " +
"sessions/backups.",
ArgsUsage: "pubkey",
Action: actionDecorator(deactivateTower),
}
func deactivateTower(ctx *cli.Context) error {
ctxc := getContext()
// Display the command's help message if the number of arguments/flags
// is not what we expect.
if ctx.NArg() != 1 || ctx.NumFlags() > 0 {
return cli.ShowCommandHelp(ctx, "deactivate")
}
pubKey, err := hex.DecodeString(ctx.Args().First())
if err != nil {
return fmt.Errorf("invalid public key: %w", err)
}
client, cleanUp := getWtclient(ctx)
defer cleanUp()
req := &wtclientrpc.DeactivateTowerRequest{
Pubkey: pubKey,
}
resp, err := client.DeactivateTower(ctxc, req)
if err != nil {
return err
}
printRespJSON(resp)
return nil
}
var removeTowerCommand = cli.Command{
Name: "remove",
Usage: "Remove a watchtower to prevent its use for future " +
@ -333,3 +374,47 @@ func policy(ctx *cli.Context) error {
printRespJSON(resp)
return nil
}
var sessionCommands = cli.Command{
Name: "session",
Subcommands: []cli.Command{
terminateSessionCommand,
},
}
var terminateSessionCommand = cli.Command{
Name: "terminate",
ArgsUsage: "id",
Action: actionDecorator(terminateSession),
}
func terminateSession(ctx *cli.Context) error {
ctxc := getContext()
// Display the command's help message if the number of arguments/flags
// is not what we expect.
if ctx.NArg() > 1 || ctx.NumFlags() != 0 {
return cli.ShowCommandHelp(ctx, "terminate")
}
client, cleanUp := getWtclient(ctx)
defer cleanUp()
sessionID, err := hex.DecodeString(ctx.Args().First())
if err != nil {
return fmt.Errorf("invalid session ID: %w", err)
}
resp, err := client.TerminateSession(
ctxc, &wtclientrpc.TerminateSessionRequest{
SessionId: sessionID,
},
)
if err != nil {
return err
}
printRespJSON(resp)
return nil
}

View File

@ -155,6 +155,13 @@
about the txid and don't want the calling code to block while the channel
drains the active HTLCs.
* [New watchtower client DeactivateTower and
TerminateSession](https://github.com/lightningnetwork/lnd/pull/8239) commands
have been added. The DeactivateTower command can be used to mark a tower as
inactive so that its sessions are not loaded on startup and so that the tower
is not considered for session negotiation. TerminateSession can be used to
mark a specific session as terminal so that that specific is never used again.
## lncli Additions
* Deprecate `bumpclosefee` for `bumpforceclosefee` to accommodate for the fact

View File

@ -519,16 +519,8 @@ var allTestCases = []*lntest.TestCase{
TestFunc: testLookupHtlcResolution,
},
{
Name: "watchtower session management",
TestFunc: testWatchtowerSessionManagement,
},
{
// NOTE: this test must be put in the same tranche as
// `testWatchtowerSessionManagement` to avoid parallel use of
// the default watchtower port.
Name: "revoked uncooperative close retribution altruist " +
"watchtower",
TestFunc: testRevokedCloseRetributionAltruistWatchtower,
Name: "watchtower",
TestFunc: testWatchtower,
},
{
Name: "channel fundmax",

View File

@ -4,7 +4,6 @@ import (
"bytes"
"fmt"
"testing"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
@ -12,7 +11,6 @@ import (
"github.com/go-errors/errors"
"github.com/lightningnetwork/lnd/funding"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/wtclientrpc"
"github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/lntest/wait"
"github.com/stretchr/testify/require"
@ -707,304 +705,3 @@ func testRevokedCloseRetributionRemoteHodl(ht *lntest.HarnessTest) {
})
}
}
// testRevokedCloseRetributionAltruistWatchtower establishes a channel between
// Carol and Dave, where Carol is using a third node Willy as her watchtower.
// After sending some payments, Dave reverts his state and force closes to
// trigger a breach. Carol is kept offline throughout the process and the test
// asserts that Willy responds by broadcasting the justice transaction on
// Carol's behalf sweeping her funds without a reward.
func testRevokedCloseRetributionAltruistWatchtower(ht *lntest.HarnessTest) {
for _, commitType := range []lnrpc.CommitmentType{
lnrpc.CommitmentType_LEGACY,
lnrpc.CommitmentType_ANCHORS,
lnrpc.CommitmentType_SIMPLE_TAPROOT,
} {
testName := fmt.Sprintf("%v", commitType.String())
ct := commitType
testFunc := func(ht *lntest.HarnessTest) {
testRevokedCloseRetributionAltruistWatchtowerCase(
ht, ct,
)
}
success := ht.Run(testName, func(tt *testing.T) {
st := ht.Subtest(tt)
st.RunTestCase(&lntest.TestCase{
Name: testName,
TestFunc: testFunc,
})
})
if !success {
// Log failure time to help relate the lnd logs to the
// failure.
ht.Logf("Failure time: %v", time.Now().Format(
"2006-01-02 15:04:05.000",
))
break
}
}
}
func testRevokedCloseRetributionAltruistWatchtowerCase(ht *lntest.HarnessTest,
commitType lnrpc.CommitmentType) {
const (
chanAmt = funding.MaxBtcFundingAmount
paymentAmt = 10000
numInvoices = 6
externalIP = "1.2.3.4"
)
// Since we'd like to test some multi-hop failure scenarios, we'll
// introduce another node into our test network: Carol.
carolArgs := lntest.NodeArgsForCommitType(commitType)
carolArgs = append(carolArgs, "--hodl.exit-settle")
carol := ht.NewNode("Carol", carolArgs)
// Willy the watchtower will protect Dave from Carol's breach. He will
// remain online in order to punish Carol on Dave's behalf, since the
// breach will happen while Dave is offline.
willy := ht.NewNode(
"Willy", []string{
"--watchtower.active",
"--watchtower.externalip=" + externalIP,
},
)
willyInfo := willy.RPC.GetInfoWatchtower()
// Assert that Willy has one listener and it is 0.0.0.0:9911 or
// [::]:9911. Since no listener is explicitly specified, one of these
// should be the default depending on whether the host supports IPv6 or
// not.
require.Len(ht, willyInfo.Listeners, 1, "Willy should have 1 listener")
listener := willyInfo.Listeners[0]
if listener != "0.0.0.0:9911" && listener != "[::]:9911" {
ht.Fatalf("expected listener on 0.0.0.0:9911 or [::]:9911, "+
"got %v", listener)
}
// Assert the Willy's URIs properly display the chosen external IP.
require.Len(ht, willyInfo.Uris, 1, "Willy should have 1 uri")
require.Contains(ht, willyInfo.Uris[0], externalIP)
// Dave will be the breached party. We set --nolisten to ensure Carol
// won't be able to connect to him and trigger the channel data
// protection logic automatically.
daveArgs := lntest.NodeArgsForCommitType(commitType)
daveArgs = append(daveArgs, "--nolisten", "--wtclient.active")
dave := ht.NewNode("Dave", daveArgs)
addTowerReq := &wtclientrpc.AddTowerRequest{
Pubkey: willyInfo.Pubkey,
Address: listener,
}
dave.RPC.AddTower(addTowerReq)
// We must let Dave have an open channel before she can send a node
// announcement, so we open a channel with Carol,
ht.ConnectNodes(dave, carol)
// Before we make a channel, we'll load up Dave with some coins sent
// directly from the miner.
ht.FundCoins(btcutil.SatoshiPerBitcoin, dave)
// Send one more UTXOs if this is a neutrino backend.
if ht.IsNeutrinoBackend() {
ht.FundCoins(btcutil.SatoshiPerBitcoin, dave)
}
// In order to test Dave's response to an uncooperative channel
// closure by Carol, we'll first open up a channel between them with a
// 0.5 BTC value.
params := lntest.OpenChannelParams{
Amt: 3 * (chanAmt / 4),
PushAmt: chanAmt / 4,
CommitmentType: commitType,
Private: true,
}
chanPoint := ht.OpenChannel(dave, carol, params)
// With the channel open, we'll create a few invoices for Carol that
// Dave will pay to in order to advance the state of the channel.
carolPayReqs, _, _ := ht.CreatePayReqs(carol, paymentAmt, numInvoices)
// Next query for Carol's channel state, as we sent 0 payments, Carol
// should still see her balance as the push amount, which is 1/4 of the
// capacity.
carolChan := ht.AssertChannelLocalBalance(
carol, chanPoint, int64(chanAmt/4),
)
// Grab Carol's current commitment height (update number), we'll later
// revert her to this state after additional updates to force him to
// broadcast this soon to be revoked state.
carolStateNumPreCopy := int(carolChan.NumUpdates)
// With the temporary file created, copy Carol's current state into the
// temporary file we created above. Later after more updates, we'll
// restore this state.
ht.BackupDB(carol)
// Reconnect the peers after the restart that was needed for the db
// backup.
ht.EnsureConnected(dave, carol)
// Once connected, give Dave some time to enable the channel again.
ht.AssertTopologyChannelOpen(dave, chanPoint)
// Finally, send payments from Dave to Carol, consuming Carol's
// remaining payment hashes.
ht.CompletePaymentRequestsNoWait(dave, carolPayReqs, chanPoint)
daveBalResp := dave.RPC.WalletBalance()
davePreSweepBalance := daveBalResp.ConfirmedBalance
// Wait until the backup has been accepted by the watchtower before
// shutting down Dave.
err := wait.NoError(func() error {
bkpStats := dave.RPC.WatchtowerStats()
if bkpStats == nil {
return errors.New("no active backup sessions")
}
if bkpStats.NumBackups == 0 {
return errors.New("no backups accepted")
}
return nil
}, defaultTimeout)
require.NoError(ht, err, "unable to verify backup task completed")
// Shutdown Dave to simulate going offline for an extended period of
// time. Once he's not watching, Carol will try to breach the channel.
restart := ht.SuspendNode(dave)
// Now we shutdown Carol, copying over the his temporary database state
// which has the *prior* channel state over his current most up to date
// state. With this, we essentially force Carol to travel back in time
// within the channel's history.
ht.RestartNodeAndRestoreDB(carol)
// Now query for Carol's channel state, it should show that he's at a
// state number in the past, not the *latest* state.
ht.AssertChannelCommitHeight(carol, chanPoint, carolStateNumPreCopy)
// Now force Carol to execute a *force* channel closure by unilaterally
// broadcasting his current channel state. This is actually the
// commitment transaction of a prior *revoked* state, so he'll soon
// feel the wrath of Dave's retribution.
closeUpdates, closeTxID := ht.CloseChannelAssertPending(
carol, chanPoint, true,
)
// Finally, generate a single block, wait for the final close status
// update, then ensure that the closing transaction was included in the
// block.
block := ht.MineBlocksAndAssertNumTxes(1, 1)[0]
breachTXID := ht.WaitForChannelCloseEvent(closeUpdates)
ht.Miner.AssertTxInBlock(block, breachTXID)
// The breachTXID should match the above closeTxID.
require.EqualValues(ht, breachTXID, closeTxID)
// Query the mempool for Dave's justice transaction, this should be
// broadcast as Carol's contract breaching transaction gets confirmed
// above.
justiceTXID := ht.Miner.AssertNumTxsInMempool(1)[0]
// Query for the mempool transaction found above. Then assert that all
// the inputs of this transaction are spending outputs generated by
// Carol's breach transaction above.
justiceTx := ht.Miner.GetRawTransaction(justiceTXID)
for _, txIn := range justiceTx.MsgTx().TxIn {
require.Equal(ht, breachTXID[:], txIn.PreviousOutPoint.Hash[:],
"justice tx not spending commitment utxo")
}
willyBalResp := willy.RPC.WalletBalance()
require.Zero(ht, willyBalResp.ConfirmedBalance,
"willy should have 0 balance before mining justice transaction")
// Now mine a block, this transaction should include Dave's justice
// transaction which was just accepted into the mempool.
block = ht.MineBlocksAndAssertNumTxes(1, 1)[0]
// The block should have exactly *two* transactions, one of which is
// the justice transaction.
require.Len(ht, block.Transactions, 2, "transaction wasn't mined")
justiceSha := block.Transactions[1].TxHash()
require.Equal(ht, justiceTx.Hash()[:], justiceSha[:],
"justice tx wasn't mined")
// Ensure that Willy doesn't get any funds, as he is acting as an
// altruist watchtower.
err = wait.NoError(func() error {
willyBalResp := willy.RPC.WalletBalance()
if willyBalResp.ConfirmedBalance != 0 {
return fmt.Errorf("expected Willy to have no funds "+
"after justice transaction was mined, found %v",
willyBalResp)
}
return nil
}, time.Second*5)
require.NoError(ht, err, "timeout checking willy's balance")
// Before restarting Dave, shutdown Carol so Dave won't sync with her.
// Otherwise, during the restart, Dave will realize Carol is falling
// behind and return `ErrCommitSyncRemoteDataLoss`, thus force closing
// the channel. Although this force close tx will be later replaced by
// the breach tx, it will create two anchor sweeping txes for neutrino
// backend, causing the confirmed wallet balance to be zero later on
// because the utxos are used in sweeping.
ht.Shutdown(carol)
// Restart Dave, who will still think his channel with Carol is open.
// We should him to detect the breach, but realize that the funds have
// then been swept to his wallet by Willy.
require.NoError(ht, restart(), "unable to restart dave")
err = wait.NoError(func() error {
daveBalResp := dave.RPC.ChannelBalance()
if daveBalResp.LocalBalance.Sat != 0 {
return fmt.Errorf("Dave should end up with zero "+
"channel balance, instead has %d",
daveBalResp.LocalBalance.Sat)
}
return nil
}, defaultTimeout)
require.NoError(ht, err, "timeout checking dave's channel balance")
ht.AssertNumPendingForceClose(dave, 0)
// If this is an anchor channel, Dave would sweep the anchor.
if lntest.CommitTypeHasAnchors(commitType) {
ht.MineBlocksAndAssertNumTxes(1, 1)
}
// Check that Dave's wallet balance is increased.
err = wait.NoError(func() error {
daveBalResp := dave.RPC.WalletBalance()
if daveBalResp.ConfirmedBalance <= davePreSweepBalance {
return fmt.Errorf("Dave should have more than %d "+
"after sweep, instead has %d",
davePreSweepBalance,
daveBalResp.ConfirmedBalance)
}
return nil
}, defaultTimeout)
require.NoError(ht, err, "timeout checking dave's wallet balance")
// Dave should have no open channels.
ht.AssertNodeNumChannels(dave, 0)
}

View File

@ -2,24 +2,234 @@ package itest
import (
"fmt"
"testing"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/go-errors/errors"
"github.com/lightningnetwork/lnd/funding"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lnrpc/wtclientrpc"
"github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/lntest/node"
"github.com/lightningnetwork/lnd/lntest/rpc"
"github.com/lightningnetwork/lnd/lntest/wait"
"github.com/stretchr/testify/require"
)
// testWatchtowerSessionManagement tests that session deletion is done
// correctly.
func testWatchtowerSessionManagement(ht *lntest.HarnessTest) {
// testWatchtower tests the behaviour of the watchtower client and server.
func testWatchtower(ht *lntest.HarnessTest) {
ht.Run("revocation", func(t *testing.T) {
tt := ht.Subtest(t)
testRevokedCloseRetributionAltruistWatchtower(tt)
})
ht.Run("session deletion", func(t *testing.T) {
tt := ht.Subtest(t)
testTowerClientSessionDeletion(tt)
})
ht.Run("tower and session activation", func(t *testing.T) {
tt := ht.Subtest(t)
testTowerClientTowerAndSessionManagement(tt)
})
}
// testTowerClientTowerAndSessionManagement tests the various control commands
// that a user has over the client's set of active towers and sessions.
func testTowerClientTowerAndSessionManagement(ht *lntest.HarnessTest) {
const (
chanAmt = funding.MaxBtcFundingAmount
externalIP = "1.2.3.4"
externalIP2 = "1.2.3.5"
sessionCloseRange = 1
)
// Set up Wallis the watchtower who will be used by Dave to watch over
// his channel commitment transactions.
wallisPk, wallisListener, _ := setUpNewTower(ht, "Wallis", externalIP)
// Dave will be the tower client.
daveArgs := []string{
"--wtclient.active",
fmt.Sprintf(
"--wtclient.session-close-range=%d", sessionCloseRange,
),
}
dave := ht.NewNode("Dave", daveArgs)
addWallisReq := &wtclientrpc.AddTowerRequest{
Pubkey: wallisPk,
Address: wallisListener,
}
dave.RPC.AddTower(addWallisReq)
assertNumSessions := func(towerPk []byte, expectedNum int) {
err := wait.NoError(func() error {
info := dave.RPC.GetTowerInfo(
&wtclientrpc.GetTowerInfoRequest{
Pubkey: towerPk,
IncludeSessions: true,
},
)
var numSessions uint32
for _, sessionType := range info.SessionInfo {
numSessions += sessionType.NumSessions
}
if numSessions == uint32(expectedNum) {
return nil
}
return fmt.Errorf("expected %d sessions, got %d",
expectedNum, numSessions)
}, defaultTimeout)
require.NoError(ht, err)
}
// Assert that there are a few sessions between Dave and Wallis. There
// should be one per client. There are currently 3 types of clients, so
// we expect 3 sessions.
assertNumSessions(wallisPk, 3)
// Before we make a channel, we'll load up Dave with some coins sent
// directly from the miner.
ht.FundCoins(btcutil.SatoshiPerBitcoin, dave)
// Connect Dave and Alice.
ht.ConnectNodes(dave, ht.Alice)
// Open a channel between Dave and Alice.
params := lntest.OpenChannelParams{
Amt: chanAmt,
}
chanPoint := ht.OpenChannel(dave, ht.Alice, params)
// Show that the Wallis tower is currently seen as an active session
// candidate.
info := dave.RPC.GetTowerInfo(&wtclientrpc.GetTowerInfoRequest{
Pubkey: wallisPk,
})
require.GreaterOrEqual(ht, len(info.SessionInfo), 1)
require.True(ht, info.SessionInfo[0].ActiveSessionCandidate)
// Make some back-ups and assert that they are added to a session with
// the tower.
generateBackups(ht, dave, ht.Alice, 4)
// Assert that one of the sessions now has 4 backups.
assertNumBackups(ht, dave.RPC, wallisPk, 4, false)
// Now, deactivate the tower and show that it is no longer considered
// an active session candidate.
dave.RPC.DeactivateTower(&wtclientrpc.DeactivateTowerRequest{
Pubkey: wallisPk,
})
info = dave.RPC.GetTowerInfo(&wtclientrpc.GetTowerInfoRequest{
Pubkey: wallisPk,
})
require.GreaterOrEqual(ht, len(info.SessionInfo), 1)
require.False(ht, info.SessionInfo[0].ActiveSessionCandidate)
// Back up a few more states.
generateBackups(ht, dave, ht.Alice, 4)
// These should _not_ be on the tower. Therefore, the number of
// back-ups on the tower should be the same as before.
assertNumBackups(ht, dave.RPC, wallisPk, 4, false)
// Add new tower and connect Dave to it.
wilmaPk, wilmaListener, _ := setUpNewTower(ht, "Wilma", externalIP2)
dave.RPC.AddTower(&wtclientrpc.AddTowerRequest{
Pubkey: wilmaPk,
Address: wilmaListener,
})
assertNumSessions(wilmaPk, 3)
// The updates from before should now appear on the new watchtower.
assertNumBackups(ht, dave.RPC, wilmaPk, 4, false)
// Reactivate the Wallis tower and then deactivate the Wilma one.
dave.RPC.AddTower(addWallisReq)
dave.RPC.DeactivateTower(&wtclientrpc.DeactivateTowerRequest{
Pubkey: wilmaPk,
})
// Generate some more back-ups.
generateBackups(ht, dave, ht.Alice, 4)
// Assert that they get added to the first tower (Wallis) and that the
// number of sessions with Wallis has not changed - in other words, the
// previously used session was re-used.
assertNumBackups(ht, dave.RPC, wallisPk, 8, false)
assertNumSessions(wallisPk, 3)
findSession := func(towerPk []byte, numBackups uint32) []byte {
info := dave.RPC.GetTowerInfo(&wtclientrpc.GetTowerInfoRequest{
Pubkey: towerPk,
IncludeSessions: true,
})
for _, sessionType := range info.SessionInfo {
for _, session := range sessionType.Sessions {
if session.NumBackups == numBackups {
return session.Id
}
}
}
ht.Fatalf("session with %d backups not found", numBackups)
return nil
}
// Now we will test the termination of a session.
// First, we need to figure out the ID of the session that has been used
// for back-ups.
sessionID := findSession(wallisPk, 8)
// Now, terminate the session.
dave.RPC.TerminateSession(&wtclientrpc.TerminateSessionRequest{
SessionId: sessionID,
})
// This should force the client to negotiate a new session. The old
// session still remains in our session list since the channel for which
// it has updates for is still open.
assertNumSessions(wallisPk, 4)
// Any new back-ups should now be backed up on a different session.
generateBackups(ht, dave, ht.Alice, 2)
assertNumBackups(ht, dave.RPC, wallisPk, 10, false)
findSession(wallisPk, 2)
// Close the channel.
ht.CloseChannelAssertPending(dave, chanPoint, false)
// Mine enough blocks to surpass the session close range buffer.
ht.MineBlocksAndAssertNumTxes(sessionCloseRange+6, 1)
// The session that was previously terminated now gets deleted since
// the channel for which it has updates has now been closed. All the
// remaining sessions are not yet closable since they are not yet
// exhausted and are all still active.
assertNumSessions(wallisPk, 3)
// For the sake of completion, we call RemoveTower here for both towers
// to show that this should never error.
dave.RPC.RemoveTower(&wtclientrpc.RemoveTowerRequest{
Pubkey: wallisPk,
})
dave.RPC.RemoveTower(&wtclientrpc.RemoveTowerRequest{
Pubkey: wilmaPk,
})
}
// testTowerClientSessionDeletion tests that sessions are correctly deleted
// when they are deemed closable.
func testTowerClientSessionDeletion(ht *lntest.HarnessTest) {
const (
chanAmt = funding.MaxBtcFundingAmount
paymentAmt = 10_000
numInvoices = 5
maxUpdates = numInvoices * 2
externalIP = "1.2.3.4"
@ -28,24 +238,7 @@ func testWatchtowerSessionManagement(ht *lntest.HarnessTest) {
// Set up Wallis the watchtower who will be used by Dave to watch over
// his channel commitment transactions.
wallis := ht.NewNode("Wallis", []string{
"--watchtower.active",
"--watchtower.externalip=" + externalIP,
})
wallisInfo := wallis.RPC.GetInfoWatchtower()
// Assert that Wallis has one listener and it is 0.0.0.0:9911 or
// [::]:9911. Since no listener is explicitly specified, one of these
// should be the default depending on whether the host supports IPv6 or
// not.
require.Len(ht, wallisInfo.Listeners, 1)
listener := wallisInfo.Listeners[0]
require.True(ht, listener == "0.0.0.0:9911" || listener == "[::]:9911")
// Assert the Wallis's URIs properly display the chosen external IP.
require.Len(ht, wallisInfo.Uris, 1)
require.Contains(ht, wallisInfo.Uris[0], externalIP)
wallisPk, listener, _ := setUpNewTower(ht, "Wallis", externalIP)
// Dave will be the tower client.
daveArgs := []string{
@ -58,7 +251,7 @@ func testWatchtowerSessionManagement(ht *lntest.HarnessTest) {
dave := ht.NewNode("Dave", daveArgs)
addTowerReq := &wtclientrpc.AddTowerRequest{
Pubkey: wallisInfo.Pubkey,
Pubkey: wallisPk,
Address: listener,
}
dave.RPC.AddTower(addTowerReq)
@ -66,7 +259,7 @@ func testWatchtowerSessionManagement(ht *lntest.HarnessTest) {
// Assert that there exists a session between Dave and Wallis.
err := wait.NoError(func() error {
info := dave.RPC.GetTowerInfo(&wtclientrpc.GetTowerInfoRequest{
Pubkey: wallisInfo.Pubkey,
Pubkey: wallisPk,
IncludeSessions: true,
})
@ -98,61 +291,10 @@ func testWatchtowerSessionManagement(ht *lntest.HarnessTest) {
// Since there are 2 updates made for every payment and the maximum
// number of updates per session has been set to 10, make 5 payments
// between the pair so that the session is exhausted.
alicePayReqs, _, _ := ht.CreatePayReqs(
ht.Alice, paymentAmt, numInvoices,
)
send := func(node *node.HarnessNode, payReq string) {
stream := node.RPC.SendPayment(&routerrpc.SendPaymentRequest{
PaymentRequest: payReq,
TimeoutSeconds: 60,
FeeLimitMsat: noFeeLimitMsat,
})
ht.AssertPaymentStatusFromStream(
stream, lnrpc.Payment_SUCCEEDED,
)
}
for i := 0; i < numInvoices; i++ {
send(dave, alicePayReqs[i])
}
// assertNumBackups is a closure that asserts that Dave has a certain
// number of backups backed up to the tower. If mineOnFail is true,
// then a block will be mined each time the assertion fails.
assertNumBackups := func(expected int, mineOnFail bool) {
err = wait.NoError(func() error {
info := dave.RPC.GetTowerInfo(
&wtclientrpc.GetTowerInfoRequest{
Pubkey: wallisInfo.Pubkey,
IncludeSessions: true,
},
)
var numBackups uint32
for _, sessionType := range info.SessionInfo {
for _, session := range sessionType.Sessions {
numBackups += session.NumBackups
}
}
if numBackups == uint32(expected) {
return nil
}
if mineOnFail {
ht.Miner.MineBlocksSlow(1)
}
return fmt.Errorf("expected %d backups, got %d",
expected, numBackups)
}, defaultTimeout)
require.NoError(ht, err)
}
generateBackups(ht, dave, ht.Alice, maxUpdates)
// Assert that one of the sessions now has 10 backups.
assertNumBackups(10, false)
assertNumBackups(ht, dave.RPC, wallisPk, 10, false)
// Now close the channel and wait for the close transaction to appear
// in the mempool so that it is included in a block when we mine.
@ -168,5 +310,387 @@ func testWatchtowerSessionManagement(ht *lntest.HarnessTest) {
// would immediately negotiate another session after deleting the
// exhausted one. This time we set the "mineOnFail" parameter to true to
// ensure that the session deleting logic is run.
assertNumBackups(0, true)
assertNumBackups(ht, dave.RPC, wallisPk, 0, true)
}
// testRevokedCloseRetributionAltruistWatchtower establishes a channel between
// Carol and Dave, where Carol is using a third node Willy as her watchtower.
// After sending some payments, Dave reverts his state and force closes to
// trigger a breach. Carol is kept offline throughout the process and the test
// asserts that Willy responds by broadcasting the justice transaction on
// Carol's behalf sweeping her funds without a reward.
func testRevokedCloseRetributionAltruistWatchtower(ht *lntest.HarnessTest) {
for _, commitType := range []lnrpc.CommitmentType{
lnrpc.CommitmentType_LEGACY,
lnrpc.CommitmentType_ANCHORS,
lnrpc.CommitmentType_SIMPLE_TAPROOT,
} {
testName := fmt.Sprintf("%v", commitType.String())
ct := commitType
testFunc := func(ht *lntest.HarnessTest) {
testRevokedCloseRetributionAltruistWatchtowerCase(
ht, ct,
)
}
success := ht.Run(testName, func(tt *testing.T) {
st := ht.Subtest(tt)
st.RunTestCase(&lntest.TestCase{
Name: testName,
TestFunc: testFunc,
})
})
if !success {
// Log failure time to help relate the lnd logs to the
// failure.
ht.Logf("Failure time: %v", time.Now().Format(
"2006-01-02 15:04:05.000",
))
break
}
}
}
func testRevokedCloseRetributionAltruistWatchtowerCase(ht *lntest.HarnessTest,
commitType lnrpc.CommitmentType) {
const (
chanAmt = funding.MaxBtcFundingAmount
paymentAmt = 10000
numInvoices = 6
externalIP = "1.2.3.4"
)
// Since we'd like to test some multi-hop failure scenarios, we'll
// introduce another node into our test network: Carol.
carolArgs := lntest.NodeArgsForCommitType(commitType)
carolArgs = append(carolArgs, "--hodl.exit-settle")
carol := ht.NewNode("Carol", carolArgs)
// Set up Willy the watchtower who will protect Dave from Carol's
// breach. He will remain online in order to punish Carol on Dave's
// behalf, since the breach will happen while Dave is offline.
willyInfoPk, listener, willy := setUpNewTower(ht, "Willy", externalIP)
// Dave will be the breached party. We set --nolisten to ensure Carol
// won't be able to connect to him and trigger the channel data
// protection logic automatically.
daveArgs := lntest.NodeArgsForCommitType(commitType)
daveArgs = append(daveArgs, "--nolisten", "--wtclient.active")
dave := ht.NewNode("Dave", daveArgs)
addTowerReq := &wtclientrpc.AddTowerRequest{
Pubkey: willyInfoPk,
Address: listener,
}
dave.RPC.AddTower(addTowerReq)
// We must let Dave have an open channel before she can send a node
// announcement, so we open a channel with Carol,
ht.ConnectNodes(dave, carol)
// Before we make a channel, we'll load up Dave with some coins sent
// directly from the miner.
ht.FundCoins(btcutil.SatoshiPerBitcoin, dave)
// Send one more UTXOs if this is a neutrino backend.
if ht.IsNeutrinoBackend() {
ht.FundCoins(btcutil.SatoshiPerBitcoin, dave)
}
// In order to test Dave's response to an uncooperative channel
// closure by Carol, we'll first open up a channel between them with a
// 0.5 BTC value.
params := lntest.OpenChannelParams{
Amt: 3 * (chanAmt / 4),
PushAmt: chanAmt / 4,
CommitmentType: commitType,
Private: true,
}
chanPoint := ht.OpenChannel(dave, carol, params)
// With the channel open, we'll create a few invoices for Carol that
// Dave will pay to in order to advance the state of the channel.
carolPayReqs, _, _ := ht.CreatePayReqs(carol, paymentAmt, numInvoices)
// Next query for Carol's channel state, as we sent 0 payments, Carol
// should still see her balance as the push amount, which is 1/4 of the
// capacity.
carolChan := ht.AssertChannelLocalBalance(
carol, chanPoint, int64(chanAmt/4),
)
// Grab Carol's current commitment height (update number), we'll later
// revert her to this state after additional updates to force him to
// broadcast this soon to be revoked state.
carolStateNumPreCopy := int(carolChan.NumUpdates)
// With the temporary file created, copy Carol's current state into the
// temporary file we created above. Later after more updates, we'll
// restore this state.
ht.BackupDB(carol)
// Reconnect the peers after the restart that was needed for the db
// backup.
ht.EnsureConnected(dave, carol)
// Once connected, give Dave some time to enable the channel again.
ht.AssertTopologyChannelOpen(dave, chanPoint)
// Finally, send payments from Dave to Carol, consuming Carol's
// remaining payment hashes.
ht.CompletePaymentRequestsNoWait(dave, carolPayReqs, chanPoint)
daveBalResp := dave.RPC.WalletBalance()
davePreSweepBalance := daveBalResp.ConfirmedBalance
// Wait until the backup has been accepted by the watchtower before
// shutting down Dave.
err := wait.NoError(func() error {
bkpStats := dave.RPC.WatchtowerStats()
if bkpStats == nil {
return errors.New("no active backup sessions")
}
if bkpStats.NumBackups == 0 {
return errors.New("no backups accepted")
}
return nil
}, defaultTimeout)
require.NoError(ht, err, "unable to verify backup task completed")
// Shutdown Dave to simulate going offline for an extended period of
// time. Once he's not watching, Carol will try to breach the channel.
restart := ht.SuspendNode(dave)
// Now we shutdown Carol, copying over the his temporary database state
// which has the *prior* channel state over his current most up to date
// state. With this, we essentially force Carol to travel back in time
// within the channel's history.
ht.RestartNodeAndRestoreDB(carol)
// Now query for Carol's channel state, it should show that he's at a
// state number in the past, not the *latest* state.
ht.AssertChannelCommitHeight(carol, chanPoint, carolStateNumPreCopy)
// Now force Carol to execute a *force* channel closure by unilaterally
// broadcasting his current channel state. This is actually the
// commitment transaction of a prior *revoked* state, so he'll soon
// feel the wrath of Dave's retribution.
closeUpdates, closeTxID := ht.CloseChannelAssertPending(
carol, chanPoint, true,
)
// Finally, generate a single block, wait for the final close status
// update, then ensure that the closing transaction was included in the
// block.
block := ht.MineBlocksAndAssertNumTxes(1, 1)[0]
breachTXID := ht.WaitForChannelCloseEvent(closeUpdates)
ht.Miner.AssertTxInBlock(block, breachTXID)
// The breachTXID should match the above closeTxID.
require.EqualValues(ht, breachTXID, closeTxID)
// Query the mempool for Dave's justice transaction, this should be
// broadcast as Carol's contract breaching transaction gets confirmed
// above.
justiceTXID := ht.Miner.AssertNumTxsInMempool(1)[0]
// Query for the mempool transaction found above. Then assert that all
// the inputs of this transaction are spending outputs generated by
// Carol's breach transaction above.
justiceTx := ht.Miner.GetRawTransaction(justiceTXID)
for _, txIn := range justiceTx.MsgTx().TxIn {
require.Equal(ht, breachTXID[:], txIn.PreviousOutPoint.Hash[:],
"justice tx not spending commitment utxo")
}
willyBalResp := willy.WalletBalance()
require.Zero(ht, willyBalResp.ConfirmedBalance,
"willy should have 0 balance before mining justice transaction")
// Now mine a block, this transaction should include Dave's justice
// transaction which was just accepted into the mempool.
block = ht.MineBlocksAndAssertNumTxes(1, 1)[0]
// The block should have exactly *two* transactions, one of which is
// the justice transaction.
require.Len(ht, block.Transactions, 2, "transaction wasn't mined")
justiceSha := block.Transactions[1].TxHash()
require.Equal(ht, justiceTx.Hash()[:], justiceSha[:],
"justice tx wasn't mined")
// Ensure that Willy doesn't get any funds, as he is acting as an
// altruist watchtower.
err = wait.NoError(func() error {
willyBalResp := willy.WalletBalance()
if willyBalResp.ConfirmedBalance != 0 {
return fmt.Errorf("expected Willy to have no funds "+
"after justice transaction was mined, found %v",
willyBalResp)
}
return nil
}, time.Second*5)
require.NoError(ht, err, "timeout checking willy's balance")
// Before restarting Dave, shutdown Carol so Dave won't sync with her.
// Otherwise, during the restart, Dave will realize Carol is falling
// behind and return `ErrCommitSyncRemoteDataLoss`, thus force closing
// the channel. Although this force close tx will be later replaced by
// the breach tx, it will create two anchor sweeping txes for neutrino
// backend, causing the confirmed wallet balance to be zero later on
// because the utxos are used in sweeping.
ht.Shutdown(carol)
// Restart Dave, who will still think his channel with Carol is open.
// We should him to detect the breach, but realize that the funds have
// then been swept to his wallet by Willy.
require.NoError(ht, restart(), "unable to restart dave")
err = wait.NoError(func() error {
daveBalResp := dave.RPC.ChannelBalance()
if daveBalResp.LocalBalance.Sat != 0 {
return fmt.Errorf("Dave should end up with zero "+
"channel balance, instead has %d",
daveBalResp.LocalBalance.Sat)
}
return nil
}, defaultTimeout)
require.NoError(ht, err, "timeout checking dave's channel balance")
ht.AssertNumPendingForceClose(dave, 0)
// If this is an anchor channel, Dave would sweep the anchor.
if lntest.CommitTypeHasAnchors(commitType) {
ht.MineBlocksAndAssertNumTxes(1, 1)
}
// Check that Dave's wallet balance is increased.
err = wait.NoError(func() error {
daveBalResp := dave.RPC.WalletBalance()
if daveBalResp.ConfirmedBalance <= davePreSweepBalance {
return fmt.Errorf("Dave should have more than %d "+
"after sweep, instead has %d",
davePreSweepBalance,
daveBalResp.ConfirmedBalance)
}
return nil
}, defaultTimeout)
require.NoError(ht, err, "timeout checking dave's wallet balance")
// Dave should have no open channels.
ht.AssertNodeNumChannels(dave, 0)
}
func setUpNewTower(ht *lntest.HarnessTest, name, externalIP string) ([]byte,
string, *rpc.HarnessRPC) {
port := node.NextAvailablePort()
listenAddr := fmt.Sprintf("0.0.0.0:%d", port)
// Set up the new watchtower.
tower := ht.NewNode(name, []string{
"--watchtower.active",
"--watchtower.externalip=" + externalIP,
"--watchtower.listen=" + listenAddr,
})
towerInfo := tower.RPC.GetInfoWatchtower()
require.Len(ht, towerInfo.Listeners, 1)
listener := towerInfo.Listeners[0]
require.True(
ht, listener == listenAddr ||
listener == fmt.Sprintf("[::]:%d", port),
)
// Assert the Tower's URIs properly display the chosen external IP.
require.Len(ht, towerInfo.Uris, 1)
require.Contains(ht, towerInfo.Uris[0], externalIP)
return towerInfo.Pubkey, towerInfo.Listeners[0], tower.RPC
}
// generateBackups is a helper function that can be used to create a number of
// watchtower back-ups.
func generateBackups(ht *lntest.HarnessTest, srcNode,
dstNode *node.HarnessNode, numBackups int64) {
const paymentAmt = 10_000
require.EqualValuesf(ht, numBackups%2, 0, "the number of desired "+
"back-ups must be even")
// Two updates are made for every payment.
numPayments := int(numBackups / 2)
// Create the required number of invoices.
alicePayReqs, _, _ := ht.CreatePayReqs(
dstNode, paymentAmt, numPayments,
)
send := func(node *node.HarnessNode, payReq string) {
stream := node.RPC.SendPayment(
&routerrpc.SendPaymentRequest{
PaymentRequest: payReq,
TimeoutSeconds: 60,
FeeLimitMsat: noFeeLimitMsat,
},
)
ht.AssertPaymentStatusFromStream(
stream, lnrpc.Payment_SUCCEEDED,
)
}
// Pay each invoice.
for i := 0; i < numPayments; i++ {
send(srcNode, alicePayReqs[i])
}
}
// assertNumBackups is a helper that asserts that the given node has a certain
// number of backups backed up to the tower. If mineOnFail is true, then a block
// will be mined each time the assertion fails.
func assertNumBackups(ht *lntest.HarnessTest, node *rpc.HarnessRPC,
towerPk []byte, expectedNumBackups int, mineOnFail bool) {
err := wait.NoError(func() error {
info := node.GetTowerInfo(
&wtclientrpc.GetTowerInfoRequest{
Pubkey: towerPk,
IncludeSessions: true,
},
)
var numBackups uint32
for _, sessionType := range info.SessionInfo {
for _, session := range sessionType.Sessions {
numBackups += session.NumBackups
}
}
if numBackups == uint32(expectedNumBackups) {
return nil
}
if mineOnFail {
ht.Miner.MineBlocksSlow(1)
}
return fmt.Errorf("expected %d backups, got %d",
expectedNumBackups, numBackups)
}, defaultTimeout)
require.NoError(ht, err)
}

View File

@ -71,6 +71,56 @@ func RegisterWatchtowerClientJSONCallbacks(registry map[string]func(ctx context.
callback(string(respBytes), nil)
}
registry["wtclientrpc.WatchtowerClient.DeactivateTower"] = func(ctx context.Context,
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
req := &DeactivateTowerRequest{}
err := marshaler.Unmarshal([]byte(reqJSON), req)
if err != nil {
callback("", err)
return
}
client := NewWatchtowerClientClient(conn)
resp, err := client.DeactivateTower(ctx, req)
if err != nil {
callback("", err)
return
}
respBytes, err := marshaler.Marshal(resp)
if err != nil {
callback("", err)
return
}
callback(string(respBytes), nil)
}
registry["wtclientrpc.WatchtowerClient.TerminateSession"] = func(ctx context.Context,
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
req := &TerminateSessionRequest{}
err := marshaler.Unmarshal([]byte(reqJSON), req)
if err != nil {
callback("", err)
return
}
client := NewWatchtowerClientClient(conn)
resp, err := client.TerminateSession(ctx, req)
if err != nil {
callback("", err)
return
}
respBytes, err := marshaler.Marshal(resp)
if err != nil {
callback("", err)
return
}
callback(string(respBytes), nil)
}
registry["wtclientrpc.WatchtowerClient.ListTowers"] = func(ctx context.Context,
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {

View File

@ -44,6 +44,14 @@ var (
Entity: "offchain",
Action: "write",
}},
"/wtclientrpc.WatchtowerClient/DeactivateTower": {{
Entity: "offchain",
Action: "write",
}},
"/wtclientrpc.WatchtowerClient/TerminateSession": {{
Entity: "offchain",
Action: "write",
}},
"/wtclientrpc.WatchtowerClient/ListTowers": {{
Entity: "offchain",
Action: "read",
@ -251,6 +259,59 @@ func (c *WatchtowerClient) RemoveTower(ctx context.Context,
return &RemoveTowerResponse{}, nil
}
// DeactivateTower sets the given tower's status to inactive so that it is not
// considered for session negotiation. Its sessions will also not be used while
// the tower is inactive.
func (c *WatchtowerClient) DeactivateTower(_ context.Context,
req *DeactivateTowerRequest) (*DeactivateTowerResponse, error) {
if err := c.isActive(); err != nil {
return nil, err
}
pubKey, err := btcec.ParsePubKey(req.Pubkey)
if err != nil {
return nil, err
}
err = c.cfg.ClientMgr.DeactivateTower(pubKey)
if err != nil {
return nil, err
}
return &DeactivateTowerResponse{
Status: fmt.Sprintf("Successful deactivation of tower: %x",
req.Pubkey),
}, nil
}
// TerminateSession terminates the given session and marks it as terminal so
// that it is never used again.
func (c *WatchtowerClient) TerminateSession(_ context.Context,
req *TerminateSessionRequest) (*TerminateSessionResponse, error) {
if err := c.isActive(); err != nil {
return nil, err
}
pubKey, err := btcec.ParsePubKey(req.SessionId)
if err != nil {
return nil, err
}
sessionID := wtdb.NewSessionIDFromPubKey(pubKey)
err = c.cfg.ClientMgr.TerminateSession(sessionID)
if err != nil {
return nil, err
}
return &TerminateSessionResponse{
Status: fmt.Sprintf("Successful termination of session: %s",
sessionID),
}, nil
}
// ListTowers returns the list of watchtowers registered with the client.
func (c *WatchtowerClient) ListTowers(ctx context.Context,
req *ListTowersRequest) (*ListTowersResponse, error) {
@ -489,6 +550,7 @@ func marshallTower(tower *wtclient.RegisteredTower, policyType PolicyType,
for _, session := range sessions {
satPerVByte := session.Policy.SweepFeeRate.FeePerVByte()
rpcSessions = append(rpcSessions, &TowerSession{
Id: session.ID[:],
NumBackups: uint32(ackCounts[session.ID]),
NumPendingBackups: uint32(pendingCounts[session.ID]),
MaxBackups: uint32(session.Policy.MaxUpdates),

View File

@ -264,6 +264,198 @@ func (*RemoveTowerResponse) Descriptor() ([]byte, []int) {
return file_wtclientrpc_wtclient_proto_rawDescGZIP(), []int{3}
}
type DeactivateTowerRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// The identifying public key of the watchtower to deactivate.
Pubkey []byte `protobuf:"bytes,1,opt,name=pubkey,proto3" json:"pubkey,omitempty"`
}
func (x *DeactivateTowerRequest) Reset() {
*x = DeactivateTowerRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *DeactivateTowerRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeactivateTowerRequest) ProtoMessage() {}
func (x *DeactivateTowerRequest) ProtoReflect() protoreflect.Message {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[4]
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 DeactivateTowerRequest.ProtoReflect.Descriptor instead.
func (*DeactivateTowerRequest) Descriptor() ([]byte, []int) {
return file_wtclientrpc_wtclient_proto_rawDescGZIP(), []int{4}
}
func (x *DeactivateTowerRequest) GetPubkey() []byte {
if x != nil {
return x.Pubkey
}
return nil
}
type DeactivateTowerResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// A string describing the action that took place.
Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"`
}
func (x *DeactivateTowerResponse) Reset() {
*x = DeactivateTowerResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *DeactivateTowerResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeactivateTowerResponse) ProtoMessage() {}
func (x *DeactivateTowerResponse) ProtoReflect() protoreflect.Message {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[5]
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 DeactivateTowerResponse.ProtoReflect.Descriptor instead.
func (*DeactivateTowerResponse) Descriptor() ([]byte, []int) {
return file_wtclientrpc_wtclient_proto_rawDescGZIP(), []int{5}
}
func (x *DeactivateTowerResponse) GetStatus() string {
if x != nil {
return x.Status
}
return ""
}
type TerminateSessionRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// The ID of the session that should be terminated.
SessionId []byte `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"`
}
func (x *TerminateSessionRequest) Reset() {
*x = TerminateSessionRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *TerminateSessionRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*TerminateSessionRequest) ProtoMessage() {}
func (x *TerminateSessionRequest) ProtoReflect() protoreflect.Message {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[6]
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 TerminateSessionRequest.ProtoReflect.Descriptor instead.
func (*TerminateSessionRequest) Descriptor() ([]byte, []int) {
return file_wtclientrpc_wtclient_proto_rawDescGZIP(), []int{6}
}
func (x *TerminateSessionRequest) GetSessionId() []byte {
if x != nil {
return x.SessionId
}
return nil
}
type TerminateSessionResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// A string describing the action that took place.
Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"`
}
func (x *TerminateSessionResponse) Reset() {
*x = TerminateSessionResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *TerminateSessionResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*TerminateSessionResponse) ProtoMessage() {}
func (x *TerminateSessionResponse) ProtoReflect() protoreflect.Message {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[7]
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 TerminateSessionResponse.ProtoReflect.Descriptor instead.
func (*TerminateSessionResponse) Descriptor() ([]byte, []int) {
return file_wtclientrpc_wtclient_proto_rawDescGZIP(), []int{7}
}
func (x *TerminateSessionResponse) GetStatus() string {
if x != nil {
return x.Status
}
return ""
}
type GetTowerInfoRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@ -281,7 +473,7 @@ type GetTowerInfoRequest struct {
func (x *GetTowerInfoRequest) Reset() {
*x = GetTowerInfoRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[4]
mi := &file_wtclientrpc_wtclient_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -294,7 +486,7 @@ func (x *GetTowerInfoRequest) String() string {
func (*GetTowerInfoRequest) ProtoMessage() {}
func (x *GetTowerInfoRequest) ProtoReflect() protoreflect.Message {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[4]
mi := &file_wtclientrpc_wtclient_proto_msgTypes[8]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -307,7 +499,7 @@ func (x *GetTowerInfoRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetTowerInfoRequest.ProtoReflect.Descriptor instead.
func (*GetTowerInfoRequest) Descriptor() ([]byte, []int) {
return file_wtclientrpc_wtclient_proto_rawDescGZIP(), []int{4}
return file_wtclientrpc_wtclient_proto_rawDescGZIP(), []int{8}
}
func (x *GetTowerInfoRequest) GetPubkey() []byte {
@ -353,12 +545,14 @@ type TowerSession struct {
// The fee rate, in satoshis per vbyte, that will be used by the watchtower for
// the justice transaction in the event of a channel breach.
SweepSatPerVbyte uint32 `protobuf:"varint,5,opt,name=sweep_sat_per_vbyte,json=sweepSatPerVbyte,proto3" json:"sweep_sat_per_vbyte,omitempty"`
// The ID of the session.
Id []byte `protobuf:"bytes,6,opt,name=id,proto3" json:"id,omitempty"`
}
func (x *TowerSession) Reset() {
*x = TowerSession{}
if protoimpl.UnsafeEnabled {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[5]
mi := &file_wtclientrpc_wtclient_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -371,7 +565,7 @@ func (x *TowerSession) String() string {
func (*TowerSession) ProtoMessage() {}
func (x *TowerSession) ProtoReflect() protoreflect.Message {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[5]
mi := &file_wtclientrpc_wtclient_proto_msgTypes[9]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -384,7 +578,7 @@ func (x *TowerSession) ProtoReflect() protoreflect.Message {
// Deprecated: Use TowerSession.ProtoReflect.Descriptor instead.
func (*TowerSession) Descriptor() ([]byte, []int) {
return file_wtclientrpc_wtclient_proto_rawDescGZIP(), []int{5}
return file_wtclientrpc_wtclient_proto_rawDescGZIP(), []int{9}
}
func (x *TowerSession) GetNumBackups() uint32 {
@ -423,6 +617,13 @@ func (x *TowerSession) GetSweepSatPerVbyte() uint32 {
return 0
}
func (x *TowerSession) GetId() []byte {
if x != nil {
return x.Id
}
return nil
}
type Tower struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@ -457,7 +658,7 @@ type Tower struct {
func (x *Tower) Reset() {
*x = Tower{}
if protoimpl.UnsafeEnabled {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[6]
mi := &file_wtclientrpc_wtclient_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -470,7 +671,7 @@ func (x *Tower) String() string {
func (*Tower) ProtoMessage() {}
func (x *Tower) ProtoReflect() protoreflect.Message {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[6]
mi := &file_wtclientrpc_wtclient_proto_msgTypes[10]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -483,7 +684,7 @@ func (x *Tower) ProtoReflect() protoreflect.Message {
// Deprecated: Use Tower.ProtoReflect.Descriptor instead.
func (*Tower) Descriptor() ([]byte, []int) {
return file_wtclientrpc_wtclient_proto_rawDescGZIP(), []int{6}
return file_wtclientrpc_wtclient_proto_rawDescGZIP(), []int{10}
}
func (x *Tower) GetPubkey() []byte {
@ -549,7 +750,7 @@ type TowerSessionInfo struct {
func (x *TowerSessionInfo) Reset() {
*x = TowerSessionInfo{}
if protoimpl.UnsafeEnabled {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[7]
mi := &file_wtclientrpc_wtclient_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -562,7 +763,7 @@ func (x *TowerSessionInfo) String() string {
func (*TowerSessionInfo) ProtoMessage() {}
func (x *TowerSessionInfo) ProtoReflect() protoreflect.Message {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[7]
mi := &file_wtclientrpc_wtclient_proto_msgTypes[11]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -575,7 +776,7 @@ func (x *TowerSessionInfo) ProtoReflect() protoreflect.Message {
// Deprecated: Use TowerSessionInfo.ProtoReflect.Descriptor instead.
func (*TowerSessionInfo) Descriptor() ([]byte, []int) {
return file_wtclientrpc_wtclient_proto_rawDescGZIP(), []int{7}
return file_wtclientrpc_wtclient_proto_rawDescGZIP(), []int{11}
}
func (x *TowerSessionInfo) GetActiveSessionCandidate() bool {
@ -621,7 +822,7 @@ type ListTowersRequest struct {
func (x *ListTowersRequest) Reset() {
*x = ListTowersRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[8]
mi := &file_wtclientrpc_wtclient_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -634,7 +835,7 @@ func (x *ListTowersRequest) String() string {
func (*ListTowersRequest) ProtoMessage() {}
func (x *ListTowersRequest) ProtoReflect() protoreflect.Message {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[8]
mi := &file_wtclientrpc_wtclient_proto_msgTypes[12]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -647,7 +848,7 @@ func (x *ListTowersRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListTowersRequest.ProtoReflect.Descriptor instead.
func (*ListTowersRequest) Descriptor() ([]byte, []int) {
return file_wtclientrpc_wtclient_proto_rawDescGZIP(), []int{8}
return file_wtclientrpc_wtclient_proto_rawDescGZIP(), []int{12}
}
func (x *ListTowersRequest) GetIncludeSessions() bool {
@ -676,7 +877,7 @@ type ListTowersResponse struct {
func (x *ListTowersResponse) Reset() {
*x = ListTowersResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[9]
mi := &file_wtclientrpc_wtclient_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -689,7 +890,7 @@ func (x *ListTowersResponse) String() string {
func (*ListTowersResponse) ProtoMessage() {}
func (x *ListTowersResponse) ProtoReflect() protoreflect.Message {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[9]
mi := &file_wtclientrpc_wtclient_proto_msgTypes[13]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -702,7 +903,7 @@ func (x *ListTowersResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListTowersResponse.ProtoReflect.Descriptor instead.
func (*ListTowersResponse) Descriptor() ([]byte, []int) {
return file_wtclientrpc_wtclient_proto_rawDescGZIP(), []int{9}
return file_wtclientrpc_wtclient_proto_rawDescGZIP(), []int{13}
}
func (x *ListTowersResponse) GetTowers() []*Tower {
@ -721,7 +922,7 @@ type StatsRequest struct {
func (x *StatsRequest) Reset() {
*x = StatsRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[10]
mi := &file_wtclientrpc_wtclient_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -734,7 +935,7 @@ func (x *StatsRequest) String() string {
func (*StatsRequest) ProtoMessage() {}
func (x *StatsRequest) ProtoReflect() protoreflect.Message {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[10]
mi := &file_wtclientrpc_wtclient_proto_msgTypes[14]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -747,7 +948,7 @@ func (x *StatsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use StatsRequest.ProtoReflect.Descriptor instead.
func (*StatsRequest) Descriptor() ([]byte, []int) {
return file_wtclientrpc_wtclient_proto_rawDescGZIP(), []int{10}
return file_wtclientrpc_wtclient_proto_rawDescGZIP(), []int{14}
}
type StatsResponse struct {
@ -773,7 +974,7 @@ type StatsResponse struct {
func (x *StatsResponse) Reset() {
*x = StatsResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[11]
mi := &file_wtclientrpc_wtclient_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -786,7 +987,7 @@ func (x *StatsResponse) String() string {
func (*StatsResponse) ProtoMessage() {}
func (x *StatsResponse) ProtoReflect() protoreflect.Message {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[11]
mi := &file_wtclientrpc_wtclient_proto_msgTypes[15]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -799,7 +1000,7 @@ func (x *StatsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use StatsResponse.ProtoReflect.Descriptor instead.
func (*StatsResponse) Descriptor() ([]byte, []int) {
return file_wtclientrpc_wtclient_proto_rawDescGZIP(), []int{11}
return file_wtclientrpc_wtclient_proto_rawDescGZIP(), []int{15}
}
func (x *StatsResponse) GetNumBackups() uint32 {
@ -849,7 +1050,7 @@ type PolicyRequest struct {
func (x *PolicyRequest) Reset() {
*x = PolicyRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[12]
mi := &file_wtclientrpc_wtclient_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -862,7 +1063,7 @@ func (x *PolicyRequest) String() string {
func (*PolicyRequest) ProtoMessage() {}
func (x *PolicyRequest) ProtoReflect() protoreflect.Message {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[12]
mi := &file_wtclientrpc_wtclient_proto_msgTypes[16]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -875,7 +1076,7 @@ func (x *PolicyRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use PolicyRequest.ProtoReflect.Descriptor instead.
func (*PolicyRequest) Descriptor() ([]byte, []int) {
return file_wtclientrpc_wtclient_proto_rawDescGZIP(), []int{12}
return file_wtclientrpc_wtclient_proto_rawDescGZIP(), []int{16}
}
func (x *PolicyRequest) GetPolicyType() PolicyType {
@ -907,7 +1108,7 @@ type PolicyResponse struct {
func (x *PolicyResponse) Reset() {
*x = PolicyResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[13]
mi := &file_wtclientrpc_wtclient_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -920,7 +1121,7 @@ func (x *PolicyResponse) String() string {
func (*PolicyResponse) ProtoMessage() {}
func (x *PolicyResponse) ProtoReflect() protoreflect.Message {
mi := &file_wtclientrpc_wtclient_proto_msgTypes[13]
mi := &file_wtclientrpc_wtclient_proto_msgTypes[17]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -933,7 +1134,7 @@ func (x *PolicyResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use PolicyResponse.ProtoReflect.Descriptor instead.
func (*PolicyResponse) Descriptor() ([]byte, []int) {
return file_wtclientrpc_wtclient_proto_rawDescGZIP(), []int{13}
return file_wtclientrpc_wtclient_proto_rawDescGZIP(), []int{17}
}
func (x *PolicyResponse) GetMaxUpdates() uint32 {
@ -975,141 +1176,167 @@ var file_wtclientrpc_wtclient_proto_rawDesc = []byte{
0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28,
0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x15, 0x0a, 0x13, 0x52, 0x65,
0x6d, 0x6f, 0x76, 0x65, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x22, 0x96, 0x01, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x49, 0x6e,
0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62,
0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65,
0x79, 0x12, 0x29, 0x0a, 0x10, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x73, 0x65, 0x73,
0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x69, 0x6e, 0x63,
0x6c, 0x75, 0x64, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x3c, 0x0a, 0x1a,
0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x65, 0x78, 0x68, 0x61, 0x75, 0x73, 0x74, 0x65,
0x64, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08,
0x52, 0x18, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x45, 0x78, 0x68, 0x61, 0x75, 0x73, 0x74,
0x65, 0x64, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xe0, 0x01, 0x0a, 0x0c, 0x54,
0x6f, 0x77, 0x65, 0x72, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x6e,
0x75, 0x6d, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d,
0x52, 0x0a, 0x6e, 0x75, 0x6d, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x12, 0x2e, 0x0a, 0x13,
0x6e, 0x75, 0x6d, 0x5f, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x62, 0x61, 0x63, 0x6b,
0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x11, 0x6e, 0x75, 0x6d, 0x50, 0x65,
0x6e, 0x64, 0x69, 0x6e, 0x67, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x12, 0x1f, 0x0a, 0x0b,
0x6d, 0x61, 0x78, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28,
0x0d, 0x52, 0x0a, 0x6d, 0x61, 0x78, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x12, 0x2f, 0x0a,
0x12, 0x73, 0x77, 0x65, 0x65, 0x70, 0x5f, 0x73, 0x61, 0x74, 0x5f, 0x70, 0x65, 0x72, 0x5f, 0x62,
0x79, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0f, 0x73,
0x77, 0x65, 0x65, 0x70, 0x53, 0x61, 0x74, 0x50, 0x65, 0x72, 0x42, 0x79, 0x74, 0x65, 0x12, 0x2d,
0x0a, 0x13, 0x73, 0x77, 0x65, 0x65, 0x70, 0x5f, 0x73, 0x61, 0x74, 0x5f, 0x70, 0x65, 0x72, 0x5f,
0x76, 0x62, 0x79, 0x74, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x10, 0x73, 0x77, 0x65,
0x65, 0x70, 0x53, 0x61, 0x74, 0x50, 0x65, 0x72, 0x56, 0x62, 0x79, 0x74, 0x65, 0x22, 0x9f, 0x02,
0x0a, 0x05, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65,
0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x12,
0x1c, 0x0a, 0x09, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03,
0x28, 0x09, 0x52, 0x09, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x3c, 0x0a,
0x18, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f,
0x63, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x42,
0x02, 0x18, 0x01, 0x52, 0x16, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69,
0x6f, 0x6e, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x12, 0x25, 0x0a, 0x0c, 0x6e,
0x75, 0x6d, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28,
0x0d, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0b, 0x6e, 0x75, 0x6d, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f,
0x6e, 0x73, 0x12, 0x39, 0x0a, 0x08, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x05,
0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72,
0x70, 0x63, 0x2e, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x42,
0x02, 0x18, 0x01, 0x52, 0x08, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x40, 0x0a,
0x0c, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x06, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70,
0x63, 0x2e, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x6e,
0x66, 0x6f, 0x52, 0x0b, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x22,
0xe0, 0x01, 0x0a, 0x10, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e,
0x49, 0x6e, 0x66, 0x6f, 0x12, 0x38, 0x0a, 0x18, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x73,
0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x16, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x53, 0x65,
0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x12, 0x21,
0x0a, 0x0c, 0x6e, 0x75, 0x6d, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02,
0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x6e, 0x75, 0x6d, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e,
0x73, 0x12, 0x35, 0x0a, 0x08, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70,
0x63, 0x2e, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x08,
0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x70, 0x6f, 0x6c, 0x69,
0x63, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e,
0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x6f, 0x6c, 0x69,
0x63, 0x79, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0a, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x54, 0x79,
0x70, 0x65, 0x22, 0x7c, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x73,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x69, 0x6e, 0x63, 0x6c, 0x75,
0x64, 0x65, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28,
0x08, 0x52, 0x0f, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f,
0x6e, 0x73, 0x12, 0x3c, 0x0a, 0x1a, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x65, 0x78,
0x68, 0x61, 0x75, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73,
0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x18, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x45,
0x78, 0x68, 0x61, 0x75, 0x73, 0x74, 0x65, 0x64, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73,
0x22, 0x40, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x73, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x74, 0x6f, 0x77, 0x65, 0x72, 0x73,
0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e,
0x74, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x06, 0x74, 0x6f, 0x77, 0x65,
0x72, 0x73, 0x22, 0x0e, 0x0a, 0x0c, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x22, 0xf8, 0x01, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x6e, 0x75, 0x6d, 0x5f, 0x62, 0x61, 0x63, 0x6b,
0x75, 0x70, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x6e, 0x75, 0x6d, 0x42, 0x61,
0x63, 0x6b, 0x75, 0x70, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x6e, 0x75, 0x6d, 0x5f, 0x70, 0x65, 0x6e,
0x64, 0x69, 0x6e, 0x67, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x01,
0x28, 0x0d, 0x52, 0x11, 0x6e, 0x75, 0x6d, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x42, 0x61,
0x63, 0x6b, 0x75, 0x70, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x6e, 0x75, 0x6d, 0x5f, 0x66, 0x61, 0x69,
0x6c, 0x65, 0x64, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28,
0x0d, 0x52, 0x10, 0x6e, 0x75, 0x6d, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x42, 0x61, 0x63, 0x6b,
0x75, 0x70, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x6e, 0x75, 0x6d, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69,
0x6f, 0x6e, 0x73, 0x5f, 0x61, 0x63, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01,
0x28, 0x0d, 0x52, 0x13, 0x6e, 0x75, 0x6d, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x41,
0x63, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x16, 0x6e, 0x75, 0x6d, 0x5f, 0x73,
0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x5f, 0x65, 0x78, 0x68, 0x61, 0x75, 0x73, 0x74, 0x65,
0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x14, 0x6e, 0x75, 0x6d, 0x53, 0x65, 0x73, 0x73,
0x69, 0x6f, 0x6e, 0x73, 0x45, 0x78, 0x68, 0x61, 0x75, 0x73, 0x74, 0x65, 0x64, 0x22, 0x49, 0x0a,
0x0d, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38,
0x0a, 0x0b, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20,
0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70,
0x63, 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0a, 0x70, 0x6f,
0x6c, 0x69, 0x63, 0x79, 0x54, 0x79, 0x70, 0x65, 0x22, 0x91, 0x01, 0x0a, 0x0e, 0x50, 0x6f, 0x6c,
0x69, 0x63, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x6d,
0x61, 0x78, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d,
0x52, 0x0a, 0x6d, 0x61, 0x78, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x12, 0x2f, 0x0a, 0x12,
0x73, 0x77, 0x65, 0x65, 0x70, 0x5f, 0x73, 0x61, 0x74, 0x5f, 0x70, 0x65, 0x72, 0x5f, 0x62, 0x79,
0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0f, 0x73, 0x77,
0x65, 0x65, 0x70, 0x53, 0x61, 0x74, 0x50, 0x65, 0x72, 0x42, 0x79, 0x74, 0x65, 0x12, 0x2d, 0x0a,
0x13, 0x73, 0x77, 0x65, 0x65, 0x70, 0x5f, 0x73, 0x61, 0x74, 0x5f, 0x70, 0x65, 0x72, 0x5f, 0x76,
0x62, 0x79, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x10, 0x73, 0x77, 0x65, 0x65,
0x70, 0x53, 0x61, 0x74, 0x50, 0x65, 0x72, 0x56, 0x62, 0x79, 0x74, 0x65, 0x2a, 0x31, 0x0a, 0x0a,
0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x4c, 0x45,
0x47, 0x41, 0x43, 0x59, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x4e, 0x43, 0x48, 0x4f, 0x52,
0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x54, 0x41, 0x50, 0x52, 0x4f, 0x4f, 0x54, 0x10, 0x02, 0x32,
0xc5, 0x03, 0x0a, 0x10, 0x57, 0x61, 0x74, 0x63, 0x68, 0x74, 0x6f, 0x77, 0x65, 0x72, 0x43, 0x6c,
0x69, 0x65, 0x6e, 0x74, 0x12, 0x47, 0x0a, 0x08, 0x41, 0x64, 0x64, 0x54, 0x6f, 0x77, 0x65, 0x72,
0x12, 0x1c, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41,
0x64, 0x64, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d,
0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64,
0x54, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x50, 0x0a,
0x0b, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x12, 0x1f, 0x2e, 0x77,
0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76,
0x65, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e,
0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x6d, 0x6f,
0x76, 0x65, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x4d, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x73, 0x12, 0x1e, 0x2e,
0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74,
0x54, 0x6f, 0x77, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e,
0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74,
0x54, 0x6f, 0x77, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44,
0x0a, 0x0c, 0x47, 0x65, 0x74, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x20,
0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x65, 0x74,
0x54, 0x6f, 0x77, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x12, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x54,
0x6f, 0x77, 0x65, 0x72, 0x12, 0x3e, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x19, 0x2e,
0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x61, 0x74,
0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69,
0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x06, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x1a,
0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x6f, 0x6c,
0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x77, 0x74, 0x63,
0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x33, 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75,
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6e,
0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6e, 0x64, 0x2f, 0x6c, 0x6e, 0x72, 0x70, 0x63,
0x2f, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x33,
0x65, 0x22, 0x30, 0x0a, 0x16, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x54,
0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70,
0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x75, 0x62,
0x6b, 0x65, 0x79, 0x22, 0x31, 0x0a, 0x17, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74,
0x65, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16,
0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x38, 0x0a, 0x17, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e,
0x61, 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18,
0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64,
0x22, 0x32, 0x0a, 0x18, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x53, 0x65, 0x73,
0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06,
0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74,
0x61, 0x74, 0x75, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x54, 0x6f, 0x77, 0x65,
0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06,
0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x75,
0x62, 0x6b, 0x65, 0x79, 0x12, 0x29, 0x0a, 0x10, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f,
0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f,
0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12,
0x3c, 0x0a, 0x1a, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x65, 0x78, 0x68, 0x61, 0x75,
0x73, 0x74, 0x65, 0x64, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20,
0x01, 0x28, 0x08, 0x52, 0x18, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x45, 0x78, 0x68, 0x61,
0x75, 0x73, 0x74, 0x65, 0x64, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xf0, 0x01,
0x0a, 0x0c, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1f,
0x0a, 0x0b, 0x6e, 0x75, 0x6d, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x18, 0x01, 0x20,
0x01, 0x28, 0x0d, 0x52, 0x0a, 0x6e, 0x75, 0x6d, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x12,
0x2e, 0x0a, 0x13, 0x6e, 0x75, 0x6d, 0x5f, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x62,
0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x11, 0x6e, 0x75,
0x6d, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x12,
0x1f, 0x0a, 0x0b, 0x6d, 0x61, 0x78, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x18, 0x03,
0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x6d, 0x61, 0x78, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73,
0x12, 0x2f, 0x0a, 0x12, 0x73, 0x77, 0x65, 0x65, 0x70, 0x5f, 0x73, 0x61, 0x74, 0x5f, 0x70, 0x65,
0x72, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x02, 0x18, 0x01,
0x52, 0x0f, 0x73, 0x77, 0x65, 0x65, 0x70, 0x53, 0x61, 0x74, 0x50, 0x65, 0x72, 0x42, 0x79, 0x74,
0x65, 0x12, 0x2d, 0x0a, 0x13, 0x73, 0x77, 0x65, 0x65, 0x70, 0x5f, 0x73, 0x61, 0x74, 0x5f, 0x70,
0x65, 0x72, 0x5f, 0x76, 0x62, 0x79, 0x74, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x10,
0x73, 0x77, 0x65, 0x65, 0x70, 0x53, 0x61, 0x74, 0x50, 0x65, 0x72, 0x56, 0x62, 0x79, 0x74, 0x65,
0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64,
0x22, 0x9f, 0x02, 0x0a, 0x05, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75,
0x62, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x75, 0x62, 0x6b,
0x65, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18,
0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73,
0x12, 0x3c, 0x0a, 0x18, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69,
0x6f, 0x6e, 0x5f, 0x63, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01,
0x28, 0x08, 0x42, 0x02, 0x18, 0x01, 0x52, 0x16, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x53, 0x65,
0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x12, 0x25,
0x0a, 0x0c, 0x6e, 0x75, 0x6d, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04,
0x20, 0x01, 0x28, 0x0d, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0b, 0x6e, 0x75, 0x6d, 0x53, 0x65, 0x73,
0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x39, 0x0a, 0x08, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e,
0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65,
0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x53, 0x65, 0x73, 0x73, 0x69,
0x6f, 0x6e, 0x42, 0x02, 0x18, 0x01, 0x52, 0x08, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73,
0x12, 0x40, 0x0a, 0x0c, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x66, 0x6f,
0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e,
0x74, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f,
0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0b, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x6e,
0x66, 0x6f, 0x22, 0xe0, 0x01, 0x0a, 0x10, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x53, 0x65, 0x73, 0x73,
0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x38, 0x0a, 0x18, 0x61, 0x63, 0x74, 0x69, 0x76,
0x65, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x61, 0x6e, 0x64, 0x69, 0x64,
0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x16, 0x61, 0x63, 0x74, 0x69, 0x76,
0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74,
0x65, 0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x75, 0x6d, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e,
0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x6e, 0x75, 0x6d, 0x53, 0x65, 0x73, 0x73,
0x69, 0x6f, 0x6e, 0x73, 0x12, 0x35, 0x0a, 0x08, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73,
0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e,
0x74, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f,
0x6e, 0x52, 0x08, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x70,
0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e,
0x32, 0x17, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x50,
0x6f, 0x6c, 0x69, 0x63, 0x79, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0a, 0x70, 0x6f, 0x6c, 0x69, 0x63,
0x79, 0x54, 0x79, 0x70, 0x65, 0x22, 0x7c, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x54, 0x6f, 0x77,
0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x69, 0x6e,
0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01,
0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x53, 0x65, 0x73,
0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x3c, 0x0a, 0x1a, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65,
0x5f, 0x65, 0x78, 0x68, 0x61, 0x75, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69,
0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x18, 0x65, 0x78, 0x63, 0x6c, 0x75,
0x64, 0x65, 0x45, 0x78, 0x68, 0x61, 0x75, 0x73, 0x74, 0x65, 0x64, 0x53, 0x65, 0x73, 0x73, 0x69,
0x6f, 0x6e, 0x73, 0x22, 0x40, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x54, 0x6f, 0x77, 0x65, 0x72,
0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x74, 0x6f, 0x77,
0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x77, 0x74, 0x63, 0x6c,
0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x06, 0x74,
0x6f, 0x77, 0x65, 0x72, 0x73, 0x22, 0x0e, 0x0a, 0x0c, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xf8, 0x01, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x6e, 0x75, 0x6d, 0x5f, 0x62,
0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x6e, 0x75,
0x6d, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x6e, 0x75, 0x6d, 0x5f,
0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x18,
0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x11, 0x6e, 0x75, 0x6d, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e,
0x67, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x6e, 0x75, 0x6d, 0x5f,
0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x18, 0x03,
0x20, 0x01, 0x28, 0x0d, 0x52, 0x10, 0x6e, 0x75, 0x6d, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x42,
0x61, 0x63, 0x6b, 0x75, 0x70, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x6e, 0x75, 0x6d, 0x5f, 0x73, 0x65,
0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x5f, 0x61, 0x63, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x18,
0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x13, 0x6e, 0x75, 0x6d, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f,
0x6e, 0x73, 0x41, 0x63, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x16, 0x6e, 0x75,
0x6d, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x5f, 0x65, 0x78, 0x68, 0x61, 0x75,
0x73, 0x74, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x14, 0x6e, 0x75, 0x6d, 0x53,
0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x78, 0x68, 0x61, 0x75, 0x73, 0x74, 0x65, 0x64,
0x22, 0x49, 0x0a, 0x0d, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x12, 0x38, 0x0a, 0x0b, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e,
0x74, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x54, 0x79, 0x70, 0x65, 0x52,
0x0a, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x54, 0x79, 0x70, 0x65, 0x22, 0x91, 0x01, 0x0a, 0x0e,
0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f,
0x0a, 0x0b, 0x6d, 0x61, 0x78, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20,
0x01, 0x28, 0x0d, 0x52, 0x0a, 0x6d, 0x61, 0x78, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x12,
0x2f, 0x0a, 0x12, 0x73, 0x77, 0x65, 0x65, 0x70, 0x5f, 0x73, 0x61, 0x74, 0x5f, 0x70, 0x65, 0x72,
0x5f, 0x62, 0x79, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x02, 0x18, 0x01, 0x52,
0x0f, 0x73, 0x77, 0x65, 0x65, 0x70, 0x53, 0x61, 0x74, 0x50, 0x65, 0x72, 0x42, 0x79, 0x74, 0x65,
0x12, 0x2d, 0x0a, 0x13, 0x73, 0x77, 0x65, 0x65, 0x70, 0x5f, 0x73, 0x61, 0x74, 0x5f, 0x70, 0x65,
0x72, 0x5f, 0x76, 0x62, 0x79, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x10, 0x73,
0x77, 0x65, 0x65, 0x70, 0x53, 0x61, 0x74, 0x50, 0x65, 0x72, 0x56, 0x62, 0x79, 0x74, 0x65, 0x2a,
0x31, 0x0a, 0x0a, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0a, 0x0a,
0x06, 0x4c, 0x45, 0x47, 0x41, 0x43, 0x59, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x4e, 0x43,
0x48, 0x4f, 0x52, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x54, 0x41, 0x50, 0x52, 0x4f, 0x4f, 0x54,
0x10, 0x02, 0x32, 0x84, 0x05, 0x0a, 0x10, 0x57, 0x61, 0x74, 0x63, 0x68, 0x74, 0x6f, 0x77, 0x65,
0x72, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x47, 0x0a, 0x08, 0x41, 0x64, 0x64, 0x54, 0x6f,
0x77, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70,
0x63, 0x2e, 0x41, 0x64, 0x64, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x1d, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e,
0x41, 0x64, 0x64, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x50, 0x0a, 0x0b, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x12,
0x1f, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65,
0x6d, 0x6f, 0x76, 0x65, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x20, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52,
0x65, 0x6d, 0x6f, 0x76, 0x65, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x5c, 0x0a, 0x0f, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65,
0x54, 0x6f, 0x77, 0x65, 0x72, 0x12, 0x23, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74,
0x72, 0x70, 0x63, 0x2e, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x54, 0x6f,
0x77, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x77, 0x74, 0x63,
0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76,
0x61, 0x74, 0x65, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x5f, 0x0a, 0x10, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x53, 0x65, 0x73,
0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72,
0x70, 0x63, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x53, 0x65, 0x73, 0x73,
0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x77, 0x74, 0x63,
0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61,
0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x4d, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x73, 0x12,
0x1e, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69,
0x73, 0x74, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x1f, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69,
0x73, 0x74, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x44, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f,
0x12, 0x20, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x47,
0x65, 0x74, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x12, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63,
0x2e, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x12, 0x3e, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12,
0x19, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74,
0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x77, 0x74, 0x63,
0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x06, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79,
0x12, 0x1a, 0x2e, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x50,
0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x77,
0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63,
0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x33, 0x5a, 0x31, 0x67, 0x69, 0x74,
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e,
0x67, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6e, 0x64, 0x2f, 0x6c, 0x6e, 0x72,
0x70, 0x63, 0x2f, 0x77, 0x74, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x72, 0x70, 0x63, 0x62, 0x06,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@ -1125,45 +1352,53 @@ func file_wtclientrpc_wtclient_proto_rawDescGZIP() []byte {
}
var file_wtclientrpc_wtclient_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_wtclientrpc_wtclient_proto_msgTypes = make([]protoimpl.MessageInfo, 14)
var file_wtclientrpc_wtclient_proto_msgTypes = make([]protoimpl.MessageInfo, 18)
var file_wtclientrpc_wtclient_proto_goTypes = []interface{}{
(PolicyType)(0), // 0: wtclientrpc.PolicyType
(*AddTowerRequest)(nil), // 1: wtclientrpc.AddTowerRequest
(*AddTowerResponse)(nil), // 2: wtclientrpc.AddTowerResponse
(*RemoveTowerRequest)(nil), // 3: wtclientrpc.RemoveTowerRequest
(*RemoveTowerResponse)(nil), // 4: wtclientrpc.RemoveTowerResponse
(*GetTowerInfoRequest)(nil), // 5: wtclientrpc.GetTowerInfoRequest
(*TowerSession)(nil), // 6: wtclientrpc.TowerSession
(*Tower)(nil), // 7: wtclientrpc.Tower
(*TowerSessionInfo)(nil), // 8: wtclientrpc.TowerSessionInfo
(*ListTowersRequest)(nil), // 9: wtclientrpc.ListTowersRequest
(*ListTowersResponse)(nil), // 10: wtclientrpc.ListTowersResponse
(*StatsRequest)(nil), // 11: wtclientrpc.StatsRequest
(*StatsResponse)(nil), // 12: wtclientrpc.StatsResponse
(*PolicyRequest)(nil), // 13: wtclientrpc.PolicyRequest
(*PolicyResponse)(nil), // 14: wtclientrpc.PolicyResponse
(PolicyType)(0), // 0: wtclientrpc.PolicyType
(*AddTowerRequest)(nil), // 1: wtclientrpc.AddTowerRequest
(*AddTowerResponse)(nil), // 2: wtclientrpc.AddTowerResponse
(*RemoveTowerRequest)(nil), // 3: wtclientrpc.RemoveTowerRequest
(*RemoveTowerResponse)(nil), // 4: wtclientrpc.RemoveTowerResponse
(*DeactivateTowerRequest)(nil), // 5: wtclientrpc.DeactivateTowerRequest
(*DeactivateTowerResponse)(nil), // 6: wtclientrpc.DeactivateTowerResponse
(*TerminateSessionRequest)(nil), // 7: wtclientrpc.TerminateSessionRequest
(*TerminateSessionResponse)(nil), // 8: wtclientrpc.TerminateSessionResponse
(*GetTowerInfoRequest)(nil), // 9: wtclientrpc.GetTowerInfoRequest
(*TowerSession)(nil), // 10: wtclientrpc.TowerSession
(*Tower)(nil), // 11: wtclientrpc.Tower
(*TowerSessionInfo)(nil), // 12: wtclientrpc.TowerSessionInfo
(*ListTowersRequest)(nil), // 13: wtclientrpc.ListTowersRequest
(*ListTowersResponse)(nil), // 14: wtclientrpc.ListTowersResponse
(*StatsRequest)(nil), // 15: wtclientrpc.StatsRequest
(*StatsResponse)(nil), // 16: wtclientrpc.StatsResponse
(*PolicyRequest)(nil), // 17: wtclientrpc.PolicyRequest
(*PolicyResponse)(nil), // 18: wtclientrpc.PolicyResponse
}
var file_wtclientrpc_wtclient_proto_depIdxs = []int32{
6, // 0: wtclientrpc.Tower.sessions:type_name -> wtclientrpc.TowerSession
8, // 1: wtclientrpc.Tower.session_info:type_name -> wtclientrpc.TowerSessionInfo
6, // 2: wtclientrpc.TowerSessionInfo.sessions:type_name -> wtclientrpc.TowerSession
10, // 0: wtclientrpc.Tower.sessions:type_name -> wtclientrpc.TowerSession
12, // 1: wtclientrpc.Tower.session_info:type_name -> wtclientrpc.TowerSessionInfo
10, // 2: wtclientrpc.TowerSessionInfo.sessions:type_name -> wtclientrpc.TowerSession
0, // 3: wtclientrpc.TowerSessionInfo.policy_type:type_name -> wtclientrpc.PolicyType
7, // 4: wtclientrpc.ListTowersResponse.towers:type_name -> wtclientrpc.Tower
11, // 4: wtclientrpc.ListTowersResponse.towers:type_name -> wtclientrpc.Tower
0, // 5: wtclientrpc.PolicyRequest.policy_type:type_name -> wtclientrpc.PolicyType
1, // 6: wtclientrpc.WatchtowerClient.AddTower:input_type -> wtclientrpc.AddTowerRequest
3, // 7: wtclientrpc.WatchtowerClient.RemoveTower:input_type -> wtclientrpc.RemoveTowerRequest
9, // 8: wtclientrpc.WatchtowerClient.ListTowers:input_type -> wtclientrpc.ListTowersRequest
5, // 9: wtclientrpc.WatchtowerClient.GetTowerInfo:input_type -> wtclientrpc.GetTowerInfoRequest
11, // 10: wtclientrpc.WatchtowerClient.Stats:input_type -> wtclientrpc.StatsRequest
13, // 11: wtclientrpc.WatchtowerClient.Policy:input_type -> wtclientrpc.PolicyRequest
2, // 12: wtclientrpc.WatchtowerClient.AddTower:output_type -> wtclientrpc.AddTowerResponse
4, // 13: wtclientrpc.WatchtowerClient.RemoveTower:output_type -> wtclientrpc.RemoveTowerResponse
10, // 14: wtclientrpc.WatchtowerClient.ListTowers:output_type -> wtclientrpc.ListTowersResponse
7, // 15: wtclientrpc.WatchtowerClient.GetTowerInfo:output_type -> wtclientrpc.Tower
12, // 16: wtclientrpc.WatchtowerClient.Stats:output_type -> wtclientrpc.StatsResponse
14, // 17: wtclientrpc.WatchtowerClient.Policy:output_type -> wtclientrpc.PolicyResponse
12, // [12:18] is the sub-list for method output_type
6, // [6:12] is the sub-list for method input_type
5, // 8: wtclientrpc.WatchtowerClient.DeactivateTower:input_type -> wtclientrpc.DeactivateTowerRequest
7, // 9: wtclientrpc.WatchtowerClient.TerminateSession:input_type -> wtclientrpc.TerminateSessionRequest
13, // 10: wtclientrpc.WatchtowerClient.ListTowers:input_type -> wtclientrpc.ListTowersRequest
9, // 11: wtclientrpc.WatchtowerClient.GetTowerInfo:input_type -> wtclientrpc.GetTowerInfoRequest
15, // 12: wtclientrpc.WatchtowerClient.Stats:input_type -> wtclientrpc.StatsRequest
17, // 13: wtclientrpc.WatchtowerClient.Policy:input_type -> wtclientrpc.PolicyRequest
2, // 14: wtclientrpc.WatchtowerClient.AddTower:output_type -> wtclientrpc.AddTowerResponse
4, // 15: wtclientrpc.WatchtowerClient.RemoveTower:output_type -> wtclientrpc.RemoveTowerResponse
6, // 16: wtclientrpc.WatchtowerClient.DeactivateTower:output_type -> wtclientrpc.DeactivateTowerResponse
8, // 17: wtclientrpc.WatchtowerClient.TerminateSession:output_type -> wtclientrpc.TerminateSessionResponse
14, // 18: wtclientrpc.WatchtowerClient.ListTowers:output_type -> wtclientrpc.ListTowersResponse
11, // 19: wtclientrpc.WatchtowerClient.GetTowerInfo:output_type -> wtclientrpc.Tower
16, // 20: wtclientrpc.WatchtowerClient.Stats:output_type -> wtclientrpc.StatsResponse
18, // 21: wtclientrpc.WatchtowerClient.Policy:output_type -> wtclientrpc.PolicyResponse
14, // [14:22] is the sub-list for method output_type
6, // [6:14] is the sub-list for method input_type
6, // [6:6] is the sub-list for extension type_name
6, // [6:6] is the sub-list for extension extendee
0, // [0:6] is the sub-list for field type_name
@ -1224,7 +1459,7 @@ func file_wtclientrpc_wtclient_proto_init() {
}
}
file_wtclientrpc_wtclient_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetTowerInfoRequest); i {
switch v := v.(*DeactivateTowerRequest); i {
case 0:
return &v.state
case 1:
@ -1236,7 +1471,7 @@ func file_wtclientrpc_wtclient_proto_init() {
}
}
file_wtclientrpc_wtclient_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*TowerSession); i {
switch v := v.(*DeactivateTowerResponse); i {
case 0:
return &v.state
case 1:
@ -1248,7 +1483,7 @@ func file_wtclientrpc_wtclient_proto_init() {
}
}
file_wtclientrpc_wtclient_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Tower); i {
switch v := v.(*TerminateSessionRequest); i {
case 0:
return &v.state
case 1:
@ -1260,7 +1495,7 @@ func file_wtclientrpc_wtclient_proto_init() {
}
}
file_wtclientrpc_wtclient_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*TowerSessionInfo); i {
switch v := v.(*TerminateSessionResponse); i {
case 0:
return &v.state
case 1:
@ -1272,7 +1507,7 @@ func file_wtclientrpc_wtclient_proto_init() {
}
}
file_wtclientrpc_wtclient_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ListTowersRequest); i {
switch v := v.(*GetTowerInfoRequest); i {
case 0:
return &v.state
case 1:
@ -1284,7 +1519,7 @@ func file_wtclientrpc_wtclient_proto_init() {
}
}
file_wtclientrpc_wtclient_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ListTowersResponse); i {
switch v := v.(*TowerSession); i {
case 0:
return &v.state
case 1:
@ -1296,7 +1531,7 @@ func file_wtclientrpc_wtclient_proto_init() {
}
}
file_wtclientrpc_wtclient_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*StatsRequest); i {
switch v := v.(*Tower); i {
case 0:
return &v.state
case 1:
@ -1308,7 +1543,7 @@ func file_wtclientrpc_wtclient_proto_init() {
}
}
file_wtclientrpc_wtclient_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*StatsResponse); i {
switch v := v.(*TowerSessionInfo); i {
case 0:
return &v.state
case 1:
@ -1320,7 +1555,7 @@ func file_wtclientrpc_wtclient_proto_init() {
}
}
file_wtclientrpc_wtclient_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*PolicyRequest); i {
switch v := v.(*ListTowersRequest); i {
case 0:
return &v.state
case 1:
@ -1332,6 +1567,54 @@ func file_wtclientrpc_wtclient_proto_init() {
}
}
file_wtclientrpc_wtclient_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ListTowersResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_wtclientrpc_wtclient_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*StatsRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_wtclientrpc_wtclient_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*StatsResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_wtclientrpc_wtclient_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*PolicyRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_wtclientrpc_wtclient_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*PolicyResponse); i {
case 0:
return &v.state
@ -1350,7 +1633,7 @@ func file_wtclientrpc_wtclient_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_wtclientrpc_wtclient_proto_rawDesc,
NumEnums: 1,
NumMessages: 14,
NumMessages: 18,
NumExtensions: 0,
NumServices: 1,
},

View File

@ -135,6 +135,110 @@ func local_request_WatchtowerClient_RemoveTower_0(ctx context.Context, marshaler
}
func request_WatchtowerClient_DeactivateTower_0(ctx context.Context, marshaler runtime.Marshaler, client WatchtowerClientClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq DeactivateTowerRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["pubkey"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "pubkey")
}
protoReq.Pubkey, err = runtime.Bytes(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "pubkey", err)
}
msg, err := client.DeactivateTower(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_WatchtowerClient_DeactivateTower_0(ctx context.Context, marshaler runtime.Marshaler, server WatchtowerClientServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq DeactivateTowerRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["pubkey"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "pubkey")
}
protoReq.Pubkey, err = runtime.Bytes(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "pubkey", err)
}
msg, err := server.DeactivateTower(ctx, &protoReq)
return msg, metadata, err
}
func request_WatchtowerClient_TerminateSession_0(ctx context.Context, marshaler runtime.Marshaler, client WatchtowerClientClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq TerminateSessionRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["session_id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "session_id")
}
protoReq.SessionId, err = runtime.Bytes(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "session_id", err)
}
msg, err := client.TerminateSession(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_WatchtowerClient_TerminateSession_0(ctx context.Context, marshaler runtime.Marshaler, server WatchtowerClientServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq TerminateSessionRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["session_id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "session_id")
}
protoReq.SessionId, err = runtime.Bytes(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "session_id", err)
}
msg, err := server.TerminateSession(ctx, &protoReq)
return msg, metadata, err
}
var (
filter_WatchtowerClient_ListTowers_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
@ -347,6 +451,52 @@ func RegisterWatchtowerClientHandlerServer(ctx context.Context, mux *runtime.Ser
})
mux.Handle("POST", pattern_WatchtowerClient_DeactivateTower_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/wtclientrpc.WatchtowerClient/DeactivateTower", runtime.WithHTTPPathPattern("/v2/watchtower/client/tower/deactivate/{pubkey}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_WatchtowerClient_DeactivateTower_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_WatchtowerClient_DeactivateTower_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_WatchtowerClient_TerminateSession_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/wtclientrpc.WatchtowerClient/TerminateSession", runtime.WithHTTPPathPattern("/v2/watchtower/client/sessions/terminate/{session_id}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_WatchtowerClient_TerminateSession_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_WatchtowerClient_TerminateSession_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_WatchtowerClient_ListTowers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@ -520,6 +670,46 @@ func RegisterWatchtowerClientHandlerClient(ctx context.Context, mux *runtime.Ser
})
mux.Handle("POST", pattern_WatchtowerClient_DeactivateTower_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/wtclientrpc.WatchtowerClient/DeactivateTower", runtime.WithHTTPPathPattern("/v2/watchtower/client/tower/deactivate/{pubkey}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_WatchtowerClient_DeactivateTower_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_WatchtowerClient_DeactivateTower_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_WatchtowerClient_TerminateSession_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/wtclientrpc.WatchtowerClient/TerminateSession", runtime.WithHTTPPathPattern("/v2/watchtower/client/sessions/terminate/{session_id}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_WatchtowerClient_TerminateSession_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_WatchtowerClient_TerminateSession_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_WatchtowerClient_ListTowers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@ -608,6 +798,10 @@ var (
pattern_WatchtowerClient_RemoveTower_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v2", "watchtower", "client", "pubkey"}, ""))
pattern_WatchtowerClient_DeactivateTower_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 5}, []string{"v2", "watchtower", "client", "tower", "deactivate", "pubkey"}, ""))
pattern_WatchtowerClient_TerminateSession_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 5}, []string{"v2", "watchtower", "client", "sessions", "terminate", "session_id"}, ""))
pattern_WatchtowerClient_ListTowers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v2", "watchtower", "client"}, ""))
pattern_WatchtowerClient_GetTowerInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"v2", "watchtower", "client", "info", "pubkey"}, ""))
@ -622,6 +816,10 @@ var (
forward_WatchtowerClient_RemoveTower_0 = runtime.ForwardResponseMessage
forward_WatchtowerClient_DeactivateTower_0 = runtime.ForwardResponseMessage
forward_WatchtowerClient_TerminateSession_0 = runtime.ForwardResponseMessage
forward_WatchtowerClient_ListTowers_0 = runtime.ForwardResponseMessage
forward_WatchtowerClient_GetTowerInfo_0 = runtime.ForwardResponseMessage

View File

@ -41,6 +41,21 @@ service WatchtowerClient {
*/
rpc RemoveTower (RemoveTowerRequest) returns (RemoveTowerResponse);
/* lncli: `wtclient deactivate`
DeactivateTower sets the given tower's status to inactive so that it
is not considered for session negotiation. Its sessions will also not
be used while the tower is inactive.
*/
rpc DeactivateTower (DeactivateTowerRequest)
returns (DeactivateTowerResponse);
/* lncli: `wtclient session terminate`
Terminate terminates the given session and marks it as terminal so that
it is not used for backups anymore.
*/
rpc TerminateSession (TerminateSessionRequest)
returns (TerminateSessionResponse);
/* lncli: `wtclient towers`
ListTowers returns the list of watchtowers registered with the client.
*/
@ -88,6 +103,26 @@ message RemoveTowerRequest {
message RemoveTowerResponse {
}
message DeactivateTowerRequest {
// The identifying public key of the watchtower to deactivate.
bytes pubkey = 1;
}
message DeactivateTowerResponse {
// A string describing the action that took place.
string status = 1;
}
message TerminateSessionRequest {
// The ID of the session that should be terminated.
bytes session_id = 1;
}
message TerminateSessionResponse {
// A string describing the action that took place.
string status = 1;
}
message GetTowerInfoRequest {
// The identifying public key of the watchtower to retrieve information for.
bytes pubkey = 1;
@ -128,6 +163,11 @@ message TowerSession {
the justice transaction in the event of a channel breach.
*/
uint32 sweep_sat_per_vbyte = 5;
/*
The ID of the session.
*/
bytes id = 6;
}
message Tower {

View File

@ -171,6 +171,39 @@
]
}
},
"/v2/watchtower/client/sessions/terminate/{session_id}": {
"post": {
"summary": "lncli: `wtclient session terminate`\nTerminate terminates the given session and marks it as terminal so that\nit is not used for backups anymore.",
"operationId": "WatchtowerClient_TerminateSession",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/wtclientrpcTerminateSessionResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "session_id",
"description": "The ID of the session that should be terminated.",
"in": "path",
"required": true,
"type": "string",
"format": "byte"
}
],
"tags": [
"WatchtowerClient"
]
}
},
"/v2/watchtower/client/stats": {
"get": {
"summary": "lncli: `wtclient stats`\nStats returns the in-memory statistics of the client since startup.",
@ -194,6 +227,39 @@
]
}
},
"/v2/watchtower/client/tower/deactivate/{pubkey}": {
"post": {
"summary": "lncli: `wtclient deactivate`\nDeactivateTower sets the given tower's status to inactive so that it\nis not considered for session negotiation. Its sessions will also not\nbe used while the tower is inactive.",
"operationId": "WatchtowerClient_DeactivateTower",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/wtclientrpcDeactivateTowerResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "pubkey",
"description": "The identifying public key of the watchtower to deactivate.",
"in": "path",
"required": true,
"type": "string",
"format": "byte"
}
],
"tags": [
"WatchtowerClient"
]
}
},
"/v2/watchtower/client/{pubkey}": {
"delete": {
"summary": "lncli: `wtclient remove`\nRemoveTower removes a watchtower from being considered for future session\nnegotiations and from being used for any subsequent backups until it's added\nagain. If an address is provided, then this RPC only serves as a way of\nremoving the address from the watchtower instead.",
@ -283,6 +349,15 @@
"wtclientrpcAddTowerResponse": {
"type": "object"
},
"wtclientrpcDeactivateTowerResponse": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "A string describing the action that took place."
}
}
},
"wtclientrpcListTowersResponse": {
"type": "object",
"properties": {
@ -358,6 +433,15 @@
}
}
},
"wtclientrpcTerminateSessionResponse": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "A string describing the action that took place."
}
}
},
"wtclientrpcTower": {
"type": "object",
"properties": {
@ -425,6 +509,11 @@
"type": "integer",
"format": "int64",
"description": "The fee rate, in satoshis per vbyte, that will be used by the watchtower for\nthe justice transaction in the event of a channel breach."
},
"id": {
"type": "string",
"format": "byte",
"description": "The ID of the session."
}
}
},

View File

@ -8,6 +8,10 @@ http:
body: "*"
- selector: wtclientrpc.WatchtowerClient.RemoveTower
delete: "/v2/watchtower/client/{pubkey}"
- selector: wtclientrpc.WatchtowerClient.DeactivateTower
post: "/v2/watchtower/client/tower/deactivate/{pubkey}"
- selector: wtclientrpc.WatchtowerClient.TerminateSession
post: "/v2/watchtower/client/sessions/terminate/{session_id}"
- selector: wtclientrpc.WatchtowerClient.ListTowers
get: "/v2/watchtower/client"
- selector: wtclientrpc.WatchtowerClient.GetTowerInfo

View File

@ -30,6 +30,15 @@ type WatchtowerClientClient interface {
// again. If an address is provided, then this RPC only serves as a way of
// removing the address from the watchtower instead.
RemoveTower(ctx context.Context, in *RemoveTowerRequest, opts ...grpc.CallOption) (*RemoveTowerResponse, error)
// lncli: `wtclient deactivate`
// DeactivateTower sets the given tower's status to inactive so that it
// is not considered for session negotiation. Its sessions will also not
// be used while the tower is inactive.
DeactivateTower(ctx context.Context, in *DeactivateTowerRequest, opts ...grpc.CallOption) (*DeactivateTowerResponse, error)
// lncli: `wtclient session terminate`
// Terminate terminates the given session and marks it as terminal so that
// it is not used for backups anymore.
TerminateSession(ctx context.Context, in *TerminateSessionRequest, opts ...grpc.CallOption) (*TerminateSessionResponse, error)
// lncli: `wtclient towers`
// ListTowers returns the list of watchtowers registered with the client.
ListTowers(ctx context.Context, in *ListTowersRequest, opts ...grpc.CallOption) (*ListTowersResponse, error)
@ -70,6 +79,24 @@ func (c *watchtowerClientClient) RemoveTower(ctx context.Context, in *RemoveTowe
return out, nil
}
func (c *watchtowerClientClient) DeactivateTower(ctx context.Context, in *DeactivateTowerRequest, opts ...grpc.CallOption) (*DeactivateTowerResponse, error) {
out := new(DeactivateTowerResponse)
err := c.cc.Invoke(ctx, "/wtclientrpc.WatchtowerClient/DeactivateTower", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *watchtowerClientClient) TerminateSession(ctx context.Context, in *TerminateSessionRequest, opts ...grpc.CallOption) (*TerminateSessionResponse, error) {
out := new(TerminateSessionResponse)
err := c.cc.Invoke(ctx, "/wtclientrpc.WatchtowerClient/TerminateSession", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *watchtowerClientClient) ListTowers(ctx context.Context, in *ListTowersRequest, opts ...grpc.CallOption) (*ListTowersResponse, error) {
out := new(ListTowersResponse)
err := c.cc.Invoke(ctx, "/wtclientrpc.WatchtowerClient/ListTowers", in, out, opts...)
@ -122,6 +149,15 @@ type WatchtowerClientServer interface {
// again. If an address is provided, then this RPC only serves as a way of
// removing the address from the watchtower instead.
RemoveTower(context.Context, *RemoveTowerRequest) (*RemoveTowerResponse, error)
// lncli: `wtclient deactivate`
// DeactivateTower sets the given tower's status to inactive so that it
// is not considered for session negotiation. Its sessions will also not
// be used while the tower is inactive.
DeactivateTower(context.Context, *DeactivateTowerRequest) (*DeactivateTowerResponse, error)
// lncli: `wtclient session terminate`
// Terminate terminates the given session and marks it as terminal so that
// it is not used for backups anymore.
TerminateSession(context.Context, *TerminateSessionRequest) (*TerminateSessionResponse, error)
// lncli: `wtclient towers`
// ListTowers returns the list of watchtowers registered with the client.
ListTowers(context.Context, *ListTowersRequest) (*ListTowersResponse, error)
@ -147,6 +183,12 @@ func (UnimplementedWatchtowerClientServer) AddTower(context.Context, *AddTowerRe
func (UnimplementedWatchtowerClientServer) RemoveTower(context.Context, *RemoveTowerRequest) (*RemoveTowerResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method RemoveTower not implemented")
}
func (UnimplementedWatchtowerClientServer) DeactivateTower(context.Context, *DeactivateTowerRequest) (*DeactivateTowerResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeactivateTower not implemented")
}
func (UnimplementedWatchtowerClientServer) TerminateSession(context.Context, *TerminateSessionRequest) (*TerminateSessionResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method TerminateSession not implemented")
}
func (UnimplementedWatchtowerClientServer) ListTowers(context.Context, *ListTowersRequest) (*ListTowersResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListTowers not implemented")
}
@ -208,6 +250,42 @@ func _WatchtowerClient_RemoveTower_Handler(srv interface{}, ctx context.Context,
return interceptor(ctx, in, info, handler)
}
func _WatchtowerClient_DeactivateTower_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeactivateTowerRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WatchtowerClientServer).DeactivateTower(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/wtclientrpc.WatchtowerClient/DeactivateTower",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WatchtowerClientServer).DeactivateTower(ctx, req.(*DeactivateTowerRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WatchtowerClient_TerminateSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(TerminateSessionRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WatchtowerClientServer).TerminateSession(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/wtclientrpc.WatchtowerClient/TerminateSession",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WatchtowerClientServer).TerminateSession(ctx, req.(*TerminateSessionRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WatchtowerClient_ListTowers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListTowersRequest)
if err := dec(in); err != nil {
@ -295,6 +373,14 @@ var WatchtowerClient_ServiceDesc = grpc.ServiceDesc{
MethodName: "RemoveTower",
Handler: _WatchtowerClient_RemoveTower_Handler,
},
{
MethodName: "DeactivateTower",
Handler: _WatchtowerClient_DeactivateTower_Handler,
},
{
MethodName: "TerminateSession",
Handler: _WatchtowerClient_TerminateSession_Handler,
},
{
MethodName: "ListTowers",
Handler: _WatchtowerClient_ListTowers_Handler,

View File

@ -52,6 +52,38 @@ func (h *HarnessRPC) AddTower(
return resp
}
// DeactivateTower makes an RPC call to the WatchtowerClient of the given node
// and asserts.
func (h *HarnessRPC) DeactivateTower(req *wtclientrpc.DeactivateTowerRequest) {
ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout)
defer cancel()
_, err := h.WatchtowerClient.DeactivateTower(ctxt, req)
h.NoError(err, "DeactivateTower")
}
// TerminateSession makes an RPC call to the WatchtowerClient of the given node
// and asserts.
func (h *HarnessRPC) TerminateSession(
req *wtclientrpc.TerminateSessionRequest) {
ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout)
defer cancel()
_, err := h.WatchtowerClient.TerminateSession(ctxt, req)
h.NoError(err, "TerminateSession")
}
// RemoveTower makes an RPC call to the WatchtowerClient of the given node
// and asserts.
func (h *HarnessRPC) RemoveTower(req *wtclientrpc.RemoveTowerRequest) {
ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout)
defer cancel()
_, err := h.WatchtowerClient.RemoveTower(ctxt, req)
h.NoError(err, "RemoveTower")
}
// WatchtowerStats makes a RPC call to the WatchtowerClient of the given node
// and asserts.
func (h *HarnessRPC) WatchtowerStats() *wtclientrpc.StatsResponse {

View File

@ -132,6 +132,35 @@ type staleTowerMsg struct {
errChan chan error
}
// deactivateTowerMsg is an internal message we'll use within the TowerClient
// to signal that a tower should be marked as inactive.
type deactivateTowerMsg struct {
// id is the unique database identifier for the tower.
id wtdb.TowerID
// pubKey is the identifying public key of the watchtower.
pubKey *btcec.PublicKey
// errChan is the channel through which we'll send a response back to
// the caller when handling their request.
//
// NOTE: This channel must be buffered.
errChan chan error
}
// terminateSessMsg is an internal message we'll use within the TowerClient to
// signal that a session should be terminated.
type terminateSessMsg struct {
// id is the session identifier.
id wtdb.SessionID
// errChan is the channel through which we'll send a response back to
// the caller when handling their request.
//
// NOTE: This channel must be buffered.
errChan chan error
}
// clientCfg holds the configuration values required by a client.
type clientCfg struct {
*Config
@ -165,8 +194,10 @@ type client struct {
statTicker *time.Ticker
stats *clientStats
newTowers chan *newTowerMsg
staleTowers chan *staleTowerMsg
newTowers chan *newTowerMsg
staleTowers chan *staleTowerMsg
deactivateTowers chan *deactivateTowerMsg
terminateSessions chan *terminateSessMsg
wg sync.WaitGroup
quit chan struct{}
@ -192,15 +223,17 @@ func newClient(cfg *clientCfg) (*client, error) {
}
c := &client{
cfg: cfg,
log: plog,
pipeline: queue,
activeSessions: newSessionQueueSet(),
statTicker: time.NewTicker(DefaultStatInterval),
stats: new(clientStats),
newTowers: make(chan *newTowerMsg),
staleTowers: make(chan *staleTowerMsg),
quit: make(chan struct{}),
cfg: cfg,
log: plog,
pipeline: queue,
activeSessions: newSessionQueueSet(),
statTicker: time.NewTicker(DefaultStatInterval),
stats: new(clientStats),
newTowers: make(chan *newTowerMsg),
staleTowers: make(chan *staleTowerMsg),
deactivateTowers: make(chan *deactivateTowerMsg),
terminateSessions: make(chan *terminateSessMsg),
quit: make(chan struct{}),
}
candidateTowers := newTowerListIterator()
@ -261,7 +294,10 @@ func getTowerAndSessionCandidates(db DB, keyRing ECDHKeyRing,
opts ...wtdb.ClientSessionListOption) (
map[wtdb.SessionID]*ClientSession, error) {
towers, err := db.ListTowers()
// Fetch all active towers from the DB.
towers, err := db.ListTowers(func(tower *wtdb.Tower) bool {
return tower.Status == wtdb.TowerStatusActive
})
if err != nil {
return nil, err
}
@ -288,9 +324,9 @@ func getTowerAndSessionCandidates(db DB, keyRing ECDHKeyRing,
// Add the session to the set of candidate sessions.
candidateSessions[s.ID] = cs
perActiveTower(tower)
}
perActiveTower(tower)
}
return candidateSessions, nil
@ -511,8 +547,8 @@ func (c *client) nextSessionQueue() (*sessionQueue, error) {
// stopAndRemoveSession stops the session with the given ID and removes it from
// the in-memory active sessions set.
func (c *client) stopAndRemoveSession(id wtdb.SessionID) error {
return c.activeSessions.StopAndRemove(id)
func (c *client) stopAndRemoveSession(id wtdb.SessionID, final bool) error {
return c.activeSessions.StopAndRemove(id, final)
}
// deleteSessionFromTower dials the tower that we created the session with and
@ -691,6 +727,16 @@ func (c *client) backupDispatcher() {
case msg := <-c.staleTowers:
msg.errChan <- c.handleStaleTower(msg)
// A tower has been requested to be de-activated. We'll
// only allow this if the tower is not currently being
// used for session negotiation.
case msg := <-c.deactivateTowers:
msg.errChan <- c.handleDeactivateTower(msg)
// A request has come through to terminate a session.
case msg := <-c.terminateSessions:
msg.errChan <- c.handleTerminateSession(msg)
case <-c.quit:
return
}
@ -776,6 +822,14 @@ func (c *client) backupDispatcher() {
case msg := <-c.staleTowers:
msg.errChan <- c.handleStaleTower(msg)
// A tower has been requested to be de-activated.
case msg := <-c.deactivateTowers:
msg.errChan <- c.handleDeactivateTower(msg)
// A request has come through to terminate a session.
case msg := <-c.terminateSessions:
msg.errChan <- c.handleTerminateSession(msg)
case <-c.quit:
return
}
@ -1043,6 +1097,124 @@ func (c *client) initActiveQueue(s *ClientSession,
return sq
}
// terminateSession sets the given session's status to CSessionTerminal meaning
// that it will not be used again.
func (c *client) terminateSession(id wtdb.SessionID) error {
errChan := make(chan error, 1)
select {
case c.terminateSessions <- &terminateSessMsg{
id: id,
errChan: errChan,
}:
case <-c.pipeline.quit:
return ErrClientExiting
}
select {
case err := <-errChan:
return err
case <-c.pipeline.quit:
return ErrClientExiting
}
}
// handleTerminateSession handles a request to terminate a session. It will
// first shut down the session if it is part of the active session set, then
// it will ensure that the active session queue is set reset if it is using the
// session in question. Finally, the session's status in the DB will be updated.
func (c *client) handleTerminateSession(msg *terminateSessMsg) error {
id := msg.id
delete(c.candidateSessions, id)
err := c.activeSessions.StopAndRemove(id, true)
if err != nil {
return fmt.Errorf("could not stop session %s: %w", id, err)
}
// If our active session queue corresponds to the session being
// terminated, then we'll proceed to negotiate a new one.
if c.sessionQueue != nil {
if bytes.Equal(c.sessionQueue.ID()[:], id[:]) {
c.sessionQueue = nil
}
}
return nil
}
// deactivateTower sends a tower deactivation request to the backupDispatcher
// where it will be handled synchronously. The request should result in all the
// sessions that we have with the given tower being shutdown and removed from
// our in-memory set of active sessions.
func (c *client) deactivateTower(id wtdb.TowerID,
pubKey *btcec.PublicKey) error {
errChan := make(chan error, 1)
select {
case c.deactivateTowers <- &deactivateTowerMsg{
id: id,
pubKey: pubKey,
errChan: errChan,
}:
case <-c.pipeline.quit:
return ErrClientExiting
}
select {
case err := <-errChan:
return err
case <-c.pipeline.quit:
return ErrClientExiting
}
}
// handleDeactivateTower handles a request to deactivate a tower. We will remove
// it from the in-memory candidate set, and we will also stop any active
// sessions we have with this tower.
func (c *client) handleDeactivateTower(msg *deactivateTowerMsg) error {
// Remove the tower from our in-memory candidate set so that it is not
// used for any new session negotiations.
err := c.candidateTowers.RemoveCandidate(msg.id, nil)
if err != nil {
return err
}
pubKey := msg.pubKey.SerializeCompressed()
sessions, err := c.cfg.DB.ListClientSessions(&msg.id)
if err != nil {
return fmt.Errorf("unable to retrieve sessions for tower %x: "+
"%v", pubKey, err)
}
// Iterate over all the sessions we have for this tower and remove them
// from our candidate set and also from our set of started, active
// sessions.
for sessionID := range sessions {
delete(c.candidateSessions, sessionID)
err = c.activeSessions.StopAndRemove(sessionID, false)
if err != nil {
return fmt.Errorf("could not stop session %s: %w",
sessionID, err)
}
}
// If our active session queue corresponds to the stale tower, we'll
// proceed to negotiate a new one.
if c.sessionQueue != nil {
towerKey := c.sessionQueue.tower.IdentityKey
if bytes.Equal(pubKey, towerKey.SerializeCompressed()) {
c.sessionQueue = nil
}
}
return nil
}
// addTower adds a new watchtower reachable at the given address and considers
// it for new sessions. If the watchtower already exists, then any new addresses
// included will be considered when dialing it for session negotiations and
@ -1149,7 +1321,7 @@ func (c *client) handleStaleTower(msg *staleTowerMsg) error {
// Shutdown the session so that any pending updates are
// replayed back onto the main task pipeline.
err = c.activeSessions.StopAndRemove(sessionID)
err = c.activeSessions.StopAndRemove(sessionID, true)
if err != nil {
c.log.Errorf("could not stop session %s: %w", sessionID,
err)

View File

@ -1034,15 +1034,19 @@ func (s *serverHarness) waitForUpdates(hints []blob.BreachHint,
// Closure to assert the server's matches are consistent with the hint
// set.
serverHasHints := func(matches []wtdb.Match) bool {
if len(hintSet) != len(matches) {
// De-dup the server matches since it might very well have
// multiple matches for a hint if that update was backed up on
// more than one session.
matchHints := make(map[blob.BreachHint]struct{})
for _, match := range matches {
matchHints[match.Hint] = struct{}{}
}
if len(hintSet) != len(matchHints) {
return false
}
for _, match := range matches {
_, ok := hintSet[match.Hint]
require.Truef(s.t, ok, "match %v in db is not in "+
"hint set", match.Hint)
}
require.EqualValues(s.t, hintSet, matchHints)
return true
}
@ -2688,6 +2692,205 @@ var clientTests = []clientTest{
require.NoError(h.t, err)
},
},
{
name: "de-activate a tower",
cfg: harnessCfg{
localBalance: localBalance,
remoteBalance: remoteBalance,
policy: wtpolicy.Policy{
TxPolicy: defaultTxPolicy,
MaxUpdates: 5,
},
},
fn: func(h *testHarness) {
const (
numUpdates = 10
chanIDInt = 0
)
// Advance the channel with a few updates.
hints := h.advanceChannelN(chanIDInt, numUpdates)
// Backup a few these updates and wait for them to
// arrive at the server.
h.backupStates(chanIDInt, 0, numUpdates/2, nil)
h.server.waitForUpdates(hints[:numUpdates/2], waitTime)
// Lookup the tower and assert that it currently is
// seen as an active session candidate.
resp, err := h.clientMgr.LookupTower(
h.server.addr.IdentityKey,
)
require.NoError(h.t, err)
tower, ok := resp[blob.TypeAltruistTaprootCommit]
require.True(h.t, ok)
require.True(h.t, tower.ActiveSessionCandidate)
// Deactivate the tower.
err = h.clientMgr.DeactivateTower(
h.server.addr.IdentityKey,
)
require.NoError(h.t, err)
// Assert that it is no longer seen as an active
// session candidate.
resp, err = h.clientMgr.LookupTower(
h.server.addr.IdentityKey,
)
require.NoError(h.t, err)
tower, ok = resp[blob.TypeAltruistTaprootCommit]
require.True(h.t, ok)
require.False(h.t, tower.ActiveSessionCandidate)
// Add a new tower.
server2 := newServerHarness(
h.t, h.net, towerAddr2Str, nil,
)
server2.start()
h.addTower(server2.addr)
// Backup a few more states and assert that they appear
// on the second tower server.
h.backupStates(
chanIDInt, numUpdates/2, numUpdates-1, nil,
)
server2.waitForUpdates(
hints[numUpdates/2:numUpdates-1], waitTime,
)
// Reactivate the first tower.
err = h.clientMgr.AddTower(h.server.addr)
require.NoError(h.t, err)
// Deactivate the second tower.
err = h.clientMgr.DeactivateTower(
server2.addr.IdentityKey,
)
require.NoError(h.t, err)
// Backup the last backup and assert that it appears
// on the first tower.
h.backupStates(chanIDInt, numUpdates-1, numUpdates, nil)
h.server.waitForUpdates(hints[numUpdates-1:], waitTime)
},
},
{
name: "terminate session",
cfg: harnessCfg{
localBalance: localBalance,
remoteBalance: remoteBalance,
policy: wtpolicy.Policy{
TxPolicy: defaultTxPolicy,
MaxUpdates: 5,
},
},
fn: func(h *testHarness) {
const (
numUpdates = 10
chanIDInt = 0
)
// Advance the channel with a few updates.
hints := h.advanceChannelN(chanIDInt, numUpdates)
// Backup one of these updates and wait for it to
// arrive at the server.
h.backupStates(chanIDInt, 0, 1, nil)
h.server.waitForUpdates(hints[:1], waitTime)
// Now, restart the server in a state where it will not
// ack updates. This will allow us to wait for an update
// to be un-acked and persisted.
h.server.restart(func(cfg *wtserver.Config) {
cfg.NoAckUpdates = true
})
// Backup another update. These should remain in the
// client as un-acked.
h.backupStates(chanIDInt, 1, 2, nil)
// Wait for the update to be persisted.
fetchUnacked := h.clientDB.FetchSessionCommittedUpdates
var sessID wtdb.SessionID
err := wait.Predicate(func() bool {
sessions, err := h.clientDB.ListClientSessions(
nil,
)
require.NoError(h.t, err)
var updates []wtdb.CommittedUpdate
for id := range sessions {
sessID = id
updates, err = fetchUnacked(&id)
require.NoError(h.t, err)
return len(updates) == 1
}
return false
}, waitTime)
require.NoError(h.t, err)
// Now try to terminate the session by directly calling
// the DB terminate method. This is expected to fail
// since the session still has un-acked updates.
err = h.clientDB.TerminateSession(sessID)
require.ErrorIs(
h.t, err, wtdb.ErrSessionHasUnackedUpdates,
)
// If we try to terminate the session through the client
// interface though, it should succeed since the client
// will handle the un-acked updates of the session.
err = h.clientMgr.TerminateSession(sessID)
require.NoError(h.t, err)
// Fetch the session from the DB and assert that it is
// in the terminal state and that it is not exhausted.
sess, err := h.clientDB.GetClientSession(sessID)
require.NoError(h.t, err)
require.Equal(h.t, wtdb.CSessionTerminal, sess.Status)
require.NotEqual(
h.t, sess.Policy.MaxUpdates, sess.SeqNum,
)
// Restart the server and allow it to ack updates again.
h.server.restart(func(cfg *wtserver.Config) {
cfg.NoAckUpdates = false
})
// Wait for the update from before to appear on the
// server. The server will actually have this back-up
// stored twice now since it would have stored it for
// the first session even though it did not send an ACK
// for it.
h.server.waitForUpdates(hints[1:2], waitTime)
// Now we want to assert that this update was definitely
// not sent on the terminated session but was instead
// sent in a new session.
var (
updateCounts = make(map[wtdb.SessionID]uint16)
totalUpdates uint16
)
sessions, err := h.clientDB.ListClientSessions(nil,
wtdb.WithPerNumAckedUpdates(
func(s *wtdb.ClientSession,
_ lnwire.ChannelID,
num uint16) {
updateCounts[s.ID] += num
totalUpdates += num
},
),
)
require.NoError(h.t, err)
require.Len(h.t, sessions, 2)
require.EqualValues(h.t, 1, updateCounts[sessID])
require.EqualValues(h.t, 2, totalUpdates)
},
},
}
// TestClient executes the client test suite, asserting the ability to backup

View File

@ -26,14 +26,17 @@ type DB interface {
// RemoveTower modifies a tower's record within the database. If an
// address is provided, then _only_ the address record should be removed
// from the tower's persisted state. Otherwise, we'll attempt to mark
// the tower as inactive by marking all of its sessions inactive. If any
// of its sessions has unacked updates, then ErrTowerUnackedUpdates is
// returned. If the tower doesn't have any sessions at all, it'll be
// completely removed from the database.
// the tower as inactive. If any of its sessions have unacked updates,
// then ErrTowerUnackedUpdates is returned. If the tower doesn't have
// any sessions at all, it'll be completely removed from the database.
//
// NOTE: An error is not returned if the tower doesn't exist.
RemoveTower(*btcec.PublicKey, net.Addr) error
// TerminateSession sets the given session's status to CSessionTerminal
// meaning that it will not be usable again.
TerminateSession(id wtdb.SessionID) error
// LoadTower retrieves a tower by its public key.
LoadTower(*btcec.PublicKey) (*wtdb.Tower, error)
@ -41,8 +44,9 @@ type DB interface {
LoadTowerByID(wtdb.TowerID) (*wtdb.Tower, error)
// ListTowers retrieves the list of towers available within the
// database.
ListTowers() ([]*wtdb.Tower, error)
// database. The filter function may be set in order to filter out the
// towers to be returned.
ListTowers(filter wtdb.TowerFilterFn) ([]*wtdb.Tower, error)
// NextSessionKeyIndex reserves a new session key derivation index for a
// particular tower id and blob type. The index is reserved for that
@ -136,9 +140,14 @@ type DB interface {
// space.
GetDBQueue(namespace []byte) wtdb.Queue[*wtdb.BackupID]
// DeleteCommittedUpdate deletes the committed update belonging to the
// given session and with the given sequence number from the db.
DeleteCommittedUpdate(id *wtdb.SessionID, seqNum uint16) error
// DeleteCommittedUpdates deletes all the committed updates belonging to
// the given session from the db.
DeleteCommittedUpdates(id *wtdb.SessionID) error
// DeactivateTower sets the given tower's status to inactive. This means
// that this tower's sessions won't be loaded and used for backups.
// CreateTower can be used to reactivate the tower again.
DeactivateTower(pubKey *btcec.PublicKey) error
}
// AuthDialer connects to a remote node using an authenticated transport, such

View File

@ -38,6 +38,15 @@ type ClientManager interface {
// instead.
RemoveTower(*btcec.PublicKey, net.Addr) error
// DeactivateTower sets the given tower's status to inactive so that it
// is not considered for session negotiation. Its sessions will also not
// be used while the tower is inactive.
DeactivateTower(pubKey *btcec.PublicKey) error
// TerminateSession sets the given session's status to CSessionTerminal
// meaning that it will not be used again.
TerminateSession(id wtdb.SessionID) error
// Stats returns the in-memory statistics of the client since startup.
Stats() ClientStats
@ -431,6 +440,73 @@ func (m *Manager) RemoveTower(key *btcec.PublicKey, addr net.Addr) error {
return nil
}
// TerminateSession sets the given session's status to CSessionTerminal meaning
// that it will not be used again.
func (m *Manager) TerminateSession(id wtdb.SessionID) error {
m.clientsMu.Lock()
defer m.clientsMu.Unlock()
for _, client := range m.clients {
err := client.terminateSession(id)
if err != nil {
return err
}
}
// Finally, mark the session as terminated in the DB.
return m.cfg.DB.TerminateSession(id)
}
// DeactivateTower sets the given tower's status to inactive so that it is not
// considered for session negotiation. Its sessions will also not be used while
// the tower is inactive.
func (m *Manager) DeactivateTower(key *btcec.PublicKey) error {
// We'll load the tower in order to retrieve its ID within the database.
tower, err := m.cfg.DB.LoadTower(key)
if err != nil {
return err
}
m.clientsMu.Lock()
defer m.clientsMu.Unlock()
for _, client := range m.clients {
err := client.deactivateTower(tower.ID, tower.IdentityKey)
if err != nil {
return err
}
}
// Finally, mark the tower as inactive in the DB.
err = m.cfg.DB.DeactivateTower(key)
if err != nil {
log.Errorf("Could not deactivate the tower. Re-activating. %v",
err)
// If the persisted state update fails, re-add the address to
// our client's in-memory state.
tower, newTowerErr := NewTowerFromDBTower(tower)
if newTowerErr != nil {
log.Errorf("Could not create new in-memory tower: %v",
newTowerErr)
return err
}
for _, client := range m.clients {
addTowerErr := client.addTower(tower)
if addTowerErr != nil {
log.Errorf("Could not re-add tower: %v",
addTowerErr)
}
}
return err
}
return nil
}
// Stats returns the in-memory statistics of the clients managed by the Manager
// since startup.
func (m *Manager) Stats() ClientStats {
@ -455,7 +531,7 @@ func (m *Manager) Stats() ClientStats {
func (m *Manager) RegisteredTowers(opts ...wtdb.ClientSessionListOption) (
map[blob.Type][]*RegisteredTower, error) {
towers, err := m.cfg.DB.ListTowers()
towers, err := m.cfg.DB.ListTowers(nil)
if err != nil {
return nil, err
}
@ -850,7 +926,7 @@ func (m *Manager) handleClosableSessions(
// Stop the session and remove it from the
// in-memory set.
err = client.stopAndRemoveSession(
item.sessionID,
item.sessionID, true,
)
if err != nil {
log.Errorf("could not remove "+

View File

@ -211,14 +211,13 @@ func (q *sessionQueue) Stop(final bool) error {
update.BackupID, err)
continue
}
}
err = q.cfg.DB.DeleteCommittedUpdate(
q.ID(), update.SeqNum,
)
if final {
err = q.cfg.DB.DeleteCommittedUpdates(q.ID())
if err != nil {
log.Errorf("could not delete committed "+
"update %d for session %s",
update.SeqNum, q.ID())
"updates for session %s", q.ID())
}
}
@ -766,7 +765,7 @@ func (s *sessionQueueSet) AddAndStart(sessionQueue *sessionQueue) {
// StopAndRemove stops the given session queue and removes it from the
// sessionQueueSet.
func (s *sessionQueueSet) StopAndRemove(id wtdb.SessionID) error {
func (s *sessionQueueSet) StopAndRemove(id wtdb.SessionID, final bool) error {
s.mu.Lock()
defer s.mu.Unlock()
@ -777,7 +776,7 @@ func (s *sessionQueueSet) StopAndRemove(id wtdb.SessionID) error {
delete(s.queues, id)
return queue.Stop(true)
return queue.Stop(final)
}
// Get fetches and returns the sessionQueue with the given ID.

View File

@ -196,9 +196,9 @@ var (
// session has updates for channels that are still open.
errSessionHasOpenChannels = errors.New("session has open channels")
// errSessionHasUnackedUpdates is an error used to indicate that a
// ErrSessionHasUnackedUpdates is an error used to indicate that a
// session has un-acked updates.
errSessionHasUnackedUpdates = errors.New("session has un-acked updates")
ErrSessionHasUnackedUpdates = errors.New("session has un-acked updates")
// errChannelHasMoreSessions is an error used to indicate that a channel
// has updates in other non-closed sessions.
@ -387,41 +387,13 @@ func (c *ClientDB) CreateTower(lnAddr *lnwire.NetAddress) (*Tower, error) {
return err
}
// Set its status to active.
tower.Status = TowerStatusActive
// Add the new address to the existing tower. If the
// address is a duplicate, this will result in no
// change.
tower.AddAddress(lnAddr.Address)
// If there are any client sessions that correspond to
// this tower, we'll mark them as active to ensure we
// load them upon restarts.
towerSessIndex := towerToSessionIndex.NestedReadBucket(
tower.ID.Bytes(),
)
if towerSessIndex == nil {
return ErrTowerNotFound
}
sessions := tx.ReadWriteBucket(cSessionBkt)
if sessions == nil {
return ErrUninitializedDB
}
err = towerSessIndex.ForEach(func(k, _ []byte) error {
session, err := getClientSessionBody(
sessions, k,
)
if err != nil {
return err
}
return markSessionStatus(
sessions, session, CSessionActive,
)
})
if err != nil {
return err
}
} else {
// No such tower exists, create a new tower id for our
// new tower. The error is unhandled since NextSequence
@ -432,6 +404,7 @@ func (c *ClientDB) CreateTower(lnAddr *lnwire.NetAddress) (*Tower, error) {
ID: TowerID(towerID),
IdentityKey: lnAddr.IdentityKey,
Addresses: []net.Addr{lnAddr.Address},
Status: TowerStatusActive,
}
towerIDBytes = tower.ID.Bytes()
@ -465,10 +438,10 @@ func (c *ClientDB) CreateTower(lnAddr *lnwire.NetAddress) (*Tower, error) {
// RemoveTower modifies a tower's record within the database. If an address is
// provided, then _only_ the address record should be removed from the tower's
// persisted state. Otherwise, we'll attempt to mark the tower as inactive by
// marking all of its sessions inactive. If any of its sessions has unacked
// updates, then ErrTowerUnackedUpdates is returned. If the tower doesn't have
// any sessions at all, it'll be completely removed from the database.
// persisted state. Otherwise, we'll attempt to mark the tower as inactive. If
// any of its sessions has unacked updates, then ErrTowerUnackedUpdates is
// returned. If the tower doesn't have any sessions at all, it'll be completely
// removed from the database.
//
// NOTE: An error is not returned if the tower doesn't exist.
func (c *ClientDB) RemoveTower(pubKey *btcec.PublicKey, addr net.Addr) error {
@ -503,14 +476,14 @@ func (c *ClientDB) RemoveTower(pubKey *btcec.PublicKey, addr net.Addr) error {
return nil
}
tower, err := getTower(towers, towerIDBytes)
if err != nil {
return err
}
// If an address is provided, then we should _only_ remove the
// address record from the database.
if addr != nil {
tower, err := getTower(towers, towerIDBytes)
if err != nil {
return err
}
// Towers should always have at least one address saved.
tower.RemoveAddress(addr)
if len(tower.Addresses) == 0 {
@ -560,25 +533,76 @@ func (c *ClientDB) RemoveTower(pubKey *btcec.PublicKey, addr net.Addr) error {
)
}
// We'll mark its sessions as inactive as long as they don't
// have any pending updates to ensure we don't load them upon
// restarts.
// Otherwise, we mark the tower as inactive.
tower.Status = TowerStatusInactive
err = putTower(towers, tower)
if err != nil {
return err
}
// We'll do a check to ensure that the tower's sessions don't
// have any pending back-ups.
for _, session := range towerSessions {
if committedUpdateCount[session.ID] > 0 {
return ErrTowerUnackedUpdates
}
err := markSessionStatus(
sessions, session, CSessionInactive,
)
if err != nil {
return err
}
}
return nil
}, func() {})
}
// DeactivateTower sets the given tower's status to inactive. This means that
// this tower's sessions won't be loaded and used for backups. CreateTower can
// be used to reactivate the tower again.
func (c *ClientDB) DeactivateTower(pubKey *btcec.PublicKey) error {
return kvdb.Update(c.db, func(tx kvdb.RwTx) error {
towers := tx.ReadWriteBucket(cTowerBkt)
if towers == nil {
return ErrUninitializedDB
}
towerIndex := tx.ReadWriteBucket(cTowerIndexBkt)
if towerIndex == nil {
return ErrUninitializedDB
}
towersToSessionsIndex := tx.ReadWriteBucket(
cTowerToSessionIndexBkt,
)
if towersToSessionsIndex == nil {
return ErrUninitializedDB
}
chanIDIndexBkt := tx.ReadBucket(cChanIDIndexBkt)
if chanIDIndexBkt == nil {
return ErrUninitializedDB
}
pubKeyBytes := pubKey.SerializeCompressed()
towerIDBytes := towerIndex.Get(pubKeyBytes)
if towerIDBytes == nil {
return ErrTowerNotFound
}
tower, err := getTower(towers, towerIDBytes)
if err != nil {
return err
}
// If the tower already has the desired status, then we can exit
// here.
if tower.Status == TowerStatusInactive {
return nil
}
// Otherwise, we update the status and re-store the tower.
tower.Status = TowerStatusInactive
return putTower(towers, tower)
}, func() {})
}
// LoadTowerByID retrieves a tower by its tower ID.
func (c *ClientDB) LoadTowerByID(towerID TowerID) (*Tower, error) {
var tower *Tower
@ -632,8 +656,14 @@ func (c *ClientDB) LoadTower(pubKey *btcec.PublicKey) (*Tower, error) {
return tower, nil
}
// ListTowers retrieves the list of towers available within the database.
func (c *ClientDB) ListTowers() ([]*Tower, error) {
// TowerFilterFn is the signature of a call-back function that can be used to
// skip certain towers in the ListTowers method.
type TowerFilterFn func(*Tower) bool
// ListTowers retrieves the list of towers available within the database that
// have a status matching the given status. The filter function may be set in
// order to filter out the towers to be returned.
func (c *ClientDB) ListTowers(filter TowerFilterFn) ([]*Tower, error) {
var towers []*Tower
err := kvdb.View(c.db, func(tx kvdb.RTx) error {
towerBucket := tx.ReadBucket(cTowerBkt)
@ -646,7 +676,13 @@ func (c *ClientDB) ListTowers() ([]*Tower, error) {
if err != nil {
return err
}
if filter != nil && !filter(tower) {
return nil
}
towers = append(towers, tower)
return nil
})
}, func() {
@ -1787,9 +1823,10 @@ func (c *ClientDB) MarkChannelClosed(chanID lnwire.ChannelID,
// isSessionClosable returns true if a session is considered closable. A session
// is considered closable only if all the following points are true:
// 1) It has no un-acked updates.
// 2) It is exhausted (ie it can't accept any more updates)
// 3) All the channels that it has acked updates for are closed.
// 1. It has no un-acked updates.
// 2. It is exhausted (ie it can't accept any more updates) OR it has been
// marked as terminal.
// 3. All the channels that it has acked updates for are closed.
func isSessionClosable(sessionsBkt, chanDetailsBkt, chanIDIndexBkt kvdb.RBucket,
id *SessionID) (bool, error) {
@ -1798,22 +1835,21 @@ func isSessionClosable(sessionsBkt, chanDetailsBkt, chanIDIndexBkt kvdb.RBucket,
return false, ErrSessionNotFound
}
// Since the DeleteCommittedUpdates method deletes the cSessionCommits
// bucket in one go, it is possible for the session to be closable even
// if this bucket no longer exists.
commitsBkt := sessBkt.NestedReadBucket(cSessionCommits)
if commitsBkt == nil {
// If the session has no cSessionCommits bucket then we can be
// sure that no updates have ever been committed to the session
// and so it is not yet exhausted.
return false, nil
}
// If the session has any un-acked updates, then it is not yet closable.
err := commitsBkt.ForEach(func(_, _ []byte) error {
return errSessionHasUnackedUpdates
})
if errors.Is(err, errSessionHasUnackedUpdates) {
return false, nil
} else if err != nil {
return false, err
if commitsBkt != nil {
// If the session has any un-acked updates, then it is not yet
// closable.
err := commitsBkt.ForEach(func(_, _ []byte) error {
return ErrSessionHasUnackedUpdates
})
if errors.Is(err, ErrSessionHasUnackedUpdates) {
return false, nil
} else if err != nil {
return false, err
}
}
session, err := getClientSessionBody(sessionsBkt, id[:])
@ -1821,10 +1857,14 @@ func isSessionClosable(sessionsBkt, chanDetailsBkt, chanIDIndexBkt kvdb.RBucket,
return false, err
}
isTerminal := session.Status == CSessionTerminal
// We have already checked that the session has no more committed
// updates. So now we can check if the session is exhausted.
if session.SeqNum < session.Policy.MaxUpdates {
// If the session is not yet exhausted, it is not yet closable.
// updates. So now we can check if the session is exhausted or has a
// terminal state.
if !isTerminal && session.SeqNum < session.Policy.MaxUpdates {
// If the session is not yet exhausted, and it is not yet in a
// terminal state then it is not yet closable.
return false, nil
}
@ -1844,11 +1884,16 @@ func isSessionClosable(sessionsBkt, chanDetailsBkt, chanIDIndexBkt kvdb.RBucket,
}
}
// If the session has no acked-updates, then something is wrong since
// the above check ensures that this session has been exhausted meaning
// that it should have MaxUpdates acked updates.
ackedRangeBkt := sessBkt.NestedReadBucket(cSessionAckRangeIndex)
if ackedRangeBkt == nil {
if isTerminal {
return true, nil
}
// If the session has no acked-updates, and it is not in a
// terminal state then something is wrong since the above check
// ensures that this session has been exhausted meaning that it
// should have MaxUpdates acked updates.
return false, fmt.Errorf("no acked-updates found for "+
"exhausted session %s", id)
}
@ -1996,7 +2041,6 @@ func (c *ClientDB) CommitUpdate(id *SessionID,
lastApplied = session.TowerLastApplied
return nil
}, func() {
lastApplied = 0
})
@ -2215,9 +2259,56 @@ func (c *ClientDB) GetDBQueue(namespace []byte) Queue[*BackupID] {
)
}
// DeleteCommittedUpdate deletes the committed update with the given sequence
// number from the given session.
func (c *ClientDB) DeleteCommittedUpdate(id *SessionID, seqNum uint16) error {
// TerminateSession sets the given session's status to CSessionTerminal meaning
// that it will not be usable again. An error will be returned if the given
// session still has un-acked updates that should be attended to.
func (c *ClientDB) TerminateSession(id SessionID) error {
return kvdb.Update(c.db, func(tx kvdb.RwTx) error {
sessions := tx.ReadWriteBucket(cSessionBkt)
if sessions == nil {
return ErrUninitializedDB
}
sessionsBkt := tx.ReadBucket(cSessionBkt)
if sessionsBkt == nil {
return ErrUninitializedDB
}
chanIDIndexBkt := tx.ReadBucket(cChanIDIndexBkt)
if chanIDIndexBkt == nil {
return ErrUninitializedDB
}
// Collect any un-acked updates for this session.
committedUpdateCount := make(map[SessionID]uint16)
perCommittedUpdate := func(s *ClientSession,
_ *CommittedUpdate) {
committedUpdateCount[s.ID]++
}
session, err := c.getClientSession(
sessionsBkt, chanIDIndexBkt, id[:],
WithPerCommittedUpdate(perCommittedUpdate),
)
if err != nil {
return err
}
// If there are any un-acked updates for this session then
// we don't allow the change of status as these updates must
// first be dealt with somehow.
if committedUpdateCount[id] > 0 {
return ErrSessionHasUnackedUpdates
}
return markSessionStatus(sessions, session, CSessionTerminal)
}, func() {})
}
// DeleteCommittedUpdates deletes all the committed updates for the given
// session.
func (c *ClientDB) DeleteCommittedUpdates(id *SessionID) error {
return kvdb.Update(c.db, func(tx kvdb.RwTx) error {
sessions := tx.ReadWriteBucket(cSessionBkt)
if sessions == nil {
@ -2231,23 +2322,54 @@ func (c *ClientDB) DeleteCommittedUpdate(id *SessionID, seqNum uint16) error {
}
// If the commits sub-bucket doesn't exist, there can't possibly
// be a corresponding update to remove.
// be corresponding updates to remove.
sessionCommits := sessionBkt.NestedReadWriteBucket(
cSessionCommits,
)
if sessionCommits == nil {
return ErrCommittedUpdateNotFound
return nil
}
var seqNumBuf [2]byte
byteOrder.PutUint16(seqNumBuf[:], seqNum)
// errFoundUpdates is an error we will use to exit early from
// the ForEach loop. The return of this error means that at
// least one committed update exists.
var errFoundUpdates = fmt.Errorf("found committed updates")
err := sessionCommits.ForEach(func(k, v []byte) error {
return errFoundUpdates
})
switch {
// If the errFoundUpdates signal error was returned then there
// are some updates that need to be deleted.
case errors.Is(err, errFoundUpdates):
if sessionCommits.Get(seqNumBuf[:]) == nil {
return ErrCommittedUpdateNotFound
// If no error is returned then the ForEach call back was never
// entered meaning that there are no un-acked committed updates.
// So we can exit now as there is nothing left to do.
case err == nil:
return nil
// If an expected error is returned, return that error.
default:
return err
}
// Remove the corresponding committed update.
return sessionCommits.Delete(seqNumBuf[:])
session, err := getClientSessionBody(sessions, id[:])
if err != nil {
return err
}
// Once we delete a committed update from the session, the
// SeqNum of the session will be incorrect and so the session
// should be marked as terminal.
session.Status = CSessionTerminal
err = putClientSessionBody(sessionBkt, session)
if err != nil {
return err
}
// Delete all the committed updates in one go by deleting the
// session commits bucket.
return sessionBkt.DeleteNestedBucket(cSessionCommits)
}, func() {})
}

View File

@ -89,6 +89,26 @@ func (h *clientDBHarness) createTower(lnAddr *lnwire.NetAddress,
return tower
}
func (h *clientDBHarness) deactivateTower(pubKey *btcec.PublicKey,
expErr error) {
h.t.Helper()
err := h.db.DeactivateTower(pubKey)
require.ErrorIs(h.t, err, expErr)
}
func (h *clientDBHarness) listTowers(filterFn wtdb.TowerFilterFn,
expErr error) []*wtdb.Tower {
h.t.Helper()
towers, err := h.db.ListTowers(filterFn)
require.ErrorIs(h.t, err, expErr)
return towers
}
func (h *clientDBHarness) removeTower(pubKey *btcec.PublicKey, addr net.Addr,
hasSessions bool, expErr error) {
@ -125,12 +145,9 @@ func (h *clientDBHarness) removeTower(pubKey *btcec.PublicKey, addr net.Addr,
return
}
for _, session := range h.listSessions(&tower.ID) {
require.Equal(h.t, wtdb.CSessionInactive,
session.Status, "expected status for session "+
"%v to be %v, got %v", session.ID,
wtdb.CSessionInactive, session.Status)
}
require.EqualValues(
h.t, wtdb.TowerStatusInactive, tower.Status,
)
}
}
@ -156,6 +173,24 @@ func (h *clientDBHarness) loadTowerByID(id wtdb.TowerID,
return tower
}
func (h *clientDBHarness) terminateSession(id wtdb.SessionID, expErr error) {
h.t.Helper()
err := h.db.TerminateSession(id)
require.ErrorIs(h.t, err, expErr)
}
func (h *clientDBHarness) getClientSession(id wtdb.SessionID,
expErr error) *wtdb.ClientSession {
h.t.Helper()
session, err := h.db.GetClientSession(id)
require.ErrorIs(h.t, err, expErr)
return session
}
func (h *clientDBHarness) fetchChanInfos() wtdb.ChannelInfos {
h.t.Helper()
@ -194,12 +229,12 @@ func (h *clientDBHarness) ackUpdate(id *wtdb.SessionID, seqNum uint16,
require.ErrorIs(h.t, err, expErr)
}
func (h *clientDBHarness) deleteCommittedUpdate(id *wtdb.SessionID,
seqNum uint16, expErr error) {
func (h *clientDBHarness) deleteCommittedUpdates(id *wtdb.SessionID,
expErr error) {
h.t.Helper()
err := h.db.DeleteCommittedUpdate(id, seqNum)
err := h.db.DeleteCommittedUpdates(id)
require.ErrorIs(h.t, err, expErr)
}
@ -531,20 +566,126 @@ func testRemoveTower(h *clientDBHarness) {
h.commitUpdate(&session.ID, update, nil)
// We should not be able to fully remove it from the database since
// there's a session and it has unacked updates.
// there's a session, and it has unacked updates.
h.removeTower(pk, nil, true, wtdb.ErrTowerUnackedUpdates)
// Removing the tower after all sessions no longer have unacked updates
// should result in the sessions becoming inactive.
// should succeed.
h.ackUpdate(&session.ID, 1, 1, nil)
h.removeTower(pk, nil, true, nil)
}
// Creating the tower again should mark all of the sessions active once
// again.
h.createTower(&lnwire.NetAddress{
func testTerminateSession(h *clientDBHarness) {
const blobType = blob.TypeAltruistCommit
tower := h.newTower()
// Create a new session that the updates in this will be tied to.
session := &wtdb.ClientSession{
ClientSessionBody: wtdb.ClientSessionBody{
TowerID: tower.ID,
Policy: wtpolicy.Policy{
TxPolicy: wtpolicy.TxPolicy{
BlobType: blobType,
},
MaxUpdates: 100,
},
RewardPkScript: []byte{0x01, 0x02, 0x03},
},
ID: wtdb.SessionID([33]byte{0x03}),
}
// Reserve a session key and insert the client session.
session.KeyIndex = h.nextKeyIndex(session.TowerID, blobType, false)
h.insertSession(session, nil)
// Commit to a random update at seqnum 1.
update1 := randCommittedUpdate(h.t, 1)
h.registerChan(update1.BackupID.ChanID, nil, nil)
lastApplied := h.commitUpdate(&session.ID, update1, nil)
require.Zero(h.t, lastApplied)
// Terminating the session now should fail since the session has an
// un-acked update.
h.terminateSession(session.ID, wtdb.ErrSessionHasUnackedUpdates)
// Fetch the session and assert that the status is still active.
sess := h.getClientSession(session.ID, nil)
require.Equal(h.t, wtdb.CSessionActive, sess.Status)
// Delete the update.
h.deleteCommittedUpdates(&session.ID, nil)
// Terminating the session now should succeed.
h.terminateSession(session.ID, nil)
// Fetch the session again and assert that its status is now Terminal.
sess = h.getClientSession(session.ID, nil)
require.Equal(h.t, wtdb.CSessionTerminal, sess.Status)
}
// testTowerStatusChange tests that the Tower status is updated accordingly
// given a variety of commands.
func testTowerStatusChange(h *clientDBHarness) {
// Create a new tower.
pk, err := randPubKey()
require.NoError(h.t, err)
towerAddr := &lnwire.NetAddress{
IdentityKey: pk,
Address: addr1,
}, nil)
Address: &net.TCPAddr{
IP: []byte{0x01, 0x00, 0x00, 0x00}, Port: 9911,
},
}
tower := h.createTower(towerAddr, nil)
// Add a new session.
session := h.randSession(h.t, tower.ID, 100)
h.insertSession(session, nil)
// assertTowerStatus is a helper function that will assert that the
// tower's status is as expected.
assertTowerStatus := func(status wtdb.TowerStatus) {
activeFilter := func(tower *wtdb.Tower) bool {
return tower.Status == status
}
towers := h.listTowers(activeFilter, nil)
require.Len(h.t, towers, 1)
require.EqualValues(h.t, towers[0].Status, status)
}
// assertSessionStatus is a helper that will assert that the session's
// status is as expected
assertSessionStatus := func(status wtdb.CSessionStatus) {
sessions := h.listSessions(&tower.ID)
require.Len(h.t, sessions, 1)
for _, sess := range sessions {
require.EqualValues(h.t, sess.Status, status)
}
}
// Initially, the tower and session should be active.
assertTowerStatus(wtdb.TowerStatusActive)
assertSessionStatus(wtdb.CSessionActive)
// Removing the tower should change its status but its session
// status should remain active.
h.removeTower(tower.IdentityKey, nil, true, nil)
assertTowerStatus(wtdb.TowerStatusInactive)
assertSessionStatus(wtdb.CSessionActive)
// Re-adding the tower in some way should re-active it and its session.
h.createTower(towerAddr, nil)
assertTowerStatus(wtdb.TowerStatusActive)
assertSessionStatus(wtdb.CSessionActive)
// Deactivating the tower should change its status but its session
// status should remain active.
h.deactivateTower(tower.IdentityKey, nil)
assertTowerStatus(wtdb.TowerStatusInactive)
assertSessionStatus(wtdb.CSessionActive)
}
// testChanSummaries tests the process of a registering a channel and its
@ -660,18 +801,7 @@ func testCommitUpdate(h *clientDBHarness) {
// We will now also test that the DeleteCommittedUpdates method also
// works.
// First, try to delete a committed update that does not exist.
h.deleteCommittedUpdate(
&session.ID, update4.SeqNum, wtdb.ErrCommittedUpdateNotFound,
)
// Now delete an existing committed update and ensure that it succeeds.
h.deleteCommittedUpdate(&session.ID, update1.SeqNum, nil)
h.assertUpdates(session.ID, []wtdb.CommittedUpdate{
*update2,
}, nil)
h.deleteCommittedUpdate(&session.ID, update2.SeqNum, nil)
h.deleteCommittedUpdates(&session.ID, nil)
h.assertUpdates(session.ID, []wtdb.CommittedUpdate{}, nil)
}
@ -929,6 +1059,44 @@ func testMarkChannelClosed(h *clientDBHarness) {
// Assert that we now can delete the session.
h.deleteSession(session1.ID, nil)
require.Empty(h.t, h.listClosableSessions(nil))
// We also want to test that a session can be deleted if it has been
// marked as terminal.
// Create session2 with MaxUpdates set to 5.
session2 := h.randSession(h.t, tower.ID, 5)
h.insertSession(session2, nil)
// Create and register channel 7.
chanID7 := randChannelID(h.t)
h.registerChan(chanID7, nil, nil)
// Add two updates for channel 7 in session 2. Ack one of them so that
// the mapping from this channel to this session is created but don't
// ack the other since we want to delete the committed update later.
update = randCommittedUpdateForChannel(h.t, chanID7, 1)
h.commitUpdate(&session2.ID, update, nil)
h.ackUpdate(&session2.ID, 1, 1, nil)
update = randCommittedUpdateForChannel(h.t, chanID7, 2)
h.commitUpdate(&session2.ID, update, nil)
// Check that attempting to delete the session will fail since it is not
// yet considered closable.
h.deleteSession(session2.ID, wtdb.ErrSessionNotClosable)
// Now delete the added committed updates. This should put the
// session in the terminal state after which we should be able to
// delete the session.
h.deleteCommittedUpdates(&session2.ID, nil)
// Marking channel 7 as closed will now return session 2 since it has
// been marked as terminal.
sl = h.markChannelClosed(chanID7, 1, nil)
require.ElementsMatch(h.t, sl, []wtdb.SessionID{session2.ID})
// We should now be able to delete the session.
h.deleteSession(session2.ID, nil)
}
// testAckUpdate asserts the behavior of AckUpdate.
@ -1153,6 +1321,14 @@ func TestClientDB(t *testing.T) {
name: "max commitment heights",
run: testMaxCommitmentHeights,
},
{
name: "test tower status change",
run: testTowerStatusChange,
},
{
name: "terminate session",
run: testTerminateSession,
},
}
for _, database := range dbs {

View File

@ -18,9 +18,9 @@ const (
// used for backups.
CSessionActive CSessionStatus = 0
// CSessionInactive indicates that the ClientSession is inactive and
// cannot be used for backups.
CSessionInactive CSessionStatus = 1
// CSessionTerminal indicates that the ClientSession is in a terminal
// state and cannot be used for backups.
CSessionTerminal CSessionStatus = 1
)
// ClientSession encapsulates a SessionInfo returned from a successful

View File

@ -179,6 +179,7 @@ func TestCodec(tt *testing.T) {
obj := wtdb.Tower{
IdentityKey: pk,
Addresses: addrs,
Status: wtdb.TowerStatus(r.Uint32()),
}
v[0] = reflect.ValueOf(obj)

View File

@ -7,6 +7,26 @@ import (
"net"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/tlv"
)
// TowerStatus represents the state of the tower as set by the tower client.
type TowerStatus uint8
const (
// TowerStatusActive is the default state of the tower, and it indicates
// that this tower should be used to attempt session creation.
TowerStatusActive TowerStatus = 0
// TowerStatusInactive indicates that the tower should not be used to
// attempt session creation.
TowerStatusInactive TowerStatus = 1
)
const (
// TowerStatusTLVType is the TLV type number that will be used to store
// the tower's status.
TowerStatusTLVType = tlv.Type(0)
)
// TowerID is a unique 64-bit identifier allocated to each unique watchtower.
@ -41,6 +61,9 @@ type Tower struct {
// Addresses is a list of possible addresses to reach the tower.
Addresses []net.Addr
// Status is the status of this tower as set by the client.
Status TowerStatus
}
// AddAddress adds the given address to the tower's in-memory list of addresses.
@ -88,17 +111,56 @@ func (t *Tower) String() string {
// Encode writes the Tower to the passed io.Writer. The TowerID is not
// serialized, since it acts as the key.
func (t *Tower) Encode(w io.Writer) error {
return WriteElements(w,
err := WriteElements(w,
t.IdentityKey,
t.Addresses,
)
if err != nil {
return err
}
status := uint8(t.Status)
tlvRecords := []tlv.Record{
tlv.MakePrimitiveRecord(TowerStatusTLVType, &status),
}
tlvStream, err := tlv.NewStream(tlvRecords...)
if err != nil {
return err
}
return tlvStream.Encode(w)
}
// Decode reads a Tower from the passed io.Reader. The TowerID is meant to be
// decoded from the key.
func (t *Tower) Decode(r io.Reader) error {
return ReadElements(r,
err := ReadElements(r,
&t.IdentityKey,
&t.Addresses,
)
if err != nil {
return err
}
var status uint8
tlvRecords := []tlv.Record{
tlv.MakePrimitiveRecord(TowerStatusTLVType, &status),
}
tlvStream, err := tlv.NewStream(tlvRecords...)
if err != nil {
return err
}
typeMap, err := tlvStream.DecodeWithParsedTypes(r)
if err != nil {
return err
}
if _, ok := typeMap[TowerStatusTLVType]; ok {
t.Status = TowerStatus(status)
}
return nil
}