LoginSignup
5
2

More than 1 year has passed since last update.

実用的なコードを通して学ぶSolidity inline assembly入門

Last updated at Posted at 2022-12-24

メリークリスマス!
25日を担当します、よろしくお願いいたします。

本当は、メリークリスマスということで、Merkle(メリクリ) Patricia Treeについて、Gethのソースコードでも読みながら解説しようと思ったのですが、マークルパトリシア木の記事は結構あるので、やめました、いつか書きたい。

本記事は、以下のライブラリ(BigNumbers.sol)を参考にしながら、inline assemblyについて学ぼうという感じです。以降、こちらのライブラリをsolidity-BigNumbersと記します。

概要&この記事を書こうと思ったきっかけ

本記事ではSolidityのinline assemblyを、solidity-BigNumbersを通じて学びたいと思います。

スマートコントラクトで何らかの暗号技術を実装するとき、大きな数での足し算掛け算等が必要になることがあります。例えば、2048ビットで表現される素数は、uint256じゃ足りないわけです。

私は東京web3ハッカソンで、加法準同型暗号をスマコンで実装することになり、加算準同型暗号×スマートコントラクトについて調べていました(加法準同型暗号って?)。ググると、以下のような記事とか出てくるのですが、問題が1つあります。セキュリティビットが2048以上でない点です(リンク先いくとわかりますが、uint使っています。uintはuint256のエイリアスです)。

この問題を解決した実装をしたかったため、セキュリティビット2048以上でも動くように多倍長整数を扱う必要がありました。きっと世界の誰かが、solidityでBignumberを実装しているだろうとGitHubを調べると、solidity-BigNumbersライブラリが見つかり、ハッカソンではセキュリティビットが2048の加法準同型暗号(paillier暗号)を実装できました。このライブラリを理解するにあたり、inline assemblyも少しかじったので、まとめようと思います。

この記事のゴール

この記事のゴールは、以下のコードが読めるようになることです(コードのコメントで既に何となく読める)。

    function _init(
        bytes memory val, 
        bool neg, 
        uint bitlen
    ) private view returns(BigNumber memory r){ 
        // use identity at location 0x4 for cheap memcpy.
        // grab contents of val, load starting from memory end, update memory end pointer.
        assembly {
            let data := add(val, 0x20)
            let length := mload(val)
            let out
            let freemem := msize()
            switch eq(mod(length, 0x20), 0)                       // if(val.length % 32 == 0)
                case 1 {
                    out     := add(freemem, 0x20)                 // freememory location + length word
                    mstore(freemem, length)                       // set new length 
                }
                default { 
                    let offset  := sub(0x20, mod(length, 0x20))   // offset: 32 - (length % 32)
                    out     := add(add(freemem, offset), 0x20)    // freememory location + offset + length word
                    mstore(freemem, add(length, offset))          // set new length 
                }
            pop(staticcall(450, 0x4, data, length, out, length))  // copy into 'out' memory location
            mstore(0x40, add(freemem, add(mload(freemem), 0x20))) // update the free memory pointer
            
            // handle leading zero words. assume freemem is pointer to bytes value
            let bn_length := mload(freemem)
            for { } eq ( eq(bn_length, 0x20), 0) { } {            // for(; length!=32; length-=32)
             switch eq(mload(add(freemem, 0x20)),0)               // if(msword==0):
                    case 1 { freemem := add(freemem, 0x20) }      //     update length pointer
                    default { break }                             // else: loop termination. non-zero word found
                bn_length := sub(bn_length,0x20)                          
            } 
            mstore(freemem, bn_length)                             

            mstore(r, freemem)                                    // store new bytes value in r
            mstore(add(r, 0x20), neg)                             // store neg value in r
        }

        r.bitlen = bitlen == 0 ? bitLength(r.val) : bitlen;
    }

bytesであるvalを受け取り、BigNumber構造体を返します。solidityのbytesは可変サイズなので、大きい数(というか、任意長のbit列)を扱えるわけです。BigNumber構造体も、中身はただのbytesです(加えて、符号情報やビット長もある、以下参考)。このライブラリは、init関数によってBigNumber構造体を取得し、その後加算メソッド乗算メソッドを呼びます。詳細はいろいろ追ってみてください。

struct BigNumber { 
    bytes val;
    bool neg;
    uint bitlen;
}

(少し脱線)加法準同型暗号ってなに?

暗号化したまま足し算ができる暗号です。例えば、paillier暗号lifted-elgamal暗号があります。
solidity-BigNumbersを使うと、簡単にpaillier暗号を実装できます(以下が実際のコード、ref)。
加算だけでもいろんなことができます。例えば、電子投票とか、画像のエッジ検出とか(私の力不足もあり、一瞬でout of gasになりましたがw)。

    /**
    * @dev homomorphic addition by Solidity-BigNumbers since cipher text cannot be represented by uint256
    */
    function _homomorphicAddition(bytes memory a, bytes memory b, bytes memory mod) internal view returns(IBigNumbers.BigNumber memory res){
        BigNumber memory bn_cur;
        bn_cur = BigNumbers.init(a, false);
        BigNumber memory bn_add;
        bn_add = BigNumbers.init(b, false);
        BigNumber memory modulus;
        modulus = BigNumbers.init(mod, false);
        res = BigNumbers.modmul(bn_cur, bn_add, modulus);
        return res;
    }

inline assemblyを読み、学ぶ

では、上から読んでいきましょうか。

assembly { .... }

inline assemblyはassembly{}内に書くようですね(ref)

let data := add(val, 0x20)

letで変数を宣言します。add(a,b)でa+bします。valはbytesの先頭アドレスなので、先頭アドレスから1word(32byte, つまり0x20)足して進めます。なぜこんなことをしているかは次の行でわかります。

let length := mload(val)

mload(p)で、アドレスpから1word(32byte)分、値を読み出します。繰り返しですが、valはbytesで、val自体は先頭アドレスを指しています。bytesの先頭32バイトには、bytes valの長さが格納されています。つまり、以下のようなイメージです。mload(val)で、受け取ったbytesの長さを変数lengthにいれています。先ほどの、let data := add(val, 0x20)のdataは、bytes valの値のスタートを指しているわけですね。
image.png

let out

変数outを宣言。今更ですが、お尻にセミコロンは要りません。

let freemem := msize()

//todo 要修正
msize()でメモリのサイズが取得できます(ref)。

switch eq(mod(length, 0x20), 0)                       // if(val.length % 32 == 0)
    case 1 {
        out     := add(freemem, 0x20)                 // freememory location + length word
        mstore(freemem, length)                       // set new length 
    }
    default { 
        let offset  := sub(0x20, mod(length, 0x20))   // offset: 32 - (length % 32)
        out     := add(add(freemem, offset), 0x20)    // freememory location + offset + length word
        mstore(freemem, add(length, offset))          // set new length 
    }

見てのとおり、switch文使えます。eq(x,y)で、if x==yなら1, それ以外は0となります(ref)。
mod(x,y)はx%yです。つまり、bytes valの長さが0x20倍なら、eq()が1となるので、case 1に飛びます。コードのコメントにある通りですね。1word単位にきちっと収まっているなら(case 1)、outにfreememに1word足したところを格納します。使える個所を指させるわけですね。その後mstore(freemem, length)でfreememを移動させます。switchのdefaultの方は、0x20で割り切れず、はみ出しがある場合ですね。offsetを計算して、1word単位で計算できるようにそろえています。

pop(staticcall(450, 0x4, data, length, out, length))  // copy into 'out' memory location

staticcallで、アドレス0x4のコントラクトをcallします。call opcodeもありますが、staticと付いているので、こちらは状態を変更しないことを意味します(ref)。んで、0x4はなんやねんってなるわけですが、これはprecompiled contractのアドレスです。読んで字のごとく、事前にコンパイル済みのコントラクトです。precompiled contractにどのようなものがあるかは、これなどをチェックしてください。Gethのソースコードからももちろんわかります。んで、0x4はデータコピーを提供するprecompiled contractです。これを呼び出し、dataからoutへlength分データコピーします。

mstore(0x40, add(freemem, add(mload(freemem), 0x20))) // update the free memory pointer

コピー後、mstore()で、アドレス0x40のデータを更新します。0x40は、free memory pointerと呼ばれ、現在割り当てられているメモリの終端を指しています。以下、ここからの引用です。

In solidity, the 0x40 slot in memory is special: it contains the "free memory pointer" which points to the end of the currently allocated memory.

// handle leading zero words. assume freemem is pointer to bytes value
let bn_length := mload(freemem)
for { } eq ( eq(bn_length, 0x20), 0) { } {            // for(; length!=32; length-=32)
 switch eq(mload(add(freemem, 0x20)),0)               // if(msword==0):
        case 1 { freemem := add(freemem, 0x20) }      //     update length pointer
        default { break }                             // else: loop termination. non-zero word found
    bn_length := sub(bn_length,0x20)                          
} 

コメントにあるように、bytes valのleading zeroを除きます。まずmload(freemem)でbytes valのlengthを取得&格納。forループは初期化部分、条件、イテレーション後のパートを含むヘッダを持っている(ref)。上の書き方は、初期化部分とイテレーション後の部分は省略されており({})、条件だけなので、while(length!=32)と理解できます。先頭ビットがゼロの部分は飛ばしたいので、eq()で1word分ゼロかどうか判断し、先頭ゼロなら(case1)、lengthを更新していきます。

mstore(freemem, bn_length)                             
mstore(r, freemem)                                    // store new bytes value in r
mstore(add(r, 0x20), neg)                             // store neg value in r

最後に、bn_lengthとnegを保存しています。これでinline assemblyをマスターしました!きっと!

[init関数補足]
inline assemblyを抜けた後、bitlenを確定させ、BigNumber構造体 rを返します(ref)。

おわりに

いかがでしたでしょうか。25日になってしまったので、とりあえず公開します。そしてあとできっと更新します(こう表現すると、そのきっとは来ないと思われるようです)。

読んでいただきありがとうございました。メリークリスマス。良いお年を。

参考

  • Solidity Assembly

  • EVM opcode

  • 中島聡さんのツイート

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2