Skip to content

Commit 160977d

Browse files
committed
autopilotserver: add an autopilot server client
Add a client for the autopilot server along with a mock server that can be used in tests.
1 parent 9594aab commit 160977d

File tree

13 files changed

+1270
-9
lines changed

13 files changed

+1270
-9
lines changed

autopilotserver/client.go

Lines changed: 640 additions & 0 deletions
Large diffs are not rendered by default.

autopilotserver/client_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package autopilotserver
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"testing"
7+
"time"
8+
9+
"github.com/btcsuite/btcd/btcec/v2"
10+
"github.com/lightninglabs/lightning-terminal/autopilotserver/mock"
11+
"github.com/lightningnetwork/lnd/lntest/wait"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
// TestAutopilotClient tests that the Client correctly interacts with the
16+
// Autopilot server.
17+
func TestAutopilotClient(t *testing.T) {
18+
ctx := context.Background()
19+
20+
// Spin up a new mock Autopilot server.
21+
server := mock.NewServer()
22+
require.NoError(t, server.Start())
23+
t.Cleanup(server.Stop)
24+
25+
// Create a new client and connect it to the mock server. We set a very
26+
// short ping cadence so that we can test that the client correctly
27+
// ensures re-activation of a session.
28+
addr := fmt.Sprintf("localhost:%d", server.GetPort())
29+
client, err := NewClient(&Config{
30+
Address: addr,
31+
Insecure: true,
32+
PingCadence: time.Second,
33+
})
34+
require.NoError(t, err)
35+
require.NoError(t, client.Start())
36+
t.Cleanup(client.Stop)
37+
38+
privKey, err := btcec.NewPrivateKey()
39+
require.NoError(t, err)
40+
pubKey := privKey.PubKey()
41+
42+
// Activating a session that the server does not yet know about should
43+
// error.
44+
_, err = client.ActivateSession(ctx, pubKey)
45+
require.ErrorContains(t, err, "no such client")
46+
47+
// Register the client.
48+
_, err = client.RegisterSession(ctx, pubKey, "", false, nil)
49+
require.NoError(t, err)
50+
51+
// Assert that the server sees the new client and has it in the Active
52+
// state.
53+
state, err := server.GetClientState(pubKey)
54+
require.NoError(t, err)
55+
require.True(t, mock.ClientStateActive == state)
56+
57+
// Let the server move the client to an Inactive state.
58+
err = server.SetClientState(pubKey, mock.ClientStateInactive)
59+
require.NoError(t, err)
60+
state, err = server.GetClientState(pubKey)
61+
require.NoError(t, err)
62+
require.True(t, mock.ClientStateInactive == state)
63+
64+
// Manually inform the server that the session is active.
65+
_, err = client.ActivateSession(ctx, pubKey)
66+
require.NoError(t, err)
67+
68+
// Assert that the server moved the client to the Active state.
69+
state, err = server.GetClientState(pubKey)
70+
require.NoError(t, err)
71+
require.True(t, mock.ClientStateActive == state)
72+
73+
// Once again, let the server move the client to the inactive state.
74+
err = server.SetClientState(pubKey, mock.ClientStateInactive)
75+
require.NoError(t, err)
76+
state, err = server.GetClientState(pubKey)
77+
require.NoError(t, err)
78+
require.True(t, mock.ClientStateInactive == state)
79+
80+
// Now wait for client to re-activate the session with the server
81+
err = wait.Predicate(func() bool {
82+
state, err = server.GetClientState(pubKey)
83+
require.NoError(t, err)
84+
return state == mock.ClientStateActive
85+
}, time.Second*5)
86+
require.NoError(t, err)
87+
}

