Skip to content

routing+lnrpc/routerrpc: enhance SendPaymentV2 to allow per-payment probability estimator config #9909

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

Open
wants to merge 3 commits into
base: mission-control-path-finding
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,386 changes: 701 additions & 685 deletions lnrpc/routerrpc/router.pb.go

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions lnrpc/routerrpc/router.proto
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,12 @@ message SendPaymentRequest {
If not specified, the default mission control namespace will be used.
*/
string mission_control_namespace = 26;

/*
An optional mission control configuration to use for this payment attempt.
If not specified, the global mission control config will be used.
*/
MissionControlConfig payment_attempt_mc_cfg = 27;
}

message TrackPaymentRequest {
Expand Down
4 changes: 4 additions & 0 deletions lnrpc/routerrpc/router.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -2032,6 +2032,10 @@
"mission_control_namespace": {
"type": "string",
"description": "An optional mission control namespace that should be used for this payment.\nIf not specified, the default mission control namespace will be used."
},
"payment_attempt_mc_cfg": {
"$ref": "#/definitions/routerrpcMissionControlConfig",
"description": "An optional mission control configuration to use for this payment attempt.\nIf not specified, the global mission control config will be used."
}
}
},
Expand Down
18 changes: 16 additions & 2 deletions lnrpc/routerrpc/router_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@
// GetProbability is expected to return the success probability of a
// payment from fromNode to toNode.
GetProbability(fromNode, toNode route.Vertex,
amt lnwire.MilliSatoshi, capacity btcutil.Amount) float64
amt lnwire.MilliSatoshi, capacity btcutil.Amount,
opts ...routing.EstimatorOption) float64

// ResetHistory resets the history of MissionControl returning it to a
// state as if no payment attempts have been made.
Expand Down Expand Up @@ -387,10 +388,11 @@
}

