diff --git a/go.mod b/go.mod index 70f77de08..a11cb6c43 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/minio/highwayhash v1.0.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect + github.com/prometheus/client_golang v1.0.0 github.com/prysmaticlabs/ethereumapis v0.0.0-20200729044127-8027cc96e2c0 github.com/prysmaticlabs/go-ssz v0.0.0-20210121151755-f6208871c388 github.com/rocket-pool/rocketpool-go v0.0.10 @@ -26,6 +27,7 @@ require ( github.com/wealdtech/go-eth2-types/v2 v2.5.0 github.com/wealdtech/go-eth2-util v1.6.0 github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4 v1.1.1 + go.uber.org/multierr v1.6.0 golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b // indirect diff --git a/go.sum b/go.sum index bd317c56c..f905868bb 100644 --- a/go.sum +++ b/go.sum @@ -54,7 +54,9 @@ github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hC github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aws/aws-sdk-go v1.25.48/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= @@ -346,6 +348,7 @@ github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/ github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= +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/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/minio/highwayhash v1.0.0 h1:iMSDhgUILCr0TNm8LWlSjF8N0ZIj2qbO8WHp6Q/J2BA= @@ -410,16 +413,21 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 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.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce h1:X0jFYGnHemYDIW6jlc+fSI8f9Cg+jqCnClYP2WgZT/A= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/tsdb v0.6.2-0.20190402121629-4f204dcbc150 h1:ZeU+auZj1iNzN8iVhff6M38Mfu73FQiJve/GEXYJBjE= github.com/prometheus/tsdb v0.6.2-0.20190402121629-4f204dcbc150/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= @@ -526,7 +534,11 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= diff --git a/rocketpool-cli/metrics/commands.go b/rocketpool-cli/metrics/commands.go new file mode 100644 index 000000000..3b544613f --- /dev/null +++ b/rocketpool-cli/metrics/commands.go @@ -0,0 +1,32 @@ +package metrics + +import ( + "github.com/urfave/cli" +) + + +// Register commands +func RegisterCommands(app *cli.App, name string, aliases []string) { + app.Commands = append(app.Commands, cli.Command{ + Name: name, + Aliases: aliases, + Usage: "Rocket Pool Metrics", + Subcommands: []cli.Command{ + + cli.Command{ + Name: "print", + Aliases: []string{"p"}, + Usage: "Print the ouptput of metrics", + UsageText: "rocketpool metrics print", + Action: func(c *cli.Context) error { + + // Run + return print(c) + + }, + }, + + }, + }) +} + diff --git a/rocketpool-cli/metrics/print.go b/rocketpool-cli/metrics/print.go new file mode 100644 index 000000000..e19e7aa13 --- /dev/null +++ b/rocketpool-cli/metrics/print.go @@ -0,0 +1,21 @@ +package metrics + +import ( + "github.com/urfave/cli" + + "github.com/rocket-pool/smartnode/shared/services/rocketpool" +) + + +func print(c *cli.Context) error { + + // Get RP client + rp, err := rocketpool.NewClientFromCtx(c) + if err != nil { return err } + defer rp.Close() + + // Print metrics + return rp.PrintMetricsOutput() + +} + diff --git a/rocketpool-cli/minipool/commands.go b/rocketpool-cli/minipool/commands.go index 5688372c4..b6aa2c4e6 100644 --- a/rocketpool-cli/minipool/commands.go +++ b/rocketpool-cli/minipool/commands.go @@ -30,6 +30,22 @@ func RegisterCommands(app *cli.App, name string, aliases []string) { }, }, + cli.Command{ + Name: "leader", + Aliases: []string{"l"}, + Usage: "minipool leaderboard", + UsageText: "rocketpool minipool leader", + Action: func(c *cli.Context) error { + + // Validate args + if err := cliutils.ValidateArgCount(c, 0); err != nil { return err } + + // Run + return getLeader(c) + + }, + }, + cli.Command{ Name: "refund", Aliases: []string{"r"}, diff --git a/rocketpool-cli/minipool/leader.go b/rocketpool-cli/minipool/leader.go new file mode 100644 index 000000000..effb87c1b --- /dev/null +++ b/rocketpool-cli/minipool/leader.go @@ -0,0 +1,77 @@ +package minipool + +import ( + "fmt" + "sort" + + "github.com/rocket-pool/rocketpool-go/utils/eth" + "github.com/urfave/cli" + + "github.com/rocket-pool/smartnode/shared/services/rocketpool" + "github.com/rocket-pool/smartnode/shared/types/api" + "github.com/rocket-pool/smartnode/shared/utils/hex" +) + + +func getLeader(c *cli.Context) error { + + // Get RP client + rp, err := rocketpool.NewClientFromCtx(c) + if err != nil { return err } + defer rp.Close() + + // Get minipool statuses + status, err := rp.MinipoolLeader() + if err != nil { + return err + } + + // Get minipools by status + minipools := []api.MinipoolDetails{} + statusCounts := map[string]int{} + for _, minipool := range status.Minipools { + + // Add to status list + if minipool.Validator.Exists { + minipools = append(minipools, minipool) + } + + // status count + statusName := minipool.Status.Status.String() + if _, ok := statusCounts[statusName]; !ok { + statusCounts[statusName] = 0 + } + statusCounts[statusName] = statusCounts[statusName]+1 + } + + fmt.Printf("Total minipools: %d\n", len(status.Minipools)) + fmt.Println("Status,Count") + + for status, count := range statusCounts { + fmt.Printf("%s,%d", status, count) + fmt.Println("") + } + + // Print & return + if len(status.Minipools) == 0 { + fmt.Println("No active minipools") + return nil + } + fmt.Println("") + + sort.SliceStable(minipools, func(i, j int) bool { return eth.WeiToEth(minipools[i].Validator.Balance) > eth.WeiToEth(minipools[j].Validator.Balance) }) + + fmt.Printf("Minipools with validators: %d\n", len(minipools)) + fmt.Println("Rank,Node address,Validator pubkey,RP status update time,Accumulated reward/penalty (ETH)") + + for i, minipool := range minipools { + nodeAddress := hex.AddPrefix(minipool.Node.Address.Hex()) + validatorAddress := hex.AddPrefix(minipool.ValidatorPubkey.Hex()) + statusTime := minipool.Status.StatusTime.Format("2006-01-02T15:04:05-0700") + diffBalance := eth.WeiToEth(minipool.Validator.Balance) - eth.WeiToEth(minipool.Node.DepositBalance) - eth.WeiToEth(minipool.User.DepositBalance) + fmt.Printf("%4d,%s,%s,%s,%+0.10f", i+1, nodeAddress, validatorAddress, statusTime, diffBalance) + fmt.Println("") + } + return nil + +} diff --git a/rocketpool-cli/node/commands.go b/rocketpool-cli/node/commands.go index 47e775c1a..3e613e480 100644 --- a/rocketpool-cli/node/commands.go +++ b/rocketpool-cli/node/commands.go @@ -31,6 +31,22 @@ func RegisterCommands(app *cli.App, name string, aliases []string) { }, }, + cli.Command{ + Name: "leader", + Aliases: []string{"l"}, + Usage: "node leaderboard", + UsageText: "rocketpool node leader", + Action: func(c *cli.Context) error { + + // Validate args + if err := cliutils.ValidateArgCount(c, 0); err != nil { return err } + + // Run + return getLeader(c) + + }, + }, + cli.Command{ Name: "register", Aliases: []string{"r"}, diff --git a/rocketpool-cli/node/leader.go b/rocketpool-cli/node/leader.go new file mode 100644 index 000000000..1aaa770bf --- /dev/null +++ b/rocketpool-cli/node/leader.go @@ -0,0 +1,49 @@ +package node + +import ( + "fmt" + "math" + + "github.com/rocket-pool/rocketpool-go/utils/eth" + "github.com/urfave/cli" + + "github.com/rocket-pool/smartnode/shared/services/rocketpool" + "github.com/rocket-pool/smartnode/shared/utils/hex" +) + + +func getLeader(c *cli.Context) error { + + // Get RP client + rp, err := rocketpool.NewClientFromCtx(c) + if err != nil { return err } + defer rp.Close() + + // Get node status + response, err := rp.NodeLeader() + if err != nil { return err } + + // Print & return + if len(response.Nodes) == 0 { + fmt.Println("No Rocketpool nodes") + return nil + } + + fmt.Printf("%d Rocketpool nodes\n", len(response.Nodes)) + fmt.Println("") + fmt.Println("Rank,Node address,Score (ETH),Minipool count,Registered,Timezone") + + for _, nodeRank := range response.Nodes { + nodeAddress := hex.AddPrefix(nodeRank.Address.Hex()) + var score float64 + if nodeRank.Score != nil { + score = eth.WeiToEth(nodeRank.Score) + } else { + score = math.NaN() + } + + fmt.Printf("%4d,%s,%+0.10f,%4d,%t,%s", nodeRank.Rank, nodeAddress, score, len(nodeRank.Details), nodeRank.Registered, nodeRank.TimezoneLocation) + fmt.Println("") + } + return nil +} diff --git a/rocketpool-cli/rocketpool-cli.go b/rocketpool-cli/rocketpool-cli.go index d63aed472..868f94aeb 100644 --- a/rocketpool-cli/rocketpool-cli.go +++ b/rocketpool-cli/rocketpool-cli.go @@ -7,6 +7,7 @@ import ( "github.com/urfave/cli" "github.com/rocket-pool/smartnode/rocketpool-cli/auction" + "github.com/rocket-pool/smartnode/rocketpool-cli/metrics" "github.com/rocket-pool/smartnode/rocketpool-cli/minipool" "github.com/rocket-pool/smartnode/rocketpool-cli/network" "github.com/rocket-pool/smartnode/rocketpool-cli/node" @@ -93,6 +94,7 @@ ______ _ _ ______ _ // Register commands auction.RegisterCommands(app, "auction", []string{"a"}) + metrics.RegisterCommands(app, "metrics", []string{"r"}) minipool.RegisterCommands(app, "minipool", []string{"m"}) network.RegisterCommands(app, "network", []string{"e"}) node.RegisterCommands(app, "node", []string{"n"}) diff --git a/rocketpool/api/minipool/commands.go b/rocketpool/api/minipool/commands.go index 37f28f118..1370ad33d 100644 --- a/rocketpool/api/minipool/commands.go +++ b/rocketpool/api/minipool/commands.go @@ -32,6 +32,23 @@ func RegisterSubcommands(command *cli.Command, name string, aliases []string) { }, }, + cli.Command{ + Name: "leader", + Aliases: []string{"l"}, + Usage: "validator leaderboard", + UsageText: "rocketpool api minipool leader", + Action: func(c *cli.Context) error { + + // Validate args + if err := cliutils.ValidateArgCount(c, 0); err != nil { return err } + + // Run + api.PrintResponse(getLeader(c)) + return nil + + }, + }, + cli.Command{ Name: "can-refund", Usage: "Check whether the node can refund ETH from the minipool", diff --git a/rocketpool/api/minipool/leader.go b/rocketpool/api/minipool/leader.go new file mode 100644 index 000000000..54d0b326a --- /dev/null +++ b/rocketpool/api/minipool/leader.go @@ -0,0 +1,180 @@ +package minipool + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "golang.org/x/sync/errgroup" + "github.com/urfave/cli" + + "github.com/rocket-pool/rocketpool-go/minipool" + "github.com/rocket-pool/rocketpool-go/rocketpool" + "github.com/rocket-pool/rocketpool-go/types" + "github.com/rocket-pool/rocketpool-go/utils/eth" + "github.com/rocket-pool/smartnode/shared/services" + "github.com/rocket-pool/smartnode/shared/types/api" + "github.com/rocket-pool/smartnode/shared/services/beacon" + rputils "github.com/rocket-pool/smartnode/shared/utils/rp" +) + + +func getLeader(c *cli.Context) (*api.MinipoolStatusResponse, error) { + + // Get services + if err := services.RequireRocketStorage(c); err != nil { return nil, err } + if err := services.RequireBeaconClientSynced(c); err != nil { return nil, err } + rp, err := services.GetRocketPool(c) + if err != nil { return nil, err } + bc, err := services.GetBeaconClient(c) + if err != nil { return nil, err } + + // Response + response := api.MinipoolStatusResponse{} + + details, err := GetAllMinipoolDetails(rp, bc) + if err != nil { + return nil, err + } + response.Minipools = details + + // Return response + return &response, nil +} + + +func GetAllMinipoolDetails(rp *rocketpool.RocketPool, bc beacon.Client) ([]api.MinipoolDetails, error) { + + // Data + var wg1 errgroup.Group + var addresses []common.Address + var currentEpoch uint64 + + // Get minipool addresses + wg1.Go(func() error { + var err error + addresses, err = minipool.GetMinipoolAddresses(rp, nil) + return err + }) + + // Get current epoch + wg1.Go(func() error { + head, err := bc.GetBeaconHead() + if err == nil { + currentEpoch = head.Epoch + } + return err + }) + + // Wait for data + if err := wg1.Wait(); err != nil { + return []api.MinipoolDetails{}, err + } + + // Load details in batches + details := make([]api.MinipoolDetails, len(addresses)) + for bsi := 0; bsi < len(addresses); bsi += MinipoolDetailsBatchSize { + + // Get batch start & end index + msi := bsi + mei := bsi + MinipoolDetailsBatchSize + if mei > len(addresses) { mei = len(addresses) } + + // Get minipool validator statuses + validators, err := rputils.GetMinipoolValidators(rp, bc, addresses[msi:mei], nil, nil) + if err != nil { + continue + } + + // Load details + var wg errgroup.Group + for mi := msi; mi < mei; mi++ { + mi := mi + wg.Go(func() error { + address := addresses[mi] + validator := validators[address] + mpDetails, err := getMinipoolBalance(rp, address, validator, currentEpoch) + if err == nil { details[mi] = mpDetails } + return err + }) + } + if err := wg.Wait(); err != nil { + return []api.MinipoolDetails{}, err + } + + } + + // Return + return details, nil +} + + +func getMinipoolBalance(rp *rocketpool.RocketPool, minipoolAddress common.Address, validator beacon.ValidatorStatus, currentEpoch uint64) (api.MinipoolDetails, error) { + + // Create minipool + mp, err := minipool.NewMinipool(rp, minipoolAddress) + if err != nil { + return api.MinipoolDetails{}, err + } + + // Data + var wg errgroup.Group + details := api.MinipoolDetails { Address: minipoolAddress } + + // Load data + wg.Go(func() error { + var err error + details.ValidatorPubkey, err = minipool.GetMinipoolPubkey(rp, minipoolAddress, nil) + return err + }) + wg.Go(func() error { + var err error + details.Status, err = mp.GetStatusDetails(nil) + return err + }) + wg.Go(func() error { + var err error + details.Node.Address, err = mp.GetNodeAddress(nil) + return err + }) + wg.Go(func() error { + var err error + details.Node.DepositBalance, err = mp.GetNodeDepositBalance(nil) + return err + }) + wg.Go(func() error { + var err error + details.User.DepositBalance, err = mp.GetUserDepositBalance(nil) + return err + }) + + // Wait for data + if err := wg.Wait(); err != nil { + return api.MinipoolDetails{}, err + } + + // Get validator details if staking + if details.Status.Status == types.Staking { + // Validator details + validatorDetails := api.ValidatorDetails{} + + // Set validator status details + validatorActivated := false + if validator.Exists { + validatorDetails.Exists = true + validatorDetails.Active = (validator.ActivationEpoch < currentEpoch && validator.ExitEpoch > currentEpoch) + validatorActivated = (validator.ActivationEpoch < currentEpoch) + } + + // use deposit balances if validator not activated + if !validatorActivated { + validatorDetails.Balance = new(big.Int) + validatorDetails.Balance.Add(details.Node.DepositBalance, details.User.DepositBalance) + } else { + // Set validator balance + validatorDetails.Balance = eth.GweiToWei(float64(validator.Balance)) + } + details.Validator = validatorDetails + } + + return details, nil +} diff --git a/rocketpool/api/minipool/status.go b/rocketpool/api/minipool/status.go index baf73c13f..e2881093a 100644 --- a/rocketpool/api/minipool/status.go +++ b/rocketpool/api/minipool/status.go @@ -28,7 +28,7 @@ func getStatus(c *cli.Context) (*api.MinipoolStatusResponse, error) { if err != nil { return nil, err } - details, err := getNodeMinipoolDetails(rp, bc, nodeAccount.Address) + details, err := GetNodeMinipoolDetails(rp, bc, nodeAccount.Address) if err != nil { return nil, err } diff --git a/rocketpool/api/minipool/utils.go b/rocketpool/api/minipool/utils.go index 26d5def8b..35325af45 100644 --- a/rocketpool/api/minipool/utils.go +++ b/rocketpool/api/minipool/utils.go @@ -38,7 +38,7 @@ func validateMinipoolOwner(mp *minipool.Minipool, nodeAddress common.Address) er // Get all node minipool details -func getNodeMinipoolDetails(rp *rocketpool.RocketPool, bc beacon.Client, nodeAddress common.Address) ([]api.MinipoolDetails, error) { +func GetNodeMinipoolDetails(rp *rocketpool.RocketPool, bc beacon.Client, nodeAddress common.Address) ([]api.MinipoolDetails, error) { // Data var wg1 errgroup.Group @@ -252,4 +252,3 @@ func getMinipoolValidatorDetails(rp *rocketpool.RocketPool, minipoolDetails api. return details, nil } - diff --git a/rocketpool/api/network/node-fee.go b/rocketpool/api/network/node-fee.go index 5bbdc4e5b..3db2dfb98 100644 --- a/rocketpool/api/network/node-fee.go +++ b/rocketpool/api/network/node-fee.go @@ -2,6 +2,7 @@ package network import ( "github.com/rocket-pool/rocketpool-go/network" + "github.com/rocket-pool/rocketpool-go/rocketpool" "github.com/rocket-pool/rocketpool-go/settings/protocol" "github.com/urfave/cli" "golang.org/x/sync/errgroup" @@ -18,6 +19,12 @@ func getNodeFee(c *cli.Context) (*api.NodeFeeResponse, error) { rp, err := services.GetRocketPool(c) if err != nil { return nil, err } + response, err := GetNodeFee(rp) + return response, err +} + + +func GetNodeFee(rp *rocketpool.RocketPool) (*api.NodeFeeResponse, error) { // Response response := api.NodeFeeResponse{} diff --git a/rocketpool/api/network/rpl-price.go b/rocketpool/api/network/rpl-price.go index 3e2b9d9c3..59d3d5c7a 100644 --- a/rocketpool/api/network/rpl-price.go +++ b/rocketpool/api/network/rpl-price.go @@ -4,6 +4,7 @@ import ( "math/big" "github.com/rocket-pool/rocketpool-go/network" + "github.com/rocket-pool/rocketpool-go/rocketpool" "github.com/rocket-pool/rocketpool-go/settings/protocol" "github.com/rocket-pool/rocketpool-go/utils/eth" "github.com/urfave/cli" @@ -21,6 +22,10 @@ func getRplPrice(c *cli.Context) (*api.RplPriceResponse, error) { rp, err := services.GetRocketPool(c) if err != nil { return nil, err } + return GetRplPrice(rp) +} + +func GetRplPrice(rp *rocketpool.RocketPool) (*api.RplPriceResponse, error) { // Response response := api.RplPriceResponse{} diff --git a/rocketpool/api/node/commands.go b/rocketpool/api/node/commands.go index 6d5e31d28..6c8cb4138 100644 --- a/rocketpool/api/node/commands.go +++ b/rocketpool/api/node/commands.go @@ -32,6 +32,23 @@ func RegisterSubcommands(command *cli.Command, name string, aliases []string) { }, }, + cli.Command{ + Name: "leader", + Aliases: []string{"l"}, + Usage: "node leaderboard", + UsageText: "rocketpool api node leader", + Action: func(c *cli.Context) error { + + // Validate args + if err := cliutils.ValidateArgCount(c, 0); err != nil { return err } + + // Run + api.PrintResponse(getLeader(c)) + return nil + + }, + }, + cli.Command{ Name: "can-register", Usage: "Check whether the node can be registered with Rocket Pool", diff --git a/rocketpool/api/node/leader.go b/rocketpool/api/node/leader.go new file mode 100644 index 000000000..f9b553c40 --- /dev/null +++ b/rocketpool/api/node/leader.go @@ -0,0 +1,165 @@ +package node + +import ( + "math/big" + "sort" + + "github.com/ethereum/go-ethereum/common" + "golang.org/x/sync/errgroup" + "github.com/urfave/cli" + "go.uber.org/multierr" + + "github.com/rocket-pool/rocketpool-go/node" + "github.com/rocket-pool/rocketpool-go/rocketpool" + "github.com/rocket-pool/smartnode/rocketpool/api/minipool" + "github.com/rocket-pool/smartnode/shared/services" + "github.com/rocket-pool/smartnode/shared/services/beacon" + "github.com/rocket-pool/smartnode/shared/types/api" +) + +// Settings +const ( + NodeDetailsBatchSize = 10 + TopMinipoolCount = 1 +) + + +func getLeader(c *cli.Context) (*api.NodeLeaderResponse, error) { + // Get services + if err := services.RequireRocketStorage(c); err != nil { return nil, err } + if err := services.RequireBeaconClientSynced(c); err != nil { return nil, err } + rp, err := services.GetRocketPool(c) + if err != nil { return nil, err } + bc, err := services.GetBeaconClient(c) + if err != nil { return nil, err } + + // Response + response := api.NodeLeaderResponse{} + + nodeRanks, err := GetNodeLeader(rp, bc) + if err != nil { return nil, err } + + response.Nodes = nodeRanks + return &response, nil +} + + +func GetNodeLeader(rp *rocketpool.RocketPool, bc beacon.Client) ([]api.NodeRank, error) { + + minipools, err := minipool.GetAllMinipoolDetails(rp, bc) + if err != nil { return nil, err } + nodeRanks, err := GetNodeDetails(rp) + if err != nil && nodeRanks == nil { return nil, err } + + // Get stating and has validator minipools + // put minipools into map by address + nodeToValMap := make(map[common.Address][]api.MinipoolDetails, len(minipools)) + for _, minipool := range minipools { + // Add to status list + address := minipool.Node.Address + if _, ok := nodeToValMap[address]; !ok { + nodeToValMap[address] = []api.MinipoolDetails{} + } + nodeToValMap[address] = append(nodeToValMap[address], minipool) + } + + for i, nodeRank := range nodeRanks { + vals, ok := nodeToValMap[nodeRank.Address] + if !ok { continue } + nodeRanks[i].Details = vals + nodeRanks[i].Score = calculateNodeScore(vals) + } + + sortFunc := func(m, n int) bool { + if nodeRanks[m].Score == nil { return false } + if nodeRanks[n].Score == nil { return true } + return nodeRanks[m].Score.Cmp(nodeRanks[n].Score) > 0 + } + sort.SliceStable(nodeRanks, sortFunc) + k := 1 + for i := range nodeRanks { + if (nodeRanks[i].Score != nil) { + nodeRanks[i].Rank = k + k++ + } else { + nodeRanks[i].Rank = 999999999 + } + } + + return nodeRanks, nil +} + + +func GetNodeDetails(rp *rocketpool.RocketPool) ([]api.NodeRank, error) { + + nodeAddresses, err := node.GetNodeAddresses(rp, nil) + if err != nil { return nil, err } + + var merr error + nodeRanks := make([]api.NodeRank, len(nodeAddresses)) + for bsi := 0; bsi < len(nodeAddresses); bsi += NodeDetailsBatchSize { + + // Get batch start & end index + msi := bsi + mei := bsi + NodeDetailsBatchSize + if mei > len(nodeAddresses) { mei = len(nodeAddresses) } + + // Load details + var wg errgroup.Group + for mi := msi; mi < mei; mi++ { + mi2 := mi + wg.Go(func() error { + address := nodeAddresses[mi2] + nodeRanks[mi2] = api.NodeRank{ Address: address } + details, err := node.GetNodeDetails(rp, address, nil) + if err == nil { + nodeRanks[mi2].Registered = details.Exists + nodeRanks[mi2].TimezoneLocation = details.TimezoneLocation + } + return err + }) + } + if err := wg.Wait(); err != nil { + merr = multierr.Append(merr, err) + } + } + + return nodeRanks, merr +} + + +func calculateNodeScore(vals []api.MinipoolDetails) *big.Int { + // score formula: take the top N performing validators + // sum up their profits or losses + // profit is defined as: current balance - initial node deposit - user deposit + // unless something is broken, this should be current balance - 32 + // unit is wei + + var prevMax *big.Int + score := new(big.Int) + + // remove non-existing validators from scoring + // use selection sort so we don't need to alloc more memory + for j := 0; j < TopMinipoolCount && j < len(vals); j++ { + var currMax *api.MinipoolDetails + for k := 0; k < len(vals); k++ { + if vals[k].Validator.Balance != nil && + (currMax == nil || vals[k].Validator.Balance.Cmp(currMax.Validator.Balance) > 0) && + (prevMax == nil || vals[k].Validator.Balance.Cmp(prevMax) < 0) { + currMax = &vals[k] + } + } + + if currMax == nil { + break + } + + score.Add(score, currMax.Validator.Balance) + score.Sub(score, currMax.Node.DepositBalance) + score.Sub(score, currMax.User.DepositBalance) + + prevMax = currMax.Validator.Balance + } + + return score +} diff --git a/rocketpool/api/node/status.go b/rocketpool/api/node/status.go index 1f7c77863..2b6568677 100644 --- a/rocketpool/api/node/status.go +++ b/rocketpool/api/node/status.go @@ -3,8 +3,10 @@ package node import ( "bytes" + "github.com/ethereum/go-ethereum/accounts" "github.com/rocket-pool/rocketpool-go/dao/trustednode" "github.com/rocket-pool/rocketpool-go/node" + "github.com/rocket-pool/rocketpool-go/rocketpool" "github.com/rocket-pool/rocketpool-go/tokens" "github.com/rocket-pool/rocketpool-go/types" "github.com/urfave/cli" @@ -25,14 +27,20 @@ func getStatus(c *cli.Context) (*api.NodeStatusResponse, error) { rp, err := services.GetRocketPool(c) if err != nil { return nil, err } - // Response - response := api.NodeStatusResponse{} - // Get node account nodeAccount, err := w.GetNodeAccount() if err != nil { return nil, err } + + return GetStatus(rp, nodeAccount) +} + + +func GetStatus(rp *rocketpool.RocketPool, nodeAccount accounts.Account) (*api.NodeStatusResponse, error) { + // Response + response := api.NodeStatusResponse{} + response.AccountAddress = nodeAccount.Address // Sync @@ -89,7 +97,7 @@ func getStatus(c *cli.Context) (*api.NodeStatusResponse, error) { // Get node minipool counts wg.Go(func() error { - details, err := getNodeMinipoolCountDetails(rp, nodeAccount.Address) + details, err := GetNodeMinipoolCountDetails(rp, nodeAccount.Address) if err == nil { response.MinipoolCounts.Total = len(details) for _, mpDetails := range details { diff --git a/rocketpool/api/node/utils.go b/rocketpool/api/node/utils.go index 392ec0c8a..4ae465b21 100644 --- a/rocketpool/api/node/utils.go +++ b/rocketpool/api/node/utils.go @@ -25,7 +25,7 @@ type minipoolCountDetails struct { // Get all node minipool count details -func getNodeMinipoolCountDetails(rp *rocketpool.RocketPool, nodeAddress common.Address) ([]minipoolCountDetails, error) { +func GetNodeMinipoolCountDetails(rp *rocketpool.RocketPool, nodeAddress common.Address) ([]minipoolCountDetails, error) { // Data var wg1 errgroup.Group diff --git a/rocketpool/metrics/auction.go b/rocketpool/metrics/auction.go new file mode 100644 index 000000000..6cad48c02 --- /dev/null +++ b/rocketpool/metrics/auction.go @@ -0,0 +1,146 @@ +package metrics + +import ( + "math/big" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/urfave/cli" + "golang.org/x/sync/errgroup" + + "github.com/rocket-pool/rocketpool-go/auction" + "github.com/rocket-pool/rocketpool-go/rocketpool" + "github.com/rocket-pool/rocketpool-go/utils/eth" + "github.com/rocket-pool/smartnode/shared/services" + "github.com/rocket-pool/smartnode/shared/utils/log" +) + + +// minipool metrics process +type auctionGauges struct { + lotCount prometheus.Gauge + balances *prometheus.GaugeVec +} + + +type auctionMetricsProcess struct { + rp *rocketpool.RocketPool + metrics auctionGauges + logger log.ColorLogger +} + + +// Start minipool metrics process +func startAuctionMetricsProcess(c *cli.Context, interval time.Duration, logger log.ColorLogger) { + + logger.Printlnf("Enter startAuctionMetricsProcess") + timer := time.NewTicker(interval) + var p *auctionMetricsProcess + var err error + // put create process in a loop because it may fail initially + for ; true; <- timer.C { + p, err = newAuctionMetricsProcss(c, logger) + if p != nil && err == nil { + break; + } + } + + // Update metrics on interval + for ; true; <- timer.C { + err = p.updateMetrics() + if err != nil { + // print error here instead of exit + logger.Printlnf("Error in updateMetrics: %w", err) + } + } + logger.Printlnf("Exit startAuctionMetricsProcess") +} + + +// Create new minipoolMetricsProcss object +func newAuctionMetricsProcss(c *cli.Context, logger log.ColorLogger) (*auctionMetricsProcess, error) { + + // Get services + if err := services.RequireRocketStorage(c); err != nil { return nil, err } + if err := services.RequireBeaconClientSynced(c); err != nil { return nil, err } + rp, err := services.GetRocketPool(c) + if err != nil { return nil, err } + + // Initialise metrics + metrics := auctionGauges { + lotCount: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "auction", + Name: "lot_count", + Help: "number of lots in auction Rocket Pool", + }), + balances: promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "auction", + Name: "balances_rpl", + Help: "the total RPL balance of the auction contract", + }, + []string{"category"}, + ), + } + + p := &auctionMetricsProcess { + rp: rp, + metrics: metrics, + logger: logger, + } + + return p, nil +} + + +// Update minipool metrics +func (p *auctionMetricsProcess) updateMetrics() error { + p.logger.Println("Enter auction updateMetrics") + + var lotCount uint64 + var totalBalance, allottedBalance, remainingBalance *big.Int + + // Sync + var wg errgroup.Group + + // Get data + wg.Go(func() error { + var err error + lotCount, err = auction.GetLotCount(p.rp, nil) + return err + }) + + wg.Go(func() error { + var err error + totalBalance, err = auction.GetTotalRPLBalance(p.rp, nil) + return err + }) + + wg.Go(func() error { + var err error + allottedBalance, err = auction.GetAllottedRPLBalance(p.rp, nil) + return err + }) + + wg.Go(func() error { + var err error + remainingBalance, err = auction.GetRemainingRPLBalance(p.rp, nil) + return err + }) + + // Wait for data + if err := wg.Wait(); err != nil { + return err + } + + p.metrics.lotCount.Set(float64(lotCount)) + p.metrics.balances.With(prometheus.Labels{"category":"TotalRPL"}).Set(eth.WeiToEth(totalBalance)) + p.metrics.balances.With(prometheus.Labels{"category":"AllottedRPL"}).Set(eth.WeiToEth(allottedBalance)) + p.metrics.balances.With(prometheus.Labels{"category":"RemainingRPL"}).Set(eth.WeiToEth(remainingBalance)) + + return nil +} + diff --git a/rocketpool/metrics/dao.go b/rocketpool/metrics/dao.go new file mode 100644 index 000000000..bc15a3bff --- /dev/null +++ b/rocketpool/metrics/dao.go @@ -0,0 +1,158 @@ +package metrics + +import ( + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/urfave/cli" + "golang.org/x/sync/errgroup" + + "github.com/rocket-pool/rocketpool-go/dao" + "github.com/rocket-pool/rocketpool-go/dao/trustednode" + "github.com/rocket-pool/rocketpool-go/rocketpool" + "github.com/rocket-pool/rocketpool-go/types" + "github.com/rocket-pool/smartnode/shared/services" + "github.com/rocket-pool/smartnode/shared/utils/log" +) + +// minipool metrics process +type daoGauges struct { + memberCount prometheus.Gauge + proposalCount prometheus.Gauge + proposalStateCount *prometheus.GaugeVec +} + + +type daoMetricsProcess struct { + rp *rocketpool.RocketPool + metrics daoGauges + logger log.ColorLogger +} + + +// Start minipool metrics process +func startDaoMetricsProcess(c *cli.Context, interval time.Duration, logger log.ColorLogger) { + + logger.Printlnf("Enter startDaoMetricsProcess") + timer := time.NewTicker(interval) + var p *daoMetricsProcess + var err error + // put create process in a loop because it may fail initially + for ; true; <- timer.C { + p, err = newDaoMetricsProcss(c, logger) + if p != nil && err == nil { + break; + } + } + + // Update metrics on interval + for ; true; <- timer.C { + err = p.updateMetrics() + if err != nil { + // print error here instead of exit + logger.Printlnf("Error in updateMetrics: %w", err) + } + } + logger.Printlnf("Exit startDaoMetricsProcess") +} + + +// Create new minipoolMetricsProcss object +func newDaoMetricsProcss(c *cli.Context, logger log.ColorLogger) (*daoMetricsProcess, error) { + + // Get services + if err := services.RequireRocketStorage(c); err != nil { return nil, err } + if err := services.RequireBeaconClientSynced(c); err != nil { return nil, err } + rp, err := services.GetRocketPool(c) + if err != nil { return nil, err } + + // Initialise metrics + metrics := daoGauges { + memberCount: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "dao", + Name: "member_count", + Help: "number of members in Rocket Pool dao", + }), + proposalCount: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "dao", + Name: "proposal_count", + Help: "number of proposals in Rocket Pool dao", + }), + proposalStateCount: promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "dao", + Name: "proposal_state_count", + Help: "the count of various states of Rocket Pool dao proposal", + }, + []string{"state"}, + ), + } + + p := &daoMetricsProcess { + rp: rp, + metrics: metrics, + logger: logger, + } + + return p, nil +} + + +// Update minipool metrics +func (p *daoMetricsProcess) updateMetrics() error { + p.logger.Println("Enter dao updateMetrics") + + var memberCount, proposalCount uint64 + var proposals []dao.ProposalDetails + + // Sync + var wg errgroup.Group + + // Get data + wg.Go(func() error { + var err error + memberCount, err = trustednode.GetMemberCount(p.rp, nil) + return err + }) + + wg.Go(func() error { + var err error + proposalCount, err = dao.GetProposalCount(p.rp, nil) + return err + }) + + wg.Go(func() error { + var err error + proposals, err = dao.GetProposals(p.rp, nil) + return err + }) + + // Wait for data + if err := wg.Wait(); err != nil { + return err + } + + p.metrics.memberCount.Set(float64(memberCount)) + p.metrics.proposalCount.Set(float64(proposalCount)) + + // Tally up proposal states + stateCounts := make(map[types.ProposalState]uint32, len(types.ProposalStates)) + for _, proposal := range proposals { + + if _, ok := stateCounts[proposal.State]; !ok { + stateCounts[proposal.State] = 0 + } + stateCounts[proposal.State]++ + } + + for state, count := range stateCounts { + p.metrics.proposalStateCount.With(prometheus.Labels{"state":types.ProposalStates[state]}).Set(float64(count)) + } + + return nil +} + diff --git a/rocketpool/metrics/metrics.go b/rocketpool/metrics/metrics.go new file mode 100644 index 000000000..794e51607 --- /dev/null +++ b/rocketpool/metrics/metrics.go @@ -0,0 +1,81 @@ +package metrics + +import ( + "net/http" + "time" + + "github.com/fatih/color" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/urfave/cli" + + "github.com/rocket-pool/smartnode/shared/utils/log" +) + + +// Config +const ( + maxConcurrentEth1Requests = 200 + networkMetricsColor = color.BgYellow + minipoolMetricsColor = color.BgGreen + nodeMetricsColor = color.BgCyan + errorColor = color.FgRed +) +var networkUpdateInterval, _ = time.ParseDuration("30s") +var minipoolUpdateInterval, _ = time.ParseDuration("5m") +var nodeUpdateInterval, _ = time.ParseDuration("30m") + + +// Register metrics command +func RegisterCommands(app *cli.App, name string, aliases []string) { + app.Commands = append(app.Commands, cli.Command{ + Name: name, + Aliases: aliases, + Usage: "Run Rocket Pool metrics daemon", + Action: func(c *cli.Context) error { + return run(c) + }, + }) +} + + +// Run process +func run(c *cli.Context) error { + logger := log.NewColorLogger(networkMetricsColor) + errorLog := log.NewColorLogger(errorColor) + logger.Println("Enter metrics.run") + + // Configure + configureHTTP() + + // Start metrics processes + go (func() { startAuctionMetricsProcess(c, networkUpdateInterval, logger) })() + go (func() { startDaoMetricsProcess(c, networkUpdateInterval, logger) })() + go (func() { startNetworkMetricsProcess(c, networkUpdateInterval, logger) })() + go (func() { startSettingsMetricsProcess(c, networkUpdateInterval, logger) })() + go (func() { startTokensMetricsProcess(c, networkUpdateInterval, logger) })() + go (func() { startNodeOwnMetricsProcess(c, minipoolUpdateInterval, logger) })() + //go (func() { startNodeNetworkMetricsProcess(c, nodeUpdateInterval, logger) })() + + // Serve metrics + http.Handle("/metrics", promhttp.Handler()) + err := http.ListenAndServe(":2112", nil) + if (err != nil) { + errorLog.Printlnf("Exit metrics.run with error: %w", err) + } else { + logger.Println("Exit metrics.run") + } + + return err +} + + +// Configure HTTP transport settings +func configureHTTP() { + + // The watchtower daemon makes a large number of concurrent RPC requests to the Eth1 client + // The HTTP transport is set to cache connections for future re-use equal to the maximum expected number of concurrent requests + // This prevents issues related to memory consumption and address allowance from repeatedly opening and closing connections + http.DefaultTransport.(*http.Transport).MaxIdleConnsPerHost = maxConcurrentEth1Requests + +} + diff --git a/rocketpool/metrics/network.go b/rocketpool/metrics/network.go new file mode 100644 index 000000000..1155d32d7 --- /dev/null +++ b/rocketpool/metrics/network.go @@ -0,0 +1,343 @@ +package metrics + +import ( + "math/big" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/urfave/cli" + "golang.org/x/sync/errgroup" + "go.uber.org/multierr" + + "github.com/rocket-pool/rocketpool-go/deposit" + "github.com/rocket-pool/rocketpool-go/minipool" + "github.com/rocket-pool/rocketpool-go/node" + "github.com/rocket-pool/rocketpool-go/network" + "github.com/rocket-pool/rocketpool-go/rocketpool" + "github.com/rocket-pool/rocketpool-go/types" + "github.com/rocket-pool/rocketpool-go/utils/eth" + apiNetwork "github.com/rocket-pool/smartnode/rocketpool/api/network" + "github.com/rocket-pool/smartnode/shared/services" + "github.com/rocket-pool/smartnode/shared/services/beacon" + "github.com/rocket-pool/smartnode/shared/utils/log" +) + + +type networkGauges struct { + nodeCount prometheus.Gauge + minipoolCount prometheus.Gauge + minipoolQueue *prometheus.GaugeVec + networkFees *prometheus.GaugeVec + rplPriceBlock prometheus.Gauge + rplPrice prometheus.Gauge + networkBlock prometheus.Gauge + networkBalances *prometheus.GaugeVec +} + + +// network metrics process +type networkMetricsProcess struct { + rp *rocketpool.RocketPool + bc beacon.Client + metrics networkGauges + logger log.ColorLogger +} + + +type networkBalances struct { + Block uint64 + TotalETH *big.Int + StakingETH *big.Int + TotalRETH *big.Int + DepositBalance *big.Int + DepositExcessBalance *big.Int + TotalRplStake *big.Int + TotalEffectiveRplStake *big.Int +} + + +// Start network metrics process +func startNetworkMetricsProcess(c *cli.Context, interval time.Duration, logger log.ColorLogger) { + + logger.Printlnf("Enter startNetworkMetricsProcess") + timer := time.NewTicker(interval) + var p *networkMetricsProcess + var err error + // put create process in a loop because it may fail initially + for ; true; <- timer.C { + p, err = newNetworkMetricsProcess(c, logger) + if p != nil && err == nil { + break; + } + } + + // Update metrics on interval + for ; true; <- timer.C { + err = p.updateMetrics() + if err != nil { + // print error here instead of exit + logger.Printlnf("Error in updateMetrics: %w", err) + } + } + logger.Printlnf("Exit startNetworkMetricsProcess") +} + + +// Create new networkMetricsProcess object +func newNetworkMetricsProcess(c *cli.Context, logger log.ColorLogger) (*networkMetricsProcess, error) { + + logger.Printlnf("Enter newNetworkMetricsProcess") + if err := services.RequireRocketStorage(c); err != nil { return nil, err } + if err := services.RequireBeaconClientSynced(c); err != nil { return nil, err } + rp, err := services.GetRocketPool(c) + if err != nil { return nil, err } + bc, err := services.GetBeaconClient(c) + if err != nil { return nil, err } + + // Initialise metrics + metrics := networkGauges { + nodeCount: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "node", + Name: "total_count", + Help: "total number of nodes in Rocket Pool", + }), + minipoolCount: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "minipool", + Name: "total_count", + Help: "total number of minipools in Rocket Pool", + }), + minipoolQueue: promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "minipool", + Name: "queue_count", + Help: "number of minipools in queue for assignment", + }, + []string{"depositType"}, + ), + networkFees: promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "network", + Name: "fee_rate", + Help: "network fees as rate of amount staked", + }, + []string{"range"}, + ), + rplPriceBlock: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "network", + Name: "rpl_price_updated_block", + Help: "block of current submitted RPL price", + }), + rplPrice: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "network", + Name: "rpl_price_eth", + Help: "RPL price in ETH", + }), + networkBlock: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "network", + Name: "balance_updated_block", + Help: "block of current submitted balances", + }), + networkBalances: promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "network", + Name: "balance_eth", + Help: "network balances and supplies in given category", + }, + []string{"category"}, + ), + } + + p := &networkMetricsProcess { + rp: rp, + bc: bc, + //account: account, + metrics: metrics, + logger: logger, + } + + logger.Printlnf("Exit newNetworkMetricsProcess") + return p, nil +} + + +// Update network metrics +func (p *networkMetricsProcess) updateMetrics() error { + p.logger.Printlnf("Enter network updateMetrics") + + err1 := p.updateCounts() + err4 := p.updateNetwork() + err5 := p.updateMinipoolQueue() + err := multierr.Combine(err1, err4, err5) + + p.logger.Printlnf("Exit network updateMetrics with %d errors", len(multierr.Errors(err))) + return err +} + + +func (p *networkMetricsProcess) updateCounts() error { + + nodeCount, err := node.GetNodeCount(p.rp, nil) + if err != nil { return err } + p.metrics.nodeCount.Set(float64(nodeCount)) + + minipoolCount, err := minipool.GetMinipoolCount(p.rp, nil) + if err != nil { return err } + p.metrics.minipoolCount.Set(float64(minipoolCount)) + + return nil +} + + +func (p *networkMetricsProcess) updateNetwork() error { + + nodeFees, err := apiNetwork.GetNodeFee(p.rp) + if err != nil { return err } + + p.metrics.networkFees.With(prometheus.Labels{"range":"current"}).Set(nodeFees.NodeFee) + p.metrics.networkFees.With(prometheus.Labels{"range":"min"}).Set(nodeFees.MinNodeFee) + p.metrics.networkFees.With(prometheus.Labels{"range":"target"}).Set(nodeFees.TargetNodeFee) + p.metrics.networkFees.With(prometheus.Labels{"range":"max"}).Set(nodeFees.MaxNodeFee) + + rplPrice, err := apiNetwork.GetRplPrice(p.rp) + if err != nil { return err } + + p.metrics.rplPriceBlock.Set(float64(rplPrice.RplPriceBlock)) + p.metrics.rplPrice.Set(eth.WeiToEth(rplPrice.RplPrice)) + + balances, err := getNetworkBalances(p.rp) + if err != nil { return err } + + p.metrics.networkBlock.Set(float64(balances.Block)) + p.metrics.networkBalances.With(prometheus.Labels{"category":"TotalETH"}).Set(eth.WeiToEth(balances.TotalETH)) + p.metrics.networkBalances.With(prometheus.Labels{"category":"StakingETH"}).Set(eth.WeiToEth(balances.StakingETH)) + p.metrics.networkBalances.With(prometheus.Labels{"category":"TotalRETH"}).Set(eth.WeiToEth(balances.TotalRETH)) + p.metrics.networkBalances.With(prometheus.Labels{"category":"Deposit"}).Set(eth.WeiToEth(balances.DepositBalance)) + p.metrics.networkBalances.With(prometheus.Labels{"category":"DepositExcess"}).Set(eth.WeiToEth(balances.DepositExcessBalance)) + p.metrics.networkBalances.With(prometheus.Labels{"category":"TotalRPL"}).Set(eth.WeiToEth(balances.TotalRplStake)) + p.metrics.networkBalances.With(prometheus.Labels{"category":"TotalEffectiveRPL"}).Set(eth.WeiToEth(balances.TotalEffectiveRplStake)) + + return nil +} + + +func getNetworkBalances(rp *rocketpool.RocketPool) (*networkBalances, error) { + stuff := networkBalances{} + + // Sync + var wg errgroup.Group + + // Get data + wg.Go(func() error { + block, err := network.GetBalancesBlock(rp, nil) + if err == nil { + stuff.Block = block + } + return err + }) + wg.Go(func() error { + totalETH, err := network.GetTotalETHBalance(rp, nil) + if err == nil { + stuff.TotalETH = totalETH + } + return err + }) + wg.Go(func() error { + stakingETH, err := network.GetStakingETHBalance(rp, nil) + if err == nil { + stuff.StakingETH = stakingETH + } + return err + }) + wg.Go(func() error { + totalRETH, err := network.GetTotalRETHSupply(rp, nil) + if err == nil { + stuff.TotalRETH = totalRETH + } + return err + }) + wg.Go(func() error { + depositBalance, err := deposit.GetBalance(rp, nil) + if err == nil { + stuff.DepositBalance = depositBalance + } + return err + }) + wg.Go(func() error { + depositExcessBalance, err := deposit.GetExcessBalance(rp, nil) + if err == nil { + stuff.DepositExcessBalance = depositExcessBalance + } + return err + }) + wg.Go(func() error { + totalRplStake, err := node.GetTotalRPLStake(rp, nil) + if err == nil { + stuff.TotalRplStake = totalRplStake + } + return err + }) + wg.Go(func() error { + totalEffectiveRplStake, err := node.GetTotalEffectiveRPLStake(rp, nil) + if err == nil { + stuff.TotalEffectiveRplStake = totalEffectiveRplStake + } + return err + }) + + // Wait for data + if err := wg.Wait(); err != nil { + return nil, err + } + + // Return response + return &stuff, nil +} + + +func (p *networkMetricsProcess) updateMinipoolQueue() error { + var wg errgroup.Group + var fullQueueLength, halfQueueLength, emptyQueueLength uint64 + + // Get data + wg.Go(func() error { + response, err := minipool.GetQueueLength(p.rp, types.Full, nil) + if err == nil { + fullQueueLength = response + } + return err + }) + wg.Go(func() error { + response, err := minipool.GetQueueLength(p.rp, types.Half, nil) + if err == nil { + halfQueueLength = response + } + return err + }) + wg.Go(func() error { + response, err := minipool.GetQueueLength(p.rp, types.Empty, nil) + if err == nil { + emptyQueueLength = response + } + return err + }) + + // Wait for data + if err := wg.Wait(); err != nil { + return err + } + p.metrics.minipoolQueue.With(prometheus.Labels{"depositType":"Full"}).Set(float64(fullQueueLength)) + p.metrics.minipoolQueue.With(prometheus.Labels{"depositType":"Half"}).Set(float64(halfQueueLength)) + p.metrics.minipoolQueue.With(prometheus.Labels{"depositType":"Empty"}).Set(float64(emptyQueueLength)) + + return nil +} + diff --git a/rocketpool/metrics/node-network.go b/rocketpool/metrics/node-network.go new file mode 100644 index 000000000..99afbe7e6 --- /dev/null +++ b/rocketpool/metrics/node-network.go @@ -0,0 +1,281 @@ +package metrics + +import ( + "fmt" + "sort" + "strconv" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/urfave/cli" + + "github.com/rocket-pool/rocketpool-go/rocketpool" + "github.com/rocket-pool/rocketpool-go/types" + "github.com/rocket-pool/rocketpool-go/utils/eth" + apiNode "github.com/rocket-pool/smartnode/rocketpool/api/node" + "github.com/rocket-pool/smartnode/shared/services" + "github.com/rocket-pool/smartnode/shared/services/beacon" + "github.com/rocket-pool/smartnode/shared/types/api" + "github.com/rocket-pool/smartnode/shared/utils/hex" + "github.com/rocket-pool/smartnode/shared/utils/log" +) + + +const ( + BucketInterval = 0.025 +) + + +// node metrics process +type nodeNetworkGauges struct { + scores *prometheus.GaugeVec + scoreHist *prometheus.GaugeVec + scoreHistSum prometheus.Gauge + scoreHistCount prometheus.Gauge + nodeMinipoolCounts *prometheus.GaugeVec + minipoolCounts *prometheus.GaugeVec +} + + +type nodeNetworkMetricsProcess struct { + rp *rocketpool.RocketPool + bc beacon.Client + metrics nodeNetworkGauges + logger log.ColorLogger +} + + +// Start node metrics process +func startNodeNetworkMetricsProcess(c *cli.Context, interval time.Duration, logger log.ColorLogger) { + + logger.Printlnf("Enter startNodeNetworkMetricsProcess") + timer := time.NewTicker(interval) + var p *nodeNetworkMetricsProcess + var err error + // put create process in a loop because it may fail initially + for ; true; <- timer.C { + p, err = newNodeNetworkMetricsProcss(c, logger) + if p != nil && err == nil { + break; + } + } + + // Update metrics on interval + for ; true; <- timer.C { + err = p.updateMetrics() + if err != nil { + // print error here instead of exit + logger.Printlnf("Error in updateMetrics: %w", err) + } + } + logger.Printlnf("Exit startNodeNetworkMetricsProcess") +} + + +// Create new nodeMetricsProcess object +func newNodeNetworkMetricsProcss(c *cli.Context, logger log.ColorLogger) (*nodeNetworkMetricsProcess, error) { + + // Get services + if err := services.RequireRocketStorage(c); err != nil { return nil, err } + if err := services.RequireBeaconClientSynced(c); err != nil { return nil, err } + rp, err := services.GetRocketPool(c) + if err != nil { return nil, err } + bc, err := services.GetBeaconClient(c) + if err != nil { return nil, err } + + // Initialise metrics + metrics := nodeNetworkGauges { + scores: promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "node_network", + Name: "eth", + Help: "sum of rewards/penalties of the top two minipools for this node", + }, + []string{"address", "rank"}, + ), + scoreHist: promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "node_network", + Name: "hist_eth", + Help: "distribution of sum of rewards/penalties of the top two minipools in rocketpool network", + }, + []string{"le"}, + ), + scoreHistSum: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "node_network", + Name: "hist_eth_sum", + }), + scoreHistCount: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "node_network", + Name: "hist_eth_count", + }), + nodeMinipoolCounts: promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "node_network", + Name: "minipool_count", + Help: "number of activated minipools running for node address", + }, + []string{"address", "timezone"}, + ), + minipoolCounts: promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "node_network", + Name: "count", + Help: "minipools counts with various aggregations", + }, + []string{"status"}, + ), + } + + p := &nodeNetworkMetricsProcess { + rp: rp, + bc: bc, + metrics: metrics, + logger: logger, + } + + return p, nil +} + + +// Update node metrics +func (p *nodeNetworkMetricsProcess) updateMetrics() error { + p.logger.Println("Enter node-network updateMetrics") + + nodeRanks, err := apiNode.GetNodeLeader(p.rp, p.bc) + if err != nil { return err } + + p.updateScore(nodeRanks) + p.updateHistogram(nodeRanks) + p.updateNodeMinipoolCount(nodeRanks) + p.updateMinipoolCount(nodeRanks) + + p.logger.Println("Exit node-network updateMetrics") + return nil +} + + +func (p *nodeNetworkMetricsProcess) updateScore(nodeRanks []api.NodeRank) { + p.metrics.scores.Reset() + + for _, nodeRank := range nodeRanks { + + nodeAddress := hex.AddPrefix(nodeRank.Address.Hex()) + + if nodeRank.Score != nil { + scoreEth := eth.WeiToEth(nodeRank.Score) + p.metrics.scores.With(prometheus.Labels{"address":nodeAddress, "rank":strconv.Itoa(nodeRank.Rank)}).Set(scoreEth) + } + } +} + + +func (p *nodeNetworkMetricsProcess) updateHistogram(nodeRanks []api.NodeRank) { + p.metrics.scoreHist.Reset() + + if len(nodeRanks) == 0 { return } + + histogram := make(map[float64]int, 100) + var sumScores float64 + + for _, nodeRank := range nodeRanks { + + if nodeRank.Score != nil { + scoreEth := eth.WeiToEth(nodeRank.Score) + + // find next highest bucket to put in + bucket := float64(int(scoreEth / BucketInterval)) * BucketInterval + if (bucket < scoreEth) { + bucket = bucket + BucketInterval + } + if _, ok := histogram[bucket]; !ok { + histogram[bucket] = 0 + } + histogram[bucket]++ + sumScores += scoreEth + } + } + + buckets := make([]float64, 0, len(histogram)) + for b := range histogram { + buckets = append(buckets, b) + } + sort.Float64s(buckets) + + accCount := 0 + + if len(buckets) > 0 { + nextB := buckets[0] + for _, b := range buckets { + + // fill in the gaps + for nextB < b { + p.metrics.scoreHist.With(prometheus.Labels{"le":fmt.Sprintf("%.3f", nextB)}).Set(float64(accCount)) + nextB = nextB + BucketInterval + } + + accCount += histogram[b] + p.metrics.scoreHist.With(prometheus.Labels{"le":fmt.Sprintf("%.3f", b)}).Set(float64(accCount)) + + nextB = b + BucketInterval + } + } + + p.metrics.scoreHistSum.Set(sumScores) + p.metrics.scoreHistCount.Set(float64(accCount)) +} + + +func (p *nodeNetworkMetricsProcess) updateNodeMinipoolCount(nodeRanks []api.NodeRank) { + p.metrics.nodeMinipoolCounts.Reset() + + for _, nodeRank := range nodeRanks { + + nodeAddress := hex.AddPrefix(nodeRank.Address.Hex()) + minipoolCount := len(nodeRank.Details) + labels := prometheus.Labels { + "address":nodeAddress, + "timezone":nodeRank.TimezoneLocation, + } + p.metrics.nodeMinipoolCounts.With(labels).Set(float64(minipoolCount)) + } +} + + +func (p *nodeNetworkMetricsProcess) updateMinipoolCount(nodeRanks []api.NodeRank) { + p.metrics.minipoolCounts.Reset() + + var totalCount, initializedCount, prelaunchCount, stakingCount, withdrawableCount, dissolvedCount int + var validatorExistsCount, validatorActiveCount int + + for _, nodeRank := range nodeRanks { + totalCount += len(nodeRank.Details) + for _, minipool := range nodeRank.Details { + switch minipool.Status.Status { + case types.Initialized: initializedCount++ + case types.Prelaunch: prelaunchCount++ + case types.Staking: stakingCount++ + case types.Withdrawable: withdrawableCount++ + case types.Dissolved: dissolvedCount++ + } + if minipool.Validator.Exists { validatorExistsCount ++ } + if minipool.Validator.Active { validatorActiveCount ++ } + } + } + p.metrics.minipoolCounts.With(prometheus.Labels{"status":"total"}).Set(float64(totalCount)) + p.metrics.minipoolCounts.With(prometheus.Labels{"status":"initialized"}).Set(float64(initializedCount)) + p.metrics.minipoolCounts.With(prometheus.Labels{"status":"prelaunch"}).Set(float64(prelaunchCount)) + p.metrics.minipoolCounts.With(prometheus.Labels{"status":"staking"}).Set(float64(stakingCount)) + p.metrics.minipoolCounts.With(prometheus.Labels{"status":"withdrawable"}).Set(float64(withdrawableCount)) + p.metrics.minipoolCounts.With(prometheus.Labels{"status":"dissolved"}).Set(float64(dissolvedCount)) + p.metrics.minipoolCounts.With(prometheus.Labels{"status":"validatorExists"}).Set(float64(validatorExistsCount)) + p.metrics.minipoolCounts.With(prometheus.Labels{"status":"validatorActive"}).Set(float64(validatorActiveCount)) +} + diff --git a/rocketpool/metrics/node-own.go b/rocketpool/metrics/node-own.go new file mode 100644 index 000000000..69186bf81 --- /dev/null +++ b/rocketpool/metrics/node-own.go @@ -0,0 +1,227 @@ +package metrics + +import ( + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/ethereum/go-ethereum/accounts" + "github.com/urfave/cli" + "go.uber.org/multierr" + + "github.com/rocket-pool/rocketpool-go/rocketpool" + "github.com/rocket-pool/rocketpool-go/utils/eth" + apiMinipool "github.com/rocket-pool/smartnode/rocketpool/api/minipool" + apiNode "github.com/rocket-pool/smartnode/rocketpool/api/node" + "github.com/rocket-pool/smartnode/shared/services" + "github.com/rocket-pool/smartnode/shared/services/beacon" + "github.com/rocket-pool/smartnode/shared/utils/hex" + "github.com/rocket-pool/smartnode/shared/utils/log" +) + + +// minipool metrics process +type nodeOwnGauges struct { + minipoolBalance *prometheus.GaugeVec + nodeTrusted prometheus.Gauge + accountBalances *prometheus.GaugeVec + rplStake *prometheus.GaugeVec + minipoolLimit prometheus.Gauge + minipoolCounts *prometheus.GaugeVec + withdrawalBalances *prometheus.GaugeVec +} + + +type nodeOwnMetricsProcess struct { + rp *rocketpool.RocketPool + bc beacon.Client + account accounts.Account + metrics nodeOwnGauges + logger log.ColorLogger +} + + +// Start minipool metrics process +func startNodeOwnMetricsProcess(c *cli.Context, interval time.Duration, logger log.ColorLogger) { + + logger.Printlnf("Enter startNodeOwnMetricsProcess") + timer := time.NewTicker(interval) + var p *nodeOwnMetricsProcess + var err error + // put create process in a loop because it may fail initially + for ; true; <- timer.C { + p, err = newMinipoolMetricsProcss(c, logger) + if p != nil && err == nil { + break; + } + logger.Printlnf("nodeOwnMetricsProcess retry loop: %w", err) + } + logger.Printlnf("nodeOwnMetricsProcess created") + + // Update metrics on interval + for ; true; <- timer.C { + err = p.updateMetrics() + if err != nil { + // print error here instead of exit + logger.Printlnf("Error in updateMetrics: %w", err) + } + } + logger.Printlnf("Exit startNodeOwnMetricsProcess") +} + + +// Create new minipoolMetricsProcss object +func newMinipoolMetricsProcss(c *cli.Context, logger log.ColorLogger) (*nodeOwnMetricsProcess, error) { + + // Get services + if err := services.RequireRocketStorage(c); err != nil { return nil, err } + if err := services.RequireBeaconClientSynced(c); err != nil { return nil, err } + w, err := services.GetWallet(c) + if err != nil { return nil, err } + rp, err := services.GetRocketPool(c) + if err != nil { return nil, err } + bc, err := services.GetBeaconClient(c) + if err != nil { return nil, err } + account, err := w.GetNodeAccount() + if err != nil { return nil, err } + + // Initialise metrics + metrics := nodeOwnGauges { + minipoolBalance: promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "node_own", + Name: "minipool_balance_eth", + Help: "balance of validator for own node", + }, + []string{"address", "validatorPubkey"}, + ), + nodeTrusted: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "node_own", + Name: "trusted_bool", + Help: "whether this node is oracle node", + }), + accountBalances: promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "node_own", + Name: "account_balance", + Help: "account balances for own node", + }, + []string{"token"}, + ), + rplStake: promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "node_own", + Name: "stake_rpl", + Help: "amounts of stake in RPL for own node", + }, + []string{"status"}, + ), + minipoolLimit: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "node_own", + Name: "minipool_limit_count", + Help: "minipool limit based on RPL stake for own node", + }), + minipoolCounts: promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "node_own", + Name: "minipool_count", + Help: "counts of minipools for own node", + }, + []string{"status"}, + ), + withdrawalBalances: promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "node_own", + Name: "withdraw_balance", + Help: "balances available for withdraw for own node", + }, + []string{"token"}, + ), + } + + p := &nodeOwnMetricsProcess { + rp: rp, + bc: bc, + account: account, + metrics: metrics, + logger: logger, + } + + return p, nil +} + + +// Update minipool metrics +func (p *nodeOwnMetricsProcess) updateMetrics() error { + p.logger.Println("Enter node-own updateMetrics") + + err1 := p.updateNode() + err2 := p.updateMinipool() + err := multierr.Combine(err1, err2) + + p.logger.Printlnf("Exit node-own updateMetrics with %d errors", len(multierr.Errors(err))) + return err +} + + +func (p *nodeOwnMetricsProcess) updateNode() (error) { + + // Response + nodeStatus, err := apiNode.GetStatus(p.rp, p.account) + if err != nil { + return err + } + + p.metrics.nodeTrusted.Set(float64(B2i(nodeStatus.Trusted))) + p.metrics.accountBalances.With(prometheus.Labels{"token":"ETH"}).Set(eth.WeiToEth(nodeStatus.AccountBalances.ETH)) + p.metrics.accountBalances.With(prometheus.Labels{"token":"RETH"}).Set(eth.WeiToEth(nodeStatus.AccountBalances.RETH)) + p.metrics.accountBalances.With(prometheus.Labels{"token":"RPL"}).Set(eth.WeiToEth(nodeStatus.AccountBalances.RPL)) + p.metrics.rplStake.With(prometheus.Labels{"status":"current"}).Set(eth.WeiToEth(nodeStatus.RplStake)) + p.metrics.rplStake.With(prometheus.Labels{"status":"effective"}).Set(eth.WeiToEth(nodeStatus.EffectiveRplStake)) + p.metrics.rplStake.With(prometheus.Labels{"status":"minimum"}).Set(eth.WeiToEth(nodeStatus.MinimumRplStake)) + p.metrics.minipoolLimit.Set(float64(nodeStatus.MinipoolLimit)) + p.metrics.minipoolCounts.With(prometheus.Labels{"status":"closeAvailable"}).Set(float64(nodeStatus.MinipoolCounts.CloseAvailable)) + p.metrics.minipoolCounts.With(prometheus.Labels{"status":"dissolved"}).Set(float64(nodeStatus.MinipoolCounts.Dissolved)) + p.metrics.minipoolCounts.With(prometheus.Labels{"status":"initialized"}).Set(float64(nodeStatus.MinipoolCounts.Initialized)) + p.metrics.minipoolCounts.With(prometheus.Labels{"status":"prelaunch"}).Set(float64(nodeStatus.MinipoolCounts.Prelaunch)) + p.metrics.minipoolCounts.With(prometheus.Labels{"status":"refundAvailable"}).Set(float64(nodeStatus.MinipoolCounts.RefundAvailable)) + p.metrics.minipoolCounts.With(prometheus.Labels{"status":"staking"}).Set(float64(nodeStatus.MinipoolCounts.Staking)) + p.metrics.minipoolCounts.With(prometheus.Labels{"status":"total"}).Set(float64(nodeStatus.MinipoolCounts.Total)) + p.metrics.minipoolCounts.With(prometheus.Labels{"status":"withdrawable"}).Set(float64(nodeStatus.MinipoolCounts.Withdrawable)) + p.metrics.minipoolCounts.With(prometheus.Labels{"status":"withdrawalAvailable"}).Set(float64(nodeStatus.MinipoolCounts.WithdrawalAvailable)) + if nodeStatus.WithdrawalBalances.ETH != nil { + p.metrics.withdrawalBalances.With(prometheus.Labels{"token":"ETH"}).Set(eth.WeiToEth(nodeStatus.WithdrawalBalances.ETH)) + p.metrics.withdrawalBalances.With(prometheus.Labels{"token":"RETH"}).Set(eth.WeiToEth(nodeStatus.WithdrawalBalances.RETH)) + p.metrics.withdrawalBalances.With(prometheus.Labels{"token":"RPL"}).Set(eth.WeiToEth(nodeStatus.WithdrawalBalances.RPL)) + } + + return nil +} + + +func (p *nodeOwnMetricsProcess) updateMinipool() error { + + minipools, err := apiMinipool.GetNodeMinipoolDetails(p.rp, p.bc, p.account.Address) + if err != nil { return err } + + for _, minipool := range minipools { + address := hex.AddPrefix(minipool.Node.Address.Hex()) + validatorPubkey := hex.AddPrefix(minipool.ValidatorPubkey.Hex()) + var balance float64 + if minipool.Validator.Balance != nil { + balance = eth.WeiToEth(minipool.Validator.Balance) + } + + p.metrics.minipoolBalance.With(prometheus.Labels{"address":address, "validatorPubkey":validatorPubkey}).Set(balance) + } + + return nil +} + diff --git a/rocketpool/metrics/settings.go b/rocketpool/metrics/settings.go new file mode 100644 index 000000000..ad7d338fd --- /dev/null +++ b/rocketpool/metrics/settings.go @@ -0,0 +1,1016 @@ +package metrics + +import ( + "math/big" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/urfave/cli" + "golang.org/x/sync/errgroup" + + "github.com/rocket-pool/rocketpool-go/rocketpool" + "github.com/rocket-pool/rocketpool-go/settings/protocol" + "github.com/rocket-pool/rocketpool-go/settings/trustednode" + "github.com/rocket-pool/rocketpool-go/utils/eth" + "github.com/rocket-pool/smartnode/shared/services" + "github.com/rocket-pool/smartnode/shared/services/beacon" + "github.com/rocket-pool/smartnode/shared/utils/log" +) + + +type settingsGauges struct { + flags *prometheus.GaugeVec + lotMinimumEth prometheus.Gauge + lotMaximumEth prometheus.Gauge + lotDuration prometheus.Gauge + lotStartingPrice prometheus.Gauge + lotReservePrice prometheus.Gauge + minimumDeposit prometheus.Gauge + maximumDepositPoolSize prometheus.Gauge + maximumDepositAssignments prometheus.Gauge + inflationIntervalRate prometheus.Gauge + inflationIntervalBlocks prometheus.Gauge + inflationStartBlock prometheus.Gauge + minipoolAmounts *prometheus.GaugeVec + minipoolLaunchTimeout prometheus.Gauge + nodeConsensusThreshold prometheus.Gauge + submitBalancesFrequency prometheus.Gauge + submitPricesFrequency prometheus.Gauge + networkNodeFee *prometheus.GaugeVec + targetRethCollateralRate prometheus.Gauge + nodeMinimumPerMinipoolStake prometheus.Gauge + nodeMaximumPerMinipoolStake prometheus.Gauge + rewardsClaimerPerc *prometheus.GaugeVec + rewardsClaimerPercUpdate *prometheus.GaugeVec + membersQuorum prometheus.Gauge + membersRPLBond prometheus.Gauge + membersMinipoolUnbondedMax prometheus.Gauge + membersChallengeCooldown prometheus.Gauge + membersChallengeWindow prometheus.Gauge + membersChallengeCost prometheus.Gauge + proposalCooldown prometheus.Gauge + proposalVoteBlocks prometheus.Gauge + proposalVoteDelayBlocks prometheus.Gauge + proposalExecuteBlocks prometheus.Gauge + proposalActionBlocks prometheus.Gauge +} + + +// network metrics process +type settingsMetricsProcess struct { + rp *rocketpool.RocketPool + bc beacon.Client + metrics settingsGauges + logger log.ColorLogger +} + + +// Start network metrics process +func startSettingsMetricsProcess(c *cli.Context, interval time.Duration, logger log.ColorLogger) { + + logger.Printlnf("Enter startSettingsMetricsProcess") + timer := time.NewTicker(interval) + var p *settingsMetricsProcess + var err error + // put create process in a loop because it may fail initially + for ; true; <- timer.C { + p, err = newSettingsMetricsProcess(c, logger) + if p != nil && err == nil { + break; + } + } + + // Update metrics on interval + for ; true; <- timer.C { + err = p.updateMetrics() + if err != nil { + // print error here instead of exit + logger.Printlnf("Error in updateMetrics: %w", err) + } + } + logger.Printlnf("Exit startSettingsMetricsProcess") +} + + +// Create new settingsMetricsProcess object +func newSettingsMetricsProcess(c *cli.Context, logger log.ColorLogger) (*settingsMetricsProcess, error) { + + logger.Printlnf("Enter newSettingsMetricsProcess") + if err := services.RequireRocketStorage(c); err != nil { return nil, err } + if err := services.RequireBeaconClientSynced(c); err != nil { return nil, err } + rp, err := services.GetRocketPool(c) + if err != nil { return nil, err } + bc, err := services.GetBeaconClient(c) + if err != nil { return nil, err } + + // Initialise metrics + metrics := settingsGauges { + flags: promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "flags_bool", + Help: "settings flags on rocketpool protocol", + }, + []string{"flag"}, + ), + lotMinimumEth: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "lot_minimum_eth", + Help: "minimum lot size in ETH", + }), + lotMaximumEth: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "lot_maximum_eth", + Help: "maximum lot size in ETH", + }), + lotDuration: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "lot_duration_blocks", + Help: "lot duration in blocks", + }), + lotStartingPrice: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "lot_starting_price_ratio", + Help: "starting price relative to current ETH price, as a fraction", + }), + lotReservePrice: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "lot_reserve_price_ratio", + Help: "reserve price relative to current ETH price, as a fraction", + }), + minimumDeposit: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "deposit_minimum_eth", + Help: "minimum deposit size", + }), + maximumDepositPoolSize: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "deposit_maximum_pool_eth", + Help: "maximum size of deposit pool", + }), + maximumDepositAssignments: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "deposit_maximum_assignments", + Help: "maximum deposit assignments per transaction", + }), + inflationIntervalRate: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "inflation_interval_rate", + Help: "RPL inflation rate per interval", + }), + inflationIntervalBlocks: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "inflation_interval_blocks", + Help: "RPL inflation interval in blocks", + }), + inflationStartBlock: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "inflation_start_block", + Help: "RPL inflation start block", + }), + minipoolAmounts: promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "minipool_amounts", + Help: "amount settings for rocketpool minipool", + }, + []string{"category"}, + ), + minipoolLaunchTimeout: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "minipool_launch_timeout_blocks", + Help: "Timeout period in blocks for prelaunch minipools to launch", + }), + nodeConsensusThreshold: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "network_node_consensus_threshold", + Help: "threshold of trusted nodes that must reach consensus on oracle data to commit it", + }), + submitBalancesFrequency: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "network_submit_balances_frequency_blocks", + Help: "frequency in blocks at which network balances should be submitted by trusted nodes", + }), + submitPricesFrequency: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "network_submit_prices_frequency_blocks", + Help: "frequency in blocks at which network prices should be submitted by trusted nodes", + }), + networkNodeFee: promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "network_node_fee_rates", + Help: "node fee settings", + }, + []string{"type"}, + ), + targetRethCollateralRate: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "network_target_reth_collateral_rate", + Help: "target collateralization rate for the rETH contract as a fraction", + }), + nodeMinimumPerMinipoolStake: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "node_minimum_per_minipool_stake", + Help: "minimum RPL stake per minipool as a fraction of assigned user ETH", + }), + nodeMaximumPerMinipoolStake: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "node_maximum_per_minipool_stake", + Help: "maximum RPL stake per minipool as a fraction of assigned user ETH", + }), + rewardsClaimerPerc: promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "rewards_claimer_perc", + Help: "claim amount for a claimer as a fraction", + }, + []string{"contract"}, + ), + rewardsClaimerPercUpdate: promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "rewards_claimer_perc_update_block", + Help: "block that a claimer's share was last updated at", + }, + []string{"contract"}, + ), + membersQuorum: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "members_quorum", + Help: "Member proposal quorum threshold", + }), + membersRPLBond: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "members_bond_rpl", + Help: "RPL bond required for a member", + }), + membersMinipoolUnbondedMax: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "members_max_unbonded_minipool_count", + Help: "maximum number of unbonded minipools a member can run", + }), + membersChallengeCooldown: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "members_challenge_cooldown_blocks", + Help: "period a member must wait for before submitting another challenge, in blocks", + }), + membersChallengeWindow: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "members_challenge_window_blocks", + Help: "period during which a member can respond to a challenge, in blocks", + }), + membersChallengeCost: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "members_challenge_cost_eth", + Help: "The fee for a non-member to challenge a member, in eth", + }), + proposalCooldown: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "proposal_cooldown_blocks", + Help: "cooldown period a member must wait after making a proposal before making another in blocks", + }), + proposalVoteBlocks: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "proposal_vote_duration_blocks", + Help: "period a proposal can be voted on for in blocks", + }), + proposalVoteDelayBlocks: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "proposal_vote_delay_blocks", + Help: "delay after creation before a proposal can be voted on in blocks", + }), + proposalExecuteBlocks: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "proposal_execute_blocks", + Help: "period during which a passed proposal can be executed in blocks", + }), + proposalActionBlocks: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "settings", + Name: "proposal_action_blocks", + Help: "period during which an action can be performed on an executed proposal in blocks", + }), + } + + p := &settingsMetricsProcess { + rp: rp, + bc: bc, + metrics: metrics, + logger: logger, + } + + logger.Printlnf("Exit newSettingsMetricsProcess") + return p, nil +} + + +// Update settings metrics +func (p *settingsMetricsProcess) updateMetrics() error { + p.logger.Printlnf("Enter settings updateMetrics") + + var wg errgroup.Group + wg.Go(func() error { + err := p.updateAuctionSettings() + return err + }) + wg.Go(func() error { + err := p.updateDepositSettings() + return err + }) + wg.Go(func() error { + err := p.updateInflationSettings() + return err + }) + wg.Go(func() error { + err := p.updateMinipoolSettings() + return err + }) + wg.Go(func() error { + err := p.updateNetworkSettings() + return err + }) + wg.Go(func() error { + err := p.updateNodeSettings() + return err + }) + wg.Go(func() error { + err := p.updateRewardsSettings() + return err + }) + wg.Go(func() error { + err := p.updateMembersSettings() + return err + }) + wg.Go(func() error { + err := p.updateProposalsSettings() + return err + }) + + // Wait + err := wg.Wait() + p.logger.Printlnf("Exit settings updateMetrics") + + return err +} + + +func (p *settingsMetricsProcess) updateAuctionSettings() error { + var createLotEnabled, bidOnLotEnabled bool + var lotMinimumEthValue, lotMaximumEthValue *big.Int + var lotDuration uint64 + var lotStartingPrice, lotReservePrice float64 + + var wg errgroup.Group + + // Auction settings + wg.Go(func() error { + response, err := protocol.GetCreateLotEnabled(p.rp, nil) + if err == nil { + createLotEnabled = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetBidOnLotEnabled(p.rp, nil) + if err == nil { + bidOnLotEnabled = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetLotMinimumEthValue(p.rp, nil) + if err == nil { + lotMinimumEthValue = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetLotMaximumEthValue(p.rp, nil) + if err == nil { + lotMaximumEthValue = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetLotDuration(p.rp, nil) + if err == nil { + lotDuration = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetLotStartingPriceRatio(p.rp, nil) + if err == nil { + lotStartingPrice = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetLotReservePriceRatio(p.rp, nil) + if err == nil { + lotReservePrice = response + } + return err + }) + + // Wait for data + if err := wg.Wait(); err != nil { + return err + } + + p.metrics.flags.With(prometheus.Labels{"flag":"CreateLotEnabled"}).Set(float64(B2i(createLotEnabled))) + p.metrics.flags.With(prometheus.Labels{"flag":"BidOnLotEnabled"}).Set(float64(B2i(bidOnLotEnabled))) + p.metrics.lotMinimumEth.Set(eth.WeiToEth(lotMinimumEthValue)) + p.metrics.lotMaximumEth.Set(eth.WeiToEth(lotMaximumEthValue)) + p.metrics.lotDuration.Set(float64(lotDuration)) + p.metrics.lotStartingPrice.Set(lotStartingPrice) + p.metrics.lotReservePrice.Set(lotReservePrice) + + return nil +} + + +func (p *settingsMetricsProcess) updateDepositSettings() error { + var maximumDepositAssignments uint64 + var depositEnabled, assignDepositEnabled bool + var minimumDeposit, maximumDepositPoolSize *big.Int + + var wg errgroup.Group + + // Deposit settings + wg.Go(func() error { + response, err := protocol.GetDepositEnabled(p.rp, nil) + if err == nil { + depositEnabled = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetAssignDepositsEnabled(p.rp, nil) + if err == nil { + assignDepositEnabled = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetMinimumDeposit(p.rp, nil) + if err == nil { + minimumDeposit = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetMaximumDepositPoolSize(p.rp, nil) + if err == nil { + maximumDepositPoolSize = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetMaximumDepositAssignments(p.rp, nil) + if err == nil { + maximumDepositAssignments = response + } + return err + }) + + // Wait for data + if err := wg.Wait(); err != nil { + return err + } + + p.metrics.flags.With(prometheus.Labels{"flag":"DepositEnabled"}).Set(float64(B2i(depositEnabled))) + p.metrics.flags.With(prometheus.Labels{"flag":"DepositAssignmentsEnabled"}).Set(float64(B2i(assignDepositEnabled))) + p.metrics.minimumDeposit.Set(eth.WeiToEth(minimumDeposit)) + p.metrics.maximumDepositPoolSize.Set(eth.WeiToEth(maximumDepositPoolSize)) + p.metrics.maximumDepositAssignments.Set(float64(maximumDepositAssignments)) + + return nil +} + + +func (p *settingsMetricsProcess) updateInflationSettings() error { + var inflationIntervalRate float64 + var inflationIntervalBlocks, inflationStartBlock uint64 + + var wg errgroup.Group + + // Inflation settings + wg.Go(func() error { + response, err := protocol.GetInflationIntervalRate(p.rp, nil) + if err == nil { + inflationIntervalRate = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetInflationIntervalBlocks(p.rp, nil) + if err == nil { + inflationIntervalBlocks = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetInflationStartBlock(p.rp, nil) + if err == nil { + inflationStartBlock = response + } + return err + }) + + // Wait for data + if err := wg.Wait(); err != nil { + return err + } + + p.metrics.inflationIntervalRate.Set(inflationIntervalRate) + p.metrics.inflationIntervalBlocks.Set(float64(inflationIntervalBlocks)) + p.metrics.inflationStartBlock.Set(float64(inflationStartBlock)) + + return nil +} + + +func (p *settingsMetricsProcess) updateMinipoolSettings() error { + var minipoolSubmitWithdrawEnabled bool + var minipoolLaunchBalance, minipoolFullDepositNodeAmount, minipoolHalfDepositNodeAmount, minipoolEmptyDepositNodeAmount *big.Int + var minipoolFullDepositUserAmount, minipoolHalfDepositUserAmount, minipoolEmptyDepositUserAmount *big.Int + var minipoolLaunchTimeout uint64 + + var wg errgroup.Group + + // Minipool settings + wg.Go(func() error { + response, err := protocol.GetMinipoolLaunchBalance(p.rp, nil) + if err == nil { + minipoolLaunchBalance = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetMinipoolFullDepositNodeAmount(p.rp, nil) + if err == nil { + minipoolFullDepositNodeAmount = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetMinipoolHalfDepositNodeAmount(p.rp, nil) + if err == nil { + minipoolHalfDepositNodeAmount = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetMinipoolEmptyDepositNodeAmount(p.rp, nil) + if err == nil { + minipoolEmptyDepositNodeAmount = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetMinipoolFullDepositUserAmount(p.rp, nil) + if err == nil { + minipoolFullDepositUserAmount = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetMinipoolHalfDepositUserAmount(p.rp, nil) + if err == nil { + minipoolHalfDepositUserAmount = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetMinipoolEmptyDepositUserAmount(p.rp, nil) + if err == nil { + minipoolEmptyDepositUserAmount = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetMinipoolSubmitWithdrawableEnabled(p.rp, nil) + if err == nil { + minipoolSubmitWithdrawEnabled = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetMinipoolLaunchTimeout(p.rp, nil) + if err == nil { + minipoolLaunchTimeout = response + } + return err + }) + + // Wait for data + if err := wg.Wait(); err != nil { + return err + } + + p.metrics.minipoolAmounts.With(prometheus.Labels{"category":"LaunchBalance"}).Set(eth.WeiToEth(minipoolLaunchBalance)) + p.metrics.minipoolAmounts.With(prometheus.Labels{"category":"FullDepositNodeAmount"}).Set(eth.WeiToEth(minipoolFullDepositNodeAmount)) + p.metrics.minipoolAmounts.With(prometheus.Labels{"category":"HalfDepositNodeAmount"}).Set(eth.WeiToEth(minipoolHalfDepositNodeAmount)) + p.metrics.minipoolAmounts.With(prometheus.Labels{"category":"EmptyDepositNodeAmount"}).Set(eth.WeiToEth(minipoolEmptyDepositNodeAmount)) + p.metrics.minipoolAmounts.With(prometheus.Labels{"category":"FullDepositUserAmount"}).Set(eth.WeiToEth(minipoolFullDepositUserAmount)) + p.metrics.minipoolAmounts.With(prometheus.Labels{"category":"HalfDepositUserAmount"}).Set(eth.WeiToEth(minipoolHalfDepositUserAmount)) + p.metrics.minipoolAmounts.With(prometheus.Labels{"category":"EmptyDepositUserAmount"}).Set(eth.WeiToEth(minipoolEmptyDepositUserAmount)) + p.metrics.flags.With(prometheus.Labels{"flag":"MinipoolSubmitWithdrawEnabled"}).Set(float64(B2i(minipoolSubmitWithdrawEnabled))) + p.metrics.minipoolLaunchTimeout.Set(float64(minipoolLaunchTimeout)) + + return nil +} + + +func (p *settingsMetricsProcess) updateNetworkSettings() error { + var submitBalancesEnabled, submitPricesEnabled, processWithdrawalEnabled bool + var nodeConsensusThreshold, targetRethCollateralRate float64 + var submitBalancesFrequency, submitPricesFrequency uint64 + var minimumNodeFee, targetNodeFee, maximumNodeFee float64 + var nodeFeeDemandRange *big.Int + + var wg errgroup.Group + + // Network + wg.Go(func() error { + response, err := protocol.GetNodeConsensusThreshold(p.rp, nil) + if err == nil { + nodeConsensusThreshold = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetSubmitBalancesEnabled(p.rp, nil) + if err == nil { + submitBalancesEnabled = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetSubmitBalancesFrequency(p.rp, nil) + if err == nil { + submitBalancesFrequency = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetSubmitPricesEnabled(p.rp, nil) + if err == nil { + submitPricesEnabled = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetSubmitPricesFrequency(p.rp, nil) + if err == nil { + submitPricesFrequency = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetProcessWithdrawalsEnabled(p.rp, nil) + if err == nil { + processWithdrawalEnabled = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetMinimumNodeFee(p.rp, nil) + if err == nil { + minimumNodeFee = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetTargetNodeFee(p.rp, nil) + if err == nil { + targetNodeFee = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetMaximumNodeFee(p.rp, nil) + if err == nil { + maximumNodeFee = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetNodeFeeDemandRange(p.rp, nil) + if err == nil { + nodeFeeDemandRange = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetTargetRethCollateralRate(p.rp, nil) + if err == nil { + targetRethCollateralRate = response + } + return err + }) + + // Wait for data + if err := wg.Wait(); err != nil { + return err + } + + p.metrics.nodeConsensusThreshold.Set(nodeConsensusThreshold) + p.metrics.flags.With(prometheus.Labels{"flag":"SubmitBalancesEnabled"}).Set(float64(B2i(submitBalancesEnabled))) + p.metrics.submitBalancesFrequency.Set(float64(submitBalancesFrequency)) + p.metrics.flags.With(prometheus.Labels{"flag":"SubmitPricesEnabled"}).Set(float64(B2i(submitPricesEnabled))) + p.metrics.submitPricesFrequency.Set(float64(submitPricesFrequency)) + p.metrics.flags.With(prometheus.Labels{"flag":"ProcessWithdrawalEnabled"}).Set(float64(B2i(processWithdrawalEnabled))) + p.metrics.networkNodeFee.With(prometheus.Labels{"type":"MinimumNodeFee"}).Set(minimumNodeFee) + p.metrics.networkNodeFee.With(prometheus.Labels{"type":"TargetNodeFee"}).Set(targetNodeFee) + p.metrics.networkNodeFee.With(prometheus.Labels{"type":"MaximumNodeFee"}).Set(maximumNodeFee) + p.metrics.networkNodeFee.With(prometheus.Labels{"type":"DemandRange"}).Set(eth.WeiToEth(nodeFeeDemandRange)) + p.metrics.targetRethCollateralRate.Set(targetRethCollateralRate) + + return nil +} + + +func (p *settingsMetricsProcess) updateNodeSettings() error { + var nodeRegistrationEnabled, nodeDepositEnabled bool + var minimumPerMinipoolStake, maximumPerMinipoolStake float64 + + var wg errgroup.Group + + // Node settings + wg.Go(func() error { + response, err := protocol.GetNodeRegistrationEnabled(p.rp, nil) + if err == nil { + nodeRegistrationEnabled = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetNodeDepositEnabled(p.rp, nil) + if err == nil { + nodeDepositEnabled = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetMinimumPerMinipoolStake(p.rp, nil) + if err == nil { + minimumPerMinipoolStake = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetMaximumPerMinipoolStake(p.rp, nil) + if err == nil { + maximumPerMinipoolStake = response + } + return err + }) + + // Wait for data + if err := wg.Wait(); err != nil { + return err + } + + p.metrics.flags.With(prometheus.Labels{"flag":"NodeRegistrationEnabled"}).Set(float64(B2i(nodeRegistrationEnabled))) + p.metrics.flags.With(prometheus.Labels{"flag":"NodeDepositEnabled"}).Set(float64(B2i(nodeDepositEnabled))) + p.metrics.nodeMinimumPerMinipoolStake.Set(minimumPerMinipoolStake) + p.metrics.nodeMaximumPerMinipoolStake.Set(maximumPerMinipoolStake) + + return nil +} + + +func (p *settingsMetricsProcess) updateRewardsSettings() error { + var rewardsClaimerPercDAO, rewardsClaimerPercNode, rewardsClaimerPercTrustedNode, rewardsClaimerPercTotal float64 + var rewardsClaimerPercUpdateDAO, rewardsClaimerPercUpdateNode, rewardsClaimerPercUpdateTrustedNode uint64 + + var wg errgroup.Group + + // Rewards settings + wg.Go(func() error { + response, err := protocol.GetRewardsClaimerPerc(p.rp, "rocketClaimDAO", nil) + if err == nil { + rewardsClaimerPercDAO = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetRewardsClaimerPerc(p.rp, "rocketClaimNode", nil) + if err == nil { + rewardsClaimerPercNode = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetRewardsClaimerPerc(p.rp, "rocketClaimTrustedNode", nil) + if err == nil { + rewardsClaimerPercTrustedNode = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetRewardsClaimerPercBlockUpdated(p.rp, "rocketClaimDAO", nil) + if err == nil { + rewardsClaimerPercUpdateDAO = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetRewardsClaimerPercBlockUpdated(p.rp, "rocketClaimNode", nil) + if err == nil { + rewardsClaimerPercUpdateNode = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetRewardsClaimerPercBlockUpdated(p.rp, "rocketClaimTrustedNode", nil) + if err == nil { + rewardsClaimerPercUpdateTrustedNode = response + } + return err + }) + wg.Go(func() error { + response, err := protocol.GetRewardsClaimersPercTotal(p.rp, nil) + if err == nil { + rewardsClaimerPercTotal = response + } + return err + }) + + // Wait for data + if err := wg.Wait(); err != nil { + return err + } + + p.metrics.rewardsClaimerPerc.With(prometheus.Labels{"contract":"rocketClaimDAO"}).Set(rewardsClaimerPercDAO) + p.metrics.rewardsClaimerPerc.With(prometheus.Labels{"contract":"rocketClaimNode"}).Set(rewardsClaimerPercNode) + p.metrics.rewardsClaimerPerc.With(prometheus.Labels{"contract":"rocketClaimTrustedNode"}).Set(rewardsClaimerPercTrustedNode) + p.metrics.rewardsClaimerPercUpdate.With(prometheus.Labels{"contract":"rocketClaimDAO"}).Set(float64(rewardsClaimerPercUpdateDAO)) + p.metrics.rewardsClaimerPercUpdate.With(prometheus.Labels{"contract":"rocketClaimNode"}).Set(float64(rewardsClaimerPercUpdateNode)) + p.metrics.rewardsClaimerPercUpdate.With(prometheus.Labels{"contract":"rocketClaimTrustedNode"}).Set(float64(rewardsClaimerPercUpdateTrustedNode)) + p.metrics.rewardsClaimerPerc.With(prometheus.Labels{"contract":"Total"}).Set(rewardsClaimerPercTotal) + + return nil +} + + +func (p *settingsMetricsProcess) updateMembersSettings() error { + var membersQuorum float64 + var membersRplBond, membersChallendgeCost *big.Int + var membersMinipoolUnbondedMax, membersChallengeCooldown, membersChallengeWindow uint64 + + var wg errgroup.Group + + // Members settings + wg.Go(func() error { + response, err := trustednode.GetQuorum(p.rp, nil) + if err == nil { + membersQuorum = response + } + return err + }) + wg.Go(func() error { + response, err := trustednode.GetRPLBond(p.rp, nil) + if err == nil { + membersRplBond = response + } + return err + }) + wg.Go(func() error { + response, err := trustednode.GetMinipoolUnbondedMax(p.rp, nil) + if err == nil { + membersMinipoolUnbondedMax = response + } + return err + }) + wg.Go(func() error { + response, err := trustednode.GetChallengeCooldown(p.rp, nil) + if err == nil { + membersChallengeCooldown = response + } + return err + }) + wg.Go(func() error { + response, err := trustednode.GetChallengeWindow(p.rp, nil) + if err == nil { + membersChallengeWindow = response + } + return err + }) + wg.Go(func() error { + response, err := trustednode.GetChallengeCost(p.rp, nil) + if err == nil { + membersChallendgeCost = response + } + return err + }) + + // Wait for data + if err := wg.Wait(); err != nil { + return err + } + + p.metrics.membersQuorum.Set(membersQuorum) + p.metrics.membersRPLBond.Set(eth.WeiToEth(membersRplBond)) + p.metrics.membersMinipoolUnbondedMax.Set(float64(membersMinipoolUnbondedMax)) + p.metrics.membersChallengeCooldown.Set(float64(membersChallengeCooldown)) + p.metrics.membersChallengeWindow.Set(float64(membersChallengeWindow)) + p.metrics.membersChallengeCost.Set(eth.WeiToEth(membersChallendgeCost)) + + return nil +} + + +func (p *settingsMetricsProcess) updateProposalsSettings() error { + var proposalsCooldown, proposalsVoteBlocks, proposalsVoteDelayBlocks, proposalsExecuteBlocks, proposalsActionBlocks uint64 + + var wg errgroup.Group + + // Proposals settings + wg.Go(func() error { + response, err := trustednode.GetProposalCooldown(p.rp, nil) + if err == nil { + proposalsCooldown = response + } + return err + }) + wg.Go(func() error { + response, err := trustednode.GetProposalVoteBlocks(p.rp, nil) + if err == nil { + proposalsVoteBlocks = response + } + return err + }) + wg.Go(func() error { + response, err := trustednode.GetProposalVoteDelayBlocks(p.rp, nil) + if err == nil { + proposalsVoteDelayBlocks = response + } + return err + }) + wg.Go(func() error { + response, err := trustednode.GetProposalExecuteBlocks(p.rp, nil) + if err == nil { + proposalsExecuteBlocks = response + } + return err + }) + wg.Go(func() error { + response, err := trustednode.GetProposalActionBlocks(p.rp, nil) + if err == nil { + proposalsActionBlocks = response + } + return err + }) + + // Wait for data + if err := wg.Wait(); err != nil { + return err + } + + p.metrics.proposalCooldown.Set(float64(proposalsCooldown)) + p.metrics.proposalVoteBlocks.Set(float64(proposalsVoteBlocks)) + p.metrics.proposalVoteDelayBlocks.Set(float64(proposalsVoteDelayBlocks)) + p.metrics.proposalExecuteBlocks.Set(float64(proposalsExecuteBlocks)) + p.metrics.proposalActionBlocks.Set(float64(proposalsActionBlocks)) + + return nil +} + diff --git a/rocketpool/metrics/tokens.go b/rocketpool/metrics/tokens.go new file mode 100644 index 000000000..1907c8c4f --- /dev/null +++ b/rocketpool/metrics/tokens.go @@ -0,0 +1,124 @@ +package metrics + +import ( + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/urfave/cli" + + "github.com/rocket-pool/rocketpool-go/rocketpool" + "github.com/rocket-pool/rocketpool-go/tokens" + "github.com/rocket-pool/rocketpool-go/utils/eth" + "github.com/rocket-pool/smartnode/shared/services" + "github.com/rocket-pool/smartnode/shared/services/beacon" + "github.com/rocket-pool/smartnode/shared/utils/log" +) + + +type tokensGauges struct { + tokenSupply *prometheus.GaugeVec + something prometheus.Gauge +} + + +// tokens metrics process +type tokensMetricsProcess struct { + rp *rocketpool.RocketPool + bc beacon.Client + metrics tokensGauges + logger log.ColorLogger +} + + + + +// Start tokens metrics process +func startTokensMetricsProcess(c *cli.Context, interval time.Duration, logger log.ColorLogger) { + + logger.Printlnf("Enter startTokensMetricsProcess") + timer := time.NewTicker(interval) + var p *tokensMetricsProcess + var err error + // put create process in a loop because it may fail initially + for ; true; <- timer.C { + p, err = newTokensMetricsProcess(c, logger) + if p != nil && err == nil { + break; + } + } + + // Update metrics on interval + for ; true; <- timer.C { + err = p.updateMetrics() + if err != nil { + // print error here instead of exit + logger.Printlnf("Error in updateMetrics: %w", err) + } + } + logger.Printlnf("Exit startTokensMetricsProcess") +} + + +// Create new tokensMetricsProcess object +func newTokensMetricsProcess(c *cli.Context, logger log.ColorLogger) (*tokensMetricsProcess, error) { + + logger.Printlnf("Enter newTokensMetricsProcess") + if err := services.RequireRocketStorage(c); err != nil { return nil, err } + if err := services.RequireBeaconClientSynced(c); err != nil { return nil, err } + rp, err := services.GetRocketPool(c) + if err != nil { return nil, err } + bc, err := services.GetBeaconClient(c) + if err != nil { return nil, err } + + // Initialise metrics + metrics := tokensGauges { + tokenSupply: promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "tokens", + Name: "supply_count", + Help: "total supply of token", + }, + []string{"token"}, + ), + something: promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "rocketpool", + Subsystem: "tokens", + Name: "something", + Help: "something", + }), + } + + p := &tokensMetricsProcess { + rp: rp, + bc: bc, + //account: account, + metrics: metrics, + logger: logger, + } + + logger.Printlnf("Exit newTokensMetricsProcess") + return p, nil +} + + +// Update tokens metrics +func (p *tokensMetricsProcess) updateMetrics() error { + p.logger.Printlnf("Enter tokens updateMetrics") + + rethSupply, err := tokens.GetRETHTotalSupply(p.rp, nil) + rplFixedSupply, err := tokens.GetFixedSupplyRPLTotalSupply(p.rp, nil) + rplSupply, err := tokens.GetRPLTotalSupply(p.rp, nil) + if err != nil { return err } + + p.metrics.tokenSupply.With(prometheus.Labels{"token":"rETH"}).Set(eth.WeiToEth(rethSupply)) + p.metrics.tokenSupply.With(prometheus.Labels{"token":"fixed-supply RPL"}).Set(eth.WeiToEth(rplFixedSupply)) + p.metrics.tokenSupply.With(prometheus.Labels{"token":"RPL"}).Set(eth.WeiToEth(rplSupply)) + + p.logger.Printlnf("Exit tokens updateMetrics") + return err +} + + + diff --git a/rocketpool/metrics/utils.go b/rocketpool/metrics/utils.go new file mode 100644 index 000000000..e228b4066 --- /dev/null +++ b/rocketpool/metrics/utils.go @@ -0,0 +1,9 @@ +package metrics + +func B2i(b bool) int8 { + if b { + return 1 + } + return 0 +} + diff --git a/rocketpool/rocketpool.go b/rocketpool/rocketpool.go index e2fe0bca8..7a9acbc4a 100644 --- a/rocketpool/rocketpool.go +++ b/rocketpool/rocketpool.go @@ -9,6 +9,7 @@ import ( "github.com/rocket-pool/smartnode/rocketpool/api" "github.com/rocket-pool/smartnode/rocketpool/node" "github.com/rocket-pool/smartnode/rocketpool/watchtower" + "github.com/rocket-pool/smartnode/rocketpool/metrics" apiutils "github.com/rocket-pool/smartnode/shared/utils/api" ) @@ -93,6 +94,7 @@ func main() { api.RegisterCommands(app, "api", []string{"a"}) node.RegisterCommands(app, "node", []string{"n"}) watchtower.RegisterCommands(app, "watchtower", []string{"w"}) + metrics.RegisterCommands(app, "metrics", []string{"t"}) // Get command being run var commandName string diff --git a/shared/services/rocketpool/client.go b/shared/services/rocketpool/client.go index defb6aa48..43a15442c 100644 --- a/shared/services/rocketpool/client.go +++ b/shared/services/rocketpool/client.go @@ -28,6 +28,7 @@ const ( ComposeFile = "docker-compose.yml" APIContainerSuffix = "_api" + MetricsContainerSuffix = "_metrics" APIBinPath = "/go/bin/rocketpool" DebugColor = color.FgYellow @@ -273,6 +274,24 @@ func (c *Client) PrintServiceStats(composeFiles []string) error { } +func (c *Client) PrintMetricsOutput() error { + + var cmd1 string + if c.daemonPath == "" { + // get metrics container IP address + metricsContainerName, err := c.getMetricsContainerName() + if err != nil { return err } + cmd1 = fmt.Sprintf("METRICS_IP_ADDRESS=`docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' %s`;", metricsContainerName) + } else { + cmd1 = "METRICS_IP_ADDRESS=localhost;" + } + cmd2 := "curl --silent --show-error http://$METRICS_IP_ADDRESS:2112/metrics" + cmd := cmd1 + cmd2 + // Print metrics output + return c.printOutput(cmd) +} + + // Get the Rocket Pool service version func (c *Client) GetServiceVersion() (string, error) { @@ -428,6 +447,18 @@ func (c *Client) getAPIContainerName() (string, error) { } +func (c *Client) getMetricsContainerName() (string, error) { + cfg, err := c.LoadMergedConfig() + if err != nil { + return "", err + } + if cfg.Smartnode.ProjectName == "" { + return "", errors.New("Rocket Pool docker project name not set") + } + return cfg.Smartnode.ProjectName + MetricsContainerSuffix, nil +} + + // Get gas price & limit flags func (c *Client) getGasOpts() string { var opts string diff --git a/shared/services/rocketpool/minipool.go b/shared/services/rocketpool/minipool.go index 53dccb432..4f61c38c5 100644 --- a/shared/services/rocketpool/minipool.go +++ b/shared/services/rocketpool/minipool.go @@ -32,6 +32,23 @@ func (c *Client) MinipoolStatus() (api.MinipoolStatusResponse, error) { } +// Get minipool leader +func (c *Client) MinipoolLeader() (api.MinipoolStatusResponse, error) { + responseBytes, err := c.callAPI("minipool leader") + if err != nil { + return api.MinipoolStatusResponse{}, fmt.Errorf("Could not get minipool status: %w", err) + } + var response api.MinipoolStatusResponse + if err := json.Unmarshal(responseBytes, &response); err != nil { + return api.MinipoolStatusResponse{}, fmt.Errorf("Could not decode minipool status response: %w", err) + } + if response.Error != "" { + return api.MinipoolStatusResponse{}, fmt.Errorf("Could not get minipool status: %s", response.Error) + } + return response, nil +} + + // Check whether a minipool is eligible for a refund func (c *Client) CanRefundMinipool(address common.Address) (api.CanRefundMinipoolResponse, error) { responseBytes, err := c.callAPI(fmt.Sprintf("minipool can-refund %s", address.Hex())) diff --git a/shared/services/rocketpool/node.go b/shared/services/rocketpool/node.go index 60043bd3f..1af494eac 100644 --- a/shared/services/rocketpool/node.go +++ b/shared/services/rocketpool/node.go @@ -30,6 +30,23 @@ func (c *Client) NodeStatus() (api.NodeStatusResponse, error) { } +// Get node status +func (c *Client) NodeLeader() (api.NodeLeaderResponse, error) { + responseBytes, err := c.callAPI("node leader") + if err != nil { + return api.NodeLeaderResponse{}, fmt.Errorf("Could not get node leader: %w", err) + } + var response api.NodeLeaderResponse + if err := json.Unmarshal(responseBytes, &response); err != nil { + return api.NodeLeaderResponse{}, fmt.Errorf("Could not decode node leader response: %w", err) + } + if response.Error != "" { + return api.NodeLeaderResponse{}, fmt.Errorf("Could not get node leader: %s", response.Error) + } + return response, nil +} + + // Check whether the node can be registered func (c *Client) CanRegisterNode() (api.CanRegisterNodeResponse, error) { responseBytes, err := c.callAPI("node can-register") diff --git a/shared/types/api/node.go b/shared/types/api/node.go index 9bdebfeaa..bf98eac49 100644 --- a/shared/types/api/node.go +++ b/shared/types/api/node.go @@ -38,6 +38,22 @@ type NodeStatusResponse struct { } +type NodeLeaderResponse struct { + Status string `json:"status"` + Error string `json:"error"` + Nodes []NodeRank `json:"nodes"` +} + +type NodeRank struct { + Rank int `json:"rank"` + Address common.Address `json:"address"` + Registered bool `json:"registered"` + TimezoneLocation string `json:"timezoneLocation"` + Score *big.Int `json:"score"` + Details []MinipoolDetails `json:"details"` +} + + type CanRegisterNodeResponse struct { Status string `json:"status"` Error string `json:"error"`