Skip to content

💡 Explainer and demo for epochs on Celo

Notifications You must be signed in to change notification settings

celo-org/epochs

Repository files navigation

Epoch distributions on Celo

This repo contains an explainer for Celo-specific epoch transactions and a demo for how to fetch them given an epoch.

IMPORTANT This repo is for educational purposes only. The information provided here may be inaccurate. Please don’t rely on it exclusively to implement low-level client libraries.

Usage

Requirement(s):

  • Node.js v18.14.2

Running demo scripts:

yarn
yarn ts-node <file_name.ts> # e.g. yarn ts-node totalVoterRewards.ts

Context

Epochs

On Celo, an "epoch" is a period of time during which a set of validators are elected to produce blocks.

$1 \text{ epoch} = 17280 \text{ blocks} = 1 \text{ day }$

Every epoch is exactly 17,280 blocks long or ~1 day, because $17280 \text{ blocks per epoch} \times 5 \text{ seconds per block} = 86400 \text{ seconds per epoch } = 1 \text{ day}$.

Epoch blocks

Every epoch, has an "epoch block", which is the last block of the epoch and contains:

  • "normal transactions" found in all blocks, and
  • special "epoch transactions", which are Celo-specific transactions described below.

You can (predictably) calculate an epoch block number as follows:

const BLOCKS_PER_EPOCH = 17280; // defined at blockchain-level
const epochNumber = 1296; // <-- your choice
const epochBlockNumber = epochNumber * BLOCKS_PER_EPOCH; // 22,394,880

Epoch transactions

As described, every "epoch block" contains special Celo-specific transactions known as "epoch transactions".

You can distinguish epoch transactions from normal transactions because epoch transactions set the transaction hash equal to the block hash, whereas normal transactions do not:

  • normalTx.transactionHash != normalTx.blockHash
  • epochTx.transactionHash == epochTx.blockHash

NOTE An epoch block contains both "normal" transactions and epoch transactions.

You can get epoch logs by fetching the logs of the entire epoch block (using the block hash or block number), and filtering transactions whose block hash is equal to the transaction hash.

For example, epoch 1,307 will be included in block 22,584,960 ($= 1307 \times 17280$) and can be fetched using the block number:

 $ curl https://forno.celo.org \
  -X POST \
  -H "Content-Type: application/json" \
  --data '{"method":"eth_getLogs","params":[{"fromBlock": "0x1589e80", "toBlock": "0x1589e80"}],"id":1,"jsonrpc":"2.0"}'

where "0x1589e80" is "22584960" in decimal, or using the block hash:

$ curl https://forno.celo.org \
  -X POST \
  -H "Content-Type: application/json" \
  --data '{"method":"eth_getLogs","params":[{"blockHash": "0xdd7a9b02f109f41e3ce710cb10ecca4a0f07e49f0f3d62e8c23d7792d6b1ca30"}],"id":1,"jsonrpc":"2.0"}'

For a TypeScript example, see rawEpochLogs.ts:

// fetches epoch block hash
const { hash } = await publicClient.getBlock({
    blockNumber: getEpochBlockNumber(epochNumber),
});

// fetches block transactions
const epochTransactions = await publicClient.getLogs({
    blockHash: hash,
});

// filters out transactions that are not epoch logs
const decodedEvents = epochTransactions.filter((tx) => tx.transactionHash == tx.blockHash);
// ...

The example output is shown below and in ./output/epoch1307.json:

$ yarn ts-node rawEpochLogs.ts

Summary: {
  'Epoch number': 1307n,
  'Epoch transactions': 447,
  'Distinct events': 6
}

Contract: EpochRewards
 Event: "TargetVotingYieldUpdated"
 Count: 1 events


Contract: Validators
 Event: "ValidatorScoreUpdated"
 Count: 110 events


Contract: Celo Dollar (cUSD)
 Event: "Transfer"
 Count: 148 events


Contract: Validators
 Event: "ValidatorEpochPaymentDistributed"
 Count: 110 events


Contract: Celo native asset (CELO)
 Event: "Transfer"
 Count: 4 events


Contract: Election
 Event: "EpochRewardsDistributedToVoters"
 Count: 65 events


For detailed logs, see: ./output/epoch1307.json
✨  Done in 1.80s.

More details

At a high-level, epoch transactions can be grouped as follows:

  1. Validator and validator group rewards
  2. Voter rewards
  3. Community fund distributions
  4. Carbon offset distributions
  5. Mento reserve distributions (deprecated, since block 21616000)

Their purpose and how to fetch their logs is described in more detail below.

Validator rewards

Validators are rewarded for producing blocks and, every epoch, the Celo blockchain distributes these rewards to them in cUSD. Of those rewards, a part goes to the group they are part of in the form of a "commission". You can learn more about validator groups and why they exist.

The relevant event in the Validators.sol smart contract is:

event ValidatorEpochPaymentDistributed(
  address indexed validator,
  uint256 validatorPayment,
  address indexed group,
  uint256 groupPayment
);

It shows the validator and group addresses, and the amount of cUSD distributed to each.

For an example, see validatorRewards.ts which is a simple script that fetches and calculates total validator and validator group rewards for a given epoch.

Example output:

$ yarn ts-node validatorRewards.ts

Total validator rewards: 4271.18813692596407949
Total validator group rewards: 6426.74597902813248148
For detailed logs, see: evt_ValidatorEpochPaymentDistributed.json
✨  Done in 1.70s.

Voter rewards (or "staking rewards")

Total voter rewards

For a given epoch, total voter rewards can be calculated by summing the rewards distributed to every validator group. This is because the Celo blockchain distributes rewards to validator groups, which then distribute rewards to their voters.

The relevant event in the Election.sol smart contract is:

event EpochRewardsDistributedToVoters(address indexed group, uint256 value);

For an example, see totalVoterRewards.ts which is a simple script that fetches and calculates total voter rewards for a given epoch.

To ensure the script works as expected, you can compare the output with the total voter rewards displayed on the Celo block explorer, for example in epoch 1,302.

Example output:

$ yarn ts-node totalVoterRewards.ts

Total voter rewards: 27347.542717542173439382
✨  Done in 1.26s.

Individual voter rewards

Unfortunately, there is no simple way to fetch individual voter rewards using event logs alone. That's because voter rewards are distributed to individuals implicitly (without logs), and instead distributed to the group they vote for explicitly (with EpochRewardsDistributedToVoters event).

The mental model is that:

  • individual voters vote for a validator group
  • if elected, the validator group produces blocks
  • all voter rewards are distributed at the validator group level (with explicit EpochRewardsDistributedToVoters events)
  • voters implicitly receive voter rewards, because the number of voting CELO at the group level increased (by the rewards), but their voting share of the group hasn't changed
  • when voters decide to withdraw their CELO, they receive a number of CELO equal to their voting share of the group, which includes the rewards distributed to the group.

Advantage: voting CELO automatically compound without user intervention.

Disadvantage: individual voter rewards cannot be fetched using event logs alone (in the current implementation)

Instead, individual voter rewards can be calculated by multiplying the voter's voting share of the group by the rewards distributed to the group. That means, given an epoch, individual voter rewards can only be calculated with knowledge of the voter's voting share of the group.

The voter's voting share of the group can be calculated as $\text{voting share} = \frac{\text{individual's votes}}{\text{total group votes}}$. That means a voter's voting share can change both because the individual's votes changed and because the total group votes changed.

We can use the following events from Election.sol to calculate a voter's votes and the total group votes over time:

event ValidatorGroupVoteActivated(
  address indexed account,
  address indexed group,
  uint256 value,
  uint256 units
);
event ValidatorGroupActiveVoteRevoked(
  address indexed account,
  address indexed group,
  uint256 value,
  uint256 units
);

Given an epoch, all activate votes (that were not revoked during the epoch) are eligible for rewards. The simplest way to identify eligible voters is to count active votes at the end of an epoch, that means at the epoch block.

Using logs alone, active votes can only be calculated by fetching activation (ValidatorGroupVoteActivated) and revocation (ValidatorGroupActiveVoteRevoked) events from genesis to the epoch of interest.

NOTE: Writing a script to calculate active votes is non-trivial. This explainer does not show how to do that, but might use an indexed data provider like dune.com to provide a demo at a later date.

Community fund distributions

The Celo blockchain makes distributions to the community fund every epoch.

For an example, see communityFundDistributions.ts which is a simple script that fetches and calculates community fund distributions for a given epoch.

To ensure the script works as expected, you can compare the output with the community fund distributions displayed on the Celo block explorer, for example in epoch 1,307.

$ yarn ts-node communityFundDistributions.ts

Summary: {
  epoch: 1307n,
  name: 'Community Fund Distribution',
  value: '16918.363034787685412848 CELO',
  to: '0xd533ca259b330c7a88f74e000a3faea2d63b7972'
}

Detail(s): [
  {
    address: '0x471ece3750da237f93b8e339c536989b8978a438',
    topics: [
      '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
      '0x0000000000000000000000000000000000000000000000000000000000000000',
      '0x000000000000000000000000d533ca259b330c7a88f74e000a3faea2d63b7972'
    ],
    data: '0x0000000000000000000000000000000000000000000003952573c6cf73b12ff0',
    blockNumber: 22584960n,
    transactionHash: '0xdd7a9b02f109f41e3ce710cb10ecca4a0f07e49f0f3d62e8c23d7792d6b1ca30',
    transactionIndex: 7,
    blockHash: '0xdd7a9b02f109f41e3ce710cb10ecca4a0f07e49f0f3d62e8c23d7792d6b1ca30',
    logIndex: 379,
    removed: false,
    args: {
      from: '0x0000000000000000000000000000000000000000',
      to: '0xD533Ca259b330c7A88f74E000a3FaEa2d63B7972',
      value: 16918363034787685412848n
    },
    eventName: 'Transfer'
  }
]

✨  Done in 1.60s.

Carbon offset distributions

The Celo blockchain makes distributions to the carbon offset fund every epoch.

For an example, see carbonOffsetDistributions.ts which is a simple script that fetches and calculates carbon offset distributions for a given epoch.

To ensure the script works as expected, you can compare the output with the carbon offset distributions displayed on the Celo block explorer, for example in epoch 1,307.

$ yarn ts-node carbonOffsetDistributions.ts

Summary: {
  epoch: 1307n,
  name: 'Carbon Offset Distribution',
  value: '67.673452139150741651 CELO',
  to: '0xCe10d577295d34782815919843a3a4ef70Dc33ce'
}

Detail(s): [
  {
    address: '0x471ece3750da237f93b8e339c536989b8978a438',
    topics: [
      '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
      '0x0000000000000000000000000000000000000000000000000000000000000000',
      '0x000000000000000000000000ce10d577295d34782815919843a3a4ef70dc33ce'
    ],
    data: '0x000000000000000000000000000000000000000000000003ab28662bd67a9093',
    blockNumber: 22584960n,
    transactionHash: '0xdd7a9b02f109f41e3ce710cb10ecca4a0f07e49f0f3d62e8c23d7792d6b1ca30',
    transactionIndex: 7,
    blockHash: '0xdd7a9b02f109f41e3ce710cb10ecca4a0f07e49f0f3d62e8c23d7792d6b1ca30',
    logIndex: 446,
    removed: false,
    args: {
      from: '0x0000000000000000000000000000000000000000',
      to: '0xCe10d577295d34782815919843a3a4ef70Dc33ce',
      value: 67673452139150741651n
    },
    eventName: 'Transfer'
  }
]

✨  Done in 1.61s.

Mento reserve distributions (⚠️ deprecated)

In the past, the Celo blockchain also made ad-hoc distributions to the Mento reserve whenever the reserve was "low". This is no longer the case since CIP-54: Community rewards go to reserve if undercollaterized was implemented in the Gingerbread hard fork on Sep 26, 2023, which removed the ad-hoc distributions.

But for completeness, the script reserveBolsterDistribution.ts can be used to fetch and calculate reserve bolster distributions for past epochs.

$ yarn ts-node reserveBolsterDistribution.ts

Summary: {
  epoch: 1234n,
  name: 'Reserve Bolster Distribution',
  value: '18451.770990182011272262 CELO',
  to: '0x9380fA34Fd9e4Fd14c06305fd7B6199089eD4eb9'
}

Detail(s): {
  address: '0x471ece3750da237f93b8e339c536989b8978a438',
  topics: [
    '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
    '0x0000000000000000000000000000000000000000000000000000000000000000',
    '0x0000000000000000000000009380fa34fd9e4fd14c06305fd7b6199089ed4eb9'
  ],
  data: '0x0000000000000000000000000000000000000000000003e845c331e5e0847c46',
  blockNumber: 21323520n,
  transactionHash: '0x11d6b078b68d16b7a5be7bdbb8dd3ca338fc5064fd59856f96a77fdfc03b9ece',
  transactionIndex: 7,
  blockHash: '0x11d6b078b68d16b7a5be7bdbb8dd3ca338fc5064fd59856f96a77fdfc03b9ece',
  logIndex: 383,
  removed: false,
  args: {
    from: '0x0000000000000000000000000000000000000000',
    to: '0x9380fA34Fd9e4Fd14c06305fd7B6199089eD4eb9',
    value: 18451770990182011272262n
  },
  eventName: 'Transfer'
}

No Reserve bolster distribution for epoch 1335
✨  Done in 1.57s.

About

💡 Explainer and demo for epochs on Celo

Resources

Code of conduct

Security policy

Stars

Watchers

Forks