Skip to content

switch to collections in the attribute module.#2677

Open
nagarajdivine wants to merge 2 commits into
mainfrom
feat/collections-attribute
Open

switch to collections in the attribute module.#2677
nagarajdivine wants to merge 2 commits into
mainfrom
feat/collections-attribute

Conversation

@nagarajdivine

@nagarajdivine nagarajdivine commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Description


Before we can merge this PR, please make sure that all the following items have been
checked off. If any of the checklist items are not applicable, please leave them but
write a little note why.

  • Targeted PR against correct branch (see CONTRIBUTING.md).
  • Linked to Github issue with discussion and accepted design OR link to spec that describes this work.
  • Wrote unit and integration tests
  • Updated relevant documentation (docs/) or specification (x/<module>/spec/).
  • Added relevant godoc comments.
  • Added relevant changelog entries under .changelog/unreleased (see Adding Changes).
  • Re-reviewed Files changed in the Github PR explorer.
  • Review Codecov Report in the comment section below once CI passes.

Summary by CodeRabbit

  • Refactor

    • Internal data storage implementation improved for enhanced performance and efficiency
  • Chores

    • Attribute module consensus version incremented to 3
    • Migration support added for upgrading from version 2 to version 3
  • Tests

    • Added migration tests validating state preservation and upgrade compatibility

@nagarajdivine nagarajdivine requested a review from a team as a code owner April 16, 2026 15:01
@coderabbitai

coderabbitai Bot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@nagarajdivine has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 49 minutes and 58 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 49 minutes and 58 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 88f8abb8-6324-40cf-80c8-b32d324f4cc6

📥 Commits

Reviewing files that changed from the base of the PR and between c3aad1f and de48797.

📒 Files selected for processing (1)
  • .changelog/unreleased/features/2489-collections-attributes.md
📝 Walkthrough

Walkthrough

The attribute module is being refactored to use Cosmos SDK collections for state storage instead of direct KVStore access. A no-op migration (v2→v3) is introduced to track the schema change. The ConsensusVersion is bumped from 2 to 3.

Changes

Cohort / File(s) Summary
Keeper Core Refactor
x/attribute/keeper/keeper.go, x/attribute/keeper/params.go, x/attribute/keeper/query_server.go
Replaced direct KVStore and KV prefix-based pagination with collections.Schema, typed maps (attributes, nameAddrCounts, expirationIndex), and an Item for params. Rewrote query handlers (Attribute, Attributes, Scan, AttributeAccounts) with new pagination helpers (attrPageWalk, nameAddrPageWalk) using cursor-key resumption and simplified params access.
Collections Codec & Keys
x/attribute/types/codec.go, x/attribute/types/keys_test.go
Implemented collcodec.KeyCodec structs and builders for AttrTriple, NameAddrPair, ExpireTriple; added value codecs (Uint64Value, SentinelValue). Verified byte-identity between old and new key encodings in new test.
App Integration
app/app.go
Updated AttributeKeeper constructor call to pass runtime.NewKVStoreService(...) instead of raw KV store key.
Migration Infrastructure
x/attribute/keeper/migrations.go, x/attribute/keeper/migration_test.go, x/attribute/module.go
Added no-op Migrate2to3 method, registered migration version 2→3 in module setup, and incremented ConsensusVersion to 3. Introduced test suite validating migration preserves attribute state.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Suggested reviewers

  • SpicyLemon

Poem

🐰 From KV stores to collections bright,
State storage now feels just right!
Hash-keyed maps and schemas clean,
A migration—v2 to v3, so lean.
The attributes hop forward with glee! 🌟

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: switching the attribute module to use collections for state storage.
Linked Issues check ✅ Passed The PR implements collections-based state storage in the x/attribute module as required by issue #2489, with keeper refactoring, new codec implementations, and migration support included.
Out of Scope Changes check ✅ Passed All changes are directly related to migrating the attribute module to collections: keeper refactoring, codec updates, query implementations, migrations, and corresponding tests.
Docstring Coverage ✅ Passed Docstring coverage is 81.25% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/collections-attribute

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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
x/attribute/keeper/keeper.go (1)

265-286: ⚠️ Potential issue | 🟠 Major

Build origKey from the normalized original name.

normalizedOrigName is computed, but originalAttribute.Name is never updated before BuildAttrTriple(originalAttribute). If the caller passes a valid non-normalized original name, this lookup misses the stored row even though the normalized names matched.

Suggested fix
 	if normalizedName != normalizedOrigName {
 		return fmt.Errorf("update and original names must match %s : %s", normalizedName, normalizedOrigName)
 	}
 
+	originalAttribute.Name = normalizedOrigName
 	updateAttribute.Name = normalizedName
 
 	if ownerAcc := k.authKeeper.GetAccount(ctx, owner); ownerAcc == nil {
 		return fmt.Errorf("no account found for owner address %q", owner.String())
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@x/attribute/keeper/keeper.go` around lines 265 - 286, The code computes
normalizedOrigName via k.nameKeeper.Normalize but still calls
types.BuildAttrTriple(originalAttribute) using the un-normalized
originalAttribute.Name, causing a lookup miss; update originalAttribute.Name =
normalizedOrigName (or otherwise construct the triple using normalizedOrigName)
before calling types.BuildAttrTriple so origKey is built from the normalized
original name and subsequent lookups succeed; ensure any later uses of
originalAttribute reflect this change and keep the call sites:
normalizedOrigName, originalAttribute, types.BuildAttrTriple, and origKey in
mind while editing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@x/attribute/keeper/keeper.go`:
- Around line 609-625: The expiration index entry is being removed
unconditionally even when k.attributes.Remove fails, which loses the retry
handle; change the logic so k.expirationIndex.Remove(ctx, entry.expKey) is only
invoked after a successful attribute deletion (i.e., when attr was fetched
(k.attributes.Get returned nil) and k.attributes.Remove returned nil). Keep the
existing log paths for Get/Remove failures and, on a failed k.attributes.Remove,
do not call k.expirationIndex.Remove so the expiration sentinel remains for
retry; retain the EmitTypedEvent and DecAttrNameAddressLookup calls in the
successful-removal branch.
- Around line 143-150: IterateRecords currently ignores the caller-supplied
prefix by calling k.attributes.Walk(ctx, nil, ...); update it to derive the walk
range from the provided prefix the same way prefixScan does and pass that range
instead of nil. Locate the IterateRecords method and replace the nil range
argument to k.attributes.Walk with the start/end (or range) values produced by
the prefix->range helper used by prefixScan so Walk only iterates keys under the
supplied prefix; keep the handler invocation and error handling the same.
- Around line 486-500: The purge loop removes the primary attribute rows and
decrements name-address counts but never clears the corresponding entries in
expirationIndex, leaving orphaned expiry sentinels; after successfully removing
the attribute with k.attributes.Remove(ctx, key) and updating counts via
k.DecAttrNameAddressLookup(ctx, name, acct), also remove the matching expiration
index entry from k.expirationIndex (e.g. call k.expirationIndex.Remove(ctx, key)
or the equivalent method your implementation exposes) and handle/return any
error from that call so expired-index sentinels are not left behind.
- Around line 216-238: The current incNameAddrCount and decNameAddrCount swallow
all errors from nameAddrCounts.Get and treat them as "not found"; change them to
only treat collections.ErrNotFound as missing and propagate any other errors.
Specifically, in incNameAddrCount, call k.nameAddrCounts.Get and if err != nil
then if errors.Is(err, collections.ErrNotFound) set current = 0 else return err;
then Set the incremented value. In decNameAddrCount, call Get and if err != nil
then if errors.Is(err, collections.ErrNotFound) return nil (nothing to
decrement) else return err; otherwise apply the existing current<=1 removal or
Set(current-1). Use errors.Is(err, collections.ErrNotFound) exactly and keep the
existing calls to k.nameAddrCounts.Get, Set, and Remove and the
DecAttrNameAddressLookup logging behavior.

In `@x/attribute/keeper/params.go`:
- Around line 11-16: The current handler treats any error from k.params.Get(ctx)
as "params missing" and returns types.Params{MaxValueLength:
types.DefaultMaxValueLength}; change this to only use the default when the Get
error is the known "missing entry"/not-found condition (e.g., check for
sdkerrors.ErrNotFound or store's IsNotFound error depending on your params store
implementation) and otherwise surface the unexpected error (propagate/return it
or wrap and fail loudly) instead of silently returning defaults; update the
function signature to return an error if needed and keep references to
k.params.Get, types.Params, and types.DefaultMaxValueLength to locate and patch
the code.

In `@x/attribute/keeper/query_server.go`:
- Around line 150-156: The current nil-page handling sets pageReq to a non-nil
PageRequest, which together with the earlier local limit := query.DefaultLimit
causes unpaginated requests to be truncated to the default page size; instead,
detect pageReq == nil and only set CountTotal = true (or leave pageReq nil) so
you do not override the intended unlimited behavior — update the nil check
around pageReq in the query handlers (symbols: limit, offset, countTotal,
pageReq, and the request handlers Attribute, Attributes, Scan,
AttributeAccounts) to avoid creating a PageRequest that forces the default limit
(e.g., if pageReq == nil set countTotal=true or set limit to math.MaxUint64
rather than assigning pageReq = &query.PageRequest{CountTotal:true}).
- Around line 110-116: AttributeAccounts currently dereferences req
unconditionally (req.AttributeName) which will panic on a nil gRPC request; add
an initial nil check at the top of Keeper.AttributeAccounts (for
types.QueryAttributeAccountsRequest) and return a proper InvalidArgument gRPC
error (e.g., using status.Error(codes.InvalidArgument, "invalid request") or
sdkerrors.Wrap) when req == nil, then proceed to use req.AttributeName, keeping
the rest of the nameHashRange/nameAddrPageWalk logic unchanged.

---

Outside diff comments:
In `@x/attribute/keeper/keeper.go`:
- Around line 265-286: The code computes normalizedOrigName via
k.nameKeeper.Normalize but still calls types.BuildAttrTriple(originalAttribute)
using the un-normalized originalAttribute.Name, causing a lookup miss; update
originalAttribute.Name = normalizedOrigName (or otherwise construct the triple
using normalizedOrigName) before calling types.BuildAttrTriple so origKey is
built from the normalized original name and subsequent lookups succeed; ensure
any later uses of originalAttribute reflect this change and keep the call sites:
normalizedOrigName, originalAttribute, types.BuildAttrTriple, and origKey in
mind while editing.
🪄 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: 9415147e-f803-49f8-b58d-fac361a94099

📥 Commits

Reviewing files that changed from the base of the PR and between ed9a492 and c3aad1f.

📒 Files selected for processing (9)
  • app/app.go
  • x/attribute/keeper/keeper.go
  • x/attribute/keeper/migration_test.go
  • x/attribute/keeper/migrations.go
  • x/attribute/keeper/params.go
  • x/attribute/keeper/query_server.go
  • x/attribute/module.go
  • x/attribute/types/codec.go
  • x/attribute/types/keys_test.go

Comment on lines 143 to +150
func (k Keeper) IterateRecords(ctx sdk.Context, prefix []byte, handle Handler) error {
// Init an attribute record iterator
store := ctx.KVStore(k.storeKey)
iterator := storetypes.KVStorePrefixIterator(store, prefix)
defer iterator.Close() //nolint:errcheck // close error safe to ignore in this context.

// Iterate over records, processing callbacks.
for ; iterator.Valid(); iterator.Next() {
record := types.Attribute{}
// get proto objects for legacy prefix with legacy amino codec.
if err := k.cdc.Unmarshal(iterator.Value(), &record); err != nil {
return err
}
if err := handle(record); err != nil {
return err
return k.attributes.Walk(ctx, nil, func(_ types.AttrTriple, record types.Attribute) (stop bool, err error) {
if err = handle(record); err != nil {
return true, err
}
}
return nil
return false, nil
})

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

Preserve the prefix filter in IterateRecords.

Walk(ctx, nil, ...) now ignores the caller-supplied prefix, so any caller asking for a subset will receive every attribute instead. Please derive the walk range from prefix here, the same way prefixScan does.

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

In `@x/attribute/keeper/keeper.go` around lines 143 - 150, IterateRecords
currently ignores the caller-supplied prefix by calling k.attributes.Walk(ctx,
nil, ...); update it to derive the walk range from the provided prefix the same
way prefixScan does and pass that range instead of nil. Locate the
IterateRecords method and replace the nil range argument to k.attributes.Walk
with the start/end (or range) values produced by the prefix->range helper used
by prefixScan so Walk only iterates keys under the supplied prefix; keep the
handler invocation and error handling the same.

Comment on lines +216 to +238
func (k Keeper) incNameAddrCount(ctx sdk.Context, name string, addrBytes []byte) error {
key := types.BuildNameAddrPair(name, addrBytes)
current, err := k.nameAddrCounts.Get(ctx, key)
if err != nil {
current = 0
}
return k.nameAddrCounts.Set(ctx, key, current+1)
}

// DecAttrNameAddressLookup decrements the name to account lookups and removes value if decremented to 0
func (k Keeper) DecAttrNameAddressLookup(ctx sdk.Context, name string, addrBytes []byte) {
store := ctx.KVStore(k.storeKey)
key := types.AttributeNameAddrKeyPrefix(name, addrBytes)
bz := store.Get(key)
if bz != nil {
value := binary.BigEndian.Uint64(bz)
if value <= uint64(1) {
store.Delete(key)
} else {
store.Set(key, sdk.Uint64ToBigEndian(value-1))
}
if err := k.decNameAddrCount(ctx, name, addrBytes); err != nil {
k.Logger(ctx).Error("DecAttrNameAddressLookup failed", "error", err)
}
}

func (k Keeper) decNameAddrCount(ctx sdk.Context, name string, addrBytes []byte) error {
key := types.BuildNameAddrPair(name, addrBytes)
current, err := k.nameAddrCounts.Get(ctx, key)
if err != nil || current <= 1 {
return k.nameAddrCounts.Remove(ctx, key)
}
return k.nameAddrCounts.Set(ctx, key, current-1)

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

🧩 Analysis chain

🏁 Script executed:

cd /tmp && find . -type f -name "keeper.go" | grep attribute | head -5

Repository: provenance-io/provenance

Length of output: 50


🏁 Script executed:

# First, let's find the file and examine the context
find . -type f -path "*/attribute/keeper/*.go" | head -10

Repository: provenance-io/provenance

Length of output: 423


🏁 Script executed:

# Search for nameAddrCounts definition
rg "nameAddrCounts" --type go -B 2 -A 2 | head -50

Repository: provenance-io/provenance

Length of output: 3541


🏁 Script executed:

# Look for collections package and Get() usage patterns
rg "collections\." --type go -A 3 | grep -E "(\.Get|\.Remove|\.Set)" | head -30

Repository: provenance-io/provenance

Length of output: 1783


🏁 Script executed:

# Check what error types the collections Get() can return
find . -type f -name "*.go" -path "*/collections/*" | head -5

Repository: provenance-io/provenance

Length of output: 50


🏁 Script executed:

# Look for collections package imports and find error handling patterns
rg "collections\." --type go -B 1 -A 1 | grep -E "(not.*found|NotFound|ErrNotFound|collections\.ErrNotFound)" | head -20

Repository: provenance-io/provenance

Length of output: 695


🏁 Script executed:

# Search for AccountsByAttribute to understand the risk of drift
rg "AccountsByAttribute" --type go -B 3 -A 10 | head -60

Repository: provenance-io/provenance

Length of output: 3974


🏁 Script executed:

# Look for how other Get() calls in the codebase handle errors from collections
rg "\.Get\(ctx," --type go -B 1 -A 3 | grep -E "(if err.*nil|ErrNotFound|collections)" | head -40

Repository: provenance-io/provenance

Length of output: 1071


🏁 Script executed:

# Find the collections package to understand Get() error semantics
find . -type d -name collections | head -5

Repository: provenance-io/provenance

Length of output: 50


🏁 Script executed:

# Look for error handling patterns with collections.Get() - checking if they distinguish not-found
rg "\.Get\(.*\)" --type go -A 2 | grep -B 1 "ErrNotFound\|collections.ErrNotFound" | head -30

Repository: provenance-io/provenance

Length of output: 304


🏁 Script executed:

# Find AccountsByAttribute implementation
rg "func.*AccountsByAttribute" --type go -A 20

Repository: provenance-io/provenance

Length of output: 1533


🏁 Script executed:

# Also check how nameAddrCounts is used throughout the keeper
rg "nameAddrCounts\." --type go -B 2 -A 2

Repository: provenance-io/provenance

Length of output: 1601


Distinguish not-found errors from other failures.

Both incNameAddrCount and decNameAddrCount should only treat collections.ErrNotFound as "missing"—other errors (decode failures, store corruption) are distinct and should not be silently treated as zero or trigger removal. Collapsing all errors creates a mismatch: the nameAddrCounts map can lose entries or counts while the attributes map remains intact, causing AccountsByAttribute to drift from the actual stored attributes. Use errors.Is(err, collections.ErrNotFound) to distinguish, matching the pattern used elsewhere in the codebase (e.g., ledger and registry keepers).

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

In `@x/attribute/keeper/keeper.go` around lines 216 - 238, The current
incNameAddrCount and decNameAddrCount swallow all errors from nameAddrCounts.Get
and treat them as "not found"; change them to only treat collections.ErrNotFound
as missing and propagate any other errors. Specifically, in incNameAddrCount,
call k.nameAddrCounts.Get and if err != nil then if errors.Is(err,
collections.ErrNotFound) set current = 0 else return err; then Set the
incremented value. In decNameAddrCount, call Get and if err != nil then if
errors.Is(err, collections.ErrNotFound) return nil (nothing to decrement) else
return err; otherwise apply the existing current<=1 removal or Set(current-1).
Use errors.Is(err, collections.ErrNotFound) exactly and keep the existing calls
to k.nameAddrCounts.Get, Set, and Remove and the DecAttrNameAddressLookup
logging behavior.

Comment on lines +486 to 500
var keysToRemove []types.AttrTriple
if walkErr := k.attributes.Walk(ctx, rng, func(key types.AttrTriple, attr types.Attribute) (stop bool, err error) {
if bytes.Equal(key.AddrBytes, []byte(acct)) && attr.Name == name {
keysToRemove = append(keysToRemove, key)
}
return false, nil
}); walkErr != nil {
return walkErr
}
for _, key := range keysToRemove {
if err = k.attributes.Remove(ctx, key); err != nil {
return err
}
k.DecAttrNameAddressLookup(ctx, name, acct)
}

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

Purge needs to remove expirationIndex entries too.

This path removes the primary attribute row and decrements nameAddrCounts, but it never clears expirationIndex. That leaves orphaned expiry sentinels behind, so later expiry sweeps waste limit budget on already-purged records and log false missing-attribute errors.

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

In `@x/attribute/keeper/keeper.go` around lines 486 - 500, The purge loop removes
the primary attribute rows and decrements name-address counts but never clears
the corresponding entries in expirationIndex, leaving orphaned expiry sentinels;
after successfully removing the attribute with k.attributes.Remove(ctx, key) and
updating counts via k.DecAttrNameAddressLookup(ctx, name, acct), also remove the
matching expiration index entry from k.expirationIndex (e.g. call
k.expirationIndex.Remove(ctx, key) or the equivalent method your implementation
exposes) and handle/return any error from that call so expired-index sentinels
are not left behind.

Comment on lines +609 to 625
attr, getErr := k.attributes.Get(ctx, entry.attrKey)
if getErr == nil {
if removeErr := k.attributes.Remove(ctx, entry.attrKey); removeErr == nil {
k.DecAttrNameAddressLookup(ctx, attr.Name, attr.GetAddressBytes())
if emitErr := ctx.EventManager().EmitTypedEvent(types.NewEventAttributeExpired(attr)); emitErr != nil {
ctx.Logger().Error(fmt.Sprintf("failed to emit typed event %v", emitErr))
}
count++
} else {
ctx.Logger().Error(fmt.Sprintf("unable to unmarshal attribute to delete key: %v error: %v", attrKey, err))
ctx.Logger().Error(fmt.Sprintf("unable to remove attribute: %v error: %v", entry.attrKey, removeErr))
}
} else {
ctx.Logger().Error(fmt.Sprintf("unable to get attribute for expiry key: %v error: %v", entry.expKey, getErr))
}

// delete the expiration lookup key
store.Delete(expirationKey)
if limit != 0 && count >= limit {
break
if removeExpErr := k.expirationIndex.Remove(ctx, entry.expKey); removeExpErr != nil {
ctx.Logger().Error(fmt.Sprintf("unable to remove expiration entry: %v error: %v", entry.expKey, removeExpErr))
}

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

Don't drop the expiry sentinel when primary deletion failed.

expirationIndex.Remove runs unconditionally, even when k.attributes.Remove failed. That loses the only retry handle for this expired record, so a transient remove error can leave an expired attribute in state permanently.

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

In `@x/attribute/keeper/keeper.go` around lines 609 - 625, The expiration index
entry is being removed unconditionally even when k.attributes.Remove fails,
which loses the retry handle; change the logic so k.expirationIndex.Remove(ctx,
entry.expKey) is only invoked after a successful attribute deletion (i.e., when
attr was fetched (k.attributes.Get returned nil) and k.attributes.Remove
returned nil). Keep the existing log paths for Get/Remove failures and, on a
failed k.attributes.Remove, do not call k.expirationIndex.Remove so the
expiration sentinel remains for retry; retain the EmitTypedEvent and
DecAttrNameAddressLookup calls in the successful-removal branch.

Comment on lines +11 to 16
params, err := k.params.Get(ctx)
if err != nil {
return types.Params{
MaxValueLength: types.DefaultMaxValueLength,
}
}

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

Don't treat every params read failure as "use defaults".

This now falls back to DefaultMaxValueLength for any k.params.Get failure, not just the "params not set yet" case. A bad on-disk params encoding or wrong collection key would be silently masked, which is especially dangerous for a no-migration storage rewrite. Please only default the missing-entry case and fail loudly on unexpected read errors.

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

In `@x/attribute/keeper/params.go` around lines 11 - 16, The current handler
treats any error from k.params.Get(ctx) as "params missing" and returns
types.Params{MaxValueLength: types.DefaultMaxValueLength}; change this to only
use the default when the Get error is the known "missing entry"/not-found
condition (e.g., check for sdkerrors.ErrNotFound or store's IsNotFound error
depending on your params store implementation) and otherwise surface the
unexpected error (propagate/return it or wrap and fail loudly) instead of
silently returning defaults; update the function signature to return an error if
needed and keep references to k.params.Get, types.Params, and
types.DefaultMaxValueLength to locate and patch the code.

Comment on lines 110 to +116
func (k Keeper) AttributeAccounts(c context.Context, req *types.QueryAttributeAccountsRequest) (*types.QueryAttributeAccountsResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}
ctx := sdk.UnwrapSDKContext(c)
accounts := make([]string, 0)
store := ctx.KVStore(k.storeKey)
keyPrefix := types.AttributeNameKeyPrefix(req.AttributeName)
attributeStore := prefix.NewStore(store, keyPrefix)

pageRes, err := query.FilteredPaginate(attributeStore, req.Pagination, func(key []byte, _ []byte, accumulate bool) (bool, error) {
addressLength := int32(key[0])
address := sdk.AccAddress(key[1 : addressLength+1])
if slices.Contains(accounts, address.String()) {
return false, nil
}
if accumulate {
accounts = append(accounts, address.String())
}
return true, nil
})
var nameHash [32]byte
copy(nameHash[:], types.GetNameKeyBytes(req.AttributeName))

rng, endBound := nameHashRange(nameHash)
accounts, pageRes, err := nameAddrPageWalk(ctx, k.nameAddrCounts, rng, endBound, req.Pagination)

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 AttributeAccounts before dereferencing req.

Line 113 reads req.AttributeName unconditionally, so a nil gRPC request now panics instead of returning InvalidArgument like the other query handlers.

Suggested fix
 func (k Keeper) AttributeAccounts(c context.Context, req *types.QueryAttributeAccountsRequest) (*types.QueryAttributeAccountsResponse, error) {
+	if req == nil {
+		return nil, status.Error(codes.InvalidArgument, "invalid request")
+	}
+	if req.AttributeName == "" {
+		return nil, status.Error(codes.InvalidArgument, "empty attribute name")
+	}
 	ctx := sdk.UnwrapSDKContext(c)
 	var nameHash [32]byte
 	copy(nameHash[:], types.GetNameKeyBytes(req.AttributeName))
📝 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
func (k Keeper) AttributeAccounts(c context.Context, req *types.QueryAttributeAccountsRequest) (*types.QueryAttributeAccountsResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}
ctx := sdk.UnwrapSDKContext(c)
accounts := make([]string, 0)
store := ctx.KVStore(k.storeKey)
keyPrefix := types.AttributeNameKeyPrefix(req.AttributeName)
attributeStore := prefix.NewStore(store, keyPrefix)
pageRes, err := query.FilteredPaginate(attributeStore, req.Pagination, func(key []byte, _ []byte, accumulate bool) (bool, error) {
addressLength := int32(key[0])
address := sdk.AccAddress(key[1 : addressLength+1])
if slices.Contains(accounts, address.String()) {
return false, nil
}
if accumulate {
accounts = append(accounts, address.String())
}
return true, nil
})
var nameHash [32]byte
copy(nameHash[:], types.GetNameKeyBytes(req.AttributeName))
rng, endBound := nameHashRange(nameHash)
accounts, pageRes, err := nameAddrPageWalk(ctx, k.nameAddrCounts, rng, endBound, req.Pagination)
func (k Keeper) AttributeAccounts(c context.Context, req *types.QueryAttributeAccountsRequest) (*types.QueryAttributeAccountsResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}
if req.AttributeName == "" {
return nil, status.Error(codes.InvalidArgument, "empty attribute name")
}
ctx := sdk.UnwrapSDKContext(c)
var nameHash [32]byte
copy(nameHash[:], types.GetNameKeyBytes(req.AttributeName))
rng, endBound := nameHashRange(nameHash)
accounts, pageRes, err := nameAddrPageWalk(ctx, k.nameAddrCounts, rng, endBound, req.Pagination)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@x/attribute/keeper/query_server.go` around lines 110 - 116, AttributeAccounts
currently dereferences req unconditionally (req.AttributeName) which will panic
on a nil gRPC request; add an initial nil check at the top of
Keeper.AttributeAccounts (for types.QueryAttributeAccountsRequest) and return a
proper InvalidArgument gRPC error (e.g., using
status.Error(codes.InvalidArgument, "invalid request") or sdkerrors.Wrap) when
req == nil, then proceed to use req.AttributeName, keeping the rest of the
nameHashRange/nameAddrPageWalk logic unchanged.

Comment on lines +150 to +156
limit := uint64(query.DefaultLimit)
offset := uint64(0)
countTotal := false

if pageReq == nil {
pageReq = &query.PageRequest{CountTotal: true}
}

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

Nil pagination now truncates these queries to the default page size.

Both helpers start with limit := query.DefaultLimit and then manufacture a non-nil PageRequest when the caller omitted pagination. That means unpaginated Attribute, Attributes, Scan, and AttributeAccounts requests only return the first page once the result set grows past the default limit.

Also applies to: 231-237

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

In `@x/attribute/keeper/query_server.go` around lines 150 - 156, The current
nil-page handling sets pageReq to a non-nil PageRequest, which together with the
earlier local limit := query.DefaultLimit causes unpaginated requests to be
truncated to the default page size; instead, detect pageReq == nil and only set
CountTotal = true (or leave pageReq nil) so you do not override the intended
unlimited behavior — update the nil check around pageReq in the query handlers
(symbols: limit, offset, countTotal, pageReq, and the request handlers
Attribute, Attributes, Scan, AttributeAccounts) to avoid creating a PageRequest
that forces the default limit (e.g., if pageReq == nil set countTotal=true or
set limit to math.MaxUint64 rather than assigning pageReq =
&query.PageRequest{CountTotal:true}).

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.

Switch to collections in the attribute module.

1 participant