diff --git a/Cargo.lock b/Cargo.lock index c2e1fc00702c..894eac4200a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10304,6 +10304,7 @@ version = "0.9.0" dependencies = [ "ic-config", "ic-types", + "ic-types-cycles", ] [[package]] diff --git a/rs/https_outcalls/client/src/client.rs b/rs/https_outcalls/client/src/client.rs index 40b1f0a5ec23..2ecd898d8f15 100644 --- a/rs/https_outcalls/client/src/client.rs +++ b/rs/https_outcalls/client/src/client.rs @@ -15,7 +15,7 @@ use ic_logger::{ReplicaLogger, info, warn}; use ic_management_canister_types_private::{CanisterHttpResponsePayload, TransformArgs}; use ic_metrics::MetricsRegistry; use ic_types::{ - CanisterId, NumBytes, NumInstructions, + CanisterId, CountBytes, NumBytes, NumInstructions, canister_http::{ CanisterHttpHeader, CanisterHttpMethod, CanisterHttpPaymentReceipt, CanisterHttpReject, CanisterHttpRequest, CanisterHttpRequestContext, CanisterHttpResponse, @@ -203,7 +203,7 @@ impl NonBlockingChannel for CanisterHttpAdapterClientImpl { // Only apply the transform if a function name is specified let transform_timer = metrics.transform_execution_duration.start_timer(); - let max_response_size_bytes = budget.get_adapter_limits().max_response_size.get(); + let max_response_size_bytes = budget.get_gossip_limit().get(); let transformed_payload = match &request_transform { Some(transform) => { let (transform_result, instruction_count) = transform_adapter_response( @@ -262,6 +262,20 @@ impl NonBlockingChannel for CanisterHttpAdapterClientImpl { } .await; + let payload_size = match &payload { + Ok(data) => data.len(), + Err(reject) => reject.count_bytes(), + }; + let payload = match budget.subtract_gossip_usage(payload_size) { + Ok(()) => payload, + Err(PricingError::InsufficientCycles) => { + Err(CanisterHttpReject { + reject_code: RejectCode::SysFatal, + message: "Insufficient cycles".to_string(), + }) + } + }; + // Create the payment receipt after all processing is complete. let receipt = budget.create_payment_receipt(); diff --git a/rs/https_outcalls/pricing/BUILD.bazel b/rs/https_outcalls/pricing/BUILD.bazel index bb6fa3989628..68f37bf1c561 100644 --- a/rs/https_outcalls/pricing/BUILD.bazel +++ b/rs/https_outcalls/pricing/BUILD.bazel @@ -9,6 +9,7 @@ rust_library( version = "0.1.0", deps = [ "//rs/config", + "//rs/types/cycles", "//rs/types/types", ], ) diff --git a/rs/https_outcalls/pricing/Cargo.toml b/rs/https_outcalls/pricing/Cargo.toml index 1773aa69401f..815c1306fa68 100644 --- a/rs/https_outcalls/pricing/Cargo.toml +++ b/rs/https_outcalls/pricing/Cargo.toml @@ -8,4 +8,5 @@ documentation.workspace = true [dependencies] ic-config = { path = "../../config" } +ic-types-cycles = { path = "../../types/cycles" } ic-types = { path = "../../types/types" } \ No newline at end of file diff --git a/rs/https_outcalls/pricing/src/legacy.rs b/rs/https_outcalls/pricing/src/legacy.rs index eb272bc8a37b..798b45796e44 100644 --- a/rs/https_outcalls/pricing/src/legacy.rs +++ b/rs/https_outcalls/pricing/src/legacy.rs @@ -3,10 +3,28 @@ use std::time::Duration; use ic_config::subnet_config::MAX_INSTRUCTIONS_PER_QUERY_MESSAGE; use ic_types::{ NumBytes, NumInstructions, - canister_http::{CanisterHttpPaymentReceipt, MAX_CANISTER_HTTP_RESPONSE_BYTES}, + canister_http::{ + CanisterHttpPaymentReceipt, CanisterHttpRequestContext, CanisterHttpResponse, + MAX_CANISTER_HTTP_RESPONSE_BYTES, + }, }; +use ic_types_cycles::Cycles; -use crate::{AdapterLimits, BudgetTracker, NetworkUsage, PricingError}; +use crate::{AdapterLimits, BudgetTracker, NetworkUsage, PricingCalculator, PricingError}; + +pub struct LegacyCalculator; + +impl PricingCalculator for LegacyCalculator { + fn consensus_cost(&self, _response: &CanisterHttpResponse, _subnet_size: u32) -> Cycles { + // Note: the legacy pricing calculator does not calculate the cost of the response. + Cycles::zero() + } + + fn base_cost(&self, _context: &CanisterHttpRequestContext, _subnet_size: u32) -> Cycles { + // Note: the legacy pricing calculator does not calculate the cost of the request. + Cycles::zero() + } +} pub struct LegacyTracker { max_response_size: NumBytes, @@ -45,6 +63,14 @@ impl BudgetTracker for LegacyTracker { Ok(()) } + fn get_gossip_limit(&self) -> NumBytes { + self.max_response_size + } + + fn subtract_gossip_usage(&mut self, _response_size: usize) -> Result<(), PricingError> { + Ok(()) + } + fn create_payment_receipt(&self) -> CanisterHttpPaymentReceipt { // Legacy pricing does not perform cycles accounting, so no cycles // are ever refunded. diff --git a/rs/https_outcalls/pricing/src/lib.rs b/rs/https_outcalls/pricing/src/lib.rs index 827459a9bcdd..8c7c20f7f388 100644 --- a/rs/https_outcalls/pricing/src/lib.rs +++ b/rs/https_outcalls/pricing/src/lib.rs @@ -4,13 +4,26 @@ use std::time::Duration; use ic_types::{ NumBytes, NumInstructions, - canister_http::{CanisterHttpPaymentReceipt, CanisterHttpRequestContext}, + canister_http::{CanisterHttpPaymentReceipt, CanisterHttpRequestContext, CanisterHttpResponse}, }; +use ic_types_cycles::Cycles; use legacy::LegacyTracker; +use crate::legacy::LegacyCalculator; + +pub trait PricingCalculator { + /// Returns the number of cycles that the replica should charge for the given HTTP response, + /// in order to include it in a block payload. + fn consensus_cost(&self, response: &CanisterHttpResponse, subnet_size: u32) -> Cycles; + + /// Returns the base cost that should be charged for the given HTTP request. + fn base_cost(&self, context: &CanisterHttpRequestContext, subnet_size: u32) -> Cycles; +} + pub trait BudgetTracker: Send { /// Returns the maximum network resources the Adapter is allowed to consume. fn get_adapter_limits(&self) -> AdapterLimits; + /// Deducts the actual network resources consumed. /// /// # Invariants @@ -19,13 +32,25 @@ pub trait BudgetTracker: Send { /// /// Note that "<=" is used here to mean field-wise less than or equal to. fn subtract_network_usage(&mut self, network_usage: NetworkUsage) -> Result<(), PricingError>; + /// Returns the maximum instructions allowed for the transformation function. fn get_transform_limit(&self) -> NumInstructions; + /// Deducts the actual instructions consumed by the transformation. /// /// # Invariants /// - This method returns `Ok(())` if and only if `usage <= get_transform_limit()`. fn subtract_transform_usage(&mut self, usage: NumInstructions) -> Result<(), PricingError>; + + /// Returns the maximum number of bytes the response may have in order to be gossiped. + fn get_gossip_limit(&self) -> NumBytes; + + /// Deducts the actual number of bytes of the response that were gossiped. + /// + /// # Invariants + /// - This method returns `Ok(())` if and only if `response_size <= get_gossip_limit()`. + fn subtract_gossip_usage(&mut self, response_size: usize) -> Result<(), PricingError>; + /// Produces the per-replica payment receipt that summarizes the cycles /// accounting outcome of the outcall, given the resources consumed so /// far via the `subtract_*` methods. @@ -58,4 +83,10 @@ impl PricingFactory { // Currently, we only support the legacy pricing version. Box::new(LegacyTracker::new(context.max_response_bytes)) } + + pub fn new_calculator(_context: &CanisterHttpRequestContext) -> Box { + // TODO(IC-1937): This should take into account context.pricing_version and a replica config. + // Currently, we only support the legacy pricing version. + Box::new(LegacyCalculator {}) + } }