autopilotserver/interface.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package autopilotserver
2+
3+
import (
4+
"context"
5+
6+
"github.com/btcsuite/btcd/btcec/v2"
7+
"gopkg.in/macaroon-bakery.v2/bakery"
8+
)
9+
10+
// Autopilot represents the functionality exposed by an autopilot server.
11+
type Autopilot interface {
12+
// ListFeatures fetches the set of features offered by the autopilot
13+
// server along with all the rules and permissions required for those
14+
// features.
15+
ListFeatures(ctx context.Context) (map[string]*Feature, error)
16+
17+
// ListFeaturePerms returns a map of feature names to a map of
18+
// permissions required for each feature. This call uses an in-memory
19+
// store that is updated periodically and so this should be used instead
20+
// of the ListFeatures call if only the permissions are required to
21+
// avoid doing multiple calls to the autopilot server. The ListFeatures
22+
// call can however be used to force the update of the in-memory list.
23+
ListFeaturePerms(ctx context.Context) (map[string]map[string]bool,
24+
error)
25+
26+
// RegisterSession attempts to register a session with the autopilot
27+
// server. If the registration is successful, then the Client will also
28+
// track the session so that it can continuously ensure that the session
29+
// remains active.
30+
RegisterSession(ctx context.Context, pubKey *btcec.PublicKey,
31+
mailboxAddr string, devServer bool,
32+
featureConf map[string][]byte) (*btcec.PublicKey, error)
33+
34+
// ActivateSession attempts to inform the autopilot server that the
35+
// given session is still active. After this is called, the autopilot
36+
// client will periodically ensure that the session remains active.
37+
// The boolean returned is true if the error received was permanent
38+
// meaning that the session should be revoked and recreated.
39+
ActivateSession(ctx context.Context, pubKey *btcec.PublicKey) (bool,
40+
error)
41+
42+
// SessionRevoked should be called when a session is no longer active
43+
// so that the client can forget the session.
44+
SessionRevoked(ctx context.Context, key *btcec.PublicKey)
45+
46+
// Start kicks off the goroutines of the client.
47+
Start(opts ...func(cfg *Config)) error
48+
49+
// Stop cleans up any resources held by the client.
50+
Stop()
51+
}
52+
53+
// Feature holds all the info necessary to subscribe to a feature offered by
54+
// the autopilot server.
55+
type Feature struct {
56+
// Name is the name of the feature.
57+
Name string
58+
59+
// Description is a human-readable description of what the feature
60+
// offers
61+
Description string
62+
63+
// Permissions is a list of RPC methods and access writes a feature
64+
// will need.
65+
Permissions map[string][]bakery.Op
66+
67+
// Rules is a list of all the firewall that must be specified for this
68+
// feature.
69+
Rules map[string]*RuleValues
70+
}
71+
72+
// RuleValues holds the default value along with the sane max and min values
73+
// that the autopilot server indicates makes sense for feature that the rule is
74+
// being applied to. The values can be unmarshalled in a higher layer if the
75+
// name of the rule is known to LiT.
76+
type RuleValues struct {
77+
Default []byte
78+
MinVal []byte
79+
MaxVal []byte
80+
}

autopilotserver/log.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package autopilotserver
2+
3+
import (
4+
"github.com/btcsuite/btclog"
5+
"github.com/lightningnetwork/lnd/build"
6+
)
7+
8+
const Subsystem = "AUTO"
9+
10+
// log is a logger that is initialized with no output filters. This
11+
// means the package will not perform any logging by default until the caller
12+
// requests it.
13+
var log btclog.Logger
14+
15+
// The default amount of logging is none.
16+
func init() {
17+
UseLogger(build.NewSubLogger(Subsystem, nil))
18+
}
19+
20+
// UseLogger uses a specified Logger to output package logging info.
21+
// This should be used in preference to SetLogWriter if the caller is also
22+
// using btclog.
23+
func UseLogger(logger btclog.Logger) {
24+
log = logger
25+
}

autopilotserver/mock/log.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package mock
2+
3+
import (
4+
"github.com/btcsuite/btclog"
5+
"github.com/lightningnetwork/lnd/build"
6+
)
7+
8+
const Subsystem = "AUTO"
9+
10+
// log is a logger that is initialized with no output filters. This
11+
// means the package will not perform any logging by default until the caller
12+
// requests it.
13+
var log btclog.Logger
14+
15+
// The default amount of logging is none.
16+
func init() {
17+
UseLogger(build.NewSubLogger(Subsystem, nil))
18+
}
19+
20+
// UseLogger uses a specified Logger to output package logging info.
21+
// This should be used in preference to SetLogWriter if the caller is also
22+
// using btclog.
23+
func UseLogger(logger btclog.Logger) {
24+
log = logger
25+
}

0 commit comments

Comments
 (0)