restrictions := &routing.RestrictParams{
FeeLimit: feeLimit,

Check failure on line 391 in lnrpc/routerrpc/router_backend.go

View workflow job for this annotation

GitHub Actions / check commits

cannot use func(fromNode, toNode route.Vertex, amt lnwire.MilliSatoshi, capacity btcutil.Amount) float64 {…} (value of type func(fromNode route.Vertex, toNode route.Vertex, amt "github.com/lightningnetwork/lnd/lnwire".MilliSatoshi, capacity btcutil.Amount) float64) as func(route.Vertex, route.Vertex, "github.com/lightningnetwork/lnd/lnwire".MilliSatoshi, btcutil.Amount, ...routing.EstimatorOption) float64 value in struct literal
ProbabilitySource: func(fromNode, toNode route.Vertex,
amt lnwire.MilliSatoshi,
capacity btcutil.Amount) float64 {
capacity btcutil.Amount,
opts ...routing.EstimatorOption) float64 {

if _, ok := ignoredNodes[fromNode]; ok {
return 0
Expand Down Expand Up @@ -949,6 +951,18 @@
// Set the mission control namespace if specified.
payIntent.MissionControlNamespace = rpcPayReq.MissionControlNamespace

// If a per-payment mission control config is provided, create the
// specified estimator.
if rpcCfg := rpcPayReq.PaymentAttemptMcCfg; rpcCfg != nil {
estimator, err := createEstimatorFromConfig(
rpcCfg, r.MissionControl.GetConfig().Estimator,
)
if err != nil {
return nil, fmt.Errorf("failed to create payment attempt estimator: %w", err)
}
payIntent.PaymentAttemptMcConfig = estimator
}

// Route hints.
routeHints, err := unmarshallRouteHints(
rpcPayReq.RouteHints,
Expand Down
3 changes: 2 additions & 1 deletion lnrpc/routerrpc/router_backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,8 @@ type mockMissionControl struct {
}

func (m *mockMissionControl) GetProbability(fromNode, toNode route.Vertex,
amt lnwire.MilliSatoshi, capacity btcutil.Amount) float64 {
amt lnwire.MilliSatoshi, capacity btcutil.Amount,
_ ...routing.EstimatorOption) float64 {

return testMissionControlProb
}
Expand Down
130 changes: 66 additions & 64 deletions lnrpc/routerrpc/router_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@
&routing.RestrictParams{
FeeLimit: routeFeeLimitSat,
CltvLimit: s.cfg.RouterBackend.MaxTotalTimelock,
ProbabilitySource: mc.GetProbability,

Check failure on line 479 in lnrpc/routerrpc/router_server.go

View workflow job for this annotation

GitHub Actions / check commits

cannot use mc.GetProbability (value of type func(fromNode route.Vertex, toNode route.Vertex, amt "github.com/lightningnetwork/lnd/lnwire".MilliSatoshi, capacity btcutil.Amount) float64) as func(route.Vertex, route.Vertex, "github.com/lightningnetwork/lnd/lnwire".MilliSatoshi, btcutil.Amount, ...routing.EstimatorOption) float64 value in struct literal
}, nil, nil, nil, s.cfg.RouterBackend.DefaultFinalCltvDelta,
)
if err != nil {
Expand Down Expand Up @@ -1037,86 +1037,88 @@
return resp, nil
}

// SetMissionControlConfig sets parameters in the mission control config.
func (s *Server) SetMissionControlConfig(ctx context.Context,
req *SetMissionControlConfigRequest) (*SetMissionControlConfigResponse,
error) {
// createEstimatorFromConfig creates a routing.Estimator based on the provided
// RPC MissionControlConfig and a global estimator to use as a base for default
// values.
func createEstimatorFromConfig(rpcCfg *MissionControlConfig,
globalEstimator routing.Estimator) (routing.Estimator, error) {

mcCfg := &routing.MissionControlConfig{
MaxMcHistory: int(req.Config.MaximumPaymentResults),
MinFailureRelaxInterval: time.Duration(
req.Config.MinimumFailureRelaxInterval,
) * time.Second,
if rpcCfg == nil || rpcCfg.EstimatorConfig == nil {
return nil, nil
}

switch req.Config.Model {
case MissionControlConfig_APRIORI:
var aprioriConfig routing.AprioriConfig

// Determine the apriori config with backward compatibility
// should the api use deprecated fields.
switch v := req.Config.EstimatorConfig.(type) {
case *MissionControlConfig_Bimodal:
return nil, fmt.Errorf("bimodal config " +
"provided, but apriori model requested")

case *MissionControlConfig_Apriori:
aprioriConfig = routing.AprioriConfig{
PenaltyHalfLife: time.Duration(
v.Apriori.HalfLifeSeconds,
) * time.Second,
AprioriHopProbability: v.Apriori.HopProbability,
AprioriWeight: v.Apriori.Weight,
CapacityFraction: v.Apriori.
CapacityFraction,
}
var (
estimator routing.Estimator
err error
)

default:
aprioriConfig = routing.AprioriConfig{
PenaltyHalfLife: time.Duration(
int64(req.Config.HalfLifeSeconds),
) * time.Second,
AprioriHopProbability: float64(
req.Config.HopProbability,
),
AprioriWeight: float64(req.Config.Weight),
CapacityFraction: routing.DefaultCapacityFraction, //nolint:ll
}
switch estCfg := rpcCfg.EstimatorConfig.(type) {
case *MissionControlConfig_Apriori:
if estCfg.Apriori == nil {
return nil, fmt.Errorf("apriori config not provided")
}

estimator, err := routing.NewAprioriEstimator(aprioriConfig)
baseCfg := routing.DefaultAprioriConfig()
if globalApriori, ok := globalEstimator.(*routing.AprioriEstimator); ok {
baseCfg = globalApriori.Config().(routing.AprioriConfig)

Check failure on line 1063 in lnrpc/routerrpc/router_server.go

View workflow job for this annotation

GitHub Actions / lint code

type assertion must be checked (forcetypeassert)
}

baseCfg.PenaltyHalfLife = time.Duration(estCfg.Apriori.HalfLifeSeconds) * time.Second
baseCfg.AprioriHopProbability = estCfg.Apriori.HopProbability
baseCfg.AprioriWeight = estCfg.Apriori.Weight
baseCfg.CapacityFraction = estCfg.Apriori.CapacityFraction
estimator, err = routing.NewAprioriEstimator(baseCfg)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to create apriori estimator: %w", err)
}
mcCfg.Estimator = estimator

case MissionControlConfig_BIMODAL:
cfg, ok := req.Config.
EstimatorConfig.(*MissionControlConfig_Bimodal)
if !ok {
return nil, fmt.Errorf("bimodal estimator requested " +
"but corresponding config not set")

case *MissionControlConfig_Bimodal:
if estCfg.Bimodal == nil {
return nil, fmt.Errorf("bimodal config not provided")
}
bCfg := cfg.Bimodal

bimodalConfig := routing.BimodalConfig{
BimodalDecayTime: time.Duration(
bCfg.DecayTime,
) * time.Second,
BimodalScaleMsat: lnwire.MilliSatoshi(bCfg.ScaleMsat),
BimodalNodeWeight: bCfg.NodeWeight,

baseCfg := routing.DefaultBimodalConfig()
if globalBimodal, ok := globalEstimator.(*routing.BimodalEstimator); ok {
baseCfg = globalBimodal.Config().(routing.BimodalConfig)

Check failure on line 1082 in lnrpc/routerrpc/router_server.go

View workflow job for this annotation

GitHub Actions / lint code

type assertion must be checked (forcetypeassert)
}

estimator, err := routing.NewBimodalEstimator(bimodalConfig)
baseCfg.BimodalNodeWeight = estCfg.Bimodal.NodeWeight
baseCfg.BimodalScaleMsat = lnwire.MilliSatoshi(estCfg.Bimodal.ScaleMsat)
baseCfg.BimodalDecayTime = time.Duration(estCfg.Bimodal.DecayTime) * time.Second
estimator, err = routing.NewBimodalEstimator(baseCfg)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to create bimodal estimator: %w", err)
}
mcCfg.Estimator = estimator

default:
return nil, fmt.Errorf("unknown estimator type %v",
req.Config.Model)
return nil, fmt.Errorf("unknown estimator type in config")
}

return estimator, nil
}

// SetMissionControlConfig sets parameters in the mission control config.
func (s *Server) SetMissionControlConfig(ctx context.Context,
req *SetMissionControlConfigRequest) (*SetMissionControlConfigResponse,
error) {

mcCfg := &routing.MissionControlConfig{
MaxMcHistory: int(req.Config.MaximumPaymentResults),
MinFailureRelaxInterval: time.Duration(
req.Config.MinimumFailureRelaxInterval,
) * time.Second,
}

// Use the new helper to create the estimator based on the request config.
// We pass the current global estimator from MissionControl to serve as a
// base for default values if some fields are not set in the request.
estimator, err := createEstimatorFromConfig(
req.Config, s.cfg.RouterBackend.MissionControl.GetConfig().Estimator,
)
if err != nil {
return nil, fmt.Errorf("failed to create estimator from config: %w", err)
}
mcCfg.Estimator = estimator

return &SetMissionControlConfigResponse{},
s.cfg.RouterBackend.MissionControl.SetConfig(mcCfg)
Expand Down
37 changes: 32 additions & 5 deletions routing/missioncontrol.go
Original file line number Diff line number Diff line change
Expand Up @@ -535,23 +535,50 @@ func (m *MissionControl) ResetHistory() error {
return nil
}

// GetProbability is expected to return the success probability of a payment
// from fromNode along edge.
// EstimatorOption defines a functional option for the GetProbability method.
type EstimatorOption func(*estimatorOverride)

type estimatorOverride struct {
estimator Estimator
}

// WithEstimator is a functional option that allows specifying a custom
// estimator for a GetProbability call.
func WithEstimator(estimator Estimator) EstimatorOption {
return func(eo *estimatorOverride) {
eo.estimator = estimator
}
}

// GetProbability is expected to return the success probability of a
// payment from fromNode along edge. Optional EstimatorOption can be
// passed to override the default estimator for this specific call.
func (m *MissionControl) GetProbability(fromNode, toNode route.Vertex,
amt lnwire.MilliSatoshi, capacity btcutil.Amount) float64 {
amt lnwire.MilliSatoshi, capacity btcutil.Amount,
opts ...EstimatorOption) float64 {

override := &estimatorOverride{}
for _, opt := range opts {
opt(override)
}

m.mu.Lock()
defer m.mu.Unlock()

now := m.cfg.clock.Now()
results, _ := m.state.getLastPairResult(fromNode)

estimatorToUse := m.estimator
if override.estimator != nil {
estimatorToUse = override.estimator
}

// Use a distinct probability estimation function for local channels.
if fromNode == m.cfg.selfNode {
return m.estimator.LocalPairProbability(now, results, toNode)
return estimatorToUse.LocalPairProbability(now, results, toNode)
}

return m.estimator.PairProbability(
return estimatorToUse.PairProbability(
now, results, toNode, amt, capacity,
)
}
Expand Down
7 changes: 4 additions & 3 deletions routing/mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ func (m *mockMissionControlOld) ReportPaymentSuccess(paymentID uint64,
}

func (m *mockMissionControlOld) GetProbability(fromNode, toNode route.Vertex,
amt lnwire.MilliSatoshi, capacity btcutil.Amount) float64 {
amt lnwire.MilliSatoshi, capacity btcutil.Amount, _ ...EstimatorOption) float64 {

return 0
}
Expand Down Expand Up @@ -687,9 +687,10 @@ func (m *mockMissionControl) ReportPaymentSuccess(paymentID uint64,
}

func (m *mockMissionControl) GetProbability(fromNode, toNode route.Vertex,
amt lnwire.MilliSatoshi, capacity btcutil.Amount) float64 {
amt lnwire.MilliSatoshi, capacity btcutil.Amount,
opts ...EstimatorOption) float64 {

args := m.Called(fromNode, toNode, amt, capacity)
args := m.Called(fromNode, toNode, amt, capacity, opts)
return args.Get(0).(float64)
}

Expand Down
3 changes: 2 additions & 1 deletion routing/pathfind.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,8 @@ type RestrictParams struct {
// ProbabilitySource is a callback that is expected to return the
// success probability of traversing the channel from the node.
ProbabilitySource func(route.Vertex, route.Vertex,
lnwire.MilliSatoshi, btcutil.Amount) float64
lnwire.MilliSatoshi, btcutil.Amount,
...EstimatorOption) float64

// FeeLimit is a maximum fee amount allowed to be used on the path from
// the source to the target.
Expand Down
8 changes: 4 additions & 4 deletions routing/pathfind_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ var (

// noProbabilitySource is used in testing to return the same probability 1 for
// all edges.
func noProbabilitySource(route.Vertex, route.Vertex, lnwire.MilliSatoshi,
btcutil.Amount) float64 {
func noProbabilitySource(_ route.Vertex, _ route.Vertex,
_ lnwire.MilliSatoshi, _ btcutil.Amount, _ ...EstimatorOption) float64 {

return 1
}
Expand Down Expand Up @@ -2766,7 +2766,7 @@ func testProbabilityRouting(t *testing.T, useCache bool,
// Configure a probability source with the test parameters.
ctx.restrictParams.ProbabilitySource = func(fromNode,
toNode route.Vertex, amt lnwire.MilliSatoshi,
capacity btcutil.Amount) float64 {
capacity btcutil.Amount, opts ...EstimatorOption) float64 {

if amt == 0 {
t.Fatal("expected non-zero amount")
Expand Down Expand Up @@ -2849,7 +2849,7 @@ func runEqualCostRouteSelection(t *testing.T, useCache bool) {

ctx.restrictParams.ProbabilitySource = func(fromNode,
toNode route.Vertex, amt lnwire.MilliSatoshi,
capacity btcutil.Amount) float64 {
capacity btcutil.Amount, opts ...EstimatorOption) float64 {

switch {
case fromNode == alias["source"] && toNode == alias["a"]:
Expand Down
Loading
Loading