Introduction to Smart Contracts

A Simple Smart Contract

まずは値をセットし、他のコントラクトから呼び出せるような基本的なコントラクトの例から始めましょう。今は全てを理解する必要はありません。後ほど細かく説明します。

Storage

pragma solidity >=0.4.0 <0.6.0;

contract SimpleStorage {
    uint storedData;

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

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

最初の行はSolidityバージョン0.4.0もしくはそれより新しいものであればこのソースコードが機能を損なわず動作するバージョンで書かれていることを示しています(0.6.0未満のバージョンまで)。これはこのコントラクトが新しい(互換性のない)バージョンのコンパイラでは異なる挙動をする恐れがあるため、コンパイルできないことを明らかにするためです。Pragmaはソースコードをどの様に取り扱うかコンパイラに指示するための一般的な命令です(例:pragma once)。

Solidity上でのコントラクトというのはコード(その function たち)とEthereumブロックチェーン上の特定のアドレスに存在するデータ(その 状態 )の集合です。 uint storedData; の行では uint (256bitの符号無し整数)型の storedData という状態変数を宣言しています。 データベース上で検索可能かつある機能を呼び出すことによって変更可能なデータの様なものだと思ってください。Ethereumの場合、コントラクトが常にそのデータを所有しています。今回の場合、 set 関数と get 関数は変数の値を修正もしくは取得する場合に使用することができます。 状態変数にアクセスするのに他の言語でよく使われる this. は必要ありません。 このコントラクトでは世界中誰でも数値を格納することが可能ということ以外には機能はありません。そしてあなたにはそれを防ぐ手段はありません。誰でも set を呼び出して好きな数字を上書きができます。しかし一度セットされた値はブロックチェーン上に残ります。あなただけが数値を変更できる様な制限を加える方法は後ほど説明します。

注釈

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

警告

似た様な見た目のUnicodeテキストを使う際には注意してください。異なったバイト配列としてエンコードされてしまいます。

Subcurrency Example

以下のコントラクトは最も単純な仮想通貨を扱います。コインを0から生成することは可能ですが、コントラクトの作成者だけが実行可能です(異なる発行スキームの実行は簡単です)。さらにユーザー名やパスワードの登録無しに誰でもお互いにコインを送ることができます。必要なのはEthereumのキーペアだけです。

pragma solidity ^0.5.0;
contract Coin {
    // "public" というキーワードは値を
    // 外部から読み込み可能にさせます。
    address public minter;
    mapping (address => uint) public balances;
    // イベントは軽量クライアントが変更に対する反応を
    // 効率的に行うことを可能にします。
    event Sent(address from, address to, uint amount);
    // これはコントラクトが作られた時にだけ動作する
    // コンストラクタです。
    constructor() public {
        minter = msg.sender;
    }
    function mint(address receiver, uint amount) public {
        require(msg.sender == minter);
        require(amount < 1e60);
        balances[receiver] += amount;
    }
    function send(address receiver, uint amount) public {
        require(amount <= balances[msg.sender], "Insufficient balance.");
        balances[msg.sender] -= amount;
        balances[receiver] += amount;
        emit Sent(msg.sender, receiver, amount);
    }
}

このコントラクトはいくつかの新しい機能が備わっていますので、一つずつ見ていきましょう。

address public minter; と書いてある行はパブリックにアクセス可能なアドレス型の変数を宣言しています。address 型は160ビットの算術演算不可の値です。これはコントラクトのアドレスか外部の人間が持っているキーペアを保存するのに適しています。public というキーワードは自動的にコントラクトの外側から現在の状態変数の中身にアクセスできる様にする機能を生成します(つまりgetterを生成します)。 このキーワードなしでは他のコントラクトからはこの変数にアクセスできません。 コンパイラで生成されたこの機能は下記のコードとほぼイコールです(今は externalview は無視してください):

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

もちろんファンクション名と状態変数が同じ名前のため、この様なファンクションを追加しても動きませんが、コンパイラがこの様に解釈するということを理解して頂けると幸いです。

次の行の mapping (address => uint) public balances; は同様にパブリックな状態変数を生成しますが、もう少し複雑なデータタイプです。 これはaddressに符号無しのinteger型を割り当てます。Mappingは hash table として扱うことができます。そしてそれは事実上初期化され、そのため全てのpossible keyは最初から存在し、バイト表現で0となる値に割り当てられます。しかしhash tableと全く同じではありません。mappingではキーや値のリストを取得することはできません。そのため、何をmappingに追加したか覚えておいてください(もしくはリストを保存するか他の高度なデータタイプを使ってください)。もしくはそんなことをしなくて済む様な場合において使用して下さい。

今回の場合 public で作られた getter function はもう少し複雑でおおまかには下記の様になります:

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

見ての通り、あるアカウントの残高をクエリするのにこのfunctionが利用できます。

event Sent(address from, address to, uint amount); の行は send ファンクションの最終行でemitされています、いわゆる"event"を宣言しています。ユーザーインターフェース(ともちろんサーバーサイドのアプリケーション)は多くのコストを支払わずにブロックチェーン上でemitされたそれらのイベントをリッスンすることができます。emitされるとすぐにlistenerは fromto そして amount を引数として受け取り、トランザクションをトラックするのに役立ちます。このイベントをリッスンするために下記のJavaScriptコードを使います(Coin はweb3.jsもしくは似た様なモジュールを用いて作られたコントラクトオブジェクトです。):

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));
    }
})

