Merge pull request #23 from asymmetric-research/rpc-refactor

RPC Refactor (5)
This commit is contained in:
Matt Johnstone 2024-06-18 17:12:36 +02:00 committed by GitHub
commit c53c7c7b9e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 602 additions and 802 deletions

View File

@ -1,246 +0,0 @@
package main
import (
"context"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestSolanaCollector_Collect_Dynamic(t *testing.T) {
client := newDynamicRPCClient()
collector := createSolanaCollector(client, slotPacerSchedule)
prometheus.NewPedanticRegistry().MustRegister(collector)
// start off by testing initial state:
testCases := []collectionTest{
{
Name: "solana_active_validators",
ExpectedResponse: `
# HELP solana_active_validators Total number of active validators by state
# TYPE solana_active_validators gauge
solana_active_validators{state="current"} 3
solana_active_validators{state="delinquent"} 0
`,
},
{
Name: "solana_validator_activated_stake",
ExpectedResponse: `
# HELP solana_validator_activated_stake Activated stake per validator
# TYPE solana_validator_activated_stake gauge
solana_validator_activated_stake{nodekey="aaa",pubkey="AAA"} 1000000
solana_validator_activated_stake{nodekey="bbb",pubkey="BBB"} 1000000
solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 1000000
`,
},
{
Name: "solana_validator_root_slot",
ExpectedResponse: `
# HELP solana_validator_root_slot Root slot per validator
# TYPE solana_validator_root_slot gauge
solana_validator_root_slot{nodekey="aaa",pubkey="AAA"} 0
solana_validator_root_slot{nodekey="bbb",pubkey="BBB"} 0
solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 0
`,
},
{
Name: "solana_validator_delinquent",
ExpectedResponse: `
# HELP solana_validator_delinquent Whether a validator is delinquent
# TYPE solana_validator_delinquent gauge
solana_validator_delinquent{nodekey="aaa",pubkey="AAA"} 0
solana_validator_delinquent{nodekey="bbb",pubkey="BBB"} 0
solana_validator_delinquent{nodekey="ccc",pubkey="CCC"} 0
`,
},
{
Name: "solana_node_version",
ExpectedResponse: `
# HELP solana_node_version Node version of solana
# TYPE solana_node_version gauge
solana_node_version{version="v1.0.0"} 1
`,
},
}
runCollectionTests(t, collector, testCases)
// now make some changes:
client.UpdateStake("aaa", 2_000_000)
client.UpdateStake("bbb", 500_000)
client.UpdateDelinquency("ccc", true)
client.UpdateVersion("v1.2.3")
// now test the final state
testCases = []collectionTest{
{
Name: "solana_active_validators",
ExpectedResponse: `
# HELP solana_active_validators Total number of active validators by state
# TYPE solana_active_validators gauge
solana_active_validators{state="current"} 2
solana_active_validators{state="delinquent"} 1
`,
},
{
Name: "solana_validator_activated_stake",
ExpectedResponse: `
# HELP solana_validator_activated_stake Activated stake per validator
# TYPE solana_validator_activated_stake gauge
solana_validator_activated_stake{nodekey="aaa",pubkey="AAA"} 2000000
solana_validator_activated_stake{nodekey="bbb",pubkey="BBB"} 500000
solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 1000000
`,
},
{
Name: "solana_validator_root_slot",
ExpectedResponse: `
# HELP solana_validator_root_slot Root slot per validator
# TYPE solana_validator_root_slot gauge
solana_validator_root_slot{nodekey="aaa",pubkey="AAA"} 0
solana_validator_root_slot{nodekey="bbb",pubkey="BBB"} 0
solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 0
`,
},
{
Name: "solana_validator_delinquent",
ExpectedResponse: `
# HELP solana_validator_delinquent Whether a validator is delinquent
# TYPE solana_validator_delinquent gauge
solana_validator_delinquent{nodekey="aaa",pubkey="AAA"} 0
solana_validator_delinquent{nodekey="bbb",pubkey="BBB"} 0
solana_validator_delinquent{nodekey="ccc",pubkey="CCC"} 1
`,
},
{
Name: "solana_node_version",
ExpectedResponse: `
# HELP solana_node_version Node version of solana
# TYPE solana_node_version gauge
solana_node_version{version="v1.2.3"} 1
`,
},
}
runCollectionTests(t, collector, testCases)
}
type slotMetricValues struct {
SlotHeight float64
TotalTransactions float64
EpochNumber float64
EpochFirstSlot float64
EpochLastSlot float64
}
func getSlotMetricValues() slotMetricValues {
return slotMetricValues{
SlotHeight: testutil.ToFloat64(confirmedSlotHeight),
TotalTransactions: testutil.ToFloat64(totalTransactionsTotal),
EpochNumber: testutil.ToFloat64(currentEpochNumber),
EpochFirstSlot: testutil.ToFloat64(epochFirstSlot),
EpochLastSlot: testutil.ToFloat64(epochLastSlot),
}
}
func TestSolanaCollector_WatchSlots_Dynamic(t *testing.T) {
// reset metrics before running tests:
leaderSlotsTotal.Reset()
leaderSlotsByEpoch.Reset()
// create clients:
client := newDynamicRPCClient()
collector := createSolanaCollector(client, 300*time.Millisecond)
prometheus.NewPedanticRegistry().MustRegister(collector)
// start client/collector and wait a bit:
runCtx, runCancel := context.WithCancel(context.Background())
defer runCancel()
go client.Run(runCtx)
time.Sleep(time.Second)
slotsCtx, slotsCancel := context.WithCancel(context.Background())
defer slotsCancel()
go collector.WatchSlots(slotsCtx)
time.Sleep(time.Second)
initial := getSlotMetricValues()
// wait a bit:
var epochChanged bool
for i := 0; i < 5; i++ {
// wait a bit then get new metrics
time.Sleep(time.Second)
final := getSlotMetricValues()
// make sure things are changing correctly:
assertSlotMetricsChangeCorrectly(t, initial, final)
// sense check to make sure the exporter is not "ahead" of the client (due to double counting or whatever)
assert.LessOrEqualf(
t,
int(final.SlotHeight),
client.Slot,
"Exporter slot (%v) ahead of client slot (%v)!",
int(final.SlotHeight),
client.Slot,
)
assert.LessOrEqualf(
t,
int(final.TotalTransactions),
client.TransactionCount,
"Exporter transaction count (%v) ahead of client transaction count (%v)!",
int(final.TotalTransactions),
client.TransactionCount,
)
assert.LessOrEqualf(
t,
int(final.EpochNumber),
client.Epoch,
"Exporter epoch (%v) ahead of client epoch (%v)!",
int(final.EpochNumber),
client.Epoch,
)
// check if epoch changed
if final.EpochNumber > initial.EpochNumber {
epochChanged = true
}
// make current final the new initial (for next iteration)
initial = final
}
// epoch should have changed somewhere
assert.Truef(t, epochChanged, "Epoch has not changed!")
}
func assertSlotMetricsChangeCorrectly(t *testing.T, initial slotMetricValues, final slotMetricValues) {
// make sure that things have increased
assert.Greaterf(
t,
final.SlotHeight,
initial.SlotHeight,
"Slot has not increased! (%v -> %v)",
initial.SlotHeight,
final.SlotHeight,
)
assert.Greaterf(
t,
final.TotalTransactions,
initial.TotalTransactions,
"Total transactions have not increased! (%v -> %v)",
initial.TotalTransactions,
final.TotalTransactions,
)
assert.GreaterOrEqualf(
t,
final.EpochNumber,
initial.EpochNumber,
"Epoch number has decreased! (%v -> %v)",
initial.EpochNumber,
final.EpochNumber,
)
}

View File

@ -140,8 +140,8 @@ func (c *staticRPCClient) GetBlockProduction(
ctx context.Context,
firstSlot *int64,
lastSlot *int64,
) (rpc.BlockProduction, error) {
return staticBlockProduction, nil
) (*rpc.BlockProduction, error) {
return &staticBlockProduction, nil
}
/*
@ -299,7 +299,7 @@ func (c *dynamicRPCClient) GetBlockProduction(
ctx context.Context,
firstSlot *int64,
lastSlot *int64,
) (rpc.BlockProduction, error) {
) (*rpc.BlockProduction, error) {
hostProduction := make(map[string]rpc.BlockProductionPerHost)
for _, identity := range identities {
hostProduction[identity] = rpc.BlockProductionPerHost{LeaderSlots: 0, BlocksProduced: 0}
@ -313,11 +313,12 @@ func (c *dynamicRPCClient) GetBlockProduction(
}
hostProduction[info.leader] = hp
}
return rpc.BlockProduction{
production := rpc.BlockProduction{
FirstSlot: *firstSlot,
LastSlot: *lastSlot,
Hosts: hostProduction,
}, nil
}
return &production, nil
}
/*
@ -349,7 +350,195 @@ func runCollectionTests(t *testing.T, collector prometheus.Collector, testCases
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
err := testutil.CollectAndCompare(collector, bytes.NewBufferString(test.ExpectedResponse), test.Name)
assert.Nilf(t, "unexpected collecting result for %s: \n%s", test.Name, err)
assert.Nilf(t, err, "unexpected collecting result for %s: \n%s", test.Name, err)
})
}
}
func TestSolanaCollector_Collect_Static(t *testing.T) {
collector := createSolanaCollector(
&staticRPCClient{},
slotPacerSchedule,
)
prometheus.NewPedanticRegistry().MustRegister(collector)
testCases := []collectionTest{
{
Name: "solana_active_validators",
ExpectedResponse: `
# HELP solana_active_validators Total number of active validators by state
# TYPE solana_active_validators gauge
solana_active_validators{state="current"} 2
solana_active_validators{state="delinquent"} 1
`,
},
{
Name: "solana_validator_activated_stake",
ExpectedResponse: `
# HELP solana_validator_activated_stake Activated stake per validator
# TYPE solana_validator_activated_stake gauge
solana_validator_activated_stake{nodekey="aaa",pubkey="AAA"} 49
solana_validator_activated_stake{nodekey="bbb",pubkey="BBB"} 42
solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 43
`,
},
{
Name: "solana_validator_last_vote",
ExpectedResponse: `
# HELP solana_validator_last_vote Last voted slot per validator
# TYPE solana_validator_last_vote gauge
solana_validator_last_vote{nodekey="aaa",pubkey="AAA"} 92
solana_validator_last_vote{nodekey="bbb",pubkey="BBB"} 147
solana_validator_last_vote{nodekey="ccc",pubkey="CCC"} 148
`,
},
{
Name: "solana_validator_root_slot",
ExpectedResponse: `
# HELP solana_validator_root_slot Root slot per validator
# TYPE solana_validator_root_slot gauge
solana_validator_root_slot{nodekey="aaa",pubkey="AAA"} 3
solana_validator_root_slot{nodekey="bbb",pubkey="BBB"} 18
solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 19
`,
},
{
Name: "solana_validator_delinquent",
ExpectedResponse: `
# HELP solana_validator_delinquent Whether a validator is delinquent
# TYPE solana_validator_delinquent gauge
solana_validator_delinquent{nodekey="aaa",pubkey="AAA"} 1
solana_validator_delinquent{nodekey="bbb",pubkey="BBB"} 0
solana_validator_delinquent{nodekey="ccc",pubkey="CCC"} 0
`,
},
{
Name: "solana_node_version",
ExpectedResponse: `
# HELP solana_node_version Node version of solana
# TYPE solana_node_version gauge
solana_node_version{version="1.16.7"} 1
`,
},
}
runCollectionTests(t, collector, testCases)
}
func TestSolanaCollector_Collect_Dynamic(t *testing.T) {
client := newDynamicRPCClient()
collector := createSolanaCollector(client, slotPacerSchedule)
prometheus.NewPedanticRegistry().MustRegister(collector)
// start off by testing initial state:
testCases := []collectionTest{
{
Name: "solana_active_validators",
ExpectedResponse: `
# HELP solana_active_validators Total number of active validators by state
# TYPE solana_active_validators gauge
solana_active_validators{state="current"} 3
solana_active_validators{state="delinquent"} 0
`,
},
{
Name: "solana_validator_activated_stake",
ExpectedResponse: `
# HELP solana_validator_activated_stake Activated stake per validator
# TYPE solana_validator_activated_stake gauge
solana_validator_activated_stake{nodekey="aaa",pubkey="AAA"} 1000000
solana_validator_activated_stake{nodekey="bbb",pubkey="BBB"} 1000000
solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 1000000
`,
},
{
Name: "solana_validator_root_slot",
ExpectedResponse: `
# HELP solana_validator_root_slot Root slot per validator
# TYPE solana_validator_root_slot gauge
solana_validator_root_slot{nodekey="aaa",pubkey="AAA"} 0
solana_validator_root_slot{nodekey="bbb",pubkey="BBB"} 0
solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 0
`,
},
{
Name: "solana_validator_delinquent",
ExpectedResponse: `
# HELP solana_validator_delinquent Whether a validator is delinquent
# TYPE solana_validator_delinquent gauge
solana_validator_delinquent{nodekey="aaa",pubkey="AAA"} 0
solana_validator_delinquent{nodekey="bbb",pubkey="BBB"} 0
solana_validator_delinquent{nodekey="ccc",pubkey="CCC"} 0
`,
},
{
Name: "solana_node_version",
ExpectedResponse: `
# HELP solana_node_version Node version of solana
# TYPE solana_node_version gauge
solana_node_version{version="v1.0.0"} 1
`,
},
}
runCollectionTests(t, collector, testCases)
// now make some changes:
client.UpdateStake("aaa", 2_000_000)
client.UpdateStake("bbb", 500_000)
client.UpdateDelinquency("ccc", true)
client.UpdateVersion("v1.2.3")
// now test the final state
testCases = []collectionTest{
{
Name: "solana_active_validators",
ExpectedResponse: `
# HELP solana_active_validators Total number of active validators by state
# TYPE solana_active_validators gauge
solana_active_validators{state="current"} 2
solana_active_validators{state="delinquent"} 1
`,
},
{
Name: "solana_validator_activated_stake",
ExpectedResponse: `
# HELP solana_validator_activated_stake Activated stake per validator
# TYPE solana_validator_activated_stake gauge
solana_validator_activated_stake{nodekey="aaa",pubkey="AAA"} 2000000
solana_validator_activated_stake{nodekey="bbb",pubkey="BBB"} 500000
solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 1000000
`,
},
{
Name: "solana_validator_root_slot",
ExpectedResponse: `
# HELP solana_validator_root_slot Root slot per validator
# TYPE solana_validator_root_slot gauge
solana_validator_root_slot{nodekey="aaa",pubkey="AAA"} 0
solana_validator_root_slot{nodekey="bbb",pubkey="BBB"} 0
solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 0
`,
},
{
Name: "solana_validator_delinquent",
ExpectedResponse: `
# HELP solana_validator_delinquent Whether a validator is delinquent
# TYPE solana_validator_delinquent gauge
solana_validator_delinquent{nodekey="aaa",pubkey="AAA"} 0
solana_validator_delinquent{nodekey="bbb",pubkey="BBB"} 0
solana_validator_delinquent{nodekey="ccc",pubkey="CCC"} 1
`,
},
{
Name: "solana_node_version",
ExpectedResponse: `
# HELP solana_node_version Node version of solana
# TYPE solana_node_version gauge
solana_node_version{version="v1.2.3"} 1
`,
},
}
runCollectionTests(t, collector, testCases)
}

View File

@ -0,0 +1,214 @@
package main
import (
"context"
"fmt"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
type slotMetricValues struct {
SlotHeight float64
TotalTransactions float64
EpochNumber float64
EpochFirstSlot float64
EpochLastSlot float64
}
func getSlotMetricValues() slotMetricValues {
return slotMetricValues{
SlotHeight: testutil.ToFloat64(confirmedSlotHeight),
TotalTransactions: testutil.ToFloat64(totalTransactionsTotal),
EpochNumber: testutil.ToFloat64(currentEpochNumber),
EpochFirstSlot: testutil.ToFloat64(epochFirstSlot),
EpochLastSlot: testutil.ToFloat64(epochLastSlot),
}
}
func testBlockProductionMetric(
t *testing.T,
metric *prometheus.CounterVec,
host string,
status string,
) {
hostInfo := staticBlockProduction.Hosts[host]
// get expected value depending on status:
var expectedValue float64
switch status {
case "valid":
expectedValue = float64(hostInfo.BlocksProduced)
case "skipped":
expectedValue = float64(hostInfo.LeaderSlots - hostInfo.BlocksProduced)
}
// get labels (leaderSlotsByEpoch requires an extra one)
labels := []string{status, host}
if metric == leaderSlotsByEpoch {
labels = append(labels, fmt.Sprintf("%d", staticEpochInfo.Epoch))
}
// now we can do the assertion:
assert.Equalf(
t,
expectedValue,
testutil.ToFloat64(metric.WithLabelValues(labels...)),
"wrong value for block-production metric with labels: %s",
labels,
)
}
func assertSlotMetricsChangeCorrectly(t *testing.T, initial slotMetricValues, final slotMetricValues) {
// make sure that things have increased
assert.Greaterf(
t,
final.SlotHeight,
initial.SlotHeight,
"Slot has not increased! (%v -> %v)",
initial.SlotHeight,
final.SlotHeight,
)
assert.Greaterf(
t,
final.TotalTransactions,
initial.TotalTransactions,
"Total transactions have not increased! (%v -> %v)",
initial.TotalTransactions,
final.TotalTransactions,
)
assert.GreaterOrEqualf(
t,
final.EpochNumber,
initial.EpochNumber,
"Epoch number has decreased! (%v -> %v)",
initial.EpochNumber,
final.EpochNumber,
)
}
func TestSolanaCollector_WatchSlots_Static(t *testing.T) {
// reset metrics before running tests:
leaderSlotsTotal.Reset()
leaderSlotsByEpoch.Reset()
collector := createSolanaCollector(
&staticRPCClient{},
100*time.Millisecond,
)
prometheus.NewPedanticRegistry().MustRegister(collector)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go collector.WatchSlots(ctx)
time.Sleep(1 * time.Second)
firstSlot := staticEpochInfo.AbsoluteSlot - staticEpochInfo.SlotIndex
lastSlot := firstSlot + staticEpochInfo.SlotsInEpoch
tests := []struct {
expectedValue float64
metric prometheus.Gauge
}{
{expectedValue: float64(staticEpochInfo.AbsoluteSlot), metric: confirmedSlotHeight},
{expectedValue: float64(staticEpochInfo.TransactionCount), metric: totalTransactionsTotal},
{expectedValue: float64(staticEpochInfo.Epoch), metric: currentEpochNumber},
{expectedValue: float64(firstSlot), metric: epochFirstSlot},
{expectedValue: float64(lastSlot), metric: epochLastSlot},
}
for _, testCase := range tests {
name := extractName(testCase.metric.Desc())
t.Run(name, func(t *testing.T) {
assert.Equal(t, testCase.expectedValue, testutil.ToFloat64(testCase.metric))
})
}
metrics := map[string]*prometheus.CounterVec{
"solana_leader_slots_total": leaderSlotsTotal,
"solana_leader_slots_by_epoch": leaderSlotsByEpoch,
}
statuses := []string{"valid", "skipped"}
for name, metric := range metrics {
// subtest for each metric:
t.Run(name, func(t *testing.T) {
for _, status := range statuses {
// sub subtest for each status (as each one requires a different calc)
t.Run(status, func(t *testing.T) {
for _, identity := range identities {
testBlockProductionMetric(t, metric, identity, status)
}
})
}
})
}
}
func TestSolanaCollector_WatchSlots_Dynamic(t *testing.T) {
// reset metrics before running tests:
leaderSlotsTotal.Reset()
leaderSlotsByEpoch.Reset()
// create clients:
client := newDynamicRPCClient()
collector := createSolanaCollector(client, 300*time.Millisecond)
prometheus.NewPedanticRegistry().MustRegister(collector)
// start client/collector and wait a bit:
runCtx, runCancel := context.WithCancel(context.Background())
defer runCancel()
go client.Run(runCtx)
time.Sleep(time.Second)
slotsCtx, slotsCancel := context.WithCancel(context.Background())
defer slotsCancel()
go collector.WatchSlots(slotsCtx)
time.Sleep(time.Second)
initial := getSlotMetricValues()
// wait a bit:
var epochChanged bool
for i := 0; i < 5; i++ {
// wait a bit then get new metrics
time.Sleep(time.Second)
final := getSlotMetricValues()
// make sure things are changing correctly:
assertSlotMetricsChangeCorrectly(t, initial, final)
// sense check to make sure the exporter is not "ahead" of the client (due to double counting or whatever)
assert.LessOrEqualf(
t,
int(final.SlotHeight),
client.Slot,
"Exporter slot (%v) ahead of client slot (%v)!",
int(final.SlotHeight),
client.Slot,
)
assert.LessOrEqualf(
t,
int(final.TotalTransactions),
client.TransactionCount,
"Exporter transaction count (%v) ahead of client transaction count (%v)!",
int(final.TotalTransactions),
client.TransactionCount,
)
assert.LessOrEqualf(
t,
int(final.EpochNumber),
client.Epoch,
"Exporter epoch (%v) ahead of client epoch (%v)!",
int(final.EpochNumber),
client.Epoch,
)
// check if epoch changed
if final.EpochNumber > initial.EpochNumber {
epochChanged = true
}
// make current final the new initial (for next iteration)
initial = final
}
// epoch should have changed somewhere
assert.Truef(t, epochChanged, "Epoch has not changed!")
}

View File

@ -1,166 +0,0 @@
package main
import (
"context"
"fmt"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestSolanaCollector_Collect_Static(t *testing.T) {
collector := createSolanaCollector(
&staticRPCClient{},
slotPacerSchedule,
)
prometheus.NewPedanticRegistry().MustRegister(collector)
testCases := []collectionTest{
{
Name: "solana_active_validators",
ExpectedResponse: `
# HELP solana_active_validators Total number of active validators by state
# TYPE solana_active_validators gauge
solana_active_validators{state="current"} 2
solana_active_validators{state="delinquent"} 1
`,
},
{
Name: "solana_validator_activated_stake",
ExpectedResponse: `
# HELP solana_validator_activated_stake Activated stake per validator
# TYPE solana_validator_activated_stake gauge
solana_validator_activated_stake{nodekey="aaa",pubkey="AAA"} 49
solana_validator_activated_stake{nodekey="bbb",pubkey="BBB"} 42
solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 43
`,
},
{
Name: "solana_validator_last_vote",
ExpectedResponse: `
# HELP solana_validator_last_vote Last voted slot per validator
# TYPE solana_validator_last_vote gauge
solana_validator_last_vote{nodekey="aaa",pubkey="AAA"} 92
solana_validator_last_vote{nodekey="bbb",pubkey="BBB"} 147
solana_validator_last_vote{nodekey="ccc",pubkey="CCC"} 148
`,
},
{
Name: "solana_validator_root_slot",
ExpectedResponse: `
# HELP solana_validator_root_slot Root slot per validator
# TYPE solana_validator_root_slot gauge
solana_validator_root_slot{nodekey="aaa",pubkey="AAA"} 3
solana_validator_root_slot{nodekey="bbb",pubkey="BBB"} 18
solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 19
`,
},
{
Name: "solana_validator_delinquent",
ExpectedResponse: `
# HELP solana_validator_delinquent Whether a validator is delinquent
# TYPE solana_validator_delinquent gauge
solana_validator_delinquent{nodekey="aaa",pubkey="AAA"} 1
solana_validator_delinquent{nodekey="bbb",pubkey="BBB"} 0
solana_validator_delinquent{nodekey="ccc",pubkey="CCC"} 0
`,
},
{
Name: "solana_node_version",
ExpectedResponse: `
# HELP solana_node_version Node version of solana
# TYPE solana_node_version gauge
solana_node_version{version="1.16.7"} 1
`,
},
}
runCollectionTests(t, collector, testCases)
}
func TestSolanaCollector_WatchSlots_Static(t *testing.T) {
// reset metrics before running tests:
leaderSlotsTotal.Reset()
leaderSlotsByEpoch.Reset()
collector := createSolanaCollector(
&staticRPCClient{},
100*time.Millisecond,
)
prometheus.NewPedanticRegistry().MustRegister(collector)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go collector.WatchSlots(ctx)
time.Sleep(1 * time.Second)
firstSlot := staticEpochInfo.AbsoluteSlot - staticEpochInfo.SlotIndex
lastSlot := firstSlot + staticEpochInfo.SlotsInEpoch
tests := []struct {
expectedValue float64
metric prometheus.Gauge
}{
{expectedValue: float64(staticEpochInfo.AbsoluteSlot), metric: confirmedSlotHeight},
{expectedValue: float64(staticEpochInfo.TransactionCount), metric: totalTransactionsTotal},
{expectedValue: float64(staticEpochInfo.Epoch), metric: currentEpochNumber},
{expectedValue: float64(firstSlot), metric: epochFirstSlot},
{expectedValue: float64(lastSlot), metric: epochLastSlot},
}
for _, testCase := range tests {
name := extractName(testCase.metric.Desc())
t.Run(name, func(t *testing.T) {
assert.Equal(t, testCase.expectedValue, testutil.ToFloat64(testCase.metric))
})
}
metrics := map[string]*prometheus.CounterVec{
"solana_leader_slots_total": leaderSlotsTotal,
"solana_leader_slots_by_epoch": leaderSlotsByEpoch,
}
statuses := []string{"valid", "skipped"}
for name, metric := range metrics {
// subtest for each metric:
t.Run(name, func(t *testing.T) {
for _, status := range statuses {
// sub subtest for each status (as each one requires a different calc)
t.Run(status, func(t *testing.T) {
for _, identity := range identities {
testBlockProductionMetric(t, metric, identity, status)
}
})
}
})
}
}
func testBlockProductionMetric(
t *testing.T,
metric *prometheus.CounterVec,
host string,
status string,
) {
hostInfo := staticBlockProduction.Hosts[host]
// get expected value depending on status:
var expectedValue float64
switch status {
case "valid":
expectedValue = float64(hostInfo.BlocksProduced)
case "skipped":
expectedValue = float64(hostInfo.LeaderSlots - hostInfo.BlocksProduced)
}
// get labels (leaderSlotsByEpoch requires an extra one)
labels := []string{status, host}
if metric == leaderSlotsByEpoch {
labels = append(labels, fmt.Sprintf("%d", staticEpochInfo.Epoch))
}
// now we can do the assertion:
assert.Equalf(
t,
expectedValue,
testutil.ToFloat64(metric.WithLabelValues(labels...)),
"wrong value for block-production metric with labels: %s",
labels,
)
}

3
go.mod
View File

@ -13,9 +13,12 @@ require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
golang.org/x/sys v0.17.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

119
go.sum
View File

@ -1,119 +1,40 @@
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY=
github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0 h1:YVIb/fVcOTMSqtqZWSKnHpSLBxu8DKgxq8z6RuBZwqI=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ=
k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
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=
k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=

View File

@ -1,94 +0,0 @@
package rpc
import (
"context"
"encoding/json"
"fmt"
"k8s.io/klog/v2"
)
type (
getBlockProductionConfig struct {
Range getBlockProductionRange `json:"range,omitempty"`
}
getBlockProductionRange struct {
FirstSlot int64 `json:"firstSlot"`
LastSlot *int64 `json:"lastSlot,omitempty"`
}
getBlockProductionValue struct {
ByIdentity map[string][]int64 `json:"byIdentity"`
Range getBlockProductionRange `json:"range"`
}
getBlockProductionResult struct {
Value getBlockProductionValue `json:"value"`
}
getBlockProductionResponse struct {
Result getBlockProductionResult `json:"result"`
Error rpcError2 `json:"error"`
}
BlockProductionPerHost struct {
LeaderSlots int64
BlocksProduced int64
}
BlockProduction struct {
FirstSlot int64
LastSlot int64
Hosts map[string]BlockProductionPerHost
}
)
// https://solana.com/docs/rpc/http/getblockproduction
func (c *Client) GetBlockProduction(ctx context.Context, firstSlot *int64, lastSlot *int64) (BlockProduction, error) {
config := make([]interface{}, 0, 1)
if firstSlot != nil {
config = append(config,
getBlockProductionConfig{
Range: getBlockProductionRange{
FirstSlot: *firstSlot,
LastSlot: lastSlot,
},
})
}
ret := BlockProduction{
FirstSlot: 0,
LastSlot: 0,
Hosts: nil,
}
body, err := c.rpcRequest(ctx, formatRPCRequest("getBlockProduction", config))
if err != nil {
return ret, fmt.Errorf("RPC call failed: %w", err)
}
klog.V(2).Infof("getBlockProduction response: %v", string(body))
var resp getBlockProductionResponse
if err = json.Unmarshal(body, &resp); err != nil {
return ret, fmt.Errorf("failed to decode response body: %w", err)
}
if resp.Error.Code != 0 {
return ret, fmt.Errorf("RPC error: %d %v", resp.Error.Code, resp.Error.Message)
}
ret.FirstSlot = resp.Result.Value.Range.FirstSlot
ret.LastSlot = *resp.Result.Value.Range.LastSlot
ret.Hosts = make(map[string]BlockProductionPerHost)
for id, arr := range resp.Result.Value.ByIdentity {
ret.Hosts[id] = BlockProductionPerHost{
LeaderSlots: arr[0],
BlocksProduced: arr[1],
}
}
return ret, nil
}

View File

@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"k8s.io/klog/v2"
"net/http"
@ -15,12 +16,7 @@ type (
rpcAddr string
}
rpcError1 struct {
Message string `json:"message"`
Code int64 `json:"id"`
}
rpcError2 struct { // TODO: combine these error types into a single one
rpcError struct {
Message string `json:"message"`
Code int64 `json:"code"`
}
@ -42,7 +38,7 @@ type Provider interface {
// GetBlockProduction retrieves the block production information for the specified slot range.
// The method takes a context for cancellation, and pointers to the first and last slots of the range.
// It returns a BlockProduction struct containing the block production details, or an error if the operation fails.
GetBlockProduction(ctx context.Context, firstSlot *int64, lastSlot *int64) (BlockProduction, error)
GetBlockProduction(ctx context.Context, firstSlot *int64, lastSlot *int64) (*BlockProduction, error)
// GetEpochInfo retrieves the information regarding the current epoch.
// The method takes a context for cancellation and a commitment level to specify the desired state.
@ -56,13 +52,13 @@ type Provider interface {
// GetVoteAccounts retrieves the vote accounts information.
// The method takes a context for cancellation and a slice of parameters to filter the vote accounts.
// It returns a pointer to a GetVoteAccountsResponse struct containing the vote accounts details,
// It returns a pointer to a VoteAccounts struct containing the vote accounts details,
// or an error if the operation fails.
GetVoteAccounts(ctx context.Context, params []interface{}) (*VoteAccounts, error)
// GetVersion retrieves the version of the Solana node.
// The method takes a context for cancellation.
// It returns a pointer to a string containing the version information, or an error if the operation fails.
// It returns a string containing the version information, or an error if the operation fails.
GetVersion(ctx context.Context) (string, error)
}
@ -82,29 +78,29 @@ const (
)
func NewRPCClient(rpcAddr string) *Client {
c := &Client{
client := &Client{
httpClient: http.Client{},
rpcAddr: rpcAddr,
}
return c
return client
}
func formatRPCRequest(method string, params []interface{}) io.Reader {
r := &rpcRequest{
request := &rpcRequest{
Version: "2.0",
ID: 1,
Method: method,
Params: params,
}
b, err := json.Marshal(r)
buffer, err := json.Marshal(request)
if err != nil {
panic(err)
}
klog.V(2).Infof("jsonrpc request: %s", string(b))
return bytes.NewBuffer(b)
klog.V(2).Infof("jsonrpc request: %s", string(buffer))
return bytes.NewBuffer(buffer)
}
func (c *Client) rpcRequest(ctx context.Context, data io.Reader) ([]byte, error) {
@ -128,3 +124,92 @@ func (c *Client) rpcRequest(ctx context.Context, data io.Reader) ([]byte, error)
return body, nil
}
func (c *Client) getResponse(ctx context.Context, method string, params []interface{}, result HasRPCError) error {
body, err := c.rpcRequest(ctx, formatRPCRequest(method, params))
// check if there was an error making the request:
if err != nil {
return fmt.Errorf("%s RPC call failed: %w", method, err)
}
// log response:
klog.V(2).Infof("%s response: %v", method, string(body))
// unmarshal the response into the predicted format
if err = json.Unmarshal(body, result); err != nil {
return fmt.Errorf("failed to decode %s response body: %w", method, err)
}
if result.getError().Code != 0 {
return fmt.Errorf("RPC error: %d %v", result.getError().Code, result.getError().Message)
}
return nil
}
func (c *Client) GetEpochInfo(ctx context.Context, commitment Commitment) (*EpochInfo, error) {
var resp response[EpochInfo]
if err := c.getResponse(ctx, "getEpochInfo", []interface{}{commitment}, &resp); err != nil {
return nil, err
}
return &resp.Result, nil
}
func (c *Client) GetVoteAccounts(ctx context.Context, params []interface{}) (*VoteAccounts, error) {
var resp response[VoteAccounts]
if err := c.getResponse(ctx, "getVoteAccounts", params, &resp); err != nil {
return nil, err
}
return &resp.Result, nil
}
func (c *Client) GetVersion(ctx context.Context) (string, error) {
var resp response[struct {
Version string `json:"solana-core"`
}]
if err := c.getResponse(ctx, "getVersion", []interface{}{}, &resp); err != nil {
return "", err
}
return resp.Result.Version, nil
}
func (c *Client) GetSlot(ctx context.Context) (int64, error) {
var resp response[int64]
if err := c.getResponse(ctx, "getSlot", []interface{}{}, &resp); err != nil {
return 0, err
}
return resp.Result, nil
}
func (c *Client) GetBlockProduction(ctx context.Context, firstSlot *int64, lastSlot *int64) (*BlockProduction, error) {
// format params:
params := make([]interface{}, 1)
if firstSlot != nil {
params[0] = map[string]interface{}{
"range": blockProductionRange{
FirstSlot: *firstSlot,
LastSlot: lastSlot,
},
}
}
// make request:
var resp response[blockProductionResult]
if err := c.getResponse(ctx, "getBlockProduction", params, &resp); err != nil {
return nil, err
}
// convert to BlockProduction format:
hosts := make(map[string]BlockProductionPerHost)
for id, arr := range resp.Result.Value.ByIdentity {
hosts[id] = BlockProductionPerHost{
LeaderSlots: arr[0],
BlocksProduced: arr[1],
}
}
production := BlockProduction{
FirstSlot: resp.Result.Value.Range.FirstSlot,
LastSlot: *resp.Result.Value.Range.LastSlot,
Hosts: hosts,
}
return &production, nil
}

View File

@ -1,51 +0,0 @@
package rpc
import (
"context"
"encoding/json"
"fmt"
"k8s.io/klog/v2"
)
type (
EpochInfo struct {
// Current absolute slot in epoch
AbsoluteSlot int64 `json:"absoluteSlot"`
// Current block height
BlockHeight int64 `json:"blockHeight"`
// Current epoch number
Epoch int64 `json:"epoch"`
// Current slot relative to the start of the current epoch
SlotIndex int64 `json:"slotIndex"`
// Number of slots in this epoch
SlotsInEpoch int64 `json:"slotsInEpoch"`
// Total number of transactions ever (?)
TransactionCount int64 `json:"transactionCount"`
}
GetEpochInfoResponse struct {
Result EpochInfo `json:"result"`
Error rpcError1 `json:"error"`
}
)
// https://docs.solana.com/developing/clients/jsonrpc-api#getepochinfo
func (c *Client) GetEpochInfo(ctx context.Context, commitment Commitment) (*EpochInfo, error) {
body, err := c.rpcRequest(ctx, formatRPCRequest("getEpochInfo", []interface{}{commitment}))
if err != nil {
return nil, fmt.Errorf("RPC call failed: %w", err)
}
klog.V(2).Infof("epoch info response: %v", string(body))
var resp GetEpochInfoResponse
if err = json.Unmarshal(body, &resp); err != nil {
return nil, fmt.Errorf("failed to decode response body: %w", err)
}
if resp.Error.Code != 0 {
return nil, fmt.Errorf("RPC error: %d %v", resp.Error.Code, resp.Error.Message)
}
return &resp.Result, nil
}

70
pkg/rpc/responses.go Normal file
View File

@ -0,0 +1,70 @@
package rpc
type (
response[T any] struct {
Result T `json:"result"`
Error rpcError `json:"error"`
}
EpochInfo struct {
// Current absolute slot in epoch
AbsoluteSlot int64 `json:"absoluteSlot"`
// Current block height
BlockHeight int64 `json:"blockHeight"`
// Current epoch number
Epoch int64 `json:"epoch"`
// Current slot relative to the start of the current epoch
SlotIndex int64 `json:"slotIndex"`
// Number of slots in this epoch
SlotsInEpoch int64 `json:"slotsInEpoch"`
// Total number of transactions
TransactionCount int64 `json:"transactionCount"`
}
VoteAccount struct {
ActivatedStake int64 `json:"activatedStake"`
Commission int `json:"commission"`
EpochCredits [][]int `json:"epochCredits"`
EpochVoteAccount bool `json:"epochVoteAccount"`
LastVote int `json:"lastVote"`
NodePubkey string `json:"nodePubkey"`
RootSlot int `json:"rootSlot"`
VotePubkey string `json:"votePubkey"`
}
VoteAccounts struct {
Current []VoteAccount `json:"current"`
Delinquent []VoteAccount `json:"delinquent"`
}
blockProductionRange struct {
FirstSlot int64 `json:"firstSlot"`
LastSlot *int64 `json:"lastSlot,omitempty"`
}
blockProductionResult struct {
Value struct {
ByIdentity map[string][]int64 `json:"byIdentity"`
Range blockProductionRange `json:"range"`
} `json:"value"`
}
BlockProductionPerHost struct {
LeaderSlots int64
BlocksProduced int64
}
BlockProduction struct {
FirstSlot int64
LastSlot int64
Hosts map[string]BlockProductionPerHost
}
)
func (r response[T]) getError() rpcError {
return r.Error
}
type HasRPCError interface {
getError() rpcError
}

View File

@ -1,30 +0,0 @@
package rpc
import (
"context"
"encoding/json"
"fmt"
"k8s.io/klog/v2"
)
type getSlotResponse struct {
Result int64 `json:"result"`
}
// https://solana.com/docs/rpc/http/getslot
func (c *Client) GetSlot(ctx context.Context) (int64, error) {
body, err := c.rpcRequest(ctx, formatRPCRequest("getSlot", []interface{}{}))
if err != nil {
return 0, fmt.Errorf("RPC call failed: %w", err)
}
klog.V(2).Infof("getSlot response: %v", string(body))
var resp getSlotResponse
if err = json.Unmarshal(body, &resp); err != nil {
return 0, fmt.Errorf("failed to decode response body: %w", err)
}
return resp.Result, nil
}

View File

@ -1,52 +0,0 @@
package rpc
import (
"context"
"encoding/json"
"fmt"
"k8s.io/klog/v2"
)
type (
VoteAccount struct {
ActivatedStake int64 `json:"activatedStake"`
Commission int `json:"commission"`
EpochCredits [][]int `json:"epochCredits"`
EpochVoteAccount bool `json:"epochVoteAccount"`
LastVote int `json:"lastVote"`
NodePubkey string `json:"nodePubkey"`
RootSlot int `json:"rootSlot"`
VotePubkey string `json:"votePubkey"`
}
VoteAccounts struct {
Current []VoteAccount `json:"current"`
Delinquent []VoteAccount `json:"delinquent"`
}
GetVoteAccountsResponse struct {
Result VoteAccounts `json:"result"`
Error rpcError1 `json:"error"`
}
)
// https://docs.solana.com/developing/clients/jsonrpc-api#getvoteaccounts
func (c *Client) GetVoteAccounts(ctx context.Context, params []interface{}) (*VoteAccounts, error) {
body, err := c.rpcRequest(ctx, formatRPCRequest("getVoteAccounts", params))
if err != nil {
return nil, fmt.Errorf("RPC call failed: %w", err)
}
klog.V(3).Infof("getVoteAccounts response: %v", string(body))
var resp GetVoteAccountsResponse
if err = json.Unmarshal(body, &resp); err != nil {
return nil, fmt.Errorf("failed to decode response body: %w", err)
}
if resp.Error.Code != 0 {
return nil, fmt.Errorf("RPC error: %d %v", resp.Error.Code, resp.Error.Message)
}
return &resp.Result, nil
}

View File

@ -1,43 +0,0 @@
package rpc
import (
"context"
"encoding/json"
"fmt"
"k8s.io/klog/v2"
)
type (
GetVersionResponse struct {
Result struct {
Version string `json:"solana-core"`
} `json:"result"`
Error rpcError1 `json:"error"`
}
)
func (c *Client) GetVersion(ctx context.Context) (string, error) {
body, err := c.rpcRequest(ctx, formatRPCRequest("getVersion", []interface{}{}))
if body == nil {
return "", fmt.Errorf("RPC call failed: Body empty")
}
if err != nil {
return "", fmt.Errorf("RPC call failed: %w", err)
}
klog.V(2).Infof("version response: %v", string(body))
var resp GetVersionResponse
if err = json.Unmarshal(body, &resp); err != nil {
return "", fmt.Errorf("failed to decode response body: %w", err)
}
if resp.Error.Code != 0 {
return "", fmt.Errorf("RPC error: %d %v", resp.Error.Code, resp.Error.Message)
}
return resp.Result.Version, nil
}