Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: replace --symbolic-storage with enableSymbolicStorage(address) cheatcode #355

Merged
merged 35 commits into from
Aug 27, 2024

Conversation

daejunpark
Copy link
Collaborator

@daejunpark daejunpark commented Aug 26, 2024

  • deprecate --symbolic-storage, which enables symbolic storage for all accounts.
  • introduce a new cheatcode that enables symbolic storage on a per-account basis.

this simplifies the code logic, and improves usability as well. note that the symbolic storage feature is not frequently used.


(to be merged after #352) todo:

Base automatically changed from feat/empty-initial-state to main August 26, 2024 21:55
Copy link
Collaborator

@karmacoma-eth karmacoma-eth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

left code style suggestions, but nothing blocking. Love where this is headed!

src/halmos/cheatcodes.py Show resolved Hide resolved
src/halmos/__main__.py Show resolved Hide resolved
src/halmos/cheatcodes.py Outdated Show resolved Hide resolved
Comment on lines +1291 to +1292
if loc.size() not in ex.storage[addr].mapping:
ex.storage[addr].mapping[loc.size()] = cls.empty(addr, loc)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if loc.size() not in ex.storage[addr].mapping:
ex.storage[addr].mapping[loc.size()] = cls.empty(addr, loc)
ex.storage[addr].mapping.setdefault(loc.size(), cls.empty(addr, loc))

...but this always evaluates cls.empty(addr, loc) so this may be more expensive?

if we want to avoid that, a defaultdict may be better

Copy link
Collaborator Author

@daejunpark daejunpark Aug 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, i want to avoid setdefault in this case. but, how to use defaultdict when the default value is not a constant but a function of loc?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If default_factory is not None, it is called without arguments to provide a default value for the given key, this value is inserted in the dictionary for the key, and returned.

well that's a problem, I thought the key was passed to the factory 🤨 oh well

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tested this out of curiosity, it seems to work the way I want:

class betterdict(dict):
    def __init__(self, factory):
        self.factory = factory
    def __missing__(self, key):
        val = self.factory(key)
        self[key] = val
        return val

I wonder if there's some weird downside to it that would explain why defaultdict doesn't behave this way

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, i will let this particular thing stay as is. this two-liner doesn't look too bad. many other parts have been improved in #356, which looks much better. this part of code is one of the most complicated/convoluted in the codebase, so i'd prefer not to use aggressive optimizations here. 😅

class Exec: # an execution path
# network
code: dict[Address, Contract]
storage: dict[Address, dict[int, Any]] # address -> { storage slot -> value }
storage: dict[Address, StorageData] # address -> { storage slot -> value }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would a defaultdict work here? we wouldn't have to check if keys exist and explicitly initialize them

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, done in #356

Comment on lines 1145 to +1163
num_keys = len(keys)
size_keys = cls.bitsize(keys)
if slot not in ex.storage[addr]:
ex.storage[addr][slot] = {}
if num_keys not in ex.storage[addr][slot]:
ex.storage[addr][slot][num_keys] = {}
if size_keys not in ex.storage[addr][slot][num_keys]:
if slot not in ex.storage[addr].mapping:
ex.storage[addr].mapping[slot] = {}
if num_keys not in ex.storage[addr].mapping[slot]:
ex.storage[addr].mapping[slot][num_keys] = {}
if size_keys not in ex.storage[addr].mapping[slot][num_keys]:
if size_keys == 0:
if ex.symbolic:
if ex.storage[addr].symbolic:
label = f"storage_{id_str(addr)}_{slot}_{num_keys}_{size_keys}_00"
ex.storage[addr][slot][num_keys][size_keys] = BitVec(
ex.storage[addr].mapping[slot][num_keys][size_keys] = BitVec(
label, BitVecSort256
)
else:
ex.storage[addr][slot][num_keys][size_keys] = ZERO
ex.storage[addr].mapping[slot][num_keys][size_keys] = ZERO
else:
# do not use z3 const array `K(BitVecSort(size_keys), ZERO)` when not ex.symbolic
# instead use normal smt array, and generate emptyness axiom; see load()
ex.storage[addr][slot][num_keys][size_keys] = cls.empty(
ex.storage[addr].mapping[slot][num_keys][size_keys] = cls.empty(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

take this as a soft suggestion and please double check if it is equivalent. I just applied a couple of my heuristics:

  • setdefault instead of if x not in y: y[x] = ...
  • avoid repeated dict lookups, factor them out in their own variable (e.g. storage = ex.storage[addr])
  • reduce nesting with early exits when applicable (e.g. if size_keys in slot_mapping[num_keys]: return)
  • move variable definitions close to their point of use

Gives the following:

    @classmethod
    def init(cls, ex: Exec, addr: Any, slot: int, keys: tuple) -> None:
        assert_address(addr)
        storage = ex.storage[addr]
        storage.mapping.setdefault(slot, {})

        num_keys = len(keys)
        slot_mapping = storage.mapping[slot]
        slot_mapping.setdefault(num_keys, {})

        size_keys = cls.bitsize(keys)
        if size_keys in slot_mapping[num_keys]:
            return

        if size_keys != 0:
            # do not use z3 const array `K(BitVecSort(size_keys), ZERO)` when not ex.symbolic
            # instead use normal smt array, and generate emptyness axiom; see load()
            slot_mapping[num_keys][size_keys] = cls.empty(addr, slot, keys)
            return

        # size_keys == 0
        if storage.symbolic:
            label = f"storage_{id_str(addr)}_{slot}_{num_keys}_{size_keys}_00"
            slot_mapping[num_keys][size_keys] = BitVec(label, BitVecSort256)
        else:
            slot_mapping[num_keys][size_keys] = ZERO

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

taking it a couple steps further:

  • factoring out slot_mapping[num_keys]
  • using the return value of setdefault)
  • turn if else blocks into a single expression

becomes:

    @classmethod
    def init(cls, ex: Exec, addr: Any, slot: int, keys: tuple) -> None:
        assert_address(addr)
        storage = ex.storage[addr]
        slot_mapping = storage.mapping.setdefault(slot, {})

        num_keys = len(keys)
        slot_mapping_num_keys = slot_mapping.setdefault(num_keys, {})

        size_keys = cls.bitsize(keys)
        if size_keys in slot_mapping_num_keys:
            return

        if size_keys != 0:
            # do not use z3 const array `K(BitVecSort(size_keys), ZERO)` when not ex.symbolic
            # instead use normal smt array, and generate emptyness axiom; see load()
            slot_mapping_num_keys[size_keys] = cls.empty(addr, slot, keys)
            return

        # size_keys == 0
        slot_mapping_num_keys[size_keys] = (
            mk_uint256(f"storage_{id_str(addr)}_{slot}_{num_keys}_{size_keys}_00")
            if storage.symbolic
            else ZERO
        )

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be improved with a better name for slot_mapping_num_keys, but I don't have a good intuition for what it should be

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in #356

Comment on lines 1178 to 1189
else:
if not ex.symbolic:
if not ex.storage[addr].symbolic:
# generate emptyness axiom for each array index, instead of using quantified formula; see init()
ex.path.append(
Select(cls.empty(addr, slot, keys), concat(keys)) == ZERO
)
return ex.select(
ex.storage[addr][slot][num_keys][size_keys], concat(keys), ex.storages
ex.storage[addr].mapping[slot][num_keys][size_keys],
concat(keys),
ex.storages,
ex.storage[addr].symbolic,
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would avoid the else block after the return and decrease nesting here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in #356

@daejunpark daejunpark changed the title feat: deprecate --symbolic-storage + add new cheatcode initEmptyStorage(address) feat: replace --symbolic-storage with a new cheatcode enableSymbolicStorage(address) Aug 26, 2024
@daejunpark daejunpark changed the title feat: replace --symbolic-storage with a new cheatcode enableSymbolicStorage(address) feat: replace --symbolic-storage with enableSymbolicStorage(address) cheatcode Aug 26, 2024
Co-authored-by: karmacoma <karma@coma.lol>
@daejunpark
Copy link
Collaborator Author

thanks for the refactoring suggestion! let me implement it in a separate pr, as this pr is already quite nontrivial.

@daejunpark daejunpark merged commit 5515db7 into main Aug 27, 2024
57 checks passed
@daejunpark daejunpark deleted the feat/symbolic-storage branch August 27, 2024 00:19
@daejunpark
Copy link
Collaborator Author

thanks for the refactoring suggestion! let me implement it in a separate pr, as this pr is already quite nontrivial.

done in #356

@daejunpark daejunpark mentioned this pull request Aug 27, 2024
12 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants