Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions canister/src/rpc_client/cbor/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod rounding_error;
#[cfg(test)]
mod tests;
18 changes: 18 additions & 0 deletions canister/src/rpc_client/cbor/rounding_error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use minicbor::decode::Decoder;
use minicbor::encode::{Encoder, Write};
use sol_rpc_types::RoundingError;

pub fn decode<Ctx>(
d: &mut Decoder<'_>,
_ctx: &mut Ctx,
) -> Result<RoundingError, minicbor::decode::Error> {
d.u64().map(RoundingError::from)
}

pub fn encode<Ctx, W: Write>(
v: &RoundingError,
e: &mut Encoder<W>,
_ctx: &mut Ctx,
) -> Result<(), minicbor::encode::Error<W::Error>> {
e.u64(*v.as_ref())?.ok()
}
31 changes: 31 additions & 0 deletions canister/src/rpc_client/cbor/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use minicbor::{Decode, Encode};
use proptest::prelude::{any, TestCaseError};
use proptest::{prop_assert_eq, proptest};
use sol_rpc_types::RoundingError;

proptest! {
#[test]
fn should_encode_decode_rounding_error(v in any::<u64>()) {
check_roundtrip(&RoundingErrorContainer {
value: RoundingError::from(v),
})
.unwrap();
}
}

#[derive(Eq, PartialEq, Debug, Decode, Encode)]
struct RoundingErrorContainer {
#[cbor(n(0), with = "crate::rpc_client::cbor::rounding_error")]
pub value: RoundingError,
}

pub fn check_roundtrip<T>(v: &T) -> Result<(), TestCaseError>
where
for<'a> T: PartialEq + std::fmt::Debug + Encode<()> + Decode<'a, ()>,
{
let mut buf = vec![];
minicbor::encode(v, &mut buf).expect("encoding should succeed");
let decoded = minicbor::decode(&buf).expect("decoding should succeed");
prop_assert_eq!(v, &decoded);
Ok(())
}
12 changes: 3 additions & 9 deletions canister/src/rpc_client/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod cbor;
pub mod json;
mod sol_rpc;
#[cfg(test)]
Expand All @@ -11,7 +12,6 @@ use crate::{
metrics::MetricRpcMethod,
providers::{request_builder, resolve_rpc_provider, Providers},
rpc_client::sol_rpc::ResponseTransform,
types::RoundingError,
};
use canhttp::{
http::json::JsonRpcRequest,
Expand Down Expand Up @@ -179,10 +179,7 @@ impl GetSlotRequest {
let max_response_bytes = config
.response_size_estimate
.unwrap_or(1024 + HEADER_SIZE_LIMIT);
let rounding_error = config
.rounding_error
.map(RoundingError::from)
.unwrap_or_default();
let rounding_error = config.rounding_error.unwrap_or_default();

Ok(MultiRpcRequest::new(
providers,
Expand Down Expand Up @@ -214,10 +211,7 @@ impl GetRecentPrioritizationFeesRequest {
JsonRpcRequest::new("getRecentPrioritizationFees", params.into()),
max_response_bytes,
ResponseTransform::GetRecentPrioritizationFees {
max_slot_rounding_error: config
.max_slot_rounding_error
.map(RoundingError::new)
.unwrap_or_default(),
max_slot_rounding_error: config.max_slot_rounding_error.unwrap_or_default(),
max_length: config.max_length.unwrap_or(100),
},
ReductionStrategy::from(consensus_strategy),
Expand Down
7 changes: 3 additions & 4 deletions canister/src/rpc_client/sol_rpc/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#[cfg(test)]
mod tests;

use crate::types::RoundingError;
use candid::candid_method;
use canhttp::http::json::JsonRpcResponse;
use ic_cdk::{
Expand All @@ -11,7 +10,7 @@ use ic_cdk::{
use minicbor::{Decode, Encode};
use serde::{de::DeserializeOwned, Serialize};
use serde_json::{from_slice, Value};
use sol_rpc_types::PrioritizationFee;
use sol_rpc_types::{PrioritizationFee, RoundingError};
use solana_clock::Slot;
use std::fmt::Debug;
use strum::EnumIter;
Expand All @@ -29,13 +28,13 @@ pub enum ResponseTransform {
GetBlock,
#[n(3)]
GetRecentPrioritizationFees {
#[n(0)]
#[cbor(n(0), with = "crate::rpc_client::cbor::rounding_error")]
max_slot_rounding_error: RoundingError,
#[n(1)]
max_length: u8,
},
#[n(4)]
GetSlot(#[n(0)] RoundingError),
GetSlot(#[cbor(n(0), with = "crate::rpc_client::cbor::rounding_error")] RoundingError),
#[n(5)]
GetTokenAccountBalance,
#[n(6)]
Expand Down
6 changes: 3 additions & 3 deletions canister/src/rpc_client/sol_rpc/tests.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use crate::{rpc_client::sol_rpc::ResponseTransform, types::RoundingError};
use crate::rpc_client::sol_rpc::ResponseTransform;
use canhttp::http::json::{Id, JsonRpcResponse};
use proptest::proptest;
use serde_json::{from_slice, json, to_vec, Value};

mod normalization_tests {
use super::*;
use sol_rpc_types::RoundingError;
use strum::IntoEnumIterator;

#[test]
Expand Down Expand Up @@ -338,7 +339,6 @@ mod normalization_tests {

mod get_recent_prioritization_fees {
use crate::rpc_client::sol_rpc::ResponseTransform;
use crate::types::RoundingError;
use proptest::arbitrary::any;
use proptest::array::uniform32;
use proptest::prelude::{prop, Strategy};
Expand All @@ -348,7 +348,7 @@ mod get_recent_prioritization_fees {
use rand_chacha::ChaCha20Rng;
use serde::Serialize;
use serde_json::json;
use sol_rpc_types::{PrioritizationFee, Slot};
use sol_rpc_types::{PrioritizationFee, RoundingError, Slot};
use std::ops::RangeInclusive;

#[test]
Expand Down
58 changes: 0 additions & 58 deletions canister/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
mod tests;

use crate::{constants::API_KEY_REPLACE_STRING, validate::validate_api_key};
use derive_more::{From, Into};
use minicbor::{Decode, Encode};
use serde::{Deserialize, Serialize};
use sol_rpc_types::{RegexSubstitution, RpcEndpoint};
use std::{fmt, fmt::Debug};
Expand Down Expand Up @@ -76,59 +74,3 @@ impl OverrideProvider {
}
}
}

/// This type defines a rounding error to use when fetching the current
/// [slot](https://solana.com/docs/references/terminology#slot) from Solana using the JSON-RPC
/// interface, meaning slots will be rounded down to the nearest multiple of this error when
/// being fetched.
///
/// This is done to achieve consensus on the HTTP outcalls whose responses contain Solana slots
/// despite Solana's fast blocktime and hence fast-changing slot value. However, this solution
/// does not guarantee consensus on the slot value across nodes and different consensus rates
/// will be achieved depending on the rounding error value used. A higher rounding error will
/// lead to a higher consensus rate, but also means the slot value may differ more from the actual
/// value on the Solana blockchain. This means, for example, that setting a large rounding error
/// and then fetching the corresponding block with the Solana
/// [`getBlock`](https://solana.com/docs/rpc/http/getblock) RPC method can result in obtaining a
/// block whose hash is too old to use in a valid Solana transaction (see more details about using
/// recent blockhashes [here](https://solana.com/developers/guides/advanced/confirmation#how-does-transaction-expiration-work).
///
/// The default value given by [`RoundingError::default`]
/// has been experimentally shown to achieve a high HTTP outcall consensus rate.
///
/// See the [`RoundingError::round`] method for more details and examples.
#[derive(Debug, Decode, Encode, Clone, Copy, Eq, PartialEq, From, Into)]
pub struct RoundingError(#[n(0)] u64);

impl Default for RoundingError {
fn default() -> Self {
Self(20)
}
}

impl RoundingError {
/// Create a new instance of [`RoundingError`] with the given value.
pub fn new(rounding_error: u64) -> Self {
Self(rounding_error)
}

/// Round the given value down to the nearest multiple of the rounding error.
/// A rounding error of 0 or 1 leads to this method returning the input unchanged.
///
/// # Examples
///
/// ```rust
/// use sol_rpc_canister::types::RoundingError;
///
/// assert_eq!(RoundingError::new(0).round(19), 19);
/// assert_eq!(RoundingError::new(1).round(19), 19);
/// assert_eq!(RoundingError::new(10).round(19), 10);
/// assert_eq!(RoundingError::new(20).round(19), 0);
/// ```
pub fn round(&self, slot: u64) -> u64 {
match self.0 {
0 | 1 => slot,
n => (slot / n) * n,
}
}
}
35 changes: 1 addition & 34 deletions canister/src/types/tests.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{
memory::{init_state, reset_state, State},
providers::{resolve_rpc_provider, PROVIDERS},
types::{ApiKey, OverrideProvider, RoundingError},
types::{ApiKey, OverrideProvider},
};
use proptest::{
prelude::{prop, Strategy},
Expand Down Expand Up @@ -114,36 +114,3 @@ mod override_provider_tests {
)
}
}
mod rounding_error_tests {
use super::*;

#[test]
fn should_round_slot() {
for (rounding_error, slot, rounded) in [
(0, 0, 0),
(0, 13, 13),
(1, 13, 13),
(10, 13, 10),
(10, 100, 100),
(10, 101, 100),
(10, 102, 100),
(10, 103, 100),
(10, 104, 100),
(10, 105, 100),
(10, 106, 100),
(10, 107, 100),
(10, 108, 100),
(10, 109, 100),
(10, 110, 110),
] {
assert_eq!(RoundingError::new(rounding_error).round(slot), rounded);
}
}

proptest! {
#[test]
fn should_not_panic (rounding_error: u64, slot: u64) {
let _result = RoundingError::new(rounding_error).round(slot);
}
}
}
16 changes: 10 additions & 6 deletions libs/client/src/request/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ use sol_rpc_types::{
AccountInfo, CommitmentLevel, ConfirmedBlock, GetAccountInfoParams, GetBalanceParams,
GetBlockCommitmentLevel, GetBlockParams, GetRecentPrioritizationFeesParams,
GetRecentPrioritizationFeesRpcConfig, GetSlotParams, GetSlotRpcConfig,
GetTokenAccountBalanceParams, GetTransactionParams, Lamport, PrioritizationFee, RpcConfig,
RpcResult, RpcSources, SendTransactionParams, Signature, TokenAmount, TransactionInfo,
GetTokenAccountBalanceParams, GetTransactionParams, Lamport, PrioritizationFee, RoundingError,
RpcConfig, RpcResult, RpcSources, SendTransactionParams, Signature, TokenAmount,
TransactionInfo,
};
use solana_account_decoder_client_types::token::UiTokenAmount;
use solana_clock::Slot;
Expand Down Expand Up @@ -479,9 +480,12 @@ impl<Runtime, Params, CandidOutput, Output>
RequestBuilder<Runtime, GetRecentPrioritizationFeesRpcConfig, Params, CandidOutput, Output>
{
/// Change the rounding error for the maximum slot value for a `getRecentPrioritizationFees` request.
pub fn with_max_slot_rounding_error(mut self, rounding_error: u64) -> Self {
pub fn with_max_slot_rounding_error<T: Into<RoundingError>>(
mut self,
rounding_error: T,
) -> Self {
let config = self.request.rpc_config_mut().get_or_insert_default();
config.max_slot_rounding_error = Some(rounding_error);
config.max_slot_rounding_error = Some(rounding_error.into());
self
}

Expand All @@ -497,9 +501,9 @@ impl<Runtime, Params, CandidOutput, Output>
RequestBuilder<Runtime, GetSlotRpcConfig, Params, CandidOutput, Output>
{
/// Change the rounding error for `getSlot` request.
pub fn with_rounding_error(mut self, rounding_error: u64) -> Self {
pub fn with_rounding_error<T: Into<RoundingError>>(mut self, rounding_error: T) -> Self {
let config = self.request.rpc_config_mut().get_or_insert_default();
config.rounding_error = Some(rounding_error);
config.rounding_error = Some(rounding_error.into());
self
}
}
Expand Down
4 changes: 2 additions & 2 deletions libs/types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ pub use response::MultiRpcResult;
pub use rpc_client::{
ConsensusStrategy, GetRecentPrioritizationFeesRpcConfig, GetSlotRpcConfig, HttpHeader,
HttpOutcallError, JsonRpcError, OverrideProvider, ProviderError, RegexString,
RegexSubstitution, RpcAccess, RpcAuth, RpcConfig, RpcEndpoint, RpcError, RpcResult, RpcSource,
RpcSources, SolanaCluster, SupportedRpcProvider, SupportedRpcProviderId,
RegexSubstitution, RoundingError, RpcAccess, RpcAuth, RpcConfig, RpcEndpoint, RpcError,
RpcResult, RpcSource, RpcSources, SolanaCluster, SupportedRpcProvider, SupportedRpcProviderId,
};
pub use solana::{
account::{AccountData, AccountEncoding, AccountInfo, ParsedAccount},
Expand Down
Loading