Skip to content

Backport release/v6.3: fix to ProposalPOLMessage poisoning (CON-222)#3138

Draft
seidroid[bot] wants to merge 1 commit intorelease/v6.3from
backport-3129-to-release/v6.3
Draft

Backport release/v6.3: fix to ProposalPOLMessage poisoning (CON-222)#3138
seidroid[bot] wants to merge 1 commit intorelease/v6.3from
backport-3129-to-release/v6.3

Conversation

@seidroid
Copy link
Copy Markdown

@seidroid seidroid bot commented Mar 30, 2026

Backport of #3129 to release/v6.3.

@seidroid seidroid bot added the backport label Mar 30, 2026
@seidroid
Copy link
Copy Markdown
Author

seidroid bot commented Mar 30, 2026

Please cherry-pick the changes locally and resolve any conflicts.

git fetch origin backport-3129-to-release/v6.3
git worktree add --checkout .worktree/backport-3129-to-release/v6.3 backport-3129-to-release/v6.3
cd .worktree/backport-3129-to-release/v6.3
git reset --hard HEAD^
git cherry-pick -x d201e89ad5f867eea4e275b6d4ec5b80e5cf6afc
git push --force-with-lease

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 30, 2026

The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedMar 30, 2026, 3:41 PM

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 30, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 44.73%. Comparing base (b84fd97) to head (a8d4ee2).

Additional details and impacted files

Impacted file tree graph

@@               Coverage Diff                @@
##           release/v6.3    #3138      +/-   ##
================================================
- Coverage         46.02%   44.73%   -1.30%     
================================================
  Files              1199       75    -1124     
  Lines            104568     8146   -96422     
================================================
- Hits              48130     3644   -44486     
+ Misses            52200     4088   -48112     
+ Partials           4238      414    -3824     
Flag Coverage Δ
sei-chain ?
sei-db 45.73% <ø> (+0.04%) ⬆️
sei-ibc-go ?
sei-tendermint ?
sei-wasmd ?
sei-wasmvm 39.88% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.
see 1127 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

return vote, true
}
=======
return votes.GetByIndex(int32(index)) //nolint:gosec // index is bounded by validator set size which fits in int32

Check warning

Code scanning / CodeQL

Unreachable statement Warning

This statement is unreachable.

Copilot Autofix

AI 7 days ago

To fix this, we should resolve the bad merge in PickVoteToSend by ensuring there is exactly one return path inside the if index, ok := ...; ok block that both matches the function’s return signature and is reachable. The simplest way that preserves existing behavior is to keep the HEAD logic (which already returns (*types.Vote, bool) and checks for nil) and delete the unreachable return votes.GetByIndex(int32(index)) line (and its conflict markers). This maintains the previous semantics: pick a random index, get the vote, return (vote, true) if non-nil, otherwise fall through and eventually return (nil, false).

Concretely, in sei-tendermint/internal/consensus/peer_state.go, within func (ps *PeerState) PickVoteToSend, replace the whole conflict-marked region (lines 195–203) with the HEAD implementation only:

  • After if index, ok := votes.BitArray().Sub(psVotes).PickRandom(); ok {, keep:
    • vote := votes.GetByIndex(int32(index))
    • if vote != nil { return vote, true }
  • Remove the merge markers <<<<<<< HEAD, =======, >>>>>>> ... and the unreachable return votes.GetByIndex(int32(index)) line.

No additional imports or helper methods are necessary.

Suggested changeset 1
sei-tendermint/internal/consensus/peer_state.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/sei-tendermint/internal/consensus/peer_state.go b/sei-tendermint/internal/consensus/peer_state.go
--- a/sei-tendermint/internal/consensus/peer_state.go
+++ b/sei-tendermint/internal/consensus/peer_state.go
@@ -193,14 +193,10 @@
 	}
 
 	if index, ok := votes.BitArray().Sub(psVotes).PickRandom(); ok {
-<<<<<<< HEAD
 		vote := votes.GetByIndex(int32(index))
 		if vote != nil {
 			return vote, true
 		}
-=======
-		return votes.GetByIndex(int32(index)) //nolint:gosec // index is bounded by validator set size which fits in int32
->>>>>>> d201e89 (fix to ProposalPOLMessage poisoning (CON-222) (#3129))
 	}
 	return nil, false
 }
EOF
@@ -193,14 +193,10 @@
}

