1

My solidity code example:

// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.2;

library TestLibrary {
    function sz_varint(uint256 i) internal pure returns (uint256) {
        uint256 count = 1;
        assembly {
            i := shr(7, i)
            for {} gt(i, 0) {} {
                i := shr(7, i)
                count := add(count, 1)
            }
        }
        return count;
    }

    function encode_varint_assembly(uint256 x, uint256 p, bytes memory bs) internal pure returns (uint256) {
        uint256 sz = 0;
        assembly {
            let bsptr := add(bs, p)
            let byt := and(x, 0x7f)
            for {} gt(shr(7, x), 0) {} {
                mstore8(bsptr, or(0x80, byt))
                bsptr := add(bsptr, 1)
                sz := add(sz, 1)
                x := shr(7, x)
                byt := and(x, 0x7f)
            }
            mstore8(bsptr, byt)
            sz := add(sz, 1)
        }
        return sz;
    }
    function encode_varint_assembly_nomstore8(uint256 x, uint256 p, bytes memory bs) internal pure returns (uint256) {
        uint256 sz = 0;
        assembly {
            let bsptr := add(bs, p)
            let byt := and(x, 0x7f)
            for {} gt(shr(7, x), 0) {} {
                //mstore8(bsptr, or(0x80, byt))
                bsptr := add(bsptr, 1)
                sz := add(sz, 1)
                x := shr(7, x)
                byt := and(x, 0x7f)
            }
            //mstore8(bsptr, byt)
            sz := add(sz, 1)
        }
        return sz;
    }
    function encode_varint(uint256 x, uint256 p, bytes memory bs) internal pure returns (uint256) {
        uint256 tmp = x;
        uint256 idx = p;
        bytes1 byt = bytes1(uint8(tmp & 0x7f));
        while (tmp > 0x7f) {
            bs[idx] = byt | 0x80;
            tmp = tmp >> 7;
            byt = bytes1(uint8(tmp & 0x7f));
            idx += 1;
        }
        bs[idx] = byt;
        return idx - p + 1;
    }
}

contract TestContract {
    function encode_varint_assembly(uint256 x) public pure returns(bytes memory) {
        uint256 sz = TestLibrary.sz_varint(x);
        bytes memory buffer = new bytes(sz);
        TestLibrary.encode_varint_assembly(x, 0, buffer);
        return buffer;
    }
    function encode_varint_assembly_nomstore8(uint256 x) public pure returns(bytes memory) {
        uint256 sz = TestLibrary.sz_varint(x);
        bytes memory buffer = new bytes(sz);
        TestLibrary.encode_varint_assembly_nomstore8(x, 0, buffer);
        return buffer;
    }
    function encode_varint(uint256 x) public pure returns(bytes memory) {
        uint256 sz = TestLibrary.sz_varint(x);
        bytes memory buffer = new bytes(sz);
        TestLibrary.encode_varint(x, 0, buffer);
        return buffer;
    }
}

try running some unit tests in go using abigen here it's my go code

package main

import (
    "fmt"
    "math/big"

    "github.com/ethereum/go-ethereum/accounts/abi/bind"
    "github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/core"
    "github.com/ethereum/go-ethereum/crypto"
)

func main() {
    auth, client := setup()
    _, _, entry, err := DeployTestContract(auth, client)
    if err != nil {
        panic(err)
    }
    client.Commit()

    res, err := entry.EncodeVarint(nil, big.NewInt(10))
    if err != nil {
        panic(err)
    }
    fmt.Println("EncodeVarint", res)
    res, err = entry.EncodeVarintAssemblyNomstore8(nil, big.NewInt(10))
    if err != nil {
        panic(err)
    }
    fmt.Println("EncodeVarintAssemblyNomstore8", res)
    res, err = entry.EncodeVarintAssembly(nil, big.NewInt(10))
    if err != nil {
        panic(err)
    }
    fmt.Println("EncodeVarintAssembly", res)
}

func setup() (*bind.TransactOpts, *backends.SimulatedBackend) {
    gAlloc := make(map[common.Address]core.GenesisAccount, 1)
    deployerKey, err := crypto.GenerateKey()
    if err != nil {
        panic(err)
    }
    deployerAuth := bind.NewKeyedTransactor(deployerKey)
    gAlloc[deployerAuth.From] = core.GenesisAccount{Balance: big.NewInt(1000000000000000000)} // 1 eth
    client := backends.NewSimulatedBackend(gAlloc, 1000000000000000000) // 1 eth
    return deployerAuth, client
}

versions are

$ solc --version
solc, the solidity compiler commandline interface
Version: 0.8.9+commit.e5eed63a.Linux.g++
$ abigen --version
abigen version 1.10.0-stable

a snippet of my go.mod

module experiments/solidity_varint
go 1.17

require github.com/ethereum/go-ethereum v1.10.0
...

output of running the main is:

EncodeVarint [10]
EncodeVarintAssemblyNomstore8 [0]
panic: out of gas

goroutine 1 [running]:
main.main()
    /home/giulio/go/src/experiments/solidty_varint/main.go:34 +0x24b
exit status 2

Apparently the mstore8 instruction makes the whole call goes out of gas. Is there any reason?? Where is the error in my mstore8 assembly call?

Note: This varint functions are decoding function for a protobuf solidity runtime decoder I am working with/on

1 Answer 1

0

You are not really running out of gas your are seeing an EVM panic.

I think it is coming from overwriting the arrays lengths (see this answer).

In your implementation :

  function encode_varint_assembly(uint256 x, uint256 p, bytes memory bs) internal pure returns (uint256) {
        uint256 sz = 0;
        assembly {
            let bsptr := add(bs, p)
            let byt := and(x, 0x7f)
            for {} gt(shr(7, x), 0) {} {
                mstore8(bsptr, or(0x80, byt))
                bsptr := add(bsptr, 1)
                sz := add(sz, 1)
                x := shr(7, x)
                byt := and(x, 0x7f)
            }
            mstore8(bsptr, byt)
            sz := add(sz, 1)
        }
        return sz;
    }

I think that this line is the problem :

let bsptr := add(bs,p)

As you call your functions with p = 0 from your TestContract you are effectively writing : let bsptr := add(bs, 0) but bs being an array, bsptr point to the array length. Rewriting it will send the EVM into panic.

Changing it to :

let bsptr := add(bs,32)

makes bsptr points to the actual data of the array. (skipping the first 32bytes reserved for the read only array size)

With this change calls to encode_varint and encore_varint_assembly are both successfull and return the same value. I don't know much about protobuf so I cannot really validate the content of the returned value...

If I understand correctly, p might be an offset in the array where you'd want to write your encoded integer x. If so, this might be more correct regarding what you want to do :

let bsptr := add(bs, add(32, p))

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.