diff --git a/KIPs/kip-227.md b/KIPs/kip-227.md index 8e510f0..4cda8ad 100644 --- a/KIPs/kip-227.md +++ b/KIPs/kip-227.md @@ -50,8 +50,19 @@ The framework promotes consistent uptime and reliability. Validators have an inc #### Block Header Extension (VRank) -Starting from `FORK_BLOCK`, the block header includes a new field `VRank`. -The `VRank` field contains `RLPEncode(cfReport)` or `nil` if `cfReport` is empty. +Starting from `FORK_BLOCK`, the block header includes a new field `VRank`. Its payload depends on the block position within the epoch: + +| Block position | `header.VRank` | +| :---------------------- | :---------------------------------------------- | +| `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: +> +> - **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)`. +> +> In short: `evaluate(N-1) → cfReport(N) → header(N).VRank`. ```go type Header struct { @@ -71,13 +82,11 @@ type Header struct { Both `pfReport` and `cfReport` are per-block data structures. A node's presence in either report is undesirable: it indicates a failure, and the node may be penalized in future epoch evaluations. -**pfReport** (Proposal Failure Report): Extractable from `header.Extra`. Contains the list of proposers who induced round-change during the consensus of the block. +**pfReport(N)** (Proposal Failure Report): For the mined block `N`, `pfReport(N) = { GetProposer(N, R) : R ∈ [0, r) }` where `r` is the round that reached consensus for block `N`. Extractable from `header(N).Extra`. Format: `pfReport(N) -> [proposerAddrRound0, proposerAddrRound1, ...]` with at most one entry per validator (`validator(N)`). -**cfReport** (Candidate Failure Report): Encoded in `header.VRank` at block `N` for target block `N-1`. -Contains the list of candidates (nodes in `CandTesting`) that failed to send a valid `VRankCandidate` message on-time for block `N-1`. -If `N % k*EPOCH_LENGTH == 0` (epoch start), `cfReport(N)` MUST be empty. +**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`. Format: `cfReport(N) -> [candidateAddr1, candidateAddr2, ...]` with at most one entry per candidate of previous block (`candidate(N-1)`). @@ -135,19 +144,16 @@ VRank runs in parallel with consensus. Per block, reports (`pfReport` and `cfRep #### Proposer of block N+1 -1. When proposing block `N+1`, the proposer MUST build `header.VRank` from `cfReport(N)`. +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). -3. The proposer MUST encode `cfReport` in `header.VRank` as `RLPEncode(cfReport)` or `nil` if empty. -4. Candidates in `cfReport` are counted as failures for CFS aggregation. +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. #### Block Validation -After `FORK_BLOCK`, validators MUST validate the newly added `VRank` field in the block header. -The values of the subfields (`cfReport`) are used to evaluate node performance using the components of the VRank framework. -Given a header with number `N`: +After `FORK_BLOCK`, validators MUST validate `header.VRank` per the encoding table in [Block Header Extension (VRank)](#block-header-extension-vrank), and additionally: -- `header.VRank` MUST be `nil` or `RLPEncode(cfReport(N))`. -- `cfReport(N)` MUST contain at most one entry per candidate ID. Each entry must be a candidate address from `candidates(N-1)`. +- At epoch-start (`N % EPOCH_LENGTH == 0`), the decoded list MUST equal `CandTesting(N)` resolved at block `N`, with no duplicates. +- Otherwise, `cfReport(N)` MUST contain at most one entry per candidate ID, and each entry MUST be a candidate address from `candidates(N-1)`. ### Failure Scores (PFS, CFS) @@ -292,12 +298,12 @@ Given that signatures cannot fully prevent manipulation in either direction, and `pfReport` is extracted from `header.Extra` rather than stored in `header.VRank`. Round-change information is recorded during consensus, before the block is finalized. If `pfReport` were written into `header.VRank` upon each round change, the header would need to be updated mid-consensus. Supporting such updates would require substantial changes to the current implementation. The `Extra` field is already populated during consensus with round-change data, so `pfReport` is derived from there instead. -### Empty `CfReport(k*EPOCH_LENGTH)` +### `header.VRank` at `k*EPOCH_LENGTH` The validator set changes every `EPOCH_LENGTH`, so there may be new validators at block `k*EPOCH_LENGTH` that were not validators at block `k*EPOCH_LENGTH - 1`. Those new validators did not participate in consensus for block `k*EPOCH_LENGTH - 1` and therefore could not have collected `VRankCandidate` messages. The proposer of block `k*EPOCH_LENGTH` may be such a new validator, so they cannot produce a valid `cfReport(k*EPOCH_LENGTH)`. -Hence the `vrank` field in the header at `k*EPOCH_LENGTH` MUST be `nil`. +Instead of leaving the field empty, the proposer MUST embed `CandTesting(k*EPOCH_LENGTH)` — the full candidate list for the new epoch — into `header.VRank`. This anchors the epoch's candidate set into the consensus-validated header, giving all nodes a single authoritative reference for who the candidates are when CFS aggregation begins. ## Backward Compatibility