From c3aad1fcfdd12365b3cc79e6040d6ed51d429674 Mon Sep 17 00:00:00 2001 From: nagarajdivine Date: Thu, 16 Apr 2026 22:53:39 +0800 Subject: [PATCH 1/2] switch to collections in the attribute module. --- app/app.go | 2 +- x/attribute/keeper/keeper.go | 527 ++++++++++++++++----------- x/attribute/keeper/migration_test.go | 94 +++++ x/attribute/keeper/migrations.go | 23 ++ x/attribute/keeper/params.go | 15 +- x/attribute/keeper/query_server.go | 279 +++++++++----- x/attribute/module.go | 7 +- x/attribute/types/codec.go | 296 +++++++++++++++ x/attribute/types/keys_test.go | 77 ++++ 9 files changed, 1007 insertions(+), 313 deletions(-) create mode 100644 x/attribute/keeper/migration_test.go diff --git a/app/app.go b/app/app.go index 578ccc4f82..dc6e779d3f 100644 --- a/app/app.go +++ b/app/app.go @@ -553,7 +553,7 @@ func New( app.NameKeeper = namekeeper.NewKeeper(appCodec, keys[nametypes.StoreKey]) app.AttributeKeeper = attributekeeper.NewKeeper( - appCodec, keys[attributetypes.StoreKey], app.AccountKeeper, &app.NameKeeper, + appCodec, runtime.NewKVStoreService(keys[attributetypes.StoreKey]), app.AccountKeeper, &app.NameKeeper, ) markerReqAttrBypassAddrs := []sdk.AccAddress{ diff --git a/x/attribute/keeper/keeper.go b/x/attribute/keeper/keeper.go index dfc80f28ce..0b78bf612f 100644 --- a/x/attribute/keeper/keeper.go +++ b/x/attribute/keeper/keeper.go @@ -2,12 +2,12 @@ package keeper import ( "bytes" - "encoding/binary" "fmt" "strings" + "cosmossdk.io/collections" + "cosmossdk.io/core/store" "cosmossdk.io/log" - storetypes "cosmossdk.io/store/types" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/telemetry" @@ -28,15 +28,30 @@ type Keeper struct { // The keeper used for ensuring names resolve to owners. nameKeeper types.NameKeeper - // Key to access the key-value store from sdk.Context. - storeKey storetypes.StoreKey - // The codec for binary encoding/decoding. cdc codec.BinaryCodec modAddr sdk.AccAddress authority string + // Collections schema. + schema collections.Schema + + // attributes stores each Attribute. + // Key prefix: 0x02 Layout: [len(addr)][addr][sha256(name)][sha256(value)] + attributes collections.Map[types.AttrTriple, types.Attribute] + + // nameAddrCounts holds the per-(name, addr) attribute reference counter. + // Key prefix: 0x03 Layout: [sha256(name)][len(addr)][addr] + nameAddrCounts collections.Map[types.NameAddrPair, uint64] + + // expirationIndex is a sentinel index ordered by expiration epoch. + // Key prefix: 0x04 Layout: [8-byte epoch][len(addr)][addr][sha256(name)][sha256(value)] + expirationIndex collections.Map[types.ExpireTriple, bool] + + // params holds the module Params (MaxValueLength). + // Key prefix: 0x05 — identical to old AttributeParamPrefix. + params collections.Item[types.Params] } // NewKeeper returns an attribute keeper. It handles: @@ -46,19 +61,31 @@ type Keeper struct { // // CONTRACT: the parameter Subspace must have the param key table already initialized func NewKeeper( - cdc codec.BinaryCodec, key storetypes.StoreKey, + cdc codec.BinaryCodec, storeService store.KVStoreService, authKeeper types.AccountKeeper, nameKeeper types.NameKeeper, ) Keeper { - keeper := Keeper{ - storeKey: key, - authKeeper: authKeeper, - nameKeeper: nameKeeper, - cdc: cdc, - modAddr: authtypes.NewModuleAddress(types.ModuleName), - authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(), - } - nameKeeper.SetAttributeKeeper(keeper) - return keeper + sb := collections.NewSchemaBuilder(storeService) + + k := Keeper{ + authKeeper: authKeeper, + nameKeeper: nameKeeper, + cdc: cdc, + modAddr: authtypes.NewModuleAddress(types.ModuleName), + authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(), + attributes: collections.NewMap(sb, collections.NewPrefix(types.AttributeKeyPrefix), "attributes", types.AttrTripleKey, codec.CollValue[types.Attribute](cdc)), + nameAddrCounts: collections.NewMap(sb, types.AttributeAddrLookupKeyPrefix, "name_addr_counts", types.NameAddrPairKey, types.Uint64Value), + expirationIndex: collections.NewMap(sb, types.AttributeExpirationKeyPrefix, "expiration_index", types.ExpireTripleKey, types.SentinelValue), + params: collections.NewItem(sb, types.AttributeParamPrefix, "params", codec.CollValue[types.Params](cdc)), + } + + schema, err := sb.Build() + if err != nil { + panic(fmt.Errorf("attribute: failed to build collections schema: %w", err)) + } + k.schema = schema + + nameKeeper.SetAttributeKeeper(k) + return k } // GetAuthority is signer of the proposal @@ -115,22 +142,12 @@ func (k Keeper) GetAttributes(ctx sdk.Context, addr string, name string) ([]type // IterateRecords iterates over all the stored attribute records and passes them to a callback function. 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 + }) } // SetAttribute stores an attribute under the given account. The attribute name must resolve to the given owner address. @@ -167,55 +184,60 @@ func (k Keeper) SetAttribute( if !k.nameKeeper.ResolvesTo(ctx, attr.Name, owner) { return fmt.Errorf("%q does not resolve to address %q", attr.Name, owner.String()) } - // Store the sanitized account attribute - bz, err := k.cdc.Marshal(&attr) - if err != nil { - return err + key := types.BuildAttrTriple(attr) + has, hasErr := k.attributes.Has(ctx, key) + if hasErr != nil { + return hasErr } + isNew := !has - key := types.AddrAttributeKey(attr.GetAddressBytes(), attr) - - store := ctx.KVStore(k.storeKey) - isNew := !store.Has(key) - store.Set(key, bz) + if err = k.attributes.Set(ctx, key, attr); err != nil { + return err + } if isNew { - k.IncAttrNameAddressLookup(ctx, attr.Name, attr.GetAddressBytes()) + if err = k.incNameAddrCount(ctx, attr.Name, attr.GetAddressBytes()); err != nil { + return err + } + } + if err = k.addExpireEntry(ctx, attr); err != nil { + return err } - k.addAttributeExpireLookup(store, attr) - attributeAddEvent := types.NewEventAttributeAdd(attr, owner.String()) - return ctx.EventManager().EmitTypedEvent(attributeAddEvent) } // IncAttrNameAddressLookup increments the count of name to address lookups func (k Keeper) IncAttrNameAddressLookup(ctx sdk.Context, name string, addrBytes []byte) { - store := ctx.KVStore(k.storeKey) - key := types.AttributeNameAddrKeyPrefix(name, addrBytes) - bz := store.Get(key) - id := uint64(0) - if bz != nil { - id = binary.BigEndian.Uint64(bz) - } - bz = sdk.Uint64ToBigEndian(id + 1) - store.Set(key, bz) + if err := k.incNameAddrCount(ctx, name, addrBytes); err != nil { + k.Logger(ctx).Error("IncAttrNameAddressLookup failed", "error", err) + } +} + +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) +} + // UpdateAttribute updates an attribute under the given account. The attribute name must resolve to the given owner address and value must resolve to an existing attribute. func (k Keeper) UpdateAttribute(ctx sdk.Context, originalAttribute types.Attribute, updateAttribute types.Attribute, owner sdk.AccAddress, ) error { @@ -259,36 +281,36 @@ func (k Keeper) UpdateAttribute(ctx sdk.Context, originalAttribute types.Attribu return fmt.Errorf("%q does not resolve to address %q", updateAttribute.Name, owner.String()) } - store := ctx.KVStore(k.storeKey) addrBz := originalAttribute.GetAddressBytes() - attrKey := types.AddrAttributeKey(addrBz, originalAttribute) - currentAttr := store.Get(attrKey) + origKey := types.BuildAttrTriple(originalAttribute) - var found bool - if currentAttr != nil { - attr := types.Attribute{} - if err := k.cdc.Unmarshal(currentAttr, &attr); err != nil { - return err - } + currentAttr, getErr := k.attributes.Get(ctx, origKey) - if attr.AttributeType == originalAttribute.AttributeType { + var found bool + if getErr == nil { + if currentAttr.AttributeType == originalAttribute.AttributeType { found = true - store.Delete(attrKey) - k.DecAttrNameAddressLookup(ctx, attr.Name, addrBz) - k.deleteAttributeExpireLookup(store, attr) + if err = k.attributes.Remove(ctx, origKey); err != nil { + return err + } + k.DecAttrNameAddressLookup(ctx, currentAttr.Name, addrBz) + if err = k.removeExpireEntry(ctx, currentAttr); err != nil { + return err + } - bz, err := k.cdc.Marshal(&updateAttribute) - if err != nil { + if err = k.attributes.Set(ctx, types.BuildAttrTriple(updateAttribute), updateAttribute); err != nil { + return err + } + if err = k.incNameAddrCount(ctx, updateAttribute.Name, updateAttribute.GetAddressBytes()); err != nil { + return err + } + if err = k.addExpireEntry(ctx, updateAttribute); err != nil { return err } - updatedKey := types.AddrAttributeKey(addrBz, updateAttribute) - store.Set(updatedKey, bz) - k.IncAttrNameAddressLookup(ctx, updateAttribute.Name, updateAttribute.GetAddressBytes()) - k.addAttributeExpireLookup(store, updateAttribute) attributeUpdateEvent := types.NewEventAttributeUpdate(originalAttribute, updateAttribute, owner.String()) - if err := ctx.EventManager().EmitTypedEvent(attributeUpdateEvent); err != nil { + if err = ctx.EventManager().EmitTypedEvent(attributeUpdateEvent); err != nil { return err } } @@ -323,35 +345,32 @@ func (k Keeper) UpdateAttributeExpiration(ctx sdk.Context, updateAttribute types return fmt.Errorf("%q does not resolve to address %q", updateAttribute.Name, owner.String()) } - store := ctx.KVStore(k.storeKey) - attrKey := types.AddrAttributeKey(updateAttribute.GetAddressBytes(), updateAttribute) - currentAttr := store.Get(attrKey) - if currentAttr != nil { - attr := types.Attribute{} - if err := k.cdc.Unmarshal(currentAttr, &attr); err != nil { - return err - } + origKey := types.BuildAttrTriple(updateAttribute) + attr, getErr := k.attributes.Get(ctx, origKey) + if getErr != nil { + errorMessage := "no attributes updated" + ctx.Logger().Error(errorMessage, "name", updateAttribute.Name, "value", string(updateAttribute.Value)) + return fmt.Errorf("%s with name %q : value %q : type: %s", + errorMessage, updateAttribute.Name, string(updateAttribute.Value), updateAttribute.AttributeType.String()) + } - k.deleteAttributeExpireLookup(store, attr) + if err := k.removeExpireEntry(ctx, attr); err != nil { + return err + } - originalExpiration := attr.ExpirationDate - attr.ExpirationDate = updateAttribute.ExpirationDate - bz, err := k.cdc.Marshal(&attr) - if err != nil { - return err - } - store.Set(attrKey, bz) + originalExpiration := attr.ExpirationDate + attr.ExpirationDate = updateAttribute.ExpirationDate - k.addAttributeExpireLookup(store, attr) + if err := k.attributes.Set(ctx, origKey, attr); err != nil { + return err + } + if err := k.addExpireEntry(ctx, attr); err != nil { + return err + } - attributeExpirationUpdateEvent := types.NewEventAttributeExpirationUpdate(attr, originalExpiration, owner.String()) - if err := ctx.EventManager().EmitTypedEvent(attributeExpirationUpdateEvent); err != nil { - return err - } - } else { - errorMessage := "no attributes updated" - ctx.Logger().Error(errorMessage, "name", updateAttribute.Name, "value", string(updateAttribute.Value)) - return fmt.Errorf("%s with name %q : value %q : type: %s", errorMessage, updateAttribute.Name, string(updateAttribute.Value), updateAttribute.AttributeType.String()) + attributeExpirationUpdateEvent := types.NewEventAttributeExpirationUpdate(attr, originalExpiration, owner.String()) + if err := ctx.EventManager().EmitTypedEvent(attributeExpirationUpdateEvent); err != nil { + return err } return nil @@ -359,17 +378,17 @@ func (k Keeper) UpdateAttributeExpiration(ctx sdk.Context, updateAttribute types // AccountsByAttribute returns a list of sdk.AccAddress that have attribute name assigned func (k Keeper) AccountsByAttribute(ctx sdk.Context, name string) (addresses []sdk.AccAddress, err error) { - store := ctx.KVStore(k.storeKey) - keyPrefix := types.AttributeNameKeyPrefix(name) - it := storetypes.KVStorePrefixIterator(store, keyPrefix) - defer it.Close() //nolint:errcheck // close error safe to ignore in this context. - for ; it.Valid(); it.Next() { - addressBytes, err := types.GetAddressFromKey(it.Key()) - if err != nil { - return nil, err + var nameHash [32]byte + copy(nameHash[:], types.GetNameKeyBytes(name)) + rng, _ := nameHashRange(nameHash) + err = k.nameAddrCounts.Walk(ctx, rng, func(key types.NameAddrPair, _ uint64) (stop bool, walkErr error) { + if key.NameHash == nameHash { + addrCopy := make([]byte, len(key.AddrBytes)) + copy(addrCopy, key.AddrBytes) + addresses = append(addresses, addrCopy) } - addresses = append(addresses, addressBytes) - } + return false, nil + }) return } @@ -392,42 +411,41 @@ func (k Keeper) DeleteAttribute(ctx sdk.Context, addr string, name string, value } // else name does not exist (anymore) so we can't enforce permission check on delete here, proceed. } + addrz := types.GetAttributeAddressBytes(addr) + var nameHash [32]byte + copy(nameHash[:], types.GetNameKeyBytes(name)) + rng, _ := attrAddrNameRange(addrz, nameHash) + attrToDelete := make([]types.Attribute, 0) - store := ctx.KVStore(k.storeKey) - iter := storetypes.KVStorePrefixIterator(store, types.AddrStrAttributesNameKeyPrefix(addr, name)) - defer func() { - if iter != nil { - iter.Close() //nolint:errcheck,gosec // close error safe to ignore in this context + walkErr := k.attributes.Walk(ctx, rng, func(_ types.AttrTriple, attr types.Attribute) (stop bool, err error) { + if attr.Address != addr || attr.Name != name { + return false, nil } - }() - - attrToDelete := []types.Attribute{} // do delete logic outside of iterator - for ; iter.Valid(); iter.Next() { - attr := types.Attribute{} - if err := k.cdc.Unmarshal(iter.Value(), &attr); err != nil { - return err - } - - if attr.Name == name && (!deleteDistinct || bytes.Equal(*value, attr.Value)) { - attrToDelete = append(attrToDelete, attr) + if deleteDistinct && !bytes.Equal(*value, attr.Value) { + return false, nil } + attrToDelete = append(attrToDelete, attr) + return false, nil + }) + if walkErr != nil { + return walkErr } - iter.Close() //nolint:errcheck,gosec // close error safe to ignore in this context - iter = nil for _, attr := range attrToDelete { addrBz := attr.GetAddressBytes() - store.Delete(types.AddrAttributeKey(addrBz, attr)) + if err := k.attributes.Remove(ctx, types.BuildAttrTriple(attr)); err != nil { + return err + } k.DecAttrNameAddressLookup(ctx, attr.Name, addrBz) - k.deleteAttributeExpireLookup(store, attr) + if err := k.removeExpireEntry(ctx, attr); err != nil { + return err + } if !deleteDistinct { - deleteEvent := types.NewEventAttributeDelete(name, addr, owner.String()) - if err := ctx.EventManager().EmitTypedEvent(deleteEvent); err != nil { + if err := ctx.EventManager().EmitTypedEvent(types.NewEventAttributeDelete(name, addr, owner.String())); err != nil { return err } } else { - deleteEvent := types.NewEventDistinctAttributeDelete(name, string(*value), addr, owner.String()) - if err := ctx.EventManager().EmitTypedEvent(deleteEvent); err != nil { + if err := ctx.EventManager().EmitTypedEvent(types.NewEventDistinctAttributeDelete(name, string(*value), addr, owner.String())); err != nil { return err } } @@ -460,45 +478,62 @@ func (k Keeper) PurgeAttribute(ctx sdk.Context, name string, owner sdk.AccAddres if err != nil { return err } - store := ctx.KVStore(k.storeKey) + var nameHash [32]byte + copy(nameHash[:], types.GetNameKeyBytes(name)) + for _, acct := range accts { - attrToDelete := k.getAddrAttributesKeysByName(store, acct, name) - for _, key := range attrToDelete { - store.Delete(key) + rng, _ := attrAddrNameRange(acct, nameHash) + 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) } } return nil } -// getAddrAttributesKeysByName returns an list of attribute keys for the an account and attribute name -func (k Keeper) getAddrAttributesKeysByName(store storetypes.KVStore, acctAddr sdk.AccAddress, attributeName string) (attributeKeys [][]byte) { - it := storetypes.KVStorePrefixIterator(store, types.AddrAttributesNameKeyPrefix(acctAddr, attributeName)) - defer it.Close() //nolint:errcheck // close error safe to ignore in this context. - for ; it.Valid(); it.Next() { - attributeKeys = append(attributeKeys, it.Key()) - } - return -} - // A predicate function for matching names type namePred = func(string) bool // Scan all attributes that match the given prefix. func (k Keeper) prefixScan(ctx sdk.Context, prefix []byte, f namePred) (attrs []types.Attribute, err error) { - store := ctx.KVStore(k.storeKey) - it := storetypes.KVStorePrefixIterator(store, prefix) - defer it.Close() //nolint:errcheck // close error safe to ignore in this context. - for ; it.Valid(); it.Next() { - attr := types.Attribute{} - if err = k.cdc.Unmarshal(it.Value(), &attr); err != nil { - return - } + if len(prefix) < 2 { + return nil, fmt.Errorf("attribute: prefixScan: prefix too short") + } + rest := prefix[1:] + addrLen := int(rest[0]) + if len(rest) < 1+addrLen { + return nil, fmt.Errorf("attribute: prefixScan: prefix truncated at addr") + } + addrBz := rest[1 : 1+addrLen] + rest = rest[1+addrLen:] + + var rng *collections.Range[types.AttrTriple] + if len(rest) == 32 { + var nameHash [32]byte + copy(nameHash[:], rest) + rng, _ = attrAddrNameRange(addrBz, nameHash) + } else { + rng, _ = attrAddrRange(addrBz) + } + + err = k.attributes.Walk(ctx, rng, func(_ types.AttrTriple, attr types.Attribute) (bool, error) { if f(attr.Name) { attrs = append(attrs, attr) } - } - return + return false, nil + }) + return attrs, err } // A genesis helper that imports attribute state without owner checks. @@ -519,80 +554,79 @@ func (k Keeper) importAttribute(ctx sdk.Context, attr types.Attribute) error { return fmt.Errorf("unable to normalize attribute name %q: %w", attrNameOrig, err) } // Store the sanitized account attribute - bz, err := k.cdc.Marshal(&attr) - if err != nil { + key := types.BuildAttrTriple(attr) + + has, hasErr := k.attributes.Has(ctx, key) + if hasErr != nil { + return hasErr + } + isNew := !has + + if err = k.attributes.Set(ctx, key, attr); err != nil { return err } - key := types.AddrAttributeKey(attr.GetAddressBytes(), attr) - store := ctx.KVStore(k.storeKey) - isNew := !store.Has(key) - store.Set(key, bz) if isNew { - k.IncAttrNameAddressLookup(ctx, attr.Name, attr.GetAddressBytes()) + if err = k.incNameAddrCount(ctx, attr.Name, attr.GetAddressBytes()); err != nil { + return err + } + } + if err := k.addExpireEntry(ctx, attr); err != nil { + return err } - k.addAttributeExpireLookup(store, attr) return nil } // DeleteExpiredAttributes find and delete expired attributes returns the total deleted // limit sets the max amount to delete in a call, 0 for not limit func (k Keeper) DeleteExpiredAttributes(ctx sdk.Context, limit int) int { - expirationKeys := [][]byte{} - store := ctx.KVStore(k.storeKey) + upperEpoch := ctx.BlockTime().Unix() + + endBound := types.ExpireTriple{EpochSecs: upperEpoch} + expiredRange := new(collections.Range[types.ExpireTriple]).EndExclusive(endBound) - iterator := store.Iterator(types.AttributeExpirationKeyPrefix, types.GetAttributeExpireTimePrefix(ctx.BlockTime())) - for ; iterator.Valid(); iterator.Next() { - expirationKeys = append(expirationKeys, iterator.Key()) + type expiredEntry struct { + expKey types.ExpireTriple + attrKey types.AttrTriple + } + var toDelete []expiredEntry + + if walkErr := k.expirationIndex.Walk(ctx, expiredRange, func(key types.ExpireTriple, _ bool) (bool, error) { + toDelete = append(toDelete, expiredEntry{ + expKey: key, + attrKey: types.AttrTriple{AddrBytes: key.AddrBytes, NameHash: key.NameHash, ValueHash: key.ValueHash}, + }) + if limit != 0 && len(toDelete) >= limit { + return true, nil + } + return false, nil + }); walkErr != nil { + ctx.Logger().Error("attribute: DeleteExpiredAttributes walk failed", "error", walkErr) + return 0 } - iterator.Close() //nolint:errcheck,gosec // close error safe to ignore in this context. count := 0 - for _, expirationKey := range expirationKeys { - attrKey := types.GetAddrAttributeKeyFromExpireKey(expirationKey) - bz := store.Get(attrKey) - if bz != nil { - var attribute types.Attribute - if err := k.cdc.Unmarshal(bz, &attribute); err == nil { - // delete attribute from store - store.Delete(attrKey) - // dec name to address lookup table count - k.DecAttrNameAddressLookup(ctx, attribute.Name, attribute.GetAddressBytes()) - - deleteExpirationEvent := types.NewEventAttributeExpired(attribute) - if err = ctx.EventManager().EmitTypedEvent(deleteExpirationEvent); err != nil { - ctx.Logger().Error(fmt.Sprintf("failed to emit typed event %v", err)) + for _, entry := range toDelete { + 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)) } } return count } -// addAttributeExpireLookup safely adds attribute expire key to store, if expire date exists, else no-op -func (k Keeper) addAttributeExpireLookup(store storetypes.KVStore, attr types.Attribute) { - expireKey := types.AttributeExpireKey(attr) - if expireKey != nil { - store.Set(expireKey, []byte{}) - } -} - -// deleteAttributeExpireLookup safely removes attribute expire key from store if expire date exists, else no-op -func (k Keeper) deleteAttributeExpireLookup(store storetypes.KVStore, attr types.Attribute) { - expireKey := types.AttributeExpireKey(attr) - if expireKey != nil { - store.Delete(expireKey) - } -} - // ValidateExpirationDate returns error if attribute has an expiration date that is in the past of current block time func (k Keeper) ValidateExpirationDate(ctx sdk.Context, attr types.Attribute) error { if attr.ExpirationDate != nil && attr.ExpirationDate.Unix() < ctx.BlockTime().Unix() { @@ -645,3 +679,64 @@ func (k Keeper) SetAccountData(ctx sdk.Context, addr string, value string) error return ctx.EventManager().EmitTypedEvent(&types.EventAccountDataUpdated{Account: addr}) } +func (k Keeper) addExpireEntry(ctx sdk.Context, attr types.Attribute) error { + key, ok := types.BuildExpireTriple(attr) + if !ok { + return nil + } + return k.expirationIndex.Set(ctx, key, true) +} + +func (k Keeper) removeExpireEntry(ctx sdk.Context, attr types.Attribute) error { + key, ok := types.BuildExpireTriple(attr) + if !ok { + return nil + } + return k.expirationIndex.Remove(ctx, key) +} + +// attrAddrRange returns a Range over all attributes for addrBz, plus its end key. +func attrAddrRange(addrBz []byte) (*collections.Range[types.AttrTriple], *types.AttrTriple) { + rng := new(collections.Range[types.AttrTriple]). + StartInclusive(types.AttrTriple{AddrBytes: addrBz}) + end := make([]byte, len(addrBz)) + copy(end, addrBz) + for i := len(end) - 1; i >= 0; i-- { + end[i]++ + if end[i] != 0 { + endKey := types.AttrTriple{AddrBytes: end} + return rng.EndExclusive(endKey), &endKey + } + } + return rng, nil +} + +// attrAddrNameRange returns a Range over attributes for addrBz+nameHash, plus its end key. +func attrAddrNameRange(addrBz []byte, nameHash [32]byte) (*collections.Range[types.AttrTriple], *types.AttrTriple) { + endHash := nameHash + for i := 31; i >= 0; i-- { + endHash[i]++ + if endHash[i] != 0 { + endKey := types.AttrTriple{AddrBytes: addrBz, NameHash: endHash} + return new(collections.Range[types.AttrTriple]). + StartInclusive(types.AttrTriple{AddrBytes: addrBz, NameHash: nameHash}). + EndExclusive(endKey), &endKey + } + } + return attrAddrRange(addrBz) // nameHash all-0xFF fallback +} + +// nameHashRange returns a Range over all nameAddrCounts entries for nameHash, plus its end key. +func nameHashRange(nameHash [32]byte) (*collections.Range[types.NameAddrPair], *types.NameAddrPair) { + rng := new(collections.Range[types.NameAddrPair]). + StartInclusive(types.NameAddrPair{NameHash: nameHash}) + end := nameHash + for i := 31; i >= 0; i-- { + end[i]++ + if end[i] != 0 { + endKey := types.NameAddrPair{NameHash: end} + return rng.EndExclusive(endKey), &endKey + } + } + return rng, nil +} diff --git a/x/attribute/keeper/migration_test.go b/x/attribute/keeper/migration_test.go new file mode 100644 index 0000000000..f2ce55c8aa --- /dev/null +++ b/x/attribute/keeper/migration_test.go @@ -0,0 +1,94 @@ +package keeper_test + +import ( + "testing" + "time" + + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + "github.com/stretchr/testify/suite" + + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/provenance-io/provenance/app" + simapp "github.com/provenance-io/provenance/app" + "github.com/provenance-io/provenance/x/attribute/keeper" + "github.com/provenance-io/provenance/x/attribute/types" + nametypes "github.com/provenance-io/provenance/x/name/types" +) + +type MigrationTestSuite struct { + suite.Suite + + app *app.App + ctx sdk.Context + + ownerPubKey cryptotypes.PubKey + ownerAddr sdk.AccAddress + ownerBech32 string +} + +func TestMigrationTestSuite(t *testing.T) { + suite.Run(t, new(MigrationTestSuite)) +} + +func (s *MigrationTestSuite) SetupTest() { + s.app = simapp.Setup(s.T()) + s.ctx = s.app.BaseApp.NewContextLegacy(false, cmtproto.Header{Time: time.Now()}) + + s.ownerPubKey = secp256k1.GenPrivKey().PubKey() + s.ownerAddr = sdk.AccAddress(s.ownerPubKey.Address()) + s.ownerBech32 = s.ownerAddr.String() + + // Bind the attribute name to the owner via the NameKeeper genesis, + // same pattern used by KeeperTestSuite.SetupTest. + var nameData nametypes.GenesisState + nameData.Bindings = append(nameData.Bindings, nametypes.NewNameRecord("kyc", s.ownerAddr, false)) + nameData.Bindings = append(nameData.Bindings, nametypes.NewNameRecord("provenance.kyc", s.ownerAddr, false)) + nameData.Params.AllowUnrestrictedNames = false + nameData.Params.MaxNameLevels = 3 + nameData.Params.MinSegmentLength = 3 + nameData.Params.MaxSegmentLength = 12 + s.app.NameKeeper.InitGenesis(s.ctx, nameData) + + // Materialize the owner account so SetAttribute's GetAccount check passes. + s.app.AccountKeeper.SetAccount( + s.ctx, + s.app.AccountKeeper.NewAccountWithAddress(s.ctx, s.ownerAddr), + ) +} + +// state written by the old binary must still be readable by the new one. +func (s *MigrationTestSuite) TestMigrate2to3_IsNoOp_AndPreservesState() { + // 1) Seed state through the keeper. + attr := types.Attribute{ + Name: "provenance.kyc", + Value: []byte("verified"), + AttributeType: types.AttributeType_String, + Address: s.ownerBech32, + } + s.Require().NoError( + s.app.AttributeKeeper.SetAttribute(s.ctx, attr, s.ownerAddr), + "SetAttribute must succeed during seeding", + ) + + pre, err := s.app.AttributeKeeper.GetAttributes(s.ctx, s.ownerBech32, attr.Name) + s.Require().NoError(err) + s.Require().Len(pre, 1, "expected one attribute before migration") + + // Run Migrate2to3. + m := keeper.NewMigrator(s.app.AttributeKeeper) + s.Require().NoError(m.Migrate2to3(s.ctx), "Migrate2to3 must not error") + + post, err := s.app.AttributeKeeper.GetAttributes(s.ctx, s.ownerBech32, attr.Name) + s.Require().NoError(err) + s.Require().Len(post, 1, "expected one attribute after migration") + s.Require().Equal(pre[0], post[0], "attribute contents must be byte-identical before/after migration") +} + +// Running Migrate2to3 on an empty store must be safe. +func (s *MigrationTestSuite) TestMigrate2to3_EmptyStore() { + m := keeper.NewMigrator(s.app.AttributeKeeper) + s.Require().NoError(m.Migrate2to3(s.ctx), "Migrate2to3 must handle empty state without error") +} diff --git a/x/attribute/keeper/migrations.go b/x/attribute/keeper/migrations.go index 92902cbad2..4597bc2b8a 100644 --- a/x/attribute/keeper/migrations.go +++ b/x/attribute/keeper/migrations.go @@ -1,5 +1,11 @@ package keeper +import ( + "cosmossdk.io/log" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + // Migrator is a struct for handling in-place store migrations. type Migrator struct { keeper Keeper @@ -9,3 +15,20 @@ type Migrator struct { func NewMigrator(keeper Keeper) Migrator { return Migrator{keeper: keeper} } + +// Migrate2to3 migrates state from consensus version 2 to 3. +// +// This migration converts the attribute module from raw KV-store access +// to cosmossdk.io/collections. +func (m Migrator) Migrate2to3(ctx sdk.Context) error { + logger := m.keeper.Logger(ctx) + logger.Info( + "attribute: Migrate2to3 — KV to Collections is a no-op (byte-identical on-disk layout)", + ) + return nil +} + +// Compile-time assurance the keeper exposes a Logger that returns log.Logger. +var _ interface { + Logger(sdk.Context) log.Logger +} = Keeper{} diff --git a/x/attribute/keeper/params.go b/x/attribute/keeper/params.go index 5bf88c49bf..4373702c35 100644 --- a/x/attribute/keeper/params.go +++ b/x/attribute/keeper/params.go @@ -8,29 +8,20 @@ import ( // GetParams returns the attribute Params. func (k Keeper) GetParams(ctx sdk.Context) (params types.Params) { - store := ctx.KVStore(k.storeKey) - bz := store.Get(types.AttributeParamPrefix) - if bz == nil { + params, err := k.params.Get(ctx) + if err != nil { return types.Params{ MaxValueLength: types.DefaultMaxValueLength, } } - err := k.cdc.Unmarshal(bz, ¶ms) - if err != nil { - panic(err) - } return params } // SetParams sets the account parameters to the param store. func (k Keeper) SetParams(ctx sdk.Context, params types.Params) { - bz, err := k.cdc.Marshal(¶ms) - if err != nil { + if err := k.params.Set(ctx, params); err != nil { panic(err) } - - store := ctx.KVStore(k.storeKey) - store.Set(types.AttributeParamPrefix, bz) } // GetMaxValueLength returns the max value for attribute length. diff --git a/x/attribute/keeper/query_server.go b/x/attribute/keeper/query_server.go index af3d3b3934..30bd2bf137 100644 --- a/x/attribute/keeper/query_server.go +++ b/x/attribute/keeper/query_server.go @@ -2,13 +2,13 @@ package keeper import ( "context" - "slices" + "fmt" "strings" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "cosmossdk.io/store/prefix" + "cosmossdk.io/collections" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/query" @@ -35,30 +35,23 @@ func (k Keeper) Attribute(c context.Context, req *types.QueryAttributeRequest) ( if err := types.ValidateAttributeAddress(req.Account); err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid account address: %v", err) } - ctx := sdk.UnwrapSDKContext(c) - attributes := make([]types.Attribute, 0) - store := ctx.KVStore(k.storeKey) - attributeStore := prefix.NewStore(store, types.AddrStrAttributesNameKeyPrefix(req.Account, req.Name)) - pageRes, err := query.FilteredPaginate(attributeStore, req.Pagination, func(_ []byte, value []byte, accumulate bool) (bool, error) { - var result types.Attribute - err := k.cdc.Unmarshal(value, &result) - if err != nil { - return false, err - } - if result.ExpirationDate != nil && ctx.BlockTime().UTC().After(result.ExpirationDate.UTC()) { - return false, nil - } + ctx := sdk.UnwrapSDKContext(c) + addrBz := types.GetAttributeAddressBytes(req.Account) + var nameHash [32]byte + copy(nameHash[:], types.GetNameKeyBytes(req.Name)) + blockTime := ctx.BlockTime().UTC() - if accumulate { - attributes = append(attributes, result) - } - return true, nil - }) + rng, endBound := attrAddrNameRange(addrBz, nameHash) + attrs, pageRes, err := attrPageWalk(ctx, k.attributes, rng, endBound, req.Pagination, + func(attr types.Attribute) bool { + return attr.ExpirationDate == nil || !blockTime.After(attr.ExpirationDate.UTC()) + }, + ) if err != nil { return nil, err } - return &types.QueryAttributeResponse{Account: req.Account, Attributes: attributes, Pagination: pageRes}, nil + return &types.QueryAttributeResponse{Account: req.Account, Attributes: attrs, Pagination: pageRes}, nil } // Attributes queries for all attributes on a specified account @@ -70,32 +63,19 @@ func (k Keeper) Attributes(c context.Context, req *types.QueryAttributesRequest) return nil, status.Errorf(codes.InvalidArgument, "invalid account address: %v", err) } ctx := sdk.UnwrapSDKContext(c) - attributes := make([]types.Attribute, 0) - store := ctx.KVStore(k.storeKey) - attributeStore := prefix.NewStore(store, types.AddrStrAttributesKeyPrefix(req.Account)) - - pageRes, err := query.FilteredPaginate(attributeStore, req.Pagination, func(_ []byte, value []byte, accumulate bool) (bool, error) { - var result types.Attribute - err := k.cdc.Unmarshal(value, &result) - if err != nil { - return false, err - } - - if result.ExpirationDate != nil && ctx.BlockTime().UTC().After(result.ExpirationDate.UTC()) { - return false, nil - } - - if accumulate { - attributes = append(attributes, result) - } - return true, nil - }) + addrBz := types.GetAttributeAddressBytes(req.Account) + blockTime := ctx.BlockTime().UTC() + rng, endBound := attrAddrRange(addrBz) + attrs, pageRes, err := attrPageWalk(ctx, k.attributes, rng, endBound, req.Pagination, + func(attr types.Attribute) bool { + return attr.ExpirationDate == nil || !blockTime.After(attr.ExpirationDate.UTC()) + }, + ) if err != nil { return nil, err } - - return &types.QueryAttributesResponse{Account: req.Account, Attributes: attributes, Pagination: pageRes}, nil + return &types.QueryAttributesResponse{Account: req.Account, Attributes: attrs, Pagination: pageRes}, nil } // Scan queries all attributes associated with a specified account that contain a particular suffix in their name. @@ -110,59 +90,33 @@ func (k Keeper) Scan(c context.Context, req *types.QueryScanRequest) (*types.Que return nil, status.Errorf(codes.InvalidArgument, "invalid account address: %v", err) } ctx := sdk.UnwrapSDKContext(c) - attributes := make([]types.Attribute, 0) - store := ctx.KVStore(k.storeKey) - attributeStore := prefix.NewStore(store, types.AddrStrAttributesKeyPrefix(req.Account)) - - pageRes, err := query.FilteredPaginate(attributeStore, req.Pagination, func(_ []byte, value []byte, accumulate bool) (bool, error) { - var result types.Attribute - err := k.cdc.Unmarshal(value, &result) - if err != nil { - return false, err - } - if !strings.HasSuffix(result.Name, req.Suffix) || (result.ExpirationDate != nil && ctx.BlockTime().UTC().After(result.ExpirationDate.UTC())) { - return false, nil - } - if accumulate { - attributes = append(attributes, result) - } - return true, nil - }) + addrBz := types.GetAttributeAddressBytes(req.Account) + blockTime := ctx.BlockTime().UTC() + rng, endBound := attrAddrRange(addrBz) + attrs, pageRes, err := attrPageWalk(ctx, k.attributes, rng, endBound, req.Pagination, + func(attr types.Attribute) bool { + return strings.HasSuffix(attr.Name, req.Suffix) && + (attr.ExpirationDate == nil || !blockTime.After(attr.ExpirationDate.UTC())) + }, + ) if err != nil { return nil, err } - - return &types.QueryScanResponse{Account: req.Account, Attributes: attributes, Pagination: pageRes}, nil + return &types.QueryScanResponse{Account: req.Account, Attributes: attrs, Pagination: pageRes}, nil } // AttributeAccounts queries for all accounts that have a specific attribute 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) if err != nil { return nil, err } - return &types.QueryAttributeAccountsResponse{Accounts: accounts, Pagination: pageRes}, nil } @@ -183,3 +137,162 @@ func (k Keeper) AccountData(c context.Context, req *types.QueryAccountDataReques } return resp, nil } + +// attrPageWalk walks col over rng with full pagination: +func attrPageWalk( + ctx sdk.Context, + col collections.Map[types.AttrTriple, types.Attribute], + rng *collections.Range[types.AttrTriple], + endBound *types.AttrTriple, + pageReq *query.PageRequest, + accept func(types.Attribute) bool, +) ([]types.Attribute, *query.PageResponse, error) { + limit := uint64(query.DefaultLimit) + offset := uint64(0) + countTotal := false + + if pageReq == nil { + pageReq = &query.PageRequest{CountTotal: true} + } + + if pageReq != nil { + if pageReq.Limit > 0 { + limit = pageReq.Limit + } + offset = pageReq.Offset + countTotal = pageReq.CountTotal + + // Cursor resume: start from the key returned as NextKey by the previous page. + if len(pageReq.Key) > 0 { + _, startKey, err := types.AttrTripleKey.Decode(pageReq.Key) + if err != nil { + return nil, nil, fmt.Errorf("attribute: invalid pagination key: %w", err) + } + newRng := new(collections.Range[types.AttrTriple]).StartInclusive(startKey) + if endBound != nil { + newRng = newRng.EndExclusive(*endBound) + } + rng = newRng + offset = 0 + } + } + + var ( + attrs []types.Attribute + total uint64 + skipped uint64 + nextKey []byte + ) + + if err := col.Walk(ctx, rng, func(key types.AttrTriple, attr types.Attribute) (bool, error) { + if !accept(attr) { + return false, nil + } + total++ + if skipped < offset { + skipped++ + return false, nil + } + if uint64(len(attrs)) < limit { + attrs = append(attrs, attr) + return false, nil + } + if nextKey == nil { + buf := make([]byte, types.AttrTripleKey.Size(key)) + if n, encErr := types.AttrTripleKey.Encode(buf, key); encErr == nil { + nextKey = buf[:n] + } else { + ctx.Logger().Error("attribute: failed to encode next page key", "error", encErr) + } + } + if !countTotal { + return true, nil + } + return false, nil + }); err != nil { + return nil, nil, err + } + + pageRes := &query.PageResponse{NextKey: nextKey} + if countTotal { + pageRes.Total = total + } + return attrs, pageRes, nil +} + +// nameAddrPageWalk is the equivalent helper for the nameAddrCounts map. +func nameAddrPageWalk( + ctx sdk.Context, + col collections.Map[types.NameAddrPair, uint64], + rng *collections.Range[types.NameAddrPair], + endBound *types.NameAddrPair, + pageReq *query.PageRequest, +) ([]string, *query.PageResponse, error) { + limit := uint64(query.DefaultLimit) + offset := uint64(0) + countTotal := false + + if pageReq == nil { + pageReq = &query.PageRequest{CountTotal: true} + } + + if pageReq != nil { + if pageReq.Limit > 0 { + limit = pageReq.Limit + } + offset = pageReq.Offset + countTotal = pageReq.CountTotal + + if len(pageReq.Key) > 0 { + _, startKey, err := types.NameAddrPairKey.Decode(pageReq.Key) + if err != nil { + return nil, nil, fmt.Errorf("attribute: invalid pagination key: %w", err) + } + newRng := new(collections.Range[types.NameAddrPair]).StartInclusive(startKey) + if endBound != nil { + newRng = newRng.EndExclusive(*endBound) + } + rng = newRng + offset = 0 + } + } + + var ( + accounts []string + total uint64 + skipped uint64 + nextKey []byte + ) + + if err := col.Walk(ctx, rng, func(key types.NameAddrPair, _ uint64) (bool, error) { + total++ + if skipped < offset { + skipped++ + return false, nil + } + if uint64(len(accounts)) < limit { + accounts = append(accounts, sdk.AccAddress(key.AddrBytes).String()) + return false, nil + } + if nextKey == nil { + buf := make([]byte, types.NameAddrPairKey.Size(key)) + if n, encErr := types.NameAddrPairKey.Encode(buf, key); encErr == nil { + nextKey = buf[:n] + } else { + ctx.Logger().Error("attribute: failed to encode next page key", "error", encErr) + } + } + if !countTotal { + return true, nil + } + return false, nil + }); err != nil { + return nil, nil, err + } + + pageRes := &query.PageResponse{NextKey: nextKey} + if countTotal { + pageRes.Total = total + } + return accounts, pageRes, nil +} diff --git a/x/attribute/module.go b/x/attribute/module.go index 929d576140..4fc68d0ca1 100644 --- a/x/attribute/module.go +++ b/x/attribute/module.go @@ -128,6 +128,11 @@ func (AppModule) Name() string { func (am AppModule) RegisterServices(cfg module.Configurator) { types.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(am.keeper)) types.RegisterQueryServer(cfg.QueryServer(), am.keeper) + // This migration converts the attribute module from raw KV-store access to cosmossdk.io/collections. + m := keeper.NewMigrator(am.keeper) + if err := cfg.RegisterMigration(types.ModuleName, 2, m.Migrate2to3); err != nil { + panic(fmt.Errorf("failed to register attribute migration 2 to 3: %w", err)) + } } // InitGenesis performs genesis initialization for the attribute module. It returns no validator updates. @@ -175,4 +180,4 @@ func (am AppModule) WeightedOperations(simState module.SimulationState) []simtyp } // ConsensusVersion implements AppModule/ConsensusVersion. -func (AppModule) ConsensusVersion() uint64 { return 2 } +func (AppModule) ConsensusVersion() uint64 { return 3 } diff --git a/x/attribute/types/codec.go b/x/attribute/types/codec.go index 11b25a620c..2a9a12927e 100644 --- a/x/attribute/types/codec.go +++ b/x/attribute/types/codec.go @@ -1,8 +1,15 @@ package types import ( + "encoding/binary" + "encoding/json" + fmt "fmt" + + collcodec "cosmossdk.io/collections/codec" + "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" "github.com/cosmos/cosmos-sdk/types/msgservice" "github.com/cosmos/gogoproto/proto" ) @@ -14,3 +21,292 @@ func RegisterInterfaces(registry types.InterfaceRegistry) { registry.RegisterImplementations((*sdk.Msg)(nil), messages...) msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc) } + +// AttrTriple key (0x02),Format: [addr_len][addr][hash(name)][hash(value)]. +type AttrTriple struct { + AddrBytes []byte + NameHash [32]byte + ValueHash [32]byte +} + +var AttrTripleKey attrTripleKeyCodec +var _ collcodec.KeyCodec[AttrTriple] = attrTripleKeyCodec{} + +type attrTripleKeyCodec struct{} + +func (attrTripleKeyCodec) Encode(buffer []byte, key AttrTriple) (int, error) { + if len(key.AddrBytes) > 255 { + return 0, fmt.Errorf("attribute: address length %d exceeds 255", len(key.AddrBytes)) + } + n := 0 + // Use address.MustLengthPrefix to match AddrAttributeKey byte-for-byte. + lp := address.MustLengthPrefix(key.AddrBytes) + copy(buffer[n:], lp) + n += len(lp) + copy(buffer[n:], key.NameHash[:]) + n += 32 + copy(buffer[n:], key.ValueHash[:]) + n += 32 + return n, nil +} + +func (attrTripleKeyCodec) Decode(buffer []byte) (int, AttrTriple, error) { + // Empty address: 64 bytes (two 32-byte hashes). + if len(buffer) == 64 { + var nameHash, valueHash [32]byte + copy(nameHash[:], buffer[0:32]) + copy(valueHash[:], buffer[32:64]) + return 64, AttrTriple{NameHash: nameHash, ValueHash: valueHash}, nil + } + if len(buffer) < 1 { + return 0, AttrTriple{}, fmt.Errorf("attribute: buffer too short for AttrTriple") + } + n := 0 + addrLen := int(buffer[n]) + n++ + if len(buffer) < n+addrLen+64 { + return 0, AttrTriple{}, fmt.Errorf("attribute: buffer too short: need %d bytes, have %d", n+addrLen+64, len(buffer)) + } + addrBytes := make([]byte, addrLen) + copy(addrBytes, buffer[n:n+addrLen]) + n += addrLen + var nameHash, valueHash [32]byte + copy(nameHash[:], buffer[n:n+32]) + n += 32 + copy(valueHash[:], buffer[n:n+32]) + n += 32 + return n, AttrTriple{AddrBytes: addrBytes, NameHash: nameHash, ValueHash: valueHash}, nil +} + +func (c attrTripleKeyCodec) EncodeNonTerminal(buffer []byte, key AttrTriple) (int, error) { + return c.Encode(buffer, key) +} +func (c attrTripleKeyCodec) DecodeNonTerminal(buffer []byte) (int, AttrTriple, error) { + return c.Decode(buffer) +} +func (c attrTripleKeyCodec) SizeNonTerminal(key AttrTriple) int { return c.Size(key) } +func (attrTripleKeyCodec) Size(key AttrTriple) int { + if len(key.AddrBytes) == 0 { + return 64 // no length prefix + two 32-byte hashes + } + return 1 + len(key.AddrBytes) + 32 + 32 +} +func (attrTripleKeyCodec) EncodeJSON(key AttrTriple) ([]byte, error) { + return json.Marshal(fmt.Sprintf("attr(%x,%x,%x)", key.AddrBytes, key.NameHash, key.ValueHash)) +} +func (attrTripleKeyCodec) DecodeJSON(_ []byte) (AttrTriple, error) { + return AttrTriple{}, fmt.Errorf("AttrTriple JSON decode not supported") +} +func (attrTripleKeyCodec) Stringify(key AttrTriple) string { + return fmt.Sprintf("attr(%x)", key.AddrBytes) +} +func (attrTripleKeyCodec) KeyType() string { return "AttrTriple" } + +// BuildAttrTriple constructs an AttrTriple from an Attribute. +func BuildAttrTriple(attr Attribute) AttrTriple { + addrBz := attr.GetAddressBytes() + var nameHash [32]byte + copy(nameHash[:], GetNameKeyBytes(attr.Name)) + var valueHash [32]byte + copy(valueHash[:], attr.Hash()) + return AttrTriple{AddrBytes: addrBz, NameHash: nameHash, ValueHash: valueHash} +} + +// NameAddrPair key (0x03),Format: [hash(name)][addr_len][addr]. +type NameAddrPair struct { + NameHash [32]byte + AddrBytes []byte +} + +var NameAddrPairKey nameAddrPairKeyCodec +var _ collcodec.KeyCodec[NameAddrPair] = nameAddrPairKeyCodec{} + +type nameAddrPairKeyCodec struct{} + +func (nameAddrPairKeyCodec) Encode(buffer []byte, key NameAddrPair) (int, error) { + if len(key.AddrBytes) > 255 { + return 0, fmt.Errorf("attribute: address length %d exceeds 255", len(key.AddrBytes)) + } + n := 0 + copy(buffer[n:], key.NameHash[:]) + n += 32 + lp := address.MustLengthPrefix(key.AddrBytes) + copy(buffer[n:], lp) + n += len(lp) + return n, nil +} + +func (nameAddrPairKeyCodec) Decode(buffer []byte) (int, NameAddrPair, error) { + if len(buffer) < 33 { + return 0, NameAddrPair{}, fmt.Errorf("attribute: buffer too short for NameAddrPair") + } + n := 0 + var nameHash [32]byte + copy(nameHash[:], buffer[n:n+32]) + n += 32 + addrLen := int(buffer[n]) + n++ + if len(buffer) < n+addrLen { + return 0, NameAddrPair{}, fmt.Errorf("attribute: buffer too short for NameAddrPair address bytes") + } + addrBytes := make([]byte, addrLen) + copy(addrBytes, buffer[n:n+addrLen]) + n += addrLen + return n, NameAddrPair{NameHash: nameHash, AddrBytes: addrBytes}, nil +} + +func (c nameAddrPairKeyCodec) EncodeNonTerminal(buffer []byte, key NameAddrPair) (int, error) { + return c.Encode(buffer, key) +} +func (c nameAddrPairKeyCodec) DecodeNonTerminal(buffer []byte) (int, NameAddrPair, error) { + return c.Decode(buffer) +} +func (c nameAddrPairKeyCodec) SizeNonTerminal(key NameAddrPair) int { return c.Size(key) } +func (nameAddrPairKeyCodec) Size(key NameAddrPair) int { return 32 + 1 + len(key.AddrBytes) } +func (nameAddrPairKeyCodec) EncodeJSON(key NameAddrPair) ([]byte, error) { + return json.Marshal(fmt.Sprintf("nap(%x,%x)", key.NameHash, key.AddrBytes)) +} +func (nameAddrPairKeyCodec) DecodeJSON(_ []byte) (NameAddrPair, error) { + return NameAddrPair{}, fmt.Errorf("NameAddrPair JSON decode not supported") +} +func (nameAddrPairKeyCodec) Stringify(key NameAddrPair) string { + return fmt.Sprintf("nap(%x)", key.NameHash) +} +func (nameAddrPairKeyCodec) KeyType() string { return "NameAddrPair" } + +// BuildNameAddrPair constructs a NameAddrPair from name and raw address bytes. +func BuildNameAddrPair(name string, addrBytes []byte) NameAddrPair { + var nameHash [32]byte + copy(nameHash[:], GetNameKeyBytes(name)) + return NameAddrPair{NameHash: nameHash, AddrBytes: addrBytes} +} + +// ExpireTriple key (0x04),Format: [epoch(8)][addr_len][addr][hash(name)][hash(value)] +type ExpireTriple struct { + EpochSecs int64 + AddrBytes []byte + NameHash [32]byte + ValueHash [32]byte +} + +var ExpireTripleKey expireTripleKeyCodec +var _ collcodec.KeyCodec[ExpireTriple] = expireTripleKeyCodec{} + +type expireTripleKeyCodec struct{} + +func (expireTripleKeyCodec) Encode(buffer []byte, key ExpireTriple) (int, error) { + if len(key.AddrBytes) > 255 { + return 0, fmt.Errorf("attribute: address length %d exceeds 255", len(key.AddrBytes)) + } + n := 0 + binary.BigEndian.PutUint64(buffer[n:], uint64(key.EpochSecs)) //nolint:gosec // EpochSecs is non-negative and fits in uint64 + n += 8 + lp := address.MustLengthPrefix(key.AddrBytes) + copy(buffer[n:], lp) + n += len(lp) + copy(buffer[n:], key.NameHash[:]) + n += 32 + copy(buffer[n:], key.ValueHash[:]) + n += 32 + return n, nil +} + +func (expireTripleKeyCodec) Decode(buffer []byte) (int, ExpireTriple, error) { + if len(buffer) < 9 { + return 0, ExpireTriple{}, fmt.Errorf("attribute: buffer too short for ExpireTriple") + } + n := 0 + epochSecs := int64(binary.BigEndian.Uint64(buffer[n:])) //nolint:gosec // EpochSecs is non-negative and fits in uint64 + n += 8 + addrLen := int(buffer[n]) + n++ + if len(buffer) < n+addrLen+64 { + return 0, ExpireTriple{}, fmt.Errorf("attribute: buffer too short for ExpireTriple body") + } + addrBytes := make([]byte, addrLen) + copy(addrBytes, buffer[n:n+addrLen]) + n += addrLen + var nameHash, valueHash [32]byte + copy(nameHash[:], buffer[n:n+32]) + n += 32 + copy(valueHash[:], buffer[n:n+32]) + n += 32 + return n, ExpireTriple{EpochSecs: epochSecs, AddrBytes: addrBytes, NameHash: nameHash, ValueHash: valueHash}, nil +} + +func (c expireTripleKeyCodec) EncodeNonTerminal(buffer []byte, key ExpireTriple) (int, error) { + return c.Encode(buffer, key) +} +func (c expireTripleKeyCodec) DecodeNonTerminal(buffer []byte) (int, ExpireTriple, error) { + return c.Decode(buffer) +} +func (c expireTripleKeyCodec) SizeNonTerminal(key ExpireTriple) int { return c.Size(key) } +func (expireTripleKeyCodec) Size(key ExpireTriple) int { return 8 + 1 + len(key.AddrBytes) + 32 + 32 } +func (expireTripleKeyCodec) EncodeJSON(key ExpireTriple) ([]byte, error) { + return json.Marshal(fmt.Sprintf("expire(%d,%x)", key.EpochSecs, key.AddrBytes)) +} +func (expireTripleKeyCodec) DecodeJSON(_ []byte) (ExpireTriple, error) { + return ExpireTriple{}, fmt.Errorf("ExpireTriple JSON decode not supported") +} +func (expireTripleKeyCodec) Stringify(key ExpireTriple) string { + return fmt.Sprintf("expire(%d)", key.EpochSecs) +} +func (expireTripleKeyCodec) KeyType() string { return "ExpireTriple" } + +// BuildExpireTriple constructs an ExpireTriple from an Attribute. Returns (key, false) when no expiration. +func BuildExpireTriple(attr Attribute) (ExpireTriple, bool) { + if attr.ExpirationDate == nil { + return ExpireTriple{}, false + } + var nameHash [32]byte + copy(nameHash[:], GetNameKeyBytes(attr.Name)) + var valueHash [32]byte + copy(valueHash[:], attr.Hash()) + return ExpireTriple{ + EpochSecs: attr.ExpirationDate.Unix(), + AddrBytes: attr.GetAddressBytes(), + NameHash: nameHash, + ValueHash: valueHash, + }, true +} + +// Uint64Value — encodes uint64 as big-endian 8 bytes. Identical to sdk.Uint64ToBigEndian. +var Uint64Value uint64ValueCodec +var _ collcodec.ValueCodec[uint64] = uint64ValueCodec{} + +type uint64ValueCodec struct{} + +func (uint64ValueCodec) Encode(value uint64) ([]byte, error) { + bz := make([]byte, 8) + binary.BigEndian.PutUint64(bz, value) + return bz, nil +} +func (uint64ValueCodec) Decode(bz []byte) (uint64, error) { + if len(bz) != 8 { + return 0, fmt.Errorf("attribute: uint64 value must be 8 bytes, got %d", len(bz)) + } + return binary.BigEndian.Uint64(bz), nil +} +func (uint64ValueCodec) EncodeJSON(value uint64) ([]byte, error) { return json.Marshal(value) } +func (uint64ValueCodec) DecodeJSON(bz []byte) (uint64, error) { + var v uint64 + return v, json.Unmarshal(bz, &v) +} +func (uint64ValueCodec) Stringify(value uint64) string { return fmt.Sprintf("%d", value) } +func (uint64ValueCodec) ValueType() string { return "uint64" } + +// SentinelValue — encodes []byte{} on disk. Identical to store.Set(key, []byte{}). +var SentinelValue sentinelValueCodec +var _ collcodec.ValueCodec[bool] = sentinelValueCodec{} + +type sentinelValueCodec struct{} + +func (sentinelValueCodec) Encode(_ bool) ([]byte, error) { return []byte{}, nil } +func (sentinelValueCodec) Decode(_ []byte) (bool, error) { return true, nil } +func (sentinelValueCodec) EncodeJSON(_ bool) ([]byte, error) { return json.Marshal(true) } +func (sentinelValueCodec) DecodeJSON(bz []byte) (bool, error) { + var v bool + return v, json.Unmarshal(bz, &v) +} +func (sentinelValueCodec) Stringify(_ bool) string { return "sentinel" } +func (sentinelValueCodec) ValueType() string { return "sentinel" } diff --git a/x/attribute/types/keys_test.go b/x/attribute/types/keys_test.go index 9c67a34bf5..05a2a51b07 100644 --- a/x/attribute/types/keys_test.go +++ b/x/attribute/types/keys_test.go @@ -139,3 +139,80 @@ func TestGetAddressFromKey(t *testing.T) { assert.NoError(t, err) assert.Equal(t, attr2.GetAddressBytes(), shortKey.Bytes()) } + +// TestAttrCodecByteIdentity verifies that the new Collections key codecs produce +// byte-for-byte identical output to the old KV key construction functions. +func TestAttrCodecByteIdentity(t *testing.T) { + cases := []struct { + name string + addr string + attrVal []byte + attrTyp AttributeType + expTime *time.Time + }{ + { + name: "cosmos acc address, no expiration", + addr: "cosmos1qyjgkz7c8mh8hfmqszpwchmlkfxe4cwzv7k85s", + attrVal: []byte("verified"), + attrTyp: AttributeType_String, + }, + { + name: "cosmos acc address, with expiration", + addr: "cosmos1qyjgkz7c8mh8hfmqszpwchmlkfxe4cwzv7k85s", + attrVal: []byte("kyc-approved"), + attrTyp: AttributeType_String, + expTime: tp(time.Date(2030, 1, 1, 0, 0, 0, 0, time.UTC)), + }, + { + name: "large value", + addr: "cosmos1qyjgkz7c8mh8hfmqszpwchmlkfxe4cwzv7k85s", + attrVal: make([]byte, 1024), // all zeros + attrTyp: AttributeType_Bytes, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + attr := Attribute{ + Name: "kyc.provenance.io", + Value: tc.attrVal, + AttributeType: tc.attrTyp, + Address: tc.addr, + ExpirationDate: tc.expTime, + } + addrBz := attr.GetAddressBytes() + + // AttrTriple attributes map, prefix 0x02. + oldAttrKey := AddrAttributeKey(addrBz, attr) + triple := BuildAttrTriple(attr) + buf := make([]byte, AttrTripleKey.Size(triple)) + n, err := AttrTripleKey.Encode(buf, triple) + require.NoError(t, err, "AttrTripleKey.Encode") + newAttrKey := append(append([]byte{}, AttributeKeyPrefix...), buf[:n]...) + require.Equal(t, oldAttrKey, newAttrKey, "AttrTriple byte layout must match old KV key") + + // NameAddrPair nameAddrCounts map, prefix 0x03. + oldNap := AttributeNameAddrKeyPrefix(attr.Name, addrBz) + nap := BuildNameAddrPair(attr.Name, addrBz) + napBuf := make([]byte, NameAddrPairKey.Size(nap)) + nn, err := NameAddrPairKey.Encode(napBuf, nap) + require.NoError(t, err, "NameAddrPairKey.Encode") + newNap := append(append([]byte{}, AttributeAddrLookupKeyPrefix...), napBuf[:nn]...) + require.Equal(t, oldNap, newNap, "NameAddrPair byte layout must match old KV key") + + // ExpireTriple expirationIndex map, prefix 0x04, only when expiration set. + if tc.expTime != nil { + oldExp := AttributeExpireKey(attr) + et, ok := BuildExpireTriple(attr) + require.True(t, ok, "BuildExpireTriple should return ok when ExpirationDate is set") + etBuf := make([]byte, ExpireTripleKey.Size(et)) + ne, err := ExpireTripleKey.Encode(etBuf, et) + require.NoError(t, err, "ExpireTripleKey.Encode") + newExp := append(append([]byte{}, AttributeExpirationKeyPrefix...), etBuf[:ne]...) + require.Equal(t, oldExp, newExp, "ExpireTriple byte layout must match old KV key") + } + }) + } +} + +func tp(t time.Time) *time.Time { return &t } From de487975780c260995f4af41725dd2e5f21afdf1 Mon Sep 17 00:00:00 2001 From: nagarajdivine Date: Thu, 16 Apr 2026 23:04:59 +0800 Subject: [PATCH 2/2] update changelog. --- .changelog/unreleased/features/2489-collections-attributes.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changelog/unreleased/features/2489-collections-attributes.md diff --git a/.changelog/unreleased/features/2489-collections-attributes.md b/.changelog/unreleased/features/2489-collections-attributes.md new file mode 100644 index 0000000000..bfc20695b0 --- /dev/null +++ b/.changelog/unreleased/features/2489-collections-attributes.md @@ -0,0 +1 @@ +* Switch to collections in the attribute module [#2489](https://github.com/provenance-io/provenance/issues/2489).