cip | title | author | discussions-to | status | type | category | created | license |
---|---|---|---|---|---|---|---|---|
21 |
Governable lookback window |
Andres di Govanni (@andres-dg) |
Final |
Standards Track |
Ring 0 |
2020-09-25 |
Apache 2.0 |
The key words "MUST", "MUST NOT", "SHOULD", and "MAY" in this document are to be interpreted as described in RFC 2119.
Validators are paid with rewards to maintain the network. Their pay rate is lowered if they failed to sign blocks for a fixed duration (the "lookback window"). This proposal suggests adjusting the lookback window on-chain via community governance.
Celo protocol elects validators trusted to sign mined blocks and, in return, they recieve Celo Native Asset (CELO) at the end of the epoch. What happens if an elected validator fails to sign the blocks it's been assigned? During an epoch, the Celo protocol will keep track of each validator's score, as a percentage, and the rewards paid will be proportional to their score.
The purpose of this score is to make sure the validator is doing the work it has been assigned. A validator's score can be penalized for various reasons. This proposal is focused on the uptime metric.
A validator's uptime is measured by keeping track of the blocks it signs. If a determined amount of consecutive blocks haven't been signed, the validator is considered to be down. This fixed amount of blocks is called the lookback window and its value is currently 12 blocks. Given that a block is mined every 5 seconds, the lookback window is 1 minute. The value currently lies in the chain config, meaning that changing the value is a hard fork, and requires rebooting the node with an updated chain config.
In practice, the lookback window has penalized validators for performing routine node maintenance. To solve this, the window this CIP updates the lookback window to be dynamically adjustable by the community - in other words, governable. This will let anyone with stake vote on the value of this parameter.
- The lookback window parameter must be adjustable.
- The lookback window parameter must be constant during an epoch
- The community must decide on the value.
The proposed solution is to migrate the parameter to the EVM state (layer 2).
Starting at the first full post-fork epoch (i.e. the same epoch as HARDFORK_BLOCK
if it's the first block of its epoch, or the next epoch if it isn't), block creation and validation MUST read the lookback window from chain state by calling the BlockchainParameters
contract, rather than using the value from the genesis config. Reward calculations are unaffected.
In order for uptimeLookbackWindow
to remain constant during an epoch, the following rules are applied:
- The
setter
onBlockchainParameters.sol
foruptimeLookbackWindow
MUST apply the new value to the next epoch (not the current one) - The
getter
onBlockchainParameters.sol
foruptimeLookbackWindow
MUST revert if there's no value for the current epoch. This error signals the blockchain client to use thedefaultValue
(pre Hardfork value).
As the uptimeLookbackWindow
is now governable. Work must be done to verify the value is within safe boundaries; which are defined by the following rules:
uptimeLookbackWindow
MUST BE within the range[MIN_SAFE_LOOKBACK_WINDOW, MAX_SAFE_LOOKBACK_WINDOW]
uptimeLookbackWindow
MUST BE less than or equal toepoch size - 2
where:
MIN_SAFE_LOOKBACK_WINDOW = 3
MAX_SAFE_LOOKBACK_WINDOW = 720
BlockchainParameters.sol
MUST enforce these rules.
As a security measure, blockchain client MUST enforce the same rules. If the return value is outside the safe window, the node MUST clamp the return value to that range such that MIN_SAFE_LOOKBACK_WINDOW <= window <= MAX_SAFE_LOOKBACK_WINDOW
. If the result is greater than the epoch size - 2
, the node MUST discard the result and use epochSize - 2
instead. If the epoch size is 3 or less, the node MUST error, as it is in an invalid state.
If the BlockchainParameters.sol
contract errors or returns 0, the node MUST use the DEFAULT_VALUE
which is defined as:
DEFAULT_VALUE
: Configured value for lookbackWindow on the ChainParams
def clamp_lookback_window(window: int) -> int:
# clamp to range
window = max(window, MIN_SAFE_LOOKBACK_WINDOW)
window = min(window, MAX_SAFE_LOOKBACK_WINDOW)
return window
def get_lookback_window(chain: ChainState) -> int:
window: int
# get the lookback window from the chain state.
# default to defaultValue in case of error
try:
window = chain.evm.get_blockchain_param("lookbackWindow")
window = clamp_lookback_window(window)
except EVMExecutionError:
window = defaultValue
# invalid chain param
assert chain.params.epoch_size > 3
# ensure it is sensible given the chain params
if window > chain.params.epoch_size - 2:
window = chain.params.epoch_size - 2
return window
This way, the parameter will be handled by the BlockchainParameters
contract and it will be fetched with a static view call from the node (layer 1). This requires a hard fork, replacing any reference to the parameter with a contract call.
struct LookbackWindow {
// Value for lookbackWindow before `nextValueActivationBlock`
uint256 oldValue;
// Value for lookbackWindow after `nextValueActivationBlock`
uint256 nextValue;
// Epoch where next value is activated
uint256 nextValueActivationEpoch;
}
LookbackWindow public uptimeLookbackWindow;
event UptimeLookbackWindowSet(uint256 window, uint256 activationEpoch);
/**
* @notice Sets the uptime lookback window.
* @param window New window.
*/
function setUptimeLookbackWindow(uint256 window) public onlyOwner {
require(window >= 3 && window <= 720, "UptimeLookbackWindow must be within safe range");
require(
window <= getEpochSize().sub(2),
"UptimeLookbackWindow must be smaller or equal to epochSize - 2"
);
uptimeLookbackWindow.oldValue = _getUptimeLookbackWindow();
// changes only take place on the next epoch
uptimeLookbackWindow.nextValueActivationEpoch = getEpochNumber().add(1);
uptimeLookbackWindow.nextValue = window;
emit UptimeLookbackWindowSet(window, uptimeLookbackWindow.nextValueActivationEpoch);
}
/**
* @notice Gets the uptime lookback window.
*/
function getUptimeLookbackWindow() public view returns (uint256 lookbackWindow) {
lookbackWindow = _getUptimeLookbackWindow();
require(lookbackWindow != 0, "UptimeLookbackWindow is not initialized");
}
/**
* @notice Gets the uptime lookback window.
*/
function _getUptimeLookbackWindow() internal view returns (uint256 lookbackWindow) {
if (getEpochNumber() >= uptimeLookbackWindow.nextValueActivationEpoch) {
return uptimeLookbackWindow.nextValue;
} else {
return uptimeLookbackWindow.oldValue;
}
}
In the current specification, the following migration scenarios are considered:
- The first post-fork epoch starts before a lookback window from the smart contract is available: This scenario includes several cases: the smart contract not having been upgraded yet, the smart contract having been upgraded but
setUptimeLookbackWindow()
not having been called, orsetUptimeLookbackWindow()
having been called but its value not having come into effect yet. In all these cases, in the first epoch after the hard fork, the blockchain client attempt to obtainuptimeLookbackWindow
from the contract, but will get an error, and will then useDEFAULT_VALUE
. This will continue until the first block of the first epoch after the contract has been upgraded andsetUptimeLookbackWindow()
has been called successfully, from which point onwards the smart contract call will succeed and return the lookback window which the client will then use. Thus, the value does not change within an epoch. - The smart Contract is upgraded and initialized before, or in the same epoch as, the hard fork activation: In this scenario, in the first post-fork epoch, the blockchain client will obtain
uptimeLookbackWindow
successfully from the smart contract, and use that value during the epoch. - setUptimeLookbackWindow() is called mid-epoch: On this scenario, the new value come into effect at the beginning of the next epoch, thus the value returned by the contract will remain constant during the epoch.
The proposed solution will be used only after the Donut Hard Fork is active. If Donut is not active at the beginning of an epoch, processing MUST use the original value configured on the genesis block. For mainnet, that value is 12
The lookback window is queried for each block processed, so this would add a contract call for each block. It might be quite expensive but it is a view call.
The lookback window must have built in boundaries to limit the window from being excessively long (meaning validators could be down for a long time without hurting their score) or too small. The specification limits window size to between 3 blocks (15 seconds) and 720 blocks (1 hour). These are not suggestions for the window. They are sanity checks on the output of the governance process to preserve invariants necessary for chain operation.
Consider a lazy, but not malicious validator. To maintain uptime, they need only sign one block per lookback window. If the lookback window is one day, the lazy validator may choose to run their node for only 5 minutes per day, while earning full rewards. If this becomes the dominant strategy among validators, it may have severe impacts on network liveness. It is important to keep the lookback window relatively short compared to the epoch, to prevent this behavior.