argotorg / argotorg/solidity

Masking Bytecode Generation Improvements

Open
#16,716 0 comments 0 reactions 1 assignee Claimed by @cameel View on GitHub
feature
Dominant language
C++
Stars
25.7k
Forks
6.2k
Avg merge
1d 11h
Merged PRs (30d)
21

Description

## Abstract

In looking at deployed smart contracts on ethereum, 12%-16%+ of all generated solidity bytecode is masking operations. This is because the stack is 32 bytes wide, and safely using values smaller than that from untrusted or from packed sources require a cleaning operation.

![](https://ethereum-magicians.org/uploads/default/original/3X/f/b/fbce69180ca94fb1aaaa44d14696ee3009aab088.png)

Improvements here would be impactful, since bytecode size limits are a critical constraint for product development and working around them adds a lot of complication to the work of major protocols. Bytecode size also directly affects deployment costs.

## Possible Methods

Here's a survey of different methods. I'll use masking addresses as examples, since they are the most common masking size.

### Push

`PUSH20` `AND` 22 bytes, 6 gas

This is the default output when the optimizer is off. It is gas optimal but it uses increasing amounts of bytecode as the mask gets bigger.

### SHL/SUB

`PUSH1` `PUSH1` `SHL` `PUSH1` `SUB` `AND` 9 bytes, 18 gas

This is an alternative used selectively by solidity with the optimizer on. This pushs a single bit, shifts that bit left to one bit above the mask target size, and then subtracts one to fill all bits below with ones and create the mask.

```
# push 1
0x000000000000000001
# <-- shift left
0x000001000000000000
# sub1
0x000000FFFFFFFFFFFF
```

This is not Pareto optimal - there are other ways that use both less gas and less bytes.

### SHL/SHR

`PUSH1` `SHL` `PUSH1` `SHR` 6 bytes, 12 gas

This works by shifting left, to remove bytes to the left, then shifting right again to return the kept bits to their original position.

```
0x123456aaaaaaaaaaaa
# <-- shift left
0xaaaaaaaaaaaa000000
# --> shift rightt
0x000000aaaaaaaaaaaa
```

While it is more gas efficient than the next method, it does not easily compose with the existing optimization pipeline in solidity.

### Negated(0) and SHIFT

`ZERO` `NOT` `PUSH1` `SHR` `AND` 6 bytes , 15 gas

There is a stalled PR at #15935 that implements this method. There is also a rework of the same approach applied to a recent commit at #16729

### MLOAD

`PUSH1` `MLOAD` `AND` 4 bytes, 9 gas

By adding a 32 byte slot of ones after the solidity zero address, and moving where the solidity memory pointer points to, extremely efficient constants and masks can be created by just reading from the correct memory location.

The default memory now looks like:

```
SCRATCH
SCRATCH
FREEPOINTER
ZEROS
ONES
```

Then constants can be cheaply created by loading a memory region:

```
000000000000FFFFFFFFFFFF
|----------|
|----------|
```

A concept PR for this has been created #16708. This shows improvements of about 5% of total contact bytecode size vs the current SHL/SUB style size optimization, as well as corresponding gas improvements.

This method requires that the zeros and ones memory regions be untouched.

It also imposes a slight overhead of 5 bytes, and 15 gas. The bytecode overhead is more than paid back by the space savings throughout the rest of the contract. The gas overhead may result in a small additional gas usage for minimal contract methods that do not do masking.

### Masking opcode

`PUSH1` `MASKBITS` 3 bytes, 6 gas

The EVM could support a custom opcode for masking. I've written a [draft EIP]( https://gist.github.com/DanielVF/ee818a2335c465b64d596ed9c1c817ca) for a `MASKBITS` opcode that would be Pareto optimal choice for all but the smallest masks if implemented. The primary concern with this approach is if any code alters the reserved memory regions, then the compiled code will no longer behave as expected.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.