From fcac5f0b6ab311be12f21c4e2f277c203cf2477f Mon Sep 17 00:00:00 2001 From: bitromortac Date: Fri, 26 Jan 2024 14:01:16 +0100 Subject: [PATCH] litcli: session registration with generic rules This commit adds generic feature rule flags. Legacy flags `channel-restrict-list` and `peer-restrict-list` are deprecated. --- cmd/litcli/autopilot.go | 188 +++++++++++++++++++++++++++++++--------- 1 file changed, 149 insertions(+), 39 deletions(-) diff --git a/cmd/litcli/autopilot.go b/cmd/litcli/autopilot.go index bbb309134..a7b5a9cad 100644 --- a/cmd/litcli/autopilot.go +++ b/cmd/litcli/autopilot.go @@ -11,6 +11,7 @@ import ( "github.com/lightninglabs/lightning-terminal/litrpc" "github.com/lightninglabs/lightning-terminal/rules" + "github.com/lightningnetwork/lnd/lnrpc" "github.com/urfave/cli" ) @@ -39,10 +40,48 @@ var addAutopilotSessionCmd = cli.Command{ Name: "add", ShortName: "a", Usage: "Initialize an Autopilot session.", - Description: "Initialize an Autopilot session.\n\n" + - " If set for any feature, configuration flags need to be " + - "repeated for each feature that is registered, corresponding " + - "to the order of features.", + Description: ` + Initialize an Autopilot session. + + If one of the 'feature-' flags is set for any 'feature', then that flag + must be provided for each 'feature'. + + The rules and configuration options available for each feature can be + seen in the 'autopilot features' output. + + An example call reads: + + ./litcli autopilot add --label=customRules \ + --feature=SampleFeature \ + --feature-rules='{ + "rules": { + "rate-limit": { + "rate_limit": { + "read_limit": { + "iterations": 10, + "num_hours": 1 + }, + "write_limit": { + "iterations": 1, + "num_hours": 1 + } + } + }, + "channel-restriction": { + "channel_restrict": { + "channel_ids": [ + 18283782789, + 22891928322, + 31878293727 + ] + } + } + } + }' \ + --feature-config='{ + "version": 0, + "complexity": "complex" + }'`, Action: initAutopilotSession, Flags: []cli.Flag{ labelFlag, @@ -55,17 +94,19 @@ var addAutopilotSessionCmd = cli.Command{ }, cli.StringFlag{ Name: "channel-restrict-list", - Usage: "List of channel IDs that the " + + Usage: "[deprecated] List of channel IDs that the " + "Autopilot server should not " + "perform actions on. In the " + "form of: chanID1,chanID2,...", + Hidden: true, }, cli.StringFlag{ Name: "peer-restrict-list", - Usage: "List of peer IDs that the " + + Usage: "[deprecated] List of peer IDs that the " + "Autopilot server should not " + "perform actions on. In the " + "form of: peerID1,peerID2,...", + Hidden: true, }, cli.StringFlag{ Name: "group_id", @@ -81,6 +122,13 @@ var addAutopilotSessionCmd = cli.Command{ "configuration is allowed with {} to use the " + "default configuration.", }, + cli.StringSliceFlag{ + Name: "feature-rules", + Usage: `JSON-serialized rule map (see main ` + + `description for a format example).` + + `An empty rule map is allowed with {} to ` + + `use the default rules.`, + }, }, } @@ -190,58 +238,113 @@ func initAutopilotSession(ctx *cli.Context) error { defer cleanup() client := litrpc.NewAutopilotClient(clientConn) - ruleMap := &litrpc.RulesMap{ - Rules: make(map[string]*litrpc.RuleValue), - } + features := ctx.StringSlice("feature") + rulesFlags := ctx.StringSlice("feature-rules") chanRestrictList := ctx.String("channel-restrict-list") - if chanRestrictList != "" { - var chanIDs []uint64 - chans := strings.Split(chanRestrictList, ",") - for _, c := range chans { - i, err := strconv.ParseUint(c, 10, 64) - if err != nil { - return err - } - chanIDs = append(chanIDs, i) + peerRestrictList := ctx.String("peer-restrict-list") + + // rulesMap stores the rules per each feature. + rulesMap := make(map[string]*litrpc.RulesMap) + + // For legacy flags, we allow setting the channel and peer restrict + // lists when only a single feature is added. + if chanRestrictList != "" || peerRestrictList != "" { + // Check that the user did not set both the legacy flags and the + // generic rules flags together. + if len(rulesFlags) > 0 { + return fmt.Errorf("either set channel-restrict-list/" + + "peer-restrict-list or feature-rules, not both") + } + + if len(features) > 1 { + return fmt.Errorf("cannot set channel-restrict-list/" + + "peer-restrict-list when multiple features " + + "are set") } - ruleMap.Rules[rules.ChannelRestrictName] = &litrpc.RuleValue{ - Value: &litrpc.RuleValue_ChannelRestrict{ - ChannelRestrict: &litrpc.ChannelRestrict{ - ChannelIds: chanIDs, + feature := features[0] + + // Init the rule map for this feature. + ruleMap := make(map[string]*litrpc.RuleValue) + + if chanRestrictList != "" { + var chanIDs []uint64 + chans := strings.Split(chanRestrictList, ",") + for _, c := range chans { + i, err := strconv.ParseUint(c, 10, 64) + if err != nil { + return err + } + chanIDs = append(chanIDs, i) + } + + channelRestrict := &litrpc.ChannelRestrict{ + ChannelIds: chanIDs, + } + + ruleMap[rules.ChannelRestrictName] = &litrpc.RuleValue{ + Value: &litrpc.RuleValue_ChannelRestrict{ + ChannelRestrict: channelRestrict, }, - }, + } } - } - peerRestrictList := ctx.String("peer-restrict-list") - if peerRestrictList != "" { - peerIDs := strings.Split(peerRestrictList, ",") + if peerRestrictList != "" { + peerIDs := strings.Split(peerRestrictList, ",") - ruleMap.Rules[rules.PeersRestrictName] = &litrpc.RuleValue{ - Value: &litrpc.RuleValue_PeerRestrict{ - PeerRestrict: &litrpc.PeerRestrict{ - PeerIds: peerIDs, + ruleMap[rules.PeersRestrictName] = &litrpc.RuleValue{ + Value: &litrpc.RuleValue_PeerRestrict{ + PeerRestrict: &litrpc.PeerRestrict{ + PeerIds: peerIDs, + }, }, - }, + } + } + + rulesMap[feature] = &litrpc.RulesMap{Rules: ruleMap} + } else { + // We make sure that if the rules or configs flags are set, they + // are set for all features, to avoid ambiguity. + if len(rulesFlags) > 0 && len(features) != len(rulesFlags) { + return fmt.Errorf("number of features (%v) and rules "+ + "(%v) must match", len(features), + len(rulesFlags)) + } + + // Parse the rules and store them in the rulesMap. + for i, rulesFlag := range rulesFlags { + var ruleMap litrpc.RulesMap + + // We allow empty rules, to signal the usage of the + // default rules when the session is registered. + if rulesFlag != "{}" { + err = lnrpc.ProtoJSONUnmarshalOpts.Unmarshal( + []byte(rulesFlag), &ruleMap, + ) + if err != nil { + return err + } + } + + rulesMap[features[i]] = &ruleMap } } - features := ctx.StringSlice("feature") configs := ctx.StringSlice("feature-config") if len(configs) > 0 && len(features) != len(configs) { return fmt.Errorf("number of features (%v) and configurations "+ "(%v) must match", len(features), len(configs)) } - featureMap := make(map[string]*litrpc.FeatureConfig) - for i, feature := range ctx.StringSlice("feature") { + // Parse the configs and store them in the configsMap. + configsMap := make(map[string][]byte) + for i, configFlag := range configs { var config []byte // We allow empty configs, to signal the usage of the default // configuration when the session is registered. - if len(configs) > 0 && configs[i] != "{}" { + if configFlag != "{}" { // We expect the config to be a JSON dictionary, so we // unmarshal it into a map to do a first validation. var configMap map[string]interface{} @@ -249,15 +352,22 @@ func initAutopilotSession(ctx *cli.Context) error { if err != nil { return fmt.Errorf("could not parse "+ "configuration for feature %v: %v", - feature, err) + features[i], err) } config = []byte(configs[i]) } + configsMap[features[i]] = config + } + + featureMap := make(map[string]*litrpc.FeatureConfig) + for _, feature := range features { + // Map access for unknown features will return their zero value + // if not set, which is what we want to signal default usage. featureMap[feature] = &litrpc.FeatureConfig{ - Rules: ruleMap, - Config: config, + Rules: rulesMap[feature], + Config: configsMap[feature], } }