Skip to content
Merged
Show file tree
Hide file tree
Changes from 96 commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
ac96a9e
spent
eichhorl Jul 8, 2026
a383497
spend can exceed allowance on free subnets
eichhorl Jul 8, 2026
77ed954
deliver
eichhorl Jul 10, 2026
47483d5
merge
eichhorl Jul 13, 2026
e57d9c4
comment
eichhorl Jul 13, 2026
e375240
limit
eichhorl Jul 13, 2026
f56db0d
review
eichhorl Jul 13, 2026
9c9cb37
wording
eichhorl Jul 13, 2026
0f603b3
bound
eichhorl Jul 14, 2026
ea0c318
Merge branch 'eichhorl/switch-to-spent-receipts' of github.com:dfinit…
eichhorl Jul 14, 2026
a8f7edc
merge
eichhorl Jul 14, 2026
1d5e743
clean
eichhorl Jul 14, 2026
ce71533
tests
eichhorl Jul 14, 2026
b17c562
test
eichhorl Jul 14, 2026
dc0ace7
.
eichhorl Jul 23, 2026
2651fa6
Automatically updated Cargo*.lock
Jul 23, 2026
7de89c8
fix
eichhorl Jul 23, 2026
cf70f90
clean
eichhorl Jul 23, 2026
61f0ba9
Merge branch 'eichhorl/use-ctxt-size-and-schedule' of github.com:dfin…
eichhorl Jul 23, 2026
5df4c80
merge
eichhorl Jul 23, 2026
4eb7990
fix
eichhorl Jul 23, 2026
a9d52da
fix
eichhorl Jul 23, 2026
29d00e4
fix
eichhorl Jul 23, 2026
9387429
unify pricing
eichhorl Jul 23, 2026
1ad7149
Automatically updated Cargo*.lock
Jul 23, 2026
1777d91
clean
eichhorl Jul 23, 2026
f633f7e
Merge branch 'eichhorl/deliver-spent' of github.com:dfinity/ic into e…
eichhorl Jul 23, 2026
8a20297
clean
eichhorl Jul 23, 2026
923c26d
split
eichhorl Jul 24, 2026
4c808f8
fix
eichhorl Jul 24, 2026
9ed03f7
fix
eichhorl Jul 24, 2026
6e9912b
merge
eichhorl Jul 24, 2026
5bb7a9f
fix
eichhorl Jul 24, 2026
c517eeb
fix
eichhorl Jul 24, 2026
574d469
fix
eichhorl Jul 24, 2026
35d9d37
clean
eichhorl Jul 27, 2026
7274acb
fix
eichhorl Jul 27, 2026
2976cb2
fix
eichhorl Jul 27, 2026
cbc2d0a
fix
eichhorl Jul 27, 2026
e704162
merge
eichhorl Jul 27, 2026
d6642d5
review
eichhorl Jul 29, 2026
e59379b
Merge branch 'master' into eichhorl/deliver-spent
eichhorl Jul 29, 2026
1fbe761
record cycles refunded to deleted canisters as consumed
eichhorl Jul 29, 2026
289ec18
Revert "record cycles refunded to deleted canisters as consumed"
eichhorl Jul 29, 2026
3647bf6
review
eichhorl Jul 29, 2026
f786526
first try
eichhorl Jul 30, 2026
002521b
NominalCycles::new
eichhorl Jul 30, 2026
a9d1ffa
cover the consensus cost of non-flexible HTTP outcall responses
eichhorl Jul 30, 2026
0d08515
Revert "NominalCycles::new"
eichhorl Jul 30, 2026
b9bb0e9
report spend exceeding allowance
eichhorl Jul 30, 2026
49ff56d
Merge branch 'eichhorl/deliver-spent' into eichhorl/cover-consensus-c…
eichhorl Jul 30, 2026
26bb637
merge
eichhorl Jul 30, 2026
44a1cfe
metric
eichhorl Jul 31, 2026
d870775
comment
eichhorl Jul 31, 2026
e91ff4f
comment
eichhorl Jul 31, 2026
b71a461
fix
eichhorl Jul 31, 2026
665edde
merge
eichhorl Jul 31, 2026
aed98db
merge
eichhorl Jul 31, 2026
d10feb6
Merge branch 'eichhorl/cover-consensus-cost-non-flexible' into eichho…
eichhorl Jul 31, 2026
c9dd362
merge
eichhorl Aug 4, 2026
dfd893d
merge
eichhorl Aug 4, 2026
d71eaed
fix
eichhorl Aug 5, 2026
97b4a76
out of cycles
eichhorl Aug 5, 2026
6c24910
move the out-of-cycles error to its own branch
eichhorl Aug 5, 2026
6803a21
report a flexible HTTP outcall that can no longer pay for a response
eichhorl Aug 5, 2026
834654a
Automatically fixing code for linting and formatting issues
Aug 5, 2026
ec03aa5
Merge branch 'eichhorl/cover-consensus-cost' into eichhorl/out-of-cyc…
eichhorl Aug 5, 2026
743ecf0
merge
eichhorl Aug 6, 2026
1aac90a
revert
eichhorl Aug 6, 2026
9d2264d
clean
eichhorl Aug 7, 2026
97a5660
clean
eichhorl Aug 7, 2026
5082d31
fix
eichhorl Aug 7, 2026
afe7f1c
fix
eichhorl Aug 7, 2026
4404bf9
enforce claimed response size
eichhorl Aug 10, 2026
04e7e2d
impl
eichhorl Aug 10, 2026
86eff5d
review
eichhorl Aug 10, 2026
52c3bb5
fix
eichhorl Aug 10, 2026
2b7a6ff
Merge branch 'eichhorl/out-of-cycles-error' into eichhorl/enforce-cla…
eichhorl Aug 10, 2026
e36ae78
merge
eichhorl Aug 10, 2026
949904c
merge
eichhorl Aug 10, 2026
3faa347
review
eichhorl Aug 11, 2026
38b1d48
doc
eichhorl Aug 11, 2026
ce8152c
Merge branch 'eichhorl/enforce-claimed-response-size' into eichhorl/o…
eichhorl Aug 11, 2026
0b558e6
Merge branch 'master' into eichhorl/out-of-cycles-non-flexible
eichhorl Aug 11, 2026
863af01
review
eichhorl Aug 12, 2026
da972d6
fix
eichhorl Aug 12, 2026
112de44
impl
eichhorl Aug 12, 2026
9e46600
clean
eichhorl Aug 13, 2026
df59fb0
fix
eichhorl Aug 13, 2026
b91471c
rename
eichhorl Aug 13, 2026
954dc5d
merge
eichhorl Aug 13, 2026
72f6d41
review
eichhorl Aug 13, 2026
bb52daf
enforce timeout during validation
eichhorl Aug 17, 2026
ca587ff
extract already_refunded
eichhorl Aug 17, 2026
e6b71cd
comment
eichhorl Aug 17, 2026
be0f7be
also test flexible
eichhorl Aug 17, 2026
4e4ee26
comment
eichhorl Aug 17, 2026
eda9f49
get latest state once
eichhorl Aug 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions rs/consensus/src/consensus/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ pub(crate) struct FinalizerMetrics {
pub canister_http_timeouts_delivered: IntCounter,
pub canister_http_divergences_delivered: IntCounter,
pub canister_http_out_of_cycles_delivered: IntCounter,
pub canister_http_async_receipts_delivered: IntCounter,
pub canister_http_flexible_candid_failures: IntCounter,
pub canister_http_flexible_errors_delivered: IntCounter,
pub canister_http_payload_bytes_delivered: Histogram,
Expand Down Expand Up @@ -304,6 +305,10 @@ impl FinalizerMetrics {
"canister_http_out_of_cycles_delivered",
"Total number of canister http messages delivered as out of cycles",
),
canister_http_async_receipts_delivered: metrics_registry.int_counter(
"canister_http_async_receipts_delivered",
"Total number of canister http asynchronous receipts delivered",
),
canister_http_flexible_candid_failures: metrics_registry.int_counter(
"canister_http_flexible_candid_failures",
"Total number of flexible canister http responses skipped due to candid encoding/decoding failures",
Expand Down Expand Up @@ -371,6 +376,8 @@ impl FinalizerMetrics {
.inc_by(batch_stats.canister_http.divergence_responses as u64);
self.canister_http_out_of_cycles_delivered
.inc_by(batch_stats.canister_http.out_of_cycles as u64);
self.canister_http_async_receipts_delivered
.inc_by(batch_stats.canister_http.async_receipts as u64);

let flexible_ok_candid_failures = batch_stats
.canister_http
Expand Down
7 changes: 4 additions & 3 deletions rs/https_outcalls/consensus/benches/payload_validation.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
//! Benchmark for the validation of canister HTTP outcall
//! payloads.

use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;

use criterion::{BenchmarkId, Criterion, black_box, criterion_group, criterion_main};

use ic_consensus_mocks::{Dependencies, DependenciesBuilder};
use ic_crypto_temp_crypto::{NodeKeysToGenerate, TempCryptoComponent};
use ic_https_outcalls_consensus::payload_builder::CanisterHttpPayloadBuilderImpl;
use ic_https_outcalls_consensus::payload_builder::{CanisterHttpPayloadBuilderImpl, PastPayloads};
use ic_https_outcalls_pricing::fees::{flexible_initial_spent, non_flexible_initial_spent};
use ic_interfaces::crypto::BasicSigner;
use ic_interfaces_registry::RegistryClient;
Expand Down Expand Up @@ -148,7 +148,7 @@ fn bench_payload_verification(c: &mut Criterion) {
black_box(target.builder.validate_canister_http_payload_impl(
black_box(&target.payload),
black_box(&target.validation_context),
black_box(HashSet::new()),
black_box(PastPayloads::default()),
))
.expect("validation failed");
})
Expand Down Expand Up @@ -470,6 +470,7 @@ impl<'a> PayloadAssembler<'a> {
divergence_responses,
flexible_responses,
flexible_errors: vec![],
async_receipts: vec![],
};

assert!(
Expand Down
151 changes: 149 additions & 2 deletions rs/https_outcalls/consensus/src/gossip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,13 @@ impl<Pool: CanisterHttpPool> BouncerFactory<CanisterHttpResponseId, Pool>
&latest_state.get_ref().metadata.subnet_call_context_manager;
let known_request_ids: BTreeSet<_> = subnet_call_context_manger
.canister_http_request_contexts
.iter()
.map(|item| *item.0)
.keys()
.chain(
subnet_call_context_manger
.delivered_canister_http_request_contexts
.keys(),
)
.copied()
.collect();
let next_callback_id = subnet_call_context_manger.next_callback_id();
(known_request_ids, next_callback_id)
Expand Down Expand Up @@ -72,3 +77,145 @@ impl<Pool: CanisterHttpPool> BouncerFactory<CanisterHttpResponseId, Pool>
std::time::Duration::from_secs(3)
}
}

#[cfg(test)]
mod tests {
use super::*;
use ic_artifact_pool::canister_http_pool::CanisterHttpPoolImpl;
use ic_interfaces_state_manager::Labeled;
use ic_logger::replica_logger::no_op_logger;
use ic_metrics::MetricsRegistry;
use ic_registry_subnet_type::SubnetType;
use ic_replicated_state::metadata_state::subnet_call_context_manager::SubnetCallContext;
use ic_test_utilities::state_manager::RefMockStateManager;
use ic_test_utilities_types::{
ids::{node_test_id, subnet_test_id},
messages::RequestBuilder,
};
use ic_types::{
Height, NumberOfNodes, RegistryVersion, ReplicaVersion,
canister_http::{
CanisterHttpMethod, CanisterHttpPaymentReceipt, CanisterHttpRequestContext,
CanisterHttpResponseMetadata, CanisterHttpResponseReceipt, PricingVersion,
RefundStatus, Replication,
},
crypto::{BasicSig, BasicSigOf, CryptoHash, CryptoHashOf, Signed},
signature::BasicSignature,
time::UNIX_EPOCH,
};
use ic_types_cycles::CanisterCyclesCostSchedule;
use std::sync::Arc;

fn request_context() -> CanisterHttpRequestContext {
CanisterHttpRequestContext {
request: RequestBuilder::new().build(),
url: String::new(),
max_response_bytes: None,
headers: vec![],
body: None,
http_method: CanisterHttpMethod::GET,
transform: None,
time: UNIX_EPOCH,
replication: Replication::FullyReplicated,
pricing_version: PricingVersion::PayAsYouGo,
refund_status: RefundStatus::default(),
registry_version: RegistryVersion::from(1),
subnet_size: NumberOfNodes::from(13),
cost_schedule: CanisterCyclesCostSchedule::Normal,
}
}

fn share_id(callback_id: CallbackId) -> CanisterHttpResponseId {
Signed {
content: CanisterHttpResponseReceipt {
metadata: CanisterHttpResponseMetadata {
id: callback_id,
content_hash: CryptoHashOf::new(CryptoHash(vec![])),
content_size: 0,
is_reject: false,
replica_version: ReplicaVersion::default(),
},
payment_receipt: CanisterHttpPaymentReceipt::default(),
},
signature: BasicSignature {
signature: BasicSigOf::new(BasicSig(vec![])),
signer: node_test_id(0),
},
}
}

/// The `next_callback_id` of the state [`test_bouncer`] builds.
const NEXT_CALLBACK_ID: u64 = 3;

/// A bouncer over a state that has handed out callback ids 0, 1 and 2 — so
/// `next_callback_id` is [`NEXT_CALLBACK_ID`] — of which 0 is still awaiting a
/// response, 1 has been responded to, and 2 is gone for good.
fn test_bouncer() -> Bouncer<CanisterHttpResponseId> {
let mut state = ReplicatedState::new(subnet_test_id(0), SubnetType::Application);
let contexts = &mut state.metadata.subnet_call_context_manager;
// Advance `next_callback_id` to NEXT_CALLBACK_ID
for _ in 0..NEXT_CALLBACK_ID {
contexts.push_context(SubnetCallContext::CanisterHttpRequest(request_context()));
}
contexts.canister_http_request_contexts.clear();
contexts
.canister_http_request_contexts
.insert(CallbackId::new(0), request_context());
contexts
.delivered_canister_http_request_contexts
.insert(CallbackId::new(1), request_context());

let state_manager = Arc::new(RefMockStateManager::default());
state_manager
.get_mut()
.expect_get_latest_state()
.return_const(Labeled::new(Height::new(1), Arc::new(state)));

let gossip = CanisterHttpGossipImpl::new(state_manager);
let pool = CanisterHttpPoolImpl::new(MetricsRegistry::new(), no_op_logger());
gossip.new_bouncer(&pool)
}

#[test]
fn shares_of_delivered_contexts_are_wanted() {
let bouncer = test_bouncer();

assert_eq!(bouncer(&share_id(CallbackId::new(0))), BouncerValue::Wants);
assert_eq!(bouncer(&share_id(CallbackId::new(1))), BouncerValue::Wants);
// A request that is neither pending nor delivered is settled for good.
assert_eq!(
bouncer(&share_id(CallbackId::new(2))),
BouncerValue::Unwanted
);
}

#[test]
fn shares_of_upcoming_requests_are_wanted() {
let bouncer = test_bouncer();

// The very next id execution will hand out, ...
assert_eq!(
bouncer(&share_id(CallbackId::new(NEXT_CALLBACK_ID))),
BouncerValue::Wants
);
// ... and everything up to the far edge of the look-ahead window.
assert_eq!(
bouncer(&share_id(CallbackId::new(
NEXT_CALLBACK_ID + MAX_NUMBER_OF_REQUESTS_AHEAD
))),
BouncerValue::Wants
);
}

#[test]
fn shares_beyond_the_look_ahead_window_are_stashed() {
let bouncer = test_bouncer();

assert_eq!(
bouncer(&share_id(CallbackId::new(
NEXT_CALLBACK_ID + MAX_NUMBER_OF_REQUESTS_AHEAD + 1
))),
BouncerValue::MaybeWantsLater
);
}
}
Loading
Loading