dcposch/btcmirror

๐Ÿง™โ€โ™‚๏ธ Outlining a path towards gas optimization

Opened this issue ยท 3 comments

Hello, I've been looking over the codebase the last few days and I noticed you could use some help optimizing the contract so that it may be better suited for a production environment. To get us started, I thought I'd first start by listing some potential solutions:

  • SSTORE2 could potentially be implemented

  • I'm not entirely sure, but I think most of the arithmetic can be unchecked. (NOTE: where underflow/overflow is impossible or not possible in realistic conditions)

  • Storage can be optimized/packed into less slots since most of mutable state is updated together.

    // single storage slot, rather than 3
    uint32 private latestBlockTime; // equal to lastBlockTime % 2**32
    uint112 private latestBlockHeight;
    uint112 private expectedTarget;

    // can these be simplified or condesed?
    mapping(uint256 => bytes32) private blockHeightToHash;
    mapping(uint256 => uint256) private blockHeightToTotalDifficulty;
  • SLOADs can be avoided, consider caching state variables in memory before using them several times in a function.

  • A rewrite in inline assembly or yul+ could potentially provide some benefit as well.

P.S. Feel free to send questions my way!

@0xClandestine
That should work right? exactly uint256

    uint32 private latestBlockTime; // equal to lastBlockTime % 2**32
    uint112 private latestBlockHeight;
    uint112 private expectedTarget;
    // can these be simplified or condesed?
    mapping(uint256 => bytes32) private blockHeightToHash;
    mapping(uint256 => uint256) private blockHeightToTotalDifficulty;

BTC hash is 256 bit long, so that is one slot gone, Difficulty could fit in a 48bit uint

struct Block { hash: byte32, difficulty: uint}
mapping(uint256 => Block) private blockHeightToBlock;

It may save a table of look up key, not sure how much overhead the struct would introduce.

@0xClandestine i just saw this. Thank you!

This looks excellent.

The biggest single cost before was the 20k gas per block to open a new storage slot.

I've optimized that using a ring buffer--by only storing the last n (eg n=1000) blocks, after the initial n blocks to fill the array, we pay only 3k gas per block, because we're overwriting an existing storage slot.

PR #6

Combining latestBlockTime/latestBlockHeight is a good suggestion. I'll do that too.

After #6, we're already down to 11k marginal cost per block proven for a big batch. Doing that in addition should bring us to ~8k.