mirror of
https://github.com/lightninglabs/faraday.git
synced 2026-08-13 12:33:35 +02:00
Merge pull request #243 from bitromortac/2604-fwd-prep-12
rpc: add sparse `ForwardingAbility` endpoint
This commit is contained in:
commit
edc3a405e6
24 changed files with 2744 additions and 586 deletions
|
|
@ -6,7 +6,6 @@ import (
|
|||
"fmt"
|
||||
"iter"
|
||||
"log/slog"
|
||||
"math"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
|
|
@ -38,7 +37,8 @@ type EventsSource interface {
|
|||
|
||||
// GetChannelEvents fetches up to limit events for a channel with id >
|
||||
// afterID and timestamp in [startTime, endTime), ordered by id ASC.
|
||||
// A large limit (e.g. math.MaxInt32) retrieves the entire range.
|
||||
// Callers page through a range by passing the last returned id as
|
||||
// afterID until a short page comes back.
|
||||
GetChannelEvents(ctx context.Context, channelID, afterID int64,
|
||||
startTime, endTime time.Time,
|
||||
limit int32) ([]*ChannelEvent, error)
|
||||
|
|
@ -64,21 +64,19 @@ type ForwardingAnalyzer struct {
|
|||
// paired with a propagated error value.
|
||||
type channelEventSeq = iter.Seq2[*ChannelEvent, error]
|
||||
|
||||
// ForwardingAbility quantifies the historical routing performance of a peer
|
||||
// pair. Inconsistent flags the pathological case where forwards were observed
|
||||
// without the pair ever crossing the liquidity threshold; Velocity is zero in
|
||||
// that case because the rate is undefined over zero qualifying uptime.
|
||||
// ForwardingAbility holds the raw forwarding facts for one direction of a peer
|
||||
// pair over the analysis window. It carries no derived rates or categories. The
|
||||
// consumer derives velocity and uptime fraction from these and the window, and
|
||||
// reconstructs any categorization (such as forwards observed without qualifying
|
||||
// uptime) from EffectiveUptime and ForwardedAmount.
|
||||
type ForwardingAbility struct {
|
||||
// Velocity is the forwarding velocity in sat/s during effective uptime.
|
||||
Velocity float64
|
||||
// EffectiveUptime is the time the pair held at least the liquidity floor
|
||||
// of directional forwardable liquidity over the window.
|
||||
EffectiveUptime time.Duration
|
||||
|
||||
// UptimeFraction is the ratio of effective uptime to the full window
|
||||
// duration, in [0, 1].
|
||||
UptimeFraction float64
|
||||
|
||||
// Inconsistent is set when forwards landed but effective uptime was
|
||||
// zero, indicating the input data and the threshold model disagree.
|
||||
Inconsistent bool
|
||||
// ForwardedAmount is the total successfully forwarded amount over the
|
||||
// window.
|
||||
ForwardedAmount btcutil.Amount
|
||||
}
|
||||
|
||||
// PeerPair identifies a unidirectional routing edge from PeerIn to PeerOut.
|
||||
|
|
@ -89,13 +87,6 @@ type PeerPair struct {
|
|||
PeerOut string
|
||||
}
|
||||
|
||||
// pairInputs encapsulates the routing performance thresholds for a single
|
||||
// direction.
|
||||
type pairInputs struct {
|
||||
threshold btcutil.Amount
|
||||
totalSuccessfulAmount btcutil.Amount
|
||||
}
|
||||
|
||||
// channelState is the per-channel snapshot the uptime walk carries forward as
|
||||
// it consumes events: liveness plus the two balances that determine forwarding
|
||||
// liquidity.
|
||||
|
|
@ -117,25 +108,18 @@ func NewForwardingAnalyzer(store EventsSource,
|
|||
|
||||
// EffectiveUptime returns a ForwardingAbility for every (peerIn, peerOut) pair
|
||||
// over [startTime, endTime). Closed channels are folded into the considered set
|
||||
// so survivorship bias does not skew the uptime denominator. The liquidity
|
||||
// floor is the fwdPercentile-th percentile of successful forward amounts (with
|
||||
// fwdPercentile in [0, 100]), bounded below by threshold. When forwards land
|
||||
// but the floor is never crossed, the returned ability is flagged Inconsistent.
|
||||
// so survivorship bias does not skew the uptime denominator. A single
|
||||
// liquidityFloor is applied uniformly to every pair, so effective uptime is the
|
||||
// time each pair held at least that much directional forwardable liquidity.
|
||||
func (a *ForwardingAnalyzer) EffectiveUptime(ctx context.Context, startTime,
|
||||
endTime time.Time, fwdPercentile float64, threshold btcutil.Amount) (
|
||||
endTime time.Time, liquidityFloor btcutil.Amount) (
|
||||
map[PeerPair]ForwardingAbility, error) {
|
||||
|
||||
if fwdPercentile < 0 || fwdPercentile > 100 {
|
||||
return nil, fmt.Errorf("fwdPercentile %v outside [0, 100]",
|
||||
fwdPercentile)
|
||||
}
|
||||
|
||||
log.DebugS(
|
||||
ctx, "Calculating effective uptime",
|
||||
slog.Time("startTime", startTime),
|
||||
slog.Time("endTime", endTime),
|
||||
slog.Float64("fwdPercentile", fwdPercentile),
|
||||
slog.Int64("threshold", int64(threshold)),
|
||||
slog.Int64("liquidityFloor", int64(liquidityFloor)),
|
||||
)
|
||||
|
||||
scidToPeer, err := a.store.ScidToPeerMap(ctx)
|
||||
|
|
@ -175,37 +159,63 @@ func (a *ForwardingAnalyzer) EffectiveUptime(ctx context.Context, startTime,
|
|||
)
|
||||
|
||||
return calculateAllPairsUptime(
|
||||
ctx, a.store, startTime, endTime, fwdPercentile, threshold,
|
||||
ctx, a.store, startTime, endTime, liquidityFloor,
|
||||
successfulForwards, initialStates, peerChannels,
|
||||
)
|
||||
}
|
||||
|
||||
// getForwardingData returns successful forwards and channels from lnd's
|
||||
// forwarding history over [startTime, endTime), indexed by peer pair. Unknown
|
||||
// channels are skipped.
|
||||
// getForwardingData queries lnd's forwarding history sequentially in paginated
|
||||
// batches to retrieve successful forwarding events within the specified time
|
||||
// range, indexing the results by peer pair.
|
||||
func (a *ForwardingAnalyzer) getForwardingData(ctx context.Context, startTime,
|
||||
endTime time.Time, scidToPeer map[uint64]string) (
|
||||
map[PeerPair][]btcutil.Amount, map[uint64]string, error) {
|
||||
|
||||
fwds, err := a.lnd.Client.ForwardingHistory(
|
||||
ctx, lndclient.ForwardingHistoryRequest{
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
var events []lndclient.ForwardingEvent
|
||||
var offset uint32
|
||||
const forwardingPageSize = 1000
|
||||
|
||||
for {
|
||||
fwds, err := a.lnd.Client.ForwardingHistory(
|
||||
ctx, lndclient.ForwardingHistoryRequest{
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
Offset: offset,
|
||||
MaxEvents: forwardingPageSize,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if len(fwds.Events) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
events = append(events, fwds.Events...)
|
||||
if len(fwds.Events) < forwardingPageSize {
|
||||
break
|
||||
}
|
||||
|
||||
// Guard against a non-advancing offset: if lnd does not move
|
||||
// LastIndexOffset past the cursor we already queried, stop
|
||||
// rather than re-fetch the same page forever.
|
||||
if fwds.LastIndexOffset <= offset {
|
||||
break
|
||||
}
|
||||
offset = fwds.LastIndexOffset
|
||||
}
|
||||
|
||||
log.DebugS(
|
||||
ctx, "Found forwarding events",
|
||||
slog.Int(
|
||||
"count", len(fwds.Events),
|
||||
"count", len(events),
|
||||
),
|
||||
)
|
||||
|
||||
channelPeersConsidered := make(map[uint64]string)
|
||||
successfulForwards := make(map[PeerPair][]btcutil.Amount)
|
||||
for _, fwd := range fwds.Events {
|
||||
for _, fwd := range events {
|
||||
inPeer, ok := scidToPeer[fwd.ChannelIn]
|
||||
if !ok {
|
||||
log.WarnS(
|
||||
|
|
@ -416,7 +426,7 @@ func (a *ForwardingAnalyzer) getInitialChannelState(ctx context.Context,
|
|||
// calculateAllPairsUptime returns forwarding abilities for every peer pair,
|
||||
// computing both directions (A→B and B→A) in a single pass.
|
||||
func calculateAllPairsUptime(ctx context.Context, store EventsSource, startTime,
|
||||
endTime time.Time, fwdPercentile float64, threshold btcutil.Amount,
|
||||
endTime time.Time, liquidityFloor btcutil.Amount,
|
||||
successfulForwards map[PeerPair][]btcutil.Amount,
|
||||
initialStates map[string]map[int64]*channelState,
|
||||
peerChannels map[string][]int64) (
|
||||
|
|
@ -496,21 +506,12 @@ func calculateAllPairsUptime(ctx context.Context, store EventsSource, startTime,
|
|||
statesB := initialStates[peerB]
|
||||
sumsB := initialSums[peerB]
|
||||
|
||||
inputsAB, err := pairThresholdInputs(
|
||||
fwdPercentile, threshold, successfulForwards,
|
||||
peerA, peerB,
|
||||
forwardedAB := pairForwardedTotal(
|
||||
successfulForwards, peerA, peerB,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inputsBA, err := pairThresholdInputs(
|
||||
fwdPercentile, threshold, successfulForwards,
|
||||
peerB, peerA,
|
||||
forwardedBA := pairForwardedTotal(
|
||||
successfulForwards, peerB, peerA,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sliceB := sliceA
|
||||
if i != j {
|
||||
|
|
@ -523,11 +524,12 @@ func calculateAllPairsUptime(ctx context.Context, store EventsSource, startTime,
|
|||
abilityAB, abilityBA, err :=
|
||||
calculateBothDirectionsUptime(
|
||||
ctx, startTime, endTime,
|
||||
inputsAB, inputsBA,
|
||||
liquidityFloor,
|
||||
statesA, statesB,
|
||||
sumsA.remote, sumsA.local,
|
||||
sumsB.remote, sumsB.local,
|
||||
mergeEventSlices(sliceA, sliceB),
|
||||
forwardedAB, forwardedBA,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -543,22 +545,39 @@ func calculateAllPairsUptime(ctx context.Context, store EventsSource, startTime,
|
|||
return results, nil
|
||||
}
|
||||
|
||||
// eventPageSize bounds a single channel-event page so the per-channel fetch
|
||||
// never asks the store for an unbounded result set.
|
||||
const eventPageSize = 1000
|
||||
|
||||
// loadPeerEvents fetches every event in [startTime, endTime) on the given
|
||||
// channels and returns them merged into a single chronologically sorted slice.
|
||||
// Events sharing a timestamp are ordered by ascending id so the result is
|
||||
// deterministic.
|
||||
// Each channel is paged through in id-ascending batches so no single store
|
||||
// query is unbounded. Events sharing a timestamp are ordered by ascending id so
|
||||
// the result is deterministic.
|
||||
func loadPeerEvents(ctx context.Context, store EventsSource, startTime,
|
||||
endTime time.Time, chanIDs []int64) ([]*ChannelEvent, error) {
|
||||
|
||||
var events []*ChannelEvent
|
||||
for _, chanID := range chanIDs {
|
||||
chanEvents, err := store.GetChannelEvents(
|
||||
ctx, chanID, 0, startTime, endTime, math.MaxInt32,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var afterID int64
|
||||
for {
|
||||
page, err := store.GetChannelEvents(
|
||||
ctx, chanID, afterID, startTime, endTime,
|
||||
eventPageSize,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
events = append(events, page...)
|
||||
if len(page) < eventPageSize {
|
||||
break
|
||||
}
|
||||
|
||||
// Events come back id-ASC, so the last id is the
|
||||
// largest; continue the next page after it.
|
||||
afterID = page[len(page)-1].ID
|
||||
}
|
||||
events = append(events, chanEvents...)
|
||||
}
|
||||
|
||||
sort.SliceStable(
|
||||
|
|
@ -575,57 +594,32 @@ func loadPeerEvents(ctx context.Context, store EventsSource, startTime,
|
|||
return events, nil
|
||||
}
|
||||
|
||||
// pairThresholdInputs resolves the liquidity floor and cumulative forwarded
|
||||
// amount for one direction of a peer pair, applying the percentile rule when
|
||||
// historical forwards exist.
|
||||
func pairThresholdInputs(fwdPercentile float64, threshold btcutil.Amount,
|
||||
successfulForwards map[PeerPair][]btcutil.Amount,
|
||||
peerIn, peerOut string) (pairInputs, error) {
|
||||
|
||||
successAmts := successfulForwards[PeerPair{
|
||||
PeerIn: peerIn, PeerOut: peerOut,
|
||||
}]
|
||||
t, err := determineThreshold(fwdPercentile, threshold, successAmts)
|
||||
if err != nil {
|
||||
return pairInputs{}, err
|
||||
}
|
||||
// pairForwardedTotal sums the successfully forwarded amounts for one direction
|
||||
// of a peer pair over the analysis window.
|
||||
func pairForwardedTotal(successfulForwards map[PeerPair][]btcutil.Amount,
|
||||
peerIn, peerOut string) btcutil.Amount {
|
||||
|
||||
var total btcutil.Amount
|
||||
for _, amt := range successAmts {
|
||||
for _, amt := range successfulForwards[PeerPair{
|
||||
PeerIn: peerIn, PeerOut: peerOut,
|
||||
}] {
|
||||
total += amt
|
||||
}
|
||||
|
||||
return pairInputs{threshold: t, totalSuccessfulAmount: total}, nil
|
||||
}
|
||||
|
||||
// determineThreshold establishes the required liquidity floor based on the
|
||||
// user's manual threshold or the calculated percentile of successful forwards.
|
||||
func determineThreshold(forwardPercentile float64,
|
||||
thresholdAmount btcutil.Amount,
|
||||
successAmts []btcutil.Amount) (btcutil.Amount, error) {
|
||||
|
||||
if len(successAmts) == 0 {
|
||||
return thresholdAmount, nil
|
||||
}
|
||||
|
||||
q := forwardPercentile / 100
|
||||
p, err := Quantile(successAmts, q)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return max(btcutil.Amount(math.RoundToEven(p)), thresholdAmount), nil
|
||||
return total
|
||||
}
|
||||
|
||||
// calculateBothDirectionsUptime computes the effective forwarding uptime for
|
||||
// both directions of a peer pair in a single chronological walk of the merged
|
||||
// event stream. Only the liquidity-direction roles and the per-direction
|
||||
// thresholds differ between the two accumulators. For self-pair calls (statesA
|
||||
// == statesB, inputsAB == inputsBA) both returned abilities are equal.
|
||||
// event stream. Only the liquidity-direction roles differ between the two
|
||||
// accumulators, as both share the same uniform liquidityFloor. forwardedAB and
|
||||
// forwardedBA carry each direction's total forwarded volume through to the
|
||||
// returned abilities. For self-pair calls both returned abilities are equal.
|
||||
func calculateBothDirectionsUptime(ctx context.Context, startTime,
|
||||
endTime time.Time, inputsAB, inputsBA pairInputs, statesA,
|
||||
endTime time.Time, liquidityFloor btcutil.Amount, statesA,
|
||||
statesB map[int64]*channelState, sumARemote, sumALocal, sumBRemote,
|
||||
sumBLocal btcutil.Amount, mergedEvents channelEventSeq) (
|
||||
sumBLocal btcutil.Amount, mergedEvents channelEventSeq,
|
||||
forwardedAB, forwardedBA btcutil.Amount) (
|
||||
*ForwardingAbility, *ForwardingAbility, error) {
|
||||
|
||||
traceOn := log.Level() <= btclog.LevelTrace
|
||||
|
|
@ -667,13 +661,8 @@ func calculateBothDirectionsUptime(ctx context.Context, startTime,
|
|||
)
|
||||
}
|
||||
log.TraceS(
|
||||
ctx, "Using final forwarding liquidity thresholds",
|
||||
slog.Int64(
|
||||
"thresholdAB", int64(inputsAB.threshold),
|
||||
),
|
||||
slog.Int64(
|
||||
"thresholdBA", int64(inputsBA.threshold),
|
||||
),
|
||||
ctx, "Using uniform forwarding liquidity floor",
|
||||
slog.Int64("liquidityFloor", int64(liquidityFloor)),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -704,10 +693,16 @@ func calculateBothDirectionsUptime(ctx context.Context, startTime,
|
|||
),
|
||||
)
|
||||
}
|
||||
if liqAB > inputsAB.threshold {
|
||||
|
||||
// A direction qualifies when its bottleneck liquidity is at
|
||||
// least the floor, matching the "at least" contract documented
|
||||
// on the ForwardingAbility proto, struct, and CLI flag. The
|
||||
// liquidity must also be strictly positive: zero forwardable
|
||||
// liquidity can never route a payment, even when the floor is 0.
|
||||
if liqAB >= liquidityFloor && liqAB > 0 {
|
||||
uptimeAB += intervalDuration
|
||||
}
|
||||
if liqBA > inputsBA.threshold {
|
||||
if liqBA >= liquidityFloor && liqBA > 0 {
|
||||
uptimeBA += intervalDuration
|
||||
}
|
||||
}
|
||||
|
|
@ -792,12 +787,8 @@ func calculateBothDirectionsUptime(ctx context.Context, startTime,
|
|||
)
|
||||
}
|
||||
|
||||
abilityAB := makeAbility(
|
||||
startTime, endTime, uptimeAB, inputsAB.totalSuccessfulAmount,
|
||||
)
|
||||
abilityBA := makeAbility(
|
||||
startTime, endTime, uptimeBA, inputsBA.totalSuccessfulAmount,
|
||||
)
|
||||
abilityAB := makeAbility(uptimeAB, forwardedAB)
|
||||
abilityBA := makeAbility(uptimeBA, forwardedBA)
|
||||
|
||||
return abilityAB, abilityBA, nil
|
||||
}
|
||||
|
|
@ -892,19 +883,13 @@ func applyEvent(state *channelState, event *ChannelEvent) error {
|
|||
}
|
||||
|
||||
// makeAbility folds an accumulated uptime and successful-amount total into a
|
||||
// ForwardingAbility. When uptime is zero and forwards landed, the result is
|
||||
// flagged Inconsistent with zero Velocity.
|
||||
func makeAbility(startTime, endTime time.Time, totalUptime time.Duration,
|
||||
// ForwardingAbility carrying the raw facts. Derived rates and categories are
|
||||
// left to the consumer.
|
||||
func makeAbility(totalUptime time.Duration,
|
||||
totalAmt btcutil.Amount) *ForwardingAbility {
|
||||
|
||||
if totalUptime == 0 {
|
||||
return &ForwardingAbility{Inconsistent: totalAmt > 0}
|
||||
}
|
||||
|
||||
totalDuration := endTime.Sub(startTime)
|
||||
|
||||
return &ForwardingAbility{
|
||||
Velocity: float64(totalAmt) / totalUptime.Seconds(),
|
||||
UptimeFraction: float64(totalUptime) / float64(totalDuration),
|
||||
EffectiveUptime: totalUptime,
|
||||
ForwardedAmount: totalAmt,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -225,12 +225,10 @@ func TestCalculateBothDirectionsUptime(t *testing.T) {
|
|||
inEvents []*ChannelEvent
|
||||
outEvents []*ChannelEvent
|
||||
|
||||
successAmts []btcutil.Amount
|
||||
thresholdAmount btcutil.Amount
|
||||
forwardPercentile float64
|
||||
successAmts []btcutil.Amount
|
||||
liquidityFloor btcutil.Amount
|
||||
|
||||
expected *ForwardingAbility
|
||||
expectedErr string
|
||||
expectedUptime time.Duration
|
||||
}{
|
||||
{
|
||||
name: "Basic case always online",
|
||||
|
|
@ -249,10 +247,28 @@ func TestCalculateBothDirectionsUptime(t *testing.T) {
|
|||
successAmts: []btcutil.Amount{
|
||||
100,
|
||||
},
|
||||
expected: &ForwardingAbility{
|
||||
Velocity: 1, // 100 sats / 100s
|
||||
UptimeFraction: 1.0,
|
||||
expectedUptime: 100 * time.Second,
|
||||
},
|
||||
{
|
||||
// Liquidity sits exactly on the floor for the whole
|
||||
// window. The "at least" contract counts equality, so
|
||||
// the pair accrues the full window (a strict greater-
|
||||
// than comparison would wrongly report zero).
|
||||
name: "Liquidity exactly at floor qualifies",
|
||||
inStates: map[int64]*channelState{
|
||||
chanInID: {
|
||||
online: true,
|
||||
remoteBalance: 500,
|
||||
},
|
||||
},
|
||||
outStates: map[int64]*channelState{
|
||||
chanOutID: {
|
||||
online: true,
|
||||
localBalance: 500,
|
||||
},
|
||||
},
|
||||
liquidityFloor: 500,
|
||||
expectedUptime: 100 * time.Second,
|
||||
},
|
||||
{
|
||||
// The forward in successAmts updates both channels:
|
||||
|
|
@ -297,16 +313,13 @@ func TestCalculateBothDirectionsUptime(t *testing.T) {
|
|||
successAmts: []btcutil.Amount{
|
||||
200,
|
||||
},
|
||||
thresholdAmount: 900,
|
||||
liquidityFloor: 900,
|
||||
// t=100..150 (50s): liq = min(1500, 1000) = 1000
|
||||
// > 900 → qualifies.
|
||||
// t=150..200 (50s): liq = min(1300, 800) = 800
|
||||
// < 900 → drops out.
|
||||
// Total uptime = 50s, total amount = 200 sats.
|
||||
expected: &ForwardingAbility{
|
||||
Velocity: 4, // 200 sats / 50s
|
||||
UptimeFraction: 0.5,
|
||||
},
|
||||
expectedUptime: 50 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "Channel goes offline",
|
||||
|
|
@ -328,11 +341,8 @@ func TestCalculateBothDirectionsUptime(t *testing.T) {
|
|||
successAmts: []btcutil.Amount{
|
||||
100,
|
||||
},
|
||||
thresholdAmount: 1,
|
||||
expected: &ForwardingAbility{
|
||||
Velocity: 2, // 100 sats / 50s
|
||||
UptimeFraction: 0.5,
|
||||
},
|
||||
liquidityFloor: 1,
|
||||
expectedUptime: 50 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "Balance change",
|
||||
|
|
@ -357,15 +367,12 @@ func TestCalculateBothDirectionsUptime(t *testing.T) {
|
|||
successAmts: []btcutil.Amount{
|
||||
100,
|
||||
},
|
||||
thresholdAmount: 1,
|
||||
liquidityFloor: 1,
|
||||
// Balance changes at t=150, so for the first 50s the
|
||||
// liquidity is 800, then it's 1000 for the next 50s.
|
||||
// The total effective uptime is 100s, because the
|
||||
// liquidity threshold is low.
|
||||
expected: &ForwardingAbility{
|
||||
Velocity: 1, // 100 sats / 100s
|
||||
UptimeFraction: 1,
|
||||
},
|
||||
expectedUptime: 100 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "Duplicate event timestamps",
|
||||
|
|
@ -397,10 +404,7 @@ func TestCalculateBothDirectionsUptime(t *testing.T) {
|
|||
// (50s), liquidity is min(1000, 800) = 800. After
|
||||
// t=150, chanIn is offline, so liquidity is 0 for the
|
||||
// remaining 50s.
|
||||
expected: &ForwardingAbility{
|
||||
Velocity: 2, // 100 sats / 50s
|
||||
UptimeFraction: 0.5,
|
||||
},
|
||||
expectedUptime: 50 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "No initial state",
|
||||
|
|
@ -427,17 +431,13 @@ func TestCalculateBothDirectionsUptime(t *testing.T) {
|
|||
successAmts: []btcutil.Amount{
|
||||
100,
|
||||
},
|
||||
thresholdAmount: 1,
|
||||
liquidityFloor: 1,
|
||||
// We don't have initial balance states, so we can't
|
||||
// determine liquidity until we see an event on both
|
||||
// channels. At t=140 we know the liquidity is 800, and
|
||||
// it's online for the remaining 60s of the 100s total.
|
||||
// So uptime fraction is 0.6 for 800.
|
||||
expected: &ForwardingAbility{
|
||||
// 100 sats / 60s
|
||||
Velocity: 1.6666666666666667,
|
||||
UptimeFraction: 0.6,
|
||||
},
|
||||
expectedUptime: 60 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "Multiple channels for out peer",
|
||||
|
|
@ -466,15 +466,12 @@ func TestCalculateBothDirectionsUptime(t *testing.T) {
|
|||
successAmts: []btcutil.Amount{
|
||||
100,
|
||||
},
|
||||
thresholdAmount: 900,
|
||||
liquidityFloor: 900,
|
||||
// We expect the liquidity to be the sum of the
|
||||
// available balances of the out channels. t=100-150:
|
||||
// min(1000, 800 + 500) = 1000 t=150-200: min(1000, 1200
|
||||
// + 500) = 1000
|
||||
expected: &ForwardingAbility{
|
||||
Velocity: 1, // 100 sats / 100s
|
||||
UptimeFraction: 1.0,
|
||||
},
|
||||
expectedUptime: 100 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "Circular payment ability",
|
||||
|
|
@ -505,13 +502,10 @@ func TestCalculateBothDirectionsUptime(t *testing.T) {
|
|||
successAmts: []btcutil.Amount{
|
||||
100,
|
||||
},
|
||||
thresholdAmount: 1,
|
||||
liquidityFloor: 1,
|
||||
// For the first 50s, liquidity is min(1000, 0) = 0. For
|
||||
// the next 50s, liquidity is min(500, 500) = 500.
|
||||
expected: &ForwardingAbility{
|
||||
Velocity: 2, // 100 sats / 50s
|
||||
UptimeFraction: 0.5,
|
||||
},
|
||||
expectedUptime: 50 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "Self route multiple channels",
|
||||
|
|
@ -551,19 +545,16 @@ func TestCalculateBothDirectionsUptime(t *testing.T) {
|
|||
successAmts: []btcutil.Amount{
|
||||
100,
|
||||
},
|
||||
thresholdAmount: 1500,
|
||||
liquidityFloor: 1500,
|
||||
// Initial fwdLiquidity = min(2000, 2000) = 2000. 2000 >
|
||||
// 1500, so first 50s accrue. At t=150, chanOut local
|
||||
// drops to 0. outStates total local becomes 1000 (from
|
||||
// chanIn). fwdLiquidity = min(2000, 1000) = 1000. 1000
|
||||
// is not > 1500, so last 50s do not accrue.
|
||||
expected: &ForwardingAbility{
|
||||
Velocity: 2, // 100 sats / 50s
|
||||
UptimeFraction: 0.5,
|
||||
},
|
||||
expectedUptime: 50 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "Zero uptime no forwards yields zero velocity",
|
||||
name: "Zero uptime no forwards",
|
||||
inStates: map[int64]*channelState{
|
||||
chanInID: {
|
||||
online: false,
|
||||
|
|
@ -574,13 +565,14 @@ func TestCalculateBothDirectionsUptime(t *testing.T) {
|
|||
online: false,
|
||||
},
|
||||
},
|
||||
expected: &ForwardingAbility{
|
||||
Velocity: 0,
|
||||
UptimeFraction: 0,
|
||||
},
|
||||
expectedUptime: 0,
|
||||
},
|
||||
{
|
||||
name: "Zero uptime with forwards is flagged inconsistent",
|
||||
// Forwards landed but the pair never held qualifying
|
||||
// liquidity: zero uptime, yet the forwarded volume is
|
||||
// still reported so the consumer keeps the demand
|
||||
// signal.
|
||||
name: "Zero uptime with forwards retains volume",
|
||||
inStates: map[int64]*channelState{
|
||||
chanInID: {
|
||||
online: false,
|
||||
|
|
@ -594,11 +586,7 @@ func TestCalculateBothDirectionsUptime(t *testing.T) {
|
|||
successAmts: []btcutil.Amount{
|
||||
100,
|
||||
},
|
||||
expected: &ForwardingAbility{
|
||||
Velocity: 0,
|
||||
UptimeFraction: 0,
|
||||
Inconsistent: true,
|
||||
},
|
||||
expectedUptime: 0,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -617,16 +605,6 @@ func TestCalculateBothDirectionsUptime(t *testing.T) {
|
|||
tc.inEvents, tc.outEvents,
|
||||
)
|
||||
|
||||
inputsAB := pairInputs{
|
||||
threshold: tc.thresholdAmount,
|
||||
totalSuccessfulAmount: totalSuccessfulAmount,
|
||||
}
|
||||
|
||||
// The (B→A) inputs are not asserted by this
|
||||
// test. Pass zero so the second ability is
|
||||
// well-defined but ignored.
|
||||
var inputsBA pairInputs
|
||||
|
||||
// Precompute the starting balances.
|
||||
var sumARemote, sumALocal, sumBRemote,
|
||||
sumBLocal btcutil.Amount
|
||||
|
|
@ -644,18 +622,25 @@ func TestCalculateBothDirectionsUptime(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// The (B→A) forwarded total is not asserted by
|
||||
// this test; pass zero so the second ability is
|
||||
// well-defined but ignored.
|
||||
abilityAB, _, err :=
|
||||
calculateBothDirectionsUptime(
|
||||
context.Background(),
|
||||
startTime, endTime,
|
||||
inputsAB, inputsBA,
|
||||
tc.liquidityFloor,
|
||||
tc.inStates, tc.outStates,
|
||||
sumARemote, sumALocal,
|
||||
sumBRemote, sumBLocal,
|
||||
mergedEvents,
|
||||
totalSuccessfulAmount, 0,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expected, abilityAB)
|
||||
require.Equal(t, &ForwardingAbility{
|
||||
EffectiveUptime: tc.expectedUptime,
|
||||
ForwardedAmount: totalSuccessfulAmount,
|
||||
}, abilityAB)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -692,17 +677,10 @@ func TestCalculateBothDirectionsUptimeAsymmetric(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
// Threshold sits between the two directions: A→B has min(1000, 1000)
|
||||
// The floor sits between the two directions: A→B has min(1000, 1000)
|
||||
// = 1000 ≥ 500 (qualifying); B→A has min(100, 100) = 100 < 500 (not
|
||||
// qualifying).
|
||||
inputsAB := pairInputs{
|
||||
threshold: 500,
|
||||
totalSuccessfulAmount: 100,
|
||||
}
|
||||
inputsBA := pairInputs{
|
||||
threshold: 500,
|
||||
totalSuccessfulAmount: 50,
|
||||
}
|
||||
const liquidityFloor btcutil.Amount = 500
|
||||
|
||||
// Precompute the starting balances.
|
||||
var sumARemote, sumALocal, sumBRemote, sumBLocal btcutil.Amount
|
||||
|
|
@ -721,24 +699,25 @@ func TestCalculateBothDirectionsUptimeAsymmetric(t *testing.T) {
|
|||
|
||||
abilityAB, abilityBA, err := calculateBothDirectionsUptime(
|
||||
context.Background(), startTime, endTime,
|
||||
inputsAB, inputsBA, statesA, statesB,
|
||||
liquidityFloor, statesA, statesB,
|
||||
sumARemote, sumALocal, sumBRemote, sumBLocal,
|
||||
mergeEventSlices(nil, nil),
|
||||
100, 50,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(
|
||||
t, &ForwardingAbility{
|
||||
Velocity: 1, // 100 sats / 100s
|
||||
UptimeFraction: 1.0,
|
||||
EffectiveUptime: 100 * time.Second,
|
||||
ForwardedAmount: 100,
|
||||
}, abilityAB,
|
||||
)
|
||||
require.Equal(
|
||||
t, &ForwardingAbility{
|
||||
Velocity: 0,
|
||||
UptimeFraction: 0,
|
||||
// Forwards landed but BA never crossed threshold.
|
||||
Inconsistent: true,
|
||||
// Forwards landed but BA never crossed the floor: zero
|
||||
// uptime, volume still reported.
|
||||
EffectiveUptime: 0,
|
||||
ForwardedAmount: 50,
|
||||
}, abilityBA,
|
||||
)
|
||||
}
|
||||
|
|
@ -836,13 +815,17 @@ func (s *stubLndChannelClient) ClosedChannels(_ context.Context) (
|
|||
}
|
||||
|
||||
func (s *stubLndChannelClient) ForwardingHistory(_ context.Context,
|
||||
_ lndclient.ForwardingHistoryRequest) (
|
||||
req lndclient.ForwardingHistoryRequest) (
|
||||
*lndclient.ForwardingHistoryResponse, error) {
|
||||
|
||||
if s.forwardingHistory == nil {
|
||||
return &lndclient.ForwardingHistoryResponse{}, nil
|
||||
}
|
||||
|
||||
if req.Offset > 0 {
|
||||
return &lndclient.ForwardingHistoryResponse{}, nil
|
||||
}
|
||||
|
||||
return s.forwardingHistory, nil
|
||||
}
|
||||
|
||||
|
|
@ -934,7 +917,7 @@ func TestEffectiveUptimeIncludesClosedChannels(t *testing.T) {
|
|||
startTime := seedTime.Add(time.Second)
|
||||
endTime := startTime.Add(time.Minute)
|
||||
|
||||
abilities, err := a.EffectiveUptime(ctx, startTime, endTime, 0, 0)
|
||||
abilities, err := a.EffectiveUptime(ctx, startTime, endTime, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Cross-pair entries in both directions are the cleanest assertion
|
||||
|
|
@ -952,9 +935,9 @@ func TestEffectiveUptimeIncludesClosedChannels(t *testing.T) {
|
|||
)
|
||||
}
|
||||
|
||||
// TestEffectiveUptimeArgs exercises the fwdPercentile, threshold, startTime,
|
||||
// and endTime arguments of EffectiveUptime, verifying that they correctly
|
||||
// govern the calculated forwarding liquidity floor and final uptime metrics.
|
||||
// TestEffectiveUptimeArgs exercises the liquidityFloor, startTime, and endTime
|
||||
// arguments of EffectiveUptime, verifying that the single uniform floor governs
|
||||
// effective uptime and that forwarded volume is reported regardless of uptime.
|
||||
func TestEffectiveUptimeArgs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
|
@ -1035,28 +1018,24 @@ func TestEffectiveUptimeArgs(t *testing.T) {
|
|||
startTime := seedTime.Add(time.Second)
|
||||
endTime := startTime.Add(time.Minute)
|
||||
|
||||
// Case 1: fwdPercentile = 50 (percentile = 200k), threshold = 50k.
|
||||
// Since liquidity is 1M > max(200k, 50k) = 200k, uptime must be 1.0.
|
||||
abilities, err := a.EffectiveUptime(
|
||||
ctx, startTime, endTime, 50.0, 50_000,
|
||||
)
|
||||
// Case 1: liquidityFloor = 50k. Liquidity of 1M exceeds the floor for
|
||||
// the whole 60s window, so effective uptime is the full minute and the
|
||||
// forwarded volume is the 400k sat total.
|
||||
abilities, err := a.EffectiveUptime(ctx, startTime, endTime, 50_000)
|
||||
require.NoError(t, err)
|
||||
|
||||
pair := PeerPair{PeerIn: validPubKey1, PeerOut: validPubKey2}
|
||||
require.Contains(t, abilities, pair)
|
||||
require.Equal(t, 1.0, abilities[pair].UptimeFraction)
|
||||
require.False(t, abilities[pair].Inconsistent)
|
||||
require.Equal(t, time.Minute, abilities[pair].EffectiveUptime)
|
||||
require.Equal(t, btcutil.Amount(400_000), abilities[pair].ForwardedAmount)
|
||||
|
||||
// Case 2: fwdPercentile = 50, threshold = 1_500_000.
|
||||
// The threshold is now 1.5M, which is greater than the liquidity of 1M.
|
||||
// Therefore, the liquidity never crosses the floor, resulting in
|
||||
// zero uptime and the Inconsistent flag being true.
|
||||
abilities, err = a.EffectiveUptime(
|
||||
ctx, startTime, endTime, 50.0, 1_500_000,
|
||||
)
|
||||
// Case 2: liquidityFloor = 1.5M, above the 1M liquidity, so the floor is
|
||||
// never crossed and effective uptime is zero. The forwarded volume is
|
||||
// still reported so the consumer keeps the demand signal.
|
||||
abilities, err = a.EffectiveUptime(ctx, startTime, endTime, 1_500_000)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Contains(t, abilities, pair)
|
||||
require.Equal(t, 0.0, abilities[pair].UptimeFraction)
|
||||
require.True(t, abilities[pair].Inconsistent)
|
||||
require.Zero(t, abilities[pair].EffectiveUptime)
|
||||
require.Equal(t, btcutil.Amount(400_000), abilities[pair].ForwardedAmount)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
package chanevents
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
|
||||
"golang.org/x/exp/constraints"
|
||||
)
|
||||
|
||||
// number is the type constraint Quantile accepts: any sortable numeric type.
|
||||
type number interface {
|
||||
constraints.Integer | constraints.Float
|
||||
}
|
||||
|
||||
// Quantile computes the q-quantile of a slice of comparable values. This can be
|
||||
// used to compute the median (q=0.5) or the min (q=0) or max (q=1).
|
||||
func Quantile[T number](xs []T, q float64) (float64, error) {
|
||||
if q < 0 || q > 1 {
|
||||
return 0, errors.New("quantile must be between 0 and 1")
|
||||
}
|
||||
|
||||
if len(xs) == 0 {
|
||||
return 0, errors.New("cannot compute quantile of empty slice")
|
||||
}
|
||||
|
||||
if len(xs) == 1 {
|
||||
return float64(xs[0]), nil
|
||||
}
|
||||
|
||||
// Create a copy of the slice to avoid mutating the original.
|
||||
ys := make([]T, len(xs))
|
||||
copy(ys, xs)
|
||||
|
||||
sort.Slice(ys, func(i, j int) bool {
|
||||
return ys[i] < ys[j]
|
||||
})
|
||||
|
||||
// Compute fractional index of q-quantile.
|
||||
if q == 1.0 {
|
||||
return float64(ys[len(ys)-1]), nil
|
||||
}
|
||||
i := q * float64(len(ys)-1)
|
||||
|
||||
// Interpolate between the two consecutive values, depending on the
|
||||
// fractional index position in between.
|
||||
lowerIdx := int(i)
|
||||
upperIdx := lowerIdx + 1
|
||||
|
||||
lowerVal := float64(ys[lowerIdx])
|
||||
upperVal := float64(ys[upperIdx])
|
||||
|
||||
indexDiff := i - float64(lowerIdx)
|
||||
|
||||
return lowerVal + (upperVal-lowerVal)*indexDiff, nil
|
||||
}
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
package chanevents
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestQuantile pins the interpolation contract and the error paths Quantile
|
||||
// surfaces to callers.
|
||||
func TestQuantile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
q float64
|
||||
xs []float64
|
||||
want float64
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty slice",
|
||||
xs: []float64{},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "single value",
|
||||
xs: []float64{
|
||||
1,
|
||||
},
|
||||
want: 1.0,
|
||||
},
|
||||
{
|
||||
name: "single value median",
|
||||
xs: []float64{
|
||||
1,
|
||||
},
|
||||
q: 0.5,
|
||||
want: 1.0,
|
||||
},
|
||||
{
|
||||
name: "quantile out of bound below",
|
||||
xs: []float64{},
|
||||
q: -0.1,
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "quantile out of bound above",
|
||||
xs: []float64{},
|
||||
q: 1.1,
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "median odd values",
|
||||
q: 0.5,
|
||||
xs: []float64{
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
},
|
||||
want: 3.0,
|
||||
},
|
||||
{
|
||||
name: "median even values",
|
||||
q: 0.5,
|
||||
xs: []float64{
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
},
|
||||
want: 2.5,
|
||||
},
|
||||
{
|
||||
name: "median unsorted",
|
||||
q: 0.5,
|
||||
xs: []float64{
|
||||
1,
|
||||
3,
|
||||
2,
|
||||
4,
|
||||
},
|
||||
want: 2.5,
|
||||
},
|
||||
{
|
||||
name: "0 percentile",
|
||||
q: 0,
|
||||
xs: []float64{
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
},
|
||||
want: 1.0,
|
||||
},
|
||||
{
|
||||
name: "25 percentile",
|
||||
q: 0.25,
|
||||
xs: []float64{
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
},
|
||||
want: 2.0,
|
||||
},
|
||||
{
|
||||
name: "75 percentile",
|
||||
q: 0.75,
|
||||
xs: []float64{
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
},
|
||||
want: 4.0,
|
||||
},
|
||||
{
|
||||
name: "0.875 percentile",
|
||||
q: 0.875,
|
||||
xs: []float64{
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
},
|
||||
want: 4.5,
|
||||
},
|
||||
{
|
||||
name: "100 percentile",
|
||||
q: 1.0,
|
||||
xs: []float64{
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
},
|
||||
want: 5.0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(tt *testing.T) {
|
||||
tt.Parallel()
|
||||
|
||||
got, err := Quantile(tc.xs, tc.q)
|
||||
if tc.expectErr {
|
||||
require.Error(tt, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.InDelta(tt, tc.want, got, 1e-6)
|
||||
})
|
||||
}
|
||||
}
|
||||
122
cmd/frcli/forwarding_ability.go
Normal file
122
cmd/frcli/forwarding_ability.go
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
"github.com/lightninglabs/faraday/frdrpc"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var forwardingAbilityCommand = cli.Command{
|
||||
Name: "forwardingability",
|
||||
Category: "insights",
|
||||
Usage: "Get forwarding ability analysis of peer pairs.",
|
||||
Flags: []cli.Flag{
|
||||
cli.Uint64Flag{
|
||||
Name: "start_time",
|
||||
Usage: "start time of the query range as a unix " +
|
||||
"timestamp",
|
||||
},
|
||||
cli.Uint64Flag{
|
||||
Name: "end_time",
|
||||
Usage: "end time of the query range as a unix " +
|
||||
"timestamp; zero defaults to the server's " +
|
||||
"current time",
|
||||
},
|
||||
cli.Uint64Flag{
|
||||
Name: "liquidity_floor_sat",
|
||||
Usage: "the minimum directional liquidity in " +
|
||||
"satoshis for a pair to count as " +
|
||||
"economically forwardable; zero uses the " +
|
||||
"server default",
|
||||
},
|
||||
cli.Float64Flag{
|
||||
Name: "uptime_threshold",
|
||||
Usage: "the uptime fraction in [0,1] at or above " +
|
||||
"which a non-forwarding pair is reported as " +
|
||||
"up but idle; zero uses the server default",
|
||||
},
|
||||
},
|
||||
Action: queryForwardingAbility,
|
||||
}
|
||||
|
||||
type pairView struct {
|
||||
PeerIn string `json:"peer_in"`
|
||||
PeerOut string `json:"peer_out"`
|
||||
EffectiveUptimeS int64 `json:"effective_uptime_s"`
|
||||
ForwardedSat int64 `json:"forwarded_sat"`
|
||||
UptimeFraction float64 `json:"uptime_fraction"`
|
||||
Velocity float64 `json:"velocity"`
|
||||
}
|
||||
|
||||
func queryForwardingAbility(ctx *cli.Context) error {
|
||||
client, cleanup := getClient(ctx)
|
||||
defer cleanup()
|
||||
|
||||
req := &frdrpc.ForwardingAbilityRequest{
|
||||
StartTime: ctx.Uint64("start_time"),
|
||||
EndTime: ctx.Uint64("end_time"),
|
||||
LiquidityFloorSat: ctx.Uint64("liquidity_floor_sat"),
|
||||
UptimeThreshold: ctx.Float64("uptime_threshold"),
|
||||
}
|
||||
|
||||
rpcCtx := context.Background()
|
||||
resp, err := client.ForwardingAbility(rpcCtx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
abilities, err := frdrpc.DecodeForwardingAbility(resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The metrics are raw, so derive uptime fraction and velocity here from
|
||||
// the window the server reported.
|
||||
windowSeconds := resp.EndTime - resp.StartTime
|
||||
|
||||
var views []pairView
|
||||
for inPeer, outMap := range abilities {
|
||||
for outPeer, ability := range outMap {
|
||||
var uptimeFraction, velocity float64
|
||||
if windowSeconds > 0 {
|
||||
uptimeFraction = float64(
|
||||
ability.EffectiveUptimeS,
|
||||
) / float64(windowSeconds)
|
||||
}
|
||||
|
||||
if ability.EffectiveUptimeS > 0 {
|
||||
velocity = float64(ability.ForwardedSat) /
|
||||
float64(ability.EffectiveUptimeS)
|
||||
}
|
||||
|
||||
views = append(
|
||||
views, pairView{
|
||||
PeerIn: inPeer,
|
||||
PeerOut: outPeer,
|
||||
EffectiveUptimeS: ability.EffectiveUptimeS,
|
||||
ForwardedSat: ability.ForwardedSat,
|
||||
UptimeFraction: uptimeFraction,
|
||||
Velocity: velocity,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Stable sort by PeerIn, then PeerOut.
|
||||
sort.SliceStable(
|
||||
views,
|
||||
func(i, j int) bool {
|
||||
if views[i].PeerIn != views[j].PeerIn {
|
||||
return views[i].PeerIn < views[j].PeerIn
|
||||
}
|
||||
|
||||
return views[i].PeerOut < views[j].PeerOut
|
||||
},
|
||||
)
|
||||
|
||||
printJSON(views)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -58,6 +58,7 @@ func main() {
|
|||
onChainReportCommand,
|
||||
closeReportCommand,
|
||||
chanEventsCommand,
|
||||
forwardingAbilityCommand,
|
||||
}
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
|
|
|
|||
20
faraday.go
20
faraday.go
|
|
@ -177,10 +177,14 @@ func (f *Faraday) Start() error {
|
|||
return fmt.Errorf("error initializing faraday: %v", err)
|
||||
}
|
||||
|
||||
fwdAnalyzer := chanevents.NewForwardingAnalyzer(
|
||||
f.stores.ChanEventsStore, f.lnd.LndServices,
|
||||
)
|
||||
cfg := &frdrpcserver.Config{
|
||||
Lnd: f.lnd.LndServices,
|
||||
ChanEvents: f.stores.ChanEventsStore,
|
||||
BitcoinClient: f.bitcoinClient,
|
||||
Lnd: f.lnd.LndServices,
|
||||
ChanEvents: f.stores.ChanEventsStore,
|
||||
ForwardingAnalyzer: fwdAnalyzer,
|
||||
BitcoinClient: f.bitcoinClient,
|
||||
}
|
||||
|
||||
// Create the RPC server.
|
||||
|
|
@ -400,10 +404,14 @@ func (f *Faraday) StartAsSubserver(lndGrpc *lndclient.GrpcLndServices,
|
|||
return fmt.Errorf("error initializing faraday: %v", err)
|
||||
}
|
||||
|
||||
fwdAnalyzer := chanevents.NewForwardingAnalyzer(
|
||||
f.stores.ChanEventsStore, lndGrpc.LndServices,
|
||||
)
|
||||
cfg := &frdrpcserver.Config{
|
||||
Lnd: lndGrpc.LndServices,
|
||||
ChanEvents: f.stores.ChanEventsStore,
|
||||
BitcoinClient: f.bitcoinClient,
|
||||
Lnd: lndGrpc.LndServices,
|
||||
ChanEvents: f.stores.ChanEventsStore,
|
||||
ForwardingAnalyzer: fwdAnalyzer,
|
||||
BitcoinClient: f.bitcoinClient,
|
||||
}
|
||||
|
||||
// Create the RPC server, but don't start it.
|
||||
|
|
|
|||
|
|
@ -2210,6 +2210,263 @@ func (x *ChannelEvent) GetId() int64 {
|
|||
return 0
|
||||
}
|
||||
|
||||
type ForwardingAbilityRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// The start time of the query range as unix seconds. A value of 0 means
|
||||
// the earliest available data.
|
||||
StartTime uint64 `protobuf:"varint,1,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"`
|
||||
// The end time of the query range as unix seconds. A value of 0 means the
|
||||
// server's current time.
|
||||
EndTime uint64 `protobuf:"varint,2,opt,name=end_time,json=endTime,proto3" json:"end_time,omitempty"`
|
||||
// The minimum directional liquidity in satoshis for a peer pair to count as
|
||||
// economically forwardable. A value of 0 selects the server default. Hold
|
||||
// this constant across calls for comparable series.
|
||||
LiquidityFloorSat uint64 `protobuf:"varint,3,opt,name=liquidity_floor_sat,json=liquidityFloorSat,proto3" json:"liquidity_floor_sat,omitempty"`
|
||||
// The uptime fraction in [0, 1] at or above which a peer pair that did not
|
||||
// forward is reported compactly as a single bit in up_but_idle_bitmask
|
||||
// rather than as a full entry. A value of 0 selects the server default.
|
||||
// Pairs below this threshold that also did not forward are omitted
|
||||
// entirely. If no pair meets the threshold the server returns a
|
||||
// FailedPrecondition error, since that indicates the node itself was down
|
||||
// for the window and the data carries no signal.
|
||||
UptimeThreshold float64 `protobuf:"fixed64,4,opt,name=uptime_threshold,json=uptimeThreshold,proto3" json:"uptime_threshold,omitempty"`
|
||||
}
|
||||
|
||||
func (x *ForwardingAbilityRequest) Reset() {
|
||||
*x = ForwardingAbilityRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_faraday_proto_msgTypes[25]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *ForwardingAbilityRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ForwardingAbilityRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ForwardingAbilityRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_faraday_proto_msgTypes[25]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ForwardingAbilityRequest.ProtoReflect.Descriptor instead.
|
||||
func (*ForwardingAbilityRequest) Descriptor() ([]byte, []int) {
|
||||
return file_faraday_proto_rawDescGZIP(), []int{25}
|
||||
}
|
||||
|
||||
func (x *ForwardingAbilityRequest) GetStartTime() uint64 {
|
||||
if x != nil {
|
||||
return x.StartTime
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ForwardingAbilityRequest) GetEndTime() uint64 {
|
||||
if x != nil {
|
||||
return x.EndTime
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ForwardingAbilityRequest) GetLiquidityFloorSat() uint64 {
|
||||
if x != nil {
|
||||
return x.LiquidityFloorSat
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ForwardingAbilityRequest) GetUptimeThreshold() float64 {
|
||||
if x != nil {
|
||||
return x.UptimeThreshold
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type ForwardingAbilityResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// Sorted list of unique compressed 33-byte public keys of the peers.
|
||||
Peers [][]byte `protobuf:"bytes,1,rep,name=peers,proto3" json:"peers,omitempty"`
|
||||
// Sparse list of forwarding ability entries. An entry is present only for a
|
||||
// pair that forwarded volume, carrying its exact effective_uptime_s and
|
||||
// forwarded_sat. Pairs that did not forward are never listed here; they are
|
||||
// either flagged in quiet_uptime_bitmask or absent. Entries take precedence
|
||||
// over the bitmask for the same pair.
|
||||
Entries []*ForwardingAbilityEntry `protobuf:"bytes,2,rep,name=entries,proto3" json:"entries,omitempty"`
|
||||
// The start of the window the metrics cover, as unix seconds.
|
||||
StartTime int64 `protobuf:"varint,3,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"`
|
||||
// The end of the window the metrics cover, as unix seconds.
|
||||
EndTime int64 `protobuf:"varint,4,opt,name=end_time,json=endTime,proto3" json:"end_time,omitempty"`
|
||||
// A packed bitmask over the n*n ordered peer pairs, where n is the length
|
||||
// of peers. The bit at index in*n+out is set when that pair held at least
|
||||
// the uptime_threshold fraction of effective uptime over the window but did
|
||||
// not forward: the dense "up but idle" population. A pair with a forwarded
|
||||
// entry is never flagged here. A pair that is neither listed in entries nor
|
||||
// flagged here had sub-threshold uptime and no forwards: treat it as zero.
|
||||
UpButIdleBitmask []byte `protobuf:"bytes,5,opt,name=up_but_idle_bitmask,json=upButIdleBitmask,proto3" json:"up_but_idle_bitmask,omitempty"`
|
||||
// The uptime fraction in [0, 1] used to populate up_but_idle_bitmask,
|
||||
// echoed back so consumers know the threshold the server applied.
|
||||
UptimeThreshold float64 `protobuf:"fixed64,6,opt,name=uptime_threshold,json=uptimeThreshold,proto3" json:"uptime_threshold,omitempty"`
|
||||
}
|
||||
|
||||
func (x *ForwardingAbilityResponse) Reset() {
|
||||
*x = ForwardingAbilityResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_faraday_proto_msgTypes[26]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *ForwardingAbilityResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ForwardingAbilityResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ForwardingAbilityResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_faraday_proto_msgTypes[26]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ForwardingAbilityResponse.ProtoReflect.Descriptor instead.
|
||||
func (*ForwardingAbilityResponse) Descriptor() ([]byte, []int) {
|
||||
return file_faraday_proto_rawDescGZIP(), []int{26}
|
||||
}
|
||||
|
||||
func (x *ForwardingAbilityResponse) GetPeers() [][]byte {
|
||||
if x != nil {
|
||||
return x.Peers
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ForwardingAbilityResponse) GetEntries() []*ForwardingAbilityEntry {
|
||||
if x != nil {
|
||||
return x.Entries
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ForwardingAbilityResponse) GetStartTime() int64 {
|
||||
if x != nil {
|
||||
return x.StartTime
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ForwardingAbilityResponse) GetEndTime() int64 {
|
||||
if x != nil {
|
||||
return x.EndTime
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ForwardingAbilityResponse) GetUpButIdleBitmask() []byte {
|
||||
if x != nil {
|
||||
return x.UpButIdleBitmask
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ForwardingAbilityResponse) GetUptimeThreshold() float64 {
|
||||
if x != nil {
|
||||
return x.UptimeThreshold
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type ForwardingAbilityEntry struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// The indices of the incoming and outgoing peers packed into a single
|
||||
// 32-bit integer: packed_idx = (in << 16) | out. This caps the peer set at
|
||||
// 65535 peers per direction.
|
||||
PackedIdx uint32 `protobuf:"varint,1,opt,name=packed_idx,json=packedIdx,proto3" json:"packed_idx,omitempty"`
|
||||
// Seconds the peer pair held at least the requested liquidity floor of
|
||||
// directional forwardable liquidity over the window.
|
||||
EffectiveUptimeS int64 `protobuf:"varint,2,opt,name=effective_uptime_s,json=effectiveUptimeS,proto3" json:"effective_uptime_s,omitempty"`
|
||||
// Total successfully forwarded amount over the window, in satoshis.
|
||||
ForwardedSat int64 `protobuf:"varint,3,opt,name=forwarded_sat,json=forwardedSat,proto3" json:"forwarded_sat,omitempty"`
|
||||
}
|
||||
|
||||
func (x *ForwardingAbilityEntry) Reset() {
|
||||
*x = ForwardingAbilityEntry{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_faraday_proto_msgTypes[27]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *ForwardingAbilityEntry) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ForwardingAbilityEntry) ProtoMessage() {}
|
||||
|
||||
func (x *ForwardingAbilityEntry) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_faraday_proto_msgTypes[27]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ForwardingAbilityEntry.ProtoReflect.Descriptor instead.
|
||||
func (*ForwardingAbilityEntry) Descriptor() ([]byte, []int) {
|
||||
return file_faraday_proto_rawDescGZIP(), []int{27}
|
||||
}
|
||||
|
||||
func (x *ForwardingAbilityEntry) GetPackedIdx() uint32 {
|
||||
if x != nil {
|
||||
return x.PackedIdx
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ForwardingAbilityEntry) GetEffectiveUptimeS() int64 {
|
||||
if x != nil {
|
||||
return x.EffectiveUptimeS
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ForwardingAbilityEntry) GetForwardedSat() int64 {
|
||||
if x != nil {
|
||||
return x.ForwardedSat
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var File_faraday_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_faraday_proto_rawDesc = []byte{
|
||||
|
|
@ -2472,95 +2729,137 @@ var file_faraday_proto_rawDesc = []byte{
|
|||
0x6e, 0x63, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x5f, 0x62, 0x61,
|
||||
0x6c, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x72, 0x65, 0x6d,
|
||||
0x6f, 0x74, 0x65, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64,
|
||||
0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x2a, 0xa1, 0x01, 0x0a, 0x0b, 0x47,
|
||||
0x72, 0x61, 0x6e, 0x75, 0x6c, 0x61, 0x72, 0x69, 0x74, 0x79, 0x12, 0x17, 0x0a, 0x13, 0x55, 0x4e,
|
||||
0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x47, 0x52, 0x41, 0x4e, 0x55, 0x4c, 0x41, 0x52, 0x49, 0x54,
|
||||
0x59, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x4d, 0x49, 0x4e, 0x55, 0x54, 0x45, 0x10, 0x01, 0x12,
|
||||
0x10, 0x0a, 0x0c, 0x46, 0x49, 0x56, 0x45, 0x5f, 0x4d, 0x49, 0x4e, 0x55, 0x54, 0x45, 0x53, 0x10,
|
||||
0x02, 0x12, 0x13, 0x0a, 0x0f, 0x46, 0x49, 0x46, 0x54, 0x45, 0x45, 0x4e, 0x5f, 0x4d, 0x49, 0x4e,
|
||||
0x55, 0x54, 0x45, 0x53, 0x10, 0x03, 0x12, 0x12, 0x0a, 0x0e, 0x54, 0x48, 0x49, 0x52, 0x54, 0x59,
|
||||
0x5f, 0x4d, 0x49, 0x4e, 0x55, 0x54, 0x45, 0x53, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x4f,
|
||||
0x55, 0x52, 0x10, 0x05, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x49, 0x58, 0x5f, 0x48, 0x4f, 0x55, 0x52,
|
||||
0x53, 0x10, 0x06, 0x12, 0x10, 0x0a, 0x0c, 0x54, 0x57, 0x45, 0x4c, 0x56, 0x45, 0x5f, 0x48, 0x4f,
|
||||
0x55, 0x52, 0x53, 0x10, 0x07, 0x12, 0x07, 0x0a, 0x03, 0x44, 0x41, 0x59, 0x10, 0x08, 0x2a, 0x6a,
|
||||
0x0a, 0x0b, 0x46, 0x69, 0x61, 0x74, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x12, 0x17, 0x0a,
|
||||
0x13, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x46, 0x49, 0x41, 0x54, 0x42, 0x41, 0x43,
|
||||
0x4b, 0x45, 0x4e, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x4f, 0x49, 0x4e, 0x43, 0x41,
|
||||
0x50, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x4f, 0x49, 0x4e, 0x44, 0x45, 0x53, 0x4b, 0x10,
|
||||
0x02, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x03, 0x12, 0x0d, 0x0a,
|
||||
0x09, 0x43, 0x4f, 0x49, 0x4e, 0x47, 0x45, 0x43, 0x4b, 0x4f, 0x10, 0x04, 0x12, 0x0c, 0x0a, 0x08,
|
||||
0x42, 0x49, 0x54, 0x46, 0x49, 0x4e, 0x45, 0x58, 0x10, 0x05, 0x2a, 0xa2, 0x02, 0x0a, 0x09, 0x45,
|
||||
0x6e, 0x74, 0x72, 0x79, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e,
|
||||
0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x16, 0x0a, 0x12, 0x4c, 0x4f, 0x43, 0x41, 0x4c, 0x5f, 0x43,
|
||||
0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x01, 0x12, 0x17, 0x0a,
|
||||
0x13, 0x52, 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x5f, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f,
|
||||
0x4f, 0x50, 0x45, 0x4e, 0x10, 0x02, 0x12, 0x14, 0x0a, 0x10, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45,
|
||||
0x4c, 0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x5f, 0x46, 0x45, 0x45, 0x10, 0x03, 0x12, 0x11, 0x0a, 0x0d,
|
||||
0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x10, 0x04, 0x12,
|
||||
0x0b, 0x0a, 0x07, 0x52, 0x45, 0x43, 0x45, 0x49, 0x50, 0x54, 0x10, 0x05, 0x12, 0x0b, 0x0a, 0x07,
|
||||
0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x06, 0x12, 0x07, 0x0a, 0x03, 0x46, 0x45, 0x45,
|
||||
0x10, 0x07, 0x12, 0x14, 0x0a, 0x10, 0x43, 0x49, 0x52, 0x43, 0x55, 0x4c, 0x41, 0x52, 0x5f, 0x52,
|
||||
0x45, 0x43, 0x45, 0x49, 0x50, 0x54, 0x10, 0x08, 0x12, 0x0b, 0x0a, 0x07, 0x46, 0x4f, 0x52, 0x57,
|
||||
0x41, 0x52, 0x44, 0x10, 0x09, 0x12, 0x0f, 0x0a, 0x0b, 0x46, 0x4f, 0x52, 0x57, 0x41, 0x52, 0x44,
|
||||
0x5f, 0x46, 0x45, 0x45, 0x10, 0x0a, 0x12, 0x14, 0x0a, 0x10, 0x43, 0x49, 0x52, 0x43, 0x55, 0x4c,
|
||||
0x41, 0x52, 0x5f, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x0b, 0x12, 0x10, 0x0a, 0x0c,
|
||||
0x43, 0x49, 0x52, 0x43, 0x55, 0x4c, 0x41, 0x52, 0x5f, 0x46, 0x45, 0x45, 0x10, 0x0c, 0x12, 0x09,
|
||||
0x0a, 0x05, 0x53, 0x57, 0x45, 0x45, 0x50, 0x10, 0x0d, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x57, 0x45,
|
||||
0x45, 0x50, 0x5f, 0x46, 0x45, 0x45, 0x10, 0x0e, 0x12, 0x15, 0x0a, 0x11, 0x43, 0x48, 0x41, 0x4e,
|
||||
0x4e, 0x45, 0x4c, 0x5f, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x5f, 0x46, 0x45, 0x45, 0x10, 0x0f, 0x2a,
|
||||
0x70, 0x0a, 0x10, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54,
|
||||
0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x12, 0x43, 0x48, 0x41, 0x4e, 0x5f, 0x45, 0x56, 0x45, 0x4e,
|
||||
0x54, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x15, 0x0a, 0x11, 0x43,
|
||||
0x48, 0x41, 0x4e, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x4f, 0x4e, 0x4c, 0x49, 0x4e, 0x45,
|
||||
0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x43, 0x48, 0x41, 0x4e, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54,
|
||||
0x5f, 0x4f, 0x46, 0x46, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x02, 0x12, 0x15, 0x0a, 0x11, 0x43, 0x48,
|
||||
0x41, 0x4e, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10,
|
||||
0x03, 0x32, 0xa9, 0x05, 0x0a, 0x0d, 0x46, 0x61, 0x72, 0x61, 0x64, 0x61, 0x79, 0x53, 0x65, 0x72,
|
||||
0x76, 0x65, 0x72, 0x12, 0x65, 0x0a, 0x16, 0x4f, 0x75, 0x74, 0x6c, 0x69, 0x65, 0x72, 0x52, 0x65,
|
||||
0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x25, 0x2e,
|
||||
0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x4f, 0x75, 0x74, 0x6c, 0x69, 0x65, 0x72, 0x52, 0x65,
|
||||
0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c,
|
||||
0x6f, 0x73, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x61, 0x74, 0x69, 0x6f,
|
||||
0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x69, 0x0a, 0x18, 0x54, 0x68,
|
||||
0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x52, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64,
|
||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x27, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e,
|
||||
0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x52, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65,
|
||||
0x6e, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
|
||||
0x24, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65,
|
||||
0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73,
|
||||
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x0d, 0x52, 0x65, 0x76, 0x65, 0x6e, 0x75, 0x65,
|
||||
0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x1c, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e,
|
||||
0x52, 0x65, 0x76, 0x65, 0x6e, 0x75, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65,
|
||||
0x76, 0x65, 0x6e, 0x75, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||
0x6e, 0x73, 0x65, 0x12, 0x52, 0x0a, 0x0f, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x6e,
|
||||
0x73, 0x69, 0x67, 0x68, 0x74, 0x73, 0x12, 0x1e, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e,
|
||||
0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x73, 0x69, 0x67, 0x68, 0x74, 0x73, 0x52,
|
||||
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e,
|
||||
0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x73, 0x69, 0x67, 0x68, 0x74, 0x73, 0x52,
|
||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x0c, 0x45, 0x78, 0x63, 0x68, 0x61,
|
||||
0x6e, 0x67, 0x65, 0x52, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63,
|
||||
0x2e, 0x45, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x78,
|
||||
0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
||||
0x73, 0x65, 0x12, 0x40, 0x0a, 0x09, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x75, 0x64, 0x69, 0x74, 0x12,
|
||||
0x18, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x75, 0x64,
|
||||
0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x66, 0x72, 0x64, 0x72,
|
||||
0x70, 0x63, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x75, 0x64, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70,
|
||||
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0b, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x70,
|
||||
0x6f, 0x72, 0x74, 0x12, 0x1a, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x6f,
|
||||
0x73, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
|
||||
0x1b, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65,
|
||||
0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x10,
|
||||
0x47, 0x65, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73,
|
||||
0x12, 0x1c, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65,
|
||||
0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d,
|
||||
0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x45,
|
||||
0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x29, 0x5a,
|
||||
0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68,
|
||||
0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x66, 0x61, 0x72, 0x61, 0x64, 0x61,
|
||||
0x79, 0x2f, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x22, 0xaf, 0x01, 0x0a, 0x18, 0x46,
|
||||
0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x41, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79,
|
||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74,
|
||||
0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x73, 0x74, 0x61,
|
||||
0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69,
|
||||
0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d,
|
||||
0x65, 0x12, 0x2e, 0x0a, 0x13, 0x6c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x5f, 0x66,
|
||||
0x6c, 0x6f, 0x6f, 0x72, 0x5f, 0x73, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x11,
|
||||
0x6c, 0x69, 0x71, 0x75, 0x69, 0x64, 0x69, 0x74, 0x79, 0x46, 0x6c, 0x6f, 0x6f, 0x72, 0x53, 0x61,
|
||||
0x74, 0x12, 0x29, 0x0a, 0x10, 0x75, 0x70, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x68, 0x72, 0x65,
|
||||
0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0f, 0x75, 0x70, 0x74,
|
||||
0x69, 0x6d, 0x65, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x22, 0xff, 0x01, 0x0a,
|
||||
0x19, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x41, 0x62, 0x69, 0x6c, 0x69,
|
||||
0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x65,
|
||||
0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73,
|
||||
0x12, 0x38, 0x0a, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28,
|
||||
0x0b, 0x32, 0x1e, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61,
|
||||
0x72, 0x64, 0x69, 0x6e, 0x67, 0x41, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x45, 0x6e, 0x74, 0x72,
|
||||
0x79, 0x52, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74,
|
||||
0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09,
|
||||
0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x65, 0x6e, 0x64,
|
||||
0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x65, 0x6e, 0x64,
|
||||
0x54, 0x69, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x13, 0x75, 0x70, 0x5f, 0x62, 0x75, 0x74, 0x5f, 0x69,
|
||||
0x64, 0x6c, 0x65, 0x5f, 0x62, 0x69, 0x74, 0x6d, 0x61, 0x73, 0x6b, 0x18, 0x05, 0x20, 0x01, 0x28,
|
||||
0x0c, 0x52, 0x10, 0x75, 0x70, 0x42, 0x75, 0x74, 0x49, 0x64, 0x6c, 0x65, 0x42, 0x69, 0x74, 0x6d,
|
||||
0x61, 0x73, 0x6b, 0x12, 0x29, 0x0a, 0x10, 0x75, 0x70, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x68,
|
||||
0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0f, 0x75,
|
||||
0x70, 0x74, 0x69, 0x6d, 0x65, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x22, 0x8a,
|
||||
0x01, 0x0a, 0x16, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x41, 0x62, 0x69,
|
||||
0x6c, 0x69, 0x74, 0x79, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x61, 0x63,
|
||||
0x6b, 0x65, 0x64, 0x5f, 0x69, 0x64, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x70,
|
||||
0x61, 0x63, 0x6b, 0x65, 0x64, 0x49, 0x64, 0x78, 0x12, 0x2c, 0x0a, 0x12, 0x65, 0x66, 0x66, 0x65,
|
||||
0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x75, 0x70, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x73, 0x18, 0x02,
|
||||
0x20, 0x01, 0x28, 0x03, 0x52, 0x10, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x55,
|
||||
0x70, 0x74, 0x69, 0x6d, 0x65, 0x53, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72,
|
||||
0x64, 0x65, 0x64, 0x5f, 0x73, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x66,
|
||||
0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x65, 0x64, 0x53, 0x61, 0x74, 0x2a, 0xa1, 0x01, 0x0a, 0x0b,
|
||||
0x47, 0x72, 0x61, 0x6e, 0x75, 0x6c, 0x61, 0x72, 0x69, 0x74, 0x79, 0x12, 0x17, 0x0a, 0x13, 0x55,
|
||||
0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x47, 0x52, 0x41, 0x4e, 0x55, 0x4c, 0x41, 0x52, 0x49,
|
||||
0x54, 0x59, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x4d, 0x49, 0x4e, 0x55, 0x54, 0x45, 0x10, 0x01,
|
||||
0x12, 0x10, 0x0a, 0x0c, 0x46, 0x49, 0x56, 0x45, 0x5f, 0x4d, 0x49, 0x4e, 0x55, 0x54, 0x45, 0x53,
|
||||
0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x46, 0x49, 0x46, 0x54, 0x45, 0x45, 0x4e, 0x5f, 0x4d, 0x49,
|
||||
0x4e, 0x55, 0x54, 0x45, 0x53, 0x10, 0x03, 0x12, 0x12, 0x0a, 0x0e, 0x54, 0x48, 0x49, 0x52, 0x54,
|
||||
0x59, 0x5f, 0x4d, 0x49, 0x4e, 0x55, 0x54, 0x45, 0x53, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x48,
|
||||
0x4f, 0x55, 0x52, 0x10, 0x05, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x49, 0x58, 0x5f, 0x48, 0x4f, 0x55,
|
||||
0x52, 0x53, 0x10, 0x06, 0x12, 0x10, 0x0a, 0x0c, 0x54, 0x57, 0x45, 0x4c, 0x56, 0x45, 0x5f, 0x48,
|
||||
0x4f, 0x55, 0x52, 0x53, 0x10, 0x07, 0x12, 0x07, 0x0a, 0x03, 0x44, 0x41, 0x59, 0x10, 0x08, 0x2a,
|
||||
0x6a, 0x0a, 0x0b, 0x46, 0x69, 0x61, 0x74, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x12, 0x17,
|
||||
0x0a, 0x13, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x46, 0x49, 0x41, 0x54, 0x42, 0x41,
|
||||
0x43, 0x4b, 0x45, 0x4e, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x4f, 0x49, 0x4e, 0x43,
|
||||
0x41, 0x50, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x4f, 0x49, 0x4e, 0x44, 0x45, 0x53, 0x4b,
|
||||
0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x03, 0x12, 0x0d,
|
||||
0x0a, 0x09, 0x43, 0x4f, 0x49, 0x4e, 0x47, 0x45, 0x43, 0x4b, 0x4f, 0x10, 0x04, 0x12, 0x0c, 0x0a,
|
||||
0x08, 0x42, 0x49, 0x54, 0x46, 0x49, 0x4e, 0x45, 0x58, 0x10, 0x05, 0x2a, 0xa2, 0x02, 0x0a, 0x09,
|
||||
0x45, 0x6e, 0x74, 0x72, 0x79, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b,
|
||||
0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x16, 0x0a, 0x12, 0x4c, 0x4f, 0x43, 0x41, 0x4c, 0x5f,
|
||||
0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x01, 0x12, 0x17,
|
||||
0x0a, 0x13, 0x52, 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x5f, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c,
|
||||
0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x02, 0x12, 0x14, 0x0a, 0x10, 0x43, 0x48, 0x41, 0x4e, 0x4e,
|
||||
0x45, 0x4c, 0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x5f, 0x46, 0x45, 0x45, 0x10, 0x03, 0x12, 0x11, 0x0a,
|
||||
0x0d, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x10, 0x04,
|
||||
0x12, 0x0b, 0x0a, 0x07, 0x52, 0x45, 0x43, 0x45, 0x49, 0x50, 0x54, 0x10, 0x05, 0x12, 0x0b, 0x0a,
|
||||
0x07, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x06, 0x12, 0x07, 0x0a, 0x03, 0x46, 0x45,
|
||||
0x45, 0x10, 0x07, 0x12, 0x14, 0x0a, 0x10, 0x43, 0x49, 0x52, 0x43, 0x55, 0x4c, 0x41, 0x52, 0x5f,
|
||||
0x52, 0x45, 0x43, 0x45, 0x49, 0x50, 0x54, 0x10, 0x08, 0x12, 0x0b, 0x0a, 0x07, 0x46, 0x4f, 0x52,
|
||||
0x57, 0x41, 0x52, 0x44, 0x10, 0x09, 0x12, 0x0f, 0x0a, 0x0b, 0x46, 0x4f, 0x52, 0x57, 0x41, 0x52,
|
||||
0x44, 0x5f, 0x46, 0x45, 0x45, 0x10, 0x0a, 0x12, 0x14, 0x0a, 0x10, 0x43, 0x49, 0x52, 0x43, 0x55,
|
||||
0x4c, 0x41, 0x52, 0x5f, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x0b, 0x12, 0x10, 0x0a,
|
||||
0x0c, 0x43, 0x49, 0x52, 0x43, 0x55, 0x4c, 0x41, 0x52, 0x5f, 0x46, 0x45, 0x45, 0x10, 0x0c, 0x12,
|
||||
0x09, 0x0a, 0x05, 0x53, 0x57, 0x45, 0x45, 0x50, 0x10, 0x0d, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x57,
|
||||
0x45, 0x45, 0x50, 0x5f, 0x46, 0x45, 0x45, 0x10, 0x0e, 0x12, 0x15, 0x0a, 0x11, 0x43, 0x48, 0x41,
|
||||
0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x5f, 0x46, 0x45, 0x45, 0x10, 0x0f,
|
||||
0x2a, 0x70, 0x0a, 0x10, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74,
|
||||
0x54, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x12, 0x43, 0x48, 0x41, 0x4e, 0x5f, 0x45, 0x56, 0x45,
|
||||
0x4e, 0x54, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x15, 0x0a, 0x11,
|
||||
0x43, 0x48, 0x41, 0x4e, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x4f, 0x4e, 0x4c, 0x49, 0x4e,
|
||||
0x45, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x43, 0x48, 0x41, 0x4e, 0x5f, 0x45, 0x56, 0x45, 0x4e,
|
||||
0x54, 0x5f, 0x4f, 0x46, 0x46, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x02, 0x12, 0x15, 0x0a, 0x11, 0x43,
|
||||
0x48, 0x41, 0x4e, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45,
|
||||
0x10, 0x03, 0x32, 0x83, 0x06, 0x0a, 0x0d, 0x46, 0x61, 0x72, 0x61, 0x64, 0x61, 0x79, 0x53, 0x65,
|
||||
0x72, 0x76, 0x65, 0x72, 0x12, 0x65, 0x0a, 0x16, 0x4f, 0x75, 0x74, 0x6c, 0x69, 0x65, 0x72, 0x52,
|
||||
0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x25,
|
||||
0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x4f, 0x75, 0x74, 0x6c, 0x69, 0x65, 0x72, 0x52,
|
||||
0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65,
|
||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x43,
|
||||
0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x61, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x69, 0x0a, 0x18, 0x54,
|
||||
0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x52, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e,
|
||||
0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x27, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63,
|
||||
0x2e, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x52, 0x65, 0x63, 0x6f, 0x6d, 0x6d,
|
||||
0x65, 0x6e, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
||||
0x1a, 0x24, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52,
|
||||
0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65,
|
||||
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x0d, 0x52, 0x65, 0x76, 0x65, 0x6e, 0x75,
|
||||
0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x1c, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63,
|
||||
0x2e, 0x52, 0x65, 0x76, 0x65, 0x6e, 0x75, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65,
|
||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x52,
|
||||
0x65, 0x76, 0x65, 0x6e, 0x75, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70,
|
||||
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x52, 0x0a, 0x0f, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49,
|
||||
0x6e, 0x73, 0x69, 0x67, 0x68, 0x74, 0x73, 0x12, 0x1e, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63,
|
||||
0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x73, 0x69, 0x67, 0x68, 0x74, 0x73,
|
||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63,
|
||||
0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x73, 0x69, 0x67, 0x68, 0x74, 0x73,
|
||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x0c, 0x45, 0x78, 0x63, 0x68,
|
||||
0x61, 0x6e, 0x67, 0x65, 0x52, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70,
|
||||
0x63, 0x2e, 0x45, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x61, 0x74, 0x65, 0x52, 0x65,
|
||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x45,
|
||||
0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||
0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x09, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x75, 0x64, 0x69, 0x74,
|
||||
0x12, 0x18, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x75,
|
||||
0x64, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x66, 0x72, 0x64,
|
||||
0x72, 0x70, 0x63, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x75, 0x64, 0x69, 0x74, 0x52, 0x65, 0x73,
|
||||
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0b, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65,
|
||||
0x70, 0x6f, 0x72, 0x74, 0x12, 0x1a, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c,
|
||||
0x6f, 0x73, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
||||
0x1a, 0x1b, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52,
|
||||
0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a,
|
||||
0x10, 0x47, 0x65, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74,
|
||||
0x73, 0x12, 0x1c, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e,
|
||||
0x65, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
|
||||
0x1d, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c,
|
||||
0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58,
|
||||
0x0a, 0x11, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x41, 0x62, 0x69, 0x6c,
|
||||
0x69, 0x74, 0x79, 0x12, 0x20, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x6f, 0x72,
|
||||
0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x41, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x52, 0x65,
|
||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x46,
|
||||
0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x41, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79,
|
||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68,
|
||||
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67,
|
||||
0x6c, 0x61, 0x62, 0x73, 0x2f, 0x66, 0x61, 0x72, 0x61, 0x64, 0x61, 0x79, 0x2f, 0x66, 0x72, 0x64,
|
||||
0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
@ -2576,7 +2875,7 @@ func file_faraday_proto_rawDescGZIP() []byte {
|
|||
}
|
||||
|
||||
var file_faraday_proto_enumTypes = make([]protoimpl.EnumInfo, 5)
|
||||
var file_faraday_proto_msgTypes = make([]protoimpl.MessageInfo, 26)
|
||||
var file_faraday_proto_msgTypes = make([]protoimpl.MessageInfo, 29)
|
||||
var file_faraday_proto_goTypes = []any{
|
||||
(Granularity)(0), // 0: frdrpc.Granularity
|
||||
(FiatBackend)(0), // 1: frdrpc.FiatBackend
|
||||
|
|
@ -2608,7 +2907,10 @@ var file_faraday_proto_goTypes = []any{
|
|||
(*ChannelEventsRequest)(nil), // 27: frdrpc.ChannelEventsRequest
|
||||
(*ChannelEventsResponse)(nil), // 28: frdrpc.ChannelEventsResponse
|
||||
(*ChannelEvent)(nil), // 29: frdrpc.ChannelEvent
|
||||
nil, // 30: frdrpc.RevenueReport.PairReportsEntry
|
||||
(*ForwardingAbilityRequest)(nil), // 30: frdrpc.ForwardingAbilityRequest
|
||||
(*ForwardingAbilityResponse)(nil), // 31: frdrpc.ForwardingAbilityResponse
|
||||
(*ForwardingAbilityEntry)(nil), // 32: frdrpc.ForwardingAbilityEntry
|
||||
nil, // 33: frdrpc.RevenueReport.PairReportsEntry
|
||||
}
|
||||
var file_faraday_proto_depIdxs = []int32{
|
||||
4, // 0: frdrpc.CloseRecommendationRequest.metric:type_name -> frdrpc.CloseRecommendationRequest.Metric
|
||||
|
|
@ -2616,7 +2918,7 @@ var file_faraday_proto_depIdxs = []int32{
|
|||
5, // 2: frdrpc.ThresholdRecommendationsRequest.rec_request:type_name -> frdrpc.CloseRecommendationRequest
|
||||
9, // 3: frdrpc.CloseRecommendationsResponse.recommendations:type_name -> frdrpc.Recommendation
|
||||
12, // 4: frdrpc.RevenueReportResponse.reports:type_name -> frdrpc.RevenueReport
|
||||
30, // 5: frdrpc.RevenueReport.pair_reports:type_name -> frdrpc.RevenueReport.PairReportsEntry
|
||||
33, // 5: frdrpc.RevenueReport.pair_reports:type_name -> frdrpc.RevenueReport.PairReportsEntry
|
||||
16, // 6: frdrpc.ChannelInsightsResponse.channel_insights:type_name -> frdrpc.ChannelInsight
|
||||
0, // 7: frdrpc.ExchangeRateRequest.granularity:type_name -> frdrpc.Granularity
|
||||
1, // 8: frdrpc.ExchangeRateRequest.fiat_backend:type_name -> frdrpc.FiatBackend
|
||||
|
|
@ -2632,28 +2934,31 @@ var file_faraday_proto_depIdxs = []int32{
|
|||
23, // 18: frdrpc.NodeAuditResponse.reports:type_name -> frdrpc.ReportEntry
|
||||
29, // 19: frdrpc.ChannelEventsResponse.events:type_name -> frdrpc.ChannelEvent
|
||||
3, // 20: frdrpc.ChannelEvent.event_type:type_name -> frdrpc.ChannelEventType
|
||||
13, // 21: frdrpc.RevenueReport.PairReportsEntry.value:type_name -> frdrpc.PairReport
|
||||
6, // 22: frdrpc.FaradayServer.OutlierRecommendations:input_type -> frdrpc.OutlierRecommendationsRequest
|
||||
7, // 23: frdrpc.FaradayServer.ThresholdRecommendations:input_type -> frdrpc.ThresholdRecommendationsRequest
|
||||
10, // 24: frdrpc.FaradayServer.RevenueReport:input_type -> frdrpc.RevenueReportRequest
|
||||
14, // 25: frdrpc.FaradayServer.ChannelInsights:input_type -> frdrpc.ChannelInsightsRequest
|
||||
17, // 26: frdrpc.FaradayServer.ExchangeRate:input_type -> frdrpc.ExchangeRateRequest
|
||||
21, // 27: frdrpc.FaradayServer.NodeAudit:input_type -> frdrpc.NodeAuditRequest
|
||||
25, // 28: frdrpc.FaradayServer.CloseReport:input_type -> frdrpc.CloseReportRequest
|
||||
27, // 29: frdrpc.FaradayServer.GetChannelEvents:input_type -> frdrpc.ChannelEventsRequest
|
||||
8, // 30: frdrpc.FaradayServer.OutlierRecommendations:output_type -> frdrpc.CloseRecommendationsResponse
|
||||
8, // 31: frdrpc.FaradayServer.ThresholdRecommendations:output_type -> frdrpc.CloseRecommendationsResponse
|
||||
11, // 32: frdrpc.FaradayServer.RevenueReport:output_type -> frdrpc.RevenueReportResponse
|
||||
15, // 33: frdrpc.FaradayServer.ChannelInsights:output_type -> frdrpc.ChannelInsightsResponse
|
||||
18, // 34: frdrpc.FaradayServer.ExchangeRate:output_type -> frdrpc.ExchangeRateResponse
|
||||
24, // 35: frdrpc.FaradayServer.NodeAudit:output_type -> frdrpc.NodeAuditResponse
|
||||
26, // 36: frdrpc.FaradayServer.CloseReport:output_type -> frdrpc.CloseReportResponse
|
||||
28, // 37: frdrpc.FaradayServer.GetChannelEvents:output_type -> frdrpc.ChannelEventsResponse
|
||||
30, // [30:38] is the sub-list for method output_type
|
||||
22, // [22:30] is the sub-list for method input_type
|
||||
22, // [22:22] is the sub-list for extension type_name
|
||||
22, // [22:22] is the sub-list for extension extendee
|
||||
0, // [0:22] is the sub-list for field type_name
|
||||
32, // 21: frdrpc.ForwardingAbilityResponse.entries:type_name -> frdrpc.ForwardingAbilityEntry
|
||||
13, // 22: frdrpc.RevenueReport.PairReportsEntry.value:type_name -> frdrpc.PairReport
|
||||
6, // 23: frdrpc.FaradayServer.OutlierRecommendations:input_type -> frdrpc.OutlierRecommendationsRequest
|
||||
7, // 24: frdrpc.FaradayServer.ThresholdRecommendations:input_type -> frdrpc.ThresholdRecommendationsRequest
|
||||
10, // 25: frdrpc.FaradayServer.RevenueReport:input_type -> frdrpc.RevenueReportRequest
|
||||
14, // 26: frdrpc.FaradayServer.ChannelInsights:input_type -> frdrpc.ChannelInsightsRequest
|
||||
17, // 27: frdrpc.FaradayServer.ExchangeRate:input_type -> frdrpc.ExchangeRateRequest
|
||||
21, // 28: frdrpc.FaradayServer.NodeAudit:input_type -> frdrpc.NodeAuditRequest
|
||||
25, // 29: frdrpc.FaradayServer.CloseReport:input_type -> frdrpc.CloseReportRequest
|
||||
27, // 30: frdrpc.FaradayServer.GetChannelEvents:input_type -> frdrpc.ChannelEventsRequest
|
||||
30, // 31: frdrpc.FaradayServer.ForwardingAbility:input_type -> frdrpc.ForwardingAbilityRequest
|
||||
8, // 32: frdrpc.FaradayServer.OutlierRecommendations:output_type -> frdrpc.CloseRecommendationsResponse
|
||||
8, // 33: frdrpc.FaradayServer.ThresholdRecommendations:output_type -> frdrpc.CloseRecommendationsResponse
|
||||
11, // 34: frdrpc.FaradayServer.RevenueReport:output_type -> frdrpc.RevenueReportResponse
|
||||
15, // 35: frdrpc.FaradayServer.ChannelInsights:output_type -> frdrpc.ChannelInsightsResponse
|
||||
18, // 36: frdrpc.FaradayServer.ExchangeRate:output_type -> frdrpc.ExchangeRateResponse
|
||||
24, // 37: frdrpc.FaradayServer.NodeAudit:output_type -> frdrpc.NodeAuditResponse
|
||||
26, // 38: frdrpc.FaradayServer.CloseReport:output_type -> frdrpc.CloseReportResponse
|
||||
28, // 39: frdrpc.FaradayServer.GetChannelEvents:output_type -> frdrpc.ChannelEventsResponse
|
||||
31, // 40: frdrpc.FaradayServer.ForwardingAbility:output_type -> frdrpc.ForwardingAbilityResponse
|
||||
32, // [32:41] is the sub-list for method output_type
|
||||
23, // [23:32] is the sub-list for method input_type
|
||||
23, // [23:23] is the sub-list for extension type_name
|
||||
23, // [23:23] is the sub-list for extension extendee
|
||||
0, // [0:23] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_faraday_proto_init() }
|
||||
|
|
@ -2962,6 +3267,42 @@ func file_faraday_proto_init() {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
file_faraday_proto_msgTypes[25].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*ForwardingAbilityRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_faraday_proto_msgTypes[26].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*ForwardingAbilityResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_faraday_proto_msgTypes[27].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*ForwardingAbilityEntry); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
|
|
@ -2969,7 +3310,7 @@ func file_faraday_proto_init() {
|
|||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_faraday_proto_rawDesc,
|
||||
NumEnums: 5,
|
||||
NumMessages: 26,
|
||||
NumMessages: 29,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -70,6 +70,12 @@ service FaradayServer {
|
|||
Get a list of channel events that occurred for a given channel.
|
||||
*/
|
||||
rpc GetChannelEvents (ChannelEventsRequest) returns (ChannelEventsResponse);
|
||||
|
||||
/**
|
||||
Get forwarding ability analysis of peer pairs.
|
||||
*/
|
||||
rpc ForwardingAbility (ForwardingAbilityRequest)
|
||||
returns (ForwardingAbilityResponse);
|
||||
}
|
||||
|
||||
message CloseRecommendationRequest {
|
||||
|
|
@ -675,3 +681,71 @@ message ChannelEvent {
|
|||
// request's last_id when paginating.
|
||||
int64 id = 5;
|
||||
}
|
||||
|
||||
message ForwardingAbilityRequest {
|
||||
// The start time of the query range as unix seconds. A value of 0 means
|
||||
// the earliest available data.
|
||||
uint64 start_time = 1;
|
||||
|
||||
// The end time of the query range as unix seconds. A value of 0 means the
|
||||
// server's current time.
|
||||
uint64 end_time = 2;
|
||||
|
||||
// The minimum directional liquidity in satoshis for a peer pair to count as
|
||||
// economically forwardable. A value of 0 selects the server default. Hold
|
||||
// this constant across calls for comparable series.
|
||||
uint64 liquidity_floor_sat = 3;
|
||||
|
||||
// The uptime fraction in [0, 1] at or above which a peer pair that did not
|
||||
// forward is reported compactly as a single bit in up_but_idle_bitmask
|
||||
// rather than as a full entry. A value of 0 selects the server default.
|
||||
// Pairs below this threshold that also did not forward are omitted
|
||||
// entirely. If no pair meets the threshold the server returns a
|
||||
// FailedPrecondition error, since that indicates the node itself was down
|
||||
// for the window and the data carries no signal.
|
||||
double uptime_threshold = 4;
|
||||
}
|
||||
|
||||
message ForwardingAbilityResponse {
|
||||
// Sorted list of unique compressed 33-byte public keys of the peers.
|
||||
repeated bytes peers = 1;
|
||||
|
||||
// Sparse list of forwarding ability entries. An entry is present only for a
|
||||
// pair that forwarded volume, carrying its exact effective_uptime_s and
|
||||
// forwarded_sat. Pairs that did not forward are never listed here; they are
|
||||
// either flagged in quiet_uptime_bitmask or absent. Entries take precedence
|
||||
// over the bitmask for the same pair.
|
||||
repeated ForwardingAbilityEntry entries = 2;
|
||||
|
||||
// The start of the window the metrics cover, as unix seconds.
|
||||
int64 start_time = 3;
|
||||
|
||||
// The end of the window the metrics cover, as unix seconds.
|
||||
int64 end_time = 4;
|
||||
|
||||
// A packed bitmask over the n*n ordered peer pairs, where n is the length
|
||||
// of peers. The bit at index in*n+out is set when that pair held at least
|
||||
// the uptime_threshold fraction of effective uptime over the window but did
|
||||
// not forward: the dense "up but idle" population. A pair with a forwarded
|
||||
// entry is never flagged here. A pair that is neither listed in entries nor
|
||||
// flagged here had sub-threshold uptime and no forwards: treat it as zero.
|
||||
bytes up_but_idle_bitmask = 5;
|
||||
|
||||
// The uptime fraction in [0, 1] used to populate up_but_idle_bitmask,
|
||||
// echoed back so consumers know the threshold the server applied.
|
||||
double uptime_threshold = 6;
|
||||
}
|
||||
|
||||
message ForwardingAbilityEntry {
|
||||
// The indices of the incoming and outgoing peers packed into a single
|
||||
// 32-bit integer: packed_idx = (in << 16) | out. This caps the peer set at
|
||||
// 65535 peers per direction.
|
||||
uint32 packed_idx = 1;
|
||||
|
||||
// Seconds the peer pair held at least the requested liquidity floor of
|
||||
// directional forwardable liquidity over the window.
|
||||
int64 effective_uptime_s = 2;
|
||||
|
||||
// Total successfully forwarded amount over the window, in satoshis.
|
||||
int64 forwarded_sat = 3;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1025,6 +1025,67 @@
|
|||
"default": "UNKNOWN_FIATBACKEND",
|
||||
"description": "FiatBackend is the API endpoint to be used for any fiat related queries.\n\n - COINCAP: Use the CoinCap API for fiat price information.\nThis API is reached through the following URL:\nhttps://api.coincap.io/v2/assets/bitcoin/history\n - COINDESK: Use the CoinDesk API for fiat price information.\nThis API is reached through the following URL:\nhttps://api.coindesk.com/v1/bpi/historical/close.json\n - CUSTOM: Use custom price data provided in a CSV file for fiat price information.\n - COINGECKO: Use the CoinGecko API for fiat price information.\nThis API is reached through the following URL:\nhttps://api.coingecko.com/api/v3/coins/bitcoin/market_chart\n - BITFINEX: Use the Bitfinex API for fiat price information.\nThis API is reached through the following URL:\nhttps://api-pub.bitfinex.com/v2/candles/trade:1h:tBTCUSD/hist"
|
||||
},
|
||||
"frdrpcForwardingAbilityEntry": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"packed_idx": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "The indices of the incoming and outgoing peers packed into a single\n32-bit integer: packed_idx = (in \u003c\u003c 16) | out. This caps the peer set at\n65535 peers per direction."
|
||||
},
|
||||
"effective_uptime_s": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "Seconds the peer pair held at least the requested liquidity floor of\ndirectional forwardable liquidity over the window."
|
||||
},
|
||||
"forwarded_sat": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "Total successfully forwarded amount over the window, in satoshis."
|
||||
}
|
||||
}
|
||||
},
|
||||
"frdrpcForwardingAbilityResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"peers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "byte"
|
||||
},
|
||||
"description": "Sorted list of unique compressed 33-byte public keys of the peers."
|
||||
},
|
||||
"entries": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/frdrpcForwardingAbilityEntry"
|
||||
},
|
||||
"description": "Sparse list of forwarding ability entries. An entry is present only for a\npair that forwarded volume, carrying its exact effective_uptime_s and\nforwarded_sat. Pairs that did not forward are never listed here; they are\neither flagged in quiet_uptime_bitmask or absent. Entries take precedence\nover the bitmask for the same pair."
|
||||
},
|
||||
"start_time": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "The start of the window the metrics cover, as unix seconds."
|
||||
},
|
||||
"end_time": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "The end of the window the metrics cover, as unix seconds."
|
||||
},
|
||||
"up_but_idle_bitmask": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "A packed bitmask over the n*n ordered peer pairs, where n is the length\nof peers. The bit at index in*n+out is set when that pair held at least\nthe uptime_threshold fraction of effective uptime over the window but did\nnot forward: the dense \"up but idle\" population. A pair with a forwarded\nentry is never flagged here. A pair that is neither listed in entries nor\nflagged here had sub-threshold uptime and no forwards: treat it as zero."
|
||||
},
|
||||
"uptime_threshold": {
|
||||
"type": "number",
|
||||
"format": "double",
|
||||
"description": "The uptime fraction in [0, 1] used to populate up_but_idle_bitmask,\nechoed back so consumers know the threshold the server applied."
|
||||
}
|
||||
}
|
||||
},
|
||||
"frdrpcGranularity": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
|
|
|||
|
|
@ -65,6 +65,9 @@ type FaradayServerClient interface {
|
|||
// *
|
||||
// Get a list of channel events that occurred for a given channel.
|
||||
GetChannelEvents(ctx context.Context, in *ChannelEventsRequest, opts ...grpc.CallOption) (*ChannelEventsResponse, error)
|
||||
// *
|
||||
// Get forwarding ability analysis of peer pairs.
|
||||
ForwardingAbility(ctx context.Context, in *ForwardingAbilityRequest, opts ...grpc.CallOption) (*ForwardingAbilityResponse, error)
|
||||
}
|
||||
|
||||
type faradayServerClient struct {
|
||||
|
|
@ -147,6 +150,15 @@ func (c *faradayServerClient) GetChannelEvents(ctx context.Context, in *ChannelE
|
|||
return out, nil
|
||||
}
|
||||
|
||||
func (c *faradayServerClient) ForwardingAbility(ctx context.Context, in *ForwardingAbilityRequest, opts ...grpc.CallOption) (*ForwardingAbilityResponse, error) {
|
||||
out := new(ForwardingAbilityResponse)
|
||||
err := c.cc.Invoke(ctx, "/frdrpc.FaradayServer/ForwardingAbility", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// FaradayServerServer is the server API for FaradayServer service.
|
||||
// All implementations must embed UnimplementedFaradayServerServer
|
||||
// for forward compatibility
|
||||
|
|
@ -198,6 +210,9 @@ type FaradayServerServer interface {
|
|||
// *
|
||||
// Get a list of channel events that occurred for a given channel.
|
||||
GetChannelEvents(context.Context, *ChannelEventsRequest) (*ChannelEventsResponse, error)
|
||||
// *
|
||||
// Get forwarding ability analysis of peer pairs.
|
||||
ForwardingAbility(context.Context, *ForwardingAbilityRequest) (*ForwardingAbilityResponse, error)
|
||||
mustEmbedUnimplementedFaradayServerServer()
|
||||
}
|
||||
|
||||
|
|
@ -229,6 +244,9 @@ func (UnimplementedFaradayServerServer) CloseReport(context.Context, *CloseRepor
|
|||
func (UnimplementedFaradayServerServer) GetChannelEvents(context.Context, *ChannelEventsRequest) (*ChannelEventsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetChannelEvents not implemented")
|
||||
}
|
||||
func (UnimplementedFaradayServerServer) ForwardingAbility(context.Context, *ForwardingAbilityRequest) (*ForwardingAbilityResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ForwardingAbility not implemented")
|
||||
}
|
||||
func (UnimplementedFaradayServerServer) mustEmbedUnimplementedFaradayServerServer() {}
|
||||
|
||||
// UnsafeFaradayServerServer may be embedded to opt out of forward compatibility for this service.
|
||||
|
|
@ -386,6 +404,24 @@ func _FaradayServer_GetChannelEvents_Handler(srv interface{}, ctx context.Contex
|
|||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _FaradayServer_ForwardingAbility_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ForwardingAbilityRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(FaradayServerServer).ForwardingAbility(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/frdrpc.FaradayServer/ForwardingAbility",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(FaradayServerServer).ForwardingAbility(ctx, req.(*ForwardingAbilityRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// FaradayServer_ServiceDesc is the grpc.ServiceDesc for FaradayServer service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
|
|
@ -425,6 +461,10 @@ var FaradayServer_ServiceDesc = grpc.ServiceDesc{
|
|||
MethodName: "GetChannelEvents",
|
||||
Handler: _FaradayServer_GetChannelEvents_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ForwardingAbility",
|
||||
Handler: _FaradayServer_ForwardingAbility_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "faraday.proto",
|
||||
|
|
|
|||
|
|
@ -220,4 +220,29 @@ func RegisterFaradayServerJSONCallbacks(registry map[string]func(ctx context.Con
|
|||
}
|
||||
callback(string(respBytes), nil)
|
||||
}
|
||||
|
||||
registry["frdrpc.FaradayServer.ForwardingAbility"] = func(ctx context.Context,
|
||||
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
|
||||
|
||||
req := &ForwardingAbilityRequest{}
|
||||
err := marshaler.Unmarshal([]byte(reqJSON), req)
|
||||
if err != nil {
|
||||
callback("", err)
|
||||
return
|
||||
}
|
||||
|
||||
client := NewFaradayServerClient(conn)
|
||||
resp, err := client.ForwardingAbility(ctx, req)
|
||||
if err != nil {
|
||||
callback("", err)
|
||||
return
|
||||
}
|
||||
|
||||
respBytes, err := marshaler.Marshal(resp)
|
||||
if err != nil {
|
||||
callback("", err)
|
||||
return
|
||||
}
|
||||
callback(string(respBytes), nil)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
374
frdrpc/forwarding_ability_codec.go
Normal file
374
frdrpc/forwarding_ability_codec.go
Normal file
|
|
@ -0,0 +1,374 @@
|
|||
package frdrpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// maxPackedPeers is the largest peer set a response can address. packed_idx
|
||||
// splits a uint32 into two 16-bit indices (in << 16 | out), so each direction
|
||||
// can reference at most 65535 distinct peers.
|
||||
const maxPackedPeers = 1<<16 - 1
|
||||
|
||||
// ForwardingAbility is a client-facing mirror of the raw forwarding facts for
|
||||
// one direction of a peer pair over the analysis window. Derived rates and
|
||||
// categories are left to the consumer.
|
||||
type ForwardingAbility struct {
|
||||
// EffectiveUptimeS is the seconds the pair held at least the requested
|
||||
// liquidity floor of directional forwardable liquidity over the window.
|
||||
// The value is whole seconds: sub-second uptime floors to zero, so a
|
||||
// pair that forwarded volume over a fleeting qualifying window can
|
||||
// report a zero uptime alongside a non-zero ForwardedSat.
|
||||
EffectiveUptimeS int64
|
||||
|
||||
// ForwardedSat is the total successfully forwarded amount over the
|
||||
// window, in satoshis.
|
||||
ForwardedSat int64
|
||||
}
|
||||
|
||||
// abilityTier classifies how a pair is encoded: a full entry, a single "up but
|
||||
// idle" bit, or omitted entirely.
|
||||
type abilityTier int
|
||||
|
||||
const (
|
||||
// tierAbsent omits the pair: it neither forwarded nor held enough
|
||||
// uptime to clear the threshold. Consumers treat absence as zero.
|
||||
tierAbsent abilityTier = iota
|
||||
|
||||
// tierBit flags the pair in the up-but-idle bitmask: it held at least
|
||||
// the uptime threshold but did not forward.
|
||||
tierBit
|
||||
|
||||
// tierEntry emits a full entry carrying the pair's exact uptime and
|
||||
// forwarded volume. Reserved for pairs that actually forwarded.
|
||||
tierEntry
|
||||
)
|
||||
|
||||
// tier decides how a pair is encoded given the minimum qualifying uptime in
|
||||
// seconds. Forwarding always wins, so a pair that forwarded keeps its exact
|
||||
// facts even if its uptime is below the threshold; otherwise the pair is
|
||||
// compacted to a bit when it was up enough, and dropped when it was not.
|
||||
func (a ForwardingAbility) tier(minUptimeS int64) abilityTier {
|
||||
switch {
|
||||
case a.ForwardedSat > 0:
|
||||
return tierEntry
|
||||
|
||||
case a.EffectiveUptimeS >= minUptimeS:
|
||||
return tierBit
|
||||
|
||||
default:
|
||||
return tierAbsent
|
||||
}
|
||||
}
|
||||
|
||||
// MinQualifyingUptime converts an uptime fraction threshold into the smallest
|
||||
// whole-second uptime that clears it over the given window. It is the single
|
||||
// source of truth shared by the encoder (to bucket pairs) and the server (to
|
||||
// apply the node-down guard), so the two cannot drift. A pair clears the
|
||||
// threshold when EffectiveUptimeS >= the returned value, matching the "at least
|
||||
// the threshold fraction" contract. The result is floored at one second so a
|
||||
// pair with zero uptime is never treated as up. A non-positive window admits
|
||||
// nothing.
|
||||
func MinQualifyingUptime(threshold float64, windowSeconds int64) int64 {
|
||||
if windowSeconds <= 0 {
|
||||
return math.MaxInt64
|
||||
}
|
||||
|
||||
v := int64(math.Ceil(threshold * float64(windowSeconds)))
|
||||
if v < 1 {
|
||||
v = 1
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
// setBit sets the bit at the given index in a packed bitmask. The index is an
|
||||
// int64 because an n*n bitmask over the full peer set overflows a 32-bit int.
|
||||
func setBit(mask []byte, index int64) {
|
||||
mask[index/8] |= 1 << (index % 8)
|
||||
}
|
||||
|
||||
// getBit reports whether the bit at the given index in a packed bitmask is set.
|
||||
// The index is an int64 because an n*n bitmask over the full peer set overflows
|
||||
// a 32-bit int.
|
||||
func getBit(mask []byte, index int64) bool {
|
||||
return mask[index/8]&(1<<(index%8)) != 0
|
||||
}
|
||||
|
||||
// EncodeForwardingAbility serializes a nested map of peer forwarding abilities
|
||||
// into a memory-efficient sparse gRPC response over [startTime, endTime]. To
|
||||
// optimize payload size it tiers each pair: pairs that forwarded keep a full
|
||||
// entry, pairs that were up at least uptimeThreshold of the window but did not
|
||||
// forward collapse to a single bit in the up-but-idle bitmask, and pairs below
|
||||
// the threshold that did not forward are omitted entirely. Public keys are
|
||||
// deduplicated and peer pairs packed into 32-bit indices.
|
||||
func EncodeForwardingAbility(abilities map[string]map[string]ForwardingAbility,
|
||||
startTime, endTime int64,
|
||||
uptimeThreshold float64) (*ForwardingAbilityResponse, error) {
|
||||
|
||||
minUptimeS := MinQualifyingUptime(uptimeThreshold, endTime-startTime)
|
||||
|
||||
// First, find all unique peers involved in pairs that warrant either an
|
||||
// entry or a bit. Keys are normalized to lower-case hex so a peer that
|
||||
// appears in mixed case across entries collapses to a single index
|
||||
// rather than being silently dropped at lookup time.
|
||||
peerSet := make(map[string]struct{})
|
||||
for inPeer, outMap := range abilities {
|
||||
for outPeer, ability := range outMap {
|
||||
if ability.tier(minUptimeS) == tierAbsent {
|
||||
continue
|
||||
}
|
||||
|
||||
peerSet[strings.ToLower(inPeer)] = struct{}{}
|
||||
peerSet[strings.ToLower(outPeer)] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Decode to raw bytes and sort.
|
||||
var rawPeers [][]byte
|
||||
for peerHex := range peerSet {
|
||||
b, err := hex.DecodeString(peerHex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawPeers = append(rawPeers, b)
|
||||
}
|
||||
|
||||
sort.Slice(
|
||||
rawPeers,
|
||||
func(i, j int) bool {
|
||||
return bytes.Compare(rawPeers[i], rawPeers[j]) < 0
|
||||
},
|
||||
)
|
||||
|
||||
// Peer indices occupy 16 bits each in packed_idx, so the set must stay
|
||||
// within maxPackedPeers. Beyond it, an index would overflow its field
|
||||
// and silently decode to the wrong peer pair, so fail loudly instead.
|
||||
if len(rawPeers) > maxPackedPeers {
|
||||
return nil, fmt.Errorf("peer set of %d exceeds the %d "+
|
||||
"addressable by packed_idx", len(rawPeers),
|
||||
maxPackedPeers)
|
||||
}
|
||||
|
||||
// Create map for index lookup using normalized lowercase hex strings.
|
||||
peerIndex := make(map[string]uint32)
|
||||
for idx, b := range rawPeers {
|
||||
peerIndex[hex.EncodeToString(b)] = uint32(idx)
|
||||
}
|
||||
|
||||
// The bitmask addresses every ordered pair over the peer set, so it
|
||||
// needs n*n bits. Allocation is deferred until a bit is actually set so
|
||||
// a response with no up-but-idle pairs carries no bitmask at all.
|
||||
n := int64(len(rawPeers))
|
||||
var bitmask []byte
|
||||
|
||||
// Build the entries and bitmask. seen guards against two input keys
|
||||
// that differ only by hex case collapsing onto the same packed pair.
|
||||
var entries []*ForwardingAbilityEntry
|
||||
seen := make(map[uint32]struct{})
|
||||
|
||||
// addEntry appends a full entry for a forwarded pair.
|
||||
addEntry := func(packed uint32, a ForwardingAbility) {
|
||||
entries = append(entries, &ForwardingAbilityEntry{
|
||||
PackedIdx: packed,
|
||||
EffectiveUptimeS: a.EffectiveUptimeS,
|
||||
ForwardedSat: a.ForwardedSat,
|
||||
})
|
||||
}
|
||||
|
||||
for inPeer, outMap := range abilities {
|
||||
inIdx, okIn := peerIndex[strings.ToLower(inPeer)]
|
||||
if !okIn {
|
||||
continue
|
||||
}
|
||||
|
||||
for outPeer, ability := range outMap {
|
||||
tier := ability.tier(minUptimeS)
|
||||
if tier == tierAbsent {
|
||||
continue
|
||||
}
|
||||
|
||||
outIdx, okOut := peerIndex[strings.ToLower(outPeer)]
|
||||
if !okOut {
|
||||
continue
|
||||
}
|
||||
|
||||
// Pack the in-peer index into the high 16 bits and the
|
||||
// out-peer index into the low 16.
|
||||
packed := (inIdx << 16) | outIdx
|
||||
|
||||
// Reject a case-folded collision rather than silently
|
||||
// dropping one of the two entries' facts.
|
||||
if _, dup := seen[packed]; dup {
|
||||
return nil, fmt.Errorf("duplicate peer pair "+
|
||||
"after case normalization: "+
|
||||
"in=%s out=%s", inPeer, outPeer)
|
||||
}
|
||||
seen[packed] = struct{}{}
|
||||
|
||||
switch tier {
|
||||
case tierEntry:
|
||||
addEntry(packed, ability)
|
||||
|
||||
case tierBit:
|
||||
// The bitmask addresses n*n ordered pairs, one
|
||||
// bit each.
|
||||
if bitmask == nil {
|
||||
bitmask = make(
|
||||
[]byte, (n*n+7)/8,
|
||||
)
|
||||
}
|
||||
setBit(
|
||||
bitmask,
|
||||
int64(inIdx)*n+int64(outIdx),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort entries by packed_idx for deterministic output and testability.
|
||||
sort.Slice(
|
||||
entries,
|
||||
func(i, j int) bool {
|
||||
return entries[i].PackedIdx < entries[j].PackedIdx
|
||||
},
|
||||
)
|
||||
|
||||
return &ForwardingAbilityResponse{
|
||||
Peers: rawPeers,
|
||||
Entries: entries,
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
UpButIdleBitmask: bitmask,
|
||||
UptimeThreshold: uptimeThreshold,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DecodeForwardingAbility reconstructs the nested map of peer forwarding
|
||||
// abilities from a sparse packed gRPC response. Forwarded pairs come back with
|
||||
// their exact facts; up-but-idle pairs flagged in the bitmask come back at full
|
||||
// window uptime with zero forwarded volume. It validates packed indices and the
|
||||
// bitmask length against the decoded peer list to prevent out-of-bounds errors.
|
||||
func DecodeForwardingAbility(resp *ForwardingAbilityResponse) (
|
||||
map[string]map[string]ForwardingAbility, error) {
|
||||
|
||||
result := make(map[string]map[string]ForwardingAbility)
|
||||
if resp == nil {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
numPeers := len(resp.Peers)
|
||||
|
||||
// packed_idx addresses peers with 16-bit indices, so a response with
|
||||
// more than maxPackedPeers peers is malformed. Rejecting it here also
|
||||
// keeps the n*n bitmask-length computation below from overflowing a
|
||||
// 32-bit int.
|
||||
if numPeers > maxPackedPeers {
|
||||
return nil, fmt.Errorf("peer set of %d exceeds the %d "+
|
||||
"addressable by packed_idx", numPeers, maxPackedPeers)
|
||||
}
|
||||
|
||||
record := func(inIdx, outIdx int, ability ForwardingAbility) {
|
||||
inPeer := hex.EncodeToString(resp.Peers[inIdx])
|
||||
outPeer := hex.EncodeToString(resp.Peers[outIdx])
|
||||
|
||||
if _, ok := result[inPeer]; !ok {
|
||||
result[inPeer] = make(map[string]ForwardingAbility)
|
||||
}
|
||||
result[inPeer][outPeer] = ability
|
||||
}
|
||||
|
||||
// Decode the forwarded entries first so they take precedence over any
|
||||
// bit set for the same pair.
|
||||
for _, entry := range resp.Entries {
|
||||
// Unpack the pair: the in-peer index is the high 16 bits, the
|
||||
// out-peer index the low 16.
|
||||
inIdx := int(entry.PackedIdx >> 16)
|
||||
outIdx := int(entry.PackedIdx & 0xffff)
|
||||
|
||||
if inIdx >= numPeers || outIdx >= numPeers {
|
||||
return nil, errors.New("decoded peer index out of " +
|
||||
"bounds")
|
||||
}
|
||||
|
||||
record(
|
||||
inIdx, outIdx, ForwardingAbility{
|
||||
EffectiveUptimeS: entry.EffectiveUptimeS,
|
||||
ForwardedSat: entry.ForwardedSat,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Expand the up-but-idle bitmask. An absent bitmask simply means no
|
||||
// pair was flagged; a present one must address exactly the n*n pairs.
|
||||
bitmask := resp.UpButIdleBitmask
|
||||
if len(bitmask) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Compute the expected length in int64 so the n*n multiplication does
|
||||
// not overflow a 32-bit int for a large peer set.
|
||||
totalPairs := int64(numPeers) * int64(numPeers)
|
||||
if want := int((totalPairs + 7) / 8); len(bitmask) != want {
|
||||
return nil, fmt.Errorf("bitmask length %d does not match the "+
|
||||
"%d expected for %d peers", len(bitmask), want,
|
||||
numPeers)
|
||||
}
|
||||
|
||||
// Up-but-idle pairs were up the whole window by definition of the
|
||||
// threshold bucket, so reconstruct them at full window uptime with zero
|
||||
// forwarded volume. Iterate over the bitmask bytes directly, skipping
|
||||
// zero bytes, so cost scales with the number of set bits rather than
|
||||
// the O(n*n) pair space; padding bits beyond n*n are ignored.
|
||||
windowSeconds := resp.EndTime - resp.StartTime
|
||||
for i, b := range bitmask {
|
||||
if b == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for bit := range 8 {
|
||||
// If the bit is not set, skip the pair. This also
|
||||
// implicitly ignores any padding bits in the last byte
|
||||
// beyond the n*n pairs.
|
||||
if b&(1<<bit) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Compute the pair index from the byte index and bit
|
||||
// position.
|
||||
k := int64(i)*8 + int64(bit)
|
||||
if k >= totalPairs {
|
||||
break
|
||||
}
|
||||
|
||||
// Unpack the pair: the in-peer index is the high 16
|
||||
// bits, the out-peer index the low 16. The bounds were
|
||||
// already checked against the bitmask length, so this
|
||||
// cannot overflow.
|
||||
inIdx := int(k / int64(numPeers))
|
||||
outIdx := int(k % int64(numPeers))
|
||||
|
||||
// An entry for this pair takes precedence; never
|
||||
// overwrite it.
|
||||
inPeer := hex.EncodeToString(resp.Peers[inIdx])
|
||||
outPeer := hex.EncodeToString(resp.Peers[outIdx])
|
||||
if _, ok := result[inPeer][outPeer]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
record(
|
||||
inIdx, outIdx, ForwardingAbility{
|
||||
EffectiveUptimeS: windowSeconds,
|
||||
ForwardedSat: 0,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
587
frdrpc/forwarding_ability_codec_test.go
Normal file
587
frdrpc/forwarding_ability_codec_test.go
Normal file
|
|
@ -0,0 +1,587 @@
|
|||
package frdrpc
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// fwdKey returns a distinct 33-byte compressed-pubkey hex string for n. Keys
|
||||
// sort ascending in n, matching the byte ordering the encoder applies.
|
||||
func fwdKey(n int) string {
|
||||
return fmt.Sprintf("02%064x", n)
|
||||
}
|
||||
|
||||
// pair is one expected decoded entry, flattened from the nested result map for
|
||||
// easy comparison.
|
||||
type pair struct {
|
||||
in string
|
||||
out string
|
||||
ability ForwardingAbility
|
||||
}
|
||||
|
||||
// TestForwardingAbilityCodecRoundTrip verifies the three-tier encoding: pairs
|
||||
// that forwarded keep exact facts as entries, pairs up at least the threshold
|
||||
// but idle collapse to a bitmask bit (decoded at full window uptime), and
|
||||
// sub-threshold idle pairs are dropped. The window is [0, 100) and the
|
||||
// threshold 0.5, so the minimum qualifying uptime is 50 seconds.
|
||||
func TestForwardingAbilityCodecRoundTrip(t *testing.T) {
|
||||
const (
|
||||
startTime, endTime int64 = 0, 100
|
||||
threshold float64 = 0.5
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
abilities map[string]map[string]ForwardingAbility
|
||||
wantPeers []string
|
||||
wantPairs []pair
|
||||
wantBitmask bool
|
||||
}{
|
||||
{
|
||||
// Forwarding wins regardless of uptime, so a
|
||||
// zero-uptime pair that moved volume survives with its
|
||||
// exact facts and never lands in the bitmask.
|
||||
name: "forwarded pairs keep exact facts",
|
||||
abilities: map[string]map[string]ForwardingAbility{
|
||||
fwdKey(1): {
|
||||
fwdKey(2): {
|
||||
EffectiveUptimeS: 80,
|
||||
ForwardedSat: 1500,
|
||||
},
|
||||
},
|
||||
fwdKey(2): {
|
||||
fwdKey(1): {
|
||||
EffectiveUptimeS: 0,
|
||||
ForwardedSat: 2500,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantPeers: []string{
|
||||
fwdKey(1),
|
||||
fwdKey(2),
|
||||
},
|
||||
wantPairs: []pair{
|
||||
{
|
||||
fwdKey(1),
|
||||
fwdKey(2),
|
||||
ForwardingAbility{
|
||||
80,
|
||||
1500,
|
||||
},
|
||||
},
|
||||
{
|
||||
fwdKey(2),
|
||||
fwdKey(1),
|
||||
ForwardingAbility{
|
||||
0,
|
||||
2500,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantBitmask: false,
|
||||
},
|
||||
{
|
||||
// Up at or above the threshold but no forwards: a bit,
|
||||
// decoded back at the full window's uptime.
|
||||
name: "up but idle becomes a bit",
|
||||
abilities: map[string]map[string]ForwardingAbility{
|
||||
fwdKey(1): {
|
||||
fwdKey(2): {
|
||||
EffectiveUptimeS: 80,
|
||||
},
|
||||
},
|
||||
fwdKey(3): {
|
||||
fwdKey(1): {
|
||||
EffectiveUptimeS: 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantPeers: []string{
|
||||
fwdKey(1),
|
||||
fwdKey(2),
|
||||
fwdKey(3),
|
||||
},
|
||||
wantPairs: []pair{
|
||||
{
|
||||
fwdKey(1),
|
||||
fwdKey(2),
|
||||
ForwardingAbility{
|
||||
100,
|
||||
0,
|
||||
},
|
||||
},
|
||||
{
|
||||
fwdKey(3),
|
||||
fwdKey(1),
|
||||
ForwardingAbility{
|
||||
100,
|
||||
0,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantBitmask: true,
|
||||
},
|
||||
{
|
||||
// Below the threshold with no forwards: dropped.
|
||||
name: "sub-threshold idle pairs dropped",
|
||||
abilities: map[string]map[string]ForwardingAbility{
|
||||
fwdKey(1): {
|
||||
fwdKey(2): {
|
||||
EffectiveUptimeS: 49,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantPeers: []string{},
|
||||
wantPairs: []pair{},
|
||||
wantBitmask: false,
|
||||
},
|
||||
{
|
||||
// All three tiers at once, including a peer that only
|
||||
// appears via the bitmask.
|
||||
name: "mixed tiers",
|
||||
abilities: map[string]map[string]ForwardingAbility{
|
||||
fwdKey(1): {
|
||||
fwdKey(2): {
|
||||
EffectiveUptimeS: 80,
|
||||
ForwardedSat: 1500,
|
||||
},
|
||||
fwdKey(3): {
|
||||
EffectiveUptimeS: 60,
|
||||
},
|
||||
},
|
||||
fwdKey(2): {
|
||||
fwdKey(3): {
|
||||
EffectiveUptimeS: 10,
|
||||
},
|
||||
},
|
||||
fwdKey(3): {
|
||||
fwdKey(1): {
|
||||
ForwardedSat: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantPeers: []string{
|
||||
fwdKey(1),
|
||||
fwdKey(2),
|
||||
fwdKey(3),
|
||||
},
|
||||
wantPairs: []pair{
|
||||
{
|
||||
fwdKey(1),
|
||||
fwdKey(2),
|
||||
ForwardingAbility{
|
||||
80,
|
||||
1500,
|
||||
},
|
||||
},
|
||||
{
|
||||
fwdKey(1),
|
||||
fwdKey(3),
|
||||
ForwardingAbility{
|
||||
100,
|
||||
0,
|
||||
},
|
||||
},
|
||||
{
|
||||
fwdKey(3),
|
||||
fwdKey(1),
|
||||
ForwardingAbility{
|
||||
0,
|
||||
500,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantBitmask: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
resp, err := EncodeForwardingAbility(
|
||||
tc.abilities, startTime, endTime, threshold,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, startTime, resp.StartTime)
|
||||
require.Equal(t, endTime, resp.EndTime)
|
||||
require.Equal(t, threshold, resp.UptimeThreshold)
|
||||
|
||||
require.Equal(
|
||||
t, tc.wantBitmask,
|
||||
len(resp.UpButIdleBitmask) > 0,
|
||||
)
|
||||
|
||||
// A present bitmask must address exactly n*n bits.
|
||||
if tc.wantBitmask {
|
||||
n := len(resp.Peers)
|
||||
require.Len(t, resp.UpButIdleBitmask, (n*n+7)/8)
|
||||
}
|
||||
|
||||
gotPeers := make([]string, len(resp.Peers))
|
||||
for i, p := range resp.Peers {
|
||||
gotPeers[i] = hex.EncodeToString(p)
|
||||
}
|
||||
require.Equal(t, tc.wantPeers, gotPeers)
|
||||
|
||||
decoded, err := DecodeForwardingAbility(resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
got := make(map[string]ForwardingAbility)
|
||||
for in, outMap := range decoded {
|
||||
for out, ability := range outMap {
|
||||
got[in+"->"+out] = ability
|
||||
}
|
||||
}
|
||||
require.Len(t, got, len(tc.wantPairs))
|
||||
for _, wp := range tc.wantPairs {
|
||||
require.Equal(
|
||||
t, wp.ability, got[wp.in+"->"+wp.out],
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMinQualifyingUptime verifies the threshold-to-seconds conversion shared
|
||||
// by the encoder and the server guard, including its boundary behavior.
|
||||
func TestMinQualifyingUptime(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
threshold float64
|
||||
window int64
|
||||
want int64
|
||||
}{
|
||||
{
|
||||
"half of clean window",
|
||||
0.5,
|
||||
100,
|
||||
50,
|
||||
},
|
||||
{
|
||||
"rounds up a fraction",
|
||||
0.333,
|
||||
100,
|
||||
34,
|
||||
},
|
||||
{
|
||||
"integer boundary",
|
||||
0.9,
|
||||
2_592_000,
|
||||
2_332_800,
|
||||
},
|
||||
{
|
||||
"floored at one second",
|
||||
0.0,
|
||||
100,
|
||||
1,
|
||||
},
|
||||
{
|
||||
"non-positive window admits nothing",
|
||||
0.5,
|
||||
0,
|
||||
math.MaxInt64,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(
|
||||
tc.name,
|
||||
func(t *testing.T) {
|
||||
require.Equal(
|
||||
t, tc.want, MinQualifyingUptime(
|
||||
tc.threshold, tc.window,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBitmaskHelpers verifies that setBit and getBit address the same bit.
|
||||
func TestBitmaskHelpers(t *testing.T) {
|
||||
mask := make([]byte, 2)
|
||||
require.False(t, getBit(mask, 9))
|
||||
|
||||
setBit(mask, 9)
|
||||
require.True(t, getBit(mask, 9))
|
||||
require.False(t, getBit(mask, 8))
|
||||
require.False(t, getBit(mask, 10))
|
||||
}
|
||||
|
||||
// TestForwardingAbilityDecodeEntryPrecedence verifies that when a pair is both
|
||||
// listed as an entry and flagged in the bitmask, the entry's exact facts win.
|
||||
func TestForwardingAbilityDecodeEntryPrecedence(t *testing.T) {
|
||||
// Two peers => a 2*2 bitmask needs (4+7)/8 = 1 byte. Set the bit for
|
||||
// pair (0, 1) at index 0*2+1 = 1, and also list it as an entry.
|
||||
mask := make([]byte, 1)
|
||||
setBit(mask, 1)
|
||||
|
||||
resp := &ForwardingAbilityResponse{
|
||||
Peers: [][]byte{
|
||||
{
|
||||
1,
|
||||
},
|
||||
{
|
||||
2,
|
||||
},
|
||||
},
|
||||
StartTime: 0,
|
||||
EndTime: 100,
|
||||
Entries: []*ForwardingAbilityEntry{
|
||||
{
|
||||
PackedIdx: (0 << 16) | 1,
|
||||
EffectiveUptimeS: 42,
|
||||
ForwardedSat: 7,
|
||||
},
|
||||
},
|
||||
UpButIdleBitmask: mask,
|
||||
}
|
||||
|
||||
decoded, err := DecodeForwardingAbility(resp)
|
||||
require.NoError(t, err)
|
||||
require.Equal(
|
||||
t, ForwardingAbility{42, 7},
|
||||
decoded[hex.EncodeToString([]byte{1})][hex.EncodeToString(
|
||||
[]byte{2},
|
||||
)],
|
||||
)
|
||||
}
|
||||
|
||||
// TestForwardingAbilityDecodeBadIndex verifies that a packed index referencing
|
||||
// a peer beyond the decoded peer list is rejected rather than silently mapped.
|
||||
func TestForwardingAbilityDecodeBadIndex(t *testing.T) {
|
||||
resp := &ForwardingAbilityResponse{
|
||||
Peers: [][]byte{
|
||||
{
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
},
|
||||
},
|
||||
Entries: []*ForwardingAbilityEntry{
|
||||
{
|
||||
// Out index 1 is out of bounds for a single
|
||||
// peer.
|
||||
PackedIdx: (0 << 16) | 1,
|
||||
EffectiveUptimeS: 3600,
|
||||
ForwardedSat: 1000,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := DecodeForwardingAbility(resp)
|
||||
require.ErrorContains(t, err, "peer index out of bounds")
|
||||
}
|
||||
|
||||
// TestForwardingAbilityDecodeBadBitmaskLen verifies that a bitmask whose length
|
||||
// does not match the n*n pairs of the peer set is rejected.
|
||||
func TestForwardingAbilityDecodeBadBitmaskLen(t *testing.T) {
|
||||
resp := &ForwardingAbilityResponse{
|
||||
// Two peers expect a 1-byte bitmask; supply two bytes.
|
||||
Peers: [][]byte{
|
||||
{
|
||||
1,
|
||||
},
|
||||
{
|
||||
2,
|
||||
},
|
||||
},
|
||||
UpButIdleBitmask: []byte{
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
}
|
||||
|
||||
_, err := DecodeForwardingAbility(resp)
|
||||
require.ErrorContains(t, err, "bitmask length")
|
||||
}
|
||||
|
||||
// TestForwardingAbilityEncodePeerCap verifies that a peer set too large to
|
||||
// address with packed_idx is rejected loudly instead of overflowing an index
|
||||
// into the wrong peer pair.
|
||||
func TestForwardingAbilityEncodePeerCap(t *testing.T) {
|
||||
outMap := make(map[string]ForwardingAbility)
|
||||
for i := 1; i <= maxPackedPeers+1; i++ {
|
||||
// Use forwarded volume so inclusion is threshold-independent.
|
||||
outMap[fwdKey(i)] = ForwardingAbility{ForwardedSat: 1}
|
||||
}
|
||||
abilities := map[string]map[string]ForwardingAbility{
|
||||
fwdKey(0): outMap,
|
||||
}
|
||||
|
||||
_, err := EncodeForwardingAbility(abilities, 0, 1, 0.5)
|
||||
require.ErrorContains(t, err, "exceeds")
|
||||
}
|
||||
|
||||
// TestForwardingAbilityEncodeNormalizesCase verifies that a peer appearing in
|
||||
// mixed hex case collapses to a single index rather than producing a duplicate
|
||||
// peer entry.
|
||||
func TestForwardingAbilityEncodeNormalizesCase(t *testing.T) {
|
||||
// Use a key with hex letters so its upper- and lower-case forms are
|
||||
// genuinely distinct map keys.
|
||||
peer := fwdKey(0xabcdef)
|
||||
|
||||
abilities := map[string]map[string]ForwardingAbility{
|
||||
strings.ToUpper(peer): {
|
||||
fwdKey(2): {
|
||||
ForwardedSat: 20,
|
||||
},
|
||||
},
|
||||
peer: {
|
||||
fwdKey(3): {
|
||||
ForwardedSat: 40,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := EncodeForwardingAbility(abilities, 0, 1, 0.5)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The upper- and lower-case forms of the shared peer must dedup to one
|
||||
// index, leaving exactly three distinct peers.
|
||||
require.Len(t, resp.Peers, 3)
|
||||
|
||||
decoded, err := DecodeForwardingAbility(resp)
|
||||
require.NoError(t, err)
|
||||
require.Equal(
|
||||
t, ForwardingAbility{
|
||||
ForwardedSat: 20,
|
||||
},
|
||||
decoded[peer][fwdKey(2)],
|
||||
)
|
||||
require.Equal(
|
||||
t, ForwardingAbility{
|
||||
ForwardedSat: 40,
|
||||
},
|
||||
decoded[peer][fwdKey(3)],
|
||||
)
|
||||
}
|
||||
|
||||
// TestForwardingAbilityEncodeRejectsCaseCollision verifies that two input keys
|
||||
// that differ only by hex case but address the same peer pair are rejected
|
||||
// rather than silently collapsing onto one packed index and dropping a fact.
|
||||
func TestForwardingAbilityEncodeRejectsCaseCollision(t *testing.T) {
|
||||
inPeer := fwdKey(0xabcdef)
|
||||
outPeer := fwdKey(2)
|
||||
|
||||
// Both in-peer spellings normalize to the same index and share the same
|
||||
// out-peer, so they collide on packed_idx.
|
||||
abilities := map[string]map[string]ForwardingAbility{
|
||||
strings.ToUpper(inPeer): {
|
||||
outPeer: {
|
||||
ForwardedSat: 10,
|
||||
},
|
||||
},
|
||||
inPeer: {
|
||||
outPeer: {
|
||||
ForwardedSat: 20,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := EncodeForwardingAbility(abilities, 0, 1, 0.5)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
// TestForwardingAbilityCodecRoundTripHighIndices round-trips a large peer set
|
||||
// so that packed indices exceed a single byte and exercise the high bits of
|
||||
// each 16-bit direction field, and the up-but-idle bitmask spans many bytes. It
|
||||
// guards index packing and bitmask addressing against regressions that only
|
||||
// surface beyond the small indices the other round-trip cases use.
|
||||
func TestForwardingAbilityCodecRoundTripHighIndices(t *testing.T) {
|
||||
const (
|
||||
numPeers = 300
|
||||
startTime, endTime = int64(0), int64(100)
|
||||
threshold = 0.5
|
||||
)
|
||||
|
||||
// Build a cycle so every peer appears and takes a stable index equal to
|
||||
// its fwdKey ordinal. Even edges forward (kept as exact entries); odd
|
||||
// edges are up but idle at >= threshold (collapsed to a bitmask bit,
|
||||
// decoded back at the full window uptime).
|
||||
abilities := make(map[string]map[string]ForwardingAbility, numPeers)
|
||||
want := make(map[string]ForwardingAbility, numPeers)
|
||||
for i := range numPeers {
|
||||
in, out := fwdKey(i), fwdKey((i+1)%numPeers)
|
||||
|
||||
var enc, dec ForwardingAbility
|
||||
if i%2 == 0 {
|
||||
// Add pair that forwarded.
|
||||
enc = ForwardingAbility{
|
||||
EffectiveUptimeS: 70,
|
||||
ForwardedSat: int64(i + 1),
|
||||
}
|
||||
dec = enc
|
||||
} else {
|
||||
// Add up, but idle pair.
|
||||
enc = ForwardingAbility{EffectiveUptimeS: 60}
|
||||
dec = ForwardingAbility{
|
||||
EffectiveUptimeS: endTime - startTime,
|
||||
}
|
||||
}
|
||||
|
||||
abilities[in] = map[string]ForwardingAbility{out: enc}
|
||||
want[in+"->"+out] = dec
|
||||
}
|
||||
|
||||
resp, err := EncodeForwardingAbility(
|
||||
abilities, startTime, endTime, threshold,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Peers, numPeers)
|
||||
|
||||
// With 300 peers the indices exceed one byte, so at least one packed
|
||||
// index must use the high bits of its 16-bit field.
|
||||
var sawHighIdx bool
|
||||
for _, e := range resp.Entries {
|
||||
if e.PackedIdx>>16 > 0xff || e.PackedIdx&0xffff > 0xff {
|
||||
sawHighIdx = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, sawHighIdx, "expected an index beyond one byte")
|
||||
|
||||
decoded, err := DecodeForwardingAbility(resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
got := make(map[string]ForwardingAbility)
|
||||
for in, outMap := range decoded {
|
||||
for out, ability := range outMap {
|
||||
got[in+"->"+out] = ability
|
||||
}
|
||||
}
|
||||
require.Equal(t, want, got)
|
||||
}
|
||||
|
||||
// TestForwardingAbilityDecodeNil verifies that decoding a nil response yields
|
||||
// an empty map rather than panicking.
|
||||
func TestForwardingAbilityDecodeNil(t *testing.T) {
|
||||
decoded, err := DecodeForwardingAbility(nil)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, decoded)
|
||||
}
|
||||
|
||||
// TestForwardingAbilityDecodeIgnoresPaddingBit verifies that a bit set in the
|
||||
// padding region beyond the n*n pairs of the final byte is ignored rather than
|
||||
// decoded into a bogus pair.
|
||||
func TestForwardingAbilityDecodeIgnoresPaddingBit(t *testing.T) {
|
||||
// Two peers => 2*2 = 4 valid bits in a 1-byte mask; bits 4..7 are
|
||||
// padding. Set padding bit 5 and assert nothing decodes from it.
|
||||
mask := make([]byte, 1)
|
||||
setBit(mask, 5)
|
||||
|
||||
resp := &ForwardingAbilityResponse{
|
||||
Peers: [][]byte{{1}, {2}},
|
||||
StartTime: 0,
|
||||
EndTime: 100,
|
||||
UpButIdleBitmask: mask,
|
||||
}
|
||||
|
||||
decoded, err := DecodeForwardingAbility(resp)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, decoded)
|
||||
}
|
||||
|
|
@ -2,16 +2,22 @@ module github.com/lightninglabs/faraday/frdrpc
|
|||
|
||||
require (
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
google.golang.org/grpc v1.65.0
|
||||
google.golang.org/protobuf v1.34.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.15.0 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
go 1.25.5
|
||||
|
|
|
|||
|
|
@ -1,7 +1,20 @@
|
|||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
|
||||
github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
|
|
@ -16,3 +29,8 @@ google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
|||
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
169
frdrpcserver/forwarding_ability.go
Normal file
169
frdrpcserver/forwarding_ability.go
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
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
|
||||
}
|
||||
299
frdrpcserver/forwarding_ability_test.go
Normal file
299
frdrpcserver/forwarding_ability_test.go
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
package frdrpcserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightninglabs/faraday/chanevents"
|
||||
"github.com/lightninglabs/faraday/frdrpc"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type mockForwardingAnalyzer struct {
|
||||
effectiveUptimeFunc func(ctx context.Context, startTime, endTime time.Time,
|
||||
liquidityFloor btcutil.Amount) (
|
||||
map[chanevents.PeerPair]chanevents.ForwardingAbility, error)
|
||||
}
|
||||
|
||||
func (m *mockForwardingAnalyzer) EffectiveUptime(ctx context.Context, startTime,
|
||||
endTime time.Time, liquidityFloor btcutil.Amount) (
|
||||
map[chanevents.PeerPair]chanevents.ForwardingAbility, error) {
|
||||
|
||||
return m.effectiveUptimeFunc(ctx, startTime, endTime, liquidityFloor)
|
||||
}
|
||||
|
||||
// TestForwardingAbility tests the ForwardingAbility RPC method, covering both
|
||||
// successful and error cases.
|
||||
func TestForwardingAbility(t *testing.T) {
|
||||
const (
|
||||
peerIn = "02aaaabbbbcccc0000000000000000000000000000000000000000000000000001"
|
||||
peerOut = "02aaaabbbbcccc0000000000000000000000000000000000000000000000000002"
|
||||
)
|
||||
|
||||
// analyzerResult is the canned analyzer return for a case. A nil
|
||||
// analyzerResult means the case leaves ForwardingAnalyzer unconfigured.
|
||||
type analyzerResult func() (
|
||||
map[chanevents.PeerPair]chanevents.ForwardingAbility, error)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
analyzer analyzerResult
|
||||
req *frdrpc.ForwardingAbilityRequest
|
||||
|
||||
// wantCode is the expected gRPC status; codes.OK denotes
|
||||
// success.
|
||||
wantCode codes.Code
|
||||
|
||||
// check runs on success with the response and the floor the
|
||||
// handler resolved and passed to the analyzer.
|
||||
check func(t *testing.T, resp *frdrpc.ForwardingAbilityResponse,
|
||||
gotFloor btcutil.Amount)
|
||||
}{
|
||||
{
|
||||
name: "encodes analyzer facts",
|
||||
analyzer: func() (
|
||||
map[chanevents.PeerPair]chanevents.ForwardingAbility,
|
||||
error) {
|
||||
|
||||
return map[chanevents.PeerPair]chanevents.ForwardingAbility{
|
||||
{
|
||||
PeerIn: peerIn,
|
||||
PeerOut: peerOut,
|
||||
}: {
|
||||
EffectiveUptime: 90 * time.Second,
|
||||
ForwardedAmount: 550,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
req: &frdrpc.ForwardingAbilityRequest{
|
||||
StartTime: 100,
|
||||
EndTime: 200,
|
||||
LiquidityFloorSat: 1000,
|
||||
},
|
||||
wantCode: codes.OK,
|
||||
check: func(t *testing.T,
|
||||
resp *frdrpc.ForwardingAbilityResponse,
|
||||
gotFloor btcutil.Amount) {
|
||||
|
||||
// The explicit floor passes straight through.
|
||||
require.Equal(t, btcutil.Amount(1000), gotFloor)
|
||||
require.Len(t, resp.Peers, 2)
|
||||
require.Len(t, resp.Entries, 1)
|
||||
require.EqualValues(t, 100, resp.StartTime)
|
||||
require.EqualValues(t, 200, resp.EndTime)
|
||||
require.EqualValues(
|
||||
t, 90, resp.Entries[0].EffectiveUptimeS,
|
||||
)
|
||||
require.EqualValues(
|
||||
t, 550, resp.Entries[0].ForwardedSat,
|
||||
)
|
||||
|
||||
// An unset threshold echoes the server default,
|
||||
// and a forwarded pair leaves the bitmask empty.
|
||||
require.Equal(
|
||||
t, defaultUptimeThreshold,
|
||||
resp.UptimeThreshold,
|
||||
)
|
||||
require.Empty(t, resp.UpButIdleBitmask)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unset floor uses server default",
|
||||
analyzer: func() (
|
||||
map[chanevents.PeerPair]chanevents.ForwardingAbility,
|
||||
error) {
|
||||
|
||||
// Return a fully-up pair so the node-down guard
|
||||
// passes and the default floor can be observed.
|
||||
return map[chanevents.PeerPair]chanevents.ForwardingAbility{
|
||||
{PeerIn: peerIn, PeerOut: peerOut}: {
|
||||
EffectiveUptime: 100 * time.Second,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
req: &frdrpc.ForwardingAbilityRequest{
|
||||
StartTime: 100,
|
||||
EndTime: 200,
|
||||
},
|
||||
wantCode: codes.OK,
|
||||
check: func(t *testing.T,
|
||||
_ *frdrpc.ForwardingAbilityResponse,
|
||||
gotFloor btcutil.Amount) {
|
||||
|
||||
require.Equal(
|
||||
t, btcutil.Amount(
|
||||
defaultLiquidityFloorSat,
|
||||
), gotFloor,
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "node down trips guard",
|
||||
analyzer: func() (
|
||||
map[chanevents.PeerPair]chanevents.ForwardingAbility,
|
||||
error) {
|
||||
|
||||
// A single pair, up well below the default 0.9
|
||||
// threshold over the 100s window and with no
|
||||
// forwards, leaves nothing that clears it.
|
||||
return map[chanevents.PeerPair]chanevents.ForwardingAbility{
|
||||
{PeerIn: peerIn, PeerOut: peerOut}: {
|
||||
EffectiveUptime: 10 * time.Second,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
req: &frdrpc.ForwardingAbilityRequest{
|
||||
StartTime: 100,
|
||||
EndTime: 200,
|
||||
},
|
||||
wantCode: codes.FailedPrecondition,
|
||||
},
|
||||
{
|
||||
name: "low-uptime forward does not rescue guard",
|
||||
analyzer: func() (
|
||||
map[chanevents.PeerPair]chanevents.ForwardingAbility,
|
||||
error) {
|
||||
|
||||
// Forwarded volume at sub-threshold uptime must
|
||||
// not satisfy the guard.
|
||||
return map[chanevents.PeerPair]chanevents.ForwardingAbility{
|
||||
{PeerIn: peerIn, PeerOut: peerOut}: {
|
||||
EffectiveUptime: 10 * time.Second,
|
||||
ForwardedAmount: 999,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
req: &frdrpc.ForwardingAbilityRequest{
|
||||
StartTime: 100,
|
||||
EndTime: 200,
|
||||
},
|
||||
wantCode: codes.FailedPrecondition,
|
||||
},
|
||||
{
|
||||
name: "explicit threshold flags up-but-idle pair",
|
||||
analyzer: func() (
|
||||
map[chanevents.PeerPair]chanevents.ForwardingAbility,
|
||||
error) {
|
||||
|
||||
return map[chanevents.PeerPair]chanevents.ForwardingAbility{
|
||||
{PeerIn: peerIn, PeerOut: peerOut}: {
|
||||
EffectiveUptime: 60 * time.Second,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
req: &frdrpc.ForwardingAbilityRequest{
|
||||
StartTime: 100,
|
||||
EndTime: 200,
|
||||
UptimeThreshold: 0.5,
|
||||
},
|
||||
wantCode: codes.OK,
|
||||
check: func(t *testing.T,
|
||||
resp *frdrpc.ForwardingAbilityResponse,
|
||||
_ btcutil.Amount) {
|
||||
|
||||
// Up 60s of a 100s window at a 0.5 threshold:
|
||||
// idle, so a bit and no entry.
|
||||
require.Equal(t, 0.5, resp.UptimeThreshold)
|
||||
require.Empty(t, resp.Entries)
|
||||
require.NotEmpty(t, resp.UpButIdleBitmask)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "out of range threshold is rejected",
|
||||
analyzer: func() (
|
||||
map[chanevents.PeerPair]chanevents.ForwardingAbility,
|
||||
error) {
|
||||
|
||||
return nil, nil
|
||||
},
|
||||
req: &frdrpc.ForwardingAbilityRequest{
|
||||
StartTime: 100,
|
||||
EndTime: 200,
|
||||
UptimeThreshold: 1.5,
|
||||
},
|
||||
wantCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "start after end is rejected",
|
||||
req: &frdrpc.ForwardingAbilityRequest{
|
||||
StartTime: 200,
|
||||
EndTime: 100,
|
||||
},
|
||||
wantCode: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "missing analyzer is unavailable",
|
||||
req: &frdrpc.ForwardingAbilityRequest{
|
||||
StartTime: 100,
|
||||
EndTime: 200,
|
||||
LiquidityFloorSat: 1000,
|
||||
},
|
||||
wantCode: codes.Unavailable,
|
||||
},
|
||||
{
|
||||
name: "analyzer error is internal",
|
||||
analyzer: func() (
|
||||
map[chanevents.PeerPair]chanevents.ForwardingAbility,
|
||||
error) {
|
||||
|
||||
return nil, errors.New("db lookup failed")
|
||||
},
|
||||
req: &frdrpc.ForwardingAbilityRequest{
|
||||
StartTime: 100,
|
||||
EndTime: 200,
|
||||
LiquidityFloorSat: 1000,
|
||||
},
|
||||
wantCode: codes.Internal,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(
|
||||
tc.name,
|
||||
func(t *testing.T) {
|
||||
var gotFloor btcutil.Amount
|
||||
|
||||
cfg := &Config{}
|
||||
if tc.analyzer != nil {
|
||||
cfg.ForwardingAnalyzer = &mockForwardingAnalyzer{
|
||||
effectiveUptimeFunc: func(
|
||||
_ context.Context, _,
|
||||
_ time.Time,
|
||||
floor btcutil.Amount) (
|
||||
map[chanevents.PeerPair]chanevents.ForwardingAbility,
|
||||
error) {
|
||||
|
||||
gotFloor = floor
|
||||
|
||||
return tc.analyzer()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
server := NewRPCServer(cfg)
|
||||
resp, err := server.ForwardingAbility(
|
||||
t.Context(), tc.req,
|
||||
)
|
||||
|
||||
if tc.wantCode != codes.OK {
|
||||
st, ok := status.FromError(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, tc.wantCode, st.Code())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
if tc.check != nil {
|
||||
tc.check(t, resp, gotFloor)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -37,4 +37,8 @@ var RequiredPermissions = map[string][]bakery.Op{
|
|||
Entity: "events",
|
||||
Action: "read",
|
||||
}},
|
||||
"/frdrpc.FaradayServer/ForwardingAbility": {{
|
||||
Entity: "insights",
|
||||
Action: "read",
|
||||
}},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ package frdrpcserver
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightninglabs/faraday/accounting"
|
||||
"github.com/lightninglabs/faraday/chain"
|
||||
"github.com/lightninglabs/faraday/chanevents"
|
||||
|
|
@ -53,6 +55,15 @@ type RPCServer struct {
|
|||
cfg *Config
|
||||
}
|
||||
|
||||
// ForwardingAnalyzer is the seam the RPC server uses to compute per-peer-pair
|
||||
// forwarding facts. It is an interface so the handler can be exercised against
|
||||
// a mock analyzer in tests.
|
||||
type ForwardingAnalyzer interface {
|
||||
EffectiveUptime(ctx context.Context, startTime, endTime time.Time,
|
||||
liquidityFloor btcutil.Amount) (
|
||||
map[chanevents.PeerPair]chanevents.ForwardingAbility, error)
|
||||
}
|
||||
|
||||
// Config provides closures and settings required to run the rpc server.
|
||||
type Config struct {
|
||||
// Lnd is a client which can be used to query lnd.
|
||||
|
|
@ -61,6 +72,11 @@ type Config struct {
|
|||
// ChanEvents is a database of channel events.
|
||||
ChanEvents *chanevents.Store
|
||||
|
||||
// ForwardingAnalyzer computes forwarding-ability facts for the
|
||||
// ForwardingAbility RPC. When nil, that endpoint returns
|
||||
// codes.Unavailable.
|
||||
ForwardingAnalyzer ForwardingAnalyzer
|
||||
|
||||
// BitcoinClient is an optional client which can be used to query
|
||||
// on-chain data from a connected bitcoin node. If nil, faraday will
|
||||
// not be able to serve endpoints which require on-chain data.
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -20,7 +20,6 @@ require (
|
|||
github.com/shopspring/decimal v1.2.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/urfave/cli v1.22.14
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
|
||||
google.golang.org/grpc v1.65.0
|
||||
google.golang.org/protobuf v1.34.2
|
||||
gopkg.in/macaroon-bakery.v2 v2.0.1
|
||||
|
|
@ -164,6 +163,7 @@ require (
|
|||
go.uber.org/multierr v1.6.0 // indirect
|
||||
go.uber.org/zap v1.17.0 // indirect
|
||||
golang.org/x/crypto v0.39.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -470,8 +470,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq
|
|||
github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
|
||||
github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import (
|
|||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// TestGetChannelEvents pins the GetChannelEvents RPC contract: a regtest
|
||||
|
|
@ -146,3 +148,215 @@ func TestGetChannelEvents(t *testing.T) {
|
|||
"event order mismatch at index %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
// TestForwardingAbility integration test opens a channel, sends payments to
|
||||
// seed events, and verifies that calling the ForwardingAbility RPC returns
|
||||
// the peer pair analytics successfully and can be decoded.
|
||||
func TestForwardingAbility(t *testing.T) {
|
||||
c := newTestContext(t)
|
||||
defer c.stop()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Connect nodes and open a channel from alice to bob.
|
||||
var aliceChannelAmt = btcutil.Amount(500000)
|
||||
|
||||
err := c.aliceClient.Client.Connect(
|
||||
ctx, c.bobPubkey, "localhost:10012", true,
|
||||
)
|
||||
require.NoError(c.t, err, "could not connect nodes")
|
||||
|
||||
_, _ = c.openChannel(
|
||||
c.aliceClient.Client, c.bobPubkey, aliceChannelAmt,
|
||||
)
|
||||
|
||||
// Wait until alice can route a payment to bob.
|
||||
var paymentAmount lnwire.MilliSatoshi = 20000000
|
||||
c.eventuallyf(func() bool {
|
||||
return c.channelRoutable(c.bobPubkey, paymentAmount)
|
||||
}, "channel did not become routable")
|
||||
|
||||
// Send a payment from alice to bob.
|
||||
hash, payreq := c.addInvoice(c.bobClient.Client, paymentAmount)
|
||||
c.makePayment(
|
||||
c.aliceClient.LndServices, c.bobClient.LndServices,
|
||||
lndclient.SendPaymentRequest{
|
||||
Invoice: payreq,
|
||||
PaymentHash: &hash,
|
||||
Timeout: paymentTimeout,
|
||||
}, lnrpc.Payment_SUCCEEDED,
|
||||
)
|
||||
|
||||
// The alice->bob payment moved liquidity onto bob's side of the only
|
||||
// channel, so from this point on the bob self-pair holds at least the
|
||||
// requested floor. Measuring over a window that starts now keeps the
|
||||
// pair's uptime fraction high, which both clears the uptime threshold
|
||||
// and keeps the node-down guard satisfied. A future end time extends
|
||||
// the window over the still-funded state.
|
||||
bobHex := c.bobPubkey.String()
|
||||
windowStart := time.Now()
|
||||
|
||||
// The events store ingests channel updates asynchronously, so retry
|
||||
// until the bob self-pair surfaces.
|
||||
var ability frdrpc.ForwardingAbility
|
||||
c.eventuallyf(func() bool {
|
||||
endTime := time.Now().Add(2 * time.Second).Unix()
|
||||
resp, err := c.faradayClient.ForwardingAbility(
|
||||
ctx, &frdrpc.ForwardingAbilityRequest{
|
||||
StartTime: uint64(windowStart.Unix()),
|
||||
EndTime: uint64(endTime),
|
||||
LiquidityFloorSat: 1000,
|
||||
UptimeThreshold: 0.1,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
decoded, err := frdrpc.DecodeForwardingAbility(resp)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
a, ok := decoded[bobHex][bobHex]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
ability = a
|
||||
|
||||
return true
|
||||
}, "expected bob self-pair in forwarding ability")
|
||||
|
||||
// The bob self-pair was up but never forwarded through itself, so it
|
||||
// surfaces via the up-but-idle bitmask: non-zero effective uptime and
|
||||
// zero forwarded volume.
|
||||
require.Greater(c.t, ability.EffectiveUptimeS, int64(0))
|
||||
require.Zero(c.t, ability.ForwardedSat)
|
||||
}
|
||||
|
||||
// TestForwardingDowntime exercises the offline/online plumbing end to end. It
|
||||
// disconnects the only channel peer to take the channel offline, asserts that
|
||||
// faraday records the resulting offline event, then reconnects the peer and
|
||||
// asserts the recovering online event lands and the bob self-pair surfaces in
|
||||
// ForwardingAbility again. This proves downtime and recovery flow through to
|
||||
// the analyzer; the exact per-second uptime math is covered deterministically
|
||||
// by the analyzer unit tests.
|
||||
func TestForwardingDowntime(t *testing.T) {
|
||||
c := newTestContext(t)
|
||||
defer c.stop()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Connect nodes and open a channel from alice to bob.
|
||||
var aliceChannelAmt = btcutil.Amount(500000)
|
||||
|
||||
err := c.aliceClient.Client.Connect(
|
||||
ctx, c.bobPubkey, "localhost:10012", true,
|
||||
)
|
||||
require.NoError(c.t, err, "could not connect nodes")
|
||||
|
||||
aliceChannel, _ := c.openChannel(
|
||||
c.aliceClient.Client, c.bobPubkey, aliceChannelAmt,
|
||||
)
|
||||
|
||||
// Wait until alice can route a payment to bob.
|
||||
var paymentAmount lnwire.MilliSatoshi = 20000000
|
||||
c.eventuallyf(func() bool {
|
||||
return c.channelRoutable(c.bobPubkey, paymentAmount)
|
||||
}, "channel did not become routable")
|
||||
|
||||
// Move liquidity onto bob's side so the bob self-pair clears the
|
||||
// liquidity floor while the channel is up.
|
||||
hash, payreq := c.addInvoice(c.bobClient.Client, paymentAmount)
|
||||
c.makePayment(
|
||||
c.aliceClient.LndServices, c.bobClient.LndServices,
|
||||
lndclient.SendPaymentRequest{
|
||||
Invoice: payreq,
|
||||
PaymentHash: &hash,
|
||||
Timeout: paymentTimeout,
|
||||
}, lnrpc.Payment_SUCCEEDED,
|
||||
)
|
||||
|
||||
chanPoint := aliceChannel.String()
|
||||
bobHex := c.bobPubkey.String()
|
||||
|
||||
// Snapshot the event counts before the disconnect so we can detect the
|
||||
// new offline and online events the disconnect and recovery produce.
|
||||
onlineBefore, offlineBefore := c.channelEventCounts(chanPoint)
|
||||
|
||||
// Disconnect bob to take the only channel offline.
|
||||
c.disconnectPeer(c.aliceClient, c.bobPubkey)
|
||||
|
||||
// faraday should ingest the resulting offline event: this is the
|
||||
// downtime signal that the channel went inactive.
|
||||
c.eventuallyf(func() bool {
|
||||
_, offline := c.channelEventCounts(chanPoint)
|
||||
return offline > offlineBefore
|
||||
}, "expected an offline event after disconnect")
|
||||
|
||||
// An explicit DisconnectPeer is sticky: lnd does not auto-reconnect, so
|
||||
// the channel stays offline until we reconnect. A window that sits
|
||||
// entirely in this offline period leaves no pair clearing the uptime
|
||||
// threshold, so the node-down guard rejects the request with
|
||||
// FailedPrecondition rather than returning an empty response. The
|
||||
// threshold is irrelevant here since the only pair has zero uptime.
|
||||
c.eventuallyf(func() bool {
|
||||
now := time.Now()
|
||||
_, err := c.faradayClient.ForwardingAbility(
|
||||
ctx, &frdrpc.ForwardingAbilityRequest{
|
||||
StartTime: uint64(now.Unix()),
|
||||
EndTime: uint64(
|
||||
now.Add(2 * time.Second).Unix(),
|
||||
),
|
||||
LiquidityFloorSat: 1000,
|
||||
UptimeThreshold: 0.9,
|
||||
},
|
||||
)
|
||||
|
||||
return status.Code(err) == codes.FailedPrecondition
|
||||
}, "expected node-down guard while bob is disconnected")
|
||||
|
||||
// An explicit DisconnectPeer drops lnd's persistent connection, so the
|
||||
// channel only comes back up once we reconnect. Reconnect bob to bring
|
||||
// the channel active again.
|
||||
err = c.aliceClient.Client.Connect(
|
||||
ctx, c.bobPubkey, "localhost:10012", true,
|
||||
)
|
||||
require.NoError(c.t, err, "could not reconnect nodes")
|
||||
|
||||
// The channel goes active again on reconnect, which faraday records as
|
||||
// an online event.
|
||||
c.eventuallyf(func() bool {
|
||||
online, _ := c.channelEventCounts(chanPoint)
|
||||
return online > onlineBefore
|
||||
}, "expected an online event after reconnect")
|
||||
|
||||
// With the channel back up and liquidity still on bob's side, the bob
|
||||
// self-pair surfaces in ForwardingAbility again over a fresh window
|
||||
// that opens after recovery.
|
||||
c.eventuallyf(func() bool {
|
||||
now := time.Now()
|
||||
resp, err := c.faradayClient.ForwardingAbility(
|
||||
ctx, &frdrpc.ForwardingAbilityRequest{
|
||||
StartTime: uint64(now.Unix()),
|
||||
EndTime: uint64(
|
||||
now.Add(2 * time.Second).Unix(),
|
||||
),
|
||||
LiquidityFloorSat: 1000,
|
||||
UptimeThreshold: 0.1,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
decoded, err := frdrpc.DecodeForwardingAbility(resp)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
_, ok := decoded[bobHex][bobHex]
|
||||
|
||||
return ok
|
||||
}, "expected bob self-pair after reconnect")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -466,6 +466,58 @@ func (c *testContext) channelRoutable(dest route.Vertex,
|
|||
return err == nil
|
||||
}
|
||||
|
||||
// disconnectPeer disconnects the given client from a peer, taking any channels
|
||||
// between them offline. lnd normally refuses to disconnect from a peer with an
|
||||
// active channel, but the itest lnd is a non-integration build where unsafe
|
||||
// disconnect is always permitted. The raw lnrpc client is used because the
|
||||
// high-level lndclient interface exposes no Disconnect, and the admin macaroon
|
||||
// is attached at call time since the shared connection carries none.
|
||||
func (c *testContext) disconnectPeer(client *lndclient.GrpcLndServices,
|
||||
peer route.Vertex) {
|
||||
|
||||
c.t.Helper()
|
||||
|
||||
ctx, err := client.WithMacaroonAuthForService(
|
||||
context.Background(), lndclient.AdminServiceMac,
|
||||
)
|
||||
require.NoError(c.t, err, "could not attach macaroon")
|
||||
|
||||
raw := lnrpc.NewLightningClient(client.ClientConn)
|
||||
_, err = raw.DisconnectPeer(ctx, &lnrpc.DisconnectPeerRequest{
|
||||
PubKey: peer.String(),
|
||||
})
|
||||
require.NoError(c.t, err, "could not disconnect peer")
|
||||
}
|
||||
|
||||
// channelEventCounts returns how many online and offline events faraday has
|
||||
// recorded for the given channel up to the present.
|
||||
func (c *testContext) channelEventCounts(chanPoint string) (online,
|
||||
offline int) {
|
||||
|
||||
c.t.Helper()
|
||||
|
||||
endTime := time.Now().Add(time.Second).Unix()
|
||||
events, err := c.faradayClient.GetChannelEvents(
|
||||
context.Background(), &frdrpc.ChannelEventsRequest{
|
||||
ChanPoint: chanPoint,
|
||||
EndTime: endTime,
|
||||
},
|
||||
)
|
||||
require.NoError(c.t, err, "could not get channel events")
|
||||
|
||||
for _, event := range events.Events {
|
||||
switch event.EventType {
|
||||
case frdrpc.ChannelEventType_CHAN_EVENT_ONLINE:
|
||||
online++
|
||||
|
||||
case frdrpc.ChannelEventType_CHAN_EVENT_OFFLINE:
|
||||
offline++
|
||||
}
|
||||
}
|
||||
|
||||
return online, offline
|
||||
}
|
||||
|
||||
// findChannel finds a channel in a set of open channels, returning nil if it
|
||||
// is not found.
|
||||
// nolint:interfacer
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue