Skip to content
Merged
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
45 changes: 24 additions & 21 deletions KIPs/kip-227.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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

Expand All @@ -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:

Expand All @@ -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

Expand All @@ -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)
Expand Down Expand Up @@ -234,7 +237,7 @@ With `F = 3`, we discard the highest 3 reporter totals per candidate and sum the
<tr><td><strong>C5</strong></td><td>71</td><td>34</td><td>62</td><td>5</td><td>11</td><td>20</td><td>18</td><td>30</td><td>19</td><td>13</td><td>283</td><td><strong>116</strong></td><td>exclude reports from P1,P2,P3</td></tr>
</table>

_Note: Each `cfReport(N)` is in the header of block `N` and reports on target block `N - 1`._
_Note: Each `cfReport(N)` is in the header of block `N` and reports on block `T` — the proposer's own most recent prior proposal (`GetProposer(T) == GetProposer(N)`)._

### Score thresholds

Expand Down
Loading