Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 22 additions & 16 deletions KIPs/kip-227.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)`).

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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

Expand Down
Loading