Skip to content

Comments

fix(network): add attestation height bounds validation#359

Open
alpe wants to merge 10 commits intomainfrom
security/attestation-height-bounds
Open

fix(network): add attestation height bounds validation#359
alpe wants to merge 10 commits intomainfrom
security/attestation-height-bounds

Conversation

@alpe
Copy link
Contributor

@alpe alpe commented Feb 24, 2026

Summary

The Attest handler previously accepted attestations for any height — including arbitrarily far-future blocks (MaxInt64) and ancient blocks. This enabled permissionless storage exhaustion.

Summary by CodeRabbit

  • Bug Fixes

    • Enforced attestation height bounds: reject attestations above current block height+1 and below the retention window. Bounds are validated before any attestation processing or storage.
  • Tests

    • Added comprehensive tests for future, next, current, stale, retention-boundary, and multi-epoch scenarios.
    • Added a staking mock to simulate validator state for tests.
  • Chores

    • Increased default retention parameter (PruneAfter) from 7 to 15 blocks.

The Attest handler previously accepted attestations for any height, allowing
attackers to spam storage with attestations for arbitrarily far-future blocks
(MaxInt64) or arbitrarily old blocks.

This commit adds height bounds validation:
- Upper bound: msg.Height <= ctx.BlockHeight() + 1
- Lower bound: msg.Height >= ctx.BlockHeight() - (PruneAfter * EpochLength)
- Lower bound floor of 1 for early chain heights

The retention window is derived from the existing PruneAfter and EpochLength
params, keeping it consistent with the pruning configuration.

Adds 7 table-driven test cases covering boundary conditions.
@alpe alpe requested a review from a team as a code owner February 24, 2026 08:35
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @alpe, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the security and stability of the network module by addressing a critical vulnerability related to attestation handling. It introduces necessary height bounds validation for attestations, effectively preventing malicious actors from exploiting the system through storage exhaustion attacks using future or stale block attestations. This change ensures that the system's storage footprint remains predictable and secure.

Highlights

  • Attestation Height Validation: Introduced robust validation for attestation heights in the Attest handler, preventing submission of attestations for arbitrarily future or ancient blocks.
  • Security Enhancement: Mitigated potential storage exhaustion attacks by bounding attestation heights, ensuring storage usage remains within manageable limits.
  • Comprehensive Testing: Implemented a new suite of 7 table-driven test cases to thoroughly verify the correctness of the attestation height bounds logic, covering various boundary conditions.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • modules/network/keeper/msg_server.go
    • Implemented logic to validate msg.Height against ctx.BlockHeight() + 1 (upper bound) and ctx.BlockHeight() - (PruneAfter × EpochLength) (lower bound), with a floor of 1.
    • Replaced a todo comment with the new validation logic.
  • modules/network/keeper/msg_server_test.go
    • Added TestAttestHeightBounds function with multiple sub-tests to cover various scenarios for attestation height validation, including accepted and rejected cases.
Activity
  • The author, alpe, has implemented new validation logic for attestation heights.
  • Comprehensive table-driven tests have been added to cover all boundary conditions for the new validation.
  • The PR description includes successful test output, indicating that all new tests are passing.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds pre-validation in Attest to enforce attestation height bounds: rejects attestations above current block height + 1 or below a retention-calculated minimum before any bitmap retrieval or signature storage.

Changes

