Skip to content

Commit

Permalink
merge main
Browse files Browse the repository at this point in the history
  • Loading branch information
samcm committed Jul 5, 2022
2 parents 4f4ecdc + f5c2491 commit b1c5dd1
Show file tree
Hide file tree
Showing 5 changed files with 307 additions and 8 deletions.
1 change: 0 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ linters:
- tparallel
- typecheck
- unconvert
- unparam
- varcheck
- whitespace
- wsl
Expand Down
10 changes: 10 additions & 0 deletions pkg/exporter/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ type Config struct {
Consensus ConsensusNode `yaml:"consensus"`
// DiskUsage determines if the disk usage metrics should be exported.
DiskUsage DiskUsage `yaml:"diskUsage"`
// Pair determines if the pair metrics should be exported.
Pair PairConfig `yaml:"pair"`
}

// ConsensusNode represents a single ethereum consensus client.
Expand All @@ -31,6 +33,11 @@ type DiskUsage struct {
Directories []string `yaml:"directories"`
}

// PairConfig holds the config for a Pair of Execution and Consensus Clients
type PairConfig struct {
Enabled bool `yaml:"enabled"`
}

// DefaultConfig represents a sane-default configuration.
func DefaultConfig() *Config {
return &Config{
Expand All @@ -49,5 +56,8 @@ func DefaultConfig() *Config {
Enabled: false,
Directories: []string{},
},
Pair: PairConfig{
Enabled: true,
},
}
}
36 changes: 29 additions & 7 deletions pkg/exporter/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/samcm/ethereum-metrics-exporter/pkg/exporter/consensus"
"github.com/samcm/ethereum-metrics-exporter/pkg/exporter/disk"
"github.com/samcm/ethereum-metrics-exporter/pkg/exporter/execution"
"github.com/samcm/ethereum-metrics-exporter/pkg/exporter/pair"
"github.com/sirupsen/logrus"
)

Expand All @@ -36,12 +37,13 @@ func NewExporter(log logrus.FieldLogger, conf *Config) Exporter {
}

type exporter struct {
log logrus.FieldLogger
config *Config
consensus consensus.Node
execution execution.Node
diskUsage disk.UsageMetrics
broker *server.Server
log logrus.FieldLogger
config *Config
consensus consensus.Node
execution execution.Node
diskUsage disk.UsageMetrics
broker *server.Server
pairMetrics pair.Metrics
}

func (e *exporter) Init(ctx context.Context) error {
Expand Down Expand Up @@ -116,6 +118,17 @@ func (e *exporter) Init(ctx context.Context) error {
e.diskUsage = diskUsage
}

if e.config.Pair.Enabled && e.config.Execution.Enabled && e.config.Consensus.Enabled {
e.log.Info("Initializing pair...")

pairMetrics, err := pair.NewMetrics(ctx, e.log.WithField("exporter", "pair"), fmt.Sprintf("%s_pair", namespace), e.config.Consensus.URL, e.config.Execution.URL)
if err != nil {
return err
}

e.pairMetrics = pairMetrics
}

return nil
}

Expand All @@ -142,7 +155,16 @@ func (e *exporter) Serve(ctx context.Context, port int) error {
go e.consensus.StartMetrics(ctx)
}

e.log.Info(fmt.Sprintf("Starting metrics server on :%v", port))
if e.config.Pair.Enabled && e.config.Execution.Enabled && e.config.Consensus.Enabled {
e.log.Info("Starting pair metrics...")

go e.pairMetrics.StartAsync(ctx)
}

e.log.
WithField("consensus_url", e.consensus.URL()).
WithField("execution_url", e.execution.URL()).
Info(fmt.Sprintf("Starting metrics server on :%v", port))

http.Handle("/metrics", promhttp.Handler())

Expand Down
33 changes: 33 additions & 0 deletions pkg/exporter/pair/networks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package pair

type ConsensusMechanism struct {
Name string
Short string
Priority float64
}

var (
ProofOfWork = ConsensusMechanism{
Name: "Proof of Work",
Short: "PoW",
Priority: 1,
}
ProofOfAuthority = ConsensusMechanism{
Name: "Proof of Authority",
Short: "PoA",
Priority: 2,
}
ProofOfStake = ConsensusMechanism{
Name: "Proof of Stake",
Short: "PoS",
Priority: 3,
}
)

var DefaultConsensusMechanism = ProofOfWork

var (
DefaultNetworkConsensusMechanism = map[uint64]ConsensusMechanism{
5: ProofOfAuthority,
}
)
235 changes: 235 additions & 0 deletions pkg/exporter/pair/pair.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
package pair

import (
"context"
"errors"
"fmt"
"math/big"
"time"

eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/http"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/onrik/ethrpc"
"github.com/prometheus/client_golang/prometheus"
"github.com/rs/zerolog"
"github.com/sirupsen/logrus"
"github.com/spf13/cast"
)

// Metrics reports pair metrics
type Metrics interface {
// StartAsync starts the disk usage metrics collection.
StartAsync(ctx context.Context)
}

type pair struct {
log logrus.FieldLogger

consensusMechanism *prometheus.GaugeVec

executionClient *ethclient.Client
consensusClient eth2client.Service
ethrpcClient *ethrpc.EthRPC
bootstrapped bool
consensusURL string
executionURL string

totalDifficulty *big.Int
terminalTotalDifficulty *big.Int
networkID *big.Int

networkIDFetchedAt time.Time
ttdFetchedAt time.Time
tdFetchedAt time.Time
}

// NewMetrics returns a new Metrics instance.
func NewMetrics(ctx context.Context, log logrus.FieldLogger, namespace, consensusURL, executionURL string) (Metrics, error) {
p := &pair{
log: log,

executionURL: executionURL,
consensusURL: consensusURL,

consensusClient: nil,
executionClient: nil,
ethrpcClient: nil,

bootstrapped: false,

consensusMechanism: prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: namespace,
Name: "consensus_mechanism",
Help: "Consensus mechanism used",
},
[]string{
"consensus_mechanism",
"consensus_mechanism_short",
},
),

totalDifficulty: big.NewInt(0),
terminalTotalDifficulty: big.NewInt(0),
}

prometheus.MustRegister(p.consensusMechanism)

return p, nil
}

func (p *pair) Bootstrap(ctx context.Context) error {
consenusClient, err := http.New(ctx,
http.WithAddress(p.consensusURL),
http.WithLogLevel(zerolog.Disabled),
)
if err != nil {
return err
}

p.consensusClient = consenusClient

executionClient, err := ethclient.Dial(p.executionURL)
if err != nil {
return err
}

p.executionClient = executionClient

p.ethrpcClient = ethrpc.New(p.executionURL)

p.bootstrapped = true

return nil
}

func (p *pair) StartAsync(ctx context.Context) {
go func() {
for {
select {
case <-ctx.Done():
return
case <-time.After(time.Second * 5):
if !p.bootstrapped {
if err := p.Bootstrap(ctx); err != nil {
continue
}
}

if time.Since(p.ttdFetchedAt) > 15*time.Minute {
if err := p.fetchTerminalTotalDifficulty(ctx); err != nil {
p.log.WithError(err).Error("Failed to fetch terminal total difficulty")
}
}

if time.Since(p.tdFetchedAt) > 12*time.Second {
if err := p.fetchTotalDifficulty(ctx); err != nil {
p.log.WithError(err).Error("Failed to fetch total difficulty")
}
}

if time.Since(p.networkIDFetchedAt) > 15*time.Minute {
if err := p.fetchNetworkID(ctx); err != nil {
p.log.WithError(err).Error("Failed to fetch network ID")
}
}

if err := p.deriveConsensusMechanism(ctx); err != nil {
p.log.WithError(err).Error("Failed to derive consensus mechanism")
}
}
}
}()
}

func (p *pair) fetchTotalDifficulty(ctx context.Context) error {
mostRecentBlockNumber, err := p.executionClient.BlockNumber(ctx)
if err != nil {
return err
}

block, err := p.ethrpcClient.EthGetBlockByNumber(int(mostRecentBlockNumber), false)
if err != nil {
return err
}

p.totalDifficulty = &block.TotalDifficulty

p.tdFetchedAt = time.Now()

return nil
}

func (p *pair) fetchNetworkID(ctx context.Context) error {
networkID, err := p.executionClient.NetworkID(ctx)
if err != nil {
return err
}

p.networkID = networkID

p.networkIDFetchedAt = time.Now()

return nil
}

func (p *pair) fetchTerminalTotalDifficulty(ctx context.Context) error {
provider, isProvider := p.consensusClient.(eth2client.SpecProvider)
if !isProvider {
return errors.New("client does not implement eth2client.SpecProvider")
}

spec, err := provider.Spec(ctx)
if err != nil {
return err
}

terminalTotalDifficulty, exists := spec["TERMINAL_TOTAL_DIFFICULTY"]
if !exists {
return errors.New("TERMINAL_TOTAL_DIFFICULTY not found in spec")
}

ttd := cast.ToString(fmt.Sprintf("%v", terminalTotalDifficulty))

asBigInt, success := big.NewInt(0).SetString(ttd, 10)
if !success {
return errors.New("TERMINAL_TOTAL_DIFFICULTY not a valid integer")
}

p.terminalTotalDifficulty = asBigInt

p.ttdFetchedAt = time.Now()

return nil
}

func (p *pair) deriveConsensusMechanism(ctx context.Context) error {
if p.totalDifficulty == big.NewInt(0) {
return errors.New("total difficulty not fetched")
}

if p.terminalTotalDifficulty == big.NewInt(0) {
return errors.New("terminal total difficulty not fetched")
}

if p.networkID == big.NewInt(0) {
return errors.New("network ID not fetched")
}

consensusMechanism := DefaultConsensusMechanism

// Support networks like Goerli that use Proof of Authority as the default consensus mechanism.
if value, exists := DefaultNetworkConsensusMechanism[p.networkID.Uint64()]; exists {
consensusMechanism = value
}

if p.totalDifficulty.Cmp(p.terminalTotalDifficulty) >= 0 {
consensusMechanism = ProofOfStake
}

p.consensusMechanism.Reset()
p.consensusMechanism.WithLabelValues(consensusMechanism.Name, consensusMechanism.Short).Set(consensusMechanism.Priority)

return nil
}

0 comments on commit b1c5dd1

Please sign in to comment.