Skip to content

Commit 6bc86e5

Browse files
Merge pull request #634 from bitromortac/per-feature-restrictions
autopilot: generic rules per feature
2 parents 6b53a86 + 34eeb2a commit 6bc86e5

File tree

1 file changed

+175
-41
lines changed

1 file changed

+175
-41
lines changed

cmd/litcli/autopilot.go

Lines changed: 175 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/lightninglabs/lightning-terminal/litrpc"
1313
"github.com/lightninglabs/lightning-terminal/rules"
14+
"github.com/lightningnetwork/lnd/lnrpc"
1415
"github.com/urfave/cli"
1516
)
1617

@@ -39,10 +40,48 @@ var addAutopilotSessionCmd = cli.Command{
3940
Name: "add",
4041
ShortName: "a",
4142
Usage: "Initialize an Autopilot session.",
42-
Description: "Initialize an Autopilot session.\n\n" +
43-
" If set for any feature, configuration flags need to be " +
44-
"repeated for each feature that is registered, corresponding " +
45-
"to the order of features.",
43+
Description: `
44+
Initialize an Autopilot session.
45+
46+
If one of the 'feature-' flags is set for any 'feature', then that flag
47+
must be provided for each 'feature'.
48+
49+
The rules and configuration options available for each feature can be
50+
seen in the 'autopilot features' output. For a rule, all fields must be
51+
set since the unset ones are interpreteded as zero values. Rule values
52+
must adhere to the limits found in 'autopilot features'. If a rule is
53+
not set, default values are used.
54+
55+
An example call for AutoFees reads:
56+
57+
#!/bin/bash
58+
./litcli autopilot add --label=customRules \
59+
--feature=AutoFees \
60+
--feature-rules='{
61+
"rules": {
62+
"channel-policy-bounds": {
63+
"chan_policy_bounds": {
64+
"min_base_msat": "0",
65+
"max_base_msat": "10000",
66+
"min_rate_ppm": 10,
67+
"max_rate_ppm": 5000,
68+
"min_cltv_delta": 60,
69+
"max_cltv_delta": 120,
70+
"min_htlc_msat": "1",
71+
"max_htlc_msat": "100000000000"
72+
}
73+
},
74+
"peer-restriction": {
75+
"peer_restrict": {
76+
"peer_ids": [
77+
"abcabc",
78+
"defdef"
79+
]
80+
}
81+
}
82+
}
83+
}' \
84+
--feature-config='{}'`,
4685
Action: initAutopilotSession,
4786
Flags: []cli.Flag{
4887
labelFlag,
@@ -53,19 +92,21 @@ var addAutopilotSessionCmd = cli.Command{
5392
Name: "feature",
5493
Required: true,
5594
},
56-
cli.StringFlag{
95+
cli.StringSliceFlag{
5796
Name: "channel-restrict-list",
58-
Usage: "List of channel IDs that the " +
97+
Usage: "[deprecated] List of channel IDs that the " +
5998
"Autopilot server should not " +
6099
"perform actions on. In the " +
61100
"form of: chanID1,chanID2,...",
101+
Hidden: true,
62102
},
63-
cli.StringFlag{
103+
cli.StringSliceFlag{
64104
Name: "peer-restrict-list",
65-
Usage: "List of peer IDs that the " +
105+
Usage: "[deprecated] List of peer IDs that the " +
66106
"Autopilot server should not " +
67107
"perform actions on. In the " +
68108
"form of: peerID1,peerID2,...",
109+
Hidden: true,
69110
},
70111
cli.StringFlag{
71112
Name: "group_id",
@@ -81,6 +122,13 @@ var addAutopilotSessionCmd = cli.Command{
81122
"configuration is allowed with {} to use the " +
82123
"default configuration.",
83124
},
125+
cli.StringSliceFlag{
126+
Name: "feature-rules",
127+
Usage: `JSON-serialized rule map (see main ` +
128+
`description for a format example).` +
129+
`An empty rule map is allowed with {} to ` +
130+
`use the default rules.`,
131+
},
84132
},
85133
}
86134

@@ -190,74 +238,160 @@ func initAutopilotSession(ctx *cli.Context) error {
190238
defer cleanup()
191239
client := litrpc.NewAutopilotClient(clientConn)
192240

193-
ruleMap := &litrpc.RulesMap{
194-
Rules: make(map[string]*litrpc.RuleValue),
241+
features := ctx.StringSlice("feature")
242+
243+
// Check that the user only sets unique features.
244+
fs := make(map[string]struct{})
245+
for _, feature := range features {
246+
if _, ok := fs[feature]; ok {
247+
return fmt.Errorf("feature %v is set multiple times",
248+
feature)
249+
}
250+
fs[feature] = struct{}{}
195251
}
196252

197-
chanRestrictList := ctx.String("channel-restrict-list")
198-
if chanRestrictList != "" {
199-
var chanIDs []uint64
200-
chans := strings.Split(chanRestrictList, ",")
201-
for _, c := range chans {
202-
i, err := strconv.ParseUint(c, 10, 64)
203-
if err != nil {
204-
return err
205-
}
206-
chanIDs = append(chanIDs, i)
253+
// Check that the user did not set multiple restrict lists.
254+
var chanRestrictList, peerRestrictList string
255+
256+
channelRestrictSlice := ctx.StringSlice("channel-restrict-list")
257+
if len(channelRestrictSlice) > 1 {
258+
return fmt.Errorf("channel-restrict-list can only be used once")
259+
} else if len(channelRestrictSlice) == 1 {
260+
chanRestrictList = channelRestrictSlice[0]
261+
}
262+
263+
peerRestrictSlice := ctx.StringSlice("peer-restrict-list")
264+
if len(peerRestrictSlice) > 1 {
265+
return fmt.Errorf("peer-restrict-list can only be used once")
266+
} else if len(peerRestrictSlice) == 1 {
267+
peerRestrictList = peerRestrictSlice[0]
268+
}
269+
270+
// rulesMap stores the rules per each feature.
271+
rulesMap := make(map[string]*litrpc.RulesMap)
272+
rulesFlags := ctx.StringSlice("feature-rules")
273+
274+
// For legacy flags, we allow setting the channel and peer restrict
275+
// lists when only a single feature is added.
276+
if chanRestrictList != "" || peerRestrictList != "" {
277+
// Check that the user did not set both the legacy flags and the
278+
// generic rules flags together.
279+
if len(rulesFlags) > 0 {
280+
return fmt.Errorf("either set channel-restrict-list/" +
281+
"peer-restrict-list or feature-rules, not both")
207282
}
208283

209-
ruleMap.Rules[rules.ChannelRestrictName] = &litrpc.RuleValue{
210-
Value: &litrpc.RuleValue_ChannelRestrict{
211-
ChannelRestrict: &litrpc.ChannelRestrict{
212-
ChannelIds: chanIDs,
284+
if len(features) > 1 {
285+
return fmt.Errorf("cannot set channel-restrict-list/" +
286+
"peer-restrict-list when multiple features " +
287+
"are set")
288+
}
289+
290+
feature := features[0]
291+
292+
// Init the rule map for this feature.
293+
ruleMap := make(map[string]*litrpc.RuleValue)
294+
295+
if chanRestrictList != "" {
296+
var chanIDs []uint64
297+
chans := strings.Split(chanRestrictList, ",")
298+
for _, c := range chans {
299+
i, err := strconv.ParseUint(c, 10, 64)
300+
if err != nil {
301+
return err
302+
}
303+
chanIDs = append(chanIDs, i)
304+
}
305+
306+
channelRestrict := &litrpc.ChannelRestrict{
307+
ChannelIds: chanIDs,
308+
}
309+
310+
ruleMap[rules.ChannelRestrictName] = &litrpc.RuleValue{
311+
Value: &litrpc.RuleValue_ChannelRestrict{
312+
ChannelRestrict: channelRestrict,
213313
},
214-
},
314+
}
215315
}
216-
}
217316

218-
peerRestrictList := ctx.String("peer-restrict-list")
219-
if peerRestrictList != "" {
220-
peerIDs := strings.Split(peerRestrictList, ",")
317+
if peerRestrictList != "" {
318+
peerIDs := strings.Split(peerRestrictList, ",")
221319

222-
ruleMap.Rules[rules.PeersRestrictName] = &litrpc.RuleValue{
223-
Value: &litrpc.RuleValue_PeerRestrict{
224-
PeerRestrict: &litrpc.PeerRestrict{
225-
PeerIds: peerIDs,
320+
ruleMap[rules.PeersRestrictName] = &litrpc.RuleValue{
321+
Value: &litrpc.RuleValue_PeerRestrict{
322+
PeerRestrict: &litrpc.PeerRestrict{
323+
PeerIds: peerIDs,
324+
},
226325
},
227-
},
326+
}
327+
}
328+
329+
rulesMap[feature] = &litrpc.RulesMap{Rules: ruleMap}
330+
} else {
331+
// We make sure that if the rules or configs flags are set, they
332+
// are set for all features, to avoid ambiguity.
333+
if len(rulesFlags) > 0 && len(features) != len(rulesFlags) {
334+
return fmt.Errorf("number of features (%v) and rules "+
335+
"(%v) must match", len(features),
336+
len(rulesFlags))
337+
}
338+
339+
// Parse the rules and store them in the rulesMap.
340+
for i, rulesFlag := range rulesFlags {
341+
var ruleMap litrpc.RulesMap
342+
343+
// We allow empty rules, to signal the usage of the
344+
// default rules when the session is registered.
345+
if rulesFlag != "{}" {
346+
err = lnrpc.ProtoJSONUnmarshalOpts.Unmarshal(
347+
[]byte(rulesFlag), &ruleMap,
348+
)
349+
if err != nil {
350+
return err
351+
}
352+
}
353+
354+
rulesMap[features[i]] = &ruleMap
228355
}
229356
}
230357

231-
features := ctx.StringSlice("feature")
232358
configs := ctx.StringSlice("feature-config")
233359
if len(configs) > 0 && len(features) != len(configs) {
234360
return fmt.Errorf("number of features (%v) and configurations "+
235361
"(%v) must match", len(features), len(configs))
236362
}
237363

238-
featureMap := make(map[string]*litrpc.FeatureConfig)
239-
for i, feature := range ctx.StringSlice("feature") {
364+
// Parse the configs and store them in the configsMap.
365+
configsMap := make(map[string][]byte)
366+
for i, configFlag := range configs {
240367
var config []byte
241368

242369
// We allow empty configs, to signal the usage of the default
243370
// configuration when the session is registered.
244-
if len(configs) > 0 && configs[i] != "{}" {
371+
if configFlag != "{}" {
245372
// We expect the config to be a JSON dictionary, so we
246373
// unmarshal it into a map to do a first validation.
247374
var configMap map[string]interface{}
248375
err := json.Unmarshal([]byte(configs[i]), &configMap)
249376
if err != nil {
250377
return fmt.Errorf("could not parse "+
251378
"configuration for feature %v: %v",
252-
feature, err)
379+
features[i], err)
253380
}
254381

255382
config = []byte(configs[i])
256383
}
257384

385+
configsMap[features[i]] = config
386+
}
387+
388+
featureMap := make(map[string]*litrpc.FeatureConfig)
389+
for _, feature := range features {
390+
// Map access for unknown features will return their zero value
391+
// if not set, which is what we want to signal default usage.
258392
featureMap[feature] = &litrpc.FeatureConfig{
259-
Rules: ruleMap,
260-
Config: config,
393+
Rules: rulesMap[feature],
394+
Config: configsMap[feature],
261395
}
262396
}
263397

0 commit comments

Comments
 (0)