Help us understand the problem. What is going on with this article?

ブロックチェーンを作ることで学ぶ 〜ブロックチェーンがどのように動いているのか学ぶ最速の方法は作ってみることだ〜

この記事について

この記事はDaniel van FlymenさんのLearn Blockchains by Building One - The fastest way to learn how Blockchains work is to build oneを本人の許可を得て翻訳したものです。

このブロックチェーンのリポジトリではPython以外での言語の実装者の募集も行われているので、興味がある方は是非どうぞ!
また、この翻訳で出てくる日本語版のリポジトリはこちらにあるので是非参考にしてみてください。

はじめに

あなたがここにいるのは、私と同じように、暗号通貨の盛り上がりに対して心構えが出来ているからだ。そしてあなたはブロックチェーンがどのように動いているのか -その裏にある基本的なテクノロジー- を理解したいと思っている。

しかしブロックチェーンを理解するのは簡単ではない、少なくとも私にとっては簡単ではなかった。私は多くのビデオを見て、抜け漏れの多いチュートリアルをこなし、少なすぎる事例から来るフラストレーションに苦しんでいた。

私は手を動かして学ぶことが好きだ。手を動かすことは私を、抽象的なことをコードのレベルで扱うことを強制し、コードのレベルで扱うことにより抽象的なことが頭に定着する。もしあなたが同じことをすれば、このガイドが終わる頃には動いているブロックチェーンが、確かな理解とともに手に入るだろう。

始める前に

ブロックチェーンとは、不変の、連続したブロックと呼ばれる記録のチェーンであることを覚えておいてほしい。ブロックは取引、ファイルやあらゆるデータを格納することが出来る。しかし重要なことは、ハッシュを使って繋がっているということだ。

もしハッシュが何かについて自信がない場合は、ここに例がある。

この記事の対象者は? 以下が必要な要素だ。基本的なPythonの読み書きが出来ること、HTTPリクエストがどのように働くかについての理解していること(私達のブロックチェーンはHTTPを介して動くためだ)。

何が必要ですか? Python 3.6以上とpipがインストールされていることが必要だ。また、Flaskと素晴らしいRequest libraryもインストールする必要がある。

pip install Flask==0.12.2 requests==2.18.4

そうだった、PostmanやcURLのようなHTTPクライアントも必要だ。まあ動けばなんでも大丈夫だ。

最終的なコードはどこ? ソースコードはここにある。

ステップ1: ブロックチェーンを作る

あなたの好きなエディタかIDEを開いてほしい、個人的にはPyCharmが好きだ。そしてblockchain.pyという新しいファイルを作る。私たちは一つのファイルしか使わないが、もしどこにいるのかわからなくなったら、いつでもソースコードを参照することが出来る。

ブロックチェーンを書く

私たちがつくるBlockchainクラスのコンストラクタが、ブロックチェーンを納めるための最初の空のリストと、トランザクションを納めるための空のリストを作る。これが私たちのクラスの設計図だ。

blockchain.py
# coding: UTF-8

class Blockchain(object):
    def __init__(self):
        self.chain = []
        self.current_transactions = []

    def new_block(self):
        # 新しいブロックを作り、チェーンに加える
        pass

    def new_transaction(self):
        # 新しいトランザクションをリストに加える
        pass

    @staticmethod
    def hash(block):
        # ブロックをハッシュ化する
        pass

    @property
    def last_block(self):
        # チェーンの最後のブロックをリターンする
        pass

私たちのBlockchainクラスはチェーンの取り扱いを司っている。チェーンはトランザクションを収納し、新しいブロックをチェーンに加えるためのヘルパーメソッドを持っている。早速いくつかのメソッドを肉付けしていこう。

ブロックとはどのようなものなのか

それぞれのブロックは、インデックスタイムスタンプ(UNIXタイム)、トランザクションのリストプルーフ(詳細は後ほど)そしてそれまでの全てのブロックから生成されるハッシュを持っている。

これが一つのブロックの例だ。

block = {
    'index': 1,
    'timestamp': 1506057125.900785,
    'transactions': [
        {
            'sender': "8527147fe1f5426f9dd545de4b27ee00",
            'recipient': "a77f5cdfa2934df3954a5c7c7da5df1f",
            'amount': 5,
        }
    ],
    'proof': 324984774000,
    'previous_hash': "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
}

この時点で、チェーンのアイデアは明確だ -全ての新しいブロックはそれまでの全てのブロックから生成されるハッシュを自分自身の中に含んでいる。これこそがまさにブロックチェーンに不変性を与えているものであり、そのために重要なポイントだ。もしアタッカーがチェーン初期のブロックを破壊した場合、それに続く全てのブロックが不正なハッシュを含むことになる。

この意味がわかるだろうか?もし分からなければ、少し考える時間を取ってほしい -これはブロックチェーンのコアとなるアイデアだ。

トランザクションをブロックに加える

私たちにはトランザクションをブロックに加える方法が必要だ。new_transaction()メソッドがそれを司っており、非常に簡単だ。

blockchain.py
# coding: UTF-8

class Blockchain(object):
    ...

    def new_transaction(self, sender, recipient, amount):
        """
        次に採掘されるブロックに加える新しいトランザクションを作る
        :param sender: <str> 送信者のアドレス
        :param recipient: <str> 受信者のアドレス
        :param amount: <int> 量
        :return: <int> このトランザクションを含むブロックのアドレス
        """

        self.current_transactions.append({
            'sender': sender,
            'recipient': recipient,
            'amount': amount,
        })

        return self.last_block['index'] + 1

new_transaction()メソッドは、新しいトランザクションをリストに加えた後、そのトランザクションが加えられるブロック -次に採掘されるブロックだ-のインデックスをリターンする。

新しいブロックを作る

我々のBlockchainがインスタンス化されるとき、私たちはジェネシスブロック -先祖を持たないブロック- とともにシードする必要がある。それと同時に、ジェネシスブロックにプルーフ -マイニング(またはプルーフ・オブ・ワーク)の結果- も加える必要がある。マイニングについては後で取り上げる。

ジェネシスブロックを加えるのと同時に、new_block()メソッド、last_block()メソッドとhash()メソッドも作成しよう。

blockchain.py
# coding: UTF-8

import hashlib
import json
from time import time


class Blockchain(object):
    def __init__(self):
        self.current_transactions = []
        self.chain = []

        # ジェネシスブロックを作る
        self.new_block(previous_hash=1, proof=100)

    def new_block(self, proof, previous_hash=None):
        """
        ブロックチェーンに新しいブロックを作る
        :param proof: <int> プルーフ・オブ・ワークアルゴリズムから得られるプルーフ
        :param previous_hash: (オプション) <str> 前のブロックのハッシュ
        :return: <dict> 新しいブロック
        """

        block = {
            'index': len(self.chain) + 1,
            'timestamp': time(),
            'transactions': self.current_transactions,
            'proof': proof,
            'previous_hash': previous_hash or self.hash(self.chain[-1]),
        }

        # 現在のトランザクションリストをリセット
        self.current_transactions = []

        self.chain.append(block)
        return block

    def new_transaction(self, sender, recipient, amount):
        """
        次に採掘されるブロックに加える新しいトランザクションを作る
        :param sender: <str> 送信者のアドレス
        :param recipient: <str> 受信者のアドレス
        :param amount: <int> 量
        :return: <int> このトランザクションを含むブロックのアドレス
        """

        self.current_transactions.append({
            'sender': sender,
            'recipient': recipient,
            'amount': amount,
        })

        return self.last_block['index'] + 1

    @property
    def last_block(self):
        return self.chain[-1]

    @staticmethod
    def hash(block):
        """
        ブロックの SHA-256 ハッシュを作る
        :param block: <dict> ブロック
        :return: <str>
        """

        # 必ずディクショナリ(辞書型のオブジェクト)がソートされている必要がある。そうでないと、一貫性のないハッシュとなってしまう
        block_string = json.dumps(block, sort_keys=True).encode()
        return hashlib.sha256(block_string).hexdigest()

ここで追加したものは非常に簡単なはずだ。何をしたかをクリアにしておくために、いくつかのコメントとdocstringsを加えておいた。我々のブロックチェーンはもうすぐ完成だ。だがしかし、ここで新しいブロックかどのように出来るのかを考える必要がある -鋳造 (forged) か採掘 (mined) か-

プルーフ・オブ・ワークを理解する

プルーフ・オブ・ワークアルゴリズム (PoW) とは、ブロックチェーン上でどのように新しいブロックが作られるか、または採掘されるかということを表している。PoWのゴールは、問題を解く番号を発見することだ。その番号はネットワーク上の誰からも見つけるのは難しく、確認するのは簡単 -コンピュータ的に言えば- なものでなければならない。これがプルーフ・オブ・ワークのコアとなるアイデアだ。

理解するために簡単な例を見てみよう。

ある整数xかけるある整数yhashが0で終わらないといけないとしよう。というわけで、hash(x * y) = ac23d...0というようになる。そしてこの簡単な例では、x = 5と固定しよう。Pythonで実装するとこうなる。

from hashlib import sha256

x = 5
y = 0  # まだこのyがどの数字であるべきかはわからない

while sha256(f'{x*y}'.encode()).hexdigest()[-1] != "0":
    y += 1

print(f'The solution is y = {y}')

解はy = 21。よってそれにより作られたハッシュは0で終わる。

>>> sha256(f'{5*21}'.encode()).hexdigest()
'1253e9373e781b7500266caa55150e08e210bc8cd8cc70d89985e3600155e860'

ビットコインでは、プルーフ・オブ・ワークのアルゴリズムはハッシュキャッシュ  (Hashcash)と呼ばれている。そしてそれはこの基本的な例とそこまで違うものではない。ハッシュキャッシュは、採掘者が競い合って新しいブロックを作るために問題を解く、というものだ。一般的に、難易度は探す文字の数によって決まる。採掘者はその解に対して、報酬としてトランザクションの中でコインを受け取る。

ネットワークは簡単に採掘者の解が正しいかを確認することが出来る。

基本的なプルーフ・オブ・ワークを実装する

私たちのブロックチェーンのために似たアルゴリズムを実装しよう。ルールは上の例と似ている。

前のブロックの解とともにハッシュを作ったときに、最初に4つの0が出てくるような番号pを探そう。

blockchain.py
# coding: UTF-8

import hashlib
import json

from time import time
from uuid import uuid4

class Blockchain(object):
    ...

    def proof_of_work(self, last_proof):
        """
        シンプルなプルーフ・オブ・ワークのアルゴリズム:
         - hash(pp') の最初の4つが0となるような p' を探す
         - p は1つ前のブロックのプルーフ、 p' は新しいブロックのプルーフ
        :param last_proof: <int>
        :return: <int>
        """

        proof = 0
        while self.valid_proof(last_proof, proof) is False:
            proof += 1

        return proof

    @staticmethod
    def valid_proof(last_proof, proof):
        """
        プルーフが正しいかを確認する: hash(last_proof, proof)の最初の4つが0となっているか?
        :param last_proof: <int> 前のプルーフ
        :param proof: <int> 現在のプルーフ
        :return: <bool> 正しければ true 、そうでなれけば false
        """

        guess = f'{last_proof}{proof}'.encode()
        guess_hash = hashlib.sha256(guess).hexdigest()

        return guess_hash[:4] == "0000"

アルゴリズムの難易度を調整するためには、最初の0の数を変えることで出来る。しかし4は充分な数だ。0を一つ加えることで、解を見つけるための時間にマンモス級の違いが出ることに気がつくだろう。

私たちのクラスはほとんど完成しているので、HTTPリクエストとともにこのクラスを使ってみよう。

ステップ2: APIとしての私たちのブロックチェーン

ここではPython Flaskフレームワークを使う。Flaskはマイクロフレームワークであり、簡単にエンドポイントをPythonのファンクションに対応させることが出来る。そして、私たちのブロックチェーンがHTTPリクエストを使ってWebで通信することが出来るようになる。

ここでは3つのメソッドを作る:
* ブロックへの新しいトランザクションを作るための/transactions/new
* サーバーに対して新しいブロックを採掘するように伝える/mine
* フルブロックチェーンを返す/chain

Flaskをセットアップする

サーバーは、このブロックチェーンのネットワークに一つのノードを作り出す。早速いくつかの例となるコードを作ろう。

blockchain.py
# coding: UTF-8

import hashlib
import json
from textwrap import dedent
from time import time
from uuid import uuid4

from flask import Flask, jsonify

class Blockchain(object):
    ...


# ノードを作る
# Flaskについて詳しくはこちらを読んでほしい http://flask.pocoo.org/docs/0.12/quickstart/#a-minimal-application
app = Flask(__name__)