Cohort / File(s) Summary
Attestation Height Bounds
modules/network/keeper/msg_server.go
Adds early bounds checks in Attest: computes minHeight from PruneAfter and EpochLength, sets maxFutureHeight = currentBlockHeight + 1, and returns InvalidRequest if msg.Height is outside these bounds before bitmap/signature operations.
Tests & Mock Staking Keeper
modules/network/keeper/msg_server_test.go
Adds TestAttestHeightBounds exercising multiple scenarios (future, two-ahead, current/next, stale, retention, early-chain edge). Introduces MockStakingKeeper and methods to simulate validators for test interactions with the Keeper.
Parameters Default
modules/network/types/params.go
Updates DefaultPruneAfter from 7 to 15 (affects retention window used in height bound computation).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Keeper as Keeper/Attest
    participant Chain as BlockContext/Params
    participant Storage as AttestationStore

    Client->>Keeper: Send Attest(msg{Height, Signatures})
    Keeper->>Chain: Query current block height, PruneAfter, EpochLength
    Chain-->>Keeper: Return height and params
    Keeper-->>Keeper: Compute minHeight and maxFutureHeight
    alt Height out of bounds
        Keeper-->>Client: Return InvalidRequest (height out of range)
    else Height within bounds
        Keeper->>Storage: Retrieve or create bitmap for Height/Epoch
        Storage-->>Keeper: Bitmap
        Keeper->>Storage: Set bit, store signature, update quorum/epoch bitmap
        Keeper-->>Client: Return success, emit events
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • julienrbrt

Poem

🐇
I hop through blocks with careful bounds,
Not too future, not too far behind,
I check each height, I mark each bit,
Safe attestations — neat and sound,
A rabbit keeps the ledger kind.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly describes the main change: adding attestation height bounds validation to prevent future-height and stale-attestation attacks.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch security/attestation-height-bounds

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
modules/network/keeper/msg_server.go (1)

33-52: GetParams is called twice within the same handler.

k.GetParams(ctx) is invoked at line 33 (for SignMode) and again at line 52. Hoisting a single params variable to the top of the handler eliminates the duplicate store read.

♻️ Proposed refactor
+	params := k.GetParams(ctx)
-	if k.GetParams(ctx).SignMode == types.SignMode_SIGN_MODE_CHECKPOINT &&
+	if params.SignMode == types.SignMode_SIGN_MODE_CHECKPOINT &&
 		!k.IsCheckpointHeight(ctx, msg.Height) {
 		return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, "height %d is not a checkpoint", msg.Height)
 	}
 	// ... existing attester-set and index checks ...

-	params := k.GetParams(ctx)
 	currentHeight := ctx.BlockHeight()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/network/keeper/msg_server.go` around lines 33 - 52, Handler calls
k.GetParams(ctx) twice (once for SignMode check and again later); hoist a single
params := k.GetParams(ctx) at the start of the handler and replace both
k.GetParams(ctx) usages with that local variable (used for SignMode comparison
and later param checks). Update references to SignMode (params.SignMode) and any
later uses of params so you do one store read instead of two.
modules/network/keeper/msg_server_test.go (2)

260-267: Non-deterministic ordering in GetLastValidators.

Iterating m.activeSet (a plain Go map) produces a non-deterministic slice order. Currently harmless because tests insert at most one validator, but any future multi-validator test could yield flaky results.

♻️ Proposed fix: sort consistently (mirrors the existing `GetAllValidators` pattern)
 func (m MockStakingKeeper) GetLastValidators(ctx context.Context) (validators []stakingtypes.Validator, err error) {
-	for _, validator := range m.activeSet {
-		if validator.IsBonded() {
-			validators = append(validators, validator)
-		}
-	}
-	return
+	for _, validator := range m.activeSet {
+		if validator.IsBonded() {
+			validators = append(validators, validator)
+		}
+	}
+	slices.SortFunc(validators, func(v1, v2 stakingtypes.Validator) int {
+		return strings.Compare(v1.OperatorAddress, v2.OperatorAddress)
+	})
+	return
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/network/keeper/msg_server_test.go` around lines 260 - 267,
GetLastValidators iterates the map m.activeSet which yields non-deterministic
ordering; change the implementation to collect bonded validators into a slice
and then sort that slice deterministically (same approach used by
GetAllValidators) before returning. Specifically, inside
MockStakingKeeper.GetLastValidators gather validators where IsBonded() is true
into a local slice and then sort the slice by a stable key such as
Validator.OperatorAddress (or the same field used by GetAllValidators) using
sort.Slice to ensure consistent order across runs.

127-182: No test for PruneAfter × EpochLength overflow in retentionWindow.

The table has no case with extreme PruneAfter/EpochLength values that would trigger the overflow described in msg_server.go line 55. Once that overflow is fixed, a regression test here would prevent silent reversion.

