feat(blockchain): add validator evidence submission and round aggregation - #47
Merged
Conversation
…tion Third slice of #29/ADR-011, after the validator registry (#43) and validator-gated origins (#46). This is the scoring pipeline itself. pallet-network-validator gains two extrinsics (ADR-011 §3/§5): - submit_evidence(provider, round, dimension, score_bps, sample_count, payload_hash): one attributable integer observation per validator. Rejects non-active validators, self-scoring (validator == provider), duplicate submissions for the same (provider, round, dimension), out-of-range scores (basis points, 0..=10_000), zero-sample claims, and anything arriving after the round closed. Bounded by MaxSubmissionsPerRound so consensus state stays bounded; only the integer summary is stored, with payload_hash addressing the full evidence off-chain. - close_round(provider, round, dimension): requires MinQuorum independent submissions, aggregates with an integer trimmed mean (drop one lowest + one highest when >= 3 submissions), stores the result with submissions/committee_target/closed_at so readers can compute confidence and spot degraded quorum, then pushes the score into reputation. Idempotent: a closed round can neither re-close nor accept late evidence. pallet-reputation gains set_dimension_score(), a non-extrinsic entry point that sets one vector component from a basis-points score and recomputes global. It keeps pallet-reputation the only writer of ReputationVectors, still enforcing MaxScore, regardless of which pallet triggered the update. Neither pallet depends on the other: each declares its own dimension enum and the runtime's new ScoringReputationUpdater maps between them, matching the existing ProviderInspector/ActiveValidatorLookup pattern. All arithmetic is integer and checked/saturating -- no floats, no unbounded loops (aggregation iterates a BoundedVec). Adds 10 scoring tests: inactive/suspended validator rejected, self-scoring rejected, duplicate/out-of-range/zero-sample rejected, sub-quorum round refuses to close and pushes nothing to reputation, outliers trimmed (a 0 and a 10_000 among three 6_000s yields exactly 6_000, where a plain mean would give 5_200), minimum-quorum committee trims to the median, closed round rejects late evidence and double close with exactly one reputation update, per-round submission bound enforced. Verified (blockchain/): cargo fmt --all -- --check; SKIP_WASM_BUILD=1 cargo clippy --workspace --exclude openinfra-node --all-targets -- -D warnings; SKIP_WASM_BUILD=1 cargo test --workspace --exclude openinfra-node (network-validator 21/21, reputation 11/11, availability 11/11, runtime 4/4, full suite green). Still open in #29: deterministic committee assignment (who is expected to score whom in a round), dispute_round, validator reward/penalty accrual, and dashboard validator views. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
flo2517
added a commit
that referenced
this pull request
Aug 6, 2026
Fourth slice of #29/ADR-011 §1, closing a real gap in the scoring pipeline landed in #47: with open submission, a Sybil cluster holding several bonded validator accounts could fill every slot in a round and control the trimmed mean despite quorum, because slots were self-selected on a first-come basis. Slots are now assigned. Pallet::committee(provider, round) draws TargetCommitteeSize distinct validators from a new bounded, enumerable ActiveValidatorSet using blake2_256((provider, round, nth)) reduced modulo the remaining candidates, without replacement, always filtering out the provider itself. submit_evidence rejects anyone outside that committee with NotAssignedToRound. ActiveValidatorSet is maintained on every status transition: joined on register/reinstate, left on request_exit/suspend/withdraw_unbonded, so suspended and exiting validators stop receiving assignments. Removal is order-preserving (not swap_remove) because selection indexes into the set -- a swap would silently reshuffle assignments for unrelated providers. Growth is bounded by MaxValidators (TooManyValidators). Known limitation, documented in the code rather than left implicit: the dev chain runs Aura, which offers no VRF, so this assignment is publicly computable in advance and a validator can predict which providers it will score. Security rests on quorum, outlier trimming and bonded stake rather than on assignment secrecy -- exactly the tradeoff ADR-011 §1 already accepted. Unpredictable per-round entropy is tracked as follow-up. Tests: 25 in this pallet (up from 21). New coverage -- committee is deterministic, exactly TargetCommitteeSize, distinct, excludes the provider even when the provider is itself a registered validator, and differs across rounds; shrinks gracefully below target when few validators exist; suspended/exiting validators leave the pool and reinstatement restores them; an unassigned-but-active validator is rejected; the active set is bounded. Existing scoring tests now derive their submitters from the real committee instead of arbitrary accounts. Verified (blockchain/): cargo fmt --all -- --check; SKIP_WASM_BUILD=1 cargo clippy --workspace --exclude openinfra-node --all-targets -- -D warnings; SKIP_WASM_BUILD=1 cargo test --workspace --exclude openinfra-node (73 tests, full suite green, incl. the runtime call-index stability test). Co-authored-by: FlorianJeandenans <florian.jeandenans@skin-soft.org> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
flo2517
added a commit
that referenced
this pull request
Aug 6, 2026
…#49) Fifth slice of #29/ADR-011 §5, completing the on-chain scoring lifecycle after the registry (#43), gated origins (#46), evidence aggregation (#47) and committee assignment (#48). dispute_round(provider, round, dimension): callable by the scored provider or by any validator that sat on that round's committee, within DisputeWindow blocks of closing. It immediately rolls the dimension back to the value it held before the round applied -- a contested score must not keep influencing scheduling while it is unresolved -- and marks the round Disputed. resolve_dispute(provider, round, dimension, uphold): SuspensionOrigin- gated, since ADR-011 defers full on-chain adjudication. Upholding keeps the rollback (DisputeUpheld); rejecting re-applies the round's aggregate (DisputeRejected). Making the rollback exact rather than approximate required knowing the pre-round value, so close_round now captures it via a new ReputationUpdater::dimension_score reader and stores it on the round as previous_score_bps. pallet-reputation gains dimension_score_bps(), inverting set_dimension_score's scaling; the round trip is exact whenever MaxScore divides 10_000 evenly (it is 1_000 in the runtime, a 10 bps step) and otherwise deterministically truncating -- never node-dependent. RoundResult also gains an explicit RoundStatus (Final/Disputed/DisputeUpheld/DisputeRejected) so a reader can never mistake a contested score for an accepted one. Adds 8 dispute tests: rollback restores the exact pre-round value established by an earlier round; only the provider or a committee member may dispute (an active-but-unassigned validator is rejected); the window is enforced; a round cannot be disputed twice; disputing an unknown round fails; upholding keeps the rollback; rejecting re-applies the aggregate; resolution requires governance and an actual dispute. 33 tests in this pallet, 81 across the workspace. Verified (blockchain/): cargo fmt --all -- --check; SKIP_WASM_BUILD=1 cargo clippy --workspace --exclude openinfra-node --all-targets -- -D warnings; SKIP_WASM_BUILD=1 cargo test --workspace --exclude openinfra-node. Co-authored-by: FlorianJeandenans <florian.jeandenans@skin-soft.org> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
flo2517
pushed a commit
that referenced
this pull request
Aug 6, 2026
…ssions Sixth slice of #29/ADR-011 §5, completing the incentives bullet after the registry (#43), gated origins (#46), aggregation (#47), committee assignment (#48) and disputes (#49). When a round closes, every submission that survived outlier trimming earns PointsPerAcceptedSubmission Reward Points; the trimmed high and low earn nothing for that round. Honest reporting is therefore the paying strategy, and a validator that is consistently an outlier -- whether colluding or merely malfunctioning -- is paid nothing without needing a separate detection mechanism. Trimming had to become attributable to do this: the old trimmed_mean sorted bare scores and lost track of who submitted what. It is now trimmed(), returning the surviving submissions alongside their mean, sorted by (score_bps, validator) rather than score alone -- with tied scores that makes the choice of which entry gets trimmed total-ordered and therefore identical on every node, which a score-only sort did not guarantee. pallet-rewards gains accrue_points(), a non-extrinsic entry point, so it stays the only writer of RewardBalances and keeps its own overflow checking regardless of caller. Validators claim through the existing signed claim_reward path -- balances are keyed by account, so providers and validators share it without a second store. The new ValidatorRewards trait keeps the two pallets decoupled, wired by the runtime's ValidatorRewardsBridge, matching the existing ReputationUpdater/ProviderInspector pattern. RoundClosed now reports a 'rewarded' count, so an observer can see how many submissions actually counted without replaying the trim. Known gap, documented at the accrual site rather than left implicit: crediting is one-way -- an upheld dispute does not claw back points already accrued for that round. Clawback belongs with slashing economics, which ADR-011 explicitly leaves out of scope. Adds 3 tests (36 in this pallet, 84 workspace-wide): only survivors of trimming are paid and both outliers get zero; RoundClosed's rewarded count matches the payouts; all-tied scores trim deterministically by validator id, paying exactly the middle three. Verified (blockchain/): cargo fmt --all -- --check; SKIP_WASM_BUILD=1 cargo clippy --workspace --exclude openinfra-node --all-targets -- -D warnings; SKIP_WASM_BUILD=1 cargo test --workspace --exclude openinfra-node. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
flo2517
added a commit
that referenced
this pull request
Aug 6, 2026
…ssions (#64) Sixth slice of #29/ADR-011 §5, completing the incentives bullet after the registry (#43), gated origins (#46), aggregation (#47), committee assignment (#48) and disputes (#49). When a round closes, every submission that survived outlier trimming earns PointsPerAcceptedSubmission Reward Points; the trimmed high and low earn nothing for that round. Honest reporting is therefore the paying strategy, and a validator that is consistently an outlier -- whether colluding or merely malfunctioning -- is paid nothing without needing a separate detection mechanism. Trimming had to become attributable to do this: the old trimmed_mean sorted bare scores and lost track of who submitted what. It is now trimmed(), returning the surviving submissions alongside their mean, sorted by (score_bps, validator) rather than score alone -- with tied scores that makes the choice of which entry gets trimmed total-ordered and therefore identical on every node, which a score-only sort did not guarantee. pallet-rewards gains accrue_points(), a non-extrinsic entry point, so it stays the only writer of RewardBalances and keeps its own overflow checking regardless of caller. Validators claim through the existing signed claim_reward path -- balances are keyed by account, so providers and validators share it without a second store. The new ValidatorRewards trait keeps the two pallets decoupled, wired by the runtime's ValidatorRewardsBridge, matching the existing ReputationUpdater/ProviderInspector pattern. RoundClosed now reports a 'rewarded' count, so an observer can see how many submissions actually counted without replaying the trim. Known gap, documented at the accrual site rather than left implicit: crediting is one-way -- an upheld dispute does not claw back points already accrued for that round. Clawback belongs with slashing economics, which ADR-011 explicitly leaves out of scope. Adds 3 tests (36 in this pallet, 84 workspace-wide): only survivors of trimming are paid and both outliers get zero; RoundClosed's rewarded count matches the payouts; all-tied scores trim deterministically by validator id, paying exactly the middle three. Verified (blockchain/): cargo fmt --all -- --check; SKIP_WASM_BUILD=1 cargo clippy --workspace --exclude openinfra-node --all-targets -- -D warnings; SKIP_WASM_BUILD=1 cargo test --workspace --exclude openinfra-node. Co-authored-by: FlorianJeandenans <florian.jeandenans@skin-soft.org> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Third slice of #29 / ADR-011, after the validator registry (#43) and validator-gated origins (#46). This is the scoring pipeline itself.
pallet-network-validator: two new extrinsicssubmit_evidence(provider, round, dimension, score_bps, sample_count, payload_hash)— one attributable integer observation per validator. Rejects:validator == provider) — the cheapest possible self-dealing, per ADR-011 §4(provider, round, dimension)— replay resistance0..=10_000) and zero-sample claimsBounded by
MaxSubmissionsPerRoundso consensus state stays bounded. Only the integer summary is stored;payload_hashaddresses the full evidence off-chain.close_round(provider, round, dimension)— requiresMinQuorumindependent submissions, aggregates with an integer trimmed mean (drop one lowest + one highest when ≥3 submissions), and storessubmissions/committee_target/closed_atso readers can compute confidence and spot degraded quorum rather than seeing false success. Then pushes the score into reputation. Idempotent: a closed round can neither re-close nor accept late evidence.pallet-reputation:set_dimension_score()A non-extrinsic entry point that sets one vector component from a basis-points score and recomputes
global. Keepspallet-reputationthe only writer ofReputationVectors, still enforcing its ownMaxScore, regardless of which pallet triggered the update (ADR-011 §5).Decoupling
Neither pallet depends on the other: each declares its own dimension enum, and the runtime's new
ScoringReputationUpdatermaps between them — matching the existingProviderInspector/ActiveValidatorLookuppattern in this codebase.All arithmetic is integer and checked/saturating — no floats, no unbounded loops (aggregation iterates a
BoundedVec).Tests (10 new, 21 total in this pallet)
Inactive/suspended validator rejected · self-scoring rejected · duplicate / out-of-range / zero-sample rejected · sub-quorum round refuses to close and pushes nothing to reputation · outliers trimmed (a
0and a10_000among three6_000s yields exactly6_000, where a plain mean would give5_200) · minimum-quorum committee trims to the median (at quorum, one dishonest validator cannot shift the result at all) · closed round rejects late evidence and double-close with exactly one reputation update · per-round submission bound enforced.Verification
network-validator 21/21, reputation 11/11, availability 11/11, runtime 4/4 (incl. call-index stability), full suite green.
Still open in #29
Deterministic committee assignment (who is expected to score whom in a round),
dispute_round, validator reward/penalty accrual, and dashboard validator views.🤖 Generated with Claude Code