laconicd-deprecated/x/registry/keeper/keeper.go
Thomas E Lackey 0adc932400 118: queryRecords should return [] when no matching records exist, not an error (#121)
Whenever a key is used which doesn't exist in the DB at all, an error is returned like this:

```
[
  {
    message: `rpc error: code = Unknown desc = store doesn't have key: "\\atypeApplicationRecord"`,
    path: [ 'queryRecords' ]
  }
]
```

That doesn't make any sense, as it is not an error to find out that the thing you asked for does not exist.  This changes it to return an empty list.

Reviewed-on: cerc-io/laconicd#121
Reviewed-by: David Boreham <dboreham@noreply.git.vdb.to>
Co-authored-by: Thomas E Lackey <telackey@bozemanpass.com>
Co-committed-by: Thomas E Lackey <telackey@bozemanpass.com>
2023-11-28 20:41:28 +00:00

596 lines
18 KiB
Go

package keeper
import (
"bytes"
"encoding/json"
"fmt"
"sort"
"time"
errorsmod "cosmossdk.io/errors"
auctionkeeper "github.com/cerc-io/laconicd/x/auction/keeper"
bondkeeper "github.com/cerc-io/laconicd/x/bond/keeper"
"github.com/cerc-io/laconicd/x/registry/helpers"
"github.com/cerc-io/laconicd/x/registry/types"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/codec/legacy"
storetypes "github.com/cosmos/cosmos-sdk/store/types"
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"
paramtypes "github.com/cosmos/cosmos-sdk/x/params/types"
"github.com/tendermint/tendermint/libs/log"
)
var (
// PrefixCIDToRecordIndex is the prefix for CID -> Record index.
// Note: This is the primary index in the system.
// Note: Golang doesn't support const arrays.
PrefixCIDToRecordIndex = []byte{0x00}
// PrefixNameAuthorityRecordIndex is the prefix for the name -> NameAuthority index.
PrefixNameAuthorityRecordIndex = []byte{0x01}
// PrefixCRNToNameRecordIndex is the prefix for the CRN -> NamingRecord index.
PrefixCRNToNameRecordIndex = []byte{0x02}
// PrefixBondIDToRecordsIndex is the prefix for the Bond ID -> [Record] index.
PrefixBondIDToRecordsIndex = []byte{0x03}
// PrefixBlockChangesetIndex is the prefix for the block changeset index.
PrefixBlockChangesetIndex = []byte{0x04}
// PrefixAuctionToAuthorityNameIndex is the prefix for the auction ID -> authority name index.
PrefixAuctionToAuthorityNameIndex = []byte{0x05}
// PrefixBondIDToAuthoritiesIndex is the prefix for the Bond ID -> [Authority] index.
PrefixBondIDToAuthoritiesIndex = []byte{0x06}
// PrefixAttributesIndex is the prefix for the registry Record.Attribute -> []Record.ID index
PrefixAttributesIndex = []byte{0x07}
// PrefixExpiryTimeToRecordsIndex is the prefix for the Expiry Time -> [Record] index.
PrefixExpiryTimeToRecordsIndex = []byte{0x10}
// PrefixExpiryTimeToAuthoritiesIndex is the prefix for the Expiry Time -> [Authority] index.
PrefixExpiryTimeToAuthoritiesIndex = []byte{0x11}
// PrefixCIDToNamesIndex the the reverse index for naming, i.e. maps CID -> []Names.
// TODO(ashwin): Move out of WNS once we have an indexing service.
PrefixCIDToNamesIndex = []byte{0xe0}
)
// Keeper maintains the link to storage and exposes getter/setter methods for the various parts of the state machine
type Keeper struct {
accountKeeper auth.AccountKeeper
bankKeeper bank.Keeper
recordKeeper RecordKeeper
bondKeeper bondkeeper.Keeper
auctionKeeper auctionkeeper.Keeper
storeKey storetypes.StoreKey // Unexposed key to access store from sdk.Context
cdc codec.BinaryCodec // The wire codec for binary encoding/decoding.
paramSubspace paramtypes.Subspace
}
// NewKeeper creates new instances of the registry Keeper
func NewKeeper(cdc codec.BinaryCodec, accountKeeper auth.AccountKeeper, bankKeeper bank.Keeper, recordKeeper RecordKeeper,
bondKeeper bondkeeper.Keeper, auctionKeeper auctionkeeper.Keeper, storeKey storetypes.StoreKey, ps paramtypes.Subspace,
) Keeper {
// set KeyTable if it has not already been set
if !ps.HasKeyTable() {
ps = ps.WithKeyTable(types.ParamKeyTable())
}
return Keeper{
accountKeeper: accountKeeper,
bankKeeper: bankKeeper,
recordKeeper: recordKeeper,
bondKeeper: bondKeeper,
auctionKeeper: auctionKeeper,
storeKey: storeKey,
cdc: cdc,
paramSubspace: ps,
}
}
// Logger returns a module-specific logger.
func (k Keeper) Logger(ctx sdk.Context) log.Logger {
return ctx.Logger().With("module", types.ModuleName)
}
// GetRecordIndexKey Generates Bond ID -> Bond index key.
func GetRecordIndexKey(id string) []byte {
return append(PrefixCIDToRecordIndex, []byte(id)...)
}
// HasRecord - checks if a record by the given ID exists.
func (k Keeper) HasRecord(ctx sdk.Context, id string) bool {
store := ctx.KVStore(k.storeKey)
return store.Has(GetRecordIndexKey(id))
}
// GetRecord - gets a record from the store.
func (k Keeper) GetRecord(ctx sdk.Context, id string) (record types.Record) {
store := ctx.KVStore(k.storeKey)
result := store.Get(GetRecordIndexKey(id))
k.cdc.MustUnmarshal(result, &record)
return recordObjToRecord(store, record)
}
// ListRecords - get all records.
func (k Keeper) ListRecords(ctx sdk.Context) []types.Record {
var records []types.Record
store := ctx.KVStore(k.storeKey)
itr := sdk.KVStorePrefixIterator(store, PrefixCIDToRecordIndex)
defer itr.Close()
for ; itr.Valid(); itr.Next() {
bz := store.Get(itr.Key())
if bz != nil {
var obj types.Record
k.cdc.MustUnmarshal(bz, &obj)
records = append(records, recordObjToRecord(store, obj))
}
}
return records
}
func (k Keeper) RecordsFromAttributes(ctx sdk.Context, attributes []*types.QueryListRecordsRequest_KeyValueInput, all bool) ([]types.Record, error) {
resultRecordIds := []string{}
for i, attr := range attributes {
val := GetAttributeValue(attr.Value)
attributeIndex := GetAttributesIndexKey(attr.Key, val)
recordIds, err := k.GetAttributeMapping(ctx, attributeIndex)
if err != nil {
return nil, err
}
if i == 0 {
resultRecordIds = recordIds
} else {
resultRecordIds = getIntersection(recordIds, resultRecordIds)
}
}
records := []types.Record{}
for _, id := range resultRecordIds {
record := k.GetRecord(ctx, id)
if record.Deleted {
continue
}
store := ctx.KVStore(k.storeKey)
recordWithNames := recordObjToRecord(store, record)
if !all && len(recordWithNames.Names) == 0 {
continue
}
records = append(records, recordWithNames)
}
return records, nil
}
func GetAttributeValue(input *types.QueryListRecordsRequest_ValueInput) interface{} {
if input.Type == "int" {
return input.GetInt()
}
if input.Type == "float" {
return input.GetFloat()
}
if input.Type == "string" {
return input.GetString_()
}
if input.Type == "boolean" {
return input.GetBoolean()
}
if input.Type == "reference" {
return input.GetReference().GetId()
}
return nil
}
func getIntersection(a []string, b []string) []string {
result := []string{}
if len(a) < len(b) {
for _, str := range a {
if contains(b, str) {
result = append(result, str)
}
}
} else {
for _, str := range b {
if contains(a, str) {
result = append(result, str)
}
}
}
return result
}
func contains(arr []string, str string) bool {
for _, s := range arr {
if s == str {
return true
}
}
return false
}
func (k Keeper) GetRecordExpiryQueue(ctx sdk.Context) []*types.ExpiryQueueRecord {
var records []*types.ExpiryQueueRecord
store := ctx.KVStore(k.storeKey)
itr := sdk.KVStorePrefixIterator(store, PrefixExpiryTimeToRecordsIndex)
defer itr.Close()
for ; itr.Valid(); itr.Next() {
record, err := helpers.BytesArrToStringArr(itr.Value())
if err != nil {
return records
}
records = append(records, &types.ExpiryQueueRecord{
Id: string(itr.Key()[len(PrefixExpiryTimeToRecordsIndex):]),
Value: record,
})
}
return records
}
// ProcessSetRecord creates a record.
func (k Keeper) ProcessSetRecord(ctx sdk.Context, msg types.MsgSetRecord) (*types.RecordType, error) {
payload := msg.Payload.ToReadablePayload()
record := types.RecordType{Attributes: payload.Record, 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
if exists := k.HasRecord(ctx, record.ID); exists {
// 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 {
fmt.Println("Error decoding pubKey from bytes: ", err)
return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, "Invalid public key.")
}
sigOK := pubKey.VerifySignature(resourceSignBytes, helpers.BytesFromBase64(sig.Sig))
if !sigOK {
fmt.Println("Signature mismatch: ", sig.PubKey)
return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, "Invalid signature.")
}
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 *types.RecordType, isRenewal bool) error {
params := k.GetParams(ctx)
rent := params.RecordRent
err := k.bondKeeper.TransferCoinsToModuleAccount(ctx, record.BondID, types.RecordRentModuleAccountName, sdk.NewCoins(rent))
if 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
}
k.PutRecord(ctx, recordObj)
if err := k.ProcessAttributes(ctx, *record); err != nil {
return err
}
k.InsertRecordExpiryQueue(ctx, recordObj)
// Renewal doesn't change the name and bond indexes.
if !isRenewal {
k.AddBondToRecordIndexEntry(ctx, record.BondID, record.ID)
}
return nil
}
// PutRecord - saves a record to the store and updates ID -> Record index.
func (k Keeper) PutRecord(ctx sdk.Context, record types.Record) {
store := ctx.KVStore(k.storeKey)
store.Set(GetRecordIndexKey(record.Id), k.cdc.MustMarshal(&record))
k.updateBlockChangeSetForRecord(ctx, record.Id)
}
func (k Keeper) ProcessAttributes(ctx sdk.Context, record types.RecordType) error {
switch record.Attributes["type"] {
case "ServiceProviderRegistration":
{
// #nosec G705
for key := range record.Attributes {
if key == "x500" {
// #nosec G705
for x500Key, x500Val := range record.Attributes[key].(map[string]interface{}) {
indexKey := GetAttributesIndexKey(fmt.Sprintf("x500%s", x500Key), x500Val)
if err := k.SetAttributeMapping(ctx, indexKey, record.ID); err != nil {
return err
}
}
} else {
indexKey := GetAttributesIndexKey(key, record.Attributes[key])
if err := k.SetAttributeMapping(ctx, indexKey, record.ID); err != nil {
return err
}
}
}
}
case "WebsiteRegistrationRecord", "ApplicationRecord", "WebAppDeploymentRecord", "GeneralRecord":
{
// #nosec G705
for key := range record.Attributes {
indexKey := GetAttributesIndexKey(key, record.Attributes[key])
if err := k.SetAttributeMapping(ctx, indexKey, record.ID); err != nil {
return err
}
}
}
default:
return fmt.Errorf("unsupported record type %s", record.Attributes["type"])
}
expiryTimeKey := GetAttributesIndexKey(ExpiryTimeAttributeName, record.ExpiryTime)
if err := k.SetAttributeMapping(ctx, expiryTimeKey, record.ID); err != nil {
return err
}
return nil
}
func GetAttributesIndexKey(key string, value interface{}) []byte {
keyString := fmt.Sprintf("%s%s", key, value)
return append(PrefixAttributesIndex, []byte(keyString)...)
}
func (k Keeper) SetAttributeMapping(ctx sdk.Context, key []byte, recordID string) error {
store := ctx.KVStore(k.storeKey)
var recordIds []string
if store.Has(key) {
err := json.Unmarshal(store.Get(key), &recordIds)
if err != nil {
return fmt.Errorf("cannot unmarshal byte array, error, %w", err)
}
} else {
recordIds = []string{}
}
recordIds = append(recordIds, recordID)
bz, err := json.Marshal(recordIds)
if err != nil {
return fmt.Errorf("cannot marshal string array, error, %w", err)
}
store.Set(key, bz)
return nil
}
func (k Keeper) GetAttributeMapping(ctx sdk.Context, key []byte) ([]string, error) {
store := ctx.KVStore(k.storeKey)
if !store.Has(key) {
k.Logger(ctx).Debug(fmt.Sprintf("store doesn't have key: %q", key))
return []string{}, nil
}
var recordIds []string
if err := json.Unmarshal(store.Get(key), &recordIds); err != nil {
return nil, fmt.Errorf("cannont unmarshal byte array, error, %w", err)
}
return recordIds, nil
}
// AddBondToRecordIndexEntry adds the Bond ID -> [Record] index entry.
func (k Keeper) AddBondToRecordIndexEntry(ctx sdk.Context, bondID string, id string) {
store := ctx.KVStore(k.storeKey)
store.Set(getBondIDToRecordsIndexKey(bondID, id), []byte{})
}
// Generates Bond ID -> Records index key.
func getBondIDToRecordsIndexKey(bondID string, id string) []byte {
return append(append(PrefixBondIDToRecordsIndex, []byte(bondID)...), []byte(id)...)
}
// getRecordExpiryQueueTimeKey gets the prefix for the record expiry queue.
func getRecordExpiryQueueTimeKey(timestamp time.Time) []byte {
timeBytes := sdk.FormatTimeBytes(timestamp)
return append(PrefixExpiryTimeToRecordsIndex, timeBytes...)
}
// SetRecordExpiryQueueTimeSlice sets a specific record expiry queue timeslice.
func (k Keeper) SetRecordExpiryQueueTimeSlice(ctx sdk.Context, timestamp time.Time, cids []string) {
store := ctx.KVStore(k.storeKey)
bz, _ := helpers.StrArrToBytesArr(cids)
store.Set(getRecordExpiryQueueTimeKey(timestamp), bz)
}
// DeleteRecordExpiryQueueTimeSlice deletes a specific record expiry queue timeslice.
func (k Keeper) DeleteRecordExpiryQueueTimeSlice(ctx sdk.Context, timestamp time.Time) {
store := ctx.KVStore(k.storeKey)
store.Delete(getRecordExpiryQueueTimeKey(timestamp))
}
// GetRecordExpiryQueueTimeSlice gets a specific record queue timeslice.
// A timeslice is a slice of CIDs corresponding to records that expire at a certain time.
func (k Keeper) GetRecordExpiryQueueTimeSlice(ctx sdk.Context, timestamp time.Time) []string {
store := ctx.KVStore(k.storeKey)
bz := store.Get(getRecordExpiryQueueTimeKey(timestamp))
if bz == nil {
return []string{}
}
cids, err := helpers.BytesArrToStringArr(bz)
if err != nil {
return []string{}
}
return cids
}
// InsertRecordExpiryQueue inserts a record CID to the appropriate timeslice in the record expiry queue.
func (k Keeper) InsertRecordExpiryQueue(ctx sdk.Context, val types.Record) {
expiryTime, err := time.Parse(time.RFC3339, val.ExpiryTime)
if err != nil {
panic(err)
}
timeSlice := k.GetRecordExpiryQueueTimeSlice(ctx, expiryTime)
timeSlice = append(timeSlice, val.Id)
k.SetRecordExpiryQueueTimeSlice(ctx, expiryTime, timeSlice)
}
// DeleteRecordExpiryQueue deletes a record CID from the record expiry queue.
func (k Keeper) DeleteRecordExpiryQueue(ctx sdk.Context, record types.Record) {
expiryTime, err := time.Parse(time.RFC3339, record.ExpiryTime)
if err != nil {
panic(err)
}
timeSlice := k.GetRecordExpiryQueueTimeSlice(ctx, expiryTime)
var newTimeSlice []string
for _, cid := range timeSlice {
if !bytes.Equal([]byte(cid), []byte(record.Id)) {
newTimeSlice = append(newTimeSlice, cid)
}
}
if len(newTimeSlice) == 0 {
k.DeleteRecordExpiryQueueTimeSlice(ctx, expiryTime)
} else {
k.SetRecordExpiryQueueTimeSlice(ctx, expiryTime, newTimeSlice)
}
}
// RecordExpiryQueueIterator returns all the record expiry queue timeslices from time 0 until endTime.
func (k Keeper) RecordExpiryQueueIterator(ctx sdk.Context, endTime time.Time) sdk.Iterator {
store := ctx.KVStore(k.storeKey)
rangeEndBytes := sdk.InclusiveEndBytes(getRecordExpiryQueueTimeKey(endTime))
return store.Iterator(PrefixExpiryTimeToRecordsIndex, rangeEndBytes)
}
// GetAllExpiredRecords returns a concatenated list of all the timeslices before currTime.
func (k Keeper) GetAllExpiredRecords(ctx sdk.Context, currTime time.Time) (expiredRecordCIDs []string) {
// Gets an iterator for all timeslices from time 0 until the current block header time.
itr := k.RecordExpiryQueueIterator(ctx, ctx.BlockHeader().Time)
defer itr.Close()
for ; itr.Valid(); itr.Next() {
timeslice, err := helpers.BytesArrToStringArr(itr.Value())
if err != nil {
panic(err)
}
expiredRecordCIDs = append(expiredRecordCIDs, timeslice...)
}
return expiredRecordCIDs
}
// ProcessRecordExpiryQueue tries to renew expiring records (by collecting rent) else marks them as deleted.
func (k Keeper) ProcessRecordExpiryQueue(ctx sdk.Context) {
cids := k.GetAllExpiredRecords(ctx, ctx.BlockHeader().Time)
for _, cid := range cids {
record := k.GetRecord(ctx, cid)
// If record doesn't have an associated bond or if bond no longer exists, mark it deleted.
if record.BondId == "" || !k.bondKeeper.HasBond(ctx, record.BondId) {
record.Deleted = true
k.PutRecord(ctx, record)
k.DeleteRecordExpiryQueue(ctx, record)
return
}
// Try to renew the record by taking rent.
k.TryTakeRecordRent(ctx, record)
}
}
// TryTakeRecordRent tries to take rent from the record bond.
func (k Keeper) TryTakeRecordRent(ctx sdk.Context, record types.Record) {
params := k.GetParams(ctx)
rent := params.RecordRent
sdkErr := k.bondKeeper.TransferCoinsToModuleAccount(ctx, record.BondId, types.RecordRentModuleAccountName, sdk.NewCoins(rent))
if sdkErr != nil {
// Insufficient funds, mark record as deleted.
record.Deleted = true
k.PutRecord(ctx, record)
k.DeleteRecordExpiryQueue(ctx, record)
return
}
// Delete old expiry queue entry, create new one.
k.DeleteRecordExpiryQueue(ctx, record)
record.ExpiryTime = ctx.BlockHeader().Time.Add(params.RecordRentDuration).Format(time.RFC3339)
k.InsertRecordExpiryQueue(ctx, record)
// Save record.
record.Deleted = false
k.PutRecord(ctx, record)
k.AddBondToRecordIndexEntry(ctx, record.BondId, record.Id)
}
// GetModuleBalances gets the registry module account(s) balances.
func (k Keeper) GetModuleBalances(ctx sdk.Context) []*types.AccountBalance {
var balances []*types.AccountBalance
accountNames := []string{types.RecordRentModuleAccountName, types.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, &types.AccountBalance{
AccountName: accountName,
Balance: accountBalance,
})
}
}
return balances
}
func recordObjToRecord(store sdk.KVStore, record types.Record) types.Record {
reverseNameIndexKey := GetCIDToNamesIndexKey(record.Id)
if store.Has(reverseNameIndexKey) {
names, err := helpers.BytesArrToStringArr(store.Get(reverseNameIndexKey))
if err != nil {
panic(err)
}
record.Names = names
}
return record
}