Skip to content

Commit

Permalink
litcli: session registration with generic rules
Browse files Browse the repository at this point in the history
This commit adds generic feature rule flags. Legacy flags
`channel-restrict-list` and `peer-restrict-list` are deprecated.
  • Loading branch information
bitromortac committed Jan 26, 2024
1 parent c03b040 commit fcac5f0
Showing 1 changed file with 149 additions and 39 deletions.
188 changes: 149 additions & 39 deletions cmd/litcli/autopilot.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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,
Expand All @@ -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",
Expand All @@ -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.`,
},
},
}

Expand Down Expand Up @@ -190,74 +238,136 @@ 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{}
err := json.Unmarshal([]byte(configs[i]), &configMap)
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],
}
}

Expand Down

0 comments on commit fcac5f0

Please sign in to comment.