From 8f8a786113e05ededca4ee4298a2f3e97f1b2ca0 Mon Sep 17 00:00:00 2001 From: jacek-casper <145967538+jacek-casper@users.noreply.github.com> Date: Fri, 7 Jun 2024 18:54:23 +0100 Subject: [PATCH] Implement the reward endpoint (#321) * Implement a reward endpoint Signed-off-by: Jacek Malec <145967538+jacek-casper@users.noreply.github.com> * Map new errors * Error code update * Update error handling * Make errors more consistent --------- Signed-off-by: Jacek Malec <145967538+jacek-casper@users.noreply.github.com> --- Cargo.lock | 4 +- Cargo.toml | 4 +- resources/test/rpc_schema.json | 129 +++++++++++++++++++++++ rpc_sidecar/src/http_server.rs | 3 +- rpc_sidecar/src/node_client.rs | 38 ++++++- rpc_sidecar/src/rpcs/docs.rs | 8 +- rpc_sidecar/src/rpcs/error.rs | 12 +++ rpc_sidecar/src/rpcs/error_code.rs | 17 ++++ rpc_sidecar/src/rpcs/info.rs | 158 ++++++++++++++++++++++++++++- 9 files changed, 357 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9afe610a..804eeff4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -468,7 +468,7 @@ dependencies = [ [[package]] name = "casper-binary-port" version = "1.0.0" -source = "git+https://github.com/casper-network/casper-node.git?branch=feat-2.0#f803ee53db31edd5f7f3c1fa1e0ec0ea59550158" +source = "git+https://github.com/jacek-casper/casper-node.git?branch=reward-binary-request#41aea404afba337a4ef89fef6089a802228e5680" dependencies = [ "bincode", "bytes", @@ -670,7 +670,7 @@ dependencies = [ [[package]] name = "casper-types" version = "5.0.0" -source = "git+https://github.com/casper-network/casper-node.git?branch=feat-2.0#f803ee53db31edd5f7f3c1fa1e0ec0ea59550158" +source = "git+https://github.com/jacek-casper/casper-node.git?branch=reward-binary-request#41aea404afba337a4ef89fef6089a802228e5680" dependencies = [ "base16", "base64 0.13.1", diff --git a/Cargo.toml b/Cargo.toml index 4a8f6c46..bd8c00aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,8 +14,8 @@ members = [ anyhow = "1" async-stream = "0.3.4" async-trait = "0.1.77" -casper-types = { git = "https://github.com/casper-network/casper-node.git", branch = "feat-2.0" } -casper-binary-port = { git = "https://github.com/casper-network/casper-node.git", branch = "feat-2.0" } +casper-types = { git = "https://github.com/jacek-casper/casper-node.git", branch = "reward-binary-request" } +casper-binary-port = { git = "https://github.com/jacek-casper/casper-node.git", branch = "reward-binary-request" } casper-event-sidecar = { path = "./event_sidecar", version = "1.0.0" } casper-event-types = { path = "./types", version = "1.0.0" } casper-rpc-sidecar = { path = "./rpc_sidecar", version = "1.0.0" } diff --git a/resources/test/rpc_schema.json b/resources/test/rpc_schema.json index a8d13a23..6cb616c8 100644 --- a/resources/test/rpc_schema.json +++ b/resources/test/rpc_schema.json @@ -1513,6 +1513,106 @@ } ] }, + { + "name": "info_get_reward", + "summary": "returns the reward for a given era and a validator or a delegator", + "params": [ + { + "name": "validator", + "schema": { + "description": "The public key of the validator.", + "$ref": "#/components/schemas/PublicKey" + }, + "required": true + }, + { + "name": "era_identifier", + "schema": { + "description": "The era identifier. If `None`, the last finalized era is used.", + "anyOf": [ + { + "$ref": "#/components/schemas/EraIdentifier" + }, + { + "type": "null" + } + ] + }, + "required": false + }, + { + "name": "delegator", + "schema": { + "description": "The public key of the delegator. If `Some`, the rewards for the delegator are returned. If `None`, the rewards for the validator are returned.", + "anyOf": [ + { + "$ref": "#/components/schemas/PublicKey" + }, + { + "type": "null" + } + ] + }, + "required": false + } + ], + "result": { + "name": "info_get_reward_result", + "schema": { + "description": "Result for \"info_get_reward\" RPC response.", + "type": "object", + "required": [ + "api_version", + "era_id", + "reward_amount" + ], + "properties": { + "api_version": { + "description": "The RPC API version.", + "type": "string" + }, + "reward_amount": { + "description": "The total reward amount in the requested era.", + "$ref": "#/components/schemas/U512" + }, + "era_id": { + "description": "The era for which the reward was calculated.", + "$ref": "#/components/schemas/EraId" + } + }, + "additionalProperties": false + } + }, + "examples": [ + { + "name": "info_get_reward_example", + "params": [ + { + "name": "era_identifier", + "value": { + "Era": 1 + } + }, + { + "name": "validator", + "value": "01d9bf2148748a85c89da5aad8ee0b0fc2d105fd39d41a4c796536354f0ae2900c" + }, + { + "name": "delegator", + "value": "01d9bf2148748a85c89da5aad8ee0b0fc2d105fd39d41a4c796536354f0ae2900c" + } + ], + "result": { + "name": "info_get_reward_example_result", + "value": { + "api_version": "2.0.0", + "reward_amount": "42", + "era_id": 1 + } + } + } + ] + }, { "name": "info_get_validator_changes", "summary": "returns status changes of active validators", @@ -7784,6 +7884,35 @@ }, "additionalProperties": false }, + "EraIdentifier": { + "description": "Identifier for an era.", + "oneOf": [ + { + "type": "object", + "required": [ + "Era" + ], + "properties": { + "Era": { + "$ref": "#/components/schemas/EraId" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "Block" + ], + "properties": { + "Block": { + "$ref": "#/components/schemas/BlockIdentifier" + } + }, + "additionalProperties": false + } + ] + }, "JsonValidatorChanges": { "description": "The changes in a validator's status.", "type": "object", diff --git a/rpc_sidecar/src/http_server.rs b/rpc_sidecar/src/http_server.rs index 4ceb9ed2..43f93bcf 100644 --- a/rpc_sidecar/src/http_server.rs +++ b/rpc_sidecar/src/http_server.rs @@ -6,7 +6,7 @@ use casper_json_rpc::{CorsOrigin, RequestHandlersBuilder}; use crate::{ rpcs::{ - info::{GetPeers, GetStatus, GetTransaction}, + info::{GetPeers, GetReward, GetStatus, GetTransaction}, state::{GetAddressableEntity, QueryBalanceDetails}, }, NodeClient, @@ -54,6 +54,7 @@ pub async fn run( GetTransaction::register_as_handler(node.clone(), &mut handlers); GetPeers::register_as_handler(node.clone(), &mut handlers); GetStatus::register_as_handler(node.clone(), &mut handlers); + GetReward::register_as_handler(node.clone(), &mut handlers); GetEraInfoBySwitchBlock::register_as_handler(node.clone(), &mut handlers); GetEraSummary::register_as_handler(node.clone(), &mut handlers); GetAuctionInfo::register_as_handler(node.clone(), &mut handlers); diff --git a/rpc_sidecar/src/node_client.rs b/rpc_sidecar/src/node_client.rs index 27503408..293b130f 100644 --- a/rpc_sidecar/src/node_client.rs +++ b/rpc_sidecar/src/node_client.rs @@ -14,15 +14,16 @@ use tokio_util::codec::Framed; use casper_binary_port::{ BalanceResponse, BinaryMessage, BinaryMessageCodec, BinaryRequest, BinaryRequestHeader, BinaryResponse, BinaryResponseAndRequest, ConsensusValidatorChanges, DictionaryItemIdentifier, - DictionaryQueryResult, ErrorCode, GetRequest, GetTrieFullResult, GlobalStateQueryResult, - GlobalStateRequest, InformationRequest, KeyPrefix, NodeStatus, PayloadEntity, PurseIdentifier, - RecordId, SpeculativeExecutionResult, TransactionWithExecutionInfo, + DictionaryQueryResult, EraIdentifier, ErrorCode, GetRequest, GetTrieFullResult, + GlobalStateQueryResult, GlobalStateRequest, InformationRequest, KeyPrefix, NodeStatus, + PayloadEntity, PurseIdentifier, RecordId, RewardResponse, SpeculativeExecutionResult, + TransactionWithExecutionInfo, }; use casper_types::{ bytesrepr::{self, FromBytes, ToBytes}, AvailableBlockRange, BlockHash, BlockHeader, BlockIdentifier, ChainspecRawBytes, Digest, - GlobalStateIdentifier, Key, KeyTag, Peers, ProtocolVersion, SignedBlock, StoredValue, - Transaction, TransactionHash, Transfer, + GlobalStateIdentifier, Key, KeyTag, Peers, ProtocolVersion, PublicKey, SignedBlock, + StoredValue, Transaction, TransactionHash, Transfer, }; use std::{ fmt::{self, Display, Formatter}, @@ -238,6 +239,24 @@ pub trait NodeClient: Send + Sync { let resp = self.read_info(InformationRequest::NodeStatus).await?; parse_response::(&resp.into())?.ok_or(Error::EmptyEnvelope) } + + async fn read_reward( + &self, + era_identifier: Option, + validator: PublicKey, + delegator: Option, + ) -> Result, Error> { + let validator = validator.into(); + let delegator = delegator.map(Into::into); + let resp = self + .read_info(InformationRequest::Reward { + era_identifier, + validator, + delegator, + }) + .await?; + parse_response::(&resp.into()) + } } #[derive(Debug, thiserror::Error, PartialEq, Eq)] @@ -497,6 +516,12 @@ pub enum Error { InvalidTransaction(InvalidTransactionOrDeploy), #[error("speculative execution has failed: {0}")] SpecExecutionFailed(String), + #[error("the switch block for the requested era was not found")] + SwitchBlockNotFound, + #[error("the parent of the switch block for the requested era was not found")] + SwitchBlockParentNotFound, + #[error("cannot serve rewards stored in V1 format")] + UnsupportedRewardsV1Request, #[error("received a response with an unsupported protocol version: {0}")] UnsupportedProtocolVersion(ProtocolVersion), #[error("received an unexpected node error: {message} ({code})")] @@ -509,6 +534,9 @@ impl Error { Ok(ErrorCode::FunctionDisabled) => Self::FunctionIsDisabled, Ok(ErrorCode::RootNotFound) => Self::UnknownStateRootHash, Ok(ErrorCode::FailedQuery) => Self::QueryFailedToExecute, + Ok(ErrorCode::SwitchBlockNotFound) => Self::SwitchBlockNotFound, + Ok(ErrorCode::SwitchBlockParentNotFound) => Self::SwitchBlockParentNotFound, + Ok(ErrorCode::UnsupportedRewardsV1Request) => Self::UnsupportedRewardsV1Request, Ok( err @ (ErrorCode::InvalidDeployChainName | ErrorCode::InvalidDeployDependenciesNoLongerSupported diff --git a/rpc_sidecar/src/rpcs/docs.rs b/rpc_sidecar/src/rpcs/docs.rs index cb6bbb84..772f892e 100644 --- a/rpc_sidecar/src/rpcs/docs.rs +++ b/rpc_sidecar/src/rpcs/docs.rs @@ -18,7 +18,10 @@ use super::{ chain::{ GetBlock, GetBlockTransfers, GetEraInfoBySwitchBlock, GetEraSummary, GetStateRootHash, }, - info::{GetChainspec, GetDeploy, GetPeers, GetStatus, GetTransaction, GetValidatorChanges}, + info::{ + GetChainspec, GetDeploy, GetPeers, GetReward, GetStatus, GetTransaction, + GetValidatorChanges, + }, state::{ GetAccountInfo, GetAddressableEntity, GetAuctionInfo, GetBalance, GetDictionaryItem, GetItem, QueryBalance, QueryBalanceDetails, QueryGlobalState, @@ -86,6 +89,9 @@ pub(crate) static OPEN_RPC_SCHEMA: Lazy = Lazy::new(|| { ); schema.push_without_params::("returns a list of peers connected to the node"); schema.push_without_params::("returns the current status of the node"); + schema.push_with_params::( + "returns the reward for a given era and a validator or a delegator", + ); schema .push_without_params::("returns status changes of active validators"); schema.push_without_params::( diff --git a/rpc_sidecar/src/rpcs/error.rs b/rpc_sidecar/src/rpcs/error.rs index fa6853c0..9444bf57 100644 --- a/rpc_sidecar/src/rpcs/error.rs +++ b/rpc_sidecar/src/rpcs/error.rs @@ -37,6 +37,8 @@ pub enum Error { AccountNotFound, #[error("the requested addressable entity was not found")] AddressableEntityNotFound, + #[error("the requested reward was not found")] + RewardNotFound, #[error("the requested account has been migrated to an addressable entity")] AccountMigratedToEntity, #[error("the provided dictionary value is {0} instead of a URef")] @@ -82,11 +84,21 @@ impl Error { Error::NodeRequest(_, NodeClientError::FunctionIsDisabled) => { Some(ErrorCode::FunctionIsDisabled) } + Error::NodeRequest(_, NodeClientError::SwitchBlockNotFound) => { + Some(ErrorCode::SwitchBlockNotFound) + } + Error::NodeRequest(_, NodeClientError::SwitchBlockParentNotFound) => { + Some(ErrorCode::SwitchBlockParentNotFound) + } + Error::NodeRequest(_, NodeClientError::UnsupportedRewardsV1Request) => { + Some(ErrorCode::UnsupportedRewardsV1Request) + } Error::InvalidPurseURef(_) => Some(ErrorCode::FailedToParseGetBalanceURef), Error::InvalidDictionaryKey(_) => Some(ErrorCode::FailedToParseQueryKey), Error::MainPurseNotFound => Some(ErrorCode::NoSuchMainPurse), Error::AccountNotFound => Some(ErrorCode::NoSuchAccount), Error::AddressableEntityNotFound => Some(ErrorCode::NoSuchAddressableEntity), + Error::RewardNotFound => Some(ErrorCode::NoRewardsFound), Error::AccountMigratedToEntity => Some(ErrorCode::AccountMigratedToEntity), Error::InvalidTypeUnderDictionaryKey(_) | Error::DictionaryKeyNotFound diff --git a/rpc_sidecar/src/rpcs/error_code.rs b/rpc_sidecar/src/rpcs/error_code.rs index 9e222bdb..085c08d5 100644 --- a/rpc_sidecar/src/rpcs/error_code.rs +++ b/rpc_sidecar/src/rpcs/error_code.rs @@ -53,6 +53,14 @@ pub enum ErrorCode { NoSuchAddressableEntity = -32020, /// The requested account has been migrated to an addressable entity. AccountMigratedToEntity = -32021, + /// The requested reward was not found. + NoRewardsFound = -32022, + /// The switch block for the requested era was not found. + SwitchBlockNotFound = -32023, + /// The parent of the switch block for the requested era was not found. + SwitchBlockParentNotFound = -32024, + /// Cannot serve rewards stored in V1 format + UnsupportedRewardsV1Request = -32025, } impl From for (i64, &'static str) { @@ -92,6 +100,15 @@ impl From for (i64, &'static str) { error_code as i64, "Account migrated to an addressable entity", ), + ErrorCode::NoRewardsFound => (error_code as i64, "No rewards found"), + ErrorCode::SwitchBlockNotFound => (error_code as i64, "Switch block not found"), + ErrorCode::SwitchBlockParentNotFound => { + (error_code as i64, "Switch block parent not found") + } + ErrorCode::UnsupportedRewardsV1Request => ( + error_code as i64, + "Cannot serve rewards stored in V1 format", + ), } } } diff --git a/rpc_sidecar/src/rpcs/info.rs b/rpc_sidecar/src/rpcs/info.rs index 72973459..16f1aa1b 100644 --- a/rpc_sidecar/src/rpcs/info.rs +++ b/rpc_sidecar/src/rpcs/info.rs @@ -3,16 +3,17 @@ use std::{collections::BTreeMap, str, sync::Arc}; use async_trait::async_trait; -use casper_binary_port::MinimalBlockInfo; +use casper_binary_port::{EraIdentifier as PortEraIdentifier, MinimalBlockInfo}; use once_cell::sync::Lazy; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use casper_types::{ execution::{ExecutionResult, ExecutionResultV2}, - ActivationPoint, AvailableBlockRange, Block, BlockHash, BlockSynchronizerStatus, - ChainspecRawBytes, Deploy, DeployHash, Digest, EraId, ExecutionInfo, NextUpgrade, Peers, - ProtocolVersion, PublicKey, TimeDiff, Timestamp, Transaction, TransactionHash, ValidatorChange, + ActivationPoint, AvailableBlockRange, Block, BlockHash, BlockIdentifier, + BlockSynchronizerStatus, ChainspecRawBytes, Deploy, DeployHash, Digest, EraId, ExecutionInfo, + NextUpgrade, Peers, ProtocolVersion, PublicKey, TimeDiff, Timestamp, Transaction, + TransactionHash, ValidatorChange, U512, }; use super::{ @@ -92,6 +93,16 @@ static GET_STATUS_RESULT: Lazy = Lazy::new(|| GetStatusResult { #[cfg(test)] build_version: String::from("1.0.0-xxxxxxxxx@DEBUG"), }); +static GET_REWARD_PARAMS: Lazy = Lazy::new(|| GetRewardParams { + era_identifier: Some(EraIdentifier::Era(EraId::new(1))), + validator: PublicKey::example().clone(), + delegator: Some(PublicKey::example().clone()), +}); +static GET_REWARD_RESULT: Lazy = Lazy::new(|| GetRewardResult { + api_version: DOCS_EXAMPLE_API_VERSION, + reward_amount: U512::from(42), + era_id: EraId::new(1), +}); /// Params for "info_get_deploy" RPC request. #[derive(Serialize, Deserialize, Debug, JsonSchema)] @@ -495,6 +506,84 @@ impl RpcWithoutParams for GetStatus { } } +/// Params for "info_get_reward" RPC request. +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct GetRewardParams { + /// The era identifier. If `None`, the last finalized era is used. + pub era_identifier: Option, + /// The public key of the validator. + pub validator: PublicKey, + /// The public key of the delegator. If `Some`, the rewards for the delegator are returned. + /// If `None`, the rewards for the validator are returned. + pub delegator: Option, +} + +impl DocExample for GetRewardParams { + fn doc_example() -> &'static Self { + &GET_REWARD_PARAMS + } +} + +/// Identifier for an era. +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +pub enum EraIdentifier { + Era(EraId), + Block(BlockIdentifier), +} + +/// Result for "info_get_reward" RPC response. +#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct GetRewardResult { + /// The RPC API version. + #[schemars(with = "String")] + pub api_version: ApiVersion, + /// The total reward amount in the requested era. + pub reward_amount: U512, + /// The era for which the reward was calculated. + pub era_id: EraId, +} + +impl DocExample for GetRewardResult { + fn doc_example() -> &'static Self { + &GET_REWARD_RESULT + } +} + +/// "info_get_reward" RPC. +pub struct GetReward {} + +#[async_trait] +impl RpcWithParams for GetReward { + const METHOD: &'static str = "info_get_reward"; + type RequestParams = GetRewardParams; + type ResponseResult = GetRewardResult; + + async fn do_handle_request( + node_client: Arc, + params: Self::RequestParams, + ) -> Result { + let identifier = match params.era_identifier { + Some(EraIdentifier::Era(era_id)) => Some(PortEraIdentifier::Era(era_id)), + Some(EraIdentifier::Block(block_id)) => Some(PortEraIdentifier::Block(block_id)), + None => None, + }; + + let result = node_client + .read_reward(identifier, params.validator, params.delegator) + .await + .map_err(|err| Error::NodeRequest("rewards", err))? + .ok_or(Error::RewardNotFound)?; + + Ok(Self::ResponseResult { + api_version: CURRENT_API_VERSION, + reward_amount: result.amount(), + era_id: result.era_id(), + }) + } +} + #[cfg(not(test))] fn version_string() -> String { use std::env; @@ -526,7 +615,7 @@ mod tests { use crate::{rpcs::ErrorCode, ClientError, SUPPORTED_PROTOCOL_VERSION}; use casper_binary_port::{ BinaryRequest, BinaryResponse, BinaryResponseAndRequest, GetRequest, InformationRequest, - InformationRequestTag, TransactionWithExecutionInfo, + InformationRequestTag, RewardResponse, TransactionWithExecutionInfo, }; use casper_types::{ bytesrepr::{FromBytes, ToBytes}, @@ -715,6 +804,38 @@ mod tests { assert_eq!(err.code(), ErrorCode::VariantMismatch as i64); } + #[tokio::test] + async fn should_return_rewards() { + let rng = &mut TestRng::new(); + let reward_amount = U512::from(rng.gen_range(0..1000)); + let era_id = EraId::new(rng.gen_range(0..1000)); + let validator = PublicKey::random(rng); + let delegator = rng.gen::().then(|| PublicKey::random(rng)); + + let resp = GetReward::do_handle_request( + Arc::new(RewardMock { + reward_amount, + era_id, + }), + GetRewardParams { + era_identifier: Some(EraIdentifier::Era(era_id)), + validator: validator.clone(), + delegator, + }, + ) + .await + .expect("should handle request"); + + assert_eq!( + resp, + GetRewardResult { + api_version: CURRENT_API_VERSION, + reward_amount, + era_id, + } + ); + } + struct ValidTransactionMock { transaction_bytes: Vec, should_request_approvals: bool, @@ -763,4 +884,31 @@ mod tests { } } } + + struct RewardMock { + reward_amount: U512, + era_id: EraId, + } + + #[async_trait] + impl NodeClient for RewardMock { + async fn send_request( + &self, + req: BinaryRequest, + ) -> Result { + match req { + BinaryRequest::Get(GetRequest::Information { info_type_tag, .. }) + if InformationRequestTag::try_from(info_type_tag) + == Ok(InformationRequestTag::Reward) => + { + let resp = RewardResponse::new(self.reward_amount, self.era_id); + Ok(BinaryResponseAndRequest::new( + BinaryResponse::from_value(resp, SUPPORTED_PROTOCOL_VERSION), + &[], + )) + } + req => unimplemented!("unexpected request: {:?}", req), + } + } + } }