diff --git a/KIPs/kip-227.md b/KIPs/kip-227.md index 112e9cc..469dbf6 100644 --- a/KIPs/kip-227.md +++ b/KIPs/kip-227.md @@ -58,12 +58,12 @@ Starting from `FORK_BLOCK`, the block header includes a new field `VRank`. Its p | `N % EPOCH_LENGTH != 0` | `RLPEncode(cfReport(N))`, or `nil` if empty | | `N % EPOCH_LENGTH == 0` | `RLPEncode(CandTesting(N))` (MUST NOT be `nil`) | -> **Note**: The index `N` of `cfReport(N)` refers to the block in which the report is **recorded**, not the block it evaluates. There are two perspectives on the same data: +> **Note**: The index `N` of `cfReport(N)` refers to the block in which the report is **recorded**, not the block it evaluates. A `cfReport` is written about the writer's own most recent prior proposal: > -> - **Writer (proposer of block `N`)**: builds the report from candidate evaluation conducted during block `N-1`'s consensus (i.e., from `VRankCandidate` messages collected for block `N-1`), and writes it into `header(N).VRank`. -> - **Reader (any node)**: decodes `header(N).VRank` to obtain `cfReport(N)`. +> - **Writer (proposer of block `N`)**: evaluates the `VRankCandidate` messages it collected for `T`, its own most recent prior proposal in the same epoch (so `GetProposer(T) == GetProposer(N)`), and writes the failed candidates into `header(N).VRank`. `T` itself is not stored. +> - **Reader (any node)**: decodes `header(N).VRank` to obtain the failed candidates. > -> In short: `evaluate(N-1) → cfReport(N) → header(N).VRank`. +> In short: `evaluate(T) → cfReport(N) → header(N).VRank`, where `T` is the writer's own most recent prior proposal in the epoch. ```go type Header struct { @@ -87,9 +87,9 @@ Both `pfReport` and `cfReport` are per-block data structures. A node's presence Format: `pfReport(N) -> [proposerAddrRound0, proposerAddrRound1, ...]` with at most one entry per validator (`validator(N)`). -**cfReport(N)** (Candidate Failure Report): Covers candidate evaluation during block `N-1`'s consensus. Recorded in block `N`. Contains the list of candidates (nodes in `CandTesting` at block `N-1`) that failed to send a valid `VRankCandidate` message on-time for block `N-1`. +**cfReport(N)** (Candidate Failure Report): Recorded in block `N`, it covers candidate evaluation during block `T`, the proposer's own most recent prior proposal in the same epoch (`GetProposer(T) == GetProposer(N)`). It carries the list of candidates (nodes in `CandTesting` at block `T`) that failed to send a valid `VRankCandidate` message on-time for block `T`. -Format: `cfReport(N) -> [candidateAddr1, candidateAddr2, ...]` with at most one entry per candidate of previous block (`candidate(N-1)`). +Format: `cfReport(N) -> [candidateAddr1, candidateAddr2, ...]` with at most one entry per candidate of block `T`. #### VRankPreprepare @@ -105,7 +105,7 @@ type VRankPreprepare struct { #### VRankCandidate -`VRankCandidate` is a message type sent by each candidate (node in `CandTesting`) to all validators under `ValActive` upon receiving `VRankPreprepare`. A candidate must send `VRankCandidate` within `CANDIDATE_MSG_TIMEOUT` of the counterparty's `preprepared_time` to be counted as on-time. +`VRankCandidate` is a message type sent by each candidate (node in `CandTesting`) to the proposer of block `N` upon receiving its `VRankPreprepare`. A candidate must send `VRankCandidate` within `CANDIDATE_MSG_TIMEOUT` of the proposer's `preprepared_time` to be counted as on-time. **Signature scheme**: `VRankCandidate` carries two signatures: @@ -124,31 +124,34 @@ type VRankCandidate struct { ### Consensus Protocol Integration -VRank runs in parallel with consensus. Per block, reports (`pfReport` and `cfReport`) are produced during consensus and committed in the next block header. +VRank runs in parallel with consensus. `pfReport(N)` is produced during block `N`'s consensus and committed in the next block header. A `cfReport` for a proposer's own block is produced from that block's consensus but committed later, in that proposer's next proposal. #### Proposer of block N 1. After having sent Istanbul Preprepare messages to consensus participants, the proposer MUST send `VRankPreprepare` to all candidates in `CandTesting`. 2. The round information is recorded in `header.Extra` as part of the existing consensus. If the proposer fails to propose and a round change occurs, the failed proposer's address is recorded in `pfReport(N)`. -#### Validators during consensus for block N +#### Proposer collecting VRankCandidate for block N -1. When block `N` enters the `preprepared` pBFT state, each validator MUST record `preprepared_time`. -2. Each validator MUST collect `VRankCandidate` messages from candidates in `CandTesting` and record each message's arrival time. A message is considered valid only if both its ECDSA signature (`Sig`) and its BLS signature (`BlsSig`) are valid. The BLS public key is resolved from the candidate's KIP-113 registration at the current chain head. -3. If a validator receives more than one `VRankCandidate` from the same candidate for the same view (block number `N` and round `R`), only the first valid message MUST be accepted; subsequent messages MUST be ignored. -4. A candidate is counted as on-time if the message is valid and either (a) it arrives before `preprepared_time`, or (b) `arrival_time - preprepared_time ≤ CANDIDATE_MSG_TIMEOUT`. Otherwise, it will be recorded in `cfReport(N+1)`. +1. When block `N` enters the `preprepared` pBFT state, the proposer MUST record `preprepared_time`. +2. The proposer MUST collect `VRankCandidate` messages from candidates in `CandTesting` and record each message's arrival time. A message is considered valid only if both its ECDSA signature (`Sig`) and its BLS signature (`BlsSig`) are valid. The BLS public key is resolved from the candidate's KIP-113 registration at the current chain head. +3. If the proposer receives more than one `VRankCandidate` from the same candidate for the same view (block number `N` and round `R`), only the first valid message MUST be accepted; subsequent messages MUST be ignored. +4. A candidate is counted as on-time if the message is valid and `arrival_time - preprepared_time ≤ CANDIDATE_MSG_TIMEOUT`. Otherwise it is a candidate failure for block `N`, which block `N`'s proposer records in the `cfReport` of its own next proposal. #### Candidates (nodes in CandTesting) -1. Upon receiving `VRankPreprepare` for block `N`, each candidate MUST broadcast `VRankCandidate` to all validators in `ValActive`. -2. To be counted as on-time, the `VRankCandidate` MUST arrive at each validator within `CANDIDATE_MSG_TIMEOUT` of that validator's `preprepared_time` for block `N`. +1. Upon receiving `VRankPreprepare` for block `N`, each candidate MUST send `VRankCandidate` to the proposer that sent it (the proposer of block `N`). +2. To be counted as on-time, the `VRankCandidate` MUST arrive at the proposer within `CANDIDATE_MSG_TIMEOUT` of the proposer's `preprepared_time` for block `N`. -#### Proposer of block N+1 +#### Proposer writing a cfReport + +When a proposer produces block `N`, it reports the candidate evaluation for its own most recent prior proposal, so that the report writer is the same validator that sent the candidates their `VRankPreprepare` (any withheld or fabricated failure is then attributed to that proposer's own column under CFS aggregation). 1. The proposer MUST set `header.VRank` per the encoding table in [Block Header Extension (VRank)](#block-header-extension-vrank). -2. `cfReport(N+1)` MUST include each candidate (in `CandTesting` at block `N`) who either (a) did not send a `VRankCandidate` for block `N` on-time, or (b) sent an invalid message (including ECDSA or BLS signature failure, or a missing KIP-113 BLS key registration). If block `N` reached consensus at a round greater than `MAX_ROUND`, `cfReport(N+1)` MUST instead be empty, since `VRankCandidate` messages above `MAX_ROUND` are not collected and candidate on-time status cannot be determined for such a view. -3. Candidates in `cfReport` are counted as failures for CFS aggregation. The epoch-start candidate list is informational only and does not contribute to CFS. -4. If block `N+1` is an epoch-start block (`(N+1) % EPOCH_LENGTH == 0`), the proposer MUST NOT include a `cfReport` for block `N`; instead, `header(N+1).VRank` carries `CandTesting(N+1)` as specified in the encoding table. +2. Let `T` be the proposer's most recent prior block in the same epoch as `N` that it produced (`GetProposer(T) == GetProposer(N)`). If no such block exists — the proposer's previous proposal was in an earlier epoch, or it has not produced a block yet in this epoch — `header(N).VRank` MUST be empty. +3. `cfReport(N)` MUST include each candidate (in `CandTesting` at block `T`) who either (a) did not send a `VRankCandidate` for block `T` on-time, or (b) sent an invalid message (including ECDSA or BLS signature failure, or a missing KIP-113 BLS key registration). If block `T` reached consensus at a round greater than `MAX_ROUND`, `cfReport(N)` MUST instead be empty, since `VRankCandidate` messages above `MAX_ROUND` are not collected and candidate on-time status cannot be determined for such a view. +4. Candidates in `cfReport` are counted as failures for CFS aggregation. The epoch-start candidate list is informational only and does not contribute to CFS. +5. If block `N` is an epoch-start block (`N % EPOCH_LENGTH == 0`), the proposer MUST NOT include a `cfReport`; instead, `header(N).VRank` carries `CandTesting(N)` as specified in the encoding table. #### Block Validation @@ -157,7 +160,7 @@ Before `FORK_BLOCK`, `header.VRank` MUST be empty (zero-length bytes). After `FORK_BLOCK`, validators MUST validate `header.VRank` per the encoding table in [Block Header Extension (VRank)](#block-header-extension-vrank), and additionally: - At epoch-start (`N % EPOCH_LENGTH == 0`), the decoded list MUST exactly equal `CandTesting(N)` resolved at block `N` (preserving the order returned by the valset module, with no duplicates). -- At a non-epoch block with a non-empty payload, `cfReport(N)` MUST be sorted in ascending byte order, contain at most one entry per candidate ID, and each entry MUST be a candidate address from `CandTesting(N-1)`. +- At a non-epoch block with a non-empty payload, the failed-candidate list MUST be sorted in ascending byte order, contain at most one entry per candidate ID, and each entry MUST be a candidate address from `CandTesting(N-1)` (equivalently `CandTesting(T)`, since `CandTesting` is stable within an epoch). - At a non-epoch block, an empty payload (`nil` or zero-length) is permitted and represents no candidate failures for that block. ### Failure Scores (PFS, CFS) @@ -234,7 +237,7 @@ With `F = 3`, we discard the highest 3 reporter totals per candidate and sum the