diff --git a/rs/execution_environment/src/execution_environment/tests.rs b/rs/execution_environment/src/execution_environment/tests.rs index ff8a7c49ea89..e2c32dc66bc0 100644 --- a/rs/execution_environment/src/execution_environment/tests.rs +++ b/rs/execution_environment/src/execution_environment/tests.rs @@ -1988,6 +1988,7 @@ fn http_request_bound_holds() { context: transform_context.clone(), }), is_replicated: None, + pricing_version: None, }; // Create request to HTTP_REQUEST method. @@ -2769,6 +2770,7 @@ fn execute_canister_http_request() { context: transform_context.clone(), }), is_replicated: None, + pricing_version: None, }; // Create request to HTTP_REQUEST method. @@ -2849,6 +2851,7 @@ fn execute_canister_http_request_disabled() { context: vec![0, 1, 2], }), is_replicated: None, + pricing_version: None, }; // Create request to HTTP_REQUEST method. diff --git a/rs/execution_environment/src/scheduler/tests.rs b/rs/execution_environment/src/scheduler/tests.rs index df1f3b0eba63..3edd804833c2 100644 --- a/rs/execution_environment/src/scheduler/tests.rs +++ b/rs/execution_environment/src/scheduler/tests.rs @@ -3042,6 +3042,7 @@ fn canister_is_stopped_if_timeout_occurs_and_ready_to_stop() { transform: None, max_response_bytes: None, is_replicated: None, + pricing_version: None, }) .unwrap(); @@ -4115,6 +4116,7 @@ fn consumed_cycles_http_outcalls_are_added_to_consumed_cycles_total() { context: transform_context, }), is_replicated: None, + pricing_version: None, }; // Create request to `HttpRequest` method. @@ -4204,6 +4206,7 @@ fn http_outcalls_free() { context: transform_context, }), is_replicated: None, + pricing_version: None, }; // Create request to `HttpRequest` method. diff --git a/rs/execution_environment/tests/subnet_size_test.rs b/rs/execution_environment/tests/subnet_size_test.rs index 8b99df565cf5..ac8a83b5f8cb 100644 --- a/rs/execution_environment/tests/subnet_size_test.rs +++ b/rs/execution_environment/tests/subnet_size_test.rs @@ -534,6 +534,7 @@ fn simulate_http_request_cost(subnet_type: SubnetType, subnet_size: usize) -> Cy context: vec![], }), is_replicated: None, + pricing_version: None, }) .unwrap(), ), diff --git a/rs/https_outcalls/client/src/client.rs b/rs/https_outcalls/client/src/client.rs index 314993e34b30..322ecbb647f1 100644 --- a/rs/https_outcalls/client/src/client.rs +++ b/rs/https_outcalls/client/src/client.rs @@ -8,7 +8,7 @@ use ic_https_outcalls_service::{ }; use ic_interfaces::execution_environment::{QueryExecutionInput, QueryExecutionService}; use ic_interfaces_adapter_client::{NonBlockingChannel, SendError, TryReceiveError}; -use ic_logger::{ReplicaLogger, info}; +use ic_logger::{ReplicaLogger, info, warn}; use ic_management_canister_types_private::{CanisterHttpResponsePayload, TransformArgs}; use ic_metrics::MetricsRegistry; use ic_nns_delegation_manager::{CanisterRangesFilter, NNSDelegationReader}; @@ -146,11 +146,33 @@ impl NonBlockingChannel for CanisterHttpAdapterClientImpl { http_method: request_http_method, max_response_bytes: request_max_response_bytes, transform: request_transform, + pricing_version: request_pricing_version, .. }, socks_proxy_addrs, } = canister_http_request; + if request_pricing_version == ic_types::canister_http::PricingVersion::PayAsYouGo { + warn!( + log, + "Canister HTTP request with PayAsYouGo pricing is not supported yet: request_id {}, sender {}, process_id: {}", + request_id, + request_sender, + std::process::id(), + ); + let _ = permit.send(CanisterHttpResponse { + id: request_id, + timeout: request_timeout, + canister_id: request_sender, + content: CanisterHttpResponseContent::Reject(CanisterHttpReject { + reject_code: RejectCode::SysFatal, + message: "Canister HTTP request with PayAsYouGo pricing is not supported" + .to_string(), + }), + }); + return; + } + let adapter_req_timer = Instant::now(); let max_response_size_bytes = request_max_response_bytes .unwrap_or(NumBytes::new(MAX_CANISTER_HTTP_RESPONSE_BYTES)) @@ -396,7 +418,7 @@ mod tests { use ic_interfaces::execution_environment::{QueryExecutionError, QueryExecutionResponse}; use ic_logger::replica_logger::no_op_logger; use ic_test_utilities_types::messages::RequestBuilder; - use ic_types::canister_http::{Replication, Transform}; + use ic_types::canister_http::{PricingVersion, Replication, Transform}; use ic_types::{ Time, canister_http::CanisterHttpMethod, messages::CallbackId, time::UNIX_EPOCH, time::current_time, @@ -489,6 +511,7 @@ mod tests { }), time: UNIX_EPOCH, replication: Replication::FullyReplicated, + pricing_version: PricingVersion::Legacy, }, socks_proxy_addrs: vec![], } diff --git a/rs/https_outcalls/consensus/src/payload_builder/tests.rs b/rs/https_outcalls/consensus/src/payload_builder/tests.rs index 3ce3dfefa036..22dbdced63af 100644 --- a/rs/https_outcalls/consensus/src/payload_builder/tests.rs +++ b/rs/https_outcalls/consensus/src/payload_builder/tests.rs @@ -343,6 +343,7 @@ fn timeout_priority() { // this is the important one time: UNIX_EPOCH, replication: ic_types::canister_http::Replication::FullyReplicated, + pricing_version: ic_types::canister_http::PricingVersion::Legacy, }; init_state .metadata @@ -849,6 +850,7 @@ fn non_replicated_request_response_coming_in_gossip_payload_created() { transform: None, time: UNIX_EPOCH, replication: ic_types::canister_http::Replication::NonReplicated(delegated_node_id), + pricing_version: ic_types::canister_http::PricingVersion::Legacy, }; // Insert the context in the replicated state @@ -952,6 +954,7 @@ fn non_replicated_request_with_extra_share_includes_only_delegated_share() { transform: None, time: UNIX_EPOCH, replication: ic_types::canister_http::Replication::NonReplicated(delegated_node_id), + pricing_version: ic_types::canister_http::PricingVersion::Legacy, }; // Insert the context in the replicated state @@ -1056,6 +1059,7 @@ fn non_replicated_share_is_ignored_if_content_is_missing() { transform: None, time: UNIX_EPOCH, replication: ic_types::canister_http::Replication::NonReplicated(delegated_node_id), + pricing_version: ic_types::canister_http::PricingVersion::Legacy, }; let mut init_state = ic_test_utilities_state::get_initial_state(0, 0); @@ -1134,6 +1138,7 @@ fn validate_payload_succeeds_for_valid_non_replicated_response() { transform: None, time: UNIX_EPOCH, replication: ic_types::canister_http::Replication::NonReplicated(delegated_node_id), + pricing_version: ic_types::canister_http::PricingVersion::Legacy, }; // Inject this context into the state reader used by the validator. @@ -1200,6 +1205,7 @@ fn validate_payload_fails_for_non_replicated_response_with_wrong_signer() { transform: None, time: UNIX_EPOCH, replication: ic_types::canister_http::Replication::NonReplicated(delegated_node_id), + pricing_version: ic_types::canister_http::PricingVersion::Legacy, }; // Inject this context into the state reader. @@ -1282,6 +1288,7 @@ fn validate_payload_fails_for_response_with_no_signatures() { transform: None, time: UNIX_EPOCH, replication: ic_types::canister_http::Replication::NonReplicated(delegated_node_id), + pricing_version: ic_types::canister_http::PricingVersion::Legacy, }; // Inject this context into the state reader used by the validator. @@ -1369,6 +1376,7 @@ fn validate_payload_fails_when_non_replicated_proof_is_for_fully_replicated_requ time: UNIX_EPOCH, // The state says the request is replicated. replication: ic_types::canister_http::Replication::FullyReplicated, + pricing_version: ic_types::canister_http::PricingVersion::Legacy, }; // Inject this context into the state reader. @@ -1461,6 +1469,7 @@ fn validate_payload_fails_for_duplicate_non_replicated_response() { transform: None, time: UNIX_EPOCH, replication: ic_types::canister_http::Replication::NonReplicated(delegated_node_id), + pricing_version: ic_types::canister_http::PricingVersion::Legacy, }; // 2. Inject this context into the state reader diff --git a/rs/https_outcalls/consensus/src/pool_manager.rs b/rs/https_outcalls/consensus/src/pool_manager.rs index d8de2093e88b..a659b7ec6967 100644 --- a/rs/https_outcalls/consensus/src/pool_manager.rs +++ b/rs/https_outcalls/consensus/src/pool_manager.rs @@ -748,6 +748,7 @@ pub mod test { transform: None, time: ic_types::Time::from_nanos_since_unix_epoch(10), replication: Replication::FullyReplicated, + pricing_version: PricingVersion::Legacy, }; state_manager @@ -854,6 +855,7 @@ pub mod test { transform: None, time: ic_types::Time::from_nanos_since_unix_epoch(10), replication: Replication::FullyReplicated, + pricing_version: PricingVersion::Legacy, }; // NOTE: We need at least some context in the state, otherwise next_callback_id will be 0 and no @@ -967,6 +969,7 @@ pub mod test { transform: None, time: ic_types::Time::from_nanos_since_unix_epoch(10), replication: Replication::FullyReplicated, + pricing_version: PricingVersion::Legacy, }; state_manager @@ -1099,6 +1102,7 @@ pub mod test { transform: None, time: ic_types::Time::from_nanos_since_unix_epoch(10), replication: Replication::NonReplicated(delegated_node_id), + pricing_version: PricingVersion::Legacy, }; state_manager @@ -1237,6 +1241,7 @@ pub mod test { transform: None, time: ic_types::Time::from_nanos_since_unix_epoch(10), replication: Replication::NonReplicated(delegated_node_id), + pricing_version: PricingVersion::Legacy, }; state_manager .get_mut() @@ -1340,6 +1345,7 @@ pub mod test { transform: None, time: ic_types::Time::from_nanos_since_unix_epoch(10), replication: Replication::FullyReplicated, + pricing_version: PricingVersion::Legacy, }; state_manager @@ -1452,6 +1458,7 @@ pub mod test { transform: None, time: ic_types::Time::from_nanos_since_unix_epoch(10), replication: Replication::NonReplicated(delegated_node_id), + pricing_version: PricingVersion::Legacy, }; state_manager @@ -1633,6 +1640,7 @@ pub mod test { transform: None, time: ic_types::Time::from_nanos_since_unix_epoch(10), replication: Replication::NonReplicated(delegated_node_id), + pricing_version: PricingVersion::Legacy, }; state_manager @@ -1744,6 +1752,7 @@ pub mod test { transform: None, time: ic_types::Time::from_nanos_since_unix_epoch(10), replication: Replication::NonReplicated(delegated_node_id), + pricing_version: PricingVersion::Legacy, }; state_manager .get_mut() @@ -1867,6 +1876,7 @@ pub mod test { transform: None, time: ic_types::Time::from_nanos_since_unix_epoch(10), replication: Replication::NonReplicated(delegated_node_id), + pricing_version: PricingVersion::Legacy, }; state_manager .get_mut() @@ -1998,6 +2008,7 @@ pub mod test { transform: None, time: ic_types::Time::from_nanos_since_unix_epoch(10), replication: Replication::NonReplicated(delegated_node_id), + pricing_version: PricingVersion::Legacy, }; state_manager .get_mut() @@ -2120,6 +2131,7 @@ pub mod test { transform: None, time: ic_types::Time::from_nanos_since_unix_epoch(10), replication: Replication::NonReplicated(delegated_node_id), + pricing_version: PricingVersion::Legacy, }; state_manager @@ -2236,6 +2248,7 @@ pub mod test { transform: None, time: ic_types::Time::from_nanos_since_unix_epoch(10), replication: Replication::FullyReplicated, + pricing_version: PricingVersion::Legacy, }; state_manager @@ -2384,6 +2397,7 @@ pub mod test { transform: None, time: ic_types::Time::from_nanos_since_unix_epoch(10), replication: Replication::NonReplicated(delegated_node_id), + pricing_version: PricingVersion::Legacy, }; state_manager .get_mut() @@ -2482,6 +2496,7 @@ pub mod test { transform: None, time: ic_types::Time::from_nanos_since_unix_epoch(10), replication: Replication::FullyReplicated, + pricing_version: PricingVersion::Legacy, }; // Expect times to be called exactly once to check that already diff --git a/rs/protobuf/def/state/metadata/v1/metadata.proto b/rs/protobuf/def/state/metadata/v1/metadata.proto index 3eb93f41e156..4c40d4918a5e 100644 --- a/rs/protobuf/def/state/metadata/v1/metadata.proto +++ b/rs/protobuf/def/state/metadata/v1/metadata.proto @@ -150,9 +150,17 @@ message CanisterHttpRequestContext { optional uint64 max_response_bytes = 9; google.protobuf.BytesValue transform_context = 10; optional Replication replication = 11; + optional PricingVersion pricing_version = 12; reserved 5; } +message PricingVersion { + oneof version { + google.protobuf.Empty legacy = 1; + google.protobuf.Empty pay_as_you_go = 2; + } +} + message Replication { oneof replication_type { google.protobuf.Empty fully_replicated = 1; diff --git a/rs/protobuf/src/gen/state/state.metadata.v1.rs b/rs/protobuf/src/gen/state/state.metadata.v1.rs index 8002b3c542fd..f3076e5cea00 100644 --- a/rs/protobuf/src/gen/state/state.metadata.v1.rs +++ b/rs/protobuf/src/gen/state/state.metadata.v1.rs @@ -214,6 +214,23 @@ pub struct CanisterHttpRequestContext { pub transform_context: ::core::option::Option<::prost::alloc::vec::Vec>, #[prost(message, optional, tag = "11")] pub replication: ::core::option::Option, + #[prost(message, optional, tag = "12")] + pub pricing_version: ::core::option::Option, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct PricingVersion { + #[prost(oneof = "pricing_version::Version", tags = "1, 2")] + pub version: ::core::option::Option, +} +/// Nested message and enum types in `PricingVersion`. +pub mod pricing_version { + #[derive(Clone, Copy, PartialEq, ::prost::Oneof)] + pub enum Version { + #[prost(message, tag = "1")] + Legacy(()), + #[prost(message, tag = "2")] + PayAsYouGo(()), + } } #[derive(Clone, PartialEq, ::prost::Message)] pub struct Replication { diff --git a/rs/replicated_state/src/metadata_state/tests.rs b/rs/replicated_state/src/metadata_state/tests.rs index b98660dbb75d..93dc0754ceaf 100644 --- a/rs/replicated_state/src/metadata_state/tests.rs +++ b/rs/replicated_state/src/metadata_state/tests.rs @@ -30,7 +30,7 @@ use ic_test_utilities_types::{ use ic_types::{ Cycles, ExecutionRound, Height, batch::BlockmakerMetrics, - canister_http::{CanisterHttpMethod, CanisterHttpRequestContext, Replication}, + canister_http::{CanisterHttpMethod, CanisterHttpRequestContext, PricingVersion, Replication}, consensus::idkg::{IDkgMasterPublicKeyId, PreSigId, common::PreSignature}, crypto::{ AlgorithmId, @@ -603,6 +603,7 @@ fn subnet_call_contexts_deserialization() { transform: Some(transform.clone()), time: UNIX_EPOCH, replication: Replication::FullyReplicated, + pricing_version: PricingVersion::Legacy, }; subnet_call_context_manager.push_context(SubnetCallContext::CanisterHttpRequest( canister_http_request, diff --git a/rs/rust_canisters/proxy_canister/src/lib.rs b/rs/rust_canisters/proxy_canister/src/lib.rs index c2778e4c8abb..0a00e70823c9 100644 --- a/rs/rust_canisters/proxy_canister/src/lib.rs +++ b/rs/rust_canisters/proxy_canister/src/lib.rs @@ -39,6 +39,7 @@ pub struct UnvalidatedCanisterHttpRequestArgs { pub method: HttpMethod, pub transform: Option, pub is_replicated: Option, + pub pricing_version: Option, } impl Payload<'_> for UnvalidatedCanisterHttpRequestArgs {} @@ -53,7 +54,8 @@ impl From body: args.body, method: args.method, transform: args.transform, - is_replicated: None, + is_replicated: args.is_replicated, + pricing_version: args.pricing_version, } } } diff --git a/rs/tests/networking/canister_http_correctness_test.rs b/rs/tests/networking/canister_http_correctness_test.rs index 79bfa9a7e806..f52cde720fcf 100644 --- a/rs/tests/networking/canister_http_correctness_test.rs +++ b/rs/tests/networking/canister_http_correctness_test.rs @@ -219,6 +219,7 @@ fn test_enforce_https(env: TestEnv) { }), max_response_bytes: None, is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -256,6 +257,7 @@ fn test_transform_function_is_executed(env: TestEnv) { }), max_response_bytes: None, is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -299,6 +301,7 @@ fn test_non_existent_transform_function(env: TestEnv) { }), max_response_bytes: None, is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -338,6 +341,7 @@ fn test_composite_transform_function_is_not_allowed(env: TestEnv) { }), max_response_bytes: None, is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -372,6 +376,7 @@ fn test_no_cycles_attached(env: TestEnv) { }), max_response_bytes: None, is_replicated: None, + pricing_version: None, }, cycles: 0, }, @@ -423,6 +428,7 @@ fn test_max_possible_request_size(env: TestEnv) { }), max_response_bytes: None, is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -468,6 +474,7 @@ fn test_max_possible_request_size_exceeded(env: TestEnv) { }), max_response_bytes: None, is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -504,6 +511,7 @@ fn test_2mb_response_cycle_for_rejection_path(env: TestEnv) { }), max_response_bytes: None, is_replicated: None, + pricing_version: None, }; let (response, _) = block_on(async move { @@ -548,6 +556,7 @@ fn test_4096_max_response_cycle_case_1(env: TestEnv) { }), max_response_bytes: Some(16384), is_replicated: None, + pricing_version: None, }; let (response, _) = block_on(async move { @@ -586,6 +595,7 @@ fn test_4096_max_response_cycle_case_2(env: TestEnv) { }), max_response_bytes: Some(16384), is_replicated: None, + pricing_version: None, }; let (response, _) = block_on(async move { @@ -626,6 +636,7 @@ fn test_max_response_bytes_2_mb_returns_ok(env: TestEnv) { transform: None, max_response_bytes: Some((MAX_MAX_RESPONSE_BYTES) as u64), is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -649,6 +660,7 @@ fn test_max_response_bytes_too_large(env: TestEnv) { transform: None, max_response_bytes: Some((MAX_MAX_RESPONSE_BYTES + 1) as u64), is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -688,6 +700,7 @@ fn test_transform_that_bloats_on_the_2mb_limit(env: TestEnv) { }), max_response_bytes: None, is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -719,6 +732,7 @@ fn test_transform_that_bloats_on_the_2mb_limit_with_custom_max_response_bytes(en }), max_response_bytes: Some(max_response_bytes), is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -758,6 +772,7 @@ fn test_transform_that_bloats_response_above_2mb_limit(env: TestEnv) { }), max_response_bytes: None, is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -800,6 +815,7 @@ fn test_post_request(env: TestEnv) { }), max_response_bytes: None, is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -835,6 +851,7 @@ fn test_http_endpoint_response_is_within_limits_with_custom_max_response_bytes(e transform: None, max_response_bytes: Some(max_response_bytes), is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -880,6 +897,7 @@ fn test_http_endpoint_response_is_too_large_with_custom_max_response_bytes(env: transform, max_response_bytes: Some(max_response_bytes), is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -920,6 +938,7 @@ fn test_http_endpoint_response_is_within_limits_with_default_max_response_bytes( transform: None, max_response_bytes: None, is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -963,6 +982,7 @@ fn test_http_endpoint_response_is_too_large_with_default_max_response_bytes(env: transform, max_response_bytes: None, is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -999,6 +1019,7 @@ fn test_http_endpoint_with_delayed_response_is_rejected(env: TestEnv) { }), max_response_bytes: None, is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -1035,6 +1056,7 @@ fn test_that_redirects_are_not_followed(env: TestEnv) { }), max_response_bytes: None, is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -1065,6 +1087,7 @@ fn test_http_calls_to_ic_fails(env: TestEnv) { }), max_response_bytes: None, is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -1103,6 +1126,7 @@ fn test_invalid_domain_name(env: TestEnv) { }), max_response_bytes: None, is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -1143,6 +1167,7 @@ fn test_invalid_ip(env: TestEnv) { }), max_response_bytes: None, is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -1179,6 +1204,7 @@ fn test_get_hello_world_call(env: TestEnv) { transform: None, max_response_bytes: Some(max_response_bytes), is_replicated: None, + pricing_version: None, }; let (response, refunded_cycles) = block_on(submit_outcall( @@ -1222,6 +1248,7 @@ fn test_request_header_total_size_within_the_48_kib_limit(env: TestEnv) { transform: None, max_response_bytes: None, is_replicated: None, + pricing_version: None, }; let (response, refunded_cycles) = block_on(submit_outcall( @@ -1269,6 +1296,7 @@ fn test_request_header_total_size_over_the_48_kib_limit(env: TestEnv) { transform: None, max_response_bytes: None, is_replicated: None, + pricing_version: None, }; let (response, refunded_cycles) = block_on(submit_outcall( @@ -1314,6 +1342,7 @@ fn test_response_header_total_size_within_the_48_kib_limit(env: TestEnv) { transform: None, max_response_bytes: Some(DEFAULT_MAX_RESPONSE_BYTES), is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -1365,6 +1394,7 @@ fn test_response_header_total_size_over_the_48_kib_limit(env: TestEnv) { transform: None, max_response_bytes: Some(DEFAULT_MAX_RESPONSE_BYTES), is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -1400,6 +1430,7 @@ fn test_request_header_name_and_value_within_limits(env: TestEnv) { transform: None, max_response_bytes: None, is_replicated: None, + pricing_version: None, }; let (response, _) = block_on(submit_outcall( @@ -1431,6 +1462,7 @@ fn test_request_header_name_too_long(env: TestEnv) { transform: None, max_response_bytes: None, is_replicated: None, + pricing_version: None, }; let (response, refunded_cycles) = block_on(submit_outcall( @@ -1471,6 +1503,7 @@ fn test_request_header_value_too_long(env: TestEnv) { transform: None, max_response_bytes: None, is_replicated: None, + pricing_version: None, }; let (response, refunded_cycles) = block_on(submit_outcall( @@ -1512,6 +1545,7 @@ fn test_response_header_name_within_limit(env: TestEnv) { transform: None, max_response_bytes: None, is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -1541,6 +1575,7 @@ fn test_response_header_name_over_limit(env: TestEnv) { transform: None, max_response_bytes: None, is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -1575,6 +1610,7 @@ fn test_response_header_value_within_limit(env: TestEnv) { transform: None, max_response_bytes: None, is_replicated: None, + pricing_version: None, }; let (response, _) = block_on(submit_outcall( @@ -1606,6 +1642,7 @@ fn test_response_header_value_over_limit(env: TestEnv) { transform: None, max_response_bytes: None, is_replicated: None, + pricing_version: None, }; let (response, refunded_cycles) = block_on(submit_outcall( @@ -1656,6 +1693,7 @@ fn test_post_call(env: TestEnv) { transform: None, max_response_bytes, is_replicated: None, + pricing_version: None, }; let (response, _) = block_on(submit_outcall( @@ -1705,6 +1743,7 @@ fn test_head_call(env: TestEnv) { transform: None, max_response_bytes, is_replicated: None, + pricing_version: None, }; let (response, _) = block_on(submit_outcall( @@ -1759,6 +1798,7 @@ fn test_only_headers_with_custom_max_response_bytes(env: TestEnv) { transform: None, max_response_bytes, is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -1798,6 +1838,7 @@ fn test_only_headers_with_custom_max_response_bytes_exceeded(env: TestEnv) { transform: None, max_response_bytes, is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -1833,6 +1874,7 @@ fn test_non_ascii_url_is_accepted(env: TestEnv) { transform: None, max_response_bytes: Some(max_response_bytes), is_replicated: None, + pricing_version: None, }; let (response, refunded_cycles) = block_on(submit_outcall( @@ -1870,6 +1912,7 @@ fn test_max_url_length(env: TestEnv) { transform: None, max_response_bytes: None, is_replicated: None, + pricing_version: None, }; let (response, _) = block_on(submit_outcall( @@ -1904,6 +1947,7 @@ fn test_max_url_length_exceeded(env: TestEnv) { transform: None, max_response_bytes: None, is_replicated: None, + pricing_version: None, }; let (response, refunded_cycles) = block_on(submit_outcall( @@ -1953,6 +1997,7 @@ fn reference_transform_function_exposed_by_different_canister(env: TestEnv) { body: Some("".as_bytes().to_vec()), max_response_bytes: None, is_replicated: None, + pricing_version: None, transform: Some(TransformContext { function: TransformFunc(candid::Func { principal: proxy_canister_id_2.into(), @@ -2000,6 +2045,7 @@ fn test_max_number_of_response_headers(env: TestEnv) { transform: None, max_response_bytes: None, is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -2038,6 +2084,7 @@ fn test_max_number_of_response_headers_exceeded(env: TestEnv) { transform: None, max_response_bytes: None, is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }, @@ -2075,6 +2122,7 @@ fn test_max_number_of_request_headers(env: TestEnv) { transform: None, max_response_bytes: None, is_replicated: None, + pricing_version: None, }, cycles: HTTP_REQUEST_CYCLE_PAYMENT, }; @@ -2148,6 +2196,7 @@ fn check_caller_id_on_transform_function(env: TestEnv) { body: Some("".as_bytes().to_vec()), max_response_bytes: None, is_replicated: None, + pricing_version: None, transform: Some(TransformContext { function: TransformFunc(candid::Func { principal: get_proxy_canister_id(&env).into(), diff --git a/rs/tests/networking/canister_http_fault_tolerance_test.rs b/rs/tests/networking/canister_http_fault_tolerance_test.rs index f54f99e2bf8f..6722afe0e25a 100644 --- a/rs/tests/networking/canister_http_fault_tolerance_test.rs +++ b/rs/tests/networking/canister_http_fault_tolerance_test.rs @@ -165,6 +165,7 @@ pub fn test(env: TestEnv) { }), max_response_bytes: None, is_replicated: None, + pricing_version: None, }, cycles: 500_000_000_000, }, diff --git a/rs/tests/networking/canister_http_non_replicated_test.rs b/rs/tests/networking/canister_http_non_replicated_test.rs index f4df856ea3ec..75552ce4fd3f 100644 --- a/rs/tests/networking/canister_http_non_replicated_test.rs +++ b/rs/tests/networking/canister_http_non_replicated_test.rs @@ -103,6 +103,7 @@ async fn make_request( method: HttpMethod::GET, max_response_bytes: None, is_replicated: Some(is_replicated), + pricing_version: None, }, cycles: 500_000_000_000, }, diff --git a/rs/tests/networking/canister_http_soak_test.rs b/rs/tests/networking/canister_http_soak_test.rs index 8429b62d0d51..6b1f06f16086 100644 --- a/rs/tests/networking/canister_http_soak_test.rs +++ b/rs/tests/networking/canister_http_soak_test.rs @@ -124,6 +124,7 @@ async fn leave_proxy_canister_running(proxy_canister: &Canister<'_>, url: String method: HttpMethod::GET, max_response_bytes: None, is_replicated: Some(true), + pricing_version: None, }, cycles: 500_000_000_000, }, diff --git a/rs/tests/networking/canister_http_socks_test.rs b/rs/tests/networking/canister_http_socks_test.rs index f71f8c05adc4..b2148925e136 100644 --- a/rs/tests/networking/canister_http_socks_test.rs +++ b/rs/tests/networking/canister_http_socks_test.rs @@ -198,6 +198,7 @@ fn assert_outcall_result( max_response_bytes: None, // Not replicated, as the /ip endpoint returns different results on each call. is_replicated: Some(false), + pricing_version: None, }, cycles: 500_000_000_000, }, diff --git a/rs/tests/networking/canister_http_stress_test.rs b/rs/tests/networking/canister_http_stress_test.rs index 8f201a24d02e..f3a65dca0e9f 100644 --- a/rs/tests/networking/canister_http_stress_test.rs +++ b/rs/tests/networking/canister_http_stress_test.rs @@ -177,6 +177,7 @@ async fn do_request( method: HttpMethod::GET, max_response_bytes: None, is_replicated: None, + pricing_version: None, }, cycles: 500_000_000_000, }, diff --git a/rs/tests/networking/canister_http_test.rs b/rs/tests/networking/canister_http_test.rs index a3e17ce26f61..04b9a4d64602 100644 --- a/rs/tests/networking/canister_http_test.rs +++ b/rs/tests/networking/canister_http_test.rs @@ -95,6 +95,7 @@ async fn test_proxy_canister(proxy_canister: &Canister<'_>, url: String, logger: method: HttpMethod::GET, max_response_bytes: None, is_replicated: None, + pricing_version: None, }, cycles: 500_000_000_000, }, diff --git a/rs/tests/networking/canister_http_time_out_test.rs b/rs/tests/networking/canister_http_time_out_test.rs index 4ebff96339d6..d06adaf606d0 100644 --- a/rs/tests/networking/canister_http_time_out_test.rs +++ b/rs/tests/networking/canister_http_time_out_test.rs @@ -66,6 +66,7 @@ pub fn test(env: TestEnv) { }), max_response_bytes: None, is_replicated: None, + pricing_version: None, }, cycles: 500_000_000_000, }; diff --git a/rs/tests/query_stats/lib/src/lib.rs b/rs/tests/query_stats/lib/src/lib.rs index 7eb79b4c4d71..3b9708e7b4eb 100644 --- a/rs/tests/query_stats/lib/src/lib.rs +++ b/rs/tests/query_stats/lib/src/lib.rs @@ -126,6 +126,7 @@ pub(crate) async fn single_https_outcall(canister: &Principal, agents: &[Agent]) context: vec![], }), is_replicated: None, + pricing_version: None, }; agents diff --git a/rs/types/management_canister_types/src/http.rs b/rs/types/management_canister_types/src/http.rs index cc6805670883..7de73f8cee25 100644 --- a/rs/types/management_canister_types/src/http.rs +++ b/rs/types/management_canister_types/src/http.rs @@ -55,6 +55,22 @@ const HTTP_HEADERS_TOTAL_MAX_SIZE: usize = 48 * KIB; /// Described in . const HTTP_HEADERS_ELEMENT_MAX_SIZE: usize = 16 * KIB; // name + value = 8KiB + 8KiB +/// The numeric representation for the Legacy pricing version. +pub const PRICING_VERSION_LEGACY: u32 = 1; +/// The numeric representation for the Pay-As-You-Go pricing version. +pub const PRICING_VERSION_PAY_AS_YOU_GO: u32 = 2; + +/// The default pricing version for HTTP outcalls. +/// +/// If the field is missing, this is the version that will be assumed by the replica. +/// Described in . +pub const DEFAULT_HTTP_OUTCALLS_PRICING_VERSION: u32 = PRICING_VERSION_LEGACY; + +/// A set of all allowed pricing versions for HTTP outcalls. +/// +/// If the pricing version provided in the request is not in this set, the request will use the default pricing version. +pub const ALLOWED_HTTP_OUTCALLS_PRICING_VERSIONS: &[u32] = &[PRICING_VERSION_LEGACY]; + /// HTTP headers bounded by total size. pub type BoundedHttpHeaders = BoundedVec< HTTP_HEADERS_MAX_NUMBER, @@ -76,6 +92,7 @@ pub type BoundedHttpHeaders = BoundedVec< /// context : blob; /// }; /// is_replicated : opt bool; +/// pricing_version : opt nat32; /// } /// ``` #[derive(Clone, PartialEq, Debug, CandidType, Deserialize)] @@ -88,6 +105,7 @@ pub struct CanisterHttpRequestArgs { pub method: HttpMethod, pub transform: Option, pub is_replicated: Option, + pub pricing_version: Option, } impl Payload<'_> for CanisterHttpRequestArgs {} @@ -123,6 +141,7 @@ fn test_http_headers_max_number() { method: HttpMethod::GET, transform: None, is_replicated: None, + pricing_version: None, }; // Act. @@ -176,6 +195,7 @@ fn test_http_headers_max_total_size() { method: HttpMethod::GET, transform: None, is_replicated: None, + pricing_version: None, }; // Act. @@ -223,6 +243,7 @@ fn test_http_headers_max_element_size() { method: HttpMethod::GET, transform: None, is_replicated: None, + pricing_version: None, }; // Act. diff --git a/rs/types/management_canister_types/src/lib.rs b/rs/types/management_canister_types/src/lib.rs index 061cd93ec9cc..4682ce4eb4ba 100644 --- a/rs/types/management_canister_types/src/lib.rs +++ b/rs/types/management_canister_types/src/lib.rs @@ -10,8 +10,10 @@ pub use bounded_vec::*; use candid::{CandidType, Decode, DecoderConfig, Deserialize, Encode, Reserved}; pub use data_size::*; pub use http::{ - BoundedHttpHeaders, CanisterHttpRequestArgs, CanisterHttpResponsePayload, HttpHeader, - HttpMethod, TransformArgs, TransformContext, TransformFunc, + ALLOWED_HTTP_OUTCALLS_PRICING_VERSIONS, BoundedHttpHeaders, CanisterHttpRequestArgs, + CanisterHttpResponsePayload, DEFAULT_HTTP_OUTCALLS_PRICING_VERSION, HttpHeader, HttpMethod, + PRICING_VERSION_LEGACY, PRICING_VERSION_PAY_AS_YOU_GO, TransformArgs, TransformContext, + TransformFunc, }; use ic_base_types::{ CanisterId, EnvironmentVariables, NodeId, NumBytes, PrincipalId, RegistryVersion, SnapshotId, diff --git a/rs/types/management_canister_types/tests/ic.did b/rs/types/management_canister_types/tests/ic.did index fc237fb404d5..018d35e18aab 100644 --- a/rs/types/management_canister_types/tests/ic.did +++ b/rs/types/management_canister_types/tests/ic.did @@ -351,6 +351,7 @@ type http_request_args = record { context : blob; }; is_replicated : opt bool; + pricing_version: opt nat32; }; type ecdsa_public_key_args = record { diff --git a/rs/types/types/src/canister_http.rs b/rs/types/types/src/canister_http.rs index b8f494a09923..f0d3323ee445 100644 --- a/rs/types/types/src/canister_http.rs +++ b/rs/types/types/src/canister_http.rs @@ -54,7 +54,9 @@ use ic_error_types::{ErrorCode, RejectCode, UserError}; #[cfg(test)] use ic_exhaustive_derive::ExhaustiveSet; use ic_management_canister_types_private::{ - CanisterHttpRequestArgs, DataSize, HttpHeader, HttpMethod, TransformContext, + ALLOWED_HTTP_OUTCALLS_PRICING_VERSIONS, CanisterHttpRequestArgs, + DEFAULT_HTTP_OUTCALLS_PRICING_VERSION, DataSize, HttpHeader, HttpMethod, + PRICING_VERSION_LEGACY, PRICING_VERSION_PAY_AS_YOU_GO, TransformContext, }; use ic_protobuf::{ proxy::{ProxyDecodeError, try_from_option_field}, @@ -68,6 +70,7 @@ use std::{ mem::size_of, time::Duration, }; +use strum::FromRepr; use strum_macros::EnumIter; /// Time after which a response is considered timed out and a timeout error will be returned to execution @@ -130,6 +133,7 @@ pub struct CanisterHttpRequestContext { pub time: Time, /// The replication strategy for this request. pub replication: Replication, + pub pricing_version: PricingVersion, } #[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize)] @@ -140,6 +144,13 @@ pub enum Replication { NonReplicated(NodeId), } +#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize, FromRepr)] +#[repr(u32)] +pub enum PricingVersion { + Legacy = PRICING_VERSION_LEGACY, + PayAsYouGo = PRICING_VERSION_PAY_AS_YOU_GO, +} + impl From<&CanisterHttpRequestContext> for pb_metadata::CanisterHttpRequestContext { fn from(context: &CanisterHttpRequestContext) -> Self { let replication_type = match context.replication { @@ -157,6 +168,15 @@ impl From<&CanisterHttpRequestContext> for pb_metadata::CanisterHttpRequestConte replication_type: Some(replication_type), }; + let pricing_version = match context.pricing_version { + PricingVersion::Legacy => pb_metadata::pricing_version::Version::Legacy(()), + PricingVersion::PayAsYouGo => pb_metadata::pricing_version::Version::PayAsYouGo(()), + }; + + let pricing_message = pb_metadata::PricingVersion { + version: Some(pricing_version), + }; + pb_metadata::CanisterHttpRequestContext { request: Some((&context.request).into()), url: context.url.clone(), @@ -184,10 +204,16 @@ impl From<&CanisterHttpRequestContext> for pb_metadata::CanisterHttpRequestConte http_method: pb_metadata::HttpMethod::from(&context.http_method).into(), time: context.time.as_nanos_since_unix_epoch(), replication: Some(replication_message), + pricing_version: Some(pricing_message), } } } +pub fn default_pricing_version() -> PricingVersion { + PricingVersion::from_repr(DEFAULT_HTTP_OUTCALLS_PRICING_VERSION) + .unwrap_or(PricingVersion::Legacy) +} + impl TryFrom for CanisterHttpRequestContext { type Error = ProxyDecodeError; @@ -208,6 +234,17 @@ impl TryFrom for CanisterHttpRequestCon None => Replication::FullyReplicated, }; + let pricing_version = match context.pricing_version { + Some(pricing_version) => match pricing_version.version { + Some(pb_metadata::pricing_version::Version::Legacy(_)) => PricingVersion::Legacy, + Some(pb_metadata::pricing_version::Version::PayAsYouGo(_)) => { + PricingVersion::PayAsYouGo + } + None => default_pricing_version(), + }, + None => default_pricing_version(), + }; + let transform_method_name = context.transform_method_name; let transform_context = context.transform_context; let transform = match (transform_method_name, transform_context) { @@ -256,6 +293,7 @@ impl TryFrom for CanisterHttpRequestCon transform, time: Time::from_nanos_since_unix_epoch(context.time), replication, + pricing_version, }) } } @@ -415,6 +453,13 @@ impl CanisterHttpRequestContext { transform: args.transform.map(From::from), time, replication, + pricing_version: { + let final_version_u32 = args + .pricing_version + .filter(|v| ALLOWED_HTTP_OUTCALLS_PRICING_VERSIONS.contains(v)) + .unwrap_or(DEFAULT_HTTP_OUTCALLS_PRICING_VERSION); + PricingVersion::from_repr(final_version_u32).unwrap_or(PricingVersion::Legacy) + }, }) } } @@ -448,7 +493,6 @@ pub enum CanisterHttpRequestContextError { TooLongHeaderValue(usize), TooLargeHeaders(usize), TooLargeRequest(usize), - NonReplicatedNotSupported, NoNodesAvailableForDelegation, } @@ -503,10 +547,6 @@ impl From for UserError { "total number of bytes to represent all http header names and values and http body {total_request_size} exceeds {MAX_CANISTER_HTTP_REQUEST_BYTES}" ), ), - CanisterHttpRequestContextError::NonReplicatedNotSupported => UserError::new( - ErrorCode::CanisterRejectedMessage, - "Canister HTTP requests with is_replicated=false are not supported".to_string(), - ), CanisterHttpRequestContextError::NoNodesAvailableForDelegation => UserError::new( ErrorCode::CanisterRejectedMessage, "No nodes available for delegation for non-replicated canister HTTP request." @@ -784,6 +824,7 @@ mod tests { }, time: UNIX_EPOCH, replication: Replication::FullyReplicated, + pricing_version: PricingVersion::Legacy, }; let expected_size = context.url.len() @@ -827,6 +868,7 @@ mod tests { }, time: UNIX_EPOCH, replication: Replication::FullyReplicated, + pricing_version: PricingVersion::Legacy, }; let expected_size = context.url.len()