覚えたら書く

IT関係のデベロッパとして日々覚えたことを書き残したいです。twitter: @yyoshikaw

フィボナッチ数を扱う - 再帰呼出し

再帰についてのメモがてらフィボナッチ数(フィボナッチ数列)をJavaでサンプル的に扱ってみました。


フィボナッチ数 (フィボナッチ数列)

\( n \)番目のフィボナッチ数を \( F_{n} \) で表すと、


\begin{align}
F_{0} = 0
\end{align}


\begin{align}
F_{1} = 1
\end{align}


\begin{equation}
F_{n+2} = F_{n+1} + F_{n}  \, \, \,(n\geqq2)
\end{equation}

と定義される漸化式です。

この数列はフィボナッチ数列(Fibonacci sequence)と呼ばれています。

数列は、 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584 …と続きます。

最初の二項は 0, 1 で、以後はその直前の2つの項の和となっています。


単純に再帰で表現

フィボナッチ数を求める処理を、上記の定義に従ってJavaプログラムで表現すると、
以下のようなfibメソッドのような再帰呼び出しを利用する形になります。

public class Fibonacci1 {

    private static int fib(int num) {
        if (num == 0) {
            return 0;
        }
        if (num == 1) {
            return 1;
        }
        return fib(num - 1) + fib(num - 2);
    }

}


実行してみましょう(n=10 に対するフィボナッチ数を求めています。実行時間も確認しています。

public class Fibonacci1 {

    public static void main(String[] args) {
        long start = System.currentTimeMillis();

        int n = 10;
        int sum = fib(n);

        long end = System.currentTimeMillis();
        System.out.printf("Fibonacci1 fib: (%d) -> %d \ntime: %d(ms)\n", n, sum, (end - start));
    }

    private static int fib(int num) {
        if (num == 0) {
            return 0;
        }
        if (num == 1) {
            return 1;
        }
        return fib(num - 1) + fib(num - 2);
    }

}

実行結果

Fibonacci1 fib: (10) -> 55 
time: 0(ms)

結果もあってるようです。


計算が終わらない

同じfibメソッドの引数を n = 45 (上記コード内のnの値を45)にして実行してみました。

実行結果

Fibonacci1 fib: (45) -> 1134903170 
time: 5145(ms)

結果は出力されましたが、n = 10 の時に比べるとかなり時間がかかるようになってきました。


n = 50 (上記コード内のnの値を50に)して実行してみました。

うちのマシンでは処理が終わりませんでした・・・・
計算量が増えすぎてしまって重くなってしまったようです。


メモ化

再起処理の中で同じ n に対する計算を毎回実行してしまっているので計算量が増大してしまっているようです。
一度計算し終わった n に対する計算結果はMapにキャッシュするようにします(いわゆるメモ化)。

こういう時に便利なのが、Map#computeIfAbsent ですね。
対象のkeyに対する値が存在しない場合に、引数の処理を実行してMapに値を格納します。(戻り値は格納した値です)

import java.util.HashMap;
import java.util.Map;

public class Fibonacci2 {

    // n に対するフィボナッチ数Fnを格納するためのマップ
    // n = 0 と n = 1 に対する結果は定義に従って最初から格納
    private static final Map<Integer, Long> FIB_MAP = new HashMap<>();
    static {
        FIB_MAP.put(0, 0L);
        FIB_MAP.put(1, 1L);
    }

    private static long fib(int num) {
        // まだ値がMapに入っていなければ計算を行う
        return FIB_MAP.computeIfAbsent(num, i -> fib(num - 1) + fib(num - 2));
    }

}


では、改めて n = 50 で実行してみます。

import java.util.HashMap;
import java.util.Map;

public class Fibonacci2 {

    // n に対するフィボナッチ数Fnを格納するためのマップ
    private static final Map<Integer, Long> FIB_MAP = new HashMap<>();
    static {
        FIB_MAP.put(0, 0L);
        FIB_MAP.put(1, 1L);
    }

    public static void main(String[] args) {
        long start = System.currentTimeMillis();

        int n = 50;
        long sum = fib(n);

        long end = System.currentTimeMillis();
        System.out.printf("Fibonacci2 fib: (%d) -> %d \ntime: %d(ms)\n", n, sum, (end - start));

    }

    private static long fib(int num) {
        return FIB_MAP.computeIfAbsent(num, i -> fib(num - 1) + fib(num - 2));
    }

}

実行結果

Fibonacci2 fib: (50) -> 12586269025 
time: 55(ms)

処理が終わるまでの時間も問題なし、結果もあっています。


桁があふれた

処理時間に問題も無しなので、n = 105 (上記コード内のnの値を105)にして実行してみました。

Fibonacci2 fib: (105) -> -742723093263328478 
time: 59(ms)

あれ?結果が負数になってる・・。longの範囲では既に桁あふれしてるようです。

桁あふれ の件は、フィボナッチ数とか再帰と直接的な関係性はないとは思いますが、
まともな結果を扱えないのでは問題ありなので、扱う方をBigIntegerに変更しました。

import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map;

public class Fibonacci3 {

    private static final BigInteger ZERO = BigInteger.ZERO;
    private static final BigInteger ONE = BigInteger.ONE;
    private static final BigInteger TWO = ONE.add(ONE);

    private static final Map<BigInteger, BigInteger> FIB_MAP = new HashMap<>();
    static {
        FIB_MAP.put(ZERO, ZERO);
        FIB_MAP.put(ONE, ONE);
    }

    private static BigInteger fib(BigInteger num) {
        return FIB_MAP.computeIfAbsent(num, key -> fib(num.subtract(ONE)).add(fib(num.subtract(TWO))));
    }

}

(BigInteger では四則演算をメソッドで記述しないといけないので、コードが若干見づらくなった気がします。。。)


あらためて n = 105 で実行してみます。

import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map;

public class Fibonacci3 {

    private static final BigInteger ZERO = BigInteger.ZERO;
    private static final BigInteger ONE = BigInteger.ONE;
    private static final BigInteger TWO = ONE.add(ONE);

    private static final Map<BigInteger, BigInteger> FIB_MAP = new HashMap<>();
    static {
        FIB_MAP.put(ZERO, ZERO);
        FIB_MAP.put(ONE, ONE);
    }


    public static void main(String[] args) {
        long start = System.currentTimeMillis();

        int n = 105;
        BigInteger sum = fib(BigInteger.valueOf(n));

        long end = System.currentTimeMillis();
        System.out.printf("Fibonacci3 fib: (%d) -> %d \ntime: %d(ms)\n", n, sum, (end - start));

    }

    private static BigInteger fib(BigInteger num) {
        return FIB_MAP.computeIfAbsent(num, key -> fib(num.subtract(ONE)).add(fib(num.subtract(TWO))));
    }

}

実行結果

Fibonacci3 fib: (105) -> 3928413764606871165730 
time: 56(ms)

桁あふれなく値がもとめられました。


スタックオーバーフロー

n の値をさらに大きくして計算してみます。

n = 1000 で実行してみると、、、

実行結果

Fibonacci3 fib: (1000) -> 43466557686937456435688527675040625802564660517371780402481729089536555417949051890403879840079255169295922593080322634775209689623239873322471161642996440906533187938298969649928516003704476137795166849228875 
time: 62(ms)

計算できてそうです。


ではでは、次は n = 1800 で実行。

それっ。

実行結果

Exception in thread "main" java.lang.StackOverflowError
    at java.math.BigInteger.compareMagnitude(BigInteger.java:3541)
    at java.math.BigInteger.subtract(BigInteger.java:1434)
    at sandobox.fibonacci.Fibonacci3.lambda$fib$0(Fibonacci3.java:33)
    at java.util.HashMap.computeIfAbsent(HashMap.java:1127)
    at sandobox.fibonacci.Fibonacci3.fib(Fibonacci3.java:33)
(以下省略)

なんとまぁ、StackOverflowErrorがスローされてしまいました。
再帰呼び出しで、コールスタックの上限をついてしまったようです。

さて、どうしたものか。


フィボナッチ数列の各項を順に計算

フィボナッチ数は以下定義な訳なので、


\begin{equation}
F_{n+2} = F_{n+1} + F_{n}  \, \, \,(n\geqq2)
\end{equation}

基本的に数列の前2項の値がもとまっていれば、その和を計算すれば\( F_{n} \)のフィボナッチ数も導き出せます。
というわけでフィボナッチ数列を順に計算してキャッシュしていけば、求めたいフィボナッチ数を算出する際にスタックオーバーフローとなることも防げそうです。

n = 1800 に対するフィボナッチ数を求めようとしているのが、以下のコードです。
InsStream.rangeで0, 1, 2 ... と順nを与えてフィボナッチ数を計算してMapにキャッシュし、最終的に求めたい n に対するフィボナッチ数を計算しています。

import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.IntStream;

public class Fibonacci3B {

    private static final BigInteger ZERO = BigInteger.ZERO;
    private static final BigInteger ONE = BigInteger.ONE;
    private static final BigInteger TWO = ONE.add(ONE);

    private static final Map<BigInteger, BigInteger> FIB_MAP = new HashMap<>();
    static {
        FIB_MAP.put(ZERO, ZERO);
        FIB_MAP.put(ONE, ONE);
    }

    public static void main(String[] args) {
        long start = System.currentTimeMillis();

        int n = 1800;
        IntStream.range(1, n)
                .forEach(i -> fib(BigInteger.valueOf(i)));

        BigInteger sum = fib(BigInteger.valueOf(n));

        long end = System.currentTimeMillis();
        System.out.printf("Fibonacci3 fib(%d) -> %d\ntime: %d(ms)\n", n, sum, (end - start));

    }

    private static BigInteger fib(BigInteger num) {
        return FIB_MAP.computeIfAbsent(num, key -> fib(num.subtract(ONE)).add(fib(num.subtract(TWO))));
    }
    
}

実行結果

Fibonacci3 fib(1800) -> 6733912172802933472606353001846945074658287378884326089477601632746080275952604203199580265153593862390858117766432295498560989719530281829452850286454536277301941625978000791367655413469297462257623927534855511388238610890658838439857922737938956952361558179389004339772497124977152035343580348215676156404424782380266118900316342135562815217465023272599528784782167145877600
time: 62(ms)

スタックオーバーフローも発生せず、値を求めることができたようです。
(値でかすぎて正しいのか一見よくわかんないですね。。。)


まとめ

最終的に、とりあえず大きな数のフィボナッチ数も扱えるようになりました
(正直、最終的なやり方が綺麗なやり方なのかよくわかりませんが・・・)
再帰呼び出しとスタックオーバーフローに関しては、 末尾再帰呼び出し 等のキーワードが付いて回りますが、その辺は理解してないので、とりあえず今回は置いておきます。

Solidity - RemixでHello World

Solidityでのスマートコントラクト開発を行う上で、
統合開発環境としてはブラウザベースのRemixを利用するのが一番お手軽なようです。

とりあえずRemix を利用して Hello Wolrd を実行してみます。


Remixの起動

以下URLのアクセスします。

https://remix.ethereum.org

すると以下のような画面が表示されます

f:id:nini_y:20180813231913p:plain

ここで、左上の+ボタンをクリックします


ファイルの作成

FileNameに HelloWorld.sol と入力して「OK」をクリックします。

f:id:nini_y:20180813231920p:plain


コードの記述

以下のコードを記述します

pragma solidity ^0.4.23;

contract HelloWorld {
    
    function hello() public pure returns (string) {
        return "Hello World!!";
    }
}

HelloWorldというcontractに、"Hello World!!"という文字列をreturnするfunctionを定義します。
(constant修飾子をfunctionに付与する例が時々見られますが、現在はfunctionにconstantを付与するのは非推奨になっているようです)


コンパイル

画面右上の辺りにある「Start to compile」をクリックしてコンパイルを実行します。

f:id:nini_y:20180813232700p:plain


デプロイ

コンパイル完了後、「Run」タブをクリックします。

f:id:nini_y:20180813232839p:plain


以下の表示になるので、「Deploy」をクリックします

f:id:nini_y:20180813232936p:plain


functionの実行

「Deployed Contracts」の中のHello Worldのリストをクリックするとhelloというfunction名が表示されるので、
それをクリックします。

f:id:nini_y:20180813233006p:plain


するとfunctionの結果として "Hello World!!" の文字列が返されます。

f:id:nini_y:20180813233304p:plain


まとめ

Remixを利用することで、とりあえずSolidityのHello World を試すことができました。

Ethereum - トランザクションの実行の流れ

ビットコインのトランザクションはTXOモデルとなっています。
しかし、Ethereum(イーサリアム)ではアカウント情報を保存しているState Tree の中に残高情報を保持しているためトランザクションの実行フローが異なっています。


トランザクションが満たす必要がある条件

トランザクションは以下項目を満たす必要があります。

  • RLPフォーマット(入れ子構造のバイナリデータの符号化用データフォーマット)に従う形式で追加の後続バイトが存在しないこと。
  • 署名が有効であること。
  • nonce が有効であること。(Sate Treeのアカウントが保持するnonce と照合して有効であるかを確認する)
  • トランザクションに事前に定められたGas量 + 送信するデータを送信するのに必要となるGas量 よりも、Gas Limit が大きく設定されていること。
    コントラクト生成のトランザクションの場合は、さらに追加で 32,000 gas が必要となる。
  • 送信者アカウントの残高が上述のGas を上回っていること。

上記項目を満たす場合、トランザクションは次の処理に移り、サブ状態のSubstate を作成します。
Substate は以下の情報から構成される。

  • Self-Destruct Set
    • トランザクション完了後に破棄されるアカウント情報がある場合、ここに格納される。
  • Log Series
    • トランザクションがどこまで実行されたかを保持するチェックポイント。
      フロントエンドなどからトランザクションの実行状態を確認する場合に、この情報が必要となります。
  • Refund Balance
    • トランザクション完了後にアカウントに戻される金額。
      State Tree にデータを書き込むためにはGasが必要だが、逆にState Treeからデータを削除する場合、State Treeを小さくすることになり、
      State Tree を削減した報酬として金額が返ってくる仕組みとなっている。
      トランザクション実行の進捗状況に従って、状態データを削除していくと、本項目の値が増加する。
      実際には、トランザクション実行のコストと相殺されたあとに返却される。


トランザクションの実行

通常の送金やコントラクトを実行する "メッセージコールトランザクション" と コントラクトを生成する "コントラクト生成トランザクション" があり、
各々のトランザクションの実行ステップは以下のようになっています。


メッセージコールトランザクション

メッセージコールトランザクションの実行ステップは以下の通りです。

  1. State Tree にあるトランザクション送信者のnonceが1インクリメントされる
  2. トランザクションを実行するため、必要なGasがアカウントの残高から差し引かれる
  3. トランザクション実行後、残っているGasとRefund Balanceに記載されているGasから返金される金額を計算して返金する
    これによりトランザクションの状態が確定する
  4. Gasの返金受け取り後に、ブロックにトランザクションを取り込んだマイナー、もしくはブロック生成ノードに使用したGas分のETHが送られる
  5. Self-Destruct Set にアカウント情報が含まれていた場合、削除する
  6. トランザクションに使用したGas量とログを記録する


コントラクト生成トランザクション

コントラクト生成トランザクションの実行ステップは以下の通りです。
(メッセージコールトランザクションとは異なり、コントラクト生成トランザクションを生成したアカウントやEVMコードからサブ状態を作成し、その後に下記ステップでコントラクトアカウントを生成する。)

  1. トランザクションを発行したアカウントとnonceからコントラクトのアドレスを作成する
  2. nonceに0を指定する
  3. コントラクトアカウントにトランザクションに送金したEtherの金額を設定する
  4. アカウントのStorageを空に設定する
  5. codeHashに空の文字列ハッシュを設定する
  6. 初期化コードを実行してアカウントを作成する。
    初期化コードの実行はGasを消費する。Gasが不足した場合トランザクションは無かったものとなる
    Revert Codeでエラーが出力された場合、その時点で残っているGasが戻り、ステップ3で設定したEtherが戻る
  7. 初期化コードの実行に成功した場合、コントラクトのデータサイズに応じてGasが支払われ、余ったGasはトランザクションの送信者に戻る



関連エントリ・書籍

ブロックチェーンアプリケーション開発の教科書

ブロックチェーンアプリケーション開発の教科書

Ethereum - ブロック構造

Ethereumのブロックはヘッダとトランザクション、トランザクション実行結果の3要素で構成されています。

各要素の例と項目の説明をメモしておきます(初学者なので、以下の内容もどこまで正確なのかは不安あり・・・)


ブロックヘッダ

ブロックヘッダの例は以下の通りとなります

{
  difficulty: 174882,
  extraData: "0xd98301080a846765746888676f312e31302e328664617277696e",
  gasLimit: 67863675,
  gasUsed: 21000,
  hash: "0x369003471d86903a6527353318fcedb82ba400a551ceafabcf3c77e36a60c348",
  logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  miner: "0xf7c80f38982e23707cad8a0ed0f78b0b65c5134e",
  mixHash: "0xbe2e1f8c000f003f0124798b144a6677787ea1476f552471f1f802bf87ce0120",
  nonce: "0x55cc0b5782b8812d",
  number: 698,
  parentHash: "0xbff2a4fdbbc229934bf13d486b4f8d9a167e466cd97749bc0249f7b339da03da",
  receiptsRoot: "0x05bccc39ceb3c0844b724a7606d8e7c40746f2517875cc974554fb03b3084d6c",
  sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
  size: 655,
  stateRoot: "0x23367a17d56b419f3ec7584a7eeeade5538e63883cbf1ebd127cd18746c896aa",
  timestamp: 1534058867,
  totalDifficulty: 108836926,
  transactions: ["0x621ef85ea52fe43b547eea00809d20fced26df7873dc62c8e358fc6ed94a5c58"],
  transactionsRoot: "0xb109addf20eda9a6e5d6670b6e52038aa85282efd62ed1498fb927bea19497e1",
  uncles: []
}

ブロックヘッダの各項目は以下の通りです

  • difficulty
    • ブロックを生成する難易度。これ以前のブロックの難易度とタイムスタンプから算出されます。
  • extraData
    • ブロックに関連する任意の情報を記録する場所。サイズは32byte以下です。
  • gasLimit
    • このブロックで使用できるGasの最大サイズ。
  • gasUsed
    • このブロックで使われたGasの使用量。
  • hash
    • ブロックを表すハッシュ値。
  • logsBloom
    • ブロック内のトランザクションから出力されるログがブルームフィルタと呼ばれる形で記録されている。
      実行したアカウントの情報とそのトランザクションの内容、それに付随する情報が格納される。
      全トランザクションを確認しなくても、このブロックに特定のログがあるかどうかを判断されるために利用される。
  • miner
    • このブロックを生成し、採掘手数料を受け取るアカウントのアドレス(160bit)。
  • mixHash
    • このブロックで十分な量の計算が実行されたことを、nonceと組み合わせて証明する256bitのハッシュ値。
  • nonce
    • mixHash と組み合わせて、このブロックで十分な量の計算が実行されたことを表す。64bitのハッシュ値。
  • number
    • 現在のブロック番号。Genesisブロックの場合は値は0。
  • parentHash
    • 親ブロック(前のブロック)のヘッダのハッシュ値。KECCAK-256ハッシュ形式。
  • receiptsRoot
    • ブロックに入っているトランザクションの実行結果を保存している、データ構造のルートノードのハッシュ値。KECCAK-256ハッシュ形式。
  • sha3Uncles (ommersHash)
    • 現在のブロックのUncleブロック配列のハッシュ。KECCAK-256ハッシュ形式。
      Uncleブロックとはブロックが生成された時に同時期に生成されたブロックで、ブロック内のトランザクションは無視されるが生成の報酬をもらうべき対象のブロック。
  • size
    • このブロックのサイズ(単位:byte)
  • stateRoot
    • このブロックの全トランザクションが実行された状態のState Tree のルートノードのハッシュ値。KECCAK-256ハッシュ形式。
  • timestamp
    • ブロックがチェーンに取り込まれた時刻。Unixタイムスタンプ形式。
  • totalDifficulty
    • このブロック以前の難易度の総和。
  • transactions
    • ブロックに取り込まれているトランザクションのハッシュが配列として格納されている。
  • transactionsRoot
    • ブロックに入っているトランザクションを含んだ木構造のルートノードのハッシュ値。KECCAK-256ハッシュ形式。
  • uncles
    • Unclebブロックのハッシュ配列。


Ethereumはブロックヘッダに大まかに重要な情報が格納されています。
Ethereumのネットワークに接続する全クライアントが全ブロック情報を必要としているわけではありません。
送金やコントラクタの呼び出しを利用するクライアントは、ブロックのヘッダ情報のみをダウンロードし、
計算の際に必要に応じてヘッダ情報からブロックのトランザクション情報を取得する仕組みになっています。


トランザクション

トランザクションの例は以下の通りとなります

{
  blockHash: "0x369003471d86903a6527353318fcedb82ba400a551ceafabcf3c77e36a60c348",
  blockNumber: 698,
  from: "0xf7c80f38982e23707cad8a0ed0f78b0b65c5134e",
  gas: 90000,
  gasPrice: 18000000000,
  hash: "0x621ef85ea52fe43b547eea00809d20fced26df7873dc62c8e358fc6ed94a5c58",
  input: "0x",
  nonce: 0,
  r: "0x50de58e50150a7325321814e09ef129b2bfca265721a077492fdbe485754e356",
  s: "0xfc01614f5150abe37fdeac2f22928dfc4200404f91370e4a4c882b7cb042ee2",
  to: "0x0e41e2a7b939ffa1b3d495ab43cda793939e41e5",
  transactionIndex: 0,
  v: "0xbd",
  value: 3000000000000000000
}

トランザクションの各項目は以下の通りです

  • blockHash
    • トランザクションがどのブロックに取り込まれているかを表す値。この値がnullの場合、トランザクションはブロックに取り込まれていない。
  • blockNumber
    • トランザクションが何番目のブロックに入っているかを表す。この値がnullの場合、トランザクションはブロックに取り込まれていない。
  • from
    • トランザクションを発行した送信者のアドレス。(20byteの値)
  • gas
    • 送信者が供給したGasの量。
  • gasPrice
    • トランザクションで払っても良いと決めたGasの金額。(単位:wei)
  • hash
    • トランザクションを表す32byteのハッシュ値。
  • input
    • トランザクションに送信されたデータ。
  • nonce
    • トランザクション以前に送信者が送信したトランザクションの数。
  • r
    • 送信者のトランザクションを特定する署名を作るために使用される。
  • s
    • 送信者のトランザクションを特定する署名を作るために使用される。
  • to
    • トランザクションを受け取った受信者のアドレス。(20byteの値)
  • transactionIndex
    • トランザクションのブロックの何番目のトランザクションとして入っているかを表す。この値がnullの場合、トランザクションはブロックに取り込まれていない。
  • v
    • 送信者のトランザクションを特定する署名を作るために使用される。
  • value
    • 送信者から受信者へ送る量。(単位:wei)


トランザクションの実行結果(レシート)

トランザクションの実行結果(レシート)を木構造で保存してるものをReceipt Tree と呼ぶ。
トランザクションレシートの内部構造の例は以下の通りです。

{
  blockHash: "0x369003471d86903a6527353318fcedb82ba400a551ceafabcf3c77e36a60c348",
  blockNumber: 698,
  contractAddress: null,
  cumulativeGasUsed: 21000,
  from: "0xf7c80f38982e23707cad8a0ed0f78b0b65c5134e",
  gasUsed: 21000,
  logs: [],
  logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  root: "0x0e999db3606fdfdcde919e7b05dde9947e9b7d54a22954d810519d687401f27b",
  to: "0x0e41e2a7b939ffa1b3d495ab43cda793939e41e5",
  transactionHash: "0x621ef85ea52fe43b547eea00809d20fced26df7873dc62c8e358fc6ed94a5c58",
  transactionIndex: 0
}

トランザクションレシートの各項目は以下の通りです

  • blockHash
    • トランザクションがどのブロックに入っているかを表す。
  • blockNumber
    • トランザクションが何番目のブロックに入っているかを表す。
  • contractAddress
    • コントラクトを作成するトランザクションの場合、コントラクトのアドレスが入る。
  • cumulativeGasUsed
    • トランザクション全体で使われたGasの使用量。例えば、他の処理もフックして実行された場合は、そのGas使用量も合算される。
  • from
    • トランザクションを発行した送信者のアドレス。(20byteの値)
  • gasUsed
    • トランザクションで使われたGasの使用量。
  • logs
    • トランザクションで生成されたログ。
  • logsBloom
    • ブロック内のトランザクションから出力されるログが、ブルームフィルタと呼ばれる形で記録される。
  • root
    • トランザクションがState Tree を変化させた後のState Root の値。
  • to
    • トランザクションを受け取った受信者のアドレス。(20byteの値)
  • transactionHash
    • 32byteのトランザクションハッシュ値。
  • transactionIndex
    • トランザクションがブロックの何番目のトランザクションとして入っているかを表している。



関連エントリ・書籍

ブロックチェーンアプリケーション開発の教科書

ブロックチェーンアプリケーション開発の教科書

Ethereum - Gethコンソールのコマンドを試す

Gethコンソールのコマンドをいくつか試してみます。

以下エントリの続きという感じになっています。

Gethを起動した状態で以下色々コマンドを実行しています。


ブロック内容の確認

eth.getBlock コマンドでブロックの内容を書くにすることができます。
引数に何番目のブロックを確認したいかのインデックスをしていします。
ブロックは、0→1→2→3... と積み上がっていき、ブロック高と呼ばれることがあります。
ブロック0がgenesisブロックに該当します。

実行例

> eth.getBlock(0)
{
  difficulty: 256,
  extraData: "0x",
  gasLimit: 134217728,
  gasUsed: 0,
  hash: "0x83b885bae699c5650394963ed8927f780cf4fea39b55d3abf430e5a95aa305f3",
  logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  miner: "0x3333333333333333333333333333333333333333",
  mixHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
  nonce: "0x0000000000000042",
  number: 0,
  parentHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
  receiptsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
  sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
  size: 507,
  stateRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
  timestamp: 0,
  totalDifficulty: 256,
  transactions: [],
  transactionsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
  uncles: []
}

genesisブロックの内容が表示されました


マイニングの開始と確認

miner.start コマンドでブロックを生成する毎に具を開始します。
引数には、マイニング処理を実行するスレッド数を指定します。

実行例

> miner.start(2)
null

nulltrueと表示されれば成功です。
が、いまいち本当に動作しているのかよくわからないので、eth.mining コマンドでマイニング中かどうかを確認します。
trueが表示されれば、マイニング中です。

実行例

> eth.mining
true


コインベースの残高確認

ブロックをマイニングのたびにetherが獲得でき、コインベースカウントの残高が増加していきます。
eth.getBalance コマンドで引数にコインベースアカウントのアドレスを指定します。
コインベースアカウントがaccounts[0]になっている前提で、以下実行しています。

実行例

> eth.getBalance(eth.accounts[0])
2.53e+21

上記の表示結果は単位がweiになっています。
これだとわかりにくい場合は、以下のようにweb3.fromWeiを利用して単位をetherに変換します。

実行例

> web3.fromWei(eth.getBalance(eth.accounts[0]), "ether")
2725


マイニングの停止

マイニングを終了する場合は、miner.stop コマンドを実行します。trueと表示されればOKです。

> miner.stop()
true


送金

送金を行うためには、eth.sendTransaction コマンドを使用します。from に送金元アドレス、to に送金先アドレス、value に送金額を指定します。
ここでの総金額はweiの単位で指定する必要があり、ether単位の値を指定できないため、web3.toWeiを利用して単位をweiに変換します。
例えば accounts[0]からaccounts[3]に、3ether を送金する場合は以下のようになります

実行例

> eth.sendTransaction({from: eth.accounts[0], to: eth.accounts[3], value: web3.toWei(3, "ether")})
Error: authentication needed: password or unlock
    at web3.js:3143:20
    at web3.js:6347:15
    at web3.js:5081:36
    at <anonymous>:1:1

しかし、コマンドの結果はエラーとなり、エラーメッセージが表示されました。
これは誤って送金してしまうのを防ぐために、accounts[0]がロックされているためです。ロックを解除する必要があります。


ロックの解除

ロックの解除には personal.unlockAccount コマンドを実行します。引数にはロック解除対象アカウントのアドレスを指定します。
accounts[0]のロックを解除する場合以下のようになります

実行例

> personal.unlockAccount(eth.accounts[0])
Unlock account 0xf7c80f38982e23707cad8a0ed0f78b0b65c5134e
Passphrase: (ここでアカウント作成時に指定したpasswordを入力します)
true

true と表示されれば、ロックの解除成功です。


アンロックオプション付きの起動

本番環境であれば、アカウントをロックしておくのが望ましいでしょうがプライベートネットでは毎回パスワードを入力して、
ロックを解除するのは面倒なので、以下作業を実施してgeth起動時にアンロックしてしまうようにします。


gethの終了

作業のため一旦exit コマンドでgethを終了します

> exit


アカウントパスワードファイルの作成

password.txt というようなファイル名を作成し、ファイル内に以下のようにacctounts[0]→acctounts[1]→acctounts[2]→acctounts[3]の順に
改行区切りで各アカウントのパスワードを記述して保存します。

password123
password123
password123
password123


アンロックオプション付きでのGethの起動

以下のオプションを加えてGethを起動することで、最初からアカウントのロックを解除した状態にできます。

--unlock {アンロックするアカウントのアドレス} --password {パスワードファイルのパス}

アンロックするアカウントを複数指定する場合は、アドレスをカンマ区切りで指定します。

実行例

$ geth --networkid "10" --nodiscover --datadir /Users/yuki/work/private_eth --rpc --rpcaddr "localhost" --rpcport "8545" --rpccorsdomain "*" --rpcapi "eth,net,web3,personal" --targetgaslimit "20000000" console 2>> /Users/yuki/work/private_eth/error.log --unlock 0xf7c80f38982e23707cad8a0ed0f78b0b65c5134e,0xbaa4290b7c18afe9905060e35b44ddd2a41b34e9,0x559a14163bef0d310f489042707e4c73461e651a,0x0e41e2a7b939ffa1b3d495ab43cda793939e41e5 --password /Users/yuki/work/private_eth/password.txt
Welcome to the Geth JavaScript console!

instance: Geth/v1.8.10-stable/darwin-amd64/go1.10.2
coinbase: 0xf7c80f38982e23707cad8a0ed0f78b0b65c5134e
at block: 697 (Sun, 12 Aug 2018 15:33:13 JST)
 datadir: /Users/yuki/work/private_eth
 modules: admin:1.0 debug:1.0 eth:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0


改めて送金

一度エラーとなった送金処理を、アンロックオプションを指定して起動したGethで再度実施してみます。
(送信先のアカウントの残高を送金前に確認しておきます)

> web3.fromWei(eth.getBalance(eth.accounts[3]), "ether")
0
> eth.sendTransaction({from: eth.accounts[0], to: eth.accounts[3], value: web3.toWei(3, "ether")})
"0x621ef85ea52fe43b547eea00809d20fced26df7873dc62c8e358fc6ed94a5c58"

トランザクションが成功した場合は16進数のトランザクションハッシュ値が出力されます。(トランザクションハッシュ値は毎回異なります)


トランザクションの確認

発行したトランザクションがどうなっているかを eth.getTransaction コマンドで確認することができます。
引数にはトランザクションハッシュ値を指定します。

実行例

> eth.getTransaction("0x621ef85ea52fe43b547eea00809d20fced26df7873dc62c8e358fc6ed94a5c58")
{
  blockHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
  blockNumber: null,
  from: "0xf7c80f38982e23707cad8a0ed0f78b0b65c5134e",
  gas: 90000,
  gasPrice: 18000000000,
  hash: "0x621ef85ea52fe43b547eea00809d20fced26df7873dc62c8e358fc6ed94a5c58",
  input: "0x",
  nonce: 0,
  r: "0x50de58e50150a7325321814e09ef129b2bfca265721a077492fdbe485754e356",
  s: "0xfc01614f5150abe37fdeac2f22928dfc4200404f91370e4a4c882b7cb042ee2",
  to: "0x0e41e2a7b939ffa1b3d495ab43cda793939e41e5",
  transactionIndex: 0,
  v: "0xbd",
  value: 3000000000000000000
}

from にaccounts[0]のアドレスが、toにaccounts[3]のアドレスが表示されます。
blockNumber が null になっているので、このトランザクションはまだブロックに取り込まれていないことを意味しています。

ブロックに取り込まれていないトランザクションは確定していないため、送金はまだ完了していません。
この時点での送信先のアカウントの残高を確認してみると以下の通りです。

> web3.fromWei(eth.getBalance(eth.accounts[3]), "ether")
0

トランザクションを確定するためにはブロックを生成し、トランザクションがブロックに取り込まれる必要があります。
そのためにマイニングを再開します。

> miner.start(2)
null


マイニング後のトランザクション確認

一定時間後に改めて対象トランザクションの状態を確認してみます

> eth.getTransaction("0x621ef85ea52fe43b547eea00809d20fced26df7873dc62c8e358fc6ed94a5c58")
{
  blockHash: "0x369003471d86903a6527353318fcedb82ba400a551ceafabcf3c77e36a60c348",
  blockNumber: 698,
  from: "0xf7c80f38982e23707cad8a0ed0f78b0b65c5134e",
  gas: 90000,
  gasPrice: 18000000000,
  hash: "0x621ef85ea52fe43b547eea00809d20fced26df7873dc62c8e358fc6ed94a5c58",
  input: "0x",
  nonce: 0,
  r: "0x50de58e50150a7325321814e09ef129b2bfca265721a077492fdbe485754e356",
  s: "0xfc01614f5150abe37fdeac2f22928dfc4200404f91370e4a4c882b7cb042ee2",
  to: "0x0e41e2a7b939ffa1b3d495ab43cda793939e41e5",
  transactionIndex: 0,
  v: "0xbd",
  value: 3000000000000000000
}

blockNumberにnullではなく数値が設定されています。この例の場合は、698番目のブロックに取り込まれていることを示しています。

この後に、送信先アカウントの残高確認すると予定通り送金が完了していることがわかります。

> web3.fromWei(eth.getBalance(eth.accounts[3]), "ether")
3


トランザクションレシートの確認

トランザクションが実行されるとレシートが発行されます。eth.getTransactionReceipt コマンドで確認できます

> eth.getTransactionReceipt("0x621ef85ea52fe43b547eea00809d20fced26df7873dc62c8e358fc6ed94a5c58")
{
  blockHash: "0x369003471d86903a6527353318fcedb82ba400a551ceafabcf3c77e36a60c348",
  blockNumber: 698,
  contractAddress: null,
  cumulativeGasUsed: 21000,
  from: "0xf7c80f38982e23707cad8a0ed0f78b0b65c5134e",
  gasUsed: 21000,
  logs: [],
  logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  root: "0x0e999db3606fdfdcde919e7b05dde9947e9b7d54a22954d810519d687401f27b",
  to: "0x0e41e2a7b939ffa1b3d495ab43cda793939e41e5",
  transactionHash: "0x621ef85ea52fe43b547eea00809d20fced26df7873dc62c8e358fc6ed94a5c58",
  transactionIndex: 0
}

このレシート上からも、698番目のブロックに取り込まれていることが確認できます。


まとめ

というわけで、Gethでの送金含めたいくつかのコマンドを試してみました。



関連エントリ