Skip to content

feat(network): load fixed attester set from genesis (Part 1)#394

Open
randygrok wants to merge 3 commits intomainfrom
split/01-genesis-attester-set
Open

feat(network): load fixed attester set from genesis (Part 1)#394
randygrok wants to merge 3 commits intomainfrom
split/01-genesis-attester-set

Conversation

@randygrok
Copy link
Copy Markdown
Contributor

@randygrok randygrok commented Apr 28, 2026

Summary

This is Part 1 of the prod-readiness split. It introduces the genesis-backed fixed attester set as the foundation for the later vote verification, RPC reconstruction, attester CLI, and integration PRs.

Changes:

  • Add attester_infos to network genesis and generated protobuf types.
  • Load attesters from genesis with pubkey/consensus-address validation.
  • Derive validator indices deterministically from the fixed attester set.
  • Disable runtime MsgJoinAttesterSet and MsgLeaveAttesterSet changes.
  • Stop rebuilding the attester index map at epoch end.

Validation

  • go test ./modules/network/...

Summary by CodeRabbit

  • Breaking Changes

    • Attester set membership is immutable after genesis; join/leave requests are rejected.
  • New Features

    • Genesis now records attester info (including consensus addresses) and exports/imports it for deterministic state.
  • Bug Fixes

    • Validates attester consensus addresses match their public keys at genesis.
    • Validator indices are deterministically assigned at genesis.
  • Tests

    • Added comprehensive tests for bitmap helpers and genesis attester handling.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 28, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1305126b-fc52-4d1c-acc5-5a14807b5b97

📥 Commits

Reviewing files that changed from the base of the PR and between 64064ac and dd9e721.

📒 Files selected for processing (3)
  • modules/network/keeper/abci.go
  • modules/network/keeper/keeper.go
  • modules/network/keeper/msg_server.go
💤 Files with no reviewable changes (1)
  • modules/network/keeper/keeper.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • modules/network/keeper/msg_server.go

📝 Walkthrough

Walkthrough

Genesis now builds the validator set deterministically from AttesterInfos (validating pubkey↔consensus address, sorting by pubkey bytes, assigning indices/power). Runtime join/leave and index rebuilds are disabled. Genesis export writes attester_infos; legacy validator_indices are not exported.

Changes

Cohort / File(s) Summary
Proto Definitions
modules/proto/evabci/network/v1/attester.proto, modules/proto/evabci/network/v1/genesis.proto
Added consensus_address to AttesterInfo and repeated AttesterInfo attester_infos to GenesisState to persist genesis attester set.
Types Utilities
modules/network/types/attester.go, modules/network/types/genesis.go
Added NewAttesterInfo, GetPubKey, and UnpackInterfaces on AttesterInfo; added GenesisState.UnpackInterfaces to unpack Any pubkeys during genesis unmarshalling.
Genesis Initialization & Export
modules/network/genesis.go
Init loads AttesterInfos, verifies bech32 consensus address matches pubkey-derived address, normalizes address string, deterministically sorts by pubkey bytes, assigns validator indices with power=1; export populates attester_infos and omits legacy validator_indices.
Keeper Core
modules/network/keeper/keeper.go
Removed MaxAttesters, BuildValidatorIndexMap, and PruneOldBitmaps; keeper no longer supports rebuilding validator index map or pruning bitmaps at runtime.
Message Handlers
modules/network/keeper/msg_server.go
JoinAttesterSet and LeaveAttesterSet now unconditionally return an ErrInvalidRequest (attester set immutable at genesis); attest-related comments trimmed.
ABCI Handling
modules/network/keeper/abci.go
Removed epoch-end BuildValidatorIndexMap call and related error handling; code comments updated to note indices are genesis-established.
Tests — Keeper & Bitmap
modules/network/keeper/bitmap_test.go, modules/network/keeper/genesis_test.go, modules/network/keeper/msg_server_test.go
Added comprehensive bitmap tests; added genesis tests for attester loading, deterministic ordering, address mismatch rejection, and export roundtrip; updated msg_server tests to expect join/leave rejection and to set keeper state directly where needed.

Sequence Diagram

sequenceDiagram
    participant Genesis as Genesis
    participant Keeper as Keeper
    participant Store as KVStore

    Note over Genesis,Keeper: Genesis initialization with AttesterInfos
    Genesis->>Keeper: InitGenesis(state.attester_infos)
    Keeper->>Keeper: Decode/unpack AttesterInfo.Pubkey (Any)
    Keeper->>Keeper: Verify bech32 ConsensusAddress == pk.Address()
    Keeper->>Keeper: Normalize ConsensusAddress string
    Keeper->>Keeper: Sort attesters by pubkey address bytes
    Keeper->>Keeper: Assign validator indices (0..N-1) and set power=1
    Keeper->>Store: Persist AttesterInfo keyed by consensus_address
    Keeper->>Store: Persist ValidatorIndex & ValidatorPower entries

    Note over Genesis,Keeper: Genesis export
    Genesis->>Keeper: ExportGenesis()
    Keeper->>Store: Read all AttesterInfo entries
    Keeper->>Keeper: Sort by pubkey address bytes
    Keeper-->>Genesis: Return GenesisState with attester_infos (validator_indices nil)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰
I hop through genesis, pawprints align,
Pubkeys and addresses sorted in line.
No more joins at runtime, the list is set tight,
Validators fixed by the dawn's gentle light. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: loading a fixed attester set from genesis and deriving validator indices from it, matching the core objective of this Part 1 PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch split/01-genesis-attester-set

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
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

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

@randygrok randygrok changed the title Part 1: load fixed attester set from genesis feat(network): load fixed attester set from genesis (Part 1) Apr 28, 2026
@randygrok randygrok marked this pull request as ready for review April 29, 2026 09:06
@randygrok randygrok requested a review from a team as a code owner April 29, 2026 09:06
Copy link
Copy Markdown
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: 3

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

229-242: Consider pinning negative-bound behavior in CountInRange tests.

Optional: add negative start/end cases so current bound-handling semantics stay explicit over time.

Suggested test-table additions
 	specs := map[string]struct {
 		start, end int
 		expCount   int
 	}{
 		"full range":                   {start: 0, end: 16, expCount: 5},
+		"negative start includes valid lower bits": {start: -5, end: 2, expCount: 1},
+		"negative end is empty range":             {start: 0, end: -1, expCount: 0},
 		"first byte only":              {start: 0, end: 8, expCount: 3},
 		"second byte only":             {start: 8, end: 16, expCount: 2},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/network/keeper/bitmap_test.go` around lines 229 - 242, The test table
for CountInRange in bitmap_test.go omits negative start/end cases which can
allow boundary-handling semantics to drift; add entries to the specs map that
use negative start and/or end (e.g., start: -5 end: 2 and start: -10 end: -1)
with explicit expCount values that match the current CountInRange behavior so
the intended semantics are pinned; update the test cases in the specs map near
the existing entries and ensure they reference the same CountInRange call used
by the test harness so future changes to bounds handling will fail the tests if
behavior changes.
modules/network/keeper/genesis_test.go (1)

100-103: Use a valid-but-different consensus address to harden mismatch-path coverage.

Line 102 uses an arbitrary byte slice cast to ConsAddress. Using a second real pubkey-derived consensus address would make this test more explicitly target pubkey/address mismatch instead of relying on address-shape behavior.

Proposed test hardening
 pk := cmted25519.GenPrivKey().PubKey().(cmted25519.PubKey)
 info := mustAnyPubKey(t, pk)
-info.ConsensusAddress = sdk.ConsAddress([]byte("not-matching-20-bytes")).String()
+otherPk := cmted25519.GenPrivKey().PubKey().(cmted25519.PubKey)
+info.ConsensusAddress = sdk.ConsAddress(otherPk.Address()).String()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/network/keeper/genesis_test.go` around lines 100 - 103, The test
currently assigns a bogus byte slice to info.ConsensusAddress; instead derive a
real-but-different consensus address from a second ed25519 pubkey so the test
exercises a true pubkey/address mismatch. Generate a second key (e.g., via
cmted25519.GenPrivKey().PubKey()), derive its consensus address
(sdk.ConsAddress(secondPubKey.Address()).String()), and assign that to
info.ConsensusAddress while keeping the original pk and mustAnyPubKey usage,
ensuring the new consensus address is different from pk's address to harden the
mismatch path.
🤖 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/genesis.go`:
- Around line 58-66: The loop over attesters casts idx to uint16 when calling
SetValidatorIndex, which can overflow if len(attesters) > 65535; before calling
SetValidatorIndex (in the for idx, info := range attesters loop) add a guard
that checks if idx exceeds math.MaxUint16 (or 65535) and return a clear error
(e.g., "too many attesters: exceeds uint16 limit") to prevent index wrapping;
ensure this validation happens prior to SetValidatorIndex invocation so
SetValidatorIndex is only called with a safe uint16 cast.
- Around line 25-49: The loop over the attesters slice must detect and reject
duplicate consensus addresses after you normalize the bech32 prefix: after
computing derived := sdk.ConsAddress(pk.Address()).String() (in the attesters
processing loop), keep a map (e.g. map[string]int) keyed by the normalized
derived string (or the raw 20-byte address pk.Address()) and if the derived
address is already present return an error indicating the duplicate attester
(include both indices and the conflicting ConsensusAddress) instead of allowing
the later SetAttesterInfo / SetValidatorIndex writes to silently overwrite the
earlier entry; ensure you reference the same symbols used in the loop
(attesters, info.ConsensusAddress, pk.Address(), derived) when implementing the
check and error.

In `@modules/proto/evabci/network/v1/attester.proto`:
- Around line 25-28: The schema comment for the field consensus_address
incorrectly hard-codes the bech32 prefix; update the comment on the
consensus_address string field so it does not specify "cosmosvalcons1..." and
instead states it is a bech32 consensus address (derived from pubkey) that may
use any valid chain-specific consensus-address prefix; mention that InitGenesis
will accept and normalize any valid bech32 consensus-address prefix so the
stored consensus_address matches the keeper's collection key.

---

Nitpick comments:
In `@modules/network/keeper/bitmap_test.go`:
- Around line 229-242: The test table for CountInRange in bitmap_test.go omits
negative start/end cases which can allow boundary-handling semantics to drift;
add entries to the specs map that use negative start and/or end (e.g., start: -5
end: 2 and start: -10 end: -1) with explicit expCount values that match the
current CountInRange behavior so the intended semantics are pinned; update the
test cases in the specs map near the existing entries and ensure they reference
the same CountInRange call used by the test harness so future changes to bounds
handling will fail the tests if behavior changes.

In `@modules/network/keeper/genesis_test.go`:
- Around line 100-103: The test currently assigns a bogus byte slice to
info.ConsensusAddress; instead derive a real-but-different consensus address
from a second ed25519 pubkey so the test exercises a true pubkey/address
mismatch. Generate a second key (e.g., via cmted25519.GenPrivKey().PubKey()),
derive its consensus address (sdk.ConsAddress(secondPubKey.Address()).String()),
and assign that to info.ConsensusAddress while keeping the original pk and
mustAnyPubKey usage, ensuring the new consensus address is different from pk's
address to harden the mismatch path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ea747e09-6937-403d-bcbc-30eef5feab0b

📥 Commits

Reviewing files that changed from the base of the PR and between 49f4e8e and 64064ac.

⛔ Files ignored due to path filters (2)
  • modules/network/types/attester.pb.go is excluded by !**/*.pb.go
  • modules/network/types/genesis.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (11)
  • modules/network/genesis.go
  • modules/network/keeper/abci.go
  • modules/network/keeper/bitmap_test.go
  • modules/network/keeper/genesis_test.go
  • modules/network/keeper/keeper.go
  • modules/network/keeper/msg_server.go
  • modules/network/keeper/msg_server_test.go
  • modules/network/types/attester.go
  • modules/network/types/genesis.go
  • modules/proto/evabci/network/v1/attester.proto
  • modules/proto/evabci/network/v1/genesis.proto
💤 Files with no reviewable changes (1)
  • modules/network/keeper/keeper.go

Comment on lines +25 to +49
for i := range attesters {
info := attesters[i]
pk, err := info.GetPubKey()
if err != nil {
return fmt.Errorf("attester %d: %w", i, err)
}
// Also add to attester set
if err := k.SetAttesterSetMember(ctx, vi.Address); err != nil {
return err
// Validate pubkey ↔ consensus_address match at the raw-bytes level so
// the check is independent of bech32 prefix (e.g. "cosmosvalcons" vs
// "celestiavalcons"). Whatever prefix was used in genesis, the 20-byte
// payload must equal the pubkey's Address().
_, rawAddr, decErr := bech32.DecodeAndConvert(info.ConsensusAddress)
if decErr != nil {
return fmt.Errorf("attester %d: decode consensus_address %q: %w", i, info.ConsensusAddress, decErr)
}
if !bytes.Equal(rawAddr, pk.Address()) {
return fmt.Errorf("attester %d: pubkey address mismatch (derived bytes %x, stated bytes %x)",
i, pk.Address(), rawAddr)
}
// Re-encode consensus_address with the runtime SDK config so the
// stored value matches what ConsAddress().String() produces elsewhere
// in the module at runtime.
derived := sdk.ConsAddress(pk.Address()).String()
info.ConsensusAddress = derived
attesters[i] = info
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reject duplicate attesters after consensus-address normalization.

After Line 46 rewrites the address to the runtime bech32 prefix, two genesis entries that decode to the same 20-byte consensus address will collapse onto the same keeper key. The later SetAttesterInfo / SetValidatorIndex writes then overwrite the earlier one instead of failing, which silently corrupts the fixed attester set.

🛡️ Suggested guard
 	attesters := make([]types.AttesterInfo, len(genState.AttesterInfos))
 	copy(attesters, genState.AttesterInfos)
+	seenConsensus := make(map[string]struct{}, len(attesters))

 	for i := range attesters {
 		info := attesters[i]
 		pk, err := info.GetPubKey()
 		if err != nil {
@@
 		// Re-encode consensus_address with the runtime SDK config so the
 		// stored value matches what ConsAddress().String() produces elsewhere
 		// in the module at runtime.
 		derived := sdk.ConsAddress(pk.Address()).String()
+		if _, exists := seenConsensus[derived]; exists {
+			return fmt.Errorf("attester %d: duplicate consensus_address %q", i, derived)
+		}
+		seenConsensus[derived] = struct{}{}
 		info.ConsensusAddress = derived
 		attesters[i] = info
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/network/genesis.go` around lines 25 - 49, The loop over the attesters
slice must detect and reject duplicate consensus addresses after you normalize
the bech32 prefix: after computing derived :=
sdk.ConsAddress(pk.Address()).String() (in the attesters processing loop), keep
a map (e.g. map[string]int) keyed by the normalized derived string (or the raw
20-byte address pk.Address()) and if the derived address is already present
return an error indicating the duplicate attester (include both indices and the
conflicting ConsensusAddress) instead of allowing the later SetAttesterInfo /
SetValidatorIndex writes to silently overwrite the earlier entry; ensure you
reference the same symbols used in the loop (attesters, info.ConsensusAddress,
pk.Address(), derived) when implementing the check and error.

Comment on lines +58 to +66
for idx, info := range attesters {
if err := k.SetAttesterInfo(ctx, info.ConsensusAddress, &info); err != nil {
return fmt.Errorf("set attester info: %w", err)
}
if err := k.SetAttesterSetMember(ctx, info.ConsensusAddress); err != nil {
return fmt.Errorf("set attester set member: %w", err)
}
if err := k.SetValidatorIndex(ctx, info.ConsensusAddress, uint16(idx), 1); err != nil {
return fmt.Errorf("set validator index: %w", err)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Guard the uint16 validator-index cast.

Line 65 narrows idx to uint16 without checking the attester count first. A malformed genesis with too many attesters will wrap indices and make different attesters share the same bitmap position.

🚧 Suggested bound check
 import (
 	"bytes"
 	"fmt"
+	"math"
 	"sort"
@@
+	if len(attesters) > math.MaxUint16 {
+		return fmt.Errorf("too many attesters: %d", len(attesters))
+	}
+
 	for idx, info := range attesters {
 		if err := k.SetAttesterInfo(ctx, info.ConsensusAddress, &info); err != nil {
 			return fmt.Errorf("set attester info: %w", err)
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/network/genesis.go` around lines 58 - 66, The loop over attesters
casts idx to uint16 when calling SetValidatorIndex, which can overflow if
len(attesters) > 65535; before calling SetValidatorIndex (in the for idx, info
:= range attesters loop) add a guard that checks if idx exceeds math.MaxUint16
(or 65535) and return a clear error (e.g., "too many attesters: exceeds uint16
limit") to prevent index wrapping; ensure this validation happens prior to
SetValidatorIndex invocation so SetValidatorIndex is only called with a safe
uint16 cast.

Comment on lines +25 to +28
// consensus_address is the bech32 cosmosvalcons1... derived from pubkey.
// Redundant with pubkey but persisted so the keeper's collections key
// (consensus address) matches the stored struct.
string consensus_address = 4 [(cosmos_proto.scalar) = "cosmos.AddressString"];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Don’t hard-code the valcons prefix in the schema comment.

InitGenesis accepts any valid bech32 consensus-address prefix and normalizes it at import time, so documenting this field as always cosmosvalcons1... is misleading for chains using a different runtime prefix.

✏️ Suggested wording
-  // consensus_address is the bech32 cosmosvalcons1... derived from pubkey.
+  // consensus_address is the bech32 consensus-address string derived from pubkey.
+  // The exact prefix is chain-config dependent and is normalized during genesis import.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// consensus_address is the bech32 cosmosvalcons1... derived from pubkey.
// Redundant with pubkey but persisted so the keeper's collections key
// (consensus address) matches the stored struct.
string consensus_address = 4 [(cosmos_proto.scalar) = "cosmos.AddressString"];
// consensus_address is the bech32 consensus-address string derived from pubkey.
// The exact prefix is chain-config dependent and is normalized during genesis import.
// Redundant with pubkey but persisted so the keeper's collections key
// (consensus address) matches the stored struct.
string consensus_address = 4 [(cosmos_proto.scalar) = "cosmos.AddressString"];
🧰 Tools
🪛 Buf (1.68.4)

[error] 28-28: cannot find cosmos_proto.scalar in this scope

(COMPILE)

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

In `@modules/proto/evabci/network/v1/attester.proto` around lines 25 - 28, The
schema comment for the field consensus_address incorrectly hard-codes the bech32
prefix; update the comment on the consensus_address string field so it does not
specify "cosmosvalcons1..." and instead states it is a bech32 consensus address
(derived from pubkey) that may use any valid chain-specific consensus-address
prefix; mention that InitGenesis will accept and normalize any valid bech32
consensus-address prefix so the stored consensus_address matches the keeper's
collection key.

Comment thread modules/network/keeper/abci.go Outdated
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.

2 participants