You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
❌ Patch coverage is 99.71347% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 80.04%. Comparing base (df6a785) to head (88b1cd4).
Voted / Unvoted are not symmetric for vote switches
Flagging this now because the new ContractEvent[] stream is the first surface whose correctness depends on the logs being individually complete, and it's much cheaper to settle the shape before anything builds against it.
The gap.ConsensusV1.vote() handles a switch by calling the internal _unvote(), which emits nothing:
function vote(addressaddr) external {
...
if (voter.validator !=address(0)) {
_unvote(); // :300 — internal, emits nothing
}
...
emitVoted(msg.sender, addr); // :318
}
function unvote() external {
emitUnvoted(msg.sender, _unvote()); // :322 — the only `Unvoted` emit site
}
_unvote() (ConsensusV1.sol:636) unlinks the voter, decrements voteBalance / votersCount on the old validator, and deletes the vote — all silently. So a switch A→B produces exactly one log, Voted(voter, B), and no Unvoted(voter, A). The removal from A happened in state but is invisible in the stream. (The other emit Voted at :200 is owner-only addVote, which reverts on AlreadyVoted, so it can't switch.)
Why this is new. The existing consumer is the AccountUpdate fold in collect_dirty_accounts_and_events, which stores final state keyed by voter (account.vote = Some(validator); account.unvote = None) and is written out per-account by api-sync/source/service.ts:315. Last-write-wins per voter is correct no matter what the intermediate logs looked like, so the missing Unvoted was never observable. The ordered per-tx ContractEvent[] now published on block.applied is a log stream, not a fold — different contract with the consumer.
How bad. Not unconditionally broken: Voted carries the voter, so a consumer keeping a voter → validator map can derive the removal itself (look up the voter's current validator, decrement, apply the new one). It breaks for a consumer that maintains only a validator → voters index off the stream, or that reads Voted/Unvoted as symmetric add/remove deltas — which is the natural reading of a pair named that way.
What makes it worth fixing rather than shrugging at is that this PR ships the counter-example: UsernameRegistered(addr, username, previousUsername) (UsernamesV1.sol:12) does carry prior state, so a re-registration is self-describing. Voted doesn't. A consumer will reasonably generalize from one to the other.
Nothing in-repo consumes the stream yet — this PR only adds the block.applied payload — so there's no live bug. Options, in rough order of preference:
Emit Unvoted(msg.sender, oldValidator) before Voted in vote(). Contract change: needs a milestone/upgrade path and changes existing logs.
Add previousValidator to Voted, mirroring UsernameRegistered. Same upgrade cost, symmetric with the username design.
Synthesize the Unvoted in the Rust collector — but collect_dirty_accounts_and_events doesn't have the prior validator either, so this only works if pre-state is threaded in.
Minimum: document on Contracts.Evm.ContractEvent that Voted supersedes any prior vote and that no Unvoted accompanies a switch, so consumers key by voter rather than treating the pair as deltas.
@KovacZan Addressed, thanks. I went with the first option: vote() now emits Unvoted for the previous validator before Voted for the new one, so switching a vote is self-describing in the event stream. The emit moved into _unvote(), which unvote() also uses, so plain unvotes behave as before.
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
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
Checklist