Ethereumでガス消費の節約方法8選

Yuya Sugano
14 min readJan 15, 2019

--

Dapps のプロジェクト(Shape)で開発しているイケメン『Lucus Aschenbach』の投稿記事の日本語版。Ethereumのスマートコントラクトでガス消費を効率よく処理するための8選です。転載は本人快諾で心もイケメンですね^^。

Traditional Gas Station ^^

8選のご紹介です。Ethereum上では細かいコーディングの工夫がガス消費となってダイレクトにユーザコストへ影響します。[1]

1、データタイプの選択

2、バイトコードに変数を保存

3、変数をスロットに詰める

4、アセンブラで変数をスロットに詰める

5、関数のパラメータを連結させる

6、マークルプルーフによる効率化

7、ステートレスコントラクト

8、データをIPFSに保存する

1.データタイプの選択

256ビットのサイズ単位で変数を使用する(uint256もしくはbytes32)こと。EVM(Ethereum Virtual Machine)において各ストレージスロットのサイズは256ビットとなっているため、uint8の変数を単一で保存する場合にEVMはスロットの256ビットの内残りのビットを0で詰めてしまいます。EVMのスロットを隙間なく埋めるようにコーディングすることが望ましい形で、256ビット未満の変数を使用する場合は変数の順番を工夫したりアセンブリを行うことで、変数がスロットに収まるように処理すると綺麗です。アセンブラを使用するコーディングのやり方は『4.アセンブラで変数をスロットに詰める』でご紹介します。

2.バイトコードに変数を保存

静的な値を持つ変数についてはstorageではなくバイトコードでコントラクトに保持すると値の保存および読み込みにかかるコストは劇的に安くなります。この場合には変数の値を後から書き換えできないので注意が必要です。以下、2通りの実装方法です。

  1. 変数宣言の際にconstantを使用する
  2. 必要な値をハードコードする
uint256 public v1;
uint256 public constant v2;
function calculate() returns (uint256 result) {
return v1 * v2 * 10000;
}

v2および10000がここではコントラクトのバイトコードの一部となり、v1はコントラクトのstorage変数になっています。v1の読み込みにはSLOADオペを使用しますがSLOADオペだけで 200 ガスの消費に相当します。

3.変数をスロットに詰める

storage変数をブロックチェーン上に記録する際にSSTOREオペが使用されますが、これが最もガス消費の高いオペレーションで 20,000 ガス消費します。したがってできる限りこのSSTOREを使用しなくて良いように使用回数や変数の順番を工夫する必要があります。例えば以下のように、構造体の宣言において変数の順番を変更することで、実行されるSSTOREの回数を減らすことができます。

struct Data {
uint64 a;
uint64 b;
uint128 c;
uint256 d;
}
Data public data;constructor(uint64 _a, uint64 _b, uint128 _c, uint256 _d) public {
Data.a = _a;
Data.b = _b;
Data.c = _c;
Data.d = _d;
}

構造体の宣言の中で64ビットの変数aおよびb、128ビットの変数cと順番に並べています。このとき構造体Dataの配列変数dataに要素を追加するときSSTOREは2回の呼び出しで処理できることになります。uint64のaおよびbとuint128のcで1回、またuint256の変数dで1回です。256ビットのスロットを詰めて変数を配置できるようにすることで無駄がなくなりSSTORE呼び出しの効率がよくなります。なお構造体外でもこの動作は同様です。

4.アセンブラで変数をスロットに詰める

アセンブリで変数をスタッキングすることで256ビットのスロットに変数を押し込めることが可能です。結果上記と同様にSSOREオペの実行回数を減らすことができ、ガス消費は減少します。以下はuint64の変数4つを256ビットのスロット1つへマージして保持する例。

エンコーディング: 4つのuint64型の変数を1つのスロットへ

function encode(uint64 _a, uint64 _b, uint64 _c, uint64 _d) internal pure returns (bytes32 x) {
assembly {
let y := 0
mstore(0x20, _d)
mstore(0x18, _c)
mstore(0x10, _b)
mstore(0x8, _a)
x := mload(0x20)
}
}

変数を取り出すためには以下のようなデコード処理が必要です。

デコーディング: 1つのbytes32を4つのuint64型に読み込み戻す

function decode(bytes32 x) internal pure returns (uint64 a, uint64 b, uint64 c, uint64 d) {
assembly {
d := x
mstore(0x18, x)
a := mload(0)
mstore(0x10, x)
b := mload(0)
mstore(0x8, x)
c := mload(0)
}
}

アセンブラによる処理によってガス消費を劇的に効率化できる可能性がでてきます。理由として:

  1. アセンブラで柔軟で自由なビット処理やビットパディングが可能になる。例えば、256ビットの内最後の1ビットが不要であった場合、その1ビットを変数として使用するなどスロットを効率的に最適化して使用することが考えられるため。
  2. 各変数が単一のスロットに保存されているときそれぞれを個別でなく同時に読み込むことが可能。256ビット内の各変数の読み込みが1度のオペレーションで可能となるため。

ただしSolidityのコードの中で不必要に大量のアセンブリコードを呼ぶと開発コスト自体が高くなるうえに、コードの可読性やメンテナビリティが大幅に低下します。

5.関数のパラメータを連結させる

変数の保持および読み込みで使用したエンコード・デコードのような方法を関数のパラメータ呼び出し時にも適用することができます。以下のようにコーディングすることで、CALL DATA LOADのオペ回数が減少します。

pragma solidity ^0.4.21;contract bitCompaction {
function oldExam(uint64 a, uint64 b, uint64 c, uint64 d) public {
}
function newExam(uint256 packed) public {
}
}

この手法はBit-compactionとしてこちらの記事の中で紹介されています。[2]

6.マークルプルーフによる効率化

『マークルプルーフは1つのチャンクされたデータでより大きな量のデータの正当性を証明できること』と説明されてます。マークルルートおよびマークルプルーフの基本的なコンセプトについてはそれぞれ以下の記事を参照。マークルプルーフによってデータセットの全要素を明らかにすることなくデータ検証ができるためガス消費の節約に繋がります。[3][4]

いま車を購入するスマートコントラクトがあり、車の注文に32個の変数(例えば色など)が必要であるとします。構造体で各32個のパラメータを定義し、そこから下図のようなマークルルートを考えてみます。

  1. 32個のパラメータにたいして4つのグループ、各グループに8個づつの変数が入っている(計32個)
  2. 4つのグループそれぞれからハッシュを生成し、また4つのグループをさらにグルーピングして2つの上位グループのハッシュを生成する
  3. 上位グループの2つのハッシュをグルーピングおよびハッシュ計算を行い最上位のマークルルートとする(hash1234)
Merkle-Tree for Car-Example

下位ブランチの各要素を個別に全て検証する必要はなく、ハッシュを利用した検証プロセスで処理可能となります。例えば以下のピンク色の要素。

Merkle-Proof for the pink Element

マークルルートを保持し、ハッシュをkeccack256関数で計算します。上図のピンクの要素が例として車体の色の変数だとして、以下のハッシュ計算がマークルルートと一致するかどうか確認することで注文の車体の色が正しいかどうか検証可能です。

bytes32 public merkleRoot; // マークルルート// a,...,h はオレンジのブロックの要素function check
(
bytes32 hash4,
bytes32 hash12,
uint256 a,
uint32 b,
bytes32 c,
string d,
string e,
bool f,
uint256 g,
uint256 h
)
public view returns (bool success)
{
bytes32 hash3 = keccak256(abi.encodePacked(a, b, c, d, e, f, g, h));
bytes32 hash34 = keccak256(abi.encodePacked(hash3, hash4));
require(keccak256(abi.encodePacked(hash12, hash34)) == merkleRoot, "Wrong Element");

return true;
}

注意点としてトランザクションに対するスタック領域の超過を考慮して、各ブランチが肥大化しないように注意する必要があります。

7.ステートレスコントラクト

ステートレスコントラクトはトランザクションデータやイベントコールをコントラクトに宣言する変数へ保存するのではなく、ブロックチェーン上のトランザクションから再現します。SSTOREが高コストであるのに対してスレートレスコントラクトでは非常に少量のガス消費で処理ができるようになります。

以下の例でステートフル、ステートレスの実装パターンを検証してみます。以下はキーバリューストアのようなstoreと値を保存できるsave関数、また値の保存時にエミットされるイベントコールを定義しただけのシンプルなコントラクトです。

contract DataStore {
mapping(address => mapping(bytes32 => string)) public store;

event Save(address indexed from, bytes32 indexed key, string value);

function save(bytes32 key, string value) {
store[msg.sender][key] = value;
Save(msg.sender, key, value);
}
}

このコントラクトで以下のような値を保存します。

キー: “ethereum”
バリュー: “Ethereum is a decentralized platform that runs smart contracts: applications that run exactly as programmed without any possibility of downtime, censorship, fraud or third party interference.”

このトランザクションのコストは 181,181 ガス(IPFS使用時は 89,797 ガス)です。これに対して以下のようなステートレスコントラクトを考えます。このコントラクトではstorage変数を定義していません、bytes32型のkeyとstring型のvalueが同様に引数となっていますが値の保存やイベントのエミットもありません。

contract DataStore {
function save(bytes32 key, string value) {}
}

このときトランザクションのコストは 35,721 ガス(IPFS使用時は 25,841 ガス)となり、約80%のガス削減となります。データへのアクセスはトランザクションの中のInput Dataを参照することで行います。

実装方法のサンプルは以下の記事をご参照ください。[5]

このアプローチではガス消費をマークルプルーフより抑えることができますが、コントラクトの中からデータにアクセスすることはできなくなり、常に外部からトランザクションを通して参照するよう設定する必要があります。

8.データをIPFSに保存する

IPFSはオフチェーンの分散型ファイルストレージで、URLではなくコンテンツのハッシュを通してアクセスします。またIPFSではIPNSというネーミングサービスを利用することでパーマネントリンクが作成可能になっているため、パーマネントリンクをコントラクト内部で使用することで、常に同じファイルヘッダを参照することができます。

ステートレスコントラクトの例と同様にコントラクト内部に変数やデータを保持することはできませんが、ビデオなどの大きなデータの保持および参照には有効な方法であるといえます(Swarmという分散型ファイルストレージも有名です)。以下参考サイト。[6]

OPECODEに必要となるガス消費が気になる方はEthereum Yellowpaper上に記載されています。

まとめ

  • Ethereumのスマートコントラクトでは様々なテクニックでガス消費を抑えることができる
  • コントラクト内部に保存する変数に対してできること、また使用するデータを外部に保持することでガス消費を減少させる方法が複数ある
  • ビデオや画像など大容量のデータについては分散型ファイルストレージであるIPFSやSwarmを使用すると良い

--

--

Yuya Sugano
Yuya Sugano

Written by Yuya Sugano

Cloud Architect and Blockchain Enthusiast, techflare.blog, Vinyl DJ, Backpacker. ブロックチェーン・クラウド(AWS/Azure)関連の記事をパブリッシュ。バックパッカーとしてユーラシア大陸を陸路横断するなど旅が趣味。

No responses yet