From dd6f72c82258ec585313debb41d8f3bb973c7b49 Mon Sep 17 00:00:00 2001 From: Matt Johnstone Date: Tue, 8 Oct 2024 17:16:10 +0200 Subject: [PATCH 1/3] refactored getResponse to pure function --- pkg/rpc/client.go | 38 +++++++++++++++++--------------------- pkg/rpc/responses.go | 15 ++++++--------- 2 files changed, 23 insertions(+), 30 deletions(-) diff --git a/pkg/rpc/client.go b/pkg/rpc/client.go index b69417b..22fef7e 100644 --- a/pkg/rpc/client.go +++ b/pkg/rpc/client.go @@ -16,11 +16,6 @@ type ( rpcAddr string } - rpcError struct { - Message string `json:"message"` - Code int64 `json:"code"` - } - rpcRequest struct { Version string `json:"jsonrpc"` ID int `json:"id"` @@ -99,7 +94,9 @@ func NewRPCClient(rpcAddr string) *Client { return &Client{httpClient: http.Client{}, rpcAddr: rpcAddr} } -func (c *Client) getResponse(ctx context.Context, method string, params []any, result HasRPCError) error { +func getResponse[T any]( + ctx context.Context, httpClient http.Client, url string, method string, params []any, rpcResponse *response[T], +) error { // format request: request := &rpcRequest{Version: "2.0", ID: 1, Method: method, Params: params} buffer, err := json.Marshal(request) @@ -109,13 +106,13 @@ func (c *Client) getResponse(ctx context.Context, method string, params []any, r klog.V(2).Infof("jsonrpc request: %s", string(buffer)) // make request: - req, err := http.NewRequestWithContext(ctx, "POST", c.rpcAddr, bytes.NewBuffer(buffer)) + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(buffer)) if err != nil { klog.Fatalf("failed to create request: %v", err) } req.Header.Set("content-type", "application/json") - resp, err := c.httpClient.Do(req) + resp, err := httpClient.Do(req) if err != nil { return fmt.Errorf("%s RPC call failed: %w", method, err) } @@ -130,15 +127,14 @@ func (c *Client) getResponse(ctx context.Context, method string, params []any, r klog.V(2).Infof("%s response: %v", method, string(body)) // unmarshal the response into the predicted format - if err = json.Unmarshal(body, result); err != nil { + if err = json.Unmarshal(body, rpcResponse); err != nil { return fmt.Errorf("failed to decode %s response body: %w", method, err) } // last error check: - if result.getError().Code != 0 { - return fmt.Errorf("RPC error: %d %v", result.getError().Code, result.getError().Message) + if rpcResponse.Error.Code != 0 { + return fmt.Errorf("RPC error: %d %v", rpcResponse.Error.Code, rpcResponse.Error.Message) } - return nil } @@ -146,7 +142,7 @@ func (c *Client) getResponse(ctx context.Context, method string, params []any, r // See API docs: https://solana.com/docs/rpc/http/getepochinfo func (c *Client) GetEpochInfo(ctx context.Context, commitment Commitment) (*EpochInfo, error) { var resp response[EpochInfo] - if err := c.getResponse(ctx, "getEpochInfo", []any{commitment}, &resp); err != nil { + if err := getResponse(ctx, c.httpClient, c.rpcAddr, "getEpochInfo", []any{commitment}, &resp); err != nil { return nil, err } return &resp.Result, nil @@ -164,7 +160,7 @@ func (c *Client) GetVoteAccounts( } var resp response[VoteAccounts] - if err := c.getResponse(ctx, "getVoteAccounts", []any{config}, &resp); err != nil { + if err := getResponse(ctx, c.httpClient, c.rpcAddr, "getVoteAccounts", []any{config}, &resp); err != nil { return nil, err } return &resp.Result, nil @@ -176,7 +172,7 @@ func (c *Client) GetVersion(ctx context.Context) (string, error) { var resp response[struct { Version string `json:"solana-core"` }] - if err := c.getResponse(ctx, "getVersion", []any{}, &resp); err != nil { + if err := getResponse(ctx, c.httpClient, c.rpcAddr, "getVersion", []any{}, &resp); err != nil { return "", err } return resp.Result.Version, nil @@ -187,7 +183,7 @@ func (c *Client) GetVersion(ctx context.Context) (string, error) { func (c *Client) GetSlot(ctx context.Context, commitment Commitment) (int64, error) { config := map[string]string{"commitment": string(commitment)} var resp response[int64] - if err := c.getResponse(ctx, "getSlot", []any{config}, &resp); err != nil { + if err := getResponse(ctx, c.httpClient, c.rpcAddr, "getSlot", []any{config}, &resp); err != nil { return 0, err } return resp.Result, nil @@ -223,7 +219,7 @@ func (c *Client) GetBlockProduction( // make request: var resp response[contextualResult[BlockProduction]] - if err := c.getResponse(ctx, "getBlockProduction", []any{config}, &resp); err != nil { + if err := getResponse(ctx, c.httpClient, c.rpcAddr, "getBlockProduction", []any{config}, &resp); err != nil { return nil, err } return &resp.Result.Value, nil @@ -234,7 +230,7 @@ func (c *Client) GetBlockProduction( func (c *Client) GetBalance(ctx context.Context, commitment Commitment, address string) (float64, error) { config := map[string]string{"commitment": string(commitment)} var resp response[contextualResult[int64]] - if err := c.getResponse(ctx, "getBalance", []any{address, config}, &resp); err != nil { + if err := getResponse(ctx, c.httpClient, c.rpcAddr, "getBalance", []any{address, config}, &resp); err != nil { return 0, err } return float64(resp.Result.Value) / float64(LamportsInSol), nil @@ -255,7 +251,7 @@ func (c *Client) GetInflationReward( } var resp response[[]InflationReward] - if err := c.getResponse(ctx, "getInflationReward", []any{addresses, config}, &resp); err != nil { + if err := getResponse(ctx, c.httpClient, c.rpcAddr, "getInflationReward", []any{addresses, config}, &resp); err != nil { return nil, err } return resp.Result, nil @@ -266,7 +262,7 @@ func (c *Client) GetInflationReward( func (c *Client) GetLeaderSchedule(ctx context.Context, commitment Commitment, slot int64) (map[string][]int64, error) { config := map[string]any{"commitment": string(commitment)} var resp response[map[string][]int64] - if err := c.getResponse(ctx, "getLeaderSchedule", []any{slot, config}, &resp); err != nil { + if err := getResponse(ctx, c.httpClient, c.rpcAddr, "getLeaderSchedule", []any{slot, config}, &resp); err != nil { return nil, err } return resp.Result, nil @@ -285,7 +281,7 @@ func (c *Client) GetBlock(ctx context.Context, commitment Commitment, slot int64 "rewards": true, // what we here for! } var resp response[Block] - if err := c.getResponse(ctx, "getBlock", []any{slot, config}, &resp); err != nil { + if err := getResponse(ctx, c.httpClient, c.rpcAddr, "getBlock", []any{slot, config}, &resp); err != nil { return nil, err } return &resp.Result, nil diff --git a/pkg/rpc/responses.go b/pkg/rpc/responses.go index c88f682..c888679 100644 --- a/pkg/rpc/responses.go +++ b/pkg/rpc/responses.go @@ -6,10 +6,15 @@ import ( ) type ( + RPCError struct { + Message string `json:"message"` + Code int64 `json:"code"` + } + response[T any] struct { jsonrpc string Result T `json:"result"` - Error rpcError `json:"error"` + Error RPCError `json:"error"` Id int `json:"id"` } @@ -104,11 +109,3 @@ func (hp *HostProduction) UnmarshalJSON(data []byte) error { hp.BlocksProduced = arr[1] return nil } - -func (r response[T]) getError() rpcError { - return r.Error -} - -type HasRPCError interface { - getError() rpcError -} From 4f2c927054f0d70986489bd9f996ace6f3120588 Mon Sep 17 00:00:00 2001 From: Matt Johnstone Date: Tue, 8 Oct 2024 18:10:34 +0200 Subject: [PATCH 2/3] added rpcerror handling to fee reward fetching --- cmd/solana_exporter/slots.go | 15 +++++++++++++-- pkg/rpc/client.go | 5 +++-- pkg/rpc/responses.go | 4 ++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/cmd/solana_exporter/slots.go b/cmd/solana_exporter/slots.go index 5f88c36..8eb4f51 100644 --- a/cmd/solana_exporter/slots.go +++ b/cmd/solana_exporter/slots.go @@ -2,8 +2,10 @@ package main import ( "context" + "errors" "fmt" "slices" + "strings" "time" "github.com/asymmetric-research/solana_exporter/pkg/rpc" @@ -131,7 +133,8 @@ func (c *SlotWatcher) WatchSlots(ctx context.Context, pace time.Duration) { ctx_, cancel := context.WithTimeout(ctx, httpTimeout) // TODO: separate fee-rewards watching from general slot watching, such that general slot watching commitment level can be dropped to confirmed - epochInfo, err := c.client.GetEpochInfo(ctx_, rpc.CommitmentFinalized) + commitment := rpc.CommitmentFinalized + epochInfo, err := c.client.GetEpochInfo(ctx_, commitment) cancel() if err != nil { klog.Errorf("Failed to get epoch info, bailing out: %v", err) @@ -149,7 +152,7 @@ func (c *SlotWatcher) WatchSlots(ctx context.Context, pace time.Duration) { // if we get here, then the tracking numbers are set, so this is a "normal" run. // start by checking if we have progressed since last run: if epochInfo.AbsoluteSlot <= c.slotWatermark { - klog.Infof("confirmed slot number has not advanced from %v, skipping", c.slotWatermark) + klog.Infof("%v slot number has not advanced from %v, skipping", commitment, c.slotWatermark) continue } @@ -331,6 +334,14 @@ func (c *SlotWatcher) fetchAndEmitSingleFeeReward( ) error { block, err := c.client.GetBlock(ctx, rpc.CommitmentConfirmed, slot) if err != nil { + var rpcError *rpc.RPCError + if errors.As(err, &rpcError) { + // this is the error code for slot was skipped: + if rpcError.Code == -32007 && strings.Contains(rpcError.Message, "skipped") { + klog.Infof("slot %v was skipped, no fee rewards.", slot) + return nil + } + } return err } diff --git a/pkg/rpc/client.go b/pkg/rpc/client.go index 22fef7e..fb9fe64 100644 --- a/pkg/rpc/client.go +++ b/pkg/rpc/client.go @@ -133,7 +133,7 @@ func getResponse[T any]( // last error check: if rpcResponse.Error.Code != 0 { - return fmt.Errorf("RPC error: %d %v", rpcResponse.Error.Code, rpcResponse.Error.Message) + return &rpcResponse.Error } return nil } @@ -272,7 +272,8 @@ func (c *Client) GetLeaderSchedule(ctx context.Context, commitment Commitment, s // See API docs: https://solana.com/docs/rpc/http/getblock func (c *Client) GetBlock(ctx context.Context, commitment Commitment, slot int64) (*Block, error) { if commitment == CommitmentProcessed { - klog.Fatalf("commitment %v is not supported for GetBlock", commitment) + // as per https://solana.com/docs/rpc/http/getblock + klog.Fatalf("commitment '%v' is not supported for GetBlock", CommitmentProcessed) } config := map[string]any{ "commitment": commitment, diff --git a/pkg/rpc/responses.go b/pkg/rpc/responses.go index c888679..f6cc3dd 100644 --- a/pkg/rpc/responses.go +++ b/pkg/rpc/responses.go @@ -96,6 +96,10 @@ type ( } ) +func (e *RPCError) Error() string { + return fmt.Sprintf("RPC Error (%d): %s", e.Code, e.Message) +} + func (hp *HostProduction) UnmarshalJSON(data []byte) error { var arr []int64 if err := json.Unmarshal(data, &arr); err != nil { From bc96a3ee11d5ffc033dc3e079e69df4f49ccedd7 Mon Sep 17 00:00:00 2001 From: Matt Johnstone Date: Mon, 14 Oct 2024 11:01:24 +0200 Subject: [PATCH 3/3] rpc error code constants --- cmd/solana_exporter/slots.go | 2 +- pkg/rpc/errors.go | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 pkg/rpc/errors.go diff --git a/cmd/solana_exporter/slots.go b/cmd/solana_exporter/slots.go index 8eb4f51..77017a1 100644 --- a/cmd/solana_exporter/slots.go +++ b/cmd/solana_exporter/slots.go @@ -337,7 +337,7 @@ func (c *SlotWatcher) fetchAndEmitSingleFeeReward( var rpcError *rpc.RPCError if errors.As(err, &rpcError) { // this is the error code for slot was skipped: - if rpcError.Code == -32007 && strings.Contains(rpcError.Message, "skipped") { + if rpcError.Code == rpc.SlotSkippedCode && strings.Contains(rpcError.Message, "skipped") { klog.Infof("slot %v was skipped, no fee rewards.", slot) return nil } diff --git a/pkg/rpc/errors.go b/pkg/rpc/errors.go new file mode 100644 index 0000000..5e3fb9f --- /dev/null +++ b/pkg/rpc/errors.go @@ -0,0 +1,23 @@ +package rpc + +// error codes: https://github.com/anza-xyz/agave/blob/489f483e1d7b30ef114e0123994818b2accfa389/rpc-client-api/src/custom_error.rs#L17 +const ( + BlockCleanedUpCode = -32001 + SendTransactionPreflightFailureCode = -32002 + TransactionSignatureVerificationFailureCode = -32003 + BlockNotAvailableCode = -32004 + NodeUnhealthyCode = -32005 + TransactionPrecompileVerificationFailureCode = -32006 + SlotSkippedCode = -32007 + NoSnapshotCode = -32008 + LongTermStorageSlotSkippedCode = -32009 + KeyExcludedFromSecondaryIndexCode = -32010 + TransactionHistoryNotAvailableCode = -32011 + ScanErrorCode = -32012 + TransactionSignatureLengthMismatchCode = -32013 + BlockStatusNotYetAvailableCode = -32014 + UnsupportedTransactionVersionCode = -32015 + MinContextSlotNotReachedCode = -32016 + EpochRewardsPeriodActiveCode = -32017 + SlotNotEpochBoundaryCode = -32018 +)