🧪 Suggested additional test case
+		"overflow pruning window treated as max retention": {
+			// PruneAfter * EpochLength > math.MaxInt64 → should not reject a valid current-height attestation
+			blockHeight: 100,
+			attestH:     100,
+			pruneAfter:  math.MaxUint64,
+			epochLength: 2,
+		},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/network/keeper/msg_server_test.go` around lines 127 - 182, Add a
regression test case to TestAttestHeightBounds that exercises the overflow path
in the retentionWindow calculation by supplying extreme values for pruneAfter
and epochLength whose multiplication would overflow uint64 (e.g., very large
pruneAfter or epochLength) and assert the handler returns the expected error
(sdkerrors.ErrInvalidRequest); locate the retention logic in the code symbol
retentionWindow (or the function in msg_server.go that computes pruneAfter *
epochLength) and add a table entry similar to other cases labeled like
"pruneAfter*epochLength overflow rejected" that sets blockHeight/attestH
appropriately and expects an error to prevent silent reversion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@modules/network/keeper/msg_server_test.go`:
- Line 204: The call to keeper.SetParams(ctx, params) is unchecked so a failure
would let the test continue with defaults; update the test to assert the call
succeeds by capturing and checking its error return (e.g. using require.NoError
or t.Fatalf) immediately after invoking SetParams on keeper with ctx and params
so the test fails fast if params could not be applied.

In `@modules/network/keeper/msg_server.go`:
- Around line 55-56: The multiplication params.PruneAfter * params.EpochLength
is done in uint64 and then cast to int64, which can overflow and produce a
negative retentionWindow; update the logic in the msg server where
retentionWindow and minHeight are computed to first detect overflow by checking
if params.PruneAfter > math.MaxInt64/params.EpochLength (or similarly cast both
to int64 and compare with MaxInt64), and if an overflow would occur return an
error (or cap to MaxInt64) rather than allowing a negative retentionWindow; then
compute retentionWindow as a signed int64 (e.g., int64(params.PruneAfter) *
int64(params.EpochLength)) and only compute minHeight := currentHeight -
retentionWindow when retentionWindow is non-negative to avoid flipping the
subtraction. Include references to retentionWindow, minHeight,
params.PruneAfter, params.EpochLength, currentHeight and UpdateParams in your
changes.

---

Nitpick comments:
In `@modules/network/keeper/msg_server_test.go`:
- Around line 260-267: GetLastValidators iterates the map m.activeSet which
yields non-deterministic ordering; change the implementation to collect bonded
validators into a slice and then sort that slice deterministically (same
approach used by GetAllValidators) before returning. Specifically, inside
MockStakingKeeper.GetLastValidators gather validators where IsBonded() is true
into a local slice and then sort the slice by a stable key such as
Validator.OperatorAddress (or the same field used by GetAllValidators) using
sort.Slice to ensure consistent order across runs.
- Around line 127-182: Add a regression test case to TestAttestHeightBounds that
exercises the overflow path in the retentionWindow calculation by supplying
extreme values for pruneAfter and epochLength whose multiplication would
overflow uint64 (e.g., very large pruneAfter or epochLength) and assert the
handler returns the expected error (sdkerrors.ErrInvalidRequest); locate the
retention logic in the code symbol retentionWindow (or the function in
msg_server.go that computes pruneAfter * epochLength) and add a table entry
similar to other cases labeled like "pruneAfter*epochLength overflow rejected"
that sets blockHeight/attestH appropriately and expects an error to prevent
silent reversion.

In `@modules/network/keeper/msg_server.go`:
- Around line 33-52: Handler calls k.GetParams(ctx) twice (once for SignMode
check and again later); hoist a single params := k.GetParams(ctx) at the start
of the handler and replace both k.GetParams(ctx) usages with that local variable
(used for SignMode comparison and later param checks). Update references to
SignMode (params.SignMode) and any later uses of params so you do one store read
instead of two.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ba94ff1 and 0fb3f8f.

