Skip to content

Latest commit

 

History

History
220 lines (196 loc) · 8.67 KB

Sway-storage.md

File metadata and controls

220 lines (196 loc) · 8.67 KB

Introduction

This article explains basics of Fuel smart contract's storage mechanics. Examples will be written in Sway.

Storage is in Fuel blockchain system is used to store information beyond a single call to a contract, this means that it is part of the Fuel state. It can be considered as a key-value type of data structure where the key is the slot number. There are 2^256 slots available for each smart contract deployed on Fuel. The 256 number was choosen to match the sha256 hashing function. The value stored in each slot is also 256 bits long (32 bytes).

Storage access and slots

Storage variables need to be firstly declared in a storage block, like in the example below:

storage {
    var: u64 = 0,
}

Note that the variables declared in the storage block must be initialized. Otherwise, the compiler will return an error.

How storage can be accessed from a contract method? Let's write a simple example:

    #[storage(read)]
    fn read_var() -> u64 {
        storage.var.read()
    }

    #[storage(read, write)]
    fn write_var(v: u64) {
        storage.var.write(v);
    }

We need to use read() to access the contents of the variable. Why? Because the storage.var represents an object of type StorageKey<u64> and to operate on a a StorageKey object you need to use read() and write() methods. The StorageKey read and write implementation can be found here and the struct definition here.

Now that we know how to access storage variables and how the storage is structured it's time to answer the question in which slot our variable var is stored. If you read carefully the StorageKey source code you noticed that there is the slot member of the StorageKey struct. It's value defines the slot where the variable is held. But we don't know how is this value generated, we didn't set it in our smart contract. The value is populated by the compiler at build time.

Before forc version 0.60.0 the slot value was generated by sha256 hashing of string storage_X, where X is the consecutive number of the variable declaration in the storage block starting from 0. In our case of var the string would be storage_0. We can obtain the hash in the following way:

$ echo -n "storage_0" | shasum -a 256 -
f383b0ce51358be57daa3b725fe44acdb2d880604e367199080b4379c41bb6ed  -

We can see the slot value in the out/debug/<project-name>-storage_slots.json file which is generated at compile time:

[
  {
    "key": "f383b0ce51358be57daa3b725fe44acdb2d880604e367199080b4379c41bb6ed",
    "value": "0000000000000000000000000000000000000000000000000000000000000000"
  }
]

After upgrading to at least 0.61.0 the method for generating storage keys is changed. The hashed value is computed using string storage.<variable-name> (if no namespaces are used). We can see that in the slots JSON file:

[
  {
    "key": "311196eb246844ea3118263d38051d8d840906389f1c07726a2743e52c7aadcb",
    "value": "0000000000000000000000000000000000000000000000000000000000000000"
  },
]

That's how we compute the hash:

$ echo -n "storage.var" | shasum -a 256 -
311196eb246844ea3118263d38051d8d840906389f1c07726a2743e52c7aadcb  -

Namespaces and in keyword

0.61.0 changed also the namespaces concept in the Sway storage implementation. Namespaces now can be declared within the storage block and variables placed within, eg:

storage {
    ns1 {
        var_ns1: u64 = 1,
    }
}

In that case the key (or slot) will be computed using the following string: storage::ns1.var_ns1. As we can see the namespace identifier is also used to compute the hash. Namespaces can be nested and if that is the case then more ::<namespace> sections will be added to the string.

You can also specify the slot directly in the storage block using the in keyword:

storage {
    var in 0x02dac99c283f16bc91b74f6942db7f012699a2ad51272b15207b9cc14a70dbae: u64,
}

We can see the slot inside the JSON file:

[
  {
    "key": "02dac99c283f16bc91b74f6942db7f012699a2ad51272b15207b9cc14a70dbae",
    "value": "0000000000000000000000000000000000000000000000000000000000000000"
  }
]

Details can be found in the PR and the Sway compiler code here.

Offset

Each StorageKey struct has the offset field which represents the place within the slot where the value is placed. It looks like the compiler is always putting there a value of 0.

Writting values and structs

Let's see how our values are written into the storage. We will create a small struct with two u64 fields and we will assign them some values. Then we will have a look at the slot JSON file. The code looks like this:

struct S {
    a: u64,
    b: u64,
}

storage {
    s: S = S { a: 1, b: 2 },
}

Because we assigned some values other than zeros to our struct's fields, we should see them in our storage layout:

[
  {
    "key": "06ee74b45a12fd83e7df88440f9b3aef7a275760bf6742de13f2007316af87cb",
    "value": "0000000000000001000000000000000200000000000000000000000000000000"
  }
]

And indeed we do! They are placed from left to right. Each field of the struct occupies 64 bits as per the field's types.

Now let's see what happens if our struct is bigger than 256 bits. Let's create one like this:

struct S {
    a: u64,
    b: u64,
    c: u64,
    d: u64,
    e: u64,
}

storage {
    s: S = S { a: 1, b: 2, c: 3, d: 4, e: 5},
}

We can see the results in the storage layout:

[
  {
    "key": "06ee74b45a12fd83e7df88440f9b3aef7a275760bf6742de13f2007316af87cb",
    "value": "0000000000000001000000000000000200000000000000030000000000000004"
  },
  {
    "key": "06ee74b45a12fd83e7df88440f9b3aef7a275760bf6742de13f2007316af87cc",
    "value": "0000000000000005000000000000000000000000000000000000000000000000"
  }
]

Our struct now occupies two slots, where the second slot key is incremented by one from the one calculated using the method demostrated above. We can use structs to pack our storage densely and hence use less slots.

StorageMap and field_id

StorageMap is a type which allows you to store contents in Sway storage that are addressable using a defined key. It is similar to Solidity mapping concept. An example declaration of a StorageMap that will use tuple (Identity, AssetId) to address value of type u64:

storage {
    balance_of: StorageMap<(Identity, AssetId), u64> = StorageMap {},
}

To access values of the StorageMap we would use the insert() and get() methods. An example insertion would look like this:

storage.balance_of.insert((address, asset), new_balance);

This is pretty strightforward. Regarding reading it is a bit more complex, because storage slots in Sway when not initialized behave differently during reading. The low level function read() as defined here returns Option<T>. When the slot was previously written to, it will return Some(value), but if it wasn't it will be None. This approach is propagated up to the StorageMap.get() hence we need to handle it e.g. in the following way:

let status = storage.balance_of.get((account, asset)).try_read();
match status {
    Option::Some(balance) => balance,
    Option::None => 0,
}

Now, when we insert() data, into which slot does it go? In the end the StorageMap declaration balance_of has its own slot which is calculated as shown in the previous sections. Yet here we can include multiple keys with different values into the StorageMap. Obviously this needs to go into different slots. Let's see how insert() is implemented:

#[storage(read, write)]
pub fn insert(self, key: K, value: V)
where
    K: Hash,
{
    let key = sha256((key, self.field_id()));
    write::<V>(key, 0, value);
}

So we are using the field_id() and key to compute a hash which will determine our storage slot. field_id() is calculated similarly to the slot, details in the PR and the Sway compiler code here.