Skip to content

feat: Introduce OutOfCycles error for flexible outcalls - #11040

Open
eichhorl wants to merge 73 commits into
masterfrom
eichhorl/out-of-cycles-error
Open

feat: Introduce OutOfCycles error for flexible outcalls#11040
eichhorl wants to merge 73 commits into
masterfrom
eichhorl/out-of-cycles-error

Conversation

@eichhorl

@eichhorl eichhorl commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

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.

For an ok response, at least min_responses ok shares need to be included, whereas for a reject response at least total_requests - min_responses + 1 reject shares must be included. This determines how many "unseen" shares we need to consider in order to build the out of cycles proof.

eichhorl and others added 10 commits July 31, 2026 12:05
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>
@github-actions github-actions Bot added the feat label Aug 5, 2026
Base automatically changed from eichhorl/cover-consensus-cost to master August 6, 2026 12:50
@eichhorl
eichhorl force-pushed the eichhorl/out-of-cycles-error branch from 5eb2e2c to 5082d31 Compare August 7, 2026 12:16
@eichhorl
eichhorl requested a balanced review from Copilot August 7, 2026 12:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread rs/https_outcalls/consensus/src/payload_builder/utils.rs
Comment thread rs/https_outcalls/consensus/src/payload_builder/utils.rs
Comment thread rs/canonical_state/src/delegation.rs Outdated
@eichhorl
eichhorl force-pushed the eichhorl/out-of-cycles-error branch from 661f084 to afe7f1c Compare August 7, 2026 12:24
@eichhorl eichhorl changed the title feat: OutOfCycles flexible error feat: Introduce OutOfCycles error for flexible outcalls Aug 7, 2026
@eichhorl
eichhorl marked this pull request as ready for review August 7, 2026 12:30
@eichhorl
eichhorl requested a review from a team as a code owner August 7, 2026 12:30
@zeropath-ai

zeropath-ai Bot commented Aug 7, 2026

Copy link
Copy Markdown

No security or compliance issues detected. Reviewed everything up to afe7f1c.

Security Overview
Detected Code Changes
Change Type Relevant files
Enhancement ► rs/https_outcalls/consensus/src/payload_builder.rs
    Implement OutOfCycles handling and integrate Cycles import
► rs/https_outcalls/consensus/src/payload_builder.rs
    Extend error handling to include OutOfCycles in flexible responses
► rs/https_outcalls/consensus/src/payload_builder/tests.rs
    Add tests for OutOfCycles behavior and validation scenarios
► rs/https_outcalls/consensus/src/payload_builder/utils.rs
    Add OutOfCyclesProof struct and check_out_of_cycles function
► rs/https_outcalls/pricing/src/fees.rs
    Add min_flexible_consensus_cost function and supporting logic
► rs/interfaces/src/canister_http.rs
    Add FlexibleOutOfCycles variants to InvalidCanisterHttpPayloadReason enum
► rs/protobuf/def/types/v1/canister_http.proto
    Define FlexibleCanisterHttpOutOfCycles message and wire into errorDetails
► rs/protobuf/src/gen/types/types.v1.rs
    Add FlexibleCanisterHttpOutOfCycles message struct and fields
► rs/types/types/src/batch/canister_http.rs
    Extend FlexibleCanisterHttpError enum with OutOfCycles variant and associated fields
► rs/types/types/src/batch/canister_http.rs
    Update count_bytes and conversion logic to support OutOfCycles

}
}

pub(crate) struct OutOfCyclesProof {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a comment that explains that the "proof" is that unspent_allowance is smaller than min_cost?

Comment on lines +215 to +221
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this function ever return None?

/// The total amount of cycles spent by the subnet to produce this response.
initial_spent: Cycles,
},
OutOfCycles {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who is paying for the block space used by this new error message if the canister has not attached enough cycles?

Comment on lines +509 to +510
// 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this scenario realistic?

callback_id,
context,
)
.map_err(CanisterHttpPayloadValidationError::InvalidArtifact)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment on lines +497 to +499
reject_share(0, 50, 0),
reject_share(1, 50, 0),
reject_share(2, 50, 0),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be interesting to have different content sizes to make sure we select the 2 smallest responses

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants