mirror of
https://github.com/lightninglabs/faraday.git
synced 2026-08-13 12:33:35 +02:00
169 lines
5 KiB
Go
169 lines
5 KiB
Go
package frdrpcserver
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"math"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
"github.com/lightninglabs/faraday/frdrpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
// defaultLiquidityFloorSat is the liquidity floor applied when the request
|
|
// leaves liquidity_floor_sat unset. It approximates the smallest amount a
|
|
// rebalancer would still move, below which a pair is not economically
|
|
// forwardable.
|
|
const defaultLiquidityFloorSat = 50_000
|
|
|
|
// defaultUptimeThreshold is the uptime fraction applied when the request leaves
|
|
// uptime_threshold unset. A pair that was up at least this fraction of the
|
|
// window but did not forward is reported as a single bit rather than a full
|
|
// entry. It is high so that only reliably available pairs are flagged, keeping
|
|
// the response sparse and the node-down guard meaningful.
|
|
const defaultUptimeThreshold = 0.9
|
|
|
|
// ForwardingAbility returns the raw effective-uptime and forwarded-volume facts
|
|
// for each peer pair over the requested window. An unset end_time defaults to
|
|
// the current time and an unset liquidity_floor_sat to
|
|
// defaultLiquidityFloorSat.
|
|
func (s *RPCServer) ForwardingAbility(ctx context.Context,
|
|
req *frdrpc.ForwardingAbilityRequest) (
|
|
*frdrpc.ForwardingAbilityResponse, error) {
|
|
|
|
log.DebugS(
|
|
ctx, "Handling ForwardingAbility request",
|
|
slog.Uint64("start_time", req.StartTime),
|
|
slog.Uint64("end_time", req.EndTime),
|
|
slog.Uint64("liquidity_floor_sat", req.LiquidityFloorSat),
|
|
)
|
|
|
|
// time.Unix takes an int64, so reject any request value that would
|
|
// overflow when its uint64 seconds are narrowed below.
|
|
if req.StartTime > math.MaxInt64 {
|
|
return nil, status.Error(
|
|
codes.InvalidArgument,
|
|
"start_time exceeds maximum allowed value",
|
|
)
|
|
}
|
|
if req.EndTime > math.MaxInt64 {
|
|
return nil, status.Error(
|
|
codes.InvalidArgument,
|
|
"end_time exceeds maximum allowed value",
|
|
)
|
|
}
|
|
|
|
startTime := time.Unix(int64(req.StartTime), 0)
|
|
endTime := time.Now()
|
|
if req.EndTime != 0 {
|
|
endTime = time.Unix(int64(req.EndTime), 0)
|
|
}
|
|
|
|
if startTime.After(endTime) {
|
|
return nil, status.Error(
|
|
codes.InvalidArgument,
|
|
"start_time must be less than or equal to end_time",
|
|
)
|
|
}
|
|
|
|
if s.cfg.ForwardingAnalyzer == nil {
|
|
return nil, status.Error(
|
|
codes.Unavailable,
|
|
"forwarding analyzer is not configured",
|
|
)
|
|
}
|
|
|
|
liquidityFloor := req.LiquidityFloorSat
|
|
if liquidityFloor == 0 {
|
|
liquidityFloor = defaultLiquidityFloorSat
|
|
}
|
|
|
|
uptimeThreshold := req.UptimeThreshold
|
|
if uptimeThreshold == 0 {
|
|
uptimeThreshold = defaultUptimeThreshold
|
|
}
|
|
|
|
// Reject NaN explicitly: NaN comparisons are always false, so a bare
|
|
// range check would let it slip through.
|
|
if math.IsNaN(uptimeThreshold) || uptimeThreshold < 0 ||
|
|
uptimeThreshold > 1 {
|
|
|
|
return nil, status.Error(
|
|
codes.InvalidArgument,
|
|
"uptime_threshold must be in [0, 1]",
|
|
)
|
|
}
|
|
|
|
abilities, err := s.cfg.ForwardingAnalyzer.EffectiveUptime(
|
|
ctx, startTime, endTime, btcutil.Amount(liquidityFloor),
|
|
)
|
|
if err != nil {
|
|
log.ErrorS(
|
|
ctx, "EffectiveUptime failed", err,
|
|
slog.Time("start_time", startTime),
|
|
slog.Time("end_time", endTime),
|
|
slog.Uint64("liquidity_floor_sat", liquidityFloor),
|
|
)
|
|
|
|
return nil, status.Errorf(codes.Internal, "failed to "+
|
|
"calculate effective uptime: %v", err)
|
|
}
|
|
|
|
// Convert the flat map into the nested map the codec expects, carrying
|
|
// the raw facts through unchanged. EffectiveUptime is truncated to
|
|
// whole seconds to match the second-granularity wire field. A pair with
|
|
// only sub-second qualifying uptime therefore reports zero uptime while
|
|
// still carrying its forwarded volume.
|
|
nested := make(map[string]map[string]frdrpc.ForwardingAbility)
|
|
for pair, ability := range abilities {
|
|
if _, ok := nested[pair.PeerIn]; !ok {
|
|
nested[pair.PeerIn] =
|
|
make(map[string]frdrpc.ForwardingAbility)
|
|
}
|
|
|
|
nested[pair.PeerIn][pair.PeerOut] = frdrpc.ForwardingAbility{
|
|
EffectiveUptimeS: int64(
|
|
ability.EffectiveUptime.Seconds(),
|
|
),
|
|
ForwardedSat: int64(ability.ForwardedAmount),
|
|
}
|
|
}
|
|
|
|
// Guard against returning data when the node itself was down for the
|
|
// window. If no pair held at least the threshold fraction of uptime,
|
|
// the response carries no signal and lowering the threshold to surface
|
|
// something would only inflate it, so fail loudly instead.
|
|
minUptimeS := frdrpc.MinQualifyingUptime(
|
|
uptimeThreshold, endTime.Unix()-startTime.Unix(),
|
|
)
|
|
var qualifying int
|
|
for _, outMap := range nested {
|
|
for _, ability := range outMap {
|
|
if ability.EffectiveUptimeS >= minUptimeS {
|
|
qualifying++
|
|
}
|
|
}
|
|
}
|
|
if qualifying == 0 {
|
|
return nil, status.Error(codes.FailedPrecondition, "no peer "+
|
|
"pair met the uptime threshold over the window; the "+
|
|
"node may have been offline")
|
|
}
|
|
|
|
resp, err := frdrpc.EncodeForwardingAbility(
|
|
nested, startTime.Unix(), endTime.Unix(), uptimeThreshold,
|
|
)
|
|
if err != nil {
|
|
log.ErrorS(
|
|
ctx, "EncodeForwardingAbility failed", err,
|
|
slog.Int("pairs", len(abilities)),
|
|
)
|
|
|
|
return nil, status.Errorf(codes.Internal, "failed to encode "+
|
|
"forwarding ability: %v", err)
|
|
}
|
|
|
|
return resp, nil
|
|
}
|