mirror of
https://github.com/lightninglabs/faraday.git
synced 2026-08-13 12:33:35 +02:00
299 lines
7.6 KiB
Go
299 lines
7.6 KiB
Go
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)
|
|
}
|
|
},
|
|
)
|
|
}
|
|
}
|