Storage
Contracting stores state in a key-value storage system where each smart contract has it's own data space that cannot be accessed by other smart contracts besides through the @export
functions.
When you submit a smart contract, keys are created to store the code and compiled bytecode of the contract. For example:
owner = Variable()
@construct
def seed():
owner.set(ctx.caller)
When submitted will create the following in state space:
Key | Value |
---|---|
contract.__compiled__ | Python Bytecode |
contract.__code__ | Python Code |
contract.owner | ctx.caller at submission time |
Storage follows a simple pattern such that each variable or hash stored is prefaced by the contract name and a period delimiter. If the variable has additional keys, they are appended to the end seperated by colons.
<contract_name>.<variable_name>
<contract_name>.<variable_name>:<key_0>:<key_1>:<key_2>...
Encoding
Data is encoded as JSON in the state space. This means that you can store simple Python objects such as dictionaries, arrays, tuples, and even Datetime and Timedelta types (explained later.)
player = Variable()
stats = {
'name': 'Steve',
'level': 100,
'type': 'Mage',
'health': 1000
}
player.set(stats)
steve = player.get()
steve['health'] -= 100
steve['health'] == 900 # True
authorized_parties = Variable()
parties = ['steve', 'alex', 'bill', 'raghu', 'tejas']
authorized_parties.set(parties)
# This will fail if the contract sender isn't in the authorized parties list.
assert ctx.caller in authorized_parties.get()
Storage Types
There are two types of storage: Variable and Hash. Variable only has a single storage slot. Hash allows for a dynamic amount of dimensions to be added to them. Hashes are great for data types such as balances or mappings.
owner = Variable()
balances = Hash()
@export
def example():
owner.set('hello')
a = owner.get()
balances['bill'] = 100
a = balances['something']
Variable API
class Variable(Datum):
def __init__(self, contract, name, driver: ContractDriver=driver, t=None):
...
def set(self, value):
...
def get(self):
...
__init__(self, contract, name, driver, t)
The __init__ arguments are automatically filled in for you during compilation and runtime. You do not have to provide any of them.
some_contract.py (Smart Contract)
owner = Variable()
This translates into:
owner = Variable(contract='some_contract', name='owner')
Driver is pulled from the Runtime (rt
) module when the contract is being executed. If you provide a type to t
, the Variable object will make sure that whatever is being passed into set
is the correct type.
set(self, value)
some_contract.py (Smart Contract)
owner = Variable()
owner.set('bill')
Executes on contract runtime and sets the value for this variable. The above code causes the following key/value pair to be written into the state.
Key | Value |
---|---|
some_contract.owner | bill |
NOTE: You have to use the set
method to alter data. If you use standard =
, it will just cause the object to be set to whatever you pass.
owner = Variable()
owner
>> <Variable at 0x10577cda0>
owner = 5
owner
>> 5
get(self)
some_contract.py (Smart Contract)
owner = Variable()
owner.set('bill')
owner.get() == 'bill' # True
Returns the value that is stored at this Variable's state location.
NOTE: The converse applies to the get
function. Simply setting a variable to the Variable object will just copy the reference, not the underlying data.
owner = Variable()
owner.set('bill')
owner.get()
>> 'bill'
a = owner
a
>> <Variable at 0x10577cda0>
Hash API
class Hash(Datum):
def __init__(self, contract, name, driver: ContractDriver=driver, default_value=None):
...
def set(self, key, value):
...
def get(self, item):
...
def all(self, *args):
...
def clear(self, *args):
...
def __setitem__(self, key, value):
...
def __getitem__(self, key):
...
__init__(self, contract, name, driver, default_value)
Similar to Variable's __init__ except that a different keyword argument default_value
allows you to set a value to return when the key does not exist. This is good for ledgers or applications where you need to have a base value.
some_contract.py (Smart Contract)
balances = Hash(default_value=0)
balances['bill'] = 1_000_000
balances['bill'] == 1_000_000 # True
balances['raghu'] == 0 # True
set(self, key, value)
Equivalent to Variable's get
but accepts an additional argument to specify the key. For example, the following code executed would result in the following state space.
some_contract.py (Smart Contract)
balances = Hash(default_value=0)
balances.set('bill', 1_000_000)
balances.set('raghu', 100)
balances.set('tejas', 777)
Key | Value |
---|---|
some_contract.balances:bill | 1,000,000 |
some_contract.balances:raghu | 100 |
some_contract.balances:tejas | 777 |
Multihashes
You can provide an arbitrary number of keys (up to 16) to set
and it will react accordingly, writing data to the dimension of keys that you provided. For example:
subaccounts.py (Smart Contract)
balances = Hash(default_value=0)
balances.set('bill', 1_000_000)
balances.set(('bill', 'raghu'), 1_000)
balances.set(('raghu', 'bill'), 555)
balances.set(('bill', 'raghu', 'tejas'), 777)
This will create the following state space:
Key | Value |
---|---|
subaccounts.balances:bill | 1,000,000 |
subaccounts.balances:bill:raghu | 1,000 |
subaccounts.balances:raghu:bill | 555 |
subaccounts.balances:bill:raghu:tejas | 777 |
get(self, key)
Inverse of set
, where the value for a provided key is returned. If it is None
, it will set it to the default_value
provided on initialization.
some_contract.py (Smart Contract)
balances = Hash(default_value=0)
balances.set('bill', 1_000_000)
balances.set('raghu', 100)
balances.set('tejas', 777)
balances.get('bill') == 1_000_000 # True
balances.get('raghu') == 100 # True
balances.get('tejas') == 777 # True
The same caveat applies here
Multihashes
Just like set
, you retrieve data stored in multihashes by providing the list of keys used to write data to that location. Just like get
with a single key, the default value will be returned if no value at the storage location is found.
subaccounts.py (Smart Contract)
balances = Hash(default_value=0)
balances.set('bill', 1_000_000)
balances.set(('bill', 'raghu'), 1_000)
balances.set(('raghu', 'bill'), 555)
balances.set(('bill', 'raghu', 'tejas'), 777)
balances.get('bill') == 1_000_000 # True
balances.get(('bill', 'raghu')) == 1_000 # True
balances.get(('raghu', 'bill')) == 555 # True
balances.get(('bill', 'raghu', 'tejas')) == 777 # True
balances.get(('bill', 'raghu', 'tejas', 'steve')) == 0 # True
NOTE: If storage returns a Python object or dictionary, modifications onto that dictionary will not be synced to storage until you set the key to the altered value again. This is vitally important.
owner = Hash(default_value=0)
owner.set('bill') = {
'complex': 123,
'object': 567
}
d = owner.get('bill') # Get the dictionary from storage
d['complex'] = 999 # Set a value on the retrieved dictionary
e = owner.get('bill') # Retrieve the same value for comparison
d['complex'] == e['complex'] # False
owner = Hash(default_value=0)
owner.set('bill') = {
'complex': 123,
'object': 567
}
d = owner.get('bill') # Get the dictionary from storage
d['complex'] = 999 # Set a value on the retrieved dictionary
owner.set('bill', d) # Set storage location to the modified dictionary
e = owner.get('bill') # Retrieve the same value for comparison
d['complex'] == e['complex'] # True!
__setitem__(self, key, value):
Equal functionality to set
, but allows slice notation for convenience. This is less verbose and the preferred method of setting storage on a Hash.
subaccounts.py (Smart Contract)
balances = Hash(default_value=0)
balances['bill'] = 1_000_000
balances['bill', 'raghu'] = 1_000
balances['raghu', 'bill'] = 555
balances['bill', 'raghu', 'tejas'] = 777
NOTE: The problem that occurs with Variable's set does not occur with Hashes.
owner = Hash(default_value=0)
owner['bill'] = 100
owner['bill']
>> 100
__getitem__(self, key):
Equal functionality to set
, but allows slice notation for convenience. This is less verbose and the preferred method of setting storage on a Hash.
subaccounts.py (Smart Contract)
balances = Hash(default_value=0)
balances['bill'] = 1_000_000
balances['bill', 'raghu'] = 1_000
balances['raghu', 'bill'] = 555
balances['bill', 'raghu', 'tejas'] = 777
balances['bill'] == 1_000_000 # True
balances['bill', 'raghu'] == 1_000 # True
balances['raghu', 'bill'] == 555 # True
balances['bill', 'raghu', 'tejas'] == 777 # True
balances['bill', 'raghu', 'tejas', 'steve'] == 0 # True
all(self, *args):
Returns all of the values in a particular hash. For multihashes, it returns all values in that 'subset' of hashes. Assume the following state space:
Key | Value |
---|---|
subaccounts.balances:bill | 1,000,000 |
subaccounts.balances:bill:raghu | 1,000 |
subaccounts.balances:bill:tejas | 555 |
subaccounts.balances:raghu | 777 |
subaccounts.balances:raghu:bill | 10,000 |
subaccounts.balances:raghu:tejas | 100,000 |
balances.all()
>> [1000000, 1000, 555, 777, 10000, 100000]
balances.all('raghu')
>> [777, 10000, 100000]
clear(self, *args)
Clears an entire hash or a section of a hash if the list of keys are provided. Assume the same state space:
Key | Value |
---|---|
subaccounts.balances:bill | 1,000,000 |
subaccounts.balances:bill:raghu | 1,000 |
subaccounts.balances:bill:tejas | 555 |
subaccounts.balances:raghu | 777 |
subaccounts.balances:raghu:bill | 10,000 |
subaccounts.balances:raghu:tejas | 100,000 |
balances.clear('bill')
balances.all() # None of Raghu's accounts are affected
>> [777, 10000, 100000]
balances.clear()
balances.all() # All entries have been deleted
>> []