📒 Files selected for processing (2)
  • modules/network/keeper/msg_server.go
  • modules/network/keeper/msg_server_test.go

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces important security validation for attestation height bounds, a great addition to prevent storage exhaustion attacks. However, a medium-severity integer overflow vulnerability was identified in the calculation of the retentionWindow. This could lead to a Denial of Service (DoS) for the Attest function if specific, large values are set for the PruneAfter and EpochLength parameters via governance. The accompanying tests are well-structured and cover the necessary boundary conditions effectively.

Comment on lines 55 to 56
retentionWindow := int64(params.PruneAfter * params.EpochLength)
minHeight := currentHeight - retentionWindow
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The calculation for retentionWindow is vulnerable to an integer overflow. Multiplying two uint64 values (params.PruneAfter and params.EpochLength) and casting the result to int64 can lead to an overflow if the product exceeds math.MaxInt64. This will cause the value to wrap around, potentially becoming negative, or making minHeight an extremely large positive number. This ultimately causes height validation to fail for legitimate attestations, leading to a Denial of Service (DoS). It's crucial to validate that the multiplication will not overflow before performing the cast to int64. Additionally, consider the case where the uint64 multiplication itself could overflow if parameters are extremely large, which might require parameter validation.

pruneProduct := uint64(params.PruneAfter) * params.EpochLength	if pruneProduct > 9223372036854775807 { // math.MaxInt64
		return nil, sdkerr.Wrap(sdkerrors.ErrInvalidRequest, "pruneAfter and epochLength combination would cause an overflow")
	}
	retentionWindow := int64(pruneProduct)
	minHeight := currentHeight - retentionWindow

@alpe alpe marked this pull request as draft February 24, 2026 08:44
Cast PruneAfter and EpochLength to int64 before multiplication to avoid
uint64 overflow followed by negative int64 wrapping. If the product
overflows int64, cap at MaxInt64 to keep the lower bound permissive.
@alpe alpe changed the title security: add attestation height bounds validation fix(network): add attestation height bounds validation Feb 24, 2026
alpe added 4 commits February 24, 2026 11:03
The lower-bound check rejected stale attestations that the integration test
legitimately submits (backward-attesting from height 1). Stale attestations
are harmless — they just waste gas and get pruned by the existing pruning
logic. The real security fix is preventing future-height spam that could
exhaust state storage.
* main:
  fix(network): add vote payload validation in Attest handler (#360)
  fix(network): cap attester set size to prevent DoS and uint16 overflow (#361)
@alpe alpe marked this pull request as ready for review February 24, 2026 16:27
@alpe alpe requested a review from randygrok February 24, 2026 16:29
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
modules/network/keeper/msg_server_test.go (1)

228-316: Good boundary coverage; make the test resilient to DefaultParams drift + cover EpochLength>1 rounding.

TestAttestHeightBounds (Line 228-316) hits the important boundaries (current/next accepted; future rejected; below-min rejected; early-chain floor=1). Two tweaks would make it sturdier:

  • Don’t rely on comments/assumptions about types.DefaultParams() values (Line 230-232). Explicitly set params.EpochLength and params.PruneAfter in the test so it won’t break if defaults change.
  • Add at least one case where EpochLength > 1 and blockHeight is not an epoch boundary to validate the intended rounding behavior.
Proposed test hardening (pin params explicitly)
-            require.NoError(t, keeper.SetParams(ctx, types.DefaultParams()))
+            params := types.DefaultParams()
+            params.EpochLength = 1
+            params.PruneAfter = 7
+            require.NoError(t, keeper.SetParams(ctx, params))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/network/keeper/msg_server_test.go` around lines 228 - 316,
TestAttestHeightBounds relies on assumed DefaultParams values; instead create a
params variable from types.DefaultParams(), set params.EpochLength and
params.PruneAfter explicitly and call keeper.SetParams(ctx, params) so the test
won't break if defaults change. Also add a spec case where you set
params.EpochLength > 1 and pick a blockHeight that is not an epoch boundary
(e.g., EpochLength=3 and blockHeight=10) to assert rounding behavior for
minHeight/acceptance, then run the same attestation checks against
msgServer.Attest to validate the expected accept/reject outcome. Ensure
references to SetParams, EpochLength, PruneAfter, TestAttestHeightBounds, and
msgServer.Attest are updated accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@modules/network/keeper/msg_server.go`:
- Around line 62-76: Replace the manual epoch computation in the minHeight
calculation with GetCurrentEpoch(ctx) to ensure consistency with PruneOldBitmaps
and avoid divergence: call GetCurrentEpoch(ctx) (used elsewhere in this file) to
compute currentEpoch and derive minHeight from params.EpochLength and
params.PruneAfter, then keep the existing msg.Height < minHeight validation;
also extend TestAttestHeightBounds to include a case where params.EpochLength >
1 and blockHeight is not on an epoch boundary to assert the expected
rounding/epoch behavior remains correct.

---

Nitpick comments:
In `@modules/network/keeper/msg_server_test.go`:
- Around line 228-316: TestAttestHeightBounds relies on assumed DefaultParams
values; instead create a params variable from types.DefaultParams(), set
params.EpochLength and params.PruneAfter explicitly and call
keeper.SetParams(ctx, params) so the test won't break if defaults change. Also
add a spec case where you set params.EpochLength > 1 and pick a blockHeight that
is not an epoch boundary (e.g., EpochLength=3 and blockHeight=10) to assert
rounding behavior for minHeight/acceptance, then run the same attestation checks
against msgServer.Attest to validate the expected accept/reject outcome. Ensure
references to SetParams, EpochLength, PruneAfter, TestAttestHeightBounds, and
msgServer.Attest are updated accordingly.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0fb3f8f and 7ec9962.

📒 Files selected for processing (2)
  • modules/network/keeper/msg_server.go
  • modules/network/keeper/msg_server_test.go

Comment on lines +62 to +76
// Enforce attestation height lower bound: reject heights that fall below
// the PruneAfter retention window. Attesting pruned/about-to-be-pruned
// heights wastes storage and serves no purpose. This uses the same epoch
// calculation as PruneOldBitmaps so the two stay aligned.
params := k.GetParams(ctx)
minHeight := int64(1)
if params.PruneAfter > 0 && params.EpochLength > 0 {
currentEpoch := uint64(currentHeight) / params.EpochLength
if currentEpoch > params.PruneAfter {
minHeight = int64((currentEpoch - params.PruneAfter) * params.EpochLength)
}
}
if msg.Height < minHeight {
return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, "attestation height %d is below retention window (min %d)", msg.Height, minHeight)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify epoch & pruning math matches the new minHeight calculation.

set -euo pipefail

# Find the pruning implementation and epoch helpers.
rg -n --type go "func \\(k .*\\) PruneOldBitmaps" modules/network
rg -n --type go "func \\(k .*\\) GetCurrentEpoch" modules/network

# Show the relevant code blocks for manual comparison.
rg -n --type go -C 5 "PruneOldBitmaps" modules/network/keeper/

Repository: evstack/ev-abci

Length of output: 2761


🏁 Script executed:

#!/bin/bash
# Get GetCurrentEpoch implementation and msg_server.go context around line 142

# Get GetCurrentEpoch function
rg -n --type go -A 10 "func \\(k Keeper\\) GetCurrentEpoch" modules/network/keeper/keeper.go

# Show msg_server.go around line 142
head -150 modules/network/keeper/msg_server.go | tail -30

# Get full PruneOldBitmaps implementation
rg -n --type go -A 15 "func \\(k Keeper\\) PruneOldBitmaps" modules/network/keeper/keeper.go

Repository: evstack/ev-abci

Length of output: 2102


🏁 Script executed:

#!/bin/bash
# Check the function signature and understand what currentHeight parameter is

# Get the full AddAttestation function signature and beginning
head -100 modules/network/keeper/msg_server.go | tail -50

# Also check if there are any tests for this validation logic
find modules/network -name "*test*.go" -type f | head -5
rg -n "TestAddAttestation|EpochLength.*>" modules/network --type go

Repository: evstack/ev-abci

Length of output: 2487


🏁 Script executed:

#!/bin/bash
# Check the msg_server_test.go file for existing tests

# Count lines and check structure
wc -l modules/network/keeper/msg_server_test.go

# Look for TestAddAttestation and test cases
rg -n "func Test|EpochLength|PruneAfter|minHeight" modules/network/keeper/msg_server_test.go -A 2

Repository: evstack/ev-abci

Length of output: 1109


🏁 Script executed:

#!/bin/bash
# Get the full TestAttestHeightBounds test to see all test cases

sed -n '228,310p' modules/network/keeper/msg_server_test.go

Repository: evstack/ev-abci

Length of output: 2573


🏁 Script executed:

#!/bin/bash
# Verify the GetCurrentEpoch usage at line 142 and surrounding context

sed -n '138,148p' modules/network/keeper/msg_server.go

# Also check if there are any edge case tests or tests with EpochLength > 1
rg -n "EpochLength.*2|EpochLength.*3|EpochLength.*[0-9]" modules/network/keeper/msg_server_test.go

Repository: evstack/ev-abci

Length of output: 365


Use GetCurrentEpoch(ctx) for consistency and to reduce divergence risk.

The epoch calculation in minHeight (currently uint64(currentHeight) / params.EpochLength) matches the formula in PruneOldBitmaps, but since GetCurrentEpoch(ctx) already encapsulates this logic and is used elsewhere in the function (line 142), extracting currentHeight into GetCurrentEpoch(ctx) eliminates duplication and reduces the risk of future divergence if the epoch formula changes.

Additionally, test coverage for TestAttestHeightBounds uses only DefaultParams with EpochLength=1, which doesn't verify rounding semantics with larger epoch lengths. Add a test case with EpochLength > 1 and a non-boundary blockHeight to lock in the intended behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/network/keeper/msg_server.go` around lines 62 - 76, Replace the
manual epoch computation in the minHeight calculation with GetCurrentEpoch(ctx)
to ensure consistency with PruneOldBitmaps and avoid divergence: call
GetCurrentEpoch(ctx) (used elsewhere in this file) to compute currentEpoch and
derive minHeight from params.EpochLength and params.PruneAfter, then keep the
existing msg.Height < minHeight validation; also extend TestAttestHeightBounds
to include a case where params.EpochLength > 1 and blockHeight is not on an
epoch boundary to assert the expected rounding/epoch behavior remains correct.

alpe added 2 commits February 25, 2026 08:27
Replace the inline epoch computation (uint64(currentHeight)/params.EpochLength)
with k.GetCurrentEpoch(ctx) so the Attest lower-bound and PruneOldBitmaps
use the same formula and cannot silently diverge.

Extend TestAttestHeightBounds with two sub-cases using EpochLength=10 and a
blockHeight (35) that is not on an epoch boundary, asserting that the
floor-to-epoch-start rounding is correct: attestH=0 rejected (below
minHeight=10), attestH=10 accepted (at minHeight).
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
modules/network/keeper/msg_server_test.go (2)

379-386: Remove the speculative comment; IsBonded() is well-defined.

stakingtypes.Validator.IsBonded() is not ambiguous — it checks v.Status == BondedStatus. The "Assuming…" hedge creates noise and could mislead future readers into thinking the filter criterion is uncertain.

♻️ Proposed cleanup
 func (m MockStakingKeeper) GetLastValidators(ctx context.Context) (validators []stakingtypes.Validator, err error) {
 	for _, validator := range m.activeSet {
-		if validator.IsBonded() { // Assuming IsBonded() identifies if a validator is in the last validators
+		if validator.IsBonded() {
 			validators = append(validators, validator)
 		}
 	}
 	return
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/network/keeper/msg_server_test.go` around lines 379 - 386, Remove the
speculative inline comment in the GetLastValidators method that reads "Assuming
IsBonded() identifies if a validator is in the last validators"; IsBonded() is
well-defined. Edit the function MockStakingKeeper.GetLastValidators to delete
that hedge comment and leave the loop/filter logic using validator.IsBonded()
unchanged so the intent remains clear and concise.

271-275: Consider adding an attestH=0 sub-case to the early-chain scenario.

The "early chain no stale rejection" comment states minHeight=1, implying genesis (height 0) is below the floor. But the test only probes attestH=1. If the production code's clamp yields minHeight=0 instead of 1, genesis-height attestations would be silently accepted, and this test would give false confidence. Adding an explicit attestH=0 sub-case — with the expectation derived from the actual clamp — would close that gap.

💡 Suggested additional test case
 "early chain no stale rejection": {
     // blockHeight=5, currentEpoch=5, PruneAfter=7 → currentEpoch <= PruneAfter → minHeight=1
     blockHeight: 5,
     attestH:     1,
 },
+"early chain genesis height": {
+    // attestH=0 is below minHeight=1 on early chain; explicitly verify it is rejected
+    blockHeight: 5,
+    attestH:     0,
+    expErr:      sdkerrors.ErrInvalidRequest,
+},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/network/keeper/msg_server_test.go` around lines 271 - 275, Add a
sub-case to the "early chain no stale rejection" test case that sets attestH = 0
(alongside the existing blockHeight: 5) and assert the outcome according to the
clamp logic used in production (i.e., compare against the computed
minHeight/PruneAfter behavior used by the code under test). Locate the test map
entry keyed "early chain no stale rejection" in msg_server_test.go and add the
new sub-case using the same fields (blockHeight, attestH) and the existing
assertion pattern so the test explicitly verifies whether a genesis-height (0)
attestation is accepted or rejected per the clamp logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@modules/network/keeper/msg_server_test.go`:
- Around line 379-386: Remove the speculative inline comment in the
GetLastValidators method that reads "Assuming IsBonded() identifies if a
validator is in the last validators"; IsBonded() is well-defined. Edit the
function MockStakingKeeper.GetLastValidators to delete that hedge comment and
leave the loop/filter logic using validator.IsBonded() unchanged so the intent
remains clear and concise.
- Around line 271-275: Add a sub-case to the "early chain no stale rejection"
test case that sets attestH = 0 (alongside the existing blockHeight: 5) and
assert the outcome according to the clamp logic used in production (i.e.,
compare against the computed minHeight/PruneAfter behavior used by the code
under test). Locate the test map entry keyed "early chain no stale rejection" in
msg_server_test.go and add the new sub-case using the same fields (blockHeight,
attestH) and the existing assertion pattern so the test explicitly verifies
whether a genesis-height (0) attestation is accepted or rejected per the clamp
logic.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ec9962 and abae484.

📒 Files selected for processing (2)
  • modules/network/keeper/msg_server.go
  • modules/network/keeper/msg_server_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • modules/network/keeper/msg_server.go

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
modules/network/keeper/msg_server_test.go (1)

228-316: Good coverage of core boundary conditions; consider adding an EpochLength > 1 case.

The test cases correctly exercise all the key scenarios (future, two-ahead, current, next, stale, below/at retention, early chain), and the expected values check out against the implementation math. SetParams return value is now properly checked — nice.

All cases use DefaultParams with EpochLength=1, which means the epoch-floor rounding (uint64(currentHeight) / params.EpochLength) is a no-op. Adding a case with e.g. EpochLength=10 and a non-aligned blockHeight (like 95) would lock in the integer-division rounding behavior: currentEpoch = 95/10 = 9, minHeight = (9-7)*10 = 20.

Example additional test case
+		"epoch rounding with EpochLength > 1": {
+			// EpochLength=10, PruneAfter=7, blockHeight=95
+			// currentEpoch = 95/10 = 9, minHeight = (9-7)*10 = 20
+			// attestH=20 should be accepted (at minHeight)
+			blockHeight: 95,
+			attestH:     20,
+		},

This would require overriding DefaultParams in the test body with custom EpochLength. You could add a params field to the test spec (falling back to DefaultParams() when nil) to support this without restructuring.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/network/keeper/msg_server_test.go` around lines 228 - 316, Add a spec
that verifies epoch-floor rounding by testing with EpochLength > 1 (e.g.
EpochLength=10 and blockHeight=95) in TestAttestHeightBounds: extend the specs
map to include an optional params field (use types.DefaultParams() when nil) and
call keeper.SetParams(ctx, params) instead of always DefaultParams(); add a case
where currentEpoch = uint64(blockHeight)/params.EpochLength yields a non-aligned
epoch (blockHeight=95 → currentEpoch=9) and assert minHeight calculation and
accept/reject behavior matches expectations (e.g. attestH values at, below and
above the computed minHeight/currentEpoch boundaries). Ensure you reference and
use keeper.SetParams, types.DefaultParams, and the TestAttestHeightBounds/specs
logic when adding the new case.
modules/network/keeper/msg_server.go (1)

62-76: Lower bound logic is correct and overflow-safe; minor duplication with GetCurrentEpoch.

The epoch formula here (uint64(currentHeight) / params.EpochLength) is inherently bounded by currentHeight, so the int64() cast on line 71 cannot overflow — good. The currentEpoch > params.PruneAfter guard on line 70 prevents uint64 underflow in the subtraction.

Two observations:

  1. Duplicate GetParams call: k.GetParams(ctx) is already called implicitly on line 37 (k.GetParams(ctx).SignMode). Consider hoisting params to a single call at the top of Attest to avoid a redundant store read.

  2. Epoch formula duplication with GetCurrentEpoch(ctx): Line 69 duplicates the formula that GetCurrentEpoch (used on line 142) encapsulates. The commit history shows this was an intentional revert, so flagging for awareness only — if the epoch formula ever changes, these two sites could diverge.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/network/keeper/msg_server.go` around lines 62 - 76, The code repeats
a params lookup and duplicates epoch-calculation logic; inside the Attest
handler hoist a single call to k.GetParams(ctx) into the function scope (reuse
the returned params instead of calling k.GetParams(ctx).SignMode elsewhere) and
replace the manual epoch computation (uint64(currentHeight) /
params.EpochLength) with a call to k.GetCurrentEpoch(ctx) (or a single helper)
to compute currentEpoch, then use that value to derive minHeight and keep the
existing guards (params.PruneAfter/params.EpochLength) intact so the logic and
overflow safety remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@modules/network/keeper/msg_server_test.go`:
- Around line 228-316: Add a spec that verifies epoch-floor rounding by testing
with EpochLength > 1 (e.g. EpochLength=10 and blockHeight=95) in
TestAttestHeightBounds: extend the specs map to include an optional params field
(use types.DefaultParams() when nil) and call keeper.SetParams(ctx, params)
instead of always DefaultParams(); add a case where currentEpoch =
uint64(blockHeight)/params.EpochLength yields a non-aligned epoch
(blockHeight=95 → currentEpoch=9) and assert minHeight calculation and
accept/reject behavior matches expectations (e.g. attestH values at, below and
above the computed minHeight/currentEpoch boundaries). Ensure you reference and
use keeper.SetParams, types.DefaultParams, and the TestAttestHeightBounds/specs
logic when adding the new case.

In `@modules/network/keeper/msg_server.go`:
- Around line 62-76: The code repeats a params lookup and duplicates
epoch-calculation logic; inside the Attest handler hoist a single call to
k.GetParams(ctx) into the function scope (reuse the returned params instead of
calling k.GetParams(ctx).SignMode elsewhere) and replace the manual epoch
computation (uint64(currentHeight) / params.EpochLength) with a call to
k.GetCurrentEpoch(ctx) (or a single helper) to compute currentEpoch, then use
that value to derive minHeight and keep the existing guards
(params.PruneAfter/params.EpochLength) intact so the logic and overflow safety
remain unchanged.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between abae484 and 76494ef.

📒 Files selected for processing (2)
  • modules/network/keeper/msg_server.go
  • modules/network/keeper/msg_server_test.go

DefaultQuorumFraction = math.LegacyNewDecWithPrec(667, 3) // 2/3
DefaultMinParticipation = math.LegacyNewDecWithPrec(5, 1) // 1/2
DefaultPruneAfter = uint64(7)
DefaultPruneAfter = uint64(15) // also used as number of blocks for attestations to land
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to increase the window so that the integration tests gets their messages through

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant