home

Lazy-set in FA2-SmartPy

Quick post detailing the use of “lazy-sets” in FA2-SmartPy.
And trying to get back at the blog …

Some people found it surprising, the implementation of FA2-SmartPy contains this in its storage type:

(big_map %operators
   (pair (address %owner) (pair (address %operator) (nat %token_id)))
   unit))

See for instance the compiled output: 20200910-203659+0000_5060996_mutran_contract.tz#L30-32 or the corresponding source: multi_asset.py#L224-258.

The idea is that this is a “lazy set.” It quacks like a set data-structure but it is implemented with a big-map:

(set <VALUE-TYPE>)(big_map <VALUE-TYPE> unit)

Indeed the issue to avoid is the case where a contract's storage is so big that its deserialization consumes too much gas before even starting the execution of the entrypoint.

The Delphi protocol pushes the limit further than the current protocol but it's still very reachable.

Let's figure out how much …

This is a small contract that fills up a set of consecutive nat values every time it is called, it has another unit entrypoint with minimal gas usage (You can also open it in the SmartPy.io editor):

import smartpy as sp

class StoreValue(sp.Contract):
    def __init__(self, value):
        self.init(store = sp.set(t = sp.TNat))

    @sp.entry_point
    def add(self, params):
        sp.for v in sp.range(sp.len(self.data.store), params.value):
            self.data.store.add(v)

    @sp.entry_point
    def default(self):
        pass

if "templates" not in __name__:
    @sp.add_test(name = "StoreValue")
    def test():
        c1 = StoreValue(12)
        scenario = sp.test_scenario()
        scenario.h1("Store Value")
        scenario += c1
        scenario += c1.add(value = 15)

Let's experiment with a “manual” Flextesa sandbox …

Originate the contract with an empty set:

tezos-client originate contract c0 transferring 0 from bootacc-0 running setz.tz --init {} --burn-cap 10

Every call adds new elements from the current size of the set to the value of the argument, like this:

tezos-client transfer 0 from bootacc-0 to c0 --arg 60000 --burn-cap 10 --entrypoint add

After some dichotomy, we can see that the limit is at around 59 100. At which point we had enough gas to add a bunch of elements to the set but we put the contract in a state where “doing nothing” fails:

tezos-client transfer 0 from bootacc-0 to c0 --burn-cap 10 --entrypoint default

With Carthage the limit is much lower (I got around 3 100).

In that state, the contract is fully locked-down, in particular any balance cannot be transferred back. This is what the lazy-set is avoiding: a big-map's growth does not impact deserialization gas consumption.

After 8 years of blograstination, this is post #6 of my attempt at not getting too fast lagging behind on the #100DaysToOffload “challenge” …

And yes … that's already a long pause between posts … Quite the slow down on the blogging cadence right there! I have however done some writing here and there and even some video-talking.