balances ファンクションがユーザーインターフェースから自動的にどの様に呼ばれるか確認してください。

コンストラクタはコントラクトが作成される時に1回だけ呼ばれる特別なファンクションで、その後コンストラクタを呼ぶことはできません。このコンストラクタではコントラクトを作った人のアドレスを永久的に保存しています。msgtxblock も同様に)は特別なグローバル変数で、ブロックチェーンにアクセスできるいくつかのプロパティを含んでいます。msg.sender は外部からファンクションが呼んだアカウントのアドレスを常に返します。

コントラクトの最後にあり、ユーザもしくはコントラクトによって呼び出される mintsend です。 もし mint がコントラクトを作ったアカウント以外の誰かに呼ばれても何も起きません。これは特別なファンクション require によって保証されています。これは引数がfalseだった場合に全ての変更を元に戻す機能を持っています。 2つ目の require は後にオーバーフローを起こす様な大量のコインがないことを保証しています。

一方で、send は誰にでも(コインを持っていれば)コインを誰かに送ることができます。送るのに十分なコインを持っていなかった場合、require はプロセスを中止し、適切なエラーメッセージの文字列を返します。

注釈

もしあなたがコインをどこかに送るためにこのコントラクトを使うのであれば、ブロックチェーンエクスプローラ上のアドレスを見ても何も詳細を見ることができません。これはあなたがコインを送り、残高が変わったという事実はこの特定のコインコントラクトのデータストレージにのみ保存されるためです。イベントを使うことで比較的簡単にトランザクションと残高ををトラックする"ブロックチェーンエクスプローラ"を作成することが可能ですが、コインオーナーではなく、コントラクト作成者のあなたがコインコントラクトを検査する必要があります。

Blockchain Basics

ブロックチェーンのコンセプトを理解することはプログラマーにとってさほど難しいことではありません。その理由はほとんどの複雑なこと(mining, hashing, elliptic-curve cryptography, peer-to-peer networks, etc.)はただプラットフォームに機能と約束を与えているだけだからです。これらの機能をそういうものとして受け入れれば、内部のテクノロジーについて心配する必要はありません。(AmazonのAWSを使うのに内部でどの様に動作しているか知る必要ありますか?)

Transactions

ブロックチェーンはグローバルにシェアされたトランザクションのデータベースです。 つまり誰でもネットワークに接続するだけでこのデータベース上の項目を読み込むことができます。もしデータベース上の何かを変えたいときはいわゆるトランザクションを発行し、他の全員の同意を得る必要があります。トランザクションという言葉はあなたがしたい変更が(例えばあなたが2つの値を同時に変えたいとすると)その両方ともが変わらないか、両方とも変更されることを意味しています。さらに、あなたのトランザクションがデータベースに登録されている最中に他のトランザクションはそのトランザクションを変更することはできません。

例として、ある電子通貨の残高リストのテーブルを想像してください。もしあるアカウントから別のアカウントへの送金がリクエストされた際に、データベースのトランザクションの基本として、もしあるアカウントの残高から送金分が引かれたら、別のアカウントの残高には送金分が常に追加されなければいけません。何かの理由でその別のアカウント残高に送金分が追加されないのであれば、送金元のアカウントの残高も元のままでなければいけません。

更にトランザクションは常に送信者(作成者)によって暗号学的に署名されます。これによりデータベースのある種の改ざんを防ぐことができます。電子通貨の例で言えば、単純なチェックでキーを持っている人だけがお金を送ることができます。

Blocks

解決しなければならない大きな問題の一つとして(Bitcoinの用語で)"二重支払い攻撃"があります。もしあるアカウントを空にする様な2つのトランザクションが同時に存在していたらどうなるでしょうか。基本的には最初に承認された最初のトランザクションのみが有効です。しかし問題は"最初の"というのはpeer-to-peerネットワークにおいて客観的ではないのです。

理論的にはこの問題は気にする必要がありません。グローバルに承認された順番のトランザクションが選ばれ、このコンフリクトが解消します。いくつかのトランザクションはブロックと言われるもので一まとめにされ、全ての参加しているノードの間で処理されます。 もし2つの矛盾したトランザクションがあった場合には、2つ目のトランザクションはリジェクトされブロックの一部として組み込まれることはありません。

これらのブロックは一つのシーケンスを作るためブロックチェーンという名前がつけられました。ブロックは定期的に追加され、Ethereumでは約17秒ごとに1つ追加されます。

順序選択メカニズム(マイニング)では、ブロックが取り消されることもあります。しかしこれはチェーンの先端でだけで起こり、ブロックが追加されるごとに取り消される可能性が減ります。そのため、あなたのトランザクションは取り消されるもしくは削除される可能性もありますが、長く待てば待つほどその可能性は低くなります。

注釈

トランザクションは次のブロックやある特定の未来のブロックに組み込まれる保証はありません。これはトランザクションを送った人にではなく、マイナーにどのトランザクションをブロックに組み込むかの権限があるためです。

もしあなたのコントラクトである未来の時間でコールしたい場合には alarm clock もしくは似た様なoracleのサービスが使用可能です。

The Ethereum Virtual Machine

Overview

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

Accounts

Ethereumには2種類のアカウントがあります。両方とも同じアドレスを共有しています。外部アカウント は公開・秘密鍵のペアで管理されており、コントラクトアカウント はアカウントと一緒に保存されたコードによってコントロールされています。

外部アカウントのアドレスは公開鍵から決まる一方で、コントラクトのアドレスはコントラクトが作られた時に決まります。(コントラクトの作成者のアドレスと送られたトランザクションの数いわゆる"nonce"によって決まります。)

アカウントがコードを保存するかどうかに関わらず、EVMはこの2つのタイプを同様に扱います。

全てのアカウントは storage という256ビットのワードにmappingされた256ビットのkey-valueを持っています。

さらに、全てのアカウントは balance をEther("Wei"でいうと 1 ether10**18 wei です)で持っており、Etherを含んだトランザクションを送ることでこの値は変化します。

Transactions

トランザクションはあるアカウントから別のアカウント(これは同じアカウントもしくは空のアカウントの場合もある。下記をご参照ください)へのメッセージです。これはバイナリーデータ("payload"と呼ばれます)とEtherを含んでいます。

送信先のアカウントがコードを含んでいた場合、そのコードは実行され、payloadはインプットデータとして提供されます。

もし送信先のアカウントがセットされていなかったら(トランザクションが受信者情報を持っていないか、受信者が null だった場合には)、トランザクションは 新しいコントラクト を生成します。先にも言及した通り、コントラクトのアドレスはゼロアドレスではなく送信者やトランザクションの数(nonce)によって決まります。 この様なコントラクト作成のトランザクションのpayloadはEVM bytecodeに変換され、実行されます。この実行のアウトプットデータはコントラクトのコードとして永久的に保存されます。 これが意味するのはコントラクトを生成するために実際のコントラクトのコードを送るのではなく、コードが実行された時にそのコードを返すコードを送っています。

注釈

コントラクトが作られている間、そのコードはまだ空です。そのため、コンストラクタの実行が終了するまで、作成中のこのコントラクトを呼ぶべきではありません。

Gas

トランザクションの生成にあたり、各トランザクションはある量の gas を要求します。この目的は必要な処理の量を制限し、この処理に対しての報酬を同時に行うためです。EVMがトランザクションを実行している間、gasはあるルールに則り、徐々に減っていきます。

gas price とはトランザクションの作成者によってセットされる値であり、この作成者は gas_price * gas を送信するアカウントから支払う必要があります。もしトランザクションの実行後にgasが残っていたら、作成者に返金されます。

もしgasはある値より多く使われたら(負の値になりえます)、gas不足の例外が投げられ、現在の呼び出されたフレーム内での変更は全て取り消されます。

Storage, Memory and the Stack

Ethereum Virtual Machineはデータを保存できる場所が3つあります。それはstorage、memory、stackです。以下で説明していきます。

各アカウントは storage と呼ばれるデータエリアを持っており、functionの呼び出しからトランザクションまで残ります。 Storageは256bitのワードを256bitのワードにマッピングしているkey-value storeです。 コントラクト内ではstorageを列挙することはできません。また、storageの読み込みは比較的高価ですし、変更はさらに高価です。コントラクト外からstorageを読み書きすることはできません。

2つ目のデータエリアは memory と呼ばれ、コントラクトは各メッセージの呼び出しに対してクリアされたインスタンスを取得します。memoryはバイトレベルのリニアアドレスですが、読み取りは256bitに制限され、書き込みは8bitもしくは256bitに制限されます。過去に変更がないmemoryの単語にアクセスした際にmemoryは256-bitの単語に拡張されます(例えば単語のオフセット)。拡張の際にはgasは支払われます。memoryは成長すればするほど高くなります。(二次関数的に大きくなります。)

EVMは登録機械ではなくstack machineです。そのため全ての計算は stack と呼ばれるデータエリアで行われます。最大1024要素であり、256-bitの単語を含みます。stackへのアクセスは下記のようにトップエンドに制限されます。 トップの16要素の内の1要素を一番トップの要素にコピーするか、一番トップの要素をその下の16要素の内の一つと交換することができます。他のオペレーションはstackからトップ二つの要素(オペレーションによるが一つか二つ以上の場合もある)を取り出し、stackに追加します。 もちろん、stackの深い要素にアクセスするために、stackの要素をstorageやmemoryに移動するのは可能です。 しかし、stackのトップを最初に削除しないでstackの深いところにある任意の要素にアクセスすることはできません。

Instruction Set

EVMのインストラクションは間違った、もしくは矛盾したコンセンサス問題を起こしうる実行を避けるために最小限に保たれています。 全てのインストラクションは基本的なデータタイプ、256-bitのワードもしくはmemory(もしくは他のバイト配列)の元で成り立っています。

基本的な算術、ビット、論理、比較計算は使うことができます。条件付き分岐も可能です。更にコントラクトはブロック番号やタイムスタンプの様なプロパティにアクセスできます。

完全な表は list of opcodes をインラインアセンブリのドキュメントの一部として参照下さい。

Message Calls

コントラクトは他のコントラクトを呼び出したり、message callを使ってEtherをコントラクトアカウントではないアカウントに送ることができます。message callはソース、送信先、データpayload、Ether、gas、返り値がある点でトランザクションに似ています。実際に全てのトランザクションは次のメッセージコールを作るトップレベルメッセージコールで構成されています。

コントラクトは残っている gas をどれだけ送るか、そしてどのくらい残すかを内部のmessage callで決めることができます。内部呼び出しでgas不足の例外(もしくは他の例外)が発生したら、スタックに追加されることによりエラーが伝えられます。この場合、呼び出しと一緒に送られたgasのみが使用されます。その様な状況においてSolidityではデフォルトでコントラクトの呼び出しは手動の例外を起こし、例外は呼び出しのスタックから呼び出されます。

既に議論した様に、呼び出されたコントラクト(呼び出し元と同じになる場合もあります)はクリアされたmemoryのインスタンスを受け取り、calldata と呼ばれる別のエリアにあるコールpayloadにアクセスできます。 このコントラクト実行後に、このコントラクトは呼び出し元が事前に割り振ったmemoryの場所に保存されていたデータを返します。 これら全ての呼び出しは同時に起きます。

呼び出しは1024の深さに 制限 されます。これが意味するのはもっと複雑な運用においてループ処理は再帰的な呼び出しより好まれるということです。更に、63/64番目のgasだけはmessage callの中に送られるため、実際には深さは1000より少し小さくなります。

Delegatecall / Callcode and Libraries

delegatecall と呼ばれる特別なメッセージコールの変異型があります。これはメッセージコールと同じですが、送信先のアドレスのコードが呼び出し元のコントラクトのコンテキストで実行されるということと msg.sendermsg.value はその値を変えません。

つまりコントラクトは動的に違うアドレスからコードをロードできるということです。Storage、つまり現在のアドレスとバランスはまだ呼び出し元のコントラクトを参照していますが、コードだけは呼び出されたアドレスから取得されています。

これはSolidityにおいて"ライブラリ"機能を実装可能としています。例えば複雑なデータ構造を実行するために、再利用可能なライブラリのコードをコントラクトのstorageに保存できます。

Logs

特別にインデックスされ、ブロックレベルで全てマッピングされたデータ構造の中にデータを保存することができます。この logs と呼ばれる機能は events を実行するためにSolidityによって使用されています。コントラクトは作成後はログデータにアクセスできませんがブロックチェーンの外側から効率的にアクセスできます。いくつかのログデータは bloom filters に保存されるため、このデータは効率的かつ暗号学的に安全な方法で検索できます。そのためブロックチェーン全てをダウンロードしていないネットワーク上のpeer(いわゆる"light clients")でもこれらのログを見つけることがきます。

Create

コントラクトは特別なopcodeを使って他のコントラクトを作ることもできます(コントラクトはトランザクションがする様に単純にゼロアドレスをコールしません)。これら create calls と通常のmessage callの唯一の違いはpayloadデータが実行され、結果がコードとして保存され、呼び出し元と作成者がスタック上にある新しいコントラクトのアドレスを受け取ります。

Deactivate and Self-destruct

ブロックチェーン からコードを削除する唯一の手段はコントラクトが selfdestruct を実行する時のみです。そのアドレスに残っているEtherが設定されていた送信先に送られた時にstorageとコードは削除されます。コントラクトの削除は理論上は良いアイデアの様に聞こえますが、潜在的に危険をはらんでいます。誰かが削除されたコントラクトにEtherを送り、そのEtherは永遠に失われる様なことが起こり得ます。

注釈

もしコントラクトのコードが selfdestruct を含んでいなかったとしても、delegatecall もしくは callcode を使うことで実行可能です。

もしコントラクトを無効化したいのであれば、代わりに全てのfunctionを元に戻させる内部の状態(機能)を変更することでコントラクトを無効化すべきです。これによりコントラクトがEtherを返すとすぐにそのコントラクトを使えなくします。

警告

もしコントラクトを"selfdestruct"で削除したとしても、ブロックチェーン上の履歴には残りますし、きっとほぼ全てのノードにより保持されます。つまり"selfdestruct"はハードディスクからデータを消すのとは異なるということです。