From 1e43d02a74f2f23184424b66059983ccbe9c6503 Mon Sep 17 00:00:00 2001 From: Chihyun Song Date: Fri, 8 May 2026 09:15:29 +0900 Subject: [PATCH 1/3] kip-227: fill CT at epoch start blocks --- KIPs/kip-227.md | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/KIPs/kip-227.md b/KIPs/kip-227.md index 8e510f0..4068205 100644 --- a/KIPs/kip-227.md +++ b/KIPs/kip-227.md @@ -50,8 +50,10 @@ 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: + +- For non-epoch-start blocks (`N % EPOCH_LENGTH != 0`), `VRank` contains `RLPEncode(cfReport(N))`, or `nil` if `cfReport(N)` is empty. +- For epoch-start blocks (`N % EPOCH_LENGTH == 0`), `VRank` contains `RLPEncode(CandTesting(N))` — the full candidate list for the new epoch. ```go type Header struct { @@ -71,13 +73,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,10 +135,11 @@ 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. When proposing block `N+1`: + - If `(N+1) % EPOCH_LENGTH == 0` (epoch start), the proposer MUST build `header.VRank` from `CandTesting(N+1)` and encode it as `RLPEncode(CandTesting(N+1))`. `header.VRank` MUST NOT be `nil` in this case, even if `CandTesting(N+1)` is empty (in which case the encoding is `RLPEncode([])`). + - Otherwise, the proposer MUST build `header.VRank` from `cfReport(N+1)` (computed below) and encode it as `RLPEncode(cfReport(N+1))`, or `nil` if `cfReport(N+1)` is empty. 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 @@ -146,8 +147,8 @@ After `FORK_BLOCK`, validators MUST validate the newly added `VRank` field in th 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`: -- `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)`. +- If `N % EPOCH_LENGTH == 0` (epoch start), `header.VRank` MUST be `RLPEncode(CandTesting(N))`. The decoded list MUST equal the candidate set `CandTesting(N)` resolved at block `N`, with no duplicates. +- Otherwise, `header.VRank` MUST be `nil` or `RLPEncode(cfReport(N))`. `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 +293,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 From 7a51b3e637379a7475afc56f3b7e31625313d6a2 Mon Sep 17 00:00:00 2001 From: Chihyun Song Date: Fri, 8 May 2026 13:53:56 +0900 Subject: [PATCH 2/3] kip-227: update header.VRank definition --- KIPs/kip-227.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/KIPs/kip-227.md b/KIPs/kip-227.md index 4068205..9e51107 100644 --- a/KIPs/kip-227.md +++ b/KIPs/kip-227.md @@ -52,8 +52,17 @@ The framework promotes consistent uptime and reliability. Validators have an inc Starting from `FORK_BLOCK`, the block header includes a new field `VRank`. Its payload depends on the block position within the epoch: -- For non-epoch-start blocks (`N % EPOCH_LENGTH != 0`), `VRank` contains `RLPEncode(cfReport(N))`, or `nil` if `cfReport(N)` is empty. -- For epoch-start blocks (`N % EPOCH_LENGTH == 0`), `VRank` contains `RLPEncode(CandTesting(N))` — the full candidate list for the new 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 { From 2bec71d78c7a89b223b5a3f976b4403ae9f79761 Mon Sep 17 00:00:00 2001 From: Chihyun Song Date: Fri, 8 May 2026 15:08:54 +0900 Subject: [PATCH 3/3] kip-227: update header.VRank definition --- KIPs/kip-227.md | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/KIPs/kip-227.md b/KIPs/kip-227.md index 9e51107..4cda8ad 100644 --- a/KIPs/kip-227.md +++ b/KIPs/kip-227.md @@ -144,20 +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`: - - If `(N+1) % EPOCH_LENGTH == 0` (epoch start), the proposer MUST build `header.VRank` from `CandTesting(N+1)` and encode it as `RLPEncode(CandTesting(N+1))`. `header.VRank` MUST NOT be `nil` in this case, even if `CandTesting(N+1)` is empty (in which case the encoding is `RLPEncode([])`). - - Otherwise, the proposer MUST build `header.VRank` from `cfReport(N+1)` (computed below) and encode it as `RLPEncode(cfReport(N+1))`, or `nil` if `cfReport(N+1)` is empty. +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. 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: -- If `N % EPOCH_LENGTH == 0` (epoch start), `header.VRank` MUST be `RLPEncode(CandTesting(N))`. The decoded list MUST equal the candidate set `CandTesting(N)` resolved at block `N`, with no duplicates. -- Otherwise, `header.VRank` MUST be `nil` or `RLPEncode(cfReport(N))`. `cfReport(N)` MUST contain at most one entry per candidate ID, and 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)