This site requires Javascript to be enabled.

Advanced state modeling

The design of key-value storage may seem difficult for those with a background in SQL at first glance. Even though databases like MongoDB or other streamlined databases use key-value storage, their libraries hide the internal complexity away from developers.

This is why the storage system in Cosmos-SDK may not be easy to understand initially. However, once you grasp the concept, it becomes straightforward.

While implementing a state model, it's important to take a step back and ask some questions before starting the implementation. For example:

  • Do you really need to save that information to the blockchain state?
  • Is that connection really necessary? Could it be served to the UI by an off-chain database collector?

By asking these questions, you can avoid writing unnecessary data to the state and using excess storage. Using less storage means cheaper execution.

In this tutorial, you will create a state model for the following business case:

  • The system will contain people
  • People can become members of multiple groups
  • A group can contain multiple members
  • Members can have roles in a group, such as admin, super-admin, regular, and so on.

Naive implementation

Here is an any-to-any relation design for saving data using IDs. Firstly, the person data is indexed using an auto-incremented ID:

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]pub struct Person {    pub name: String,    pub age: i32,    pub membership_ids: Vec<String>}pub const PEOPLE: Map<&[u8], Person> = Map::new("people");

In this design, groups are also indexed using an ID.

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]pub struct Group {    pub name: String,    pub membership_ids: Vec<String>}pub const GROUPS: Map<&[u8], Group> = Map::new("groups");

Group and person relation established using membership structure:

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]pub struct Membership {  pub person_id: String,  pub group_id: String,  pub membership_status_id: String}pub const MEMBERSHIPS: Map<&[u8], Membership> = Map::new("memberships");

Membership status defined using status String field.

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]pub struct MembershipStatus {  pub status: String,  pub membership_ids: Vec<String>}pub const MEMBERSHIP_STATUSES: Map<&[u8], MembershipStatus> = Map::new("membership_statuses");

Optimized implementation

Using an ID to identify people might seem intuitive, but it creates redundancy. IDs are simply values used to identify a user, but users are already identified by a unique value: their Address. Therefore, it's best to index people using their Address, rather than auto-incremented integers.

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]pub struct Person {    pub name: String,    pub age: u8, // changed to u8 since ages are unsigned and 100 years max.}// Addr -> Personpub const PEOPLE: Map<&[u8], Person> = Map::new("people");

Removed membership_id. Changed i32 to u8. Optimizing variable types improves gas consumption which results in fewer fees.


Now for the Group:

Groups don't have an address, so it makes sense to identify them using auto-incremented IDs. If you want group names to be unique, it's better to use the name as the index.

pub const GROUP_COUNTER: Item<u64> = Item::new("group_counter");#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]pub struct Group {  pub name: String,}// u64 ID -> Grouppub const GROUPS: Map<U64Key, Group> = Map::new("groups");

When a group is saved, the auto-incremented ID is required and saved to the GROUP_COUNTER item. To implement this logic, it's best to put it under a function:

pub fn next_group_counter(store: &mut dyn Storage) -> StdResult<u64> {  let id: u64 = GROUP_COUNTER.may_load(store)?.unwrap_or_default() + 1;  GROUP_COUNTER.save(store, &id)?;  Ok(id)}pub fn save_group(store: &mut dyn Storage, group: &Group) -> StdResult<()> {  let id = next_group_counter(store)?;  let key = U64Key::new(id);  NEW_GROUPS.save(store, key, group)}

In order to set up a relation between groups and people, and define a person's role, you would need to:

  • List users under a group
  • List groups of a user

This can be accomplished by building secondary indexes. We encourage you to complete the remaining implementation as a personal exercise.