2017.01.06

Ethereum のスマートコントラクトにおけるサインチェックの方法について


こんにちは。次世代システム研究室のT.M. です。

はじめに

先日、Z.com Cloud ブロックチェーンおよびConoHa ブロックチェーンのβ版をリリース致しました。サービスの概要については、商材ページに記載されているので、そちらをご覧ください。この記事では、サービスを開発する上で発生したある問題点の解決方法を述べたいと思います。

発生した問題点および解決策

サービスを開発する上でたくさんの問題点が発生し、それらを解決してきました。ブロックチェーンサービス利用ガイドにそのすべてが記述されております。本記事では、その中でもエンドユーザ問題を解決する上で用いた技術的解説を行いたいと思います。その技術とは、コントラクトでのサインチェック法です。サインチェックの必要性はガイドに記述されております。

サインについて

その人が確かにデータを生成したことを保証するために、送信者はデータに秘密鍵でサインをし、受信者はサインからアドレスを復元することがサインチェックをします。ここでは、node.js でのサインの仕方およびSolidity でのサインチェックの仕方を説明します。

node.js でのサインについて

ethereumjs-util において、ecsign という関数が用意されておりますので、これを利用します。この関数は、データそのものではなく、ハッシュ値を秘密鍵でサインします。

Solidity におけるサインチェックについて

Solidity では、ecrecover という関数が用意されておりますので、これを利用します。この関数は、サイン前のハッシュ値およびサインからアドレスを復元することができます。ただし、サインはそのままではなく、r、s、v に分解する必要があります。r、s、v は65バイトのサインのうち、先頭から32バイト、32バイト、1バイトに分けたものです。Solidity では、バイトの操作はassembly を使う必要があります。コードは以下の通りです。
        assembly {
            r := mload(add(_sign, 32))
            s := mload(add(_sign, 64))
            v := byte(0, mload(add(_sign, 96)))
        }

切り分けた、v、r、sとサイン前のハッシュをパラメータとしてecrecover を用いることで、サインをした人のアドレスが判別することができます。これによりサインチェックすることができます。

ハッシュ関数について

サインを説明する際に述べたように、サインは生データではなく、ハッシュ値に行います。そのため、node.js およびSolidity において、同じハッシュ値が必要です。ただし、リクエストのパラメータとしてハッシュ値を送ってしまうと、送ったことを保証したいパラメータのハッシュ値であるかがわからないので、node.js およびSolidity で同じパラメータから同じハッシュ値を生成できる必要があります。ここでは、それぞれにおけるハッシュ生成について説明します。

node.js でのハッシュについて

node.js では、SHA3 のハッシュ関数として、keccakjsというライブラリがあります。今回はこれを利用します。利用方法は以下の通りです。
var SHA3 = require('keccakjs');
var h = new SHA3(256);
h.update(_param);
hash = h.digest('hex');

Solidity でのハッシュについて

Solidity では、sha3 という関数が用意されておりますので、これを利用します。利用方法は以下の通りです。
hash = sha3(_param)

ハッシュ値の不一致

上記方法でnode.js およびSolidity で同じパラメータのハッシュ値を取ったところ、異なる値になりました。その原因について説明します。

node.js でのハッシュは、パラメータを文字列としてする一方、Solidity では、バイナリとしてハッシュします。そのため、不一致が起こりました。そこで、Solidity でのハッシュ値に合うように、node.js でのハッシュの仕方を工夫しました。

node.js でのハッシュ方法について

Solidity には、様々な型があります。そのため、Solidity と同じハッシュ値を取るためには、Solidity 内で管理されているのと同様のバイナリをnode.js でも作成し、それにハッシュする必要があります。Solidity 内で管理されているのと同様のバイナリを作成するには、web3.js のライブラリとして、パラメータをエンコードするencodeParam という関数によりバイナリに変換し、それを型のサイズに応じて長さを変更します。また、バイナリを作成したところで、node.js では、文字列であるため、Buffer に変換し、ハッシュすることで、Solidity のハッシュと同様の結果になります。コードは以下の通りです。getParam は型に応じたバイナリに変換し、型の末尾の数値からサイズを取り出し、型のサイズだけのバイナリの文字列を取得します。converBuffer は、バイナリの文字列をBuffer に変換します。
    var h = new SHA3(256);
    h.update(convertBuffer(getParam(_type, _param);
    hash = h.digest('hex');

    var getParam = function(type, param) {
        let p = require('web3/lib/solidity/coder').encodeParam(type, param);
        let m = type.match(/(\[([0-9]*)\])$/);
        if (m) {
            let prefixLength = 0;
            if (m[0] === '[]') prefixLength = 32 * 2 * 2;
            return p.substr(prefixLength);
        }
        let size;
        if (type === 'bytes') {
            let size = param.length;
            return p.substr(128, size - 2);
        } else if (type === 'string') {
            // string はdynamic length のため
            return require('web3/lib/utils/utils').fromUtf8(param).substr(2);
        } else if (type.indexOf('bytes') !== -1) {
            // bytes はright padding のため
            size = type.match('[0-9]+') ? type.match('[0-9]+')[0] : 32;
            return p.substr(0, size * 2);
        } else if (type === 'address') {
            size = 20;
            return p.substr(size * 2 * -1, size * 2);
        } else if (type === 'bool') {
            size = 1;
            return p.substr(size * 2 * -1, size * 2);
        } else if (type.indexOf('uint') !== -1) {
            size = type.match('[0-9]+') ? type.match('[0-9]+')[0] / 8 : 32;
            return p.substr(size * 2 * -1, size * 2);
        } else if (type.indexOf('int') !== -1) {
            size = type.match('[0-9]+') ? type.match('[0-9]+')[0] / 8 : 32;
            return p.substr(size * 2 * -1, size * 2);
        }
    };

    var convertBuffer = function(param) {
        let buffer = new Buffer(param.length / 2);
        buffer.fill();
        param.match(new RegExp('.{2}', 'g')).map(function(b, index) {
            buffer[index] = parseInt(b, 16);
        });
        return buffer;
    };

最後に

node.js でサインしたデータを、Solidity でアドレスに復元することができました。この技術が他でどう活きるかは想像つきませんが、我々が開発したサービスにおいて、とても重要な技術のひとつです。これのおかげで、ユーザに信用されるサービスとすることができました。今回のサインやハッシュを含めサービスを利用するためのライブラリeth-client はGitHub にコードを公開し、またnpm のモジュールとしても公開しておりますので、ぜひご利用下さい。

次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。インフラ設計、構築経験者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。

皆さんのご応募をお待ちしています。