From ce60aeab78c6c64ee931ded895b140570cf16e2f Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Tue, 1 Sep 2026 14:43:06 +0000 Subject: [PATCH 1/2] enable --- packages/pocket-ic/tests/tests.rs | 47 ++------- rs/config/src/execution_environment.rs | 2 +- .../src/execution_environment/tests.rs | 93 ++++++++++++------ .../execution_environment/src/lib.rs | 5 + .../canister_http_correctness_test.rs | 95 +------------------ 5 files changed, 86 insertions(+), 156 deletions(-) diff --git a/packages/pocket-ic/tests/tests.rs b/packages/pocket-ic/tests/tests.rs index 1648e877ac5b..7816878617c8 100644 --- a/packages/pocket-ic/tests/tests.rs +++ b/packages/pocket-ic/tests/tests.rs @@ -2848,10 +2848,12 @@ fn test_canister_http_derived_spend_is_only_charged_under_pay_as_you_go() { /// Which outcalls are available, and which pricing model they end up with, on the /// three kinds of instance a test can build: one with beta features, a plain one, /// and one whose subnet does not charge for HTTP outcalls. +/// +/// The pay-as-you-go pricing model is enabled on every subnet, so all three offer +/// flexible outcalls and price them the same way; what differs between them is +/// only whether anything is actually charged. #[test] fn test_canister_http_availability_and_pricing() { - // (label, instance, canister, whether the pay-as-you-go pricing model is - // enabled, whether HTTP outcalls are free) let beta = flexible_outcalls_pic(); let beta_canister = deploy_test_canister(&beta); let default = PocketIc::new(); @@ -2862,10 +2864,10 @@ fn test_canister_http_availability_and_pricing() { system.add_cycles(system_canister, INIT_CYCLES); system.install_canister(system_canister, test_canister_wasm(), vec![], None); - for (env, pic, canister_id, pay_as_you_go_enabled, outcalls_are_free) in [ - ("beta features", &beta, beta_canister, true, false), - ("default instance", &default, default_canister, false, false), - ("system subnet", &system, system_canister, false, true), + for (env, pic, canister_id) in [ + ("beta features", &beta, beta_canister), + ("default instance", &default, default_canister), + ("system subnet", &system, system_canister), ] { for outcall in [ Outcall::FullyReplicated { @@ -2883,38 +2885,9 @@ fn test_canister_http_availability_and_pricing() { })), ] { let label = format!("{env}, {}", outcall.label()); - // A flexible outcall is only offered where the pay-as-you-go pricing - // model is enabled or where HTTP outcalls are free. - if outcall.is_flexible() && !pay_as_you_go_enabled && !outcalls_are_free { - let (method, args) = outcall.call(None); - let reply = pic - .update_call( - canister_id, - Principal::anonymous(), - "proxy_call", - proxy_call_arg(method, args, OUTCALL_CYCLES), - ) - .unwrap(); - let (reject_code, message) = decode_sync_rejection(&reply); - assert!( - matches!(reject_code, RejectionCode::CanisterError), - "{label}: unexpected reject code {reject_code:?} ({message})" - ); - assert!( - message.contains("This API is not enabled on this subnet"), - "{label}: unexpected rejection message {message:?}" - ); - continue; - } // `submit_outcall` asserts the reported replication and pricing model. - let (call_id, request) = submit_outcall( - pic, - canister_id, - &outcall, - None, - OUTCALL_CYCLES, - pay_as_you_go_enabled, - ); + let (call_id, request) = + submit_outcall(pic, canister_id, &outcall, None, OUTCALL_CYCLES, true); let body = b"hello".to_vec(); if outcall.is_flexible() { mock_committee( diff --git a/rs/config/src/execution_environment.rs b/rs/config/src/execution_environment.rs index fdbcf6b32374..ba7cac3a9716 100644 --- a/rs/config/src/execution_environment.rs +++ b/rs/config/src/execution_environment.rs @@ -13,7 +13,7 @@ const TIB: u64 = 1024 * GIB; const REPLICATED_INTER_CANISTER_LOG_FETCH_FEATURE: FlagStatus = FlagStatus::Enabled; -const FLEXIBLE_HTTP_REQUESTS_FEATURE: FlagStatus = FlagStatus::Disabled; +const FLEXIBLE_HTTP_REQUESTS_FEATURE: FlagStatus = FlagStatus::Enabled; pub const TEST_DEFAULT_LOG_MEMORY_LIMIT: u64 = 4 * KIB; pub const TEST_DEFAULT_LOG_MEMORY_USAGE: u64 = 4 * KIB + 4 * KIB + TEST_DEFAULT_LOG_MEMORY_LIMIT; // header, index table, data region diff --git a/rs/execution_environment/src/execution_environment/tests.rs b/rs/execution_environment/src/execution_environment/tests.rs index 5cf2ae22cb63..c4e5e6578130 100644 --- a/rs/execution_environment/src/execution_environment/tests.rs +++ b/rs/execution_environment/src/execution_environment/tests.rs @@ -3984,12 +3984,14 @@ fn execute_canister_http_request_pricing_version() { let own_subnet = subnet_test_id(1); let caller_canister = canister_test_id(10); - let mut builder = ExecutionTestBuilder::new() + let builder = ExecutionTestBuilder::new() .with_own_subnet_id(own_subnet) .with_caller(own_subnet, caller_canister); - if flexible_http_requests_enabled { - builder = builder.with_flexible_http_requests_enabled(); - } + let builder = if flexible_http_requests_enabled { + builder.with_flexible_http_requests_enabled() + } else { + builder.with_flexible_http_requests_disabled() + }; let mut test = builder.build(); std::sync::Arc::make_mut(&mut test.state_mut().metadata.own_subnet_info) .subnet_features @@ -4022,18 +4024,16 @@ fn execute_canister_http_request_pricing_version() { } #[test] -fn execute_flexible_canister_http_request_free_subnet_uses_legacy() { - // On a free subnet, flexible outcalls are available by default (without the - // `flexible_http_requests` feature flag) and fall back to legacy pricing, - // where pricing is moot because a free subnet charges nothing. +fn execute_flexible_canister_http_request_free_subnet_uses_pay_as_you_go() { + // Pay-as-you-go applies to every subnet, free ones included. Pricing is moot + // there — a free subnet charges nothing — but the request is still routed + // through the new pricing model rather than the legacy fallback. let own_subnet = subnet_test_id(1); let caller_canister = canister_test_id(10); let mut test = ExecutionTestBuilder::new() .with_own_subnet_id(own_subnet) .with_caller(own_subnet, caller_canister) .with_cost_schedule(CanisterCyclesCostSchedule::Free) - // The feature flag is deliberately left disabled: the free-subnet path - // does not depend on it. .build(); let args = flexible_http_request_args(caller_canister); @@ -4051,8 +4051,10 @@ fn execute_flexible_canister_http_request_free_subnet_uses_legacy() { let http_request_context = canister_http_request_contexts .get(&CallbackId::from(0)) .unwrap(); - // The request is routed through legacy pricing with flexible replication. - assert_eq!(http_request_context.pricing_version, PricingVersion::Legacy); + assert_eq!( + http_request_context.pricing_version, + PricingVersion::PayAsYouGo + ); assert!( matches!( http_request_context.replication, @@ -4062,9 +4064,9 @@ fn execute_flexible_canister_http_request_free_subnet_uses_legacy() { http_request_context.replication ); - // Legacy pricing on a free subnet charges nothing: the full payment is - // retained (to be refunded when the response is delivered) and nothing is - // marked refundable through the pay-as-you-go mechanism. + // A free subnet charges nothing whatever the pricing model: the full payment + // is retained (to be refunded when the response is delivered) and there is no + // allowance to spend, hence nothing to refund out of one. assert_eq!(http_request_context.request.payment, payment); assert_eq!( http_request_context.refund_status.refundable_cycles, @@ -4077,17 +4079,14 @@ fn execute_flexible_canister_http_request_free_subnet_uses_legacy() { } #[test] -fn execute_flexible_canister_http_request_system_subnet_uses_legacy() { +fn execute_flexible_canister_http_request_system_subnet_uses_pay_as_you_go() { // System subnets charge nothing for HTTP outcalls despite a normal cost - // schedule, so flexible outcalls are available there by default (without the - // feature flag) and fall back to legacy pricing. + // schedule. Like a free subnet, they are still routed through pay-as-you-go. let own_subnet = subnet_test_id(1); let caller_canister = canister_test_id(10); let mut test = ExecutionTestBuilder::new() .with_own_subnet_id(own_subnet) .with_caller(own_subnet, caller_canister) - // A system subnet keeps the default (normal) cost schedule but charges - // zero for HTTP outcalls; the feature flag is left disabled. .with_subnet_type(SubnetType::System) .build(); @@ -4106,9 +4105,10 @@ fn execute_flexible_canister_http_request_system_subnet_uses_legacy() { let http_request_context = canister_http_request_contexts .get(&CallbackId::from(0)) .unwrap(); - // Routed through legacy pricing with flexible replication, just like a - // free-cost-schedule subnet. - assert_eq!(http_request_context.pricing_version, PricingVersion::Legacy); + assert_eq!( + http_request_context.pricing_version, + PricingVersion::PayAsYouGo + ); assert!( matches!( http_request_context.replication, @@ -4121,8 +4121,8 @@ fn execute_flexible_canister_http_request_system_subnet_uses_legacy() { // A system subnet charges nothing for HTTP outcalls despite its normal cost // schedule, so `try_add_http_context_to_replicated_state` treats it as free // just like a free-cost-schedule subnet: the full payment is retained (to be - // refunded when the response is delivered) and nothing is marked refundable - // through the pay-as-you-go mechanism. + // refunded when the response is delivered) and there is no allowance to + // spend, hence nothing to refund out of one. assert_eq!(http_request_context.request.payment, payment); assert_eq!( http_request_context.refund_status.refundable_cycles, @@ -4230,13 +4230,14 @@ fn execute_flexible_canister_http_request_insufficient_payment() { #[test] fn execute_flexible_canister_http_request_disabled() { // On a paying subnet, flexible outcalls under pay-as-you-go pricing are - // gated behind the `flexible_http_requests` feature flag, which is disabled - // by default. + // gated behind the `flexible_http_requests` feature flag. The flag now + // defaults to enabled, so turning it back off must still shut them out. let own_subnet = subnet_test_id(1); let caller_canister = canister_test_id(10); let mut test = ExecutionTestBuilder::new() .with_own_subnet_id(own_subnet) .with_caller(own_subnet, caller_canister) + .with_flexible_http_requests_disabled() .build(); let args = flexible_http_request_args(caller_canister); @@ -4262,6 +4263,44 @@ fn execute_flexible_canister_http_request_disabled() { ); } +#[test] +fn execute_flexible_canister_http_request_disabled_falls_back_to_legacy_when_free() { + // Turning the flag off does not take flexible outcalls away from subnets + // where they are free: there they fall back to legacy pricing, which is moot + // when nothing is charged. This is the one path that still distinguishes the + // flag being off from it being on. + let own_subnet = subnet_test_id(1); + let caller_canister = canister_test_id(10); + let mut test = ExecutionTestBuilder::new() + .with_own_subnet_id(own_subnet) + .with_caller(own_subnet, caller_canister) + .with_cost_schedule(CanisterCyclesCostSchedule::Free) + .with_flexible_http_requests_disabled() + .build(); + + let args = flexible_http_request_args(caller_canister); + test.inject_call_to_ic00( + Method::FlexibleHttpRequest, + args.encode(), + Cycles::new(1_000_000_000), + ); + test.execute_all(); + + let canister_http_request_contexts = &test + .state() + .metadata + .subnet_call_context_manager + .canister_http_request_contexts; + assert_eq!(canister_http_request_contexts.len(), 1); + assert_eq!( + canister_http_request_contexts + .get(&CallbackId::from(0)) + .unwrap() + .pricing_version, + PricingVersion::Legacy + ); +} + #[test] fn execute_flexible_canister_http_request_disabled_by_subnet_feature() { /// The configurations in which flexible outcalls would be available if the diff --git a/rs/test_utilities/execution_environment/src/lib.rs b/rs/test_utilities/execution_environment/src/lib.rs index f23ce00bfa91..e8516f83e315 100644 --- a/rs/test_utilities/execution_environment/src/lib.rs +++ b/rs/test_utilities/execution_environment/src/lib.rs @@ -2719,6 +2719,11 @@ impl ExecutionTestBuilder { self } + pub fn with_flexible_http_requests_disabled(mut self) -> Self { + self.execution_config.flexible_http_requests = FlagStatus::Disabled; + self + } + pub fn without_composite_queries(mut self) -> Self { self.execution_config.composite_queries = FlagStatus::Disabled; self diff --git a/rs/tests/networking/canister_http_correctness_test.rs b/rs/tests/networking/canister_http_correctness_test.rs index e2cef1bcaab6..cf541faa7234 100644 --- a/rs/tests/networking/canister_http_correctness_test.rs +++ b/rs/tests/networking/canister_http_correctness_test.rs @@ -28,8 +28,7 @@ use ic_base_types::{CanisterId, NumBytes, PrincipalId}; use ic_config::subnet_config::DEFAULT_REFERENCE_SUBNET_SIZE; use ic_cycles_account_manager::CyclesAccountManagerSubnetConfig; use ic_management_canister_types_private::{ - BoundedHttpHeaders, FlexibleCanisterHttpRequestArgs, HttpHeader, HttpMethod, TransformContext, - TransformFunc, + HttpHeader, HttpMethod, TransformContext, TransformFunc, }; use ic_system_test_driver::{ canister_agent::HasCanisterAgentCapability, @@ -50,8 +49,8 @@ use ic_types::{ }; use ic_types_cycles::CanisterCyclesCostSchedule; use proxy_canister::{ - FlexibleRemoteHttpRequest, RejectionCode, RemoteHttpRequest, RemoteHttpResponse, - ResponseWithRefundedCycles, UnvalidatedCanisterHttpRequestArgs, + RejectionCode, RemoteHttpRequest, RemoteHttpResponse, ResponseWithRefundedCycles, + UnvalidatedCanisterHttpRequestArgs, }; use serde_json::Value; use std::collections::{BTreeSet, HashSet}; @@ -200,11 +199,7 @@ fn main() -> Result<()> { test_only_headers_with_custom_max_response_bytes_exceeded )) .add_test(systest!(test_max_response_bytes_too_large)) - .add_test(systest!(test_max_response_bytes_2_mb_returns_ok)) - // Flexible outcalls are not available on a normal (paying) subnet. - .add_test(systest!( - test_flexible_http_request_not_enabled_on_normal_subnet - )), + .add_test(systest!(test_max_response_bytes_2_mb_returns_ok)), ) .execute_from_args()?; @@ -2714,88 +2709,6 @@ where } } -/// Submits a flexible HTTP outcall through the proxy canister and returns the -/// proxy's raw reply: `Ok(bytes)` (the Candid-encoded `FlexibleHttpRequestResult`) -/// on a handled outcall, or `Err((code, message))` when `flexible_http_request` -/// is rejected synchronously (e.g. because it is not enabled on this subnet). -async fn submit_flexible_outcall( - handlers: &Handlers<'_>, - request: FlexibleRemoteHttpRequest, -) -> Result, (RejectionCode, String)> { - let args = Encode!(&request).unwrap(); - let agent = handlers.agent().await; - - let principal_id: PrincipalId = handlers.proxy_canister().effective_canister_id(); - let principal: Principal = principal_id.into(); - - let log = handlers.env.logger(); - let canister_response = match retry_agent_on_transport_errors!( - "submit_flexible_outcall: call", - &log, - agent - .update(&principal, "send_flexible_request") - .with_arg(args.clone()) - .call() - ) - .await - .expect("submit_flexible_outcall retries exhausted") - { - Ok(CallResponse::Response(response)) => Ok(response), - Ok(CallResponse::Poll(request_id)) => retry_agent_on_transport_errors!( - "submit_flexible_outcall: wait", - &log, - agent.wait(&request_id, principal) - ) - .await - .expect("submit_flexible_outcall retries exhausted"), - Err(err) => Err(err), - }; - - let serialized_bytes = canister_response.expect("send_flexible_request should reply"); - decode_one::, (RejectionCode, String)>>(&serialized_bytes.0) - .expect("Decoding the send_flexible_request reply should succeed.") -} - -/// Flexible HTTP outcalls are priced with the (not-yet-enabled) pay-as-you-go -/// pricing model, so they are rejected on a normal (paying) subnet. (On a free -/// subnet they are available via the legacy pricing fallback; that path is -/// covered by `canister_http_flexible_test`.) -fn test_flexible_http_request_not_enabled_on_normal_subnet(env: TestEnv) { - let handlers = Handlers::new(&env); - let webserver_ipv6 = get_universal_vm_address(&env); - - let request = FlexibleRemoteHttpRequest { - request: FlexibleCanisterHttpRequestArgs { - url: format!("https://[{webserver_ipv6}]/ascii/hello"), - max_response_bytes: None, - headers: BoundedHttpHeaders::new(vec![]), - body: None, - method: HttpMethod::GET, - transform: None, - replication: None, - }, - cycles: HTTP_REQUEST_CYCLE_PAYMENT, - }; - - let result = block_on(submit_flexible_outcall(&handlers, request)); - - match result { - Err((reject_code, message)) => { - // A `CanisterContractViolation` (5xx) surfaces as `CanisterError`. - assert_matches!(reject_code, RejectionCode::CanisterError); - assert!( - message.contains("This API is not enabled on this subnet"), - "unexpected rejection message: '{message}'" - ); - } - Ok(bytes) => panic!( - "expected the flexible outcall to be rejected on a normal subnet, \ - got a {}-byte reply", - bytes.len() - ), - } -} - /// Pricing function of canister http requests. fn expected_cycle_cost( proxy_canister: CanisterId, From f0b673ea9c5e71ce571c57c5cef9f9658dfecddb Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Wed, 2 Sep 2026 11:08:32 +0000 Subject: [PATCH 2/2] review --- .../src/execution_environment/tests.rs | 66 ++++++++++++++++--- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/rs/execution_environment/src/execution_environment/tests.rs b/rs/execution_environment/src/execution_environment/tests.rs index c4e5e6578130..2df7d40e47d6 100644 --- a/rs/execution_environment/src/execution_environment/tests.rs +++ b/rs/execution_environment/src/execution_environment/tests.rs @@ -4025,15 +4025,17 @@ fn execute_canister_http_request_pricing_version() { #[test] fn execute_flexible_canister_http_request_free_subnet_uses_pay_as_you_go() { - // Pay-as-you-go applies to every subnet, free ones included. Pricing is moot - // there — a free subnet charges nothing — but the request is still routed - // through the new pricing model rather than the legacy fallback. + // With the `flexible_http_requests` feature flag enabled, pay-as-you-go + // applies to every subnet, free ones included. Pricing is moot there — a free + // subnet charges nothing — but the request is still routed through the new + // pricing model rather than the legacy fallback. let own_subnet = subnet_test_id(1); let caller_canister = canister_test_id(10); let mut test = ExecutionTestBuilder::new() .with_own_subnet_id(own_subnet) .with_caller(own_subnet, caller_canister) .with_cost_schedule(CanisterCyclesCostSchedule::Free) + .with_flexible_http_requests_enabled() .build(); let args = flexible_http_request_args(caller_canister); @@ -4081,13 +4083,15 @@ fn execute_flexible_canister_http_request_free_subnet_uses_pay_as_you_go() { #[test] fn execute_flexible_canister_http_request_system_subnet_uses_pay_as_you_go() { // System subnets charge nothing for HTTP outcalls despite a normal cost - // schedule. Like a free subnet, they are still routed through pay-as-you-go. + // schedule. Like a free subnet, they are still routed through pay-as-you-go + // once the `flexible_http_requests` feature flag is enabled. let own_subnet = subnet_test_id(1); let caller_canister = canister_test_id(10); let mut test = ExecutionTestBuilder::new() .with_own_subnet_id(own_subnet) .with_caller(own_subnet, caller_canister) .with_subnet_type(SubnetType::System) + .with_flexible_http_requests_enabled() .build(); let args = flexible_http_request_args(caller_canister); @@ -4230,8 +4234,8 @@ fn execute_flexible_canister_http_request_insufficient_payment() { #[test] fn execute_flexible_canister_http_request_disabled() { // On a paying subnet, flexible outcalls under pay-as-you-go pricing are - // gated behind the `flexible_http_requests` feature flag. The flag now - // defaults to enabled, so turning it back off must still shut them out. + // gated behind the `flexible_http_requests` feature flag: with the flag + // disabled they are not offered at all. let own_subnet = subnet_test_id(1); let caller_canister = canister_test_id(10); let mut test = ExecutionTestBuilder::new() @@ -4265,10 +4269,10 @@ fn execute_flexible_canister_http_request_disabled() { #[test] fn execute_flexible_canister_http_request_disabled_falls_back_to_legacy_when_free() { - // Turning the flag off does not take flexible outcalls away from subnets - // where they are free: there they fall back to legacy pricing, which is moot - // when nothing is charged. This is the one path that still distinguishes the - // flag being off from it being on. + // A disabled flag does not take flexible outcalls away from subnets where + // they are free: there they fall back to legacy pricing, which is moot when + // nothing is charged. These fallbacks are what still distinguishes the flag + // being off from it being on. let own_subnet = subnet_test_id(1); let caller_canister = canister_test_id(10); let mut test = ExecutionTestBuilder::new() @@ -4301,6 +4305,48 @@ fn execute_flexible_canister_http_request_disabled_falls_back_to_legacy_when_fre ); } +#[test] +fn execute_flexible_canister_http_request_disabled_falls_back_to_legacy_on_system_subnet() { + // Same fallback as on a free cost schedule, via a different route: a system + // subnet keeps a normal cost schedule but charges zero for HTTP outcalls, so + // it is treated as free here. Flexible outcalls therefore remain available + // with the flag disabled, under legacy pricing. + let own_subnet = subnet_test_id(1); + let caller_canister = canister_test_id(10); + let mut test = ExecutionTestBuilder::new() + .with_own_subnet_id(own_subnet) + .with_caller(own_subnet, caller_canister) + .with_subnet_type(SubnetType::System) + .with_flexible_http_requests_disabled() + .build(); + assert_eq!( + test.state().get_own_cost_schedule(), + CanisterCyclesCostSchedule::Normal + ); + + let args = flexible_http_request_args(caller_canister); + test.inject_call_to_ic00( + Method::FlexibleHttpRequest, + args.encode(), + Cycles::new(1_000_000_000), + ); + test.execute_all(); + + let canister_http_request_contexts = &test + .state() + .metadata + .subnet_call_context_manager + .canister_http_request_contexts; + assert_eq!(canister_http_request_contexts.len(), 1); + assert_eq!( + canister_http_request_contexts + .get(&CallbackId::from(0)) + .unwrap() + .pricing_version, + PricingVersion::Legacy + ); +} + #[test] fn execute_flexible_canister_http_request_disabled_by_subnet_feature() { /// The configurations in which flexible outcalls would be available if the