Commit 5d64e8d
committed
feat(iota-core): skip scoreboard.update_scores on no-report commits (#11562)
# Description of change
Skip the per-commit `Scoreboard::update_scores(&report_aggregator)`
recompute when no `MisbehaviorReport` was processed in the commit.
Stacks on top of #11561 (which introduces
`ConsensusCommitOutput.report_state_snapshots` — the signal we gate on).
## How the gate works
- `ConsensusCommitOutput::has_report_state_changes()` returns `true` iff
at least one `process_report` ran during this commit (i.e.
`report_state_snapshots` is non-empty).
- In `process_consensus_transactions_and_commit_boundary`, the existing
call becomes:
```rust
if self.protocol_config().calculate_validator_scores() &&
output.has_report_state_changes() {
self.scoreboard.update_scores(&self.report_aggregator);
}
```
## Why this is safe (cross-validator determinism)
- `Scoreboard::update_scores` is a pure function of `(voting_power,
aggregator.reporters_with_voting_power())`. Same inputs → same output
across validators.
- All validators see the same `report_state_snapshots` emptiness
(consensus is deterministic), so all skip / run in lock-step.
- When all skip, `current_scores` retains its prior `Arc<Vec<u64>>` —
same across validators because the previous `update_scores` ran on the
same aggregator state.
- `invalid_reports_count` bumps don't affect scores
(`reporters_with_voting_power` reads only `received_metrics`), so
verify-time bumps that don't mark dirty are also score-irrelevant.
## Startup bootstrap
A freshly constructed `Scoreboard` starts at `[MAX_SCORE;
committee_size]`, but a never-restarted peer's reflects accumulated
updates. Without a fix, the next no-report commit would skip on both,
diverging the published vector. So `AuthorityPerEpochStore::new` now
calls `scoreboard.update_scores(&report_aggregator)` once, immediately
after `restore_from_tables`. If the restored aggregator is empty (fresh
epoch), `update_scores` returns `None` and leaves the default — correct
for the fresh-epoch path too.
## How the change has been tested
- [x] Basic tests (linting, compilation, formatting, unit/integration
tests)
- [x] Patch-specific tests (correctness, functionality coverage)
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] I have checked that new and existing unit tests pass locally with
my changes
Local verification:
- `cargo ci-clippy` — clean.
- `IOTA_SKIP_SIMTESTS=1 cargo nextest run -p iota-core --lib` — 564
tests pass.
- New tests in `scorer.rs`:
- `test_skipped_update_leaves_current_scores_unchanged` — proves the
skip path preserves `current_scores` bit-for-bit.
- `test_bootstrap_from_restored_aggregator_matches_live_path` — proves a
restored-and-bootstrapped scoreboard publishes the same vector as a
never-restarted peer's, given the same aggregator state.
## Links to any relevant issues
fixes iotaledger/iota-private#4081 parent d385ff5 commit 5d64e8d
2 files changed
Lines changed: 25 additions & 1 deletion
Lines changed: 18 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1118 | 1118 | | |
1119 | 1119 | | |
1120 | 1120 | | |
| 1121 | + | |
| 1122 | + | |
| 1123 | + | |
| 1124 | + | |
| 1125 | + | |
| 1126 | + | |
| 1127 | + | |
| 1128 | + | |
| 1129 | + | |
| 1130 | + | |
1121 | 1131 | | |
1122 | 1132 | | |
1123 | 1133 | | |
| |||
3247 | 3257 | | |
3248 | 3258 | | |
3249 | 3259 | | |
3250 | | - | |
| 3260 | + | |
| 3261 | + | |
| 3262 | + | |
| 3263 | + | |
| 3264 | + | |
| 3265 | + | |
| 3266 | + | |
| 3267 | + | |
3251 | 3268 | | |
3252 | 3269 | | |
3253 | 3270 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
152 | 152 | | |
153 | 153 | | |
154 | 154 | | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
155 | 162 | | |
156 | 163 | | |
157 | 164 | | |
| |||
0 commit comments