From 72017971597a03e94dc6d81ab9f6295afd2d8d51 Mon Sep 17 00:00:00 2001 From: Maksym Arutyunyan Date: Wed, 1 Oct 2025 12:18:56 +0000 Subject: [PATCH 1/6] feat: charge for fetching logs --- rs/execution_environment/src/execution_environment.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/rs/execution_environment/src/execution_environment.rs b/rs/execution_environment/src/execution_environment.rs index 18b6f8284a29..4be12869deba 100644 --- a/rs/execution_environment/src/execution_environment.rs +++ b/rs/execution_environment/src/execution_environment.rs @@ -1585,6 +1585,7 @@ impl ExecutionEnvironment { ), )) } else { + request.payment -= fetch_canister_logs_fee; // Charge for the request. FetchCanisterLogsRequest::decode(payload) .and_then(|args| { fetch_canister_logs( From e0f703e091b59e1cf53ae983b359c075f4f83420 Mon Sep 17 00:00:00 2001 From: Maksym Arutyunyan Date: Wed, 1 Oct 2025 14:28:14 +0000 Subject: [PATCH 2/6] charge --- rs/config/src/subnet_config.rs | 12 +++ rs/cycles_account_manager/src/lib.rs | 29 +++++++ .../src/execution_environment.rs | 85 +++++++++++-------- rs/types/types/src/canister_log.rs | 3 + 4 files changed, 95 insertions(+), 34 deletions(-) diff --git a/rs/config/src/subnet_config.rs b/rs/config/src/subnet_config.rs index b52eab33a0da..2dd2b7324500 100644 --- a/rs/config/src/subnet_config.rs +++ b/rs/config/src/subnet_config.rs @@ -461,6 +461,12 @@ pub struct CyclesAccountManagerConfig { /// The default value of the reserved balance limit for the case when the /// canister doesn't have it set in the settings. pub default_reserved_balance_limit: Cycles, + + /// Base fee for fetching canister logs. + pub fetch_canister_logs_base_fee: Cycles, + + /// Fee per byte for fetching canister logs. + pub fetch_canister_logs_per_byte_fee: Cycles, } impl CyclesAccountManagerConfig { @@ -497,6 +503,8 @@ impl CyclesAccountManagerConfig { http_response_per_byte_fee: Cycles::new(800), max_storage_reservation_period: Duration::from_secs(300_000_000), default_reserved_balance_limit: DEFAULT_RESERVED_BALANCE_LIMIT, + fetch_canister_logs_base_fee: Cycles::new(1_000_000), + fetch_canister_logs_per_byte_fee: Cycles::new(800), } } @@ -537,6 +545,8 @@ impl CyclesAccountManagerConfig { // This effectively disables the storage reservation mechanism on system subnets. max_storage_reservation_period: Duration::from_secs(0), default_reserved_balance_limit: DEFAULT_RESERVED_BALANCE_LIMIT, + fetch_canister_logs_base_fee: Cycles::new(0), + fetch_canister_logs_per_byte_fee: Cycles::new(0), } } @@ -563,6 +573,8 @@ impl CyclesAccountManagerConfig { http_response_per_byte_fee: Cycles::zero(), max_storage_reservation_period: Duration::from_secs(u64::MAX), default_reserved_balance_limit: Cycles::zero(), + fetch_canister_logs_base_fee: Cycles::zero(), + fetch_canister_logs_per_byte_fee: Cycles::zero(), } } } diff --git a/rs/cycles_account_manager/src/lib.rs b/rs/cycles_account_manager/src/lib.rs index e14d401087e3..f467fb6fb549 100644 --- a/rs/cycles_account_manager/src/lib.rs +++ b/rs/cycles_account_manager/src/lib.rs @@ -29,6 +29,7 @@ use ic_types::{ PrincipalId, SubnetId, batch::CanisterCyclesCostSchedule, canister_http::MAX_CANISTER_HTTP_RESPONSE_BYTES, + canister_log::MAX_FETCH_CANISTER_LOGS_RESPONSE_BYTES, messages::{MAX_INTER_CANISTER_PAYLOAD_IN_BYTES, Request, Response, SignedIngress}, }; use prometheus::IntCounter; @@ -1370,6 +1371,34 @@ impl CyclesAccountManager { pub fn default_reserved_balance_limit(&self) -> Cycles { self.config.default_reserved_balance_limit } + + pub fn fetch_canister_logs_fee( + &self, + response_size: usize, + subnet_size: usize, + cost_schedule: CanisterCyclesCostSchedule, + ) -> Cycles { + match cost_schedule { + CanisterCyclesCostSchedule::Free => Cycles::new(0), + CanisterCyclesCostSchedule::Normal => { + (self.config.fetch_canister_logs_base_fee + + self.config.fetch_canister_logs_per_byte_fee * response_size) + * subnet_size + } + } + } + + pub fn max_fetch_canister_logs_fee( + &self, + subnet_size: usize, + cost_schedule: CanisterCyclesCostSchedule, + ) -> Cycles { + self.fetch_canister_logs_fee( + MAX_FETCH_CANISTER_LOGS_RESPONSE_BYTES, + subnet_size, + cost_schedule, + ) + } } /// Encapsulates the payer and cost of inducting an ingress messages. diff --git a/rs/execution_environment/src/execution_environment.rs b/rs/execution_environment/src/execution_environment.rs index 4be12869deba..c47b5e9d3f40 100644 --- a/rs/execution_environment/src/execution_environment.rs +++ b/rs/execution_environment/src/execution_environment.rs @@ -1570,43 +1570,60 @@ impl ExecutionEnvironment { )), refund: msg.take_cycles(), }, - FlagStatus::Enabled => match &msg { - CanisterCall::Request(request) => { - let fetch_canister_logs_fee = Cycles::new(1_000_000); // TODO(EXC-2112): fix placeholder fees. - - let response = if request.payment < fetch_canister_logs_fee { - Err(UserError::new( - ErrorCode::CanisterRejectedMessage, - format!( - "{} request sent with {} cycles, but {} cycles are required.", - Ic00Method::FetchCanisterLogs, - request.payment, - fetch_canister_logs_fee - ), - )) - } else { - request.payment -= fetch_canister_logs_fee; // Charge for the request. - FetchCanisterLogsRequest::decode(payload) - .and_then(|args| { - fetch_canister_logs( - *msg.sender(), - &state, - args, - self.config.fetch_canister_logs_filter, - ) - }) - .map(|resp| (Encode!(&resp).unwrap(), None)) - }; + FlagStatus::Enabled => { + let sender = *msg.sender(); + let payload = payload.to_vec(); + match &mut msg { + CanisterCall::Request(request) => { + let max_fetch_canister_logs_fee = + self.cycles_account_manager.max_fetch_canister_logs_fee( + registry_settings.subnet_size, + cost_schedule, + ); - ExecuteSubnetMessageResult::Finished { - response, - refund: msg.take_cycles(), + let response = if request.payment < max_fetch_canister_logs_fee { + Err(UserError::new( + ErrorCode::CanisterRejectedMessage, + format!( + "{} request sent with {} cycles, but {} cycles are required.", + Ic00Method::FetchCanisterLogs, + request.payment, + max_fetch_canister_logs_fee + ), + )) + } else { + FetchCanisterLogsRequest::decode(&payload) + .and_then(|args| { + fetch_canister_logs( + sender, + &state, + args, + self.config.fetch_canister_logs_filter, + ) + }) + .map(|resp| { + let response_bytes = Encode!(&resp).unwrap(); + Arc::make_mut(request).payment -= self + .cycles_account_manager + .fetch_canister_logs_fee( + response_bytes.len(), + registry_settings.subnet_size, + cost_schedule, + ); + (response_bytes, None) + }) + }; + + ExecuteSubnetMessageResult::Finished { + response, + refund: msg.take_cycles(), + } + } + CanisterCall::Ingress(_) => { + self.reject_unexpected_ingress(Ic00Method::FetchCanisterLogs) } } - CanisterCall::Ingress(_) => { - self.reject_unexpected_ingress(Ic00Method::FetchCanisterLogs) - } - }, + } } } diff --git a/rs/types/types/src/canister_log.rs b/rs/types/types/src/canister_log.rs index f50bba227ed4..276e6412b4ae 100644 --- a/rs/types/types/src/canister_log.rs +++ b/rs/types/types/src/canister_log.rs @@ -8,6 +8,9 @@ use std::collections::VecDeque; /// The maximum allowed size of a canister log buffer. pub const MAX_ALLOWED_CANISTER_LOG_BUFFER_SIZE: usize = 4 * 1024; +/// Maximum number of response bytes for a canister http request. +pub const MAX_FETCH_CANISTER_LOGS_RESPONSE_BYTES: usize = 2_000_000; + fn truncate_content(mut record: CanisterLogRecord) -> CanisterLogRecord { let max_content_size = MAX_ALLOWED_CANISTER_LOG_BUFFER_SIZE - std::mem::size_of::(); From e1575c2dbc11a3b39b7ad356e7f4db07325c8e64 Mon Sep 17 00:00:00 2001 From: Maksym Arutyunyan Date: Wed, 1 Oct 2025 14:53:37 +0000 Subject: [PATCH 3/6] fix tests --- rs/execution_environment/tests/canister_logging.rs | 2 +- rs/execution_environment/tests/subnet_size_test.rs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/rs/execution_environment/tests/canister_logging.rs b/rs/execution_environment/tests/canister_logging.rs index 2596c110798e..86ab4b83da09 100644 --- a/rs/execution_environment/tests/canister_logging.rs +++ b/rs/execution_environment/tests/canister_logging.rs @@ -355,7 +355,7 @@ fn test_fetch_canister_logs_via_inter_canister_update_call_enabled() { call_args() .other_side(FetchCanisterLogsRequest::new(canister_b).encode()) .on_reject(wasm().reject_message().reject()), - Cycles::new(2_000_000), + Cycles::new(50_000_000_000), ) .build(), ); diff --git a/rs/execution_environment/tests/subnet_size_test.rs b/rs/execution_environment/tests/subnet_size_test.rs index 9d2bd897576c..68d3283ba3b2 100644 --- a/rs/execution_environment/tests/subnet_size_test.rs +++ b/rs/execution_environment/tests/subnet_size_test.rs @@ -748,6 +748,8 @@ fn get_cycles_account_manager_config(subnet_type: SubnetType) -> CyclesAccountMa max_storage_reservation_period: Duration::from_secs(0), default_reserved_balance_limit: CyclesAccountManagerConfig::system_subnet() .default_reserved_balance_limit, + fetch_canister_logs_base_fee: Cycles::new(0), + fetch_canister_logs_per_byte_fee: Cycles::new(0), }, SubnetType::Application | SubnetType::VerifiedApplication => CyclesAccountManagerConfig { reference_subnet_size: DEFAULT_REFERENCE_SUBNET_SIZE, @@ -782,6 +784,8 @@ fn get_cycles_account_manager_config(subnet_type: SubnetType) -> CyclesAccountMa max_storage_reservation_period: Duration::from_secs(0), default_reserved_balance_limit: CyclesAccountManagerConfig::application_subnet() .default_reserved_balance_limit, + fetch_canister_logs_base_fee: Cycles::new(1_000_000), + fetch_canister_logs_per_byte_fee: Cycles::new(800), }, } } From 2d99b9c68b5705aaf6f026a5cccaf841ead5fb5d Mon Sep 17 00:00:00 2001 From: Maksym Arutyunyan Date: Thu, 2 Oct 2025 13:14:02 +0000 Subject: [PATCH 4/6] NumBytes --- rs/cycles_account_manager/src/lib.rs | 6 +++--- rs/execution_environment/src/execution_environment.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rs/cycles_account_manager/src/lib.rs b/rs/cycles_account_manager/src/lib.rs index f467fb6fb549..840072e175ab 100644 --- a/rs/cycles_account_manager/src/lib.rs +++ b/rs/cycles_account_manager/src/lib.rs @@ -1374,7 +1374,7 @@ impl CyclesAccountManager { pub fn fetch_canister_logs_fee( &self, - response_size: usize, + response_size: NumBytes, subnet_size: usize, cost_schedule: CanisterCyclesCostSchedule, ) -> Cycles { @@ -1382,7 +1382,7 @@ impl CyclesAccountManager { CanisterCyclesCostSchedule::Free => Cycles::new(0), CanisterCyclesCostSchedule::Normal => { (self.config.fetch_canister_logs_base_fee - + self.config.fetch_canister_logs_per_byte_fee * response_size) + + self.config.fetch_canister_logs_per_byte_fee * response_size.get()) * subnet_size } } @@ -1394,7 +1394,7 @@ impl CyclesAccountManager { cost_schedule: CanisterCyclesCostSchedule, ) -> Cycles { self.fetch_canister_logs_fee( - MAX_FETCH_CANISTER_LOGS_RESPONSE_BYTES, + NumBytes::new(MAX_FETCH_CANISTER_LOGS_RESPONSE_BYTES as u64), subnet_size, cost_schedule, ) diff --git a/rs/execution_environment/src/execution_environment.rs b/rs/execution_environment/src/execution_environment.rs index c47b5e9d3f40..03e5511a409b 100644 --- a/rs/execution_environment/src/execution_environment.rs +++ b/rs/execution_environment/src/execution_environment.rs @@ -1606,7 +1606,7 @@ impl ExecutionEnvironment { Arc::make_mut(request).payment -= self .cycles_account_manager .fetch_canister_logs_fee( - response_bytes.len(), + NumBytes::new(response_bytes.len() as u64), registry_settings.subnet_size, cost_schedule, ); From fb2296272d01fc266448deb499f8b266cc10983e Mon Sep 17 00:00:00 2001 From: Maksym Arutyunyan Date: Thu, 2 Oct 2025 13:59:34 +0000 Subject: [PATCH 5/6] deduct_cycles --- rs/execution_environment/src/execution_environment.rs | 3 ++- rs/types/types/src/messages.rs | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/rs/execution_environment/src/execution_environment.rs b/rs/execution_environment/src/execution_environment.rs index 03e5511a409b..f9e5f124647c 100644 --- a/rs/execution_environment/src/execution_environment.rs +++ b/rs/execution_environment/src/execution_environment.rs @@ -1603,13 +1603,14 @@ impl ExecutionEnvironment { }) .map(|resp| { let response_bytes = Encode!(&resp).unwrap(); - Arc::make_mut(request).payment -= self + let actual_fee = self .cycles_account_manager .fetch_canister_logs_fee( NumBytes::new(response_bytes.len() as u64), registry_settings.subnet_size, cost_schedule, ); + msg.deduct_cycles(actual_fee); (response_bytes, None) }) }; diff --git a/rs/types/types/src/messages.rs b/rs/types/types/src/messages.rs index bd0d455f8521..686411e4e081 100644 --- a/rs/types/types/src/messages.rs +++ b/rs/types/types/src/messages.rs @@ -409,6 +409,14 @@ impl CanisterCall { } } + /// Deducts the specified fee from the payment of this message. + pub fn deduct_cycles(&mut self, fee: Cycles) { + match self { + CanisterCall::Request(request) => Arc::make_mut(request).payment -= fee, + CanisterCall::Ingress(_) => {} // Ingress messages don't have payments + } + } + /// Extracts the cycles received with this message. pub fn take_cycles(&mut self) -> Cycles { match self { From db49868f53b1566ba115bed323322dba10a9151b Mon Sep 17 00:00:00 2001 From: Maksym Arutyunyan Date: Thu, 2 Oct 2025 14:12:13 +0000 Subject: [PATCH 6/6] comment --- rs/execution_environment/src/execution_environment.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rs/execution_environment/src/execution_environment.rs b/rs/execution_environment/src/execution_environment.rs index f9e5f124647c..947aa45cf3c5 100644 --- a/rs/execution_environment/src/execution_environment.rs +++ b/rs/execution_environment/src/execution_environment.rs @@ -1581,6 +1581,7 @@ impl ExecutionEnvironment { cost_schedule, ); + // Check there are sufficient cycles to cover the worst-case execution cost. let response = if request.payment < max_fetch_canister_logs_fee { Err(UserError::new( ErrorCode::CanisterRejectedMessage, @@ -1610,6 +1611,7 @@ impl ExecutionEnvironment { registry_settings.subnet_size, cost_schedule, ); + // There are enough cycles, deduct the actual fee from paid cycles and refund the rest. msg.deduct_cycles(actual_fee); (response_bytes, None) })