Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

autopilot: generic rules per feature #634

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
216 changes: 175 additions & 41 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. For a rule, all fields must be
set since the unset ones are interpreteded as zero values. Rule values
must adhere to the limits found in 'autopilot features'. If a rule is
not set, default values are used.

An example call for AutoFees reads:

#!/bin/bash
./litcli autopilot add --label=customRules \
--feature=AutoFees \
--feature-rules='{
"rules": {
"channel-policy-bounds": {
"chan_policy_bounds": {
"min_base_msat": "0",
"max_base_msat": "10000",
"min_rate_ppm": 10,
"max_rate_ppm": 5000,
"min_cltv_delta": 60,
"max_cltv_delta": 120,
"min_htlc_msat": "1",
"max_htlc_msat": "100000000000"
}
},
"peer-restriction": {
"peer_restrict": {
"peer_ids": [
"abcabc",
"defdef"
]
}
}
}
}' \
--feature-config='{}'`,
Action: initAutopilotSession,
Flags: []cli.Flag{
labelFlag,
Expand All @@ -53,19 +92,21 @@ var addAutopilotSessionCmd = cli.Command{
Name: "feature",
Required: true,
},
cli.StringFlag{
cli.StringSliceFlag{
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{
cli.StringSliceFlag{
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,160 @@ 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")

// Check that the user only sets unique features.
fs := make(map[string]struct{})
for _, feature := range features {
if _, ok := fs[feature]; ok {
return fmt.Errorf("feature %v is set multiple times",
feature)
}
fs[feature] = struct{}{}
}

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)
// Check that the user did not set multiple restrict lists.
var chanRestrictList, peerRestrictList string

channelRestrictSlice := ctx.StringSlice("channel-restrict-list")
if len(channelRestrictSlice) > 1 {
return fmt.Errorf("channel-restrict-list can only be used once")
} else if len(channelRestrictSlice) == 1 {
chanRestrictList = channelRestrictSlice[0]
}

peerRestrictSlice := ctx.StringSlice("peer-restrict-list")
if len(peerRestrictSlice) > 1 {
return fmt.Errorf("peer-restrict-list can only be used once")
} else if len(peerRestrictSlice) == 1 {
peerRestrictList = peerRestrictSlice[0]
}

// rulesMap stores the rules per each feature.
rulesMap := make(map[string]*litrpc.RulesMap)
rulesFlags := ctx.StringSlice("feature-rules")

// 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")
}

ruleMap.Rules[rules.ChannelRestrictName] = &litrpc.RuleValue{
Value: &litrpc.RuleValue_ChannelRestrict{
ChannelRestrict: &litrpc.ChannelRestrict{
ChannelIds: chanIDs,
if len(features) > 1 {
return fmt.Errorf("cannot set channel-restrict-list/" +
"peer-restrict-list when multiple features " +
"are set")
}

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