From 0aae62a2158eb2c4c234b0a88b27802639715c53 Mon Sep 17 00:00:00 2001 From: 0xZensh Date: Wed, 17 Jul 2024 20:49:19 +0800 Subject: [PATCH] feat: support receiving cycles --- Cargo.lock | 88 +++++++++---------- Cargo.toml | 2 +- README.md | 4 +- examples/eth-canister-lite/src/jsonrpc.rs | 21 +++-- src/idempotent-proxy-canister/README.md | 11 +++ .../idempotent-proxy-canister.did | 18 +++- src/idempotent-proxy-canister/src/cycles.rs | 75 ++++++++++++++++ src/idempotent-proxy-canister/src/init.rs | 16 ++++ src/idempotent-proxy-canister/src/lib.rs | 83 ++++++++++++++--- src/idempotent-proxy-canister/src/store.rs | 40 ++++++++- 10 files changed, 288 insertions(+), 70 deletions(-) create mode 100644 src/idempotent-proxy-canister/src/cycles.rs diff --git a/Cargo.lock b/Cargo.lock index a156124..8559082 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,7 +77,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.69", + "syn 2.0.71", ] [[package]] @@ -166,7 +166,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.69", + "syn 2.0.71", ] [[package]] @@ -289,9 +289,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" [[package]] name = "candid" @@ -325,14 +325,14 @@ dependencies = [ "lazy_static", "proc-macro2", "quote", - "syn 2.0.69", + "syn 2.0.71", ] [[package]] name = "cc" -version = "1.0.105" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5208975e568d83b6b05cc0a063c8e7e9acc2b43bee6da15616a5b73e109d7437" +checksum = "324c74f2155653c90b04f25b2a47a8a631360cb908f92a772695f430c7e31052" [[package]] name = "cfg-if" @@ -470,7 +470,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.69", + "syn 2.0.71", ] [[package]] @@ -731,7 +731,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.69", + "syn 2.0.71", ] [[package]] @@ -887,9 +887,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http", @@ -922,9 +922,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4fe55fb7a772d59a5ff1dfbff4fe0258d19b89fec4b233e75d35d5d2316badc" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "futures-channel", @@ -951,7 +951,7 @@ dependencies = [ "http", "hyper", "hyper-util", - "rustls 0.23.10", + "rustls 0.23.11", "rustls-pki-types", "tokio", "tokio-rustls 0.26.0", @@ -1026,7 +1026,7 @@ dependencies = [ "quote", "serde", "serde_tokenstream", - "syn 2.0.69", + "syn 2.0.71", ] [[package]] @@ -1073,7 +1073,7 @@ dependencies = [ [[package]] name = "idempotent-proxy-canister" -version = "1.0.3" +version = "1.1.0" dependencies = [ "base64", "bytes", @@ -1092,7 +1092,7 @@ dependencies = [ [[package]] name = "idempotent-proxy-server" -version = "1.0.3" +version = "1.1.0" dependencies = [ "anyhow", "async-trait", @@ -1116,7 +1116,7 @@ dependencies = [ [[package]] name = "idempotent-proxy-types" -version = "1.0.3" +version = "1.1.0" dependencies = [ "anyhow", "async-trait", @@ -1381,7 +1381,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.69", + "syn 2.0.71", ] [[package]] @@ -1452,7 +1452,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.10", + "rustls 0.23.11", "thiserror", "tokio", "tracing", @@ -1468,7 +1468,7 @@ dependencies = [ "rand", "ring", "rustc-hash", - "rustls 0.23.10", + "rustls 0.23.11", "slab", "thiserror", "tinyvec", @@ -1529,9 +1529,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ "bitflags", ] @@ -1562,7 +1562,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.10", + "rustls 0.23.11", "rustls-pemfile", "rustls-pki-types", "serde", @@ -1668,9 +1668,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.10" +version = "0.23.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" +checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0" dependencies = [ "once_cell", "ring", @@ -1791,7 +1791,7 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", - "syn 2.0.69", + "syn 2.0.71", ] [[package]] @@ -1833,7 +1833,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.69", + "syn 2.0.71", ] [[package]] @@ -2064,9 +2064,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.69" +version = "2.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201fcda3845c23e8212cd466bfebf0bd20694490fc0356ae8e428e0824a915a6" +checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" dependencies = [ "proc-macro2", "quote", @@ -2087,29 +2087,29 @@ checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.69", + "syn 2.0.71", ] [[package]] name = "tinyvec" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6b6a2fb3a985e99cebfaefa9faa3024743da73304ca1c683a36429613d3d22" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] @@ -2122,9 +2122,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.0" +version = "1.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df" dependencies = [ "backtrace", "bytes", @@ -2147,7 +2147,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.69", + "syn 2.0.71", ] [[package]] @@ -2166,7 +2166,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.10", + "rustls 0.23.11", "rustls-pki-types", "tokio", ] @@ -2232,7 +2232,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.69", + "syn 2.0.71", ] [[package]] @@ -2390,7 +2390,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.69", + "syn 2.0.71", "wasm-bindgen-shared", ] @@ -2424,7 +2424,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.69", + "syn 2.0.71", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/Cargo.toml b/Cargo.toml index 4c239a4..ae50995 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ strip = true opt-level = 's' [workspace.package] -version = "1.0.3" +version = "1.1.0" edition = "2021" repository = "https://github.com/ldclabs/idempotent-proxy" keywords = ["idempotent", "reverse", "proxy", "icp"] diff --git a/README.md b/README.md index 04b338b..6e915f0 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,9 @@ This service can be used to proxy [HTTPS outcalls](https://internetcomputer.org/ - Deployable with Docker or Cloudflare Worker - On-chain Idempotent Proxy service on the ICP -## Packages +## Libraries -| Package | Description | +| Library | Description | | :----------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------- | | [idempotent-proxy-server](https://github.com/ldclabs/idempotent-proxy/tree/main/src/idempotent-proxy-server) | Idempotent Proxy implemented in Rust. | | [idempotent-proxy-cf-worker](https://github.com/ldclabs/idempotent-proxy/tree/main/src/idempotent-proxy-cf-worker) | Idempotent Proxy implemented as Cloudflare Worker. | diff --git a/examples/eth-canister-lite/src/jsonrpc.rs b/examples/eth-canister-lite/src/jsonrpc.rs index 53214a5..a5936d7 100644 --- a/examples/eth-canister-lite/src/jsonrpc.rs +++ b/examples/eth-canister-lite/src/jsonrpc.rs @@ -130,14 +130,19 @@ impl EthereumRPC { transform: None, }; - let (res,): (HttpResponse,) = ic_cdk::call(self.proxy, proxy_method, (request,)) - .await - .map_err(|(code, msg)| { - format!( - "failed to call {} on {:?}, code: {}, message: {}", - proxy_method, &self.proxy, code as u32, msg - ) - })?; + let (res,): (HttpResponse,) = ic_cdk::api::call::call_with_payment128( + self.proxy, + proxy_method, + (request,), + 1_000_000_000, // max cycles, unspent cycles will be refunded + ) + .await + .map_err(|(code, msg)| { + format!( + "failed to call {} on {:?}, code: {}, message: {}", + proxy_method, &self.proxy, code as u32, msg + ) + })?; if res.status >= 200u64 && res.status < 300u64 { Ok(res.body) diff --git a/src/idempotent-proxy-canister/README.md b/src/idempotent-proxy-canister/README.md index 91a0fa8..25c13b7 100644 --- a/src/idempotent-proxy-canister/README.md +++ b/src/idempotent-proxy-canister/README.md @@ -29,11 +29,22 @@ dfx deploy idempotent-proxy-canister --argument "(opt variant {Init = record { ecdsa_key_name = \"dfx_test_key\"; proxy_token_refresh_interval = 3600; + subnet_size = 1; + service_fee = 10_000_000; } })" dfx canister call idempotent-proxy-canister get_state '()' +# upgrade +dfx deploy idempotent-proxy-canister --argument "(opt variant {Upgrade = + record { + proxy_token_refresh_interval = null; + subnet_size = opt 13; + service_fee = opt 100_000_000; + } +})" + # 3 same agents for testing dfx canister call idempotent-proxy-canister admin_set_agents ' (vec { diff --git a/src/idempotent-proxy-canister/idempotent-proxy-canister.did b/src/idempotent-proxy-canister/idempotent-proxy-canister.did index 34aba08..1c3c092 100644 --- a/src/idempotent-proxy-canister/idempotent-proxy-canister.did +++ b/src/idempotent-proxy-canister/idempotent-proxy-canister.did @@ -21,26 +21,36 @@ type HttpResponse = record { headers : vec HttpHeader; }; type InitArgs = record { + service_fee : nat64; ecdsa_key_name : text; proxy_token_refresh_interval : nat64; + subnet_size : nat64; }; type Result = variant { Ok : bool; Err : text }; type Result_1 = variant { Ok; Err : text }; type Result_2 = variant { Ok : State; Err }; type State = record { proxy_token_public_key : text; + service_fee : nat64; ecdsa_key_name : text; managers : vec principal; allowed_callers : vec principal; + uncollectible_cycles : nat; agents : vec Agent; + incoming_cycles : nat; proxy_token_refresh_interval : nat64; + subnet_size : nat64; }; type TransformArgs = record { context : blob; response : HttpResponse }; type TransformContext = record { function : func (TransformArgs) -> (HttpResponse) query; context : blob; }; -type UpgradeArgs = record { proxy_token_refresh_interval : opt nat64 }; +type UpgradeArgs = record { + service_fee : opt nat64; + proxy_token_refresh_interval : opt nat64; + subnet_size : opt nat64; +}; service : (opt ChainArgs) -> { admin_add_caller : (principal) -> (Result); admin_remove_caller : (principal) -> (Result); @@ -49,7 +59,9 @@ service : (opt ChainArgs) -> { get_state : () -> (Result_2) query; parallel_call_all_ok : (CanisterHttpRequestArgument) -> (HttpResponse); parallel_call_any_ok : (CanisterHttpRequestArgument) -> (HttpResponse); + parallel_call_cost : (CanisterHttpRequestArgument) -> (nat) query; proxy_http_request : (CanisterHttpRequestArgument) -> (HttpResponse); - validate_admin_set_agents : (vec Agent) -> (Result_1) query; - validate_admin_set_managers : (vec principal) -> (Result_1) query; + proxy_http_request_cost : (CanisterHttpRequestArgument) -> (nat) query; + validate_admin_set_agents : (vec Agent) -> (Result_1); + validate_admin_set_managers : (vec principal) -> (Result_1); } diff --git a/src/idempotent-proxy-canister/src/cycles.rs b/src/idempotent-proxy-canister/src/cycles.rs new file mode 100644 index 0000000..21ba0b9 --- /dev/null +++ b/src/idempotent-proxy-canister/src/cycles.rs @@ -0,0 +1,75 @@ +use ic_cdk::api::management_canister::http_request::{CanisterHttpRequestArgument, HttpResponse}; + +#[derive(Clone)] +pub struct Calculator { + pub subnet_size: u64, + pub service_fee: u64, +} + +// https://github.com/internet-computer-protocol/evm-rpc-canister/blob/main/src/accounting.rs#L34 +impl Calculator { + // HTTP outcall cost calculation + // See https://internetcomputer.org/docs/current/developer-docs/gas-cost#special-features + pub const INGRESS_MESSAGE_RECEIVED_COST: u64 = 1_200_000; + pub const INGRESS_MESSAGE_BYTE_RECEIVED_COST: u64 = 2_000; + pub const HTTP_OUTCALL_REQUEST_BASE_COST: u64 = 3_000_000; + pub const HTTP_OUTCALL_REQUEST_PER_NODE_COST: u64 = 60_000; + pub const HTTP_OUTCALL_REQUEST_COST_PER_BYTE: u64 = 400; + pub const HTTP_OUTCALL_RESPONSE_COST_PER_BYTE: u64 = 800; + // Additional headers added to the request + pub const HTTP_OUTCALL_REQUEST_OVERHEAD_BYTES: u64 = 150; + // Additional cost of operating the canister per subnet node + pub const CANISTER_OVERHEAD: u64 = 1_000_000; + + pub fn count_request_bytes(&self, req: &CanisterHttpRequestArgument) -> usize { + if self.subnet_size == 0 { + return 0; + } + let mut total = req.url.len() + req.body.as_ref().map(|v| v.len()).unwrap_or_default(); + for header in &req.headers { + total += header.name.len() + header.value.len() + 3; + } + total + } + + pub fn count_response_bytes(&self, res: &HttpResponse) -> usize { + if self.subnet_size == 0 { + return 0; + } + let mut total = 4 + res.body.as_slice().len(); + for header in &res.headers { + total += header.name.len() + header.value.len() + 3; + } + total + } + + pub fn ingress_cost(&self, ingress_bytes: usize) -> u128 { + if self.subnet_size == 0 { + return 0; + } + let cost_per_node = Self::INGRESS_MESSAGE_RECEIVED_COST + + Self::INGRESS_MESSAGE_BYTE_RECEIVED_COST * ingress_bytes as u64; + cost_per_node as u128 * self.subnet_size as u128 + } + + pub fn http_outcall_request_cost(&self, request_bytes: usize, duplicates: usize) -> u128 { + if self.subnet_size == 0 { + return 0; + } + let cost_per_node = Self::HTTP_OUTCALL_REQUEST_BASE_COST + + Self::HTTP_OUTCALL_REQUEST_PER_NODE_COST * self.subnet_size + + Self::HTTP_OUTCALL_REQUEST_COST_PER_BYTE + * (Self::HTTP_OUTCALL_REQUEST_OVERHEAD_BYTES + request_bytes as u64) + + Self::CANISTER_OVERHEAD; + self.service_fee as u128 + + cost_per_node as u128 * (self.subnet_size * duplicates as u64) as u128 + } + + pub fn http_outcall_response_cost(&self, response_bytes: usize, duplicates: usize) -> u128 { + if self.subnet_size == 0 { + return 0; + } + let cost_per_node = Self::HTTP_OUTCALL_RESPONSE_COST_PER_BYTE * response_bytes as u64; + cost_per_node as u128 * (self.subnet_size * duplicates as u64) as u128 + } +} diff --git a/src/idempotent-proxy-canister/src/init.rs b/src/idempotent-proxy-canister/src/init.rs index 344c18d..f4cf7a9 100644 --- a/src/idempotent-proxy-canister/src/init.rs +++ b/src/idempotent-proxy-canister/src/init.rs @@ -14,11 +14,15 @@ pub enum ChainArgs { pub struct InitArgs { ecdsa_key_name: String, // Use "dfx_test_key" for local replica and "test_key_1" for a testing key for testnet and mainnet proxy_token_refresh_interval: u64, // seconds + subnet_size: u64, // set to 0 to disable receiving cycles + service_fee: u64, // in cycles } #[derive(Clone, Debug, CandidType, Deserialize)] pub struct UpgradeArgs { proxy_token_refresh_interval: Option, // seconds + subnet_size: Option, + service_fee: Option, // in cycles } #[ic_cdk::init] @@ -27,11 +31,17 @@ fn init(args: Option) { ChainArgs::Init(args) => { store::state::with_mut(|s| { s.ecdsa_key_name = args.ecdsa_key_name; + s.subnet_size = args.subnet_size; s.proxy_token_refresh_interval = if args.proxy_token_refresh_interval >= 10 { args.proxy_token_refresh_interval } else { 3600 }; + s.service_fee = if args.service_fee > 0 { + args.service_fee + } else { + 100_000_000 + }; }); } ChainArgs::Upgrade(_) => { @@ -73,6 +83,12 @@ fn post_upgrade(args: Option) { s.proxy_token_refresh_interval = proxy_token_refresh_interval; } + if let Some(subnet_size) = args.subnet_size { + s.subnet_size = subnet_size; + } + if let Some(service_fee) = args.service_fee { + s.service_fee = service_fee; + } }); } Some(ChainArgs::Init(_)) => { diff --git a/src/idempotent-proxy-canister/src/lib.rs b/src/idempotent-proxy-canister/src/lib.rs index 89de1d4..1f01aae 100644 --- a/src/idempotent-proxy-canister/src/lib.rs +++ b/src/idempotent-proxy-canister/src/lib.rs @@ -5,6 +5,7 @@ use ic_cdk::api::management_canister::http_request::{CanisterHttpRequestArgument use std::collections::BTreeSet; mod agent; +mod cycles; mod ecdsa; mod init; mod store; @@ -23,7 +24,7 @@ fn admin_set_managers(args: BTreeSet) -> Result<(), String> { Ok(()) } -#[ic_cdk::query] +#[ic_cdk::update] fn validate_admin_set_managers(args: BTreeSet) -> Result<(), String> { if args.is_empty() { return Err("managers cannot be empty".to_string()); @@ -44,7 +45,7 @@ async fn admin_set_agents(agents: Vec) -> Result<(), String> { Ok(()) } -#[ic_cdk::query] +#[ic_cdk::update] fn validate_admin_set_agents(agents: Vec) -> Result<(), String> { if agents.is_empty() { return Err("agents cannot be empty".to_string()); @@ -74,6 +75,26 @@ fn get_state() -> Result { Ok(s) } +#[ic_cdk::query] +async fn proxy_http_request_cost(req: CanisterHttpRequestArgument) -> u128 { + let calc = store::state::cycles_calculator(); + calc.ingress_cost(ic_cdk::api::call::arg_data_raw_size()) + + calc.http_outcall_request_cost(calc.count_request_bytes(&req), 1) + + calc.http_outcall_response_cost(req.max_response_bytes.unwrap_or(1024) as usize, 1) +} + +#[ic_cdk::query] +async fn parallel_call_cost(req: CanisterHttpRequestArgument) -> u128 { + let agents = store::state::get_agents(); + let calc = store::state::cycles_calculator(); + calc.ingress_cost(ic_cdk::api::call::arg_data_raw_size()) + + calc.http_outcall_request_cost(calc.count_request_bytes(&req), agents.len()) + + calc.http_outcall_response_cost( + req.max_response_bytes.unwrap_or(1024) as usize, + agents.len(), + ) +} + /// Proxy HTTP request by all agents in sequence until one returns an status <= 500 result. #[ic_cdk::update] async fn proxy_http_request(req: CanisterHttpRequestArgument) -> HttpResponse { @@ -86,19 +107,35 @@ async fn proxy_http_request(req: CanisterHttpRequestArgument) -> HttpResponse { } let agents = store::state::get_agents(); + if agents.is_empty() { + return HttpResponse { + status: Nat::from(503u64), + body: "no agents available".as_bytes().to_vec(), + headers: vec![], + }; + } + + let calc = store::state::cycles_calculator(); + store::state::receive_cycles( + calc.ingress_cost(ic_cdk::api::call::arg_data_raw_size()), + false, + ); + + let req_size = calc.count_request_bytes(&req); let mut last_err: Option = None; for agent in agents { + store::state::receive_cycles(calc.http_outcall_request_cost(req_size, 1), false); match agent.call(req.clone()).await { - Ok(res) => return res, + Ok(res) => { + let cycles = calc.http_outcall_response_cost(calc.count_response_bytes(&res), 1); + store::state::receive_cycles(cycles, true); + return res; + } Err(res) => last_err = Some(res), } } - last_err.unwrap_or_else(|| HttpResponse { - status: Nat::from(503u64), - body: "no agents available".as_bytes().to_vec(), - headers: vec![], - }) + last_err.unwrap() } /// Proxy HTTP request by all agents in parallel and return the result if all are the same, @@ -114,9 +151,21 @@ async fn parallel_call_all_ok(req: CanisterHttpRequestArgument) -> HttpResponse } let agents = store::state::get_agents(); + if agents.is_empty() { + return HttpResponse { + status: Nat::from(503u64), + body: "no agents available".as_bytes().to_vec(), + headers: vec![], + }; + } + + let calc = store::state::cycles_calculator(); + let cycles = calc.ingress_cost(ic_cdk::api::call::arg_data_raw_size()) + + calc.http_outcall_request_cost(calc.count_request_bytes(&req), agents.len()); + store::state::receive_cycles(cycles, false); + let results = futures::future::try_join_all(agents.iter().map(|agent| agent.call(req.clone()))).await; - match results { Err(res) => res, Ok(res) => { @@ -127,6 +176,10 @@ async fn parallel_call_all_ok(req: CanisterHttpRequestArgument) -> HttpResponse headers: vec![], }); + let cycles = calc + .http_outcall_response_cost(calc.count_response_bytes(&base_result), agents.len()); + store::state::receive_cycles(cycles, true); + let mut inconsistent_results: Vec<_> = results.filter(|result| result != &base_result).collect(); if !inconsistent_results.is_empty() { @@ -166,11 +219,21 @@ async fn parallel_call_any_ok(req: CanisterHttpRequestArgument) -> HttpResponse }; } + let calc = store::state::cycles_calculator(); + let cycles = calc.ingress_cost(ic_cdk::api::call::arg_data_raw_size()) + + calc.http_outcall_request_cost(calc.count_request_bytes(&req), agents.len()); + store::state::receive_cycles(cycles, false); + let result = futures::future::select_ok(agents.iter().map(|agent| agent.call(req.clone()).boxed())) .await; match result { - Ok((res, _)) => res, + Ok((res, _)) => { + let cycles = + calc.http_outcall_response_cost(calc.count_response_bytes(&res), agents.len()); + store::state::receive_cycles(cycles, true); + res + } Err(res) => res, } } diff --git a/src/idempotent-proxy-canister/src/store.rs b/src/idempotent-proxy-canister/src/store.rs index a70ff01..8100e6a 100644 --- a/src/idempotent-proxy-canister/src/store.rs +++ b/src/idempotent-proxy-canister/src/store.rs @@ -8,7 +8,7 @@ use ic_stable_structures::{ use serde::{Deserialize, Serialize}; use std::{borrow::Cow, cell::RefCell, collections::BTreeSet}; -use crate::{agent::Agent, ecdsa::get_proxy_token_public_key}; +use crate::{agent::Agent, cycles::Calculator, ecdsa::get_proxy_token_public_key}; type Memory = VirtualMemory; @@ -20,6 +20,14 @@ pub struct State { pub agents: Vec, pub managers: BTreeSet, pub allowed_callers: BTreeSet, + #[serde(default)] + pub subnet_size: u64, + #[serde(default)] + pub service_fee: u64, // in cycles + #[serde(default)] + pub incoming_cycles: u128, + #[serde(default)] + pub uncollectible_cycles: u128, } impl Storable for State { @@ -48,7 +56,7 @@ thread_local! { StableCell::init( MEMORY_MANAGER.with_borrow(|m| m.get(STATE_MEMORY_ID)), State::default() - ).expect("failed to init STATE store") + ).expect("failed to init STATE_STORE store") ); } @@ -60,6 +68,16 @@ pub mod state { STATE.with(|r| r.borrow().agents.clone()) } + pub fn cycles_calculator() -> Calculator { + STATE.with(|r| { + let s = r.borrow(); + Calculator { + subnet_size: s.subnet_size, + service_fee: s.service_fee, + } + }) + } + pub fn is_manager(caller: &Principal) -> bool { STATE.with(|r| r.borrow().managers.contains(caller)) } @@ -76,6 +94,24 @@ pub mod state { STATE.with(|r| f(&mut r.borrow_mut())) } + pub fn receive_cycles(cycles: u128, ignore_insufficient: bool) { + if cycles == 0 { + return; + } + + let received = ic_cdk::api::call::msg_cycles_accept128(cycles); + with_mut(|r| { + r.incoming_cycles = r.incoming_cycles.saturating_add(received); + if cycles > received { + r.uncollectible_cycles = r.uncollectible_cycles.saturating_add(cycles - received); + + if !ignore_insufficient { + ic_cdk::trap("insufficient cycles"); + } + } + }); + } + pub async fn init_ecdsa_public_key() { let ecdsa_key_name = with(|r| { if r.proxy_token_public_key.is_empty() && !r.ecdsa_key_name.is_empty() {