diff --git a/ECIPs/ECIP-1060.md b/ECIPs/ECIP-1060.md new file mode 100644 index 000000000..a1a932edc --- /dev/null +++ b/ECIPs/ECIP-1060.md @@ -0,0 +1,413 @@ +--- +ecip: 1060 +title: Cliquey proof-of-authority consensus protocol +author: Aidan Hyman (@ChainSafeSystems), Talha Cross (@soc1c) +discussions-to: https://github.com/goerli/eips-poa/issues/12 +status: Draft +type: Standards Track +category: Core +created: 2019-04-19 +requires: 1048 +--- + +## Simple Summary + +This document proposes a new proof-of-authority consensus engine that could be used by Ethereum testing and development networks in the future. + +## Abstract + +**Cliquey** is the second iteration of the _Clique_ proof-of-authority consensus protocol, previously discussed as _"Clique v2"_. It comes with some usability and stability optimizations gained from creating the _Görli_ and _Kotti Classic_ cross-client proof-of-authority networks that were implemented in Geth, Parity Ethereum, Pantheon, Nethermind, and various other clients. + +## Motivation + +The _Kotti Classic_ and _Görli_ testnets running different implementations of the _Clique_ engine got stuck multiple times due to minor issues discovered. These issues were partially addressed on the mono-client _Rinkeby_ network by optimizing the Geth code. + +However, optimizations across multiple clients should be adequately specified and discussed. This working document is a result of a couple of months testing and running cross-client _Clique_ networks, especially with the feedback gathered by several Pantheon, Nethermind, Parity Ethereum, and Geth engineers on different channels. + +The overall goal is to simplify the setup and configuration of proof-of-authority networks, ensure testnets avoid getting stuck and mimicking mainnet conditions. + +_For a general motivation on proof-of-authority testnets, please refer to the exhaustive introduction in EIP-225 which this proposal is based on._ + +## Specification + +This section specifies the **Cliquey** proof-of-authority engine. To quickly understand the differences from _Clique_, please refer to the _Rationale_ further below. + +### Constants + +We define the following constants: + + * **`EPOCH_LENGTH`**: The number of blocks after which to checkpoint and reset the pending votes. It is suggested to remain analogous to the mainnet `ethash` proof-of-work epoch (`30_000`). + * **`BLOCK_PERIOD`**: The minimum difference between two consecutive block's timestamps. It is suggested to remain analogous to the mainnet `ethash` proof-of-work block time target (`15` seconds). + * **`EXTRA_VANITY`**: The fixed number of extra-data prefix bytes reserved for signer _vanity_. It is suggested to retain the current extra-data allowance and use (`32` bytes). + * **`EXTRA_SEAL`**: The fixed number of extra-data suffix bytes reserved for signer seal: `65 bytes` fixed as signatures are based on the standard `secp256k1` curve. + * **`NONCE_AUTH`**: Magic nonce number `0xffffffffffffffff` to vote on adding a new signer. + * **`NONCE_DROP`**: Magic nonce number `0x0000000000000000` to vote on removing a signer. + * **`UNCLE_HASH`**: Always `Keccak256(RLP([]))` as uncles are meaningless outside of proof-of-work. + * **`DIFF_NOTURN`**: Block score (difficulty) for blocks containing out-of-turn signatures. It should be set to `1` since it just needs to be an arbitrary baseline constant. + * **`DIFF_INTURN`**: Block score (difficulty) for blocks containing in-turn signatures. It should be `7` to show preference over out-of-turn signatures. + * **`MIN_WAIT`**: The minimum time to wait for an out-of-turn block to be published. It is suggested to set it to `BLOCK_PERIOD / 2`. + +We also define the following per-block constants: + + * **`BLOCK_NUMBER`**: The block height in the chain, where the height of the genesis is block `0`. + * **`SIGNER_COUNT`**: The number of authorized signers valid at a particular instance in the chain. + * **`SIGNER_INDEX`**: The index of the block signer in the sorted list of currently authorized signers. + * **`SIGNER_LIMIT`**: The number of signers required to govern the list of authorities. It must be `floor(SIGNER_COUNT / 2) + 1` to enforce majority consensus on a proof-of-authority chain. + +We repurpose the `ethash` header fields as follows: + + * **`beneficiary`**: The address to propose modifying the list of authorized signers with. + * Should be filled with zeroes normally, modified only while voting. + * Arbitrary values are permitted nonetheless (even meaningless ones such as voting out non-signers) to avoid extra complexity in implementations around voting mechanics. + * It **must** be filled with zeroes on checkpoint (i.e., epoch transition) blocks. + * Transaction execution **must** use the actual block signer (see `extraData`) for the `COINBASE` opcode. + * **`nonce`**: The signer proposal regarding the account defined by the `beneficiary` field. + * It should be **`NONCE_DROP`** to propose deauthorizing `beneficiary` as an existing signer. + * It should be **`NONCE_AUTH`** to propose authorizing `beneficiary` as a new signer. + * It **must** be filled with zeroes on checkpoint (i.e., on epoch transition) blocks. + * It **must** not take up any other value apart from the two above. + * **`extraData`**: Combined field for signer vanity, checkpointing and signer signatures. + * The first **`EXTRA_VANITY`** fixed bytes may contain arbitrary signer vanity data. + * The last **`EXTRA_SEAL`** fixed bytes are the signer's signature sealing the header. + * Checkpoint blocks **must** contain a list of signers (`SIGNER_COUNT*20` bytes) in between, **omitted** otherwise. + * The list of signers in checkpoint block extra-data sections **must** be sorted in ascending order. + * **`mixHash`**: Reserved for fork protection logic, similar to the extra-data during the DAO. + * It **must** be filled with zeroes during regular operation. + * **`ommersHash`**: It **must** be **`UNCLE_HASH`** as uncles are meaningless outside of proof-of-work. + * **`timestamp`**: It **must** be greater than the parent timestamp + **`BLOCK_PERIOD`**. + * **`difficulty`**: It contains the standalone score of the block to derive the quality of a chain. + * It **must** be **`DIFF_NOTURN`** if `BLOCK_NUMBER % SIGNER_COUNT != SIGNER_INDEX` + * It **must** be **`DIFF_INTURN`** if `BLOCK_NUMBER % SIGNER_COUNT == SIGNER_INDEX` + +### Validator List + +The _initial_ validator list can be specified in the configuration at genesis, i.e., by appending it to the Clique config in Geth: + +```json +"clique":{ + "period": 15, + "epoch": 30000, + "validators": [ + "0x7d577a597b2742b498cb5cf0c26cdcd726d39e6e", + "0x82a978b3f5962a5b0957d9ee9eef472ee55b42f1" + ] +} +``` + +By using this list, for convenience, the `extraData` field of the genesis block only has to contain the 32 bytes of **`EXTRA_VANITY`**. The client automatically converts and appends the list of signers to the block `0` extra-data as specified in EIP-225 at checkpoint blocks (appending `SIGNER_COUNT*20` bytes). + +### Sealing + +For a detailed specification of the block authorization logic, please refer to EIP-225 by honoring the constants defined above. However, the following changes should be highlighted: + +* Each singer is **allowed to sign any number of consecutive blocks**. The order is not fixed, but in-turn signing weighs more (**`DIFF_INTURN`**) than out-of-turn one (**`DIFF_NOTURN`**). In case an out-of-turn block is received, an **in-turn signer should continue to publish their block** to ensure the chain always prefers in-turn blocks in any case. This strategy prevents in-turn validators from being hindered from publishing their block and potential network halting. + + * If a signer is allowed to sign a block, i.e., is on the authorized list: + * Calculate the Gaussian random signing time of the next block: `parent_timestamp + BLOCK_PERIOD + r`, where `r` is a uniform random value in `rand(-BLOCK_PERIOD/4, BLOCK_PERIOD/4)`. + * If the signer is in-turn, wait for the exact time to arrive, sign and broadcast immediately. + * If the signer is out-of-turn, delay signing by `MIN_WAIT + rand(0, SIGNER_COUNT * 500ms)`. + +This strategy will always ensure that an in-turn signer has a **substantial advantage** to sign and propagate versus the out-of-turn signers. + +### Voting + +The voting logic is unchanged and can be adapted straight from EIP-225. + +## Rationale + +The following changes were introduced over Clique EIP-225 and should be discussed briefly. + +* Cliquey introduces a **`MIN_WAIT`** period for out-of-turn block to be published which is not present for Clique. This addresses the issue of out-of-turn blocks often getting pushed into the network too fast causing a lot of short reorganizations and in some rare cases causing the network to come to a halt. By holding back out-of-turn blocks, Cliquey allows in-turn validators to seal blocks even under non-optimal network conditions, such as high network latency or validators with unsynchronized clocks. +* To further strengthen the role of in-turn blocks, an authority should continue to publish in-turn blocks even if an out-of-turn block was already received on the network. This prevents in-turn validators being hindered from publishing their block and potential network problems, such as reorganizations or the network getting stuck. +* Additionally, the **`DIFF_INTURN`** was increased from `2` to `7` to avoid situations where two different chain heads have the same total difficulty. This prevents the network from getting stuck by making in-turn blocks significantly more _heavy_ than out-of-turn blocks. +* The **`SIGNER_LIMIT`** was removed from block sealing logic and is only required for voting. This allows the network to continue sealing blocks even if all but one of the validators are offline. The voting governance is not affected and still requires signer majority. +* The block period should be less strict and slightly randomized to mimic mainnet conditions. Therefore, it is slightly randomized in the uniform range of `[-BLOCK_PERIOD/4, BLOCK_PERIOD/4]`. With this, the average block time will still hover around **`BLOCK_PERIOD`**. + +Finally, without changing any consensus logic, we propose the ability to specify an initial list of validators at genesis configuration without tampering with the `extraData`. + +## Test Cases + +```go +// block represents a single block signed by a particular account, where +// the account may or may not have cast a Clique vote. +type block struct { + signer string // Account that signed this particular block + voted string // Optional value if the signer voted on adding/removing someone + auth bool // Whether the vote was to authorize (or deauthorize) + checkpoint []string // List of authorized signers if this is an epoch block +} + +// Define the various voting scenarios to test +tests := []struct { + epoch uint64 // Number of blocks in an epoch (unset = 30000) + signers []string // Initial list of authorized signers in the genesis + blocks []block // Chain of signed blocks, potentially influencing auths + results []string // Final list of authorized signers after all blocks + failure error // Failure if some block is invalid according to the rules +}{ + { + // Single signer, no votes cast + signers: []string{"A"}, + blocks: []block{{signer: "A"}}, + results: []string{"A"}, + }, { + // Single signer, voting to add two others (only accept first, second needs 2 votes) + signers: []string{"A"}, + blocks: []block{ + {signer: "A", voted: "B", auth: true}, + {signer: "B"}, + {signer: "A", voted: "C", auth: true}, + }, + results: []string{"A", "B"}, + }, { + // Two signers, voting to add three others (only accept first two, third needs 3 votes already) + signers: []string{"A", "B"}, + blocks: []block{ + {signer: "A", voted: "C", auth: true}, + {signer: "B", voted: "C", auth: true}, + {signer: "A", voted: "D", auth: true}, + {signer: "B", voted: "D", auth: true}, + {signer: "C"}, + {signer: "A", voted: "E", auth: true}, + {signer: "B", voted: "E", auth: true}, + }, + results: []string{"A", "B", "C", "D"}, + }, { + // Single signer, dropping itself (weird, but one less cornercase by explicitly allowing this) + signers: []string{"A"}, + blocks: []block{ + {signer: "A", voted: "A", auth: false}, + }, + results: []string{}, + }, { + // Two signers, actually needing mutual consent to drop either of them (not fulfilled) + signers: []string{"A", "B"}, + blocks: []block{ + {signer: "A", voted: "B", auth: false}, + }, + results: []string{"A", "B"}, + }, { + // Two signers, actually needing mutual consent to drop either of them (fulfilled) + signers: []string{"A", "B"}, + blocks: []block{ + {signer: "A", voted: "B", auth: false}, + {signer: "B", voted: "B", auth: false}, + }, + results: []string{"A"}, + }, { + // Three signers, two of them deciding to drop the third + signers: []string{"A", "B", "C"}, + blocks: []block{ + {signer: "A", voted: "C", auth: false}, + {signer: "B", voted: "C", auth: false}, + }, + results: []string{"A", "B"}, + }, { + // Four signers, consensus of two not being enough to drop anyone + signers: []string{"A", "B", "C", "D"}, + blocks: []block{ + {signer: "A", voted: "C", auth: false}, + {signer: "B", voted: "C", auth: false}, + }, + results: []string{"A", "B", "C", "D"}, + }, { + // Four signers, consensus of three already being enough to drop someone + signers: []string{"A", "B", "C", "D"}, + blocks: []block{ + {signer: "A", voted: "D", auth: false}, + {signer: "B", voted: "D", auth: false}, + {signer: "C", voted: "D", auth: false}, + }, + results: []string{"A", "B", "C"}, + }, { + // Authorizations are counted once per signer per target + signers: []string{"A", "B"}, + blocks: []block{ + {signer: "A", voted: "C", auth: true}, + {signer: "B"}, + {signer: "A", voted: "C", auth: true}, + {signer: "B"}, + {signer: "A", voted: "C", auth: true}, + }, + results: []string{"A", "B"}, + }, { + // Authorizing multiple accounts concurrently is permitted + signers: []string{"A", "B"}, + blocks: []block{ + {signer: "A", voted: "C", auth: true}, + {signer: "B"}, + {signer: "A", voted: "D", auth: true}, + {signer: "B"}, + {signer: "A"}, + {signer: "B", voted: "D", auth: true}, + {signer: "A"}, + {signer: "B", voted: "C", auth: true}, + }, + results: []string{"A", "B", "C", "D"}, + }, { + // Deauthorizations are counted once per signer per target + signers: []string{"A", "B"}, + blocks: []block{ + {signer: "A", voted: "B", auth: false}, + {signer: "B"}, + {signer: "A", voted: "B", auth: false}, + {signer: "B"}, + {signer: "A", voted: "B", auth: false}, + }, + results: []string{"A", "B"}, + }, { + // Deauthorizing multiple accounts concurrently is permitted + signers: []string{"A", "B", "C", "D"}, + blocks: []block{ + {signer: "A", voted: "C", auth: false}, + {signer: "B"}, + {signer: "C"}, + {signer: "A", voted: "D", auth: false}, + {signer: "B"}, + {signer: "C"}, + {signer: "A"}, + {signer: "B", voted: "D", auth: false}, + {signer: "C", voted: "D", auth: false}, + {signer: "A"}, + {signer: "B", voted: "C", auth: false}, + }, + results: []string{"A", "B"}, + }, { + // Votes from deauthorized signers are discarded immediately (deauth votes) + signers: []string{"A", "B", "C"}, + blocks: []block{ + {signer: "C", voted: "B", auth: false}, + {signer: "A", voted: "C", auth: false}, + {signer: "B", voted: "C", auth: false}, + {signer: "A", voted: "B", auth: false}, + }, + results: []string{"A", "B"}, + }, { + // Votes from deauthorized signers are discarded immediately (auth votes) + signers: []string{"A", "B", "C"}, + blocks: []block{ + {signer: "C", voted: "D", auth: true}, + {signer: "A", voted: "C", auth: false}, + {signer: "B", voted: "C", auth: false}, + {signer: "A", voted: "D", auth: true}, + }, + results: []string{"A", "B"}, + }, { + // Cascading changes are not allowed, only the account being voted on may change + signers: []string{"A", "B", "C", "D"}, + blocks: []block{ + {signer: "A", voted: "C", auth: false}, + {signer: "B"}, + {signer: "C"}, + {signer: "A", voted: "D", auth: false}, + {signer: "B", voted: "C", auth: false}, + {signer: "C"}, + {signer: "A"}, + {signer: "B", voted: "D", auth: false}, + {signer: "C", voted: "D", auth: false}, + }, + results: []string{"A", "B", "C"}, + }, { + // Changes reaching consensus out of bounds (via a deauth) execute on touch + signers: []string{"A", "B", "C", "D"}, + blocks: []block{ + {signer: "A", voted: "C", auth: false}, + {signer: "B"}, + {signer: "C"}, + {signer: "A", voted: "D", auth: false}, + {signer: "B", voted: "C", auth: false}, + {signer: "C"}, + {signer: "A"}, + {signer: "B", voted: "D", auth: false}, + {signer: "C", voted: "D", auth: false}, + {signer: "A"}, + {signer: "C", voted: "C", auth: true}, + }, + results: []string{"A", "B"}, + }, { + // Changes reaching consensus out of bounds (via a deauth) may go out of consensus on first touch + signers: []string{"A", "B", "C", "D"}, + blocks: []block{ + {signer: "A", voted: "C", auth: false}, + {signer: "B"}, + {signer: "C"}, + {signer: "A", voted: "D", auth: false}, + {signer: "B", voted: "C", auth: false}, + {signer: "C"}, + {signer: "A"}, + {signer: "B", voted: "D", auth: false}, + {signer: "C", voted: "D", auth: false}, + {signer: "A"}, + {signer: "B", voted: "C", auth: true}, + }, + results: []string{"A", "B", "C"}, + }, { + // Ensure that pending votes don't survive authorization status changes. This + // corner case can only appear if a signer is quickly added, removed and then + // readded (or the inverse), while one of the original voters dropped. If a + // past vote is left cached in the system somewhere, this will interfere with + // the final signer outcome. + signers: []string{"A", "B", "C", "D", "E"}, + blocks: []block{ + {signer: "A", voted: "F", auth: true}, // Authorize F, 3 votes needed + {signer: "B", voted: "F", auth: true}, + {signer: "C", voted: "F", auth: true}, + {signer: "D", voted: "F", auth: false}, // Deauthorize F, 4 votes needed (leave A's previous vote "unchanged") + {signer: "E", voted: "F", auth: false}, + {signer: "B", voted: "F", auth: false}, + {signer: "C", voted: "F", auth: false}, + {signer: "D", voted: "F", auth: true}, // Almost authorize F, 2/3 votes needed + {signer: "E", voted: "F", auth: true}, + {signer: "B", voted: "A", auth: false}, // Deauthorize A, 3 votes needed + {signer: "C", voted: "A", auth: false}, + {signer: "D", voted: "A", auth: false}, + {signer: "B", voted: "F", auth: true}, // Finish authorizing F, 3/3 votes needed + }, + results: []string{"B", "C", "D", "E", "F"}, + }, { + // Epoch transitions reset all votes to allow chain checkpointing + epoch: 3, + signers: []string{"A", "B"}, + blocks: []block{ + {signer: "A", voted: "C", auth: true}, + {signer: "B"}, + {signer: "A", checkpoint: []string{"A", "B"}}, + {signer: "B", voted: "C", auth: true}, + }, + results: []string{"A", "B"}, + }, { + // An unauthorized signer should not be able to sign blocks + signers: []string{"A"}, + blocks: []block{ + {signer: "B"}, + }, + failure: errUnauthorizedSigner, + }, { + // An authorized signer that signed recenty should not be able to sign again + signers: []string{"A", "B"}, + blocks []block{ + {signer: "A"}, + {signer: "A"}, + }, + failure: errRecentlySigned, + }, { + // Recent signatures should not reset on checkpoint blocks imported in a batch + epoch: 3, + signers: []string{"A", "B", "C"}, + blocks: []block{ + {signer: "A"}, + {signer: "B"}, + {signer: "A", checkpoint: []string{"A", "B", "C"}}, + {signer: "A"}, + }, + failure: errRecentlySigned, + },, +} +``` + +## Implementation + +A proof-of-concept implementation is being worked on at **#ETHCapeTown**. + +## Copyright +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).