スマートコントラクトの紹介

シンプルなスマートコントラクト

まずは、変数の値を設定し、他のコントラクトがアクセスできるように公開する基本的な例から始めましょう。今はまだ全てを理解していなくても構いません、後でもっと詳しく説明します。

ストレージの例

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract SimpleStorage {
    uint storedData;

    function set(uint x) public {
        storedData = x;
    }

    function get() public view returns (uint) {
        return storedData;
    }
}

最初の行は、ソースコードがGPLバージョン3.0でライセンスされていることを示しています。機械的に読み取り可能なライセンス指定子は、ソースコードの公開がデフォルトとなっている環境では重要です。

次の行では、ソースコードがSolidityバージョン0.4.16からバージョン0.9.0の前までのバージョンで書かれたものであることを示しています(バージョン0.9.0は含まない)。 これは、コントラクトが新しい(破壊的変更があった)コンパイラのバージョンでコンパイルできないことを保証するためです。 Pragma は、ソースコードをどのように扱うかについての、コンパイラに対する指示で、一般的なものです(例: pragma once )。

Solidityでいうコントラクトとは、Ethereumブロックチェーン上の特定のアドレスに存在するコード(その functions )とデータ(その state )の集合です。 uint storedData; という行は、 uint (256ビットの unsigned integer)型の storedData という状態変数を宣言しています。 これは、データベースを管理するコードの関数を呼び出すことで問い合わせや変更ができる、データベースの1つのスロットと考えることができます。 この例では、コントラクトによって、変数の値を変更したり取得したりするのに使用できる関数 setget が定義されています。

現在のコントラクトのメンバ(状態変数など)にアクセスする場合、通常は this. という接頭辞を付けずに、その名前で直接アクセスします。 他のいくつかの言語とは異なり、これを省略することは単なるスタイルの問題ではなく、メンバへのアクセス方法が全く異なります。

このコントラクトは、(Ethereumが構築したインフラにより)世界中の誰もがアクセス可能な1つの番号を、あなたがその番号を公開するのを防ぐ(実現可能な)方法なしに、誰もが保存できることを除けば、まだあまり意味がありません。 誰もが set に別の値で再度callをし、あなたの番号を上書きできますが、その番号はブロックチェーンの履歴に保存されたままです。 後で、自分だけが番号を変更できるようにアクセス制限をかける方法を見てみましょう。

警告

Unicodeテキストを使用する際には、見た目が似ている(あるいは同じ)文字でもコードポイントが異なる場合があり、その場合は異なるバイト配列としてエンコードされるので注意が必要です。

注釈

すべての識別子(コントラクト名、関数名、変数名)は、ASCII文字に制限されています。文字列変数にUTF-8でエンコードされたデータを格納することは可能です。

サブ通貨の例

以下のコントラクトは、最も単純な形態の暗号通貨を実装したものです。 このコントラクトでは、作成者のみが新しいコインを作成できます(異なる発行スキームは可能です)。 誰もがユーザー名とパスワードを登録することなく、Ethereumのキーペアさえあればコインを送り合うことができます。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;

contract Coin {
    // キーワード「public」は、他のコントラクトから変数にアクセスできます
    address public minter;
    mapping (address => uint) public balances;

    // イベントは、宣言した特定のコントラクトの変更にクライアントが反応することを可能にします
    event Sent(address from, address to, uint amount);

    // コンストラクタのコードは、コントラクトが作成されるときにのみ実行されます
    constructor() {
        minter = msg.sender;
    }

    // 新しく作成されたコインの量をアドレスに送ります
    // コントラクトの作成者のみが呼び出すことができます
    function mint(address receiver, uint amount) public {
        require(msg.sender == minter);
        balances[receiver] += amount;
    }

    // エラーは操作に失敗した理由についての情報を提供できます
    // エラーは関数の呼び出し側に返されます
    error InsufficientBalance(uint requested, uint available);

    // 任意のコールしたアカウントのコインの量をアドレスに送ります
    function send(address receiver, uint amount) public {
        if (amount > balances[msg.sender])
            revert InsufficientBalance({
                requested: amount,
                available: balances[msg.sender]
            });

        balances[msg.sender] -= amount;
        balances[receiver] += amount;
        emit Sent(msg.sender, receiver, amount);
    }
}

今回のコントラクトでは、いくつかの新しい概念が導入されていますが、それらを一つずつ見ていきましょう。

address public minter; という行は、 address という型の状態変数を宣言しています。 address 型は160ビットの値で、算術演算を行うことができません。コントラクトのアドレスや、 external accounts に属するキーペアの公開鍵のハッシュを格納するのに適しています。

キーワード public を指定すると、コントラクトの外部から状態変数の現在の値にアクセスできる関数が自動的に生成されます。このキーワードがないと、他のコントラクトはその変数にアクセスする方法がありません。コンパイラが生成する関数のコードは以下のようになります(今のところ externalview は無視してください)。

function minter() external view returns (address) { return minter; }

上記のような関数を自分で追加することもできますが、関数と状態変数が同じ名前になってしまいます。 このようなことをする必要はありません。コンパイラが解決してくれます。

次の行の mapping (address => uint) public balances; もパブリックな状態変数を作成しますが、より複雑なデータ型です。mapping 型は、アドレスを unsigned integers にマッピングします。

マッピングは、可能なすべてのキーが最初から存在し、バイト表現がすべてゼロである値にマッピングされるように仮想的に初期化された ハッシュテーブル と見なすことができます。しかし、マッピングのすべてのキーのリストを得ることも、すべての値のリストを得ることもできません。マッピングに追加したものを記録するか、そのようなことが必要ない文脈で使用してください。あるいは、リストを保持するか、より適切なデータ型を使用することをお勧めします。

public キーワードで作成した getter function は、マッピングの場合はもっと複雑です。それは次のようなものです。

function balances(address _account) external view returns (uint) {
    return balances[_account];
}

この関数を使って、1つのアカウントの残高を照会できます。

event Sent(address from, address to, uint amount); という行は、 "event" を宣言しており、このイベントは関数 send の最終行で発せられます。WebアプリケーションなどのEthereumクライアントは、ブロックチェーン上で発せられるこれらのイベントを、それほどコストをかけずにリッスンできます。イベントが発せられると同時に、リスナーは引数の from, to, amount を受け取るため、トランザクションの追跡が可能になります。

このイベントをリッスンするには、次のJavaScriptコードを使用します。 web3.js を使って Coin のコントラクトオブジェクトを作成し、どのようなユーザーインターフェースであっても、上記で自動的に生成された balances 関数を呼び出します:

Coin.Sent().watch({}, '', function(error, result) {
    if (!error) {
        console.log("Coin transfer: " + result.args.amount +
            " coins were sent from " + result.args.from +
            " to " + result.args.to + ".");
        console.log("Balances now:\n" +
            "Sender: " + Coin.balances.call(result.args.from) +
            "Receiver: " + Coin.balances.call(result.args.to));
    }
})

constructor は、コントラクトの作成時に実行され、その後は呼び出すことができない特別な関数です。 この場合、コントラクトを作成した人のアドレスを永続的に保存します。 msg 変数は( txblock と一緒に) 特別なグローバル変数 であり、ブロックチェーンへのアクセスを可能にするプロパティを含んでいます。 msg.sender は常に、現在の(外部の)関数呼び出しが行われたアドレスです。

コントラクトを構成していて、ユーザーやコントラクトが呼び出すことのできる関数は、 mintsend です。

mint 関数は、新しく作成されたコインの量を別のアドレスに送ります。 require 関数の呼び出しでは、条件を定義し、満たされない場合はすべての変更を元に戻します。 この例では、 require(msg.sender == minter); により、コントラクトの作成者だけが mint を呼び出せるようになっています。 一般的には、作成者は好きなだけトークンをミントできますが、ある時点で「オーバーフロー」と呼ばれる現象が発生します。 デフォルトの Checked arithmetic のため、式 balances[receiver] += amount; がオーバーフローした場合、つまり、任意精度の算術演算で balances[receiver] + amountuint の最大値( 2**256 - 1 )よりも大きくなった場合には、トランザクションは元に戻ってしまうことに注意してください。 これは、関数 send の中の balances[receiver] += amount; という記述にも当てはまります。

Errors を使うと、条件や操作が失敗したときに呼び出し側に詳しい情報を提供できます。 エラーは revert statement と一緒に使用されます。 revert 文は require 関数と同様にすべての変更を無条件に中止、復帰させますが、エラーの名前や、呼び出し側(最終的にはフロントエンドアプリケーションやブロックエクスプローラ)に提供される追加データを提供することもできるので、失敗をより簡単にデバッグしたり、対応したりできます。

send 関数は、(すでにコインを持っている)誰でも、他の人にコインを送るために使えます。 送金者が送金するのに十分なコインを持っていない場合は、 if の条件が true と評価されます。 結果として、 revert は操作を失敗させ、送金者には InsufficientBalance というエラーの詳細を伝えます。

注釈

このコントラクトを使ってあるアドレスにコインを送っても、ブロックチェーンエクスプローラでそのアドレスを見ても何もわかりません。 なぜなら、コインを送ったという記録と変更された残高は、この特定のコインコントラクトのデータストレージにのみ保存されているからです。 イベントを使えば、新しいコインのトランザクションや残高を追跡する「ブロックチェーンエクスプローラ」を作ることができますが、コインの所有者のアドレスではなく、コインコントラクトのアドレスを調べる必要があります。

ブロックチェーンの基本

概念としてのブロックチェーンは、プログラマーにとってはそれほど難しいものではありません。 なぜなら、複雑な仕組み(マイニング、 ハッシュ楕円曲線暗号peer-to-peerネットワーク など)のほとんどは、プラットフォームに一定の機能や約束事を提供するために存在しているだけだからです。 これらの機能を当たり前のように受け入れれば、基盤となる技術について心配する必要はありません。AmazonのAWSを使うためには、内部でどのように機能しているかを知る必要があるでしょうか?

トランザクション

ブロックチェーンとは、グローバルに共有されたトランザクション用のデータベースです。つまり、ネットワークに参加するだけで、誰もがデータベースのエントリーを読むことができるのです。データベース内の何かを変更したい場合は、いわゆるトランザクションを作成し、他のすべての人に受け入れられなければなりません。トランザクションという言葉は、あなたが行いたい変更(2つの値を同時に変更したいと仮定)が、まったく行われないか、完全に適用されるかのどちらかであることを意味しています。さらに、あなたのトランザクションがデータベースに適用されている間は、他のトランザクションはそれを変更できません。

例として、ある電子通貨のすべての口座の残高を一覧にしたテーブルがあるとします。ある口座から別の口座への振り込みが要求された場合、データベースのトランザクションの性質上、ある口座から金額が差し引かれた場合、必ず別の口座に追加されます。何らかの理由で対象となる口座に金額を追加できない場合は、元の口座も変更されません。

さらに、トランザクションは常に送信者(作成者)によって暗号化されています。これにより、データベースの特定の変更に対するアクセスを簡単に保護できます。電子通貨の例では、簡単なチェックで、口座の鍵を持っている人だけがその口座からお金を送金できるようになっています。

ブロック

克服しなければならない大きな障害のひとつが、ビットコイン用語で「二重支出攻撃」と呼ばれるものです。ネットワーク上に2つのトランザクションが存在し、どちらもアカウントを空にしようとしていたらどうなるでしょうか?有効なトランザクションは1つだけで、通常は最初に受け入れられたトランザクションが有効です。問題は、peer-to-peerネットワークでは「最初」という言葉が客観的ではないことです。

これに対する抽象的な答えは、「気にする必要はない」というものです。世界的に認められたトランザクションの順序が選択され、対立を解決してくれます。トランザクションは「ブロック」と呼ばれるものにまとめられ、実行されて参加しているすべてのノードに分配されることになります。2つのトランザクションが互いに矛盾する場合、2番目になった方が拒否され、ブロックの一部にはなりません。

これらのブロックは、時間的に直線的な配列を形成しており、これが「ブロックチェーン」という言葉の由来となっています。 ブロックは一定の間隔でチェーンに追加され、イーサリアムの場合はおよそ17秒ごとに追加されます。

「オーダー・セレクション・メカニズム」(これを「マイニング」と呼びます)の一環として、ブロックが時々戻されることがありますが、それはチェーンの「端」に限ったことです。特定のブロックの上にブロックが追加されればされるほど、そのブロックが元に戻される可能性は低くなります。つまり、あなたのトランザクションが元に戻され、さらにはブロックチェーンから削除されることもあるかもしれませんが、待てば待つほど、その可能性は低くなります。

注釈

トランザクションが次のブロックや将来の特定のブロックに含まれることは保証されていません。 なぜなら、そのトランザクションがどのブロックに含まれるかを決めるのは、トランザクションの提出者ではなく、マイナーに任されているからです。 コントラクトの将来の呼び出しをスケジュールしたい場合は、 スマートコントラクトの自動化ツールやオラクルサービスを利用できます。

Ethereum Virtual Machine

概要

Ethereum Virtual Machine(EVM)は、Ethereumにおけるスマートコントラクトの実行環境です。EVMはサンドボックス化されているだけでなく、実際には完全に隔離されています。つまり、EVM内で実行されるコードは、ネットワーク、ファイルシステム、または他のプロセスにアクセスできません。スマートコントラクトは、他のスマートコントラクトへのアクセスも制限されています。

アカウント

Ethereumには、同じアドレス空間を共有する2種類のアカウントがあります。それは、公開鍵と秘密鍵のペア(つまり人間)によって管理される 外部アカウント と、アカウントと一緒に保存されているコードによって管理される コントラクトアカウント です。

外部アカウントのアドレスは公開鍵から決定されますが、コントラクトのアドレスはコントラクトが作成された時点で決定されます(作成者のアドレスとそのアドレスから送信されたトランザクションの数、いわゆる「nonce」から導き出されます)。

アカウントにコードが格納されているかどうかにかかわらず、EVMでは2つの型が同じように扱われます。

すべてのアカウントには、256ビットのワードと256ビットのワードをマッピングする永続的なキーバリューストアがあり、これを storage と呼びます。

さらに、すべてのアカウントはEther(正確には「Wei」で、 1 ether10**18 wei )で 残高 を持っており、Etherを含むトランザクションを送信することで変更できます。

トランザクション

トランザクションとは、あるアカウントから別のアカウント(同じアカウントの場合もあれば、空のアカウントの場合もある、以下参照)に送信されるメッセージです。このメッセージには、バイナリデータ(これを「ペイロード」と呼びます)とEtherが含まれます。

対象となるアカウントにコードが含まれている場合、そのコードが実行され、ペイロードが入力データとして提供されます。

対象となる口座が設定されていない(トランザクションに受取人がいない、または受取人が「null」に設定されている)場合、そのトランザクションは 新しいコントラクト を作成します。すでに述べたように、そのコントラクトのアドレスはゼロのアドレスではなく、送信者とその送信したトランザクション数から得られるアドレス(「nonce」)です。このようなコントラクト作成トランザクションのペイロードは、EVMバイトコードとみなされ、実行されます。この実行の出力データは、コントラクトのコードとして永続的に保存されます。つまり、コントラクトを作成するためには、コントラクトの実際のコードを送信するのではなく、実際には、実行されるとそのコードを返すコードを送信することになります。

注釈

コントラクトが作成されている間、そのコードはまだ空です。そのため、コンストラクタの実行が終了するまで、作成中のコントラクトにコールバックしてはいけません。

ガス

生成された各トランザクションには、一定量の gas が課されます。 その目的は、トランザクションを実行するために必要な作業量を制限すると同時に、その実行に対する対価を支払うことです。EVMがトランザクションを実行している間、ガスは特定のルールに従って徐々に減っていきます。

gas price は、トランザクションの作成者が設定する値で、作成者は送信側の口座から gas_price * gas を前払いする必要があります。実行後にガスが残っていた場合は、同様の方法で作成者に返金されます。

いずれかの時点でガスが使い切られると(つまりマイナスになると)、ガス切れの例外が発生し、現在のコールフレームで状態に加えられたすべての変更が元に戻ります。

ストレージ、メモリ、スタック

Ethereum Virtual Machineには、データを保存できる3つの領域「ストレージ」「メモリ」「スタック」があり、以下の段落で説明します。

各アカウントには storage と呼ばれるデータ領域があり、関数呼び出しやトランザクション間で永続的に使用されます。 storageは256ビットのワードを256ビットのワードにマッピングするkey-value storeです。 コントラクト内からストレージを列挙できず、読み込みには比較的コストがかかり、ストレージの初期化や変更にはさらにコストがかかります。 このコストのため、永続的なストレージに保存するものは、コントラクトが実行するために必要なものに限定するべきです。 派生する計算、キャッシング、アグリゲートなどのデータはコントラクトの外に保存します。コントラクトは、コントラクト以外のストレージに対して読み書きできません。

2つ目のデータ領域は memory と呼ばれ、コントラクトはメッセージを呼び出すたびにクリアされたばかりのインスタンスを取得します。メモリは線形で、バイトレベルでアドレスを指定できますが、読み出しは256ビットの幅に制限され、書き込みは8ビットまたは256ビットの幅に制限されます。メモリは、これまで手つかずだったメモリワード(ワード内の任意のオフセット)にアクセス(読み出しまたは書き込み)すると、ワード(256ビット)単位で拡張されます。拡張時には、ガスによるコストを支払わなければなりません。メモリは大きくなればなるほどコストが高くなります(二次関数的にスケールする)。

EVMはレジスタマシンではなく、スタックマシンなので、すべての計算は stack と呼ばれるデータ領域で行われます。スタックの最大サイズは1024要素で、256ビットのワードを含みます。スタックへのアクセスは次のように上端に制限されています。一番上の16個の要素の1つをスタックの一番上にコピーしたり、一番上の要素をその下の16個の要素の1つと入れ替えたりすることが可能です。それ以外の操作では、スタックから最上位の2要素(操作によっては1要素、またはそれ以上)を取り出し、その結果をスタックにプッシュします。もちろん、スタックの要素をストレージやメモリに移動させて、スタックに深くアクセスすることは可能ですが、最初にスタックの最上部を取り除かずに、スタックの深いところにある任意の要素にアクセスすることはできません。

命令セット

EVMの命令セットは、コンセンサスの問題を引き起こす可能性のある不正確な実装や矛盾した実装を避けるために、最小限に抑えられています。すべての命令は、基本的なデータ型である256ビットのワード、またはメモリのスライス(または他のバイトアレイ)で動作します。通常の算術演算、ビット演算、論理演算、比較演算が可能です。条件付きおよび無条件のジャンプが可能です。さらにコントラクトでは、番号やタイムスタンプなど、現在のブロックの関連プロパティにアクセスできます。

完全なリストについては、インラインアセンブリのドキュメントの一部である list of opcodes を参照してください。

メッセージコール

コントラクトは、メッセージコールによって、他のコントラクトを呼び出したり、コントラクト以外のアカウントにEtherを送金できます。メッセージコールは、ソース、ターゲット、データペイロード、Ether、ガス、およびリターンデータを持つという点で、トランザクションと似ています。実際、すべてのトランザクションは、トップレベルのメッセージコールで構成されており、そのメッセージコールがさらにメッセージコールを作成できます。

コントラクトは、その残りの gas のうち、どれだけを内部メッセージ呼び出しで送信し、どれだけを保持したいかを決定できます。内側の呼び出しでガス切れの例外(またはその他の例外)が発生した場合は、スタックに置かれたエラー値によって通知されます。この場合、呼び出しと一緒に送られたガスだけが使い切られます。Solidityでは、このような状況では、呼び出し側のコントラクトがデフォルトで手動例外を発生させ、例外がコールスタックを「バブルアップ」するようにしています。

すでに述べたように、呼び出されたコントラクト(呼び出し側と同じ場合もある)は、メモリのクリアされたばかりのインスタンスを受け取り、呼び出しペイロード( calldata と呼ばれる別の領域に提供される)にアクセスできます。実行終了後、呼び出し元のメモリ内で呼び出し元が事前に割り当てた場所に保存されるデータを返すことができます。このような呼び出しはすべて完全に同期しています。

呼び出しの深さは1024までに 制限 されます。 つまり、より複雑な操作を行う場合には、再帰的な呼び出しよりもループの方が望ましいということです。さらに、メッセージコールではガスの63/64だけを転送できるため、実際には1000よりも少し少ない深さの制限が発生します。

Delegatecall / Callcodeとライブラリ

メッセージコールには、 delegatecall という特別なバリエーションがあります。 これは、ターゲットアドレスのコードが呼び出し元のコントラクトのコンテキストで実行され、 msg.sendermsg.value の値が変更されないという点を除けば、メッセージコールと同じです。

これは、コントラクトが実行時に異なるアドレスからコードを動的にロードできることを意味します。ストレージ、現在のアドレス、バランスは依然として呼び出したコントラクトのものを参照しており、コードだけが呼び出されたアドレスから取得されます。

これにより、Solidityに「ライブラリ」機能を実装することが可能になりました。再利用可能なライブラリコードで、複雑なデータ構造を実装するためにコントラクトのストレージに適用することなどが可能です。

ログ

ブロックレベルまでマッピングされた特別なインデックス付きのデータ構造にデータを保存することが可能です。この logs と呼ばれる機能は、Solidityでは events を実装するために使用されています。コントラクトはログデータが作成された後はアクセスできませんが、ブロックチェーンの外部から効率的にアクセスできます。ログデータの一部は bloom filters に格納されているため、効率的かつ暗号的に安全な方法でこのデータを検索することが可能であり、ブロックチェーン全体をダウンロードしないネットワークピア(いわゆる「ライトクライアント」)でもこれらのログを見つけることができます。

Create

コントラクトは、特別なオペコードを使用して他のコントラクトを作成することもできます(つまり、トランザクションのように単純にゼロアドレスを呼び出すわけではありません)。これらの createコール と通常のメッセージコールとの唯一の違いは、ペイロードデータが実行され、その結果がコードとして保存され、呼び出し側/作成側がスタック上の新しいコントラクトのアドレスを受け取ることです。

DeactivateとSelf-destruct

ブロックチェーンからコードを削除する唯一の方法は、そのアドレスのコントラクトが selfdestruct オペレーションを実行することです。そのアドレスに保存されている残りのEtherは、指定されたターゲットに送られ、その後、ストレージとコードがステートから削除されます。理論的にはコントラクトを削除することは良いアイデアのように聞こえますが、削除されたコントラクトに誰かがEtherを送ると、そのEtherは永遠に失われてしまうため、潜在的には危険です。

警告

selfdestruct によってコントラクトが削除されたとしても、それはブロックチェーンの歴史の一部であり、おそらくほとんどのEthereumノードが保持しています。そのため、 selfdestruct を使うことは、ハードディスクからデータを削除することと同じではありません。

注釈

コントラクトのコードに selfdestruct の呼び出しが含まれていなくても、 delegatecallcallcode を使ってその操作を行うことができます。

コントラクトを無効にしたい場合は、代わりに、すべての関数を元に戻すような何らかの内部状態を変更することで 無効 にする必要があります。これにより、コントラクトはすぐにEtherを返してしまうため、使用できなくなります。

プリコンパイル済みコントラクト

コントラクトのアドレスの中には、特別なものがあります。 1 から 8 までのアドレスには「プリコンパイル済みコントラクト」が含まれており、他のコントラクトと同様に呼び出すことができますが、その動作(およびガス消費量)は、そのアドレスに格納されているEVMコードによって定義されるのではなく(コードが含まれていない)、EVMの実行環境自体に実装されています。

EVMと互換性のあるチェーンでは、異なるプリコンパイル済みコントラクトのセットを使用する可能性があります。また、将来的にEthereumのメインチェーンに新しいプリコンパイル済みコントラクトが追加される可能性もありますが、常に 1 から 0xffff (包括的)の範囲内であると考えるのが妥当でしょう。