# このノードのグローバルにユニークなアドレスを作る
node_identifier = str(uuid4()).replace('-', '')

# ブロックチェーンクラスをインスタンス化する
blockchain = Blockchain()

# メソッドはPOSTで/transactions/newエンドポイントを作る。メソッドはPOSTなのでデータを送信する
@app.route('/transactions/new', methods=['POST'])
def new_transactions():
    return '新しいトランザクションを追加します'

# メソッドはGETで/mineエンドポイントを作る
@app.route('/mine', methods=['GET'])
def mine():
    return '新しいブロックを採掘します'

# メソッドはGETで、フルのブロックチェーンをリターンする/chainエンドポイントを作る
@app.route('/chain', methods=['GET'])
def full_chain():
    response = {
        'chain': blockchain.chain,
        'length': len(blockchain.chain),
    }
    return jsonify(response), 200

# port5000でサーバーを起動する
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

トランザクションエンドポイント

これらトランザクションのリクエストの例だ。このようなものをユーザーはサーバーに送る:

{
 "sender": "my address",
 "recipient": "someone else's address",
 "amount": 5
}

すでにブロックにトランザクションを加えるメソッドは作ってあるため、残りは簡単だ。トランザクションを加えるためのメソッドを書いていこう。

blockchain.py
# coding: UTF-8

import hashlib
import json
from textwrap import dedent
from time import time
from uuid import uuid4

from flask import Flask, jsonify, request

...


# メソッドはPOSTで/transactions/newエンドポイントを作る。メソッドはPOSTなのでデータを送信する
@app.route('/transactions/new', methods=['POST'])
def new_transaction():
    values = request.get_json()

    # POSTされたデータに必要なデータがあるかを確認
    required = ['sender', 'recipient', 'amount']
    if not all(k in values for k in required):
        return 'Missing values', 400

    # 新しいトランザクションを作る
    index = blockchain.new_transaction(values['sender'], values['recipient'], values['amount'])

    response = {'message': f'トランザクションはブロック {index} に追加されました'}
    return jsonify(response), 201

採掘のエンドポイント

採掘のエンドポイントは魔法が起きるところだが、簡単だ。3つのことを行う必要がある。
1. プルーフ・オブ・ワークを計算する
2. 1コインを採掘者に与えるトランザクションを加えることで、採掘者(この場合は我々)に利益を与える
3. チェーンに新しいブロックを加えることで、新しいブロックを採掘する

blockchain.py
# coding: UTF-8

import hashlib
import json
from textwrap import dedent
from time import time
from uuid import uuid4

from flask import Flask, jsonify, request

...


# メソッドはGETで/mineエンドポイントを作る
@app.route('/mine', methods=['GET'])
def mine():
    # 次のプルーフを見つけるためプルーフ・オブ・ワークアルゴリズムを使用する
    last_block = blockchain.last_block
    last_proof = last_block['proof']
    proof = blockchain.proof_of_work(last_proof)

    # プルーフを見つけたことに対する報酬を得る
    # 送信者は、採掘者が新しいコインを採掘したことを表すために"0"とする
    blockchain.new_transaction(
        sender="0",
        recipient=node_identifier,
        amount=1,
    )

    # チェーンに新しいブロックを加えることで、新しいブロックを採掘する
    block = blockchain.new_block(proof)

    response = {
        'message': '新しいブロックを採掘しました',
        'index': block['index'],
        'transactions': block['transactions'],
        'proof': block['proof'],
        'previous_hash': block['previous_hash'],
    }
    return jsonify(response), 200

採掘されたブロックに含まれるトランザクションの受信者のアドレスは、自分のノードのアドレスであることに注意してほしい。そして、ここで行っていることの殆どは、ブロックチェーンクラスとのインタラクションにすぎない。一旦ここまでにして、このブロックチェーンとのインタラクションを始めよう。

ステップ3: オリジナルブロックチェーンとのインタラクション

cURLかPostmanを使ってAPIを叩いてみよう

サーバーを起動する:

$ python blockchain.py
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

http://localhost:5000/mineへのGETリクエストを作って採掘しよう

blockchain_mining.png

http://localhost:5000/transactions/newへのPOSTリクエストを作って新しいトランザクションを作ろう。ボディに取引の内容を入れておく。

blockchain_transaction.png

もしPostmanを使っていない場合、cURLを使っても同じことが出来る。

$ curl -X POST -H "Content-Type: application/json" -d '{
 "sender": "d4ee26eee15148ee92c6cd394edd974e",
 "recipient": "someone-other-address",
 "amount": 5
}' "http://localhost:5000/transactions/new"

サーバーを再起動して、2ブロックを採掘した、つまりトータル3ブロックだ。ここでチェーン全体をhttp://localhost:5000/chainをリクエストすることで見てみよう。

{
  "chain": [
    {
      "index": 1,
      "previous_hash": 1,
      "proof": 100,
      "timestamp": 1506280650.770839,
      "transactions": []
    },
    {
      "index": 2,
      "previous_hash": "c099bc...bfb7",
      "proof": 35293,
      "timestamp": 1506280664.717925,
      "transactions": [
        {
          "amount": 1,
          "recipient": "8bbcb347e0634905b0cac7955bae152b",
          "sender": "0"
        }
      ]
    },
    {
      "index": 3,
      "previous_hash": "eff91a...10f2",
      "proof": 35089,
      "timestamp": 1506280666.1086972,
      "transactions": [
        {
          "amount": 1,
          "recipient": "8bbcb347e0634905b0cac7955bae152b",
          "sender": "0"
        }
      ]
    }
  ],
  "length": 3
}

ステップ4: コンセンサス

これはクールだ。トランザクションを受け付けて、新しいブロックを採掘できるブロックチェーンを作ることが出来た。しかしブロックチェーンの重要なポイントは、非中央集権的であることだ。そしてもし非中央集権的であれば、我々はどのように地球上の全員が同じチェーンを反映していると確認することが出来るだろうか。これはコンセンサスの問題と呼ばれており、もし1つより多くのノードをネットワーク上に持ちたければ、コンセンサスのアルゴリズムを実装しなければならない。

新しいノードを登録する

コンセンサスアルゴリズムを実装する前に、ネットワーク上にある他のノードを知る方法を作ろう。それぞれのノードがネットワーク上の他のノードのリストを持っていなければならない。なのでいくつかのエンドポイントが追加で必要となる。

  1. URLの形での新しいノードのリストを受け取るための/nodes/register
  2. あらゆるコンフリクトを解消することで、ノードが正しいチェーンを持っていることを確認するための/nodes/resolve

これから、我々のブロックチェーンの構造を編集し、ノード登録のためのメソッドを追加する:

blockchain.py
...
from urllib.parse import urlparse
...


class Blockchain(object):
    def __init__(self):
        ...
        self.nodes = set()
        ...

    def register_node(self, address):
        """
        ノードリストに新しいノードを加える
        :param address: <str> ノードのアドレス 例: 'http://192.168.0.5:5000'
        :return: None
        """

        parsed_url = urlparse(address)
        self.nodes.add(parsed_url.netloc)

ノードのリストを保持するのにset()を使ったことに注意してほしい。これは、新しいノードの追加がべき等 -同じノードを何回加えても、一度しか現れない- ということを実現するための簡単な方法だ。

コンセンサスアルゴリズムを実装する

以前言及したとおり、コンフリクトはあるノードが他のノードと異なったチェーンを持っているときに発生する。これを解決するために、最も長いチェーンが信頼できるというルールを作る。別の言葉で言うと、ネットワーク上で最も長いチェーンは事実上正しいものといえる。このアルゴリズムを使って、ネットワーク上のノード間でコンセンサスに到達する。

blockchain.py
...
import requests


class Blockchain(object)
    ...

    def valid_chain(self, chain):
        """
        ブロックチェーンが正しいかを確認する

        :param chain: <list> ブロックチェーン
        :return: <bool> True であれば正しく、 False であればそうではない
        """

        last_block = chain[0]
        current_index = 1

        while current_index < len(chain):
            block = chain[current_index]
            print(f'{last_block}')
            print(f'{block}')
            print("\n--------------\n")

            # ブロックのハッシュが正しいかを確認
            if block['previous_hash'] != self.hash(last_block):
                return False

            # プルーフ・オブ・ワークが正しいかを確認
            if not self.valid_proof(last_block['proof'], block['proof']):
                return False

            last_block = block
            current_index += 1

        return True

    def resolve_conflicts(self):
        """
        これがコンセンサスアルゴリズムだ。ネットワーク上の最も長いチェーンで自らのチェーンを
        置き換えることでコンフリクトを解消する。
        :return: <bool> 自らのチェーンが置き換えられると True 、そうでなれけば False
        """

        neighbours = self.nodes
        new_chain = None

        # 自らのチェーンより長いチェーンを探す必要がある
        max_length = len(self.chain)

        # 他のすべてのノードのチェーンを確認
        for node in neighbours:
            response = requests.get(f'http://{node}/chain')

            if response.status_code == 200:
                length = response.json()['length']
                chain = response.json()['chain']

                # そのチェーンがより長いか、有効かを確認
                if length > max_length and self.valid_chain(chain):
                    max_length = length
                    new_chain = chain

        # もし自らのチェーンより長く、かつ有効なチェーンを見つけた場合それで置き換える
        if new_chain:
            self.chain = new_chain
            return True

        return False

この最初のメソッドvalid_chain()は、チェーンの中の全てのブロックに対してハッシュとプルーフが正しいかを確認することで、チェーンが有効かどうかの判定を行っている。

resolve_conflicts()メソッドは、全てのネットワーク上のノードに対して、それらのチェーンをダウンロードし、上記のメソッドを使うことで確認している。もし有効なチェーンで自らのチェーンよりも長いものがあった場合、それで自らのチェーンを入れ替える。

次に2つのエンドポイントをAPIに追加しよう。1つはネットワーク上に他のノードを追加するため、もう1つはコンフリクトを解消するためのものだ。

blockchain.py
...

@app.route('/nodes/register', methods=['POST'])
def register_node():
    values = request.get_json()

    nodes = values.get('nodes')
    if nodes is None:
        return "Error: 有効ではないノードのリストです", 400

    for node in nodes:
        blockchain.register_node(node)

    response = {
        'message': '新しいノードが追加されました',
        'total_nodes': list(blockchain.nodes),
    }
    return jsonify(response), 201


@app.route('/nodes/resolve', methods=['GET'])
def consensus():
    replaced = blockchain.resolve_conflicts()

    if replaced:
        response = {
            'message': 'チェーンが置き換えられました',
            'new_chain': blockchain.chain
        }
    else:
        response = {
            'message': 'チェーンが確認されました',
            'chain': blockchain.chain
        }

    return jsonify(response), 200
...

ここで、もう1つのマシンがあればそれを使って(訳注:複数マシン間でどのようにアクセスするのかは不明。ngork使うとか?)別のノードを立ち上げる。または、同じマシンで違うポートから別のノードを立ち上げる。すなわち、http://localhost:5000http://localhost:5001という2つのノードが出来る。

まず、 新しいノードを登録する。

訳注:ターミナルでcurlコマンドで日本語を表示するとユニコードエスケープで表示されてしまいます。その際は、jqをインストールして(macでHomebrewを使っていればbrew install jqで出来ます)、コマンドの後ろに| jqを加えるとデコードされて表示されます。

$ curl -X POST -H "Content-Type: application/json" -d '{
    "nodes": ["http://localhost:5001"]
}' "http://localhost:5000/nodes/register"

blockchain_register.png

そしてノード2のチェーンが長くなるように、いくつか新しいブロックをノード2で採掘する。その後、ノード1でGET /nodes/resolveを行い、コンセンサスアルゴリズムによりチェーンを置き換える。

$ curl "http://localhost:5001/mine"
$ curl "http://localhost:5000/nodes/resolve"

blockchain_consensus.png

これでおしまいだ。友達と我々のブロックチェーンを試してみてほしい。


これがあなたを何か新しいものを作るよう奮い立たせるよう願っている。ブロックチェーンが、我々の経済・政府・記録の保管への考え方を急速に変えていくと信じているので、私は暗号通貨に対して非常に興奮している。

アップデート: これの続きとなるパート2を計画中だ。そこでは、トランザクションを確認するメカニズムと、このブロックチェーンを実際に使えるようにするためのいくつかの方法についての議論を追加する予定だ。

もしこのガイドを楽しんでくれたのなら、または提案や質問があれば、コメントで知らせてほしい。また、バグを見つけたら気軽にここにコントリビュートしてほしい!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした