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.
Requirement(s):
- Node.js v18.14.2
Running demo scripts:
yarn
yarn ts-node <file_name.ts> # e.g. yarn ts-node totalVoterRewards.ts
On Celo, an "epoch" is a period of time during which a set of validators are elected to produce blocks.
Every epoch is exactly 17,280 blocks long or ~1 day, because
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
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 (
$ 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.
At a high-level, epoch transactions can be grouped as follows:
- Validator and validator group rewards
- Voter rewards
- Community fund distributions
- Carbon offset distributions
- Mento reserve distributions (deprecated, since block
21616000
)
Their purpose and how to fetch their logs is described in more detail below.
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.
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.
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
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.
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.
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.
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.