feat: Introduce OutOfCycles error for flexible outcalls - #11040
feat: Introduce OutOfCycles error for flexible outcalls#11040eichhorl wants to merge 73 commits into
OutOfCycles error for flexible outcalls#11040Conversation
…y/ic into eichhorl/switch-to-spent-receipts
…ity/ic into eichhorl/use-ctxt-size-and-schedule
…ichhorl/deliver-spent
…rl/cover-consensus-cost
An unfundable flexible outcall now stays pending until it times out, rather than being reported to the caller as `OutOfCycles`. The error, the lower bound on the consensus cost that decides it, and their tests move to eichhorl/out-of-cycles-error, which targets this branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A flexible outcall whose committee has spent too much of its per-replica allowances to cover the consensus cost of delivering any response used to sit pending until it timed out. It is now reported to the caller as `FlexibleHttpGlobalError::OutOfCycles` as soon as that is provable. The proof is the signed receipts seen so far: what is left of the committee's collective allowance is at most their unspent allowances plus a full allowance for every member not seen yet, and delivering a response costs at least `min_flexible_consensus_cost` of the same receipts. A share arriving later can only lower the former and raise the latter, so the verdict never flips back. Omitting a receipt does not help a proposer either, since an unreported replica is credited a full allowance. `min_flexible_consensus_cost` bounds the two results that deliver bodies — a group of `min_responses` successful responses, or the rejects proving a `TooManyRejects` — by their cheapest form, counting only bodies that must come out of the receipts seen at their actual size, and skipping a result that is no longer reachable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5eb2e2c to
5082d31
Compare
There was a problem hiding this comment.
Pull request overview
Adds early OutOfCycles reporting for flexible HTTP outcalls based on signed spending receipts and minimum delivery cost.
Changes:
- Adds protobuf and internal error representations.
- Computes and validates exhaustion proofs.
- Adds pricing and consensus tests.
- Includes an unrelated, inactive delegation module.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
rs/types/types/src/batch/canister_http.rs |
Adds serialization and accounting for OutOfCycles. |
rs/protobuf/src/gen/types/types.v1.rs |
Adds generated protobuf types. |
rs/protobuf/def/types/v1/canister_http.proto |
Defines the new wire-format variant. |
rs/interfaces/src/canister_http.rs |
Adds validation failure reasons. |
rs/https_outcalls/pricing/src/fees.rs |
Computes minimum deliverable-response cost. |
rs/https_outcalls/consensus/src/payload_builder/utils.rs |
Detects and constructs exhaustion proofs. |
rs/https_outcalls/consensus/src/payload_builder/tests.rs |
Tests construction and validation behavior. |
rs/https_outcalls/consensus/src/payload_builder.rs |
Validates, accounts for, and returns the error. |
rs/canonical_state/src/delegation.rs |
Adds an unrelated, undeclared delegation module. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
661f084 to
afe7f1c
Compare
OutOfCycles error for flexible outcalls
|
✅ No security or compliance issues detected. Reviewed everything up to afe7f1c. Security Overview
Detected Code Changes
|
| } | ||
| } | ||
|
|
||
| pub(crate) struct OutOfCyclesProof { |
There was a problem hiding this comment.
Maybe add a comment that explains that the "proof" is that unspent_allowance is smaller than min_cost?
| let mut unspent_allowance = Cycles::zero(); | ||
| let mut seen = 0; | ||
| for share in seen_shares { | ||
| unspent_allowance += allowance - share.content.spent(); | ||
| seen += 1; | ||
| } | ||
| unspent_allowance += allowance * committee_size.saturating_sub(seen); |
There was a problem hiding this comment.
This could be simplified with something like
let unspent_allowance = allowance * committee_size - seen_shares.iter().map(|share| share.content.spent()).sum();| subnet_size: NumberOfNodes, | ||
| committee_size: usize, | ||
| min_responses: u32, | ||
| ) -> Option<Cycles> { |
There was a problem hiding this comment.
Can this function ever return None?
| /// The total amount of cycles spent by the subnet to produce this response. | ||
| initial_spent: Cycles, | ||
| }, | ||
| OutOfCycles { |
There was a problem hiding this comment.
Who is paying for the block space used by this new error message if the canister has not attached enough cycles?
| // A committee of 4 that has all responded successfully, but needs 10 | ||
| // responses: neither the 10 successes nor a reject can still turn up, so |
There was a problem hiding this comment.
Is this scenario realistic?
| callback_id, | ||
| context, | ||
| ) | ||
| .map_err(CanisterHttpPayloadValidationError::InvalidArtifact)?; |
There was a problem hiding this comment.
Maybe could we add some debug_asserts that in the Err case, .min_cost <= .unspent_allowance and in the Ok case, .min_cost > .unspent_allowance?
| reject_share(0, 50, 0), | ||
| reject_share(1, 50, 0), | ||
| reject_share(2, 50, 0), |
There was a problem hiding this comment.
Could be interesting to have different content sizes to make sure we select the 2 smallest responses
A flexible outcall whose committee has spent too much of its per-replica allowances to cover the consensus cost of delivering any response used to sit pending until it timed out. It is now reported to the caller as
FlexibleHttpGlobalError::OutOfCyclesas soon as that is provable.The proof is the signed receipts seen so far: what is left of the committee's collective allowance is at most their unspent allowances plus a full allowance for every member not seen yet, and delivering a response costs at least
min_flexible_consensus_costof the same receipts.For an
okresponse, at leastmin_responsesok shares need to be included, whereas for a reject response at leasttotal_requests - min_responses + 1reject shares must be included. This determines how many "unseen" shares we need to consider in order to build the out of cycles proof.