if index, ok := votes.BitArray().Sub(psVotes).PickRandom(); ok {
<<<<<<< HEAD
vote := votes.GetByIndex(int32(index))
if vote != nil {
return vote, true
}
=======
return votes.GetByIndex(int32(index)) //nolint:gosec // index is bounded by validator set size which fits in int32
>>>>>>> d201e89 (fix to ProposalPOLMessage poisoning (CON-222) (#3129))
}
return nil, false
}
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +140 to +142
if ok {
return utils.Some(val)
}

Check warning

Code scanning / CodeQL

Unreachable statement Warning

This statement is unreachable.

Copilot Autofix

AI 7 days ago

In general, unreachable code created by merge conflicts is fixed by resolving the conflict and retaining a single, consistent control-flow path with no dead branches after unconditional returns. Here we must choose and keep one implementation of validatorAtHeight’s final lookup in valsWithH and delete the other, removing the merge markers as well.

The best fix that preserves existing functionality and matches the current Tendermint API is to keep the simpler HEAD version:

_, val := valsWithH.GetByAddress(privValAddress)
return val

and delete the alternative branch that expects an ok flag and uses utils.Some / none, along with the conflict markers. This is because: (1) elsewhere in Tendermint, ValidatorSet.GetByAddress commonly returns (index int, val *Validator) without ok; (2) we have no context or imports here for utils.Some or none, so introducing them would change functionality and dependencies; and (3) the rest of the function already returns *types.Validator or nil, matching the HEAD version’s behavior.

Concretely, in sei-tendermint/internal/rpc/core/status.go, in validatorAtHeight, replace the entire conflict block from line 135 through 144 (inclusive) with the two-line lookup and return:

_, val := valsWithH.GetByAddress(privValAddress)
return val

No new imports, methods, or definitions are required.

Suggested changeset 1
sei-tendermint/internal/rpc/core/status.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/sei-tendermint/internal/rpc/core/status.go b/sei-tendermint/internal/rpc/core/status.go
--- a/sei-tendermint/internal/rpc/core/status.go
+++ b/sei-tendermint/internal/rpc/core/status.go
@@ -132,14 +132,6 @@
 		}
 	}
 
-<<<<<<< HEAD
 	_, val := valsWithH.GetByAddress(privValAddress)
 	return val
-=======
-	_, val, ok := valsWithH.GetByAddress(privValAddress)
-	if ok {
-		return utils.Some(val)
-	}
-	return none
->>>>>>> d201e89 (fix to ProposalPOLMessage poisoning (CON-222) (#3129))
 }
EOF
@@ -132,14 +132,6 @@
}
}

<<<<<<< HEAD
_, val := valsWithH.GetByAddress(privValAddress)
return val
=======
_, val, ok := valsWithH.GetByAddress(privValAddress)
if ok {
return utils.Some(val)
}
return none
>>>>>>> d201e89 (fix to ProposalPOLMessage poisoning (CON-222) (#3129))
}
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +965 to +967
if !ok {
panic(fmt.Errorf("too many signatures"))
}

Check warning

Code scanning / CodeQL

Unreachable statement Warning

This statement is unreachable.

Copilot Autofix

AI 7 days ago

In general, the problem should be fixed by resolving the merge conflict in Commit.ToVoteSet and keeping a single, consistent use of commit.GetVote. The safer, more robust variant is the one that captures both vote and ok and panics if ok is false; this preserves the intended functionality of guarding against too many signatures and ensures that all statements in the function are reachable.

Concretely, in sei-tendermint/types/block.go, in the ToVoteSet method, remove all merge conflict markers (<<<<<<< HEAD, =======, >>>>>>> ...) and the old single-value assignment vote := commit.GetVote(int32(idx)). Keep only the two-value assignment and the if !ok guard:

  • Replace the entire conflict region (lines 961–968 in the snippet) with:
    vote, ok := commit.GetVote(int32(idx)) //nolint:gosec // idx is bounded by commit.Signatures length which fits in int32
    if !ok {
        panic(fmt.Errorf("too many signatures"))
    }

No additional imports or new methods are needed; fmt is already imported and GetVote is assumed to be defined elsewhere in this file or package. This change does not alter the external behavior beyond ensuring the intended safety check is active, and it removes the unreachable-statement situation that CodeQL flagged.

Suggested changeset 1
sei-tendermint/types/block.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/sei-tendermint/types/block.go b/sei-tendermint/types/block.go
--- a/sei-tendermint/types/block.go
+++ b/sei-tendermint/types/block.go
@@ -958,14 +958,10 @@
 		if cs.BlockIDFlag == BlockIDFlagAbsent {
 			continue // OK, some precommits can be missing.
 		}
-<<<<<<< HEAD
-		vote := commit.GetVote(int32(idx))
-=======
 		vote, ok := commit.GetVote(int32(idx)) //nolint:gosec // idx is bounded by commit.Signatures length which fits in int32
 		if !ok {
 			panic(fmt.Errorf("too many signatures"))
 		}
->>>>>>> d201e89 (fix to ProposalPOLMessage poisoning (CON-222) (#3129))
 		if err := vote.ValidateBasic(); err != nil {
 			panic(fmt.Errorf("failed to validate vote reconstructed from commit: %w", err))
 		}
EOF
@@ -958,14 +958,10 @@
if cs.BlockIDFlag == BlockIDFlagAbsent {
continue // OK, some precommits can be missing.
}
<<<<<<< HEAD
vote := commit.GetVote(int32(idx))
=======
vote, ok := commit.GetVote(int32(idx)) //nolint:gosec // idx is bounded by commit.Signatures length which fits in int32
if !ok {
panic(fmt.Errorf("too many signatures"))
}
>>>>>>> d201e89 (fix to ProposalPOLMessage poisoning (CON-222) (#3129))
if err := vote.ValidateBasic(); err != nil {
panic(fmt.Errorf("failed to validate vote reconstructed from commit: %w", err))
}
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +213 to +215
if !ok {
panic("VoteSignBytes() failed unexpectedly")
}

Check warning

Code scanning / CodeQL

Unreachable statement Warning

This statement is unreachable.

Copilot Autofix

AI 7 days ago

In general, the problem is caused by an unresolved merge conflict where one branch used voteSignBytes := commit.VoteSignBytes(...) (single return value) and another used voteSignBytes, ok := commit.VoteSignBytes(...); if !ok { panic(...) }. The presence of conflict markers and two competing implementations leads CodeQL to see one of the branches as unreachable or dead. To fix this without changing functionality, we must resolve the conflict by choosing the correct implementation and removing the other, ensuring that VoteSignBytes is called with its correct signature and that the ok check is reachable.

The best fix here is to keep the safer, more recent implementation that handles the two return values: replace the entire conflict region (lines 209–216) with a single, clean block:

voteSignBytes, ok := commit.VoteSignBytes(chainID, int32(idx)) //nolint:gosec // idx is bounded by len(commit.Signatures) which is validated against validator set size
if !ok {
    panic("VoteSignBytes() failed unexpectedly")
}

This removes the <<<<<<<, =======, and >>>>>>> markers, and removes the older single‑value assignment. It also keeps the semantics implied by the comment: idx is safe, and VoteSignBytes is not expected to fail; if it does, we panic. No additional imports, methods, or definitions are required, since fmt and other imports are already present, and panic is a built‑in. This change restores reachability and keeps the intended behavior of the batch verification logic.

Suggested changeset 1
sei-tendermint/types/validation.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/sei-tendermint/types/validation.go b/sei-tendermint/types/validation.go
--- a/sei-tendermint/types/validation.go
+++ b/sei-tendermint/types/validation.go
@@ -206,14 +206,10 @@
 		}
 
 		// Validate signature.
-<<<<<<< HEAD
-		voteSignBytes := commit.VoteSignBytes(chainID, int32(idx))
-=======
 		voteSignBytes, ok := commit.VoteSignBytes(chainID, int32(idx)) //nolint:gosec // idx is bounded by len(commit.Signatures) which is validated against validator set size
 		if !ok {
 			panic("VoteSignBytes() failed unexpectedly")
 		}
->>>>>>> d201e89 (fix to ProposalPOLMessage poisoning (CON-222) (#3129))
 
 		// add the key, sig and message to the verifier
 		if err := bv.Add(val.PubKey, voteSignBytes, commitSig.Signature); err != nil {
EOF
@@ -206,14 +206,10 @@
}

// Validate signature.
<<<<<<< HEAD
voteSignBytes := commit.VoteSignBytes(chainID, int32(idx))
=======
voteSignBytes, ok := commit.VoteSignBytes(chainID, int32(idx)) //nolint:gosec // idx is bounded by len(commit.Signatures) which is validated against validator set size
if !ok {
panic("VoteSignBytes() failed unexpectedly")
}
>>>>>>> d201e89 (fix to ProposalPOLMessage poisoning (CON-222) (#3129))

// add the key, sig and message to the verifier
if err := bv.Add(val.PubKey, voteSignBytes, commitSig.Signature); err != nil {
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +326 to +328
if !ok {
panic("VoteSignBytes() failed unexpectedly")
}

Check warning

Code scanning / CodeQL

Unreachable statement Warning

This statement is unreachable.

Copilot Autofix

AI 7 days ago

In general, the problem arises because the file still contains both sides of a merge conflict: an old implementation using VoteSignBytes as a single‑value function and a new implementation where VoteSignBytes returns (bytes, ok) plus more detailed signature handling. This leaves the control flow ill‑defined and can cause the static analyzer to treat parts of the newer code (including if !ok { ... }) as unreachable or otherwise inconsistent. The correct fix is to resolve the conflict by deleting the obsolete branch and conflict markers, leaving a single coherent implementation.

Concretely, in sei-tendermint/types/validation.go within verifyCommitSingle, you should remove the <<<<<<< HEAD / ======= / >>>>>>> markers and the old block:

voteSignBytes = commit.VoteSignBytes(chainID, int32(idx))

if !val.PubKey.VerifySignature(voteSignBytes, commitSig.Signature) {
    return errBadSig{fmt.Errorf("wrong signature (#%d): %X", idx, commitSig.Signature)}
}

and keep the newer block that uses (voteSignBytes, ok), panics on failure, extracts the signature via commitSig.Signature.Get(), and verifies with val.PubKey.Verify. No new imports or helper methods are required; everything used in the new block is already referenced in the file. This preserves existing intended functionality (the more defensive verification logic) while eliminating the unreachable/duplicate code path and resolving the analyzer’s complaint.

Suggested changeset 1
sei-tendermint/types/validation.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/sei-tendermint/types/validation.go b/sei-tendermint/types/validation.go
--- a/sei-tendermint/types/validation.go
+++ b/sei-tendermint/types/validation.go
@@ -316,12 +316,6 @@
 			seenVals[valIdx] = idx
 		}
 
-<<<<<<< HEAD
-		voteSignBytes = commit.VoteSignBytes(chainID, int32(idx))
-
-		if !val.PubKey.VerifySignature(voteSignBytes, commitSig.Signature) {
-			return errBadSig{fmt.Errorf("wrong signature (#%d): %X", idx, commitSig.Signature)}
-=======
 		voteSignBytes, ok := commit.VoteSignBytes(chainID, int32(idx)) //nolint:gosec // idx is bounded by len(commit.Signatures) which is validated against validator set size
 		if !ok {
 			panic("VoteSignBytes() failed unexpectedly")
@@ -332,7 +326,6 @@
 		}
 		if err := val.PubKey.Verify(voteSignBytes, sig); err != nil {
 			return errBadSig{fmt.Errorf("wrong signature (#%d): %v", idx, sig)}
->>>>>>> d201e89 (fix to ProposalPOLMessage poisoning (CON-222) (#3129))
 		}
 
 		// If this signature counts then add the voting power of the validator
EOF
@@ -316,12 +316,6 @@
seenVals[valIdx] = idx
}

<<<<<<< HEAD
voteSignBytes = commit.VoteSignBytes(chainID, int32(idx))

if !val.PubKey.VerifySignature(voteSignBytes, commitSig.Signature) {
return errBadSig{fmt.Errorf("wrong signature (#%d): %X", idx, commitSig.Signature)}
=======
voteSignBytes, ok := commit.VoteSignBytes(chainID, int32(idx)) //nolint:gosec // idx is bounded by len(commit.Signatures) which is validated against validator set size
if !ok {
panic("VoteSignBytes() failed unexpectedly")
@@ -332,7 +326,6 @@
}
if err := val.PubKey.Verify(voteSignBytes, sig); err != nil {
return errBadSig{fmt.Errorf("wrong signature (#%d): %v", idx, sig)}
>>>>>>> d201e89 (fix to ProposalPOLMessage poisoning (CON-222) (#3129))
}

// If this signature counts then add the voting power of the validator
Copilot is powered by AI and may make mistakes. Always verify output.
<<<<<<< HEAD
return int32(idx), val.Copy()
=======
return int32(idx), val.Copy(), true //nolint:gosec // validator set size is consensus-bounded, fits in int32

Check warning

Code scanning / CodeQL

Unreachable statement Warning

This statement is unreachable.

Copilot Autofix

AI 7 days ago

In general, to fix an unreachable-statement issue caused by merge conflicts, you must resolve the conflict by removing the conflict markers and retaining only the logically correct, type-consistent branch. Any remaining returns must match the function’s declared return types and be reachable under the desired control flow.

For this specific case in sei-tendermint/types/validator_set.go, the function GetByAddress is declared to return (index int32, val *Validator, ok bool). The body currently contains merge conflict markers around two alternative returns. The HEAD variant returns only two values, which is invalid; the other variant returns three values including a boolean true flag and carries a useful comment (//nolint:gosec ...). The best fix that preserves existing functionality is to delete the conflict markers and the invalid two-value return, keep the three-value return, and leave the rest of the function unchanged. The final return 0, nil, false at the end of the function already matches the signature and should stay as-is. No new imports, methods, or definitions are required.

Concretely:

  • In GetByAddress, replace the entire conflict-marked region (lines 281–285) with a single return int32(idx), val.Copy(), true line (including the existing comment).
  • Ensure all conflict markers (<<<<<<<, =======, >>>>>>>) are removed so the file is valid Go code.
Suggested changeset 1
sei-tendermint/types/validator_set.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/sei-tendermint/types/validator_set.go b/sei-tendermint/types/validator_set.go
--- a/sei-tendermint/types/validator_set.go
+++ b/sei-tendermint/types/validator_set.go
@@ -278,11 +278,7 @@
 func (vals *ValidatorSet) GetByAddress(address []byte) (index int32, val *Validator, ok bool) {
 	for idx, val := range vals.Validators {
 		if bytes.Equal(val.Address, address) {
-<<<<<<< HEAD
-			return int32(idx), val.Copy()
-=======
 			return int32(idx), val.Copy(), true //nolint:gosec // validator set size is consensus-bounded, fits in int32
->>>>>>> d201e89 (fix to ProposalPOLMessage poisoning (CON-222) (#3129))
 		}
 	}
 	return 0, nil, false
EOF
@@ -278,11 +278,7 @@
func (vals *ValidatorSet) GetByAddress(address []byte) (index int32, val *Validator, ok bool) {
for idx, val := range vals.Validators {
if bytes.Equal(val.Address, address) {
<<<<<<< HEAD
return int32(idx), val.Copy()
=======
return int32(idx), val.Copy(), true //nolint:gosec // validator set size is consensus-bounded, fits in int32
>>>>>>> d201e89 (fix to ProposalPOLMessage poisoning (CON-222) (#3129))
}
}
return 0, nil, false
Copilot is powered by AI and may make mistakes. Always verify output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants