Skip to content

Commit

Permalink
SOM: network fees (#728)
Browse files Browse the repository at this point in the history
  • Loading branch information
aalu1418 authored Jun 5, 2024
1 parent 555ff58 commit b4a91ae
Show file tree
Hide file tree
Showing 12 changed files with 480 additions and 1 deletion.
12 changes: 11 additions & 1 deletion cmd/monitoring/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,16 @@ func main() {
)
slotHeightSourceFactory := monitoring.NewSlotHeightSourceFactory(
chainReader,
logger.With(log, "component", "souce-slot-height"),
logger.With(log, "component", "source-slot-height"),
)
networkFeesSourceFactory := monitoring.NewNetworkFeesSourceFactory(
chainReader,
logger.With(log, "component", "source-network-fees"),
)
monitor.NetworkSourceFactories = append(monitor.NetworkSourceFactories,
nodeBalancesSourceFactory,
slotHeightSourceFactory,
networkFeesSourceFactory,
)

// exporter names
Expand Down Expand Up @@ -121,9 +126,14 @@ func main() {
logger.With(log, "component", promExporter),
metrics.NewSlotHeight(logger.With(log, "component", promMetrics)),
)
networkFeesExporterFactory := exporter.NewNetworkFeesFactory(
logger.With(log, "component", promExporter),
metrics.NewNetworkFees(logger.With(log, "component", promMetrics)),
)
monitor.NetworkExporterFactories = append(monitor.NetworkExporterFactories,
nodeBalancesExporterFactory,
slotHeightExporterFactory,
networkFeesExporterFactory,
)

monitor.Run()
Expand Down
16 changes: 16 additions & 0 deletions pkg/monitoring/chain_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type ChainReader interface {
GetSignaturesForAddressWithOpts(ctx context.Context, account solana.PublicKey, opts *rpc.GetSignaturesForAddressOpts) (out []*rpc.TransactionSignature, err error)
GetTransaction(ctx context.Context, txSig solana.Signature, opts *rpc.GetTransactionOpts) (out *rpc.GetTransactionResult, err error)
GetSlot(ctx context.Context) (slot uint64, err error)
GetLatestBlock(ctx context.Context, commitment rpc.CommitmentType) (*rpc.GetBlockResult, error)
}

func NewChainReader(client *rpc.Client) ChainReader {
Expand Down Expand Up @@ -56,3 +57,18 @@ func (c *chainReader) GetTransaction(ctx context.Context, txSig solana.Signature
func (c *chainReader) GetSlot(ctx context.Context) (uint64, error) {
return c.client.GetSlot(ctx, rpc.CommitmentProcessed) // get latest height
}

func (c *chainReader) GetLatestBlock(ctx context.Context, commitment rpc.CommitmentType) (*rpc.GetBlockResult, error) {
// get slot based on confirmation
slot, err := c.client.GetSlot(ctx, commitment)
if err != nil {
return nil, err
}

// get block based on slot
version := uint64(0) // pull all tx types (legacy + v0)
return c.client.GetBlockWithOpts(ctx, slot, &rpc.GetBlockOpts{
Commitment: commitment,
MaxSupportedTransactionVersion: &version,
})
}
104 changes: 104 additions & 0 deletions pkg/monitoring/exporter/networkfees.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package exporter

import (
"context"
"errors"
"slices"

commonMonitoring "github.com/smartcontractkit/chainlink-common/pkg/monitoring"
"github.com/smartcontractkit/chainlink-common/pkg/utils/mathutil"
"golang.org/x/exp/constraints"

"github.com/smartcontractkit/chainlink-solana/pkg/monitoring/metrics"
"github.com/smartcontractkit/chainlink-solana/pkg/solana/fees"
)

func NewNetworkFeesFactory(
lgr commonMonitoring.Logger,
metrics metrics.NetworkFees,
) commonMonitoring.ExporterFactory {
return &networkFeesFactory{
metrics,
lgr,
}
}

type networkFeesFactory struct {
metrics metrics.NetworkFees
lgr commonMonitoring.Logger
}

func (p *networkFeesFactory) NewExporter(
params commonMonitoring.ExporterParams,
) (commonMonitoring.Exporter, error) {
return &networkFees{
params.ChainConfig.GetNetworkName(),
p.metrics,
p.lgr,
}, nil
}

type networkFees struct {
chain string
metrics metrics.NetworkFees
lgr commonMonitoring.Logger
}

func (p *networkFees) Export(ctx context.Context, data interface{}) {
blockData, ok := data.(fees.BlockData)
if !ok {
return // skip if input could not be parsed
}

input := metrics.NetworkFeesInput{}
if err := aggregateFees(input, "computeUnitPrice", blockData.Prices); err != nil {
p.lgr.Errorw("failed to calculate computeUnitPrice", "error", err)
return
}
if err := aggregateFees(input, "totalFee", blockData.Fees); err != nil {
p.lgr.Errorw("failed to calculate totalFee", "error", err)
return
}

p.metrics.Set(input, p.chain)
}

func (p *networkFees) Cleanup(_ context.Context) {
p.metrics.Cleanup()
}

func aggregateFees[V constraints.Integer](input metrics.NetworkFeesInput, name string, data []V) error {
// skip if empty list
if len(data) == 0 {
return nil
}

slices.Sort(data) // ensure sorted

// calculate median / avg
medianPrice, medianPriceErr := mathutil.Median(data...)
input.Set(name, "median", uint64(medianPrice))
avgPrice, avgPriceErr := mathutil.Avg(data...)
input.Set(name, "avg", uint64(avgPrice))

// calculate lower / upper quartile
var lowerData, upperData []V
l := len(data)
if l%2 == 0 {
lowerData = data[:l/2]
upperData = data[l/2:]
} else {
lowerData = data[:l/2]
upperData = data[l/2+1:]
}
lowerQuartilePrice, lowerQuartilePriceErr := mathutil.Median(lowerData...)
input.Set(name, "lowerQuartile", uint64(lowerQuartilePrice))
upperQuartilePrice, upperQuartilePriceErr := mathutil.Median(upperData...)
input.Set(name, "upperQuartile", uint64(upperQuartilePrice))

// calculate min/max
input.Set(name, "max", uint64(slices.Max(data)))
input.Set(name, "min", uint64(slices.Min(data)))

return errors.Join(medianPriceErr, avgPriceErr, lowerQuartilePriceErr, upperQuartilePriceErr)
}
62 changes: 62 additions & 0 deletions pkg/monitoring/exporter/networkfees_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package exporter

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink-common/pkg/logger"
commonMonitoring "github.com/smartcontractkit/chainlink-common/pkg/monitoring"
"github.com/smartcontractkit/chainlink-common/pkg/utils/tests"

"github.com/smartcontractkit/chainlink-solana/pkg/monitoring/metrics"
"github.com/smartcontractkit/chainlink-solana/pkg/monitoring/metrics/mocks"
"github.com/smartcontractkit/chainlink-solana/pkg/monitoring/testutils"
"github.com/smartcontractkit/chainlink-solana/pkg/solana/fees"
)

func TestNetworkFees(t *testing.T) {
ctx := tests.Context(t)
m := mocks.NewNetworkFees(t)
m.On("Set", mock.Anything, mock.Anything).Once()
m.On("Cleanup").Once()

factory := NewNetworkFeesFactory(logger.Test(t), m)

chainConfig := testutils.GenerateChainConfig()
exporter, err := factory.NewExporter(commonMonitoring.ExporterParams{ChainConfig: chainConfig})
require.NoError(t, err)

// happy path
exporter.Export(ctx, fees.BlockData{})
exporter.Cleanup(ctx)

// test passing uint64 instead of NetworkFees - should not call mock
// NetworkFees alias of uint64
exporter.Export(ctx, uint64(10))
}

func TestAggregateFees(t *testing.T) {
input := metrics.NetworkFeesInput{}
v0 := []int{10, 12, 3, 4, 1, 2}
v1 := []int{5, 1, 10, 2, 3, 12, 4}

require.NoError(t, aggregateFees(input, "0", v0))
require.NoError(t, aggregateFees(input, "1", v1))

assert.Equal(t, uint64(3), input["0"]["median"])
assert.Equal(t, uint64(5), input["0"]["avg"])
assert.Equal(t, uint64(1), input["0"]["min"])
assert.Equal(t, uint64(12), input["0"]["max"])
assert.Equal(t, uint64(2), input["0"]["lowerQuartile"])
assert.Equal(t, uint64(10), input["0"]["upperQuartile"])

assert.Equal(t, uint64(4), input["1"]["median"])
assert.Equal(t, uint64(5), input["1"]["avg"])
assert.Equal(t, uint64(1), input["1"]["min"])
assert.Equal(t, uint64(12), input["1"]["max"])
assert.Equal(t, uint64(2), input["1"]["lowerQuartile"])
assert.Equal(t, uint64(10), input["1"]["upperQuartile"])
}
12 changes: 12 additions & 0 deletions pkg/monitoring/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ func init() {
},
[]string{"chain", "url"},
)

// init gauge for network fees
gauges[types.NetworkFeesMetric] = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: types.NetworkFeesMetric,
},
[]string{
"type", // compute budget price, total fee
"operation", // avg, median, upper/lower quartile, min, max
"chain",
},
)
}

type FeedInput struct {
Expand Down
38 changes: 38 additions & 0 deletions pkg/monitoring/metrics/mocks/NetworkFees.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

70 changes: 70 additions & 0 deletions pkg/monitoring/metrics/networkfees.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package metrics

import (
"github.com/prometheus/client_golang/prometheus"
commonMonitoring "github.com/smartcontractkit/chainlink-common/pkg/monitoring"

"github.com/smartcontractkit/chainlink-solana/pkg/monitoring/types"
)

//go:generate mockery --name NetworkFees --output ./mocks/

type NetworkFees interface {
Set(slot NetworkFeesInput, chain string)
Cleanup()
}

var _ NetworkFees = (*networkFees)(nil)

type networkFees struct {
simpleGauge
labels []prometheus.Labels
}

func NewNetworkFees(log commonMonitoring.Logger) *networkFees {
return &networkFees{
simpleGauge: newSimpleGauge(log, types.NetworkFeesMetric),
}
}

func (sh *networkFees) Set(input NetworkFeesInput, chain string) {
for feeType, opMap := range input {
for operation, value := range opMap {
label := prometheus.Labels{
"type": feeType,
"operation": operation,
"chain": chain,
}
sh.set(float64(value), label)
}
}
sh.labels = input.Labels(chain)
}

func (sh *networkFees) Cleanup() {
for _, l := range sh.labels {
sh.delete(l)
}
}

type NetworkFeesInput map[string]map[string]uint64

func (i NetworkFeesInput) Set(feeType, operation string, value uint64) {
if _, exists := i[feeType]; !exists {
i[feeType] = map[string]uint64{}
}
i[feeType][operation] = value
}

func (i NetworkFeesInput) Labels(chain string) (l []prometheus.Labels) {
for feeType, opMap := range i {
for operation := range opMap {
l = append(l, prometheus.Labels{
"type": feeType,
"operation": operation,
"chain": chain,
})
}
}
return
}
Loading

0 comments on commit b4a91ae

Please sign in to comment.