2022-04-05 07:09:27 +00:00
|
|
|
package keeper
|
|
|
|
|
|
|
|
import (
|
2022-04-19 07:16:24 +00:00
|
|
|
"bytes"
|
2022-11-15 06:21:14 +00:00
|
|
|
"encoding/json"
|
2022-04-05 07:09:27 +00:00
|
|
|
"fmt"
|
129: Index multivalued attributes. (#128)
This fixes #129, by indexing each value of a multivalued attribute.
This handles at least the most common use case, so that we can search on a single value of the attribute.
```
❯ laconic -c ~/.laconic/local.yml cns record list --all --type ApplicationDeploymentRequest --tags b
[
{
"id": "bafyreidrp4pylixp44rkxu5il72qhwwc4ir5ctdnssps5rnelstloxivwm",
"names": null,
"owners": [
"FCCE01FCC2472AEDBCF33902907F33262445AC2C"
],
"bondId": "4ef470a9207f00fc07663623d092a14c310794b616eb53b085cfe6976e82f56d",
"createTime": "2023-12-18T22:13:23Z",
"expiryTime": "2024-12-17T22:13:23Z",
"attributes": {
"type": "ApplicationDeploymentRequest",
"version": "1.0.6",
"application": "crn://cerc-io/applications/test-progressive-web-app@0.1.1",
"config": {
"env": {
"CERC_WEBAPP_DEBUG": "57588a9d"
}
},
"tags": [
"a",
"b",
"c"
]
}
}
]
```
Reviewed-on: https://git.vdb.to/cerc-io/laconicd/pulls/128
Co-authored-by: Thomas E Lackey <telackey@bozemanpass.com>
Co-committed-by: Thomas E Lackey <telackey@bozemanpass.com>
2023-12-19 06:55:11 +00:00
|
|
|
"reflect"
|
2022-04-08 07:33:58 +00:00
|
|
|
"sort"
|
|
|
|
"time"
|
|
|
|
|
2023-03-15 15:22:35 +00:00
|
|
|
errorsmod "cosmossdk.io/errors"
|
2022-09-07 06:36:11 +00:00
|
|
|
auctionkeeper "github.com/cerc-io/laconicd/x/auction/keeper"
|
|
|
|
bondkeeper "github.com/cerc-io/laconicd/x/bond/keeper"
|
2022-12-09 04:17:14 +00:00
|
|
|
"github.com/cerc-io/laconicd/x/registry/helpers"
|
|
|
|
"github.com/cerc-io/laconicd/x/registry/types"
|
2022-04-05 07:09:27 +00:00
|
|
|
"github.com/cosmos/cosmos-sdk/codec"
|
|
|
|
"github.com/cosmos/cosmos-sdk/codec/legacy"
|
2022-04-23 15:53:51 +00:00
|
|
|
storetypes "github.com/cosmos/cosmos-sdk/store/types"
|
2022-04-05 07:09:27 +00:00
|
|
|
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"
|
2023-11-28 20:41:28 +00:00
|
|
|
"github.com/tendermint/tendermint/libs/log"
|
2022-04-05 07:09:27 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
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}
|
|
|
|
|
2022-04-20 07:37:38 +00:00
|
|
|
// PrefixCRNToNameRecordIndex is the prefix for the CRN -> NamingRecord index.
|
|
|
|
PrefixCRNToNameRecordIndex = []byte{0x02}
|
2022-04-05 07:09:27 +00:00
|
|
|
|
|
|
|
// 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}
|
|
|
|
|
2022-12-09 04:17:14 +00:00
|
|
|
// PrefixAttributesIndex is the prefix for the registry Record.Attribute -> []Record.ID index
|
2022-11-15 06:21:14 +00:00
|
|
|
PrefixAttributesIndex = []byte{0x07}
|
|
|
|
|
2022-04-05 07:09:27 +00:00
|
|
|
// 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
|
|
|
|
|
2022-04-23 15:53:51 +00:00
|
|
|
storeKey storetypes.StoreKey // Unexposed key to access store from sdk.Context
|
2022-04-05 07:09:27 +00:00
|
|
|
|
|
|
|
cdc codec.BinaryCodec // The wire codec for binary encoding/decoding.
|
|
|
|
|
|
|
|
paramSubspace paramtypes.Subspace
|
|
|
|
}
|
|
|
|
|
2022-12-09 04:17:14 +00:00
|
|
|
// NewKeeper creates new instances of the registry Keeper
|
2022-04-05 07:09:27 +00:00
|
|
|
func NewKeeper(cdc codec.BinaryCodec, accountKeeper auth.AccountKeeper, bankKeeper bank.Keeper, recordKeeper RecordKeeper,
|
2022-10-17 06:47:56 +00:00
|
|
|
bondKeeper bondkeeper.Keeper, auctionKeeper auctionkeeper.Keeper, storeKey storetypes.StoreKey, ps paramtypes.Subspace,
|
|
|
|
) Keeper {
|
2022-04-05 07:09:27 +00:00
|
|
|
// 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,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-28 20:41:28 +00:00
|
|
|
// Logger returns a module-specific logger.
|
|
|
|
func (k Keeper) Logger(ctx sdk.Context) log.Logger {
|
|
|
|
return ctx.Logger().With("module", types.ModuleName)
|
|
|
|
}
|
|
|
|
|
2022-04-05 07:09:27 +00:00
|
|
|
// 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)
|
2023-01-11 07:36:42 +00:00
|
|
|
return recordObjToRecord(store, record)
|
2022-04-05 07:09:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
2022-10-17 07:15:01 +00:00
|
|
|
records = append(records, recordObjToRecord(store, obj))
|
2022-04-05 07:09:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return records
|
|
|
|
}
|
|
|
|
|
2022-11-15 06:21:14 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
2022-04-13 12:03:42 +00:00
|
|
|
|
2022-11-15 06:21:14 +00:00
|
|
|
records := []types.Record{}
|
|
|
|
for _, id := range resultRecordIds {
|
|
|
|
record := k.GetRecord(ctx, id)
|
|
|
|
if record.Deleted {
|
|
|
|
continue
|
|
|
|
}
|
2023-01-09 06:49:11 +00:00
|
|
|
store := ctx.KVStore(k.storeKey)
|
|
|
|
recordWithNames := recordObjToRecord(store, record)
|
|
|
|
if !all && len(recordWithNames.Names) == 0 {
|
2022-11-15 06:21:14 +00:00
|
|
|
continue
|
|
|
|
}
|
2023-01-09 06:49:11 +00:00
|
|
|
records = append(records, recordWithNames)
|
2022-11-15 06:21:14 +00:00
|
|
|
}
|
|
|
|
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)
|
2022-04-13 12:03:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-11-15 06:21:14 +00:00
|
|
|
return result
|
|
|
|
}
|
2022-04-13 12:03:42 +00:00
|
|
|
|
2022-11-15 06:21:14 +00:00
|
|
|
func contains(arr []string, str string) bool {
|
|
|
|
for _, s := range arr {
|
|
|
|
if s == str {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
2022-04-13 12:03:42 +00:00
|
|
|
}
|
|
|
|
|
2022-04-05 07:09:27 +00:00
|
|
|
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.
|
2022-04-20 11:51:52 +00:00
|
|
|
func (k Keeper) ProcessSetRecord(ctx sdk.Context, msg types.MsgSetRecord) (*types.RecordType, error) {
|
2022-04-05 07:09:27 +00:00
|
|
|
payload := msg.Payload.ToReadablePayload()
|
2022-10-17 11:03:31 +00:00
|
|
|
record := types.RecordType{Attributes: payload.Record, BondID: msg.BondId}
|
2022-04-05 07:09:27 +00:00
|
|
|
|
|
|
|
// Check signatures.
|
|
|
|
resourceSignBytes, _ := record.GetSignBytes()
|
|
|
|
cid, err := record.GetCID()
|
|
|
|
if err != nil {
|
2023-03-15 15:22:35 +00:00
|
|
|
return nil, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "Invalid record JSON")
|
2022-04-05 07:09:27 +00:00
|
|
|
}
|
|
|
|
|
2022-10-17 11:03:31 +00:00
|
|
|
record.ID = cid
|
2022-04-05 07:09:27 +00:00
|
|
|
|
2022-10-17 11:03:31 +00:00
|
|
|
if exists := k.HasRecord(ctx, record.ID); exists {
|
2022-04-20 11:51:52 +00:00
|
|
|
// Immutable record already exists. No-op.
|
|
|
|
return &record, nil
|
2022-04-05 07:09:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
2023-03-15 15:22:35 +00:00
|
|
|
return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, "Invalid public key.")
|
2022-04-05 07:09:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
sigOK := pubKey.VerifySignature(resourceSignBytes, helpers.BytesFromBase64(sig.Sig))
|
|
|
|
if !sigOK {
|
|
|
|
fmt.Println("Signature mismatch: ", sig.PubKey)
|
2023-03-15 15:22:35 +00:00
|
|
|
return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, "Invalid signature.")
|
2022-04-05 07:09:27 +00:00
|
|
|
}
|
|
|
|
record.Owners = append(record.Owners, pubKey.Address().String())
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sort owners list.
|
|
|
|
sort.Strings(record.Owners)
|
|
|
|
sdkErr := k.processRecord(ctx, &record, false)
|
|
|
|
if sdkErr != nil {
|
2022-04-20 11:51:52 +00:00
|
|
|
return nil, sdkErr
|
2022-04-05 07:09:27 +00:00
|
|
|
}
|
2022-04-20 11:51:52 +00:00
|
|
|
return &record, nil
|
2022-04-05 07:09:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (k Keeper) processRecord(ctx sdk.Context, record *types.RecordType, isRenewal bool) error {
|
|
|
|
params := k.GetParams(ctx)
|
|
|
|
rent := params.RecordRent
|
|
|
|
|
2022-10-17 11:03:31 +00:00
|
|
|
err := k.bondKeeper.TransferCoinsToModuleAccount(ctx, record.BondID, types.RecordRentModuleAccountName, sdk.NewCoins(rent))
|
2022-04-05 07:09:27 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-04-08 07:33:58 +00:00
|
|
|
record.CreateTime = ctx.BlockHeader().Time.Format(time.RFC3339)
|
2022-04-14 03:56:12 +00:00
|
|
|
record.ExpiryTime = ctx.BlockHeader().Time.Add(params.RecordRentDuration).Format(time.RFC3339)
|
2022-04-05 07:09:27 +00:00
|
|
|
record.Deleted = false
|
|
|
|
|
2022-11-15 06:21:14 +00:00
|
|
|
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)
|
2022-04-05 07:09:27 +00:00
|
|
|
|
|
|
|
// Renewal doesn't change the name and bond indexes.
|
|
|
|
if !isRenewal {
|
2022-10-17 11:03:31 +00:00
|
|
|
k.AddBondToRecordIndexEntry(ctx, record.BondID, record.ID)
|
2022-04-05 07:09:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2022-11-15 06:21:14 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-12-07 22:19:10 +00:00
|
|
|
case "WebsiteRegistrationRecord", "ApplicationRecord", "ApplicationDeploymentRequest",
|
2023-12-14 02:54:57 +00:00
|
|
|
"ApplicationDeploymentRecord", "ApplicationArtifact", "DnsRecord", "GeneralRecord":
|
2022-11-15 06:21:14 +00:00
|
|
|
{
|
|
|
|
// #nosec G705
|
|
|
|
for key := range record.Attributes {
|
129: Index multivalued attributes. (#128)
This fixes #129, by indexing each value of a multivalued attribute.
This handles at least the most common use case, so that we can search on a single value of the attribute.
```
❯ laconic -c ~/.laconic/local.yml cns record list --all --type ApplicationDeploymentRequest --tags b
[
{
"id": "bafyreidrp4pylixp44rkxu5il72qhwwc4ir5ctdnssps5rnelstloxivwm",
"names": null,
"owners": [
"FCCE01FCC2472AEDBCF33902907F33262445AC2C"
],
"bondId": "4ef470a9207f00fc07663623d092a14c310794b616eb53b085cfe6976e82f56d",
"createTime": "2023-12-18T22:13:23Z",
"expiryTime": "2024-12-17T22:13:23Z",
"attributes": {
"type": "ApplicationDeploymentRequest",
"version": "1.0.6",
"application": "crn://cerc-io/applications/test-progressive-web-app@0.1.1",
"config": {
"env": {
"CERC_WEBAPP_DEBUG": "57588a9d"
}
},
"tags": [
"a",
"b",
"c"
]
}
}
]
```
Reviewed-on: https://git.vdb.to/cerc-io/laconicd/pulls/128
Co-authored-by: Thomas E Lackey <telackey@bozemanpass.com>
Co-committed-by: Thomas E Lackey <telackey@bozemanpass.com>
2023-12-19 06:55:11 +00:00
|
|
|
attr := record.Attributes[key]
|
|
|
|
if reflect.Slice == reflect.TypeOf(attr).Kind() {
|
|
|
|
av := attr.([]interface{})
|
|
|
|
for i := range av {
|
|
|
|
indexKey := GetAttributesIndexKey(key, av[i])
|
|
|
|
if err := k.SetAttributeMapping(ctx, indexKey, record.ID); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
indexKey := GetAttributesIndexKey(key, attr)
|
|
|
|
if err := k.SetAttributeMapping(ctx, indexKey, record.ID); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-11-15 06:21:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
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 {
|
122: Fix attribute index key collision (#123)
Fix #122, where the structure of the index key allowed unintended collisions (see below).
This also adds a test cases which _fails_ under the old scheme, but passes now:
**Before:**
```
--- FAIL: TestKeeperTestSuite (0.31s)
--- FAIL: TestKeeperTestSuite/TestGrpcGetRecordLists (0.09s)
grpc_query_test.go:143:
Error Trace: /home/telackey/cerc/laconicd/x/registry/keeper/grpc_query_test.go:143
/home/telackey/cerc/laconicd/x/registry/keeper/suite.go:91
Error: Not equal:
expected: 0
actual : 1
Test: TestKeeperTestSuite/TestGrpcGetRecordLists
--- FAIL: TestKeeperTestSuite/TestGrpcGetRecordLists/Case_Filter_with_typ_(https://git.vdb.to/cerc-io/laconicd/issues/122)_ (0.00s)
testing.go:1490: test executed panic(nil) or runtime.Goexit: subtest may have called FailNow on a parent test
FAIL
FAIL github.com/cerc-io/laconicd/x/registry/keeper 0.765s
FAIL
make: *** [Makefile:333: run-tests] Error 1
❯ laconic cns record list --all --typ eWebsiteRegistrationRecord
[
{
"id": "bafyreies5he2mxyrjso2quewwlmh6cisekkjnf7yijpd2742wrzxgzuazi",
"names": null,
"owners": [
"FC9B9FB065D70DBB10C8F511348421C16669B37D"
],
"bondId": "a9c7161fc154cf44ee61efc699a59c8a27c804bb23ce4c8a7ffa0a39fcc540b1",
"createTime": "2023-11-28T19:09:03Z",
"expiryTime": "2024-11-27T19:09:03Z",
"attributes": {
"url": "https://hello-urbit.laconic.com",
"repo_registration_record_cid": "QmTZQ8ZJS6mALjEM2wY71msFno6zzxFftVCiZELj9xREPx",
"build_artifact_cid": "~lostex-rabdur-labtul-moltev/hello-urbit",
"tls_cert_cid": "QmR1acEmQt7Tjmhp9cFtymie2eFcrHURQKt9kGto1TQTW1",
"type": "WebsiteRegistrationRecord",
"version": "0.2.4"
}
}
...
❯ laconic cns record list --all --type WebsiteRegistrationRecord
[
{
"id": "bafyreies5he2mxyrjso2quewwlmh6cisekkjnf7yijpd2742wrzxgzuazi",
"names": null,
"owners": [
"FC9B9FB065D70DBB10C8F511348421C16669B37D"
],
"bondId": "a9c7161fc154cf44ee61efc699a59c8a27c804bb23ce4c8a7ffa0a39fcc540b1",
"createTime": "2023-11-28T19:09:03Z",
"expiryTime": "2024-11-27T19:09:03Z",
"attributes": {
"url": "https://hello-urbit.laconic.com",
"repo_registration_record_cid": "QmTZQ8ZJS6mALjEM2wY71msFno6zzxFftVCiZELj9xREPx",
"build_artifact_cid": "~lostex-rabdur-labtul-moltev/hello-urbit",
"tls_cert_cid": "QmR1acEmQt7Tjmhp9cFtymie2eFcrHURQKt9kGto1TQTW1",
"type": "WebsiteRegistrationRecord",
"version": "0.2.4"
}
}
...
```
**After:**
```
ok github.com/cerc-io/laconicd/x/registry/keeper 1.573s
❯ laconic cns record list --all --typ eWebsiteRegistrationRecord
[]
❯ laconic cns record list --all --type WebsiteRegistrationRecord
[
{
"id": "bafyreies5he2mxyrjso2quewwlmh6cisekkjnf7yijpd2742wrzxgzuazi",
"names": null,
"owners": [
"FC9B9FB065D70DBB10C8F511348421C16669B37D"
],
"bondId": "a9c7161fc154cf44ee61efc699a59c8a27c804bb23ce4c8a7ffa0a39fcc540b1",
"createTime": "2023-11-28T19:09:03Z",
"expiryTime": "2024-11-27T19:09:03Z",
"attributes": {
"url": "https://hello-urbit.laconic.com",
"repo_registration_record_cid": "QmTZQ8ZJS6mALjEM2wY71msFno6zzxFftVCiZELj9xREPx",
"build_artifact_cid": "~lostex-rabdur-labtul-moltev/hello-urbit",
"tls_cert_cid": "QmR1acEmQt7Tjmhp9cFtymie2eFcrHURQKt9kGto1TQTW1",
"type": "WebsiteRegistrationRecord",
"version": "0.2.4"
}
}
...
```
Reviewed-on: https://git.vdb.to/cerc-io/laconicd/pulls/123
Co-authored-by: Thomas E Lackey <telackey@bozemanpass.com>
Co-committed-by: Thomas E Lackey <telackey@bozemanpass.com>
2023-11-29 00:05:56 +00:00
|
|
|
keyString := fmt.Sprintf("%s=%s", key, value)
|
2022-11-15 06:21:14 +00:00
|
|
|
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) {
|
2023-11-28 20:41:28 +00:00
|
|
|
k.Logger(ctx).Debug(fmt.Sprintf("store doesn't have key: %q", key))
|
|
|
|
return []string{}, nil
|
2022-11-15 06:21:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-04-05 07:09:27 +00:00
|
|
|
// 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) {
|
2022-04-08 07:33:58 +00:00
|
|
|
expiryTime, err := time.Parse(time.RFC3339, val.ExpiryTime)
|
2022-04-14 03:56:12 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
2022-04-08 07:33:58 +00:00
|
|
|
}
|
2022-04-14 03:56:12 +00:00
|
|
|
|
|
|
|
timeSlice := k.GetRecordExpiryQueueTimeSlice(ctx, expiryTime)
|
|
|
|
timeSlice = append(timeSlice, val.Id)
|
|
|
|
k.SetRecordExpiryQueueTimeSlice(ctx, expiryTime, timeSlice)
|
2022-04-05 07:09:27 +00:00
|
|
|
}
|
|
|
|
|
2022-04-19 07:16:24 +00:00
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2022-12-09 04:17:14 +00:00
|
|
|
// GetModuleBalances gets the registry module account(s) balances.
|
2022-04-05 07:09:27 +00:00
|
|
|
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
|
|
|
|
}
|
2022-04-14 03:56:12 +00:00
|
|
|
|
2022-10-17 07:15:01 +00:00
|
|
|
func recordObjToRecord(store sdk.KVStore, record types.Record) types.Record {
|
2022-04-14 03:56:12 +00:00
|
|
|
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
|
|
|
|
}
|