Skip to content

refactor: Introduce new type CompoundCycles#9520

Merged
michael-weigelt merged 14 commits intodfinity:masterfrom
dsarlis:dimitris/separate-nominal-cycles
Mar 26, 2026
Merged

refactor: Introduce new type CompoundCycles#9520
michael-weigelt merged 14 commits intodfinity:masterfrom
dsarlis:dimitris/separate-nominal-cycles

Conversation

@dsarlis
Copy link
Copy Markdown
Contributor

@dsarlis dsarlis commented Mar 20, 2026

This PR is an alternative to the original approach in #9063.

The original PR, while achieving the goal of having cycles for metrics be always updated irrespective of the subnet's cycles cost schedule, it achieved so by pushing the decision making to all callers that need to deal with cycles charges or metric updates. Specifically, the APIs in the CyclesAccountManager were changed to not require a cost_schedule parameter, so each caller would then need to have some logic like:

match cost_schedule {
   CanisterCyclesCostSchedule::Free => { ... },
   CanisterCyclesCostSchedule::Normal => { ... },
}

This was not ideal as the decision points were spread across the codebase making it also easier for mistakes to be introduced if new use cases were added as every caller would need to have all the knowledge of how to handle things.

The current PR proposes another approach which attempts to encapsulate better the logic around calculating the right amounts for cycles balance and metrics updates. A new type called CompoundCycles is introduced that given an original amount of Cycles, the use case and cost schedule, calculates the "real" and "nominal" amounts that can be used to update the cycles balance and metrics respectively.

This is an improvement over the original approach as now the callers would have access to a CompoundCycles object and when they need to update Cycles amounts in the ReplicatedState they can use compound_cycles.real() while they can use compound_cycles.nominal() for NominalCycles amounts. This way the type system helps guide them to use the right amounts without having to do calculations themselves.

However, making the new type capture more information than before introduces a new risk, namely that one could mix amounts for different use cases or even cost schedule. Imagine the following update in one of the CyclesAccountManager APIs:

pub fn prepay_execution_cycles(instructions: NumInstructions, ...) -> Result<CompoundCycles, CanisterOutOfCyclesError>;

pub fn refund_unused_execution_cycles(instructions: NumInstructions, prepaid_execution_cycles: CompoundCycles, ...);

in the above there's no guarantee that the user of the API does not use CompoundCycles for Memory instead of Instructions that they should use and pass an incorrect amount in refund_unused_execution_cycles without noticing. Additionally, in order to calculate the refund we should implement arithmetic operations on CompoundCycles but it also doesn't make sense to allow adding up compound cycles for different use cases.

One solution to the above issue would be to make the operations fallible but then there would have to be a lot of error handling sprinkled all over the codebase. The PR takes another path which is to introduce generic arguments for CompoundCycles to capture the combinations of use cases. This ensures that operations can only happen between "apples" and "apples" and it also provides a nice way to clarify even more the APIs in the CyclesAccountManager like the following:

pub fn prepay_execution_cycles(instructions: NumInstructions, ...) -> Result<CompoundCycles<Instructions>, CanisterOutOfCyclesError>;

pub fn refund_unused_execution_cycles(instructions: NumInstructions, prepaid_execution_cycles: CompoundCycles<Instructions>, ...);

The changes included can be summarized as follows:

  1. The new type is introduced along with its own arithmetic.
  2. Empty structs that act as tags are added for each use case variant to lift them up to the type level and be able to implement a trait for them to bound the generic argument used in the new type.
  3. The APIs in the CyclesAccountManager and ReplicatedState are transformed to require or return the new type when they would need to deal with both the balance and metrics updates eventually. APIs that are only concerned with the balance of the canister or only with metrics are still using the existing Cycles and NominalCycles types.
  4. A lot of boilerplate changes follow after the above, with a big chunk of them being test updates.

Note that the current PR performs only a refactoring to introduce the new type and update APIs but is still using the same values for "real" and "nominal" parts of CompoundCycles. If the approach is agreed, then a follow up to perform the functional changes would be rather small -- a trivial change in the constructor of CompoundCycles as well as some test updates to fix assertions (and/or maybe adding some more tests as I don't think all cases are covered).

@dsarlis dsarlis changed the title refactor: New type CompoundCycles to decouple real from nominal cycles updates refactor: Introduce new type CompoundCycles Mar 20, 2026
@dsarlis dsarlis marked this pull request as ready for review March 25, 2026 08:31
@dsarlis dsarlis requested review from a team as code owners March 25, 2026 08:31
@dsarlis dsarlis changed the title refactor: Introduce new type CompoundCycles feat: Introduce new type CompoundCycles Mar 25, 2026
@dsarlis dsarlis changed the title feat: Introduce new type CompoundCycles refactor: Introduce new type CompoundCycles Mar 25, 2026
@dsarlis
Copy link
Copy Markdown
Contributor Author

dsarlis commented Mar 25, 2026

I noticed two more cases where we need to update APIs to follow the new approach. Will upload a fix shortly.

EDIT: Only one actually in the end.

@dsarlis dsarlis marked this pull request as draft March 25, 2026 12:25
@dsarlis
Copy link
Copy Markdown
Contributor Author

dsarlis commented Mar 26, 2026

@schneiderstefan @michael-weigelt @eichhorl Please re-approve when you can.

@github-actions
Copy link
Copy Markdown
Contributor

@cgundy
Copy link
Copy Markdown
Contributor

cgundy commented Mar 26, 2026

@dsarlis yeah sorry, I think that was just bad timing. When I rebased the PR I didn't realize I was pulling in this change too https://github.com/dfinity/ic/pull/9452/changes which changed the clippy command.

@cgundy
Copy link
Copy Markdown
Contributor

cgundy commented Mar 26, 2026

Hmm okay sorry @dsarlis looks like there is still one more error I didn't see before. It's in //rs/types/cycles:cycles_doc_test - I'll upload the logs.
rs:types:cycles:cycles_doc_test.rustdoc_test.sh system-out.txt

@github-actions github-actions bot dismissed stale reviews from schneiderstefan and michael-weigelt March 26, 2026 11:05

Review dismissed by automation script.

@dsarlis
Copy link
Copy Markdown
Contributor Author

dsarlis commented Mar 26, 2026

Hmm okay sorry @dsarlis looks like there is still one more error I didn't see before. It's in //rs/types/cycles:cycles_doc_test - I'll upload the logs. rs:types:cycles:cycles_doc_test.rustdoc_test.sh system-out.txt

Yeah that was my miss. Some doc tests needed to be updated after the last couple of commits here. I pushed a fix just now.

@dsarlis
Copy link
Copy Markdown
Contributor Author

dsarlis commented Mar 26, 2026

Btw @cgundy I don't know if it's just me but it feels things are awfully slow for me to run locally, after merging master earlier this morning. CI also seems to have taken 1+ hour to tell me that some doc test failed in rs/types/cycles. That seems like a regression from anecdotal data I have in memory.

@github-actions
Copy link
Copy Markdown
Contributor

@cgundy cgundy enabled auto-merge March 26, 2026 12:44
@cgundy cgundy added this pull request to the merge queue Mar 26, 2026
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Mar 26, 2026
@michael-weigelt michael-weigelt added this pull request to the merge queue Mar 26, 2026
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Mar 26, 2026
@michael-weigelt michael-weigelt added this pull request to the merge queue Mar 26, 2026
Merged via the queue into dfinity:master with commit ebc0927 Mar 26, 2026
18 of 19 checks passed
@dsarlis dsarlis deleted the dimitris/separate-nominal-cycles branch March 27, 2026 07:56
github-merge-queue bot pushed a commit that referenced this pull request Mar 30, 2026
This is a follow up to #9520 where `CompoundCycles` were introduced. In
the system API there can only be 3 cases of consumed cycles,
specifically `BurnedCycles`, `Instructions` and
`RequestAndResponseTransmission`. Currently, there's an awkward `panic`
to deal with the possibility that the current `BTreeMap` where consumed
cycles are stored cannot contain other variants. While this is easy to
verify that is the case now (code inspection + type imports), it is
unideal as it's error prone and we can do better with the stricter types
introduced via `CompoundCycles`.

The solution is to define a struct which captures exactly the possible
cases that can be produced via the system API calls and use that
throughout. There's some little extra boilerplate but this ensures that
no unexpected cases can happen and removes the need to handle such
cases. An alternative would have been to add a check in
`validate_cycle_change` to raise a critical error instead but it's
better if we can eliminate the possibility of errors altogether.
github-merge-queue bot pushed a commit that referenced this pull request Apr 2, 2026
… callbacks (#9676)

This PR follows up on #9520 and expands the use of `CompoundCycles` to a
few more types and APIs, primarily related to DTS executions and
callback executions.

For various DTS executions, there is a need to store the prepayment made
in the original context, something that is useful to resume a paused
execution or when an aborted one will need to be restarted. Storing just
a `Cycles` amount in these cases would be incorrect in the future when
the `Cycles` and `NominalCycles` amounts can be different. Switching to
`CompoundCycles` makes it so that no information loss happens when that
switch is implemented. Similarly for callback executions the prepayment
amounts need to be stored so that refunds can be applied correctly when
the response is processed.

One particular point of interest is the transformation in the respective
protobuf files where a new field is added. The new field of type
`CompoundCycles` will eventually replace the existing `Cycles` one. To
populate the new field on the first restart, we will use the `Cycles`
amount that's stored in the checkpoint now. Technically, a
`CanisterCyclesCostSchedule` would be needed but to avoid unnecessary
piping of this information (needs to be propagated all the way from
`StateManager` down to `CanisterStateBits`) we take a shortcut and
hardcode `Normal` schedule. This should not have an effect anyway as for
subnets with `Free` schedule now the `Cycles` and `NominalCycles` should
match so using a 0 amount for both is fine (obviously for subnets with
`Normal` schedule this is fine as well).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external-contributor refactor security-review-passed IDX or InfraSec have concluded it's safe to run CI on the external PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants