package keeper import ( "bytes" "errors" "fmt" "sort" "time" "cosmossdk.io/collections" "cosmossdk.io/collections/indexes" storetypes "cosmossdk.io/core/store" errorsmod "cosmossdk.io/errors" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec/legacy" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" auth "github.com/cosmos/cosmos-sdk/x/auth/keeper" bank "github.com/cosmos/cosmos-sdk/x/bank/keeper" "github.com/gibson042/canonicaljson-go" "github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime/codec/dagjson" "github.com/ipld/go-ipld-prime/node/basicnode" auctionkeeper "git.vdb.to/cerc-io/laconic2d/x/auction/keeper" bondkeeper "git.vdb.to/cerc-io/laconic2d/x/bond/keeper" registrytypes "git.vdb.to/cerc-io/laconic2d/x/registry" "git.vdb.to/cerc-io/laconic2d/x/registry/helpers" ) // TODO: Add required methods type RecordsIndexes struct { BondId *indexes.Multi[string, string, registrytypes.Record] } func (b RecordsIndexes) IndexesList() []collections.Index[string, registrytypes.Record] { return []collections.Index[string, registrytypes.Record]{b.BondId} } func newRecordIndexes(sb *collections.SchemaBuilder) RecordsIndexes { return RecordsIndexes{ BondId: indexes.NewMulti( sb, registrytypes.RecordsByBondIdIndexPrefix, "records_by_bond_id", collections.StringKey, collections.StringKey, func(_ string, v registrytypes.Record) (string, error) { return v.BondId, nil }, ), } } // TODO type AuthoritiesIndexes struct { } func (b AuthoritiesIndexes) IndexesList() []collections.Index[string, registrytypes.NameAuthority] { return []collections.Index[string, registrytypes.NameAuthority]{} } func newAuthorityIndexes(sb *collections.SchemaBuilder) AuthoritiesIndexes { return AuthoritiesIndexes{} } type NameRecordsIndexes struct { Cid *indexes.Multi[string, string, registrytypes.NameRecord] } func (b NameRecordsIndexes) IndexesList() []collections.Index[string, registrytypes.NameRecord] { return []collections.Index[string, registrytypes.NameRecord]{b.Cid} } func newNameRecordIndexes(sb *collections.SchemaBuilder) NameRecordsIndexes { return NameRecordsIndexes{ Cid: indexes.NewMulti( sb, registrytypes.NameRecordsByCidIndexPrefix, "name_records_by_cid", collections.StringKey, collections.StringKey, func(_ string, v registrytypes.NameRecord) (string, error) { return v.Latest.Id, nil }, ), } } type Keeper struct { cdc codec.BinaryCodec accountKeeper auth.AccountKeeper bankKeeper bank.Keeper recordKeeper RecordKeeper bondKeeper bondkeeper.Keeper auctionKeeper *auctionkeeper.Keeper // state management Schema collections.Schema Params collections.Item[registrytypes.Params] Records *collections.IndexedMap[string, registrytypes.Record, RecordsIndexes] Authorities *collections.IndexedMap[string, registrytypes.NameAuthority, AuthoritiesIndexes] NameRecords *collections.IndexedMap[string, registrytypes.NameRecord, NameRecordsIndexes] RecordExpiryQueue collections.Map[time.Time, registrytypes.ExpiryQueue] AuthorityExpiryQueue collections.Map[time.Time, registrytypes.ExpiryQueue] } // NewKeeper creates a new Keeper instance func NewKeeper( cdc codec.BinaryCodec, storeService storetypes.KVStoreService, accountKeeper auth.AccountKeeper, bankKeeper bank.Keeper, recordKeeper RecordKeeper, bondKeeper bondkeeper.Keeper, auctionKeeper *auctionkeeper.Keeper, ) Keeper { sb := collections.NewSchemaBuilder(storeService) k := Keeper{ cdc: cdc, accountKeeper: accountKeeper, bankKeeper: bankKeeper, recordKeeper: recordKeeper, bondKeeper: bondKeeper, auctionKeeper: auctionKeeper, Params: collections.NewItem(sb, registrytypes.ParamsPrefix, "params", codec.CollValue[registrytypes.Params](cdc)), Records: collections.NewIndexedMap( sb, registrytypes.RecordsPrefix, "records", collections.StringKey, codec.CollValue[registrytypes.Record](cdc), newRecordIndexes(sb), ), Authorities: collections.NewIndexedMap( sb, registrytypes.AuthoritiesPrefix, "authorities", collections.StringKey, codec.CollValue[registrytypes.NameAuthority](cdc), newAuthorityIndexes(sb), ), NameRecords: collections.NewIndexedMap( sb, registrytypes.NameRecordsPrefix, "name_records", collections.StringKey, codec.CollValue[registrytypes.NameRecord](cdc), newNameRecordIndexes(sb), ), RecordExpiryQueue: collections.NewMap( sb, registrytypes.RecordExpiryQueuePrefix, "record_expiry_queue", sdk.TimeKey, codec.CollValue[registrytypes.ExpiryQueue](cdc), ), AuthorityExpiryQueue: collections.NewMap( sb, registrytypes.AuthorityExpiryQueuePrefix, "authority_expiry_queue", sdk.TimeKey, codec.CollValue[registrytypes.ExpiryQueue](cdc), ), } schema, err := sb.Build() if err != nil { panic(err) } k.Schema = schema return k } // HasRecord - checks if a record by the given id exists. func (k Keeper) HasRecord(ctx sdk.Context, id string) (bool, error) { has, err := k.Records.Has(ctx, id) if err != nil { return false, err } return has, nil } // ListRecords - get all records. func (k Keeper) ListRecords(ctx sdk.Context) ([]registrytypes.Record, error) { var records []registrytypes.Record err := k.Records.Walk(ctx, nil, func(key string, value registrytypes.Record) (bool, error) { if err := k.populateRecordNames(ctx, &value); err != nil { return true, err } records = append(records, value) return false, nil }) if err != nil { return nil, err } return records, nil } // GetRecordById - gets a record from the store. func (k Keeper) GetRecordById(ctx sdk.Context, id string) (registrytypes.Record, error) { record, err := k.Records.Get(ctx, id) if err != nil { return registrytypes.Record{}, err } if err := k.populateRecordNames(ctx, &record); err != nil { return registrytypes.Record{}, err } return record, nil } // GetRecordsByBondId - gets a record from the store. func (k Keeper) GetRecordsByBondId(ctx sdk.Context, bondId string) ([]registrytypes.Record, error) { var records []registrytypes.Record err := k.Records.Indexes.BondId.Walk(ctx, collections.NewPrefixedPairRange[string, string](bondId), func(bondId string, id string) (bool, error) { record, err := k.Records.Get(ctx, id) if err != nil { return true, err } if err := k.populateRecordNames(ctx, &record); err != nil { return true, err } records = append(records, record) return false, nil }) if err != nil { return []registrytypes.Record{}, err } return records, nil } // RecordsFromAttributes gets a list of records whose attributes match all provided values func (k Keeper) RecordsFromAttributes(ctx sdk.Context, attributes []*registrytypes.QueryRecordsRequest_KeyValueInput, all bool) ([]registrytypes.Record, error) { panic("unimplemented") } // PutRecord - saves a record to the store. func (k Keeper) SaveRecord(ctx sdk.Context, record registrytypes.Record) error { return k.Records.Set(ctx, record.Id, record) // TODO // k.updateBlockChangeSetForRecord(ctx, record.Id) } // ProcessSetRecord creates a record. func (k Keeper) SetRecord(ctx sdk.Context, msg registrytypes.MsgSetRecord) (*registrytypes.ReadableRecord, error) { payload := msg.Payload.ToReadablePayload() record := registrytypes.ReadableRecord{Attributes: payload.RecordAttributes, BondId: msg.BondId} // Check signatures. resourceSignBytes, _ := record.GetSignBytes() cid, err := record.GetCid() if err != nil { return nil, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "Invalid record JSON") } record.Id = cid has, err := k.HasRecord(ctx, record.Id) if err != nil { return nil, err } if has { // Immutable record already exists. No-op. return &record, nil } record.Owners = []string{} for _, sig := range payload.Signatures { pubKey, err := legacy.PubKeyFromBytes(helpers.BytesFromBase64(sig.PubKey)) if err != nil { return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, fmt.Sprint("Error decoding pubKey from bytes: ", err)) } sigOK := pubKey.VerifySignature(resourceSignBytes, helpers.BytesFromBase64(sig.Sig)) if !sigOK { return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, fmt.Sprint("Signature mismatch: ", sig.PubKey)) } record.Owners = append(record.Owners, pubKey.Address().String()) } // Sort owners list. sort.Strings(record.Owners) sdkErr := k.processRecord(ctx, &record, false) if sdkErr != nil { return nil, sdkErr } return &record, nil } func (k Keeper) processRecord(ctx sdk.Context, record *registrytypes.ReadableRecord, isRenewal bool) error { params, err := k.GetParams(ctx) if err != nil { return err } rent := params.RecordRent if err = k.bondKeeper.TransferCoinsToModuleAccount( ctx, record.BondId, registrytypes.RecordRentModuleAccountName, sdk.NewCoins(rent), ); err != nil { return err } record.CreateTime = ctx.BlockHeader().Time.Format(time.RFC3339) record.ExpiryTime = ctx.BlockHeader().Time.Add(params.RecordRentDuration).Format(time.RFC3339) record.Deleted = false recordObj, err := record.ToRecordObj() if err != nil { return err } // Save record in store. if err = k.SaveRecord(ctx, recordObj); err != nil { return err } // TODO look up/validate record type here if err := k.processAttributes(ctx, record.Attributes, record.Id, ""); err != nil { return err } // TODO // expiryTimeKey := GetAttributesIndexKey(ExpiryTimeAttributeName, []byte(record.ExpiryTime)) // if err := k.SetAttributeMapping(ctx, expiryTimeKey, record.ID); err != nil { // return err // } return k.insertRecordExpiryQueue(ctx, recordObj) } func (k Keeper) processAttributes(ctx sdk.Context, attrs registrytypes.AttributeMap, id string, prefix string) error { np := basicnode.Prototype.Map nb := np.NewBuilder() encAttrs, err := canonicaljson.Marshal(attrs) if err != nil { return err } if len(attrs) == 0 { encAttrs = []byte("{}") } err = dagjson.Decode(nb, bytes.NewReader(encAttrs)) if err != nil { return fmt.Errorf("failed to decode attributes: %w", err) } n := nb.Build() if n.Kind() != ipld.Kind_Map { return fmt.Errorf("record attributes must be a map, not %T", n.Kind()) } return k.processAttributeMap(ctx, n, id, prefix) } func (k Keeper) processAttributeMap(ctx sdk.Context, n ipld.Node, id string, prefix string) error { for it := n.MapIterator(); !it.Done(); { //nolint:misspell keynode, valuenode, err := it.Next() if err != nil { return err } key, err := keynode.AsString() if err != nil { return err } if valuenode.Kind() == ipld.Kind_Map { err := k.processAttributeMap(ctx, valuenode, id, key) if err != nil { return err } } else { var buf bytes.Buffer if err := dagjson.Encode(valuenode, &buf); err != nil { return err } // TODO // value := buf.Bytes() // indexKey := GetAttributesIndexKey(prefix+key, value) // if err := k.SetAttributeMapping(ctx, indexKey, id); err != nil { // return err // } } } return nil } func (k Keeper) populateRecordNames(ctx sdk.Context, record *registrytypes.Record) error { iter, err := k.NameRecords.Indexes.Cid.MatchExact(ctx, record.Id) if err != nil { return err } names, err := iter.PrimaryKeys() if err != nil { return err } record.Names = names return nil } // GetModuleBalances gets the registry module account(s) balances. func (k Keeper) GetModuleBalances(ctx sdk.Context) []*registrytypes.AccountBalance { var balances []*registrytypes.AccountBalance accountNames := []string{ registrytypes.RecordRentModuleAccountName, registrytypes.AuthorityRentModuleAccountName, } for _, accountName := range accountNames { moduleAddress := k.accountKeeper.GetModuleAddress(accountName) moduleAccount := k.accountKeeper.GetAccount(ctx, moduleAddress) if moduleAccount != nil { accountBalance := k.bankKeeper.GetAllBalances(ctx, moduleAddress) balances = append(balances, ®istrytypes.AccountBalance{ AccountName: accountName, Balance: accountBalance, }) } } return balances } // ProcessRecordExpiryQueue tries to renew expiring records (by collecting rent) else marks them as deleted. func (k Keeper) ProcessRecordExpiryQueue(ctx sdk.Context) error { // TODO: process expired records cids, err := k.getAllExpiredRecords(ctx, ctx.BlockHeader().Time) if err != nil { return err } for _, cid := range cids { record, err := k.GetRecordById(ctx, cid) if err != nil { return err } bondExists := false if record.BondId != "" { bondExists, err = k.bondKeeper.HasBond(ctx, record.BondId) if err != nil { return err } } // If record doesn't have an associated bond or if bond no longer exists, mark it deleted. if !bondExists { record.Deleted = true if err := k.SaveRecord(ctx, record); err != nil { return err } if err := k.deleteRecordExpiryQueue(ctx, record); err != nil { return err } } // Try to renew the record by taking rent. if err := k.tryTakeRecordRent(ctx, record); err != nil { return err } } return nil } // getAllExpiredRecords returns a concatenated list of all the timeslices before currTime. func (k Keeper) getAllExpiredRecords(ctx sdk.Context, currTime time.Time) ([]string, error) { var expiredRecordCIDs []string // Get all the records with expiry time until currTime rng := new(collections.Range[time.Time]).EndInclusive(currTime) err := k.RecordExpiryQueue.Walk(ctx, rng, func(key time.Time, value registrytypes.ExpiryQueue) (stop bool, err error) { expiredRecordCIDs = append(expiredRecordCIDs, value.Value...) return false, nil }) if err != nil { return []string{}, err } return expiredRecordCIDs, nil } // insertRecordExpiryQueue inserts a record CID to the appropriate timeslice in the record expiry queue. func (k Keeper) insertRecordExpiryQueue(ctx sdk.Context, record registrytypes.Record) error { expiryTime, err := time.Parse(time.RFC3339, record.ExpiryTime) if err != nil { return err } existingRecordsList, err := k.RecordExpiryQueue.Get(ctx, expiryTime) if err != nil { if errors.Is(err, collections.ErrNotFound) { existingRecordsList = registrytypes.ExpiryQueue{ Id: expiryTime.String(), Value: []string{}, } } else { return err } } existingRecordsList.Value = append(existingRecordsList.Value, record.Id) return k.RecordExpiryQueue.Set(ctx, expiryTime, existingRecordsList) } // deleteRecordExpiryQueue deletes a record CID from the record expiry queue. func (k Keeper) deleteRecordExpiryQueue(ctx sdk.Context, record registrytypes.Record) error { expiryTime, err := time.Parse(time.RFC3339, record.ExpiryTime) if err != nil { return err } existingRecordsList, err := k.RecordExpiryQueue.Get(ctx, expiryTime) if err != nil { return err } newRecordsSlice := []string{} for _, id := range existingRecordsList.Value { if id != record.Id { newRecordsSlice = append(newRecordsSlice, id) } } if len(existingRecordsList.Value) == 0 { return k.RecordExpiryQueue.Remove(ctx, expiryTime) } else { existingRecordsList.Value = newRecordsSlice return k.RecordExpiryQueue.Set(ctx, expiryTime, existingRecordsList) } } // tryTakeRecordRent tries to take rent from the record bond. func (k Keeper) tryTakeRecordRent(ctx sdk.Context, record registrytypes.Record) error { params, err := k.GetParams(ctx) if err != nil { return err } rent := params.RecordRent sdkErr := k.bondKeeper.TransferCoinsToModuleAccount(ctx, record.BondId, registrytypes.RecordRentModuleAccountName, sdk.NewCoins(rent)) if sdkErr != nil { // Insufficient funds, mark record as deleted. record.Deleted = true if err := k.SaveRecord(ctx, record); err != nil { return err } return k.deleteRecordExpiryQueue(ctx, record) } // Delete old expiry queue entry, create new one. if err := k.deleteRecordExpiryQueue(ctx, record); err != nil { return err } record.ExpiryTime = ctx.BlockHeader().Time.Add(params.RecordRentDuration).Format(time.RFC3339) if err := k.insertRecordExpiryQueue(ctx, record); err != nil { return err } // Save record. record.Deleted = false return k.SaveRecord(ctx, record) }