From 6bebdda69e758c26179a8316e1f47221d58f181f Mon Sep 17 00:00:00 2001 From: Jakub Zajkowski Date: Tue, 14 Jan 2025 17:48:58 +0100 Subject: [PATCH] Introduced "state_get_auction_info_v2" json rpc method. --- resources/test/rpc_schema.json | 120 ++++- rpc_sidecar/src/http_server.rs | 2 + rpc_sidecar/src/rpcs.rs | 3 + rpc_sidecar/src/rpcs/docs.rs | 14 +- rpc_sidecar/src/rpcs/state.rs | 94 ++-- rpc_sidecar/src/rpcs/state/auction_state.rs | 20 +- .../src/rpcs/state_get_auction_info_v2.rs | 508 ++++++++++++++++++ rpc_sidecar/src/rpcs/test_utils.rs | 219 ++++++++ 8 files changed, 937 insertions(+), 43 deletions(-) create mode 100644 rpc_sidecar/src/rpcs/state_get_auction_info_v2.rs create mode 100644 rpc_sidecar/src/rpcs/test_utils.rs diff --git a/resources/test/rpc_schema.json b/resources/test/rpc_schema.json index ccc38e33..7a3cc675 100644 --- a/resources/test/rpc_schema.json +++ b/resources/test/rpc_schema.json @@ -2359,7 +2359,7 @@ }, { "name": "state_get_auction_info", - "summary": "returns the bids and validators as of either a specific block (by height or hash), or the most recently added block", + "summary": "returns the bids and validators as of either a specific block (by height or hash), or the most recently added block. This is a casper 1.x retro-compatibility endpoint. For blocks created in 1.x protocol it will work exactly the same as it used to. \n For 2.x blocks it will try to retrofit the changed data structure into previous schema - but it is a lossy process. Use `state_get_auction_info_v2` endpoint to get data in new format. *IMPORTANT* This method is deprecated, has been added only for compatibility with retired nodes json-rpc API and will be removed in a future release of sidecar.", "params": [ { "name": "block_identifier", @@ -2452,6 +2452,108 @@ } ] }, + { + "name": "state_get_auction_info_v2", + "summary": "returns the bids and validators as of either a specific block (by height or hash), or the most recently added block. It works for blocks created in 1.x and 2.x", + "params": [ + { + "name": "block_identifier", + "schema": { + "description": "The block identifier.", + "$ref": "#/components/schemas/BlockIdentifier" + }, + "required": false + } + ], + "result": { + "name": "state_get_auction_info_v2_result", + "schema": { + "description": "Result for \"state_get_auction_info\" RPC response.", + "type": "object", + "required": [ + "api_version", + "auction_state" + ], + "properties": { + "api_version": { + "description": "The RPC API version.", + "type": "string" + }, + "auction_state": { + "description": "The auction state.", + "$ref": "#/components/schemas/AuctionState" + } + }, + "additionalProperties": false + } + }, + "examples": [ + { + "name": "state_get_auction_info_v2_example", + "params": [ + { + "name": "block_identifier", + "value": { + "Hash": "0707070707070707070707070707070707070707070707070707070707070707" + } + } + ], + "result": { + "name": "state_get_auction_info_v2_example_result", + "value": { + "api_version": "2.0.0", + "auction_state": { + "state_root_hash": "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", + "block_height": 10, + "era_validators": [ + { + "era_id": 10, + "validator_weights": [ + { + "public_key": "01197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "weight": "10" + } + ] + } + ], + "bids": [ + { + "public_key": "01197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "bid": { + "Validator": { + "validator_public_key": "01197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "bonding_purse": "uref-fafafafafafafafafafafafafafafafafafafafafafafafafafafafafafafafa-007", + "staked_amount": "20", + "delegation_rate": 0, + "vesting_schedule": null, + "inactive": false, + "minimum_delegation_amount": 0, + "maximum_delegation_amount": 18446744073709551615, + "reserved_slots": 0 + } + } + }, + { + "public_key": "01197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "bid": { + "Delegator": { + "delegator_kind": { + "PublicKey": "014508a07aa941707f3eb2db94c8897a80b2c1197476b6de213ac273df7d86c4ff" + }, + "staked_amount": "10", + "bonding_purse": "uref-fbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfbfb-007", + "validator_public_key": "01197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61", + "vesting_schedule": null + } + } + } + ] + } + } + } + } + ] + }, { "name": "chain_get_era_summary", "summary": "returns the era summary at either a specific block (by height or hash), or the most recently added block", @@ -8526,6 +8628,22 @@ ] } } + }, + "BidKindWrapper": { + "type": "object", + "required": [ + "bid", + "public_key" + ], + "properties": { + "public_key": { + "$ref": "#/components/schemas/PublicKey" + }, + "bid": { + "$ref": "#/components/schemas/BidKind" + } + }, + "additionalProperties": false } } } diff --git a/rpc_sidecar/src/http_server.rs b/rpc_sidecar/src/http_server.rs index 2362f204..7b3298ea 100644 --- a/rpc_sidecar/src/http_server.rs +++ b/rpc_sidecar/src/http_server.rs @@ -23,6 +23,7 @@ use super::rpcs::{ GetAccountInfo, GetAuctionInfo, GetBalance, GetDictionaryItem, GetItem, GetTrie, QueryBalance, QueryGlobalState, }, + state_get_auction_info_v2::GetAuctionInfo as GetAuctionInfoV2, RpcWithOptionalParams, RpcWithParams, RpcWithoutParams, }; @@ -59,6 +60,7 @@ pub async fn run( GetEraInfoBySwitchBlock::register_as_handler(node.clone(), &mut handlers); GetEraSummary::register_as_handler(node.clone(), &mut handlers); GetAuctionInfo::register_as_handler(node.clone(), &mut handlers); + GetAuctionInfoV2::register_as_handler(node.clone(), &mut handlers); GetTrie::register_as_handler(node.clone(), &mut handlers); GetValidatorChanges::register_as_handler(node.clone(), &mut handlers); RpcDiscover::register_as_handler(node.clone(), &mut handlers); diff --git a/rpc_sidecar/src/rpcs.rs b/rpc_sidecar/src/rpcs.rs index 30d0885b..c3f61716 100644 --- a/rpc_sidecar/src/rpcs.rs +++ b/rpc_sidecar/src/rpcs.rs @@ -12,6 +12,9 @@ pub mod info; pub mod speculative_exec; pub mod speculative_open_rpc_schema; pub mod state; +pub(crate) mod state_get_auction_info_v2; +#[cfg(test)] +pub(crate) mod test_utils; mod types; use std::{fmt, str, sync::Arc, time::Duration}; diff --git a/rpc_sidecar/src/rpcs/docs.rs b/rpc_sidecar/src/rpcs/docs.rs index 17ab2ae4..ac23da0a 100644 --- a/rpc_sidecar/src/rpcs/docs.rs +++ b/rpc_sidecar/src/rpcs/docs.rs @@ -26,6 +26,7 @@ use super::{ GetAccountInfo, GetAddressableEntity, GetAuctionInfo, GetBalance, GetDictionaryItem, GetItem, GetPackage, QueryBalance, QueryBalanceDetails, QueryGlobalState, }, + state_get_auction_info_v2::GetAuctionInfo as GetAuctionInfoV2, ApiVersion, NodeClient, RpcError, RpcWithOptionalParams, RpcWithParams, RpcWithoutParams, CURRENT_API_VERSION, }; @@ -116,7 +117,12 @@ pub(crate) static OPEN_RPC_SCHEMA: Lazy = Lazy::new(|| { ); schema.push_with_optional_params::( "returns the bids and validators as of either a specific block (by height or hash), or \ - the most recently added block", + the most recently added block. This is a casper 1.x retro-compatibility endpoint. For blocks created in 1.x protocol it will work exactly the same as it used to. + For 2.x blocks it will try to retrofit the changed data structure into previous schema - but it is a lossy process. Use `state_get_auction_info_v2` endpoint to get data in new format. *IMPORTANT* This method is deprecated, has been added only for compatibility with retired nodes json-rpc API and will be removed in a future release of sidecar.", + ); + schema.push_with_optional_params::( + "returns the bids and validators as of either a specific block (by height or hash), or \ + the most recently added block. It works for blocks created in 1.x and 2.x", ); schema.push_with_optional_params::( "returns the era summary at either a specific block (by height or hash), or the most \ @@ -602,4 +608,10 @@ mod tests { let incorrect_optional_params = check_optional_params_fields::(); assert!(incorrect_optional_params.is_empty()) } + + #[test] + fn check_state_get_auction_info_v2_required_fields() { + let incorrect_optional_params = check_optional_params_fields::(); + assert!(incorrect_optional_params.is_empty()) + } } diff --git a/rpc_sidecar/src/rpcs/state.rs b/rpc_sidecar/src/rpcs/state.rs index 9641972d..66c9e641 100644 --- a/rpc_sidecar/src/rpcs/state.rs +++ b/rpc_sidecar/src/rpcs/state.rs @@ -2,6 +2,7 @@ mod auction_state; +pub(crate) use auction_state::{JsonEraValidators, JsonValidatorWeights, ERA_VALIDATORS}; use std::{collections::BTreeMap, str, sync::Arc}; use crate::node_client::{EntityResponse, PackageResponse}; @@ -333,7 +334,7 @@ impl RpcWithParams for GetBalance { /// Params for "state_get_auction_info" RPC request. #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(deny_unknown_fields)] -pub struct GetAuctionInfoParams { +pub(crate) struct GetAuctionInfoParams { /// The block identifier. pub block_identifier: BlockIdentifier, } @@ -376,27 +377,12 @@ impl RpcWithOptionalParams for GetAuctionInfo { ) -> Result { let block_identifier = maybe_params.map(|params| params.block_identifier); let block_header = common::get_block_header(&*node_client, block_identifier).await?; - let state_identifier = block_identifier.map(GlobalStateIdentifier::from); - let legacy_bid_stored_values = node_client - .query_global_state_by_tag(state_identifier, KeyTag::Bid) - .await - .map_err(|err| Error::NodeRequest("auction bids", err))? - .into_iter() - .map(|value| { - Ok(BidKind::Unified( - value.into_bid().ok_or(Error::InvalidAuctionState)?.into(), - )) - }); - let bid_stored_values = node_client - .query_global_state_by_tag(state_identifier, KeyTag::BidAddr) - .await - .map_err(|err| Error::NodeRequest("auction bids", err))? - .into_iter() - .map(|value| value.into_bid_kind().ok_or(Error::InvalidAuctionState)); - let bids = legacy_bid_stored_values - .chain(bid_stored_values) - .collect::, Error>>()?; + let state_identifier = + state_identifier.unwrap_or(GlobalStateIdentifier::BlockHeight(block_header.height())); + + let is_not_condor = block_header.protocol_version().value().major == 1; + let bids = fetch_bid_kinds(node_client.clone(), state_identifier, is_not_condor).await?; // always retrieve the latest system contract registry, old versions of the node // did not write it to the global state @@ -412,15 +398,15 @@ impl RpcWithOptionalParams for GetAuctionInfo { .into_t() .map_err(|_| Error::InvalidAuctionState)?; let &auction_hash = registry.get(AUCTION).ok_or(Error::InvalidAuctionState)?; - let maybe_version = get_seniorage_recipients_version( + let maybe_version = get_seigniorage_recipients_version( Arc::clone(&node_client), - state_identifier, + Some(state_identifier), auction_hash, ) .await?; let (snapshot_value, _) = if let Some(result) = node_client .query_global_state( - state_identifier, + Some(state_identifier), Key::addressable_entity_key(EntityKindTag::System, auction_hash), vec![SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY.to_owned()], ) @@ -431,7 +417,7 @@ impl RpcWithOptionalParams for GetAuctionInfo { } else { node_client .query_global_state( - state_identifier, + Some(state_identifier), Key::Hash(auction_hash.value()), vec![SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY.to_owned()], ) @@ -459,7 +445,7 @@ impl RpcWithOptionalParams for GetAuctionInfo { } } -async fn get_seniorage_recipients_version( +pub(crate) async fn get_seigniorage_recipients_version( node_client: Arc, state_identifier: Option, auction_hash: AddressableEntityHash, @@ -479,7 +465,35 @@ async fn get_seniorage_recipients_version( } } -async fn fetch_seigniorage_recipients_snapshot_version_key( +pub(crate) async fn fetch_bid_kinds( + node_client: Arc, + state_identifier: GlobalStateIdentifier, + is_not_condor: bool, +) -> Result, RpcError> { + let key_tag = if is_not_condor { + KeyTag::Bid + } else { + KeyTag::BidAddr + }; + let stored_values = node_client + .query_global_state_by_tag(Some(state_identifier), key_tag) + .await + .map_err(|err| Error::NodeRequest("auction bids", err))? + .into_iter(); + let res: Result, Error> = if is_not_condor { + stored_values + .map(|v| v.into_bid().ok_or(Error::InvalidAuctionState)) + .map(|bid_res| bid_res.map(|bid| BidKind::Unified(bid.into()))) + .collect() + } else { + stored_values + .map(|value| value.into_bid_kind().ok_or(Error::InvalidAuctionState)) + .collect() + }; + res.map_err(|e| e.into()) +} + +pub(crate) async fn fetch_seigniorage_recipients_snapshot_version_key( node_client: Arc, base_key: Key, state_identifier: Option, @@ -491,11 +505,13 @@ async fn fetch_seigniorage_recipients_snapshot_version_key( vec![SEIGNIORAGE_RECIPIENTS_SNAPSHOT_VERSION_KEY.to_owned()], ) .await - .map(|result| result.and_then(unwrap_seniorage_recipients_result)) + .map(|result| result.and_then(unwrap_seigniorage_recipients_result)) .map_err(|err| Error::NodeRequest("auction snapshot", err)) } -fn unwrap_seniorage_recipients_result(query_result: GlobalStateQueryResult) -> Option { +pub(crate) fn unwrap_seigniorage_recipients_result( + query_result: GlobalStateQueryResult, +) -> Option { let (version_value, _) = query_result.into_inner(); let maybe_cl_value = version_value.into_cl_value(); match maybe_cl_value { @@ -1352,16 +1368,16 @@ impl RpcWithParams for GetTrie { } } -fn era_validators_from_snapshot( +pub(crate) fn era_validators_from_snapshot( snapshot: CLValue, maybe_version: Option, ) -> Result { if maybe_version.is_some() { //handle as condor //TODO add some context to the error - let seniorage: BTreeMap = + let seigniorage: BTreeMap = snapshot.into_t().map_err(|_| Error::InvalidAuctionState)?; - Ok(seniorage + Ok(seigniorage .into_iter() .map(|(era_id, recipients)| { let validator_weights = recipients @@ -1376,9 +1392,9 @@ fn era_validators_from_snapshot( } else { //handle as pre-condor //TODO add some context to the error - let seniorage: BTreeMap = + let seigniorage: BTreeMap = snapshot.into_t().map_err(|_| Error::InvalidAuctionState)?; - Ok(seniorage + Ok(seigniorage .into_iter() .map(|(era_id, recipients)| { let validator_weights = recipients @@ -1618,8 +1634,8 @@ mod tests { } ) => { - let response = match req.clone().destructure() { - (None, GlobalStateEntityQualifier::Item { base_key: _, path }) + let response: BinaryResponse = match req.clone().destructure() { + (_, GlobalStateEntityQualifier::Item { base_key: _, path }) if path == vec!["seigniorage_recipients_snapshot_version"] => { let result = GlobalStateQueryResult::new( @@ -1672,7 +1688,7 @@ mod tests { *block.state_root_hash(), block.height(), Default::default(), - vec![bid, BidKind::Unified(legacy_bid.into())] + vec![BidKind::Unified(legacy_bid.into())] ), } ); @@ -1803,7 +1819,7 @@ mod tests { ) => { match req.clone().destructure() { - (None, GlobalStateEntityQualifier::Item { base_key: _, path }) + (_, GlobalStateEntityQualifier::Item { base_key: _, path }) if path == vec!["seigniorage_recipients_snapshot_version"] => { Ok(BinaryResponseAndRequest::new( @@ -1858,7 +1874,7 @@ mod tests { *block.state_root_hash(), block.height(), Default::default(), - vec![bid, BidKind::Unified(legacy_bid.into())] + vec![BidKind::Unified(legacy_bid.into())] ), } ); diff --git a/rpc_sidecar/src/rpcs/state/auction_state.rs b/rpc_sidecar/src/rpcs/state/auction_state.rs index 21d329c4..f54b61da 100644 --- a/rpc_sidecar/src/rpcs/state/auction_state.rs +++ b/rpc_sidecar/src/rpcs/state/auction_state.rs @@ -15,7 +15,7 @@ use casper_types::{ use crate::rpcs::docs::DocExample; -static ERA_VALIDATORS: Lazy = Lazy::new(|| { +pub(crate) static ERA_VALIDATORS: Lazy = Lazy::new(|| { use casper_types::SecretKey; let secret_key_1 = SecretKey::ed25519_from_bytes([42; SecretKey::ED25519_LENGTH]).unwrap(); @@ -75,6 +75,12 @@ pub struct JsonValidatorWeights { weight: U512, } +impl JsonValidatorWeights { + pub fn new(public_key: PublicKey, weight: U512) -> Self { + Self { public_key, weight } + } +} + /// The validators for the given era. #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, JsonSchema)] #[serde(deny_unknown_fields)] @@ -83,10 +89,20 @@ pub struct JsonEraValidators { validator_weights: Vec, } +impl JsonEraValidators { + pub fn new(era_id: EraId, validator_weights: Vec) -> Self { + Self { + era_id, + validator_weights, + } + } +} + +/* We should use the AuctionState struct from casper-types once it's ctor is updated */ /// Data structure summarizing auction contract data. #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, JsonSchema)] #[serde(deny_unknown_fields)] -pub struct AuctionState { +pub(crate) struct AuctionState { /// Global state hash. pub state_root_hash: Digest, /// Block height. diff --git a/rpc_sidecar/src/rpcs/state_get_auction_info_v2.rs b/rpc_sidecar/src/rpcs/state_get_auction_info_v2.rs new file mode 100644 index 00000000..76070ec4 --- /dev/null +++ b/rpc_sidecar/src/rpcs/state_get_auction_info_v2.rs @@ -0,0 +1,508 @@ +//! RPCs of state_get_auction_info_v2. + +use std::{collections::BTreeMap, str, sync::Arc}; + +use crate::rpcs::state::ERA_VALIDATORS; +use async_trait::async_trait; +use casper_types::system::auction::ValidatorBid; +use once_cell::sync::Lazy; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use super::common; +use super::state::{ + era_validators_from_snapshot, fetch_bid_kinds, get_seigniorage_recipients_version, + GetAuctionInfoParams, JsonEraValidators, JsonValidatorWeights, +}; +use super::{ + docs::{DocExample, DOCS_EXAMPLE_API_VERSION}, + ApiVersion, Error, NodeClient, RpcError, RpcWithOptionalParams, CURRENT_API_VERSION, +}; +use casper_types::{ + addressable_entity::EntityKindTag, + system::{ + auction::{BidKind, DelegatorBid, EraValidators, SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY}, + AUCTION, + }, + AddressableEntityHash, Digest, GlobalStateIdentifier, Key, PublicKey, U512, +}; + +static GET_AUCTION_INFO_RESULT: Lazy = Lazy::new(|| GetAuctionInfoResult { + api_version: DOCS_EXAMPLE_API_VERSION, + auction_state: AuctionState::doc_example().clone(), +}); +static AUCTION_INFO: Lazy = Lazy::new(|| { + use casper_types::{system::auction::DelegationRate, AccessRights, SecretKey, URef}; + use num_traits::Zero; + + let state_root_hash = Digest::from([11; Digest::LENGTH]); + let validator_secret_key = + SecretKey::ed25519_from_bytes([42; SecretKey::ED25519_LENGTH]).unwrap(); + let validator_public_key = PublicKey::from(&validator_secret_key); + + let mut bids = vec![]; + let validator_bid = ValidatorBid::unlocked( + validator_public_key.clone(), + URef::new([250; 32], AccessRights::READ_ADD_WRITE), + U512::from(20), + DelegationRate::zero(), + 0, + u64::MAX, + 0, + ); + bids.push(BidKind::Validator(Box::new(validator_bid))); + + let delegator_secret_key = + SecretKey::ed25519_from_bytes([43; SecretKey::ED25519_LENGTH]).unwrap(); + let delegator_public_key = PublicKey::from(&delegator_secret_key); + let delegator_bid = DelegatorBid::unlocked( + delegator_public_key.into(), + U512::from(10), + URef::new([251; 32], AccessRights::READ_ADD_WRITE), + validator_public_key, + ); + bids.push(BidKind::Delegator(Box::new(delegator_bid))); + + let height: u64 = 10; + let era_validators = ERA_VALIDATORS.clone(); + AuctionState::new(state_root_hash, height, era_validators, bids) +}); + +/// Result for "state_get_auction_info" RPC response. +#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct GetAuctionInfoResult { + /// The RPC API version. + #[schemars(with = "String")] + pub api_version: ApiVersion, + /// The auction state. + pub auction_state: AuctionState, +} + +impl DocExample for GetAuctionInfoResult { + fn doc_example() -> &'static Self { + &GET_AUCTION_INFO_RESULT + } +} + +/// "state_get_auction_info_v2" RPC. +pub struct GetAuctionInfo {} + +#[async_trait] +impl RpcWithOptionalParams for GetAuctionInfo { + const METHOD: &'static str = "state_get_auction_info_v2"; + type OptionalRequestParams = GetAuctionInfoParams; + type ResponseResult = GetAuctionInfoResult; + + async fn do_handle_request( + node_client: Arc, + maybe_params: Option, + ) -> Result { + let block_identifier = maybe_params.map(|params| params.block_identifier); + let block_header = common::get_block_header(&*node_client, block_identifier).await?; + + let state_identifier = block_identifier.map(GlobalStateIdentifier::from); + let state_identifier = + state_identifier.unwrap_or(GlobalStateIdentifier::BlockHeight(block_header.height())); + + let is_not_condor = block_header.protocol_version().value().major == 1; + let bids = fetch_bid_kinds(node_client.clone(), state_identifier, is_not_condor).await?; + + // always retrieve the latest system contract registry, old versions of the node + // did not write it to the global state + let (registry_value, _) = node_client + .query_global_state(Some(state_identifier), Key::SystemEntityRegistry, vec![]) + .await + .map_err(|err| Error::NodeRequest("system contract registry", err))? + .ok_or(Error::GlobalStateEntryNotFound)? + .into_inner(); + let registry: BTreeMap = registry_value + .into_cl_value() + .ok_or(Error::InvalidAuctionState)? + .into_t() + .map_err(|_| Error::InvalidAuctionState)?; + let &auction_hash = registry.get(AUCTION).ok_or(Error::InvalidAuctionState)?; + let maybe_version = get_seigniorage_recipients_version( + Arc::clone(&node_client), + Some(state_identifier), + auction_hash, + ) + .await?; + let (snapshot_value, _) = if let Some(result) = node_client + .query_global_state( + Some(state_identifier), + Key::addressable_entity_key(EntityKindTag::System, auction_hash), + vec![SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY.to_owned()], + ) + .await + .map_err(|err| Error::NodeRequest("auction snapshot", err))? + { + result.into_inner() + } else { + node_client + .query_global_state( + Some(state_identifier), + Key::Hash(auction_hash.value()), + vec![SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY.to_owned()], + ) + .await + .map_err(|err| Error::NodeRequest("auction snapshot", err))? + .ok_or(Error::GlobalStateEntryNotFound)? + .into_inner() + }; + let snapshot = snapshot_value + .into_cl_value() + .ok_or(Error::InvalidAuctionState)?; + + let validators = era_validators_from_snapshot(snapshot, maybe_version)?; + let auction_state = AuctionState::new( + *block_header.state_root_hash(), + block_header.height(), + validators, + bids, + ); + + Ok(Self::ResponseResult { + api_version: CURRENT_API_VERSION, + auction_state, + }) + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct BidKindWrapper { + public_key: PublicKey, + bid: BidKind, +} + +impl BidKindWrapper { + /// ctor + pub fn new(public_key: PublicKey, bid: BidKind) -> Self { + Self { public_key, bid } + } +} + +/// Data structure summarizing auction contract data. +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, JsonSchema)] +#[serde(deny_unknown_fields)] +pub(crate) struct AuctionState { + /// Global state hash. + state_root_hash: Digest, + /// Block height. + block_height: u64, + /// Era validators. + era_validators: Vec, + /// All bids. + bids: Vec, +} + +impl AuctionState { + /// Ctor + pub fn new( + state_root_hash: Digest, + block_height: u64, + era_validators: EraValidators, + bids: Vec, + ) -> Self { + let mut json_era_validators: Vec = Vec::new(); + for (era_id, validator_weights) in era_validators.iter() { + let mut json_validator_weights: Vec = Vec::new(); + for (public_key, weight) in validator_weights.iter() { + json_validator_weights.push(JsonValidatorWeights::new(public_key.clone(), *weight)); + } + json_era_validators.push(JsonEraValidators::new(*era_id, json_validator_weights)); + } + + let bids = bids + .into_iter() + .map(|bid_kind| BidKindWrapper::new(bid_kind.validator_public_key(), bid_kind)) + .collect(); + + AuctionState { + state_root_hash, + block_height, + era_validators: json_era_validators, + bids, + } + } + + // This method is not intended to be used by third party crates. + #[doc(hidden)] + pub fn example() -> &'static Self { + &AUCTION_INFO + } +} + +impl DocExample for AuctionState { + fn doc_example() -> &'static Self { + AuctionState::example() + } +} + +#[cfg(test)] +mod tests { + use crate::{ + rpcs::{ + state_get_auction_info_v2::{AuctionState, GetAuctionInfo, GetAuctionInfoResult}, + test_utils::BinaryPortMock, + RpcWithOptionalParams, CURRENT_API_VERSION, + }, + SUPPORTED_PROTOCOL_VERSION, + }; + use casper_types::{ + system::{ + auction::{ + Bid, BidKind, DelegatorKind, SeigniorageRecipientV1, SeigniorageRecipientV2, + SeigniorageRecipientsV1, SeigniorageRecipientsV2, ValidatorBid, + }, + AUCTION, + }, + testing::TestRng, + AddressableEntityHash, CLValue, EraId, PublicKey, StoredValue, TestBlockV1Builder, U512, + }; + use rand::Rng; + use std::{collections::BTreeMap, sync::Arc}; + + #[tokio::test] + async fn should_read_pre_condor_auction_info_with_addressable_entity_off() { + let rng = &mut TestRng::new(); + let mut binary_port_mock = BinaryPortMock::new(); + let auction_hash: AddressableEntityHash = AddressableEntityHash::new(rng.gen()); + let block_header = TestBlockV1Builder::new() + .build_versioned(rng) + .clone_header(); + let registry = BTreeMap::from([(AUCTION.to_string(), auction_hash)]); + let public_key_1 = PublicKey::random(rng); + let public_key_2 = PublicKey::random(rng); + let recipient_v1 = SeigniorageRecipientV1::new( + U512::from(125), + 50, + BTreeMap::from([(public_key_2, U512::from(500))]), + ); + let recipients_1: BTreeMap = + BTreeMap::from([(public_key_1.clone(), recipient_v1)]); + let v1_recipients: BTreeMap = + BTreeMap::from([(EraId::new(100), recipients_1)]); + let stored_value = StoredValue::CLValue(CLValue::from_t(v1_recipients.clone()).unwrap()); + let bid_1 = Bid::empty(PublicKey::random(rng), rng.gen()); + let bids = vec![bid_1]; + let state_identifier = Some(casper_types::GlobalStateIdentifier::BlockHeight( + block_header.height(), + )); + binary_port_mock + .add_block_header_req_res(block_header.clone()) + .await; + binary_port_mock + .add_bids_fetch_res(bids.clone(), state_identifier) + .await; + binary_port_mock + .add_system_registry(state_identifier, registry) + .await; + binary_port_mock + .add_seigniorage_recipients_version_addressable_entity( + None, + state_identifier, + auction_hash, + ) + .await; + binary_port_mock + .add_seigniorage_recipients_version_key_hash(None, state_identifier, auction_hash) + .await; + binary_port_mock + .add_seigniorage_snapshot_under_addressable_entity(state_identifier, auction_hash, None) + .await; + binary_port_mock + .add_seigniorage_snapshot_under_key_hash( + state_identifier, + auction_hash, + Some(stored_value), + ) + .await; + let resp = GetAuctionInfo::do_handle_request(Arc::new(binary_port_mock), None) + .await + .expect("should handle request"); + let bids = bids + .into_iter() + .map(|b| BidKind::Unified(Box::new(b))) + .collect(); + + let expected_validators = BTreeMap::from([( + EraId::new(100), + BTreeMap::from([(public_key_1, U512::from(625))]), + )]); + + assert_eq!( + resp, + GetAuctionInfoResult { + api_version: CURRENT_API_VERSION, + auction_state: AuctionState::new( + *block_header.state_root_hash(), + block_header.height(), + expected_validators, + bids + ), + } + ); + } + + #[tokio::test] + async fn should_read_condor_auction_info_with_addressable_entity_off() { + let rng = &mut TestRng::new(); + let mut binary_port_mock = BinaryPortMock::new(); + let auction_hash: AddressableEntityHash = AddressableEntityHash::new(rng.gen()); + let block_header = TestBlockV1Builder::new() + .protocol_version(SUPPORTED_PROTOCOL_VERSION) + .build_versioned(rng) + .clone_header(); + let registry = BTreeMap::from([(AUCTION.to_string(), auction_hash)]); + let public_key_1 = PublicKey::random(rng); + let public_key_2 = PublicKey::random(rng); + let public_key_3 = PublicKey::random(rng); + let delegator_kind_1 = DelegatorKind::PublicKey(public_key_2); + let delegator_kind_2 = DelegatorKind::PublicKey(public_key_3); + let recipient_v2 = SeigniorageRecipientV2::new( + U512::from(125), + 50, + BTreeMap::from([(delegator_kind_1, U512::from(500))]), + BTreeMap::from([(delegator_kind_2, 75)]), + ); + let recipients_1: BTreeMap = + BTreeMap::from([(public_key_1.clone(), recipient_v2)]); + let v2_recipients: BTreeMap = + BTreeMap::from([(EraId::new(100), recipients_1)]); + let stored_value = StoredValue::CLValue(CLValue::from_t(v2_recipients.clone()).unwrap()); + let state_identifier = Some(casper_types::GlobalStateIdentifier::BlockHeight( + block_header.height(), + )); + let validator_bid = ValidatorBid::empty(PublicKey::random(rng), rng.gen()); + let bid_kind_1 = BidKind::Validator(Box::new(validator_bid)); + let bid_kinds = vec![bid_kind_1]; + binary_port_mock + .add_block_header_req_res(block_header.clone()) + .await; + binary_port_mock + .add_bid_kinds_fetch_res(bid_kinds.clone(), state_identifier) + .await; + binary_port_mock + .add_system_registry(state_identifier, registry) + .await; + binary_port_mock + .add_seigniorage_recipients_version_addressable_entity( + None, + state_identifier, + auction_hash, + ) + .await; + binary_port_mock + .add_seigniorage_recipients_version_key_hash(Some(2), state_identifier, auction_hash) + .await; + binary_port_mock + .add_seigniorage_snapshot_under_addressable_entity(state_identifier, auction_hash, None) + .await; + binary_port_mock + .add_seigniorage_snapshot_under_key_hash( + state_identifier, + auction_hash, + Some(stored_value), + ) + .await; + let resp = GetAuctionInfo::do_handle_request(Arc::new(binary_port_mock), None) + .await + .expect("should handle request"); + let expected_validators = BTreeMap::from([( + EraId::new(100), + BTreeMap::from([(public_key_1, U512::from(625))]), + )]); + + assert_eq!( + resp, + GetAuctionInfoResult { + api_version: CURRENT_API_VERSION, + auction_state: AuctionState::new( + *block_header.state_root_hash(), + block_header.height(), + expected_validators, + bid_kinds + ), + } + ); + } + + #[tokio::test] + async fn should_read_condor_auction_info_with_addressable_entity_on() { + let rng = &mut TestRng::new(); + let mut binary_port_mock = BinaryPortMock::new(); + let auction_hash: AddressableEntityHash = AddressableEntityHash::new(rng.gen()); + let block_header = TestBlockV1Builder::new() + .protocol_version(SUPPORTED_PROTOCOL_VERSION) + .build_versioned(rng) + .clone_header(); + let registry = BTreeMap::from([(AUCTION.to_string(), auction_hash)]); + let public_key_1 = PublicKey::random(rng); + let public_key_2 = PublicKey::random(rng); + let public_key_3 = PublicKey::random(rng); + let delegator_kind_1 = DelegatorKind::PublicKey(public_key_2); + let delegator_kind_2 = DelegatorKind::PublicKey(public_key_3); + let recipient_v2 = SeigniorageRecipientV2::new( + U512::from(125), + 50, + BTreeMap::from([(delegator_kind_1, U512::from(500))]), + BTreeMap::from([(delegator_kind_2, 75)]), + ); + let recipients_1: BTreeMap = + BTreeMap::from([(public_key_1.clone(), recipient_v2)]); + let v2_recipients: BTreeMap = + BTreeMap::from([(EraId::new(100), recipients_1)]); + let stored_value = StoredValue::CLValue(CLValue::from_t(v2_recipients.clone()).unwrap()); + let state_identifier = Some(casper_types::GlobalStateIdentifier::BlockHeight( + block_header.height(), + )); + let validator_bid = ValidatorBid::empty(PublicKey::random(rng), rng.gen()); + let bid_kind_1 = BidKind::Validator(Box::new(validator_bid)); + let bid_kinds = vec![bid_kind_1]; + binary_port_mock + .add_block_header_req_res(block_header.clone()) + .await; + binary_port_mock + .add_bid_kinds_fetch_res(bid_kinds.clone(), state_identifier) + .await; + binary_port_mock + .add_system_registry(state_identifier, registry) + .await; + binary_port_mock + .add_seigniorage_recipients_version_addressable_entity( + Some(2), + state_identifier, + auction_hash, + ) + .await; + binary_port_mock + .add_seigniorage_snapshot_under_addressable_entity( + state_identifier, + auction_hash, + Some(stored_value), + ) + .await; + let resp = GetAuctionInfo::do_handle_request(Arc::new(binary_port_mock), None) + .await + .expect("should handle request"); + let expected_validators = BTreeMap::from([( + EraId::new(100), + BTreeMap::from([(public_key_1, U512::from(625))]), + )]); + + assert_eq!( + resp, + GetAuctionInfoResult { + api_version: CURRENT_API_VERSION, + auction_state: AuctionState::new( + *block_header.state_root_hash(), + block_header.height(), + expected_validators, + bid_kinds + ), + } + ); + } +} diff --git a/rpc_sidecar/src/rpcs/test_utils.rs b/rpc_sidecar/src/rpcs/test_utils.rs new file mode 100644 index 00000000..9c809bdb --- /dev/null +++ b/rpc_sidecar/src/rpcs/test_utils.rs @@ -0,0 +1,219 @@ +use std::{collections::BTreeMap, convert::TryInto, sync::Arc}; + +use async_trait::async_trait; +use casper_binary_port::{ + BinaryRequest, BinaryResponse, BinaryResponseAndRequest, GetRequest, + GlobalStateEntityQualifier, GlobalStateQueryResult, GlobalStateRequest, InformationRequest, +}; +use casper_types::{ + addressable_entity::EntityKindTag, + bytesrepr::ToBytes, + system::auction::{ + Bid, BidKind, SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY, + SEIGNIORAGE_RECIPIENTS_SNAPSHOT_VERSION_KEY, + }, + AddressableEntityHash, BlockHeader, CLValue, GlobalStateIdentifier, Key, KeyTag, + ProtocolVersion, SemVer, StoredValue, +}; +use once_cell::sync::Lazy; +use tokio::sync::Mutex; + +use crate::{ClientError, NodeClient}; + +pub(crate) static PROTOCOL_VERSION: Lazy = + Lazy::new(|| ProtocolVersion::new(SemVer::new(2, 0, 0))); + +pub(crate) struct BinaryPortMock { + request_responses: Arc>>, +} + +impl BinaryPortMock { + pub fn new() -> Self { + Self { + request_responses: Arc::new(Mutex::new(vec![])), + } + } + + pub async fn add_block_header_req_res(&mut self, block_header: BlockHeader) { + let get_request = InformationRequest::BlockHeader(None) + .try_into() + .expect("should create request"); + let req = BinaryRequest::Get(get_request); + let res = BinaryResponse::from_option(Some(block_header), *PROTOCOL_VERSION); + self.when_then(req, res).await; + } + + pub async fn add_bid_kinds_fetch_res( + &mut self, + bid_kinds: Vec, + state_identifier: Option, + ) { + let req = GetRequest::State(Box::new(GlobalStateRequest::new( + state_identifier, + GlobalStateEntityQualifier::AllItems { + key_tag: KeyTag::BidAddr, + }, + ))); + let stored_values: Vec = + bid_kinds.into_iter().map(StoredValue::BidKind).collect(); + let res = BinaryResponse::from_value(stored_values, *PROTOCOL_VERSION); + self.when_then(BinaryRequest::Get(req), res).await; + } + + pub async fn add_bids_fetch_res( + &mut self, + bids: Vec, + state_identifier: Option, + ) { + let req = GetRequest::State(Box::new(GlobalStateRequest::new( + state_identifier, + GlobalStateEntityQualifier::AllItems { + key_tag: KeyTag::Bid, + }, + ))); + + let stored_values: Vec = bids + .into_iter() + .map(|b| StoredValue::Bid(Box::new(b))) + .collect(); + let res = BinaryResponse::from_value(stored_values, *PROTOCOL_VERSION); + self.when_then(BinaryRequest::Get(req), res).await; + } + + pub async fn add_system_registry( + &mut self, + state_identifier: Option, + registry: BTreeMap, + ) { + let req = GetRequest::State(Box::new(GlobalStateRequest::new( + state_identifier, + GlobalStateEntityQualifier::Item { + base_key: Key::SystemEntityRegistry, + path: vec![], + }, + ))); + let cl_value = CLValue::from_t(registry).unwrap(); + let stored_value = StoredValue::CLValue(cl_value); + + let res = BinaryResponse::from_value( + GlobalStateQueryResult::new(stored_value, vec![]), + *PROTOCOL_VERSION, + ); + self.when_then(BinaryRequest::Get(req), res).await; + } + + pub async fn add_seigniorage_recipients_version_addressable_entity( + &mut self, + maybe_seigniorage_recipients_version: Option, + state_identifier: Option, + auction_hash: AddressableEntityHash, + ) { + let base_key = Key::addressable_entity_key(EntityKindTag::System, auction_hash); + let req = GetRequest::State(Box::new(GlobalStateRequest::new( + state_identifier, + GlobalStateEntityQualifier::Item { + base_key, + path: vec![SEIGNIORAGE_RECIPIENTS_SNAPSHOT_VERSION_KEY.to_owned()], + }, + ))); + let res = BinaryResponse::from_option( + maybe_seigniorage_recipients_version.map(|v| { + let cl_value = CLValue::from_t(v).unwrap(); + GlobalStateQueryResult::new(StoredValue::CLValue(cl_value), vec![]) + }), + *PROTOCOL_VERSION, + ); + self.when_then(BinaryRequest::Get(req), res).await; + } + + pub async fn add_seigniorage_recipients_version_key_hash( + &mut self, + maybe_seigniorage_recipients_version: Option, + state_identifier: Option, + auction_hash: AddressableEntityHash, + ) { + let base_key = Key::Hash(auction_hash.value()); + let req = GetRequest::State(Box::new(GlobalStateRequest::new( + state_identifier, + GlobalStateEntityQualifier::Item { + base_key, + path: vec![SEIGNIORAGE_RECIPIENTS_SNAPSHOT_VERSION_KEY.to_owned()], + }, + ))); + let res = BinaryResponse::from_option( + maybe_seigniorage_recipients_version.map(|v| { + let cl_value = CLValue::from_t(v).unwrap(); + GlobalStateQueryResult::new(StoredValue::CLValue(cl_value), vec![]) + }), + *PROTOCOL_VERSION, + ); + self.when_then(BinaryRequest::Get(req), res).await; + } + + pub async fn add_seigniorage_snapshot_under_addressable_entity( + &mut self, + state_identifier: Option, + auction_hash: AddressableEntityHash, + maybe_seigniorage_snapshot: Option, + ) { + let base_key = Key::addressable_entity_key(EntityKindTag::System, auction_hash); + let req = GetRequest::State(Box::new(GlobalStateRequest::new( + state_identifier, + GlobalStateEntityQualifier::Item { + base_key, + path: vec![SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY.to_owned()], + }, + ))); + let res = BinaryResponse::from_option( + maybe_seigniorage_snapshot.map(|v| GlobalStateQueryResult::new(v, vec![])), + *PROTOCOL_VERSION, + ); + self.when_then(BinaryRequest::Get(req), res).await; + } + + pub async fn add_seigniorage_snapshot_under_key_hash( + &mut self, + state_identifier: Option, + auction_hash: AddressableEntityHash, + maybe_seigniorage_snapshot: Option, + ) { + let base_key = Key::Hash(auction_hash.value()); + let req = GetRequest::State(Box::new(GlobalStateRequest::new( + state_identifier, + GlobalStateEntityQualifier::Item { + base_key, + path: vec![SEIGNIORAGE_RECIPIENTS_SNAPSHOT_KEY.to_owned()], + }, + ))); + let res = BinaryResponse::from_option( + maybe_seigniorage_snapshot.map(|v| GlobalStateQueryResult::new(v, vec![])), + *PROTOCOL_VERSION, + ); + self.when_then(BinaryRequest::Get(req), res).await; + } + + pub async fn when_then(&self, when: BinaryRequest, then: BinaryResponse) { + let payload = when.to_bytes().unwrap(); + let response_and_request = BinaryResponseAndRequest::new(then, &payload, 0); + let mut guard = self.request_responses.lock().await; + guard.push((when, response_and_request)); + } +} + +#[async_trait] +impl NodeClient for BinaryPortMock { + async fn send_request( + &self, + req: BinaryRequest, + ) -> Result { + let mut guard = self.request_responses.lock().await; + let (request, response) = guard.remove(0); + if request != req { + panic!( + "Got unexpected request: {:?}. \n\n Expected {:?}", + req, request + ) + } + Ok(response) + } +}