Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improving efficiency and functionality #148

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ Add the following configuration to your `dfx.json` file (replace the
Make sure you have the following installed:

- [Rust](https://www.rust-lang.org/learn/get-started)
- [DFINITY SDK](https://sdk.dfinity.org/docs/quickstart/local-quickstart.html)
- [Docker](https://www.docker.com/get-started/) (Optional for [reproducible builds](#reproducible-builds))
- [PocketIC](https://github.com/dfinity/pocketic) (Optional for testing)
- [DFINITY SDK](https://sdk.dfinity.org/docs/quickstart/local-quickstart.html)

### Building the code

Expand Down
Binary file modified ic-solana-rpc.wasm.gz
Binary file not shown.
Binary file modified ic-solana-wallet.wasm.gz
Binary file not shown.
12 changes: 9 additions & 3 deletions src/ic-solana-rpc/ic-solana-rpc.did
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ type RpcProgramAccountsConfig = record {
commitment : opt CommitmentLevel;
};
type RpcSendTransactionConfig = record {
encoding : opt UiTransactionEncoding;
encoding : opt TransactionBinaryEncoding;
preflightCommitment : opt CommitmentLevel;
maxRetries : opt nat64;
minContextSlot : opt nat64;
Expand Down Expand Up @@ -383,7 +383,7 @@ type RpcSimulateTransactionAccountsConfig = record {
};
type RpcSimulateTransactionConfig = record {
replaceRecentBlockhash : bool;
encoding : opt UiTransactionEncoding;
encoding : opt TransactionBinaryEncoding;
innerInstructions : bool;
accounts : opt RpcSimulateTransactionAccountsConfig;
sigVerify : bool;
Expand Down Expand Up @@ -832,7 +832,13 @@ service : (InitArgs) -> {
opt RpcContextConfig,
) -> (Result_34);
sol_minimumLedgerSlot : (RpcServices, opt RpcConfig) -> (Result_2);
sol_requestAirdrop : (RpcServices, opt RpcConfig, text, nat64) -> (Result);
sol_requestAirdrop : (
RpcServices,
opt RpcConfig,
text,
nat64,
opt CommitmentConfig,
) -> (Result);
sol_sendTransaction : (
RpcServices,
opt RpcConfig,
Expand Down
30 changes: 16 additions & 14 deletions src/ic-solana-rpc/src/constants.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
// HTTP outcall cost calculation
// See https://internetcomputer.org/docs/current/developer-docs/gas-cost#special-features
/// HTTP outcall cost calculation
/// See https://internetcomputer.org/docs/current/developer-docs/gas-cost#cycles-price-breakdown
/// and https://github.com/dfinity/ic/blob/b9c732eace54b47292969e77801e22317ae182a2/rs/config/src/subnet_config.rs#L442
pub const INGRESS_OVERHEAD_BYTES: u128 = 100;
pub const INGRESS_MESSAGE_RECEIVED_COST: u128 = 1_200_000;
pub const INGRESS_MESSAGE_BYTE_RECEIVED_COST: u128 = 2_000;
pub const HTTP_OUTCALL_REQUEST_BASE_COST: u128 = 3_000_000;
pub const HTTP_OUTCALL_REQUEST_PER_NODE_COST: u128 = 60_000;
pub const HTTP_OUTCALL_REQUEST_COST_PER_BYTE: u128 = 400;
pub const HTTP_OUTCALL_RESPONSE_COST_PER_BYTE: u128 = 800;
pub const INGRESS_MESSAGE_RECEPTION_FEE: u128 = 1_200_000;
pub const INGRESS_BYTE_RECEPTION_FEE: u128 = 2_000;
pub const HTTP_REQUEST_LINEAR_BASELINE_FEE: u128 = 3_000_000;
pub const HTTP_REQUEST_QUADRATIC_BASELINE_FEE: u128 = 60_000;
pub const HTTP_REQUEST_PER_BYTE_FEE: u128 = 400;
pub const HTTP_RESPONSE_PER_BYTE_FEE: u128 = 800;

// Additional cost of operating the canister per subnet node
/// Additional cost of operating the canister per subnet node
pub const CANISTER_OVERHEAD: u128 = 1_000_000;

// Cycles which must be passed with each RPC request in case the
// third-party JSON-RPC prices increase in the future (currently always refunded)
/// Cycles which must be passed with each RPC request in case the
/// third-party JSON-RPC prices increase in the future (currently always refunded)
pub const COLLATERAL_CYCLES_PER_NODE: u128 = 10_000_000;

// Minimum number of bytes charged for a URL; improves consistency of costs between providers
/// Minimum number of bytes charged for a URL; improves consistency of costs between providers
pub const RPC_URL_COST_BYTES: u32 = 256;

// pub const MINIMUM_WITHDRAWAL_CYCLES: u128 = 1_000_000_000;
/// Default subnet size which is used to scale cycles cost according to a subnet replication factor.
pub const DEFAULT_SUBNET_SIZE: u32 = 13;

pub const NODES_IN_SUBNET: u32 = 34;

pub const PROVIDER_ID_MAX_SIZE: u32 = 128;

// List of hosts which are not allowed to be used as RPC providers
/// List of hosts which are not allowed to be used as RPC providers
pub const RPC_HOSTS_BLOCKLIST: &[&str] = &[];
67 changes: 49 additions & 18 deletions src/ic-solana-rpc/src/http.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use ic_canisters_http_types::{HttpRequest, HttpResponse, HttpResponseBuilder};
use ic_cdk::api::management_canister::http_request::TransformContext;
use ic_solana::{
constants::HTTP_MAX_SIZE,
logs::{Log, Priority, Sort},
Expand All @@ -9,10 +8,9 @@ use ic_solana::{

use crate::{
constants::{
CANISTER_OVERHEAD, COLLATERAL_CYCLES_PER_NODE, HTTP_OUTCALL_REQUEST_BASE_COST,
HTTP_OUTCALL_REQUEST_COST_PER_BYTE, HTTP_OUTCALL_REQUEST_PER_NODE_COST, HTTP_OUTCALL_RESPONSE_COST_PER_BYTE,
INGRESS_MESSAGE_BYTE_RECEIVED_COST, INGRESS_MESSAGE_RECEIVED_COST, INGRESS_OVERHEAD_BYTES, NODES_IN_SUBNET,
RPC_URL_COST_BYTES,
COLLATERAL_CYCLES_PER_NODE, DEFAULT_SUBNET_SIZE, HTTP_REQUEST_LINEAR_BASELINE_FEE, HTTP_REQUEST_PER_BYTE_FEE,
HTTP_REQUEST_QUADRATIC_BASELINE_FEE, HTTP_RESPONSE_PER_BYTE_FEE, INGRESS_BYTE_RECEPTION_FEE,
INGRESS_MESSAGE_RECEPTION_FEE, INGRESS_OVERHEAD_BYTES, NODES_IN_SUBNET, RPC_URL_COST_BYTES,
},
providers::find_provider,
state::read_state,
Expand All @@ -30,7 +28,7 @@ pub fn rpc_client(source: RpcServices, config: Option<RpcConfig>) -> RpcClient {
RpcServices::Localnet => Cluster::Localnet,
_ => unreachable!(),
};
vec![get_provider_rpc_api(&cluster.to_string())]
vec![get_provider_rpc_api(cluster.as_ref())]
}
RpcServices::Provider(ids) => ids.iter().map(|id| get_provider_rpc_api(id)).collect(),
RpcServices::Custom(apis) => apis, // Use the custom APIs directly
Expand All @@ -50,7 +48,7 @@ pub fn rpc_client(source: RpcServices, config: Option<RpcConfig>) -> RpcClient {
(cycles_cost, get_cost_with_collateral(cycles_cost))
}),
host_validator: Some(|host| validate_hostname(host).is_ok()),
transform_context: Some(TransformContext::from_name("__transform_json_rpc".to_owned(), vec![])),
transform_function_name: Some("__transform_json_rpc".to_owned()),
is_demo_active: s.is_demo_active,
use_compression: false,
};
Expand All @@ -65,18 +63,22 @@ fn get_provider_rpc_api(provider_id: &str) -> RpcApi {
.api()
}

/// Calculates the cost of sending a JSON-RPC request using HTTP outcalls.
/// Calculates the baseline cost of sending a request using HTTP outcalls.
/// The corresponding code in replica:
/// https://github.com/dfinity/ic/blob/master/rs/cycles_account_manager/src/lib.rs#L1153
pub fn get_http_request_cost(payload_size_bytes: u64, max_response_bytes: u64) -> u128 {
let nodes_in_subnet = NODES_IN_SUBNET as u128;
let ingress_bytes = payload_size_bytes as u128 + RPC_URL_COST_BYTES as u128 + INGRESS_OVERHEAD_BYTES;
let cost_per_node = INGRESS_MESSAGE_RECEIVED_COST
+ INGRESS_MESSAGE_BYTE_RECEIVED_COST * ingress_bytes
+ HTTP_OUTCALL_REQUEST_BASE_COST
+ HTTP_OUTCALL_REQUEST_PER_NODE_COST * nodes_in_subnet
+ HTTP_OUTCALL_REQUEST_COST_PER_BYTE * payload_size_bytes as u128
+ HTTP_OUTCALL_RESPONSE_COST_PER_BYTE * max_response_bytes as u128
+ CANISTER_OVERHEAD;
cost_per_node * nodes_in_subnet
let subnet_size = NODES_IN_SUBNET as u128;
let request_size = payload_size_bytes as u128;
let response_size = max_response_bytes as u128;
let ingress_size = request_size + RPC_URL_COST_BYTES as u128 + INGRESS_OVERHEAD_BYTES;

(INGRESS_MESSAGE_RECEPTION_FEE / DEFAULT_SUBNET_SIZE as u128
+ INGRESS_BYTE_RECEPTION_FEE / DEFAULT_SUBNET_SIZE as u128 * ingress_size
+ HTTP_REQUEST_LINEAR_BASELINE_FEE
+ HTTP_REQUEST_QUADRATIC_BASELINE_FEE * subnet_size
+ HTTP_REQUEST_PER_BYTE_FEE * request_size
+ HTTP_RESPONSE_PER_BYTE_FEE * response_size)
* subnet_size
}

/// Calculate the cost + collateral cycles for an HTTP request.
Expand Down Expand Up @@ -162,3 +164,32 @@ pub fn serve_logs(request: HttpRequest) -> HttpResponse {
.with_body_and_content_length(log.serialize_logs(MAX_BODY_SIZE))
.build()
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_request_cost() {
let payload = r#"{"jsonrpc":"2.0","method":"sol_getHealth","params":[],"id":1}"#;
let base_cost = get_http_request_cost(payload.len() as u64, 1000);
let base_cost_10_extra_bytes = get_http_request_cost(payload.len() as u64 + 10, 1000);
let estimated_cost_10_extra_bytes = base_cost
+ 10 * (HTTP_REQUEST_PER_BYTE_FEE + INGRESS_BYTE_RECEPTION_FEE / DEFAULT_SUBNET_SIZE as u128)
* NODES_IN_SUBNET as u128;
assert_eq!(base_cost_10_extra_bytes, estimated_cost_10_extra_bytes);
}

#[test]
fn test_candid_rpc_cost() {
assert_eq!(
[
get_http_request_cost(0, 0),
get_http_request_cost(123, 123),
get_http_request_cost(123, 4567890),
get_http_request_cost(890, 4567890),
],
[176350350, 182008596, 124425270996, 124439692130]
);
}
}
13 changes: 5 additions & 8 deletions src/ic-solana-rpc/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ use ic_solana_rpc::{
providers::{do_register_provider, do_unregister_provider, do_update_provider},
state::{read_state, replace_state, InitArgs},
types::{RegisterProviderArgs, UpdateProviderArgs},
utils::{parse_pubkey, parse_pubkeys, parse_signature, parse_signatures},
utils::{parse_pubkey, parse_pubkeys, parse_signature, parse_signatures, transform_http_request},
};

/// Returns all information associated with the account of the provided Pubkey.
Expand Down Expand Up @@ -657,10 +657,11 @@ pub async fn sol_request_airdrop(
config: Option<RpcConfig>,
pubkey: String,
lamports: u64,
params: Option<CommitmentConfig>,
) -> RpcResult<String> {
let client = rpc_client(source, config);
let pubkey = parse_pubkey(&pubkey)?;
client.request_airdrop(&pubkey, lamports).await
client.request_airdrop(&pubkey, lamports, params).await
}

/// Submits a signed transaction to the cluster for processing.
Expand Down Expand Up @@ -832,12 +833,8 @@ fn get_metrics() -> Metrics {
///
/// * `args` - Transformation arguments containing the HTTP response.
#[query(hidden = true)]
fn __transform_json_rpc(mut args: TransformArgs) -> HttpResponse {
// The response header contains non-deterministic fields that make it impossible to reach
// consensus! Errors seem deterministic and do not contain data that can break consensus.
// Clear non-deterministic fields from the response headers.
args.response.headers.clear();
args.response
fn __transform_json_rpc(args: TransformArgs) -> HttpResponse {
transform_http_request(args)
}

#[ic_cdk::init]
Expand Down
4 changes: 2 additions & 2 deletions src/ic-solana-rpc/src/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ use crate::{
types::PrincipalStorable,
};

const AUTH_MEMORY_ID: MemoryId = MemoryId::new(2);
const PROVIDERS_MEMORY_ID: MemoryId = MemoryId::new(3);
pub const AUTH_MEMORY_ID: MemoryId = MemoryId::new(2);
pub const PROVIDERS_MEMORY_ID: MemoryId = MemoryId::new(3);

pub type StableMemory = VirtualMemory<DefaultMemoryImpl>;
pub type AuthMemory = StableBTreeMap<PrincipalStorable, AuthSet, StableMemory>;
Expand Down
45 changes: 22 additions & 23 deletions src/ic-solana-rpc/src/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,49 +133,48 @@ pub fn do_register_provider(caller: Principal, args: RegisterProviderArgs) {
});
}

/// Unregister provider. The caller must be the owner or administrator.
/// Unregister provider.
/// The caller must be the owner or administrator.
pub fn do_unregister_provider(caller: Principal, provider_id: &str) -> bool {
let is_admin = is_controller(&caller);
let is_manager = is_authorized(&caller, Auth::Manage);
mutate_state(|s| {
let id = ProviderId::new(provider_id);
if let Some(provider) = s.rpc_providers.get(&id) {
if provider.owner == caller || is_controller(&caller) || is_manager {
log!(INFO, "[{}] Unregistering provider: {:?}", caller, provider_id);
s.rpc_providers.remove(&id).is_some()
} else {
if !(provider.owner == caller || is_admin || is_manager) {
ic_cdk::trap("Unauthorized");
}
log!(INFO, "[{}] Unregistering provider: {:?}", caller, provider_id);
s.rpc_providers.remove(&id).is_some()
} else {
false
}
})
}

/// Change provider details. The caller must be the owner or administrator.
/// Change provider details.
/// The caller must be the owner or administrator.
pub fn do_update_provider(caller: Principal, args: UpdateProviderArgs) {
let provider_id = ProviderId::new(args.id);
let is_admin = is_controller(&caller);
let is_manager = is_authorized(&caller, Auth::Manage);
let provider_id = ProviderId::new(&args.id);
mutate_state(|s| match s.rpc_providers.get(&provider_id) {
Some(mut provider) => {
if provider.owner == caller {
if args.url.is_some() {
ic_cdk::trap("You are not authorized to update the `url` field");
}
if let Some(auth) = args.auth {
provider.auth = Some(auth);
}
s.rpc_providers.insert(provider_id, provider);
} else if is_controller(&caller) || is_manager {
if let Some(url) = args.url {
if !(provider.owner == caller || is_admin || is_manager) {
ic_cdk::trap("Unauthorized");
}
log!(INFO, "[{}] Updating provider: {:?}", caller, args);
if let Some(url) = args.url {
if is_admin {
provider.url = url;
} else {
ic_cdk::trap("You are not authorized to update the `url` field");
}
if let Some(auth) = args.auth {
provider.auth = Some(auth);
}
s.rpc_providers.insert(provider_id, provider);
} else {
ic_cdk::trap("Unauthorized");
}
if let Some(auth) = args.auth {
provider.auth = Some(auth);
}
s.rpc_providers.insert(provider_id, provider)
}
None => ic_cdk::trap("Provider not found"),
});
Expand Down
2 changes: 1 addition & 1 deletion src/ic-solana-rpc/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pub struct RegisterProviderArgs {
pub auth: Option<RpcAuth>,
}

#[derive(Clone, CandidType, Deserialize)]
#[derive(Clone, Debug, CandidType, Deserialize)]
pub struct UpdateProviderArgs {
/// The id of the provider to update
pub id: String,
Expand Down
30 changes: 30 additions & 0 deletions src/ic-solana-rpc/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::str::FromStr;

use ic_cdk::api::management_canister::http_request::{HttpResponse, TransformArgs};
use ic_solana::{
request::RpcRequest,
rpc_client::{RpcError, RpcResult},
types::{Pubkey, Signature},
};
Expand Down Expand Up @@ -45,6 +47,34 @@ pub fn parse_signatures(signatures: Vec<String>) -> RpcResult<Vec<Signature>> {
signatures.iter().map(|s| parse_signature(s)).collect()
}

pub fn transform_http_request(mut args: TransformArgs) -> HttpResponse {
// Remove headers (which may contain a timestamp) for consensus
args.response.headers.clear();

let response = serde_json::from_slice::<serde_json::Value>(&args.response.body).ok();
if response.is_none() {
return args.response;
}

let mut response = response.unwrap();

let request = serde_json::from_slice::<serde_json::Value>(&args.context).unwrap_or_default();

if request["method"] == RpcRequest::GetEpochInfo.to_string() {
response["result"]["absoluteSlot"] = serde_json::Value::Number(0.into());
}

args.response.body = serde_json::to_vec(&response).unwrap_or(args.response.body);
args.response

// HttpResponse {
// status: args.response.status,
// body: canonicalize_json(&args.response.body).unwrap_or(args.response.body),
// // Remove headers (which may contain a timestamp) for consensus
// headers: vec![],
// }
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
Loading
Loading