assembly

SolidityにおけるBit演算とInline Assemblyについて

bit演算関係について、ライブラリとしてではなく、コード書く時の参考をイメージしてまとめました。

https://github.com/eggdragons/how-to-use-foundry

Bit演算関係について

先ほどのリポジトリの中のsrc/bit/Bitwise.solには、bit演算、bitシフトについて記載しています。

test/bit/Bitwise.t.solが上記のテストになっているのですが、ここでは、bit演算についてコメント記載しています。

例えば、

    function and(uint256 x, uint256 y) external pure returns (uint256) {
        return x & y;
    }

このbit演算の解説??として

    function testBitwiseAnd() public {
        uint256 x = 10;
        uint256 y = 13;
        uint256 result = 8;

        // x = 10 = 1010
        // y = 13 = 1101
        // x & y  = 1000 = 8
        assertEq(bitwise.and(x, y), result);
    }

このような形で記載しています。テストコードを見てもらえば、雰囲気掴めるのと、自分でテストしやすいように構成しています。

Bit演算をInline Assembly化

src/bit/Bitwise.solのコードをInline Assembly化したものが、src/bit/BitFlagAssembly.solになります。

test/bit/BitFlagAssembly.t.solでは、Inline Assemblyする前とした後の比較テストになっています。

また、Fuzzテストを行うことで、簡易的にArithmetic over/underflowのチェックを行っています。

Inline Assemblyを使う際、over/underflowのチェックは重要になってくるので、要チェックですね〜。

例えばですが

    function getLastNBits(uint256 x, uint256 n) external pure returns (uint256 result) {
        assembly {
            let mask := sub(shl(n, 1), 1)
            result := and(x, mask)
        }
    }

このコードは、このまま使えません。nが256以上で必ずオーバーフローします。

もちろんfoundryのfuzzテストでは、エラーが出ます。そこで、下記のようなテストコードになるわけです。

    function testBitwiseAssemblyGetLastNBitsFuzz(uint256 x, uint256 n) public {
        vm.assume(n < 256);
        assertEq(bitwise.getLastNBits(x, n), bitwiseAssembly.getLastNBits(x, n));
    }

foundry超便利ですね!

なお、実際に実装する際は、uint256 nではなしに、uint8 nを使うとか、別途処理をかますとかすると安心です。

※一方でケアすればするほど、ガス代が増えるので、悩ましいところですね!

BitFlag関係について

先ほどのリポジトリの中のsrc/bit/BitFlag.solには、bitフラッグとbitカウントについて記載しています。

test/bit/BitFlag.t.solが上記のテストになっています。

たとえば、N番目に1をセットしたい場合のコードはこちら

    function setNBitFlag(uint256 n) public {
        uint256 mask = 1 << n;
        uint256 clear_mask = ~mask;
        bitFlag = (bitFlag & clear_mask) | mask;
    }

こちらに対するテストが、こんな感じです。

    function testSetNBitFlag() public {
        uint256 n = 1;
        bitFlag.setNBitFlag(n);
        assertEq(bitFlag.checkNBitFlag(n), 1);

        // 33 = 100001
        bitFlag.setBitFlag(33);
        assertEq(bitFlag.checkNBitFlag(2), 0);
        assertEq(bitFlag.checkNBitFlag(3), 0);
        assertEq(bitFlag.checkNBitFlag(4), 0);

        n = 3;
        // 100001 --> 101001 = 41
        bitFlag.setNBitFlag(n);
        assertEq(bitFlag.checkNBitFlag(2), 0);
        assertEq(bitFlag.checkNBitFlag(3), 1);
        assertEq(bitFlag.checkNBitFlag(4), 0);
        assertEq(bitFlag.bitFlag(), 41);
    }

色々試せるようになっているので、試してなんとなく掴んでみてください。

BitフラッグをInline Assembly化

Bit演算と同じようにInline Assembly化しています。

そういえば、ガス効率は、foundryのガスレポートで一括出力できるので、ぜひチェックしてみてください。

--gas-report

全く変わらないものもあれば、大幅に変わるものもあります。

Inline Assemblyで内部関数の呼び出し

Inline Assemblyで内部関数の呼び出しを行っています。

    function getFirstNBitsCallAssembly(uint256 x, uint256 n) external returns (uint256 result) {
        address addr = address(this);
        bytes4 sign = bytes4(keccak256("getBitLength(uint256)"));

        assembly {
            let freePointer := mload(0x40)
            mstore(freePointer, sign)

            // Set arg
            mstore(add(freePointer, 0x04), x)

            // Attention GasLimits
            let success := call(100000, addr, 0, freePointer, 0x24, freePointer, 0x20)
            let len := mload(freePointer)

            result := shr(sub(len, n), x)
        }
    }

コードが見にくくなるので、inline assemblyの内部関数の呼び出しは、おまけ程度に記載しました。

ビット演算一つにとっても、いろんな書き方ができるなぁって思いながら、適当にコードまとめてみました。

-assembly