Introducing the cycles-manager library

Hello IC Developer Community :wave:

According to developers, managing cycles is a major pain point :face_with_thermometer:

Luckily, at CycleOps, we spend all our waking cycles thinking about one thing…cycles :stuck_out_tongue_winking_eye:

That’s why our team is proud to open-source a new cycles-manager library, providing a simplified, permissioned cycles management framework for Internet Computer applications.

Motivation

Multi-canister applications tend to have the following architecture, with an index canister that holds the mapping and/or manages several child canisters. Currently, there is no library that developers can use to easily manage the cycles within a multi-canister architecture.

The cycles-manager library proposes a simple, flexible pattern for cycles management, with a “Battery” canister that implements the CyclesManager module and APIs, and a “Child” canister that implements the CyclesRequester module.

cycles_manager_arch

Concepts

The Cycles Manager

The Cycles Manager is placed in the index/battery canister and is responsible for transfering cycles to child canisters that request cycles and are permitted to do so by the cycles manager. It does this by:

  • Maintaining a list of all allowed child canisters, and ensuring child canisters do not request more than their alloted cycles quotas.
  • Tracking the aggregate cycles apportioned to all canisters, ensuring that the battery/index canister does not transfer more cycles than alloted by the aggregate quota

You can initialize the CyclesManager by calling CyclesManager.init() with the appropriate settings.`

  CyclesManager.init({
    // Make a default setting using a rate quota, such that
    // each child canister can request a maximum of 1T cycles every 24 hours
    defaultCyclesSettings = {
      quota = #rate({
        maxAmount = 1_000_000_000_000;
        durationInSeconds = 60 * 60 * 24;
      });
    };
    // Make an aggregate cycles quota (applies to all cycles transfer requests from child canisters)
    // All child canisters can request a maximum of 25T cycles every 24 hours
    aggregateSettings = {
      quota = #rate({
        maxAmount = 25_000_000_000_000;
        durationInSeconds = 60 * 60 * 24;
      });
    };
    // A child canister must request at least 50 Billion cycles per topup request
    minCyclesPerTopup = ?50_000_000_000;
  });

Adding a child canister

Adding a child canister to the Cycles Manager allows it to request cycles from the canister implementing that CyclesManager. You can add a child canister with

CyclesManager.addChildCanister(
  cyclesManager,
  <child canister principal>,
  {
    // Add a specific rate quota for this canister that 
    // allows it to burn through **2T cycles** every **365 days**
    //
    // Adding a specific quota for a canister will override the default canister cycles quota
    quota = ?#rate({
      maxAmount = 2_000_000_000_000;
      durationInSeconds = 60 * 60 * 24 * 365;
    });
  }
);

Default Settings

Default settings provide a default cycles quota setting for all canisters added to the cycles manager. Unless a specific quota is specified for a canister in the child canister map, any added canister will fall back to use the default setting.

You can update the CyclesManager’s default cycles quota by calling

CyclesManager.setDefaultCanisterCyclesQuota(
  cyclesManager,
  // update to have the default canister cycles quota be 2T every 24 hours
  #rate({
    maxAmount = 2_000_000_000_000;
    durationInSeconds = 60 * 60 * 24;
  })
);

Aggregate Settings

Aggregate settings provide a security blanket for the index/battery canister in which the CyclesManager library is used. This allows the developer to specify a limit on how many cycles can be disbursed from the CyclesManger over a desired time period.

You can update the CyclesManager’s aggregate cycles quota by calling

CyclesManager.setAggregateCyclesQuota(
  cyclesManager,
  // update to allow 20T cycles transferred in aggregate every 24 hours
  #rate({
    maxAmount = 20_000_000_000_000;
    durationInSeconds = 60 * 60 * 24;
  });
);

Min Cycles Per Topup

You have the ability to specify a minimum cycles requested per each canister topup request. This helps prevent a rogue canister from attacking your battery canister by repeatedly issuing requests for a low amount of cycles (i.e. 10 cycles). Any topup requests made that have an amount lower than this minmum amount are rejected. It is recommended to set this limit to at least 50 billion cycles.

You can update the CyclesManager’s minCyclesPerTopup property by calling

CyclesManager.setMinCyclesPerTopup(cyclesManager, 50_000_000_000);

Interface

Any canister implementing the CyclesManager must implement the following interface in order for child canisters to be able to request cycles

  public type Interface = actor {
    // prefixed so that it won't conflict with any existing APIs on the implementing canister
    cycles_manager_transferCycles: shared (Nat) -> async TransferCyclesResult;
  };

The Cycles Requester

The cycles requester allows a child canister to easily request cycles from a battery canister implementing the CyclesManager Interface (shown in the Interface section of this document).

In order to do this, a child canister must do the following:

  1. The specific child canister must have been added to the battery canister’s cycles manager via CyclesManager.addChildCanister().
  2. Initialize the CyclesRequester in the child canister
  // Example, if your battery canister id was "be2us-64aaa-aaaaa-qaabq-cai";
  let batteryCanisterPrincipal = Principal.fromText("be2us-64aaa-aaaaa-qaabq-cai");
  cyclesRequester := ?CyclesRequester.init({
    batteryCanisterPrincipal;
    topupRule = {
      // Request 200 Billion cycles from the battery canister anytime when this canister is below 1T cycles
      threshold = 1_000_000_000_000;
      method = #by_amount(200_000_000_000);
    };
  });
  1. Add the requestTopupIfBelowThreshold to any update methods of your child canister.
public func yourUpdateAPI(): async () {
  // before doing something, check if we need to request cycles
  // most of the time, this will not run (only when cycles are needed!)
  let _ = await* requestTopupIfLow();

  // the rest of your function update logic here ...
};

Using the cycles-manager with production applications via CycleOps

The cycles manager works great for managing the cycles of multi-canister applications, but at the end of the day you still need a tool for monitoring and managing the cycles of the Battery canister(s) for your application. This is where using the CycleOps service gives you monitoring, accounting, and reliability with trustless, automated cycles management. Want to learn more about CycleOps? Keep reading here.

Credit

Thanks to the DFINITY grants team for sponsoring the development of this library.

16 Likes

At PokedStudio we use CycleOps to monitor the cycles of our in-house Cycles Management canister. It has been a huge help! I look forward to replacing our current canister with the Cycles Manager in the future.

Cc: @jonit

5 Likes