Ukawacoinから学ぶブロックチェーン技術

Sunday, Jan 9, 2022

うかわです.
アドベンドカレンダー「ほぼ横浜の民 Advent Calendar 2021」
9日目の記事になります.

0. Ukawacoin Wallet

実装したUkawacoinです.
詳細は記事をご覧ください.
アドレスを教えてもらえれば送金します.
ぜひ使ってみてください.

Ukawacoin wallet1
Ukawacoin wallet2


1. はじめに

近頃, Bitcoinをはじめとした暗号資産の高騰やNFTアートが6900万$という高値で落札されたなどのニュースを背景にブロックチェーンへの注目が以前よりも高まっているように思える.
また, ブロックチェーン自体もLightning NetworkやPoS等の省エネルギーな合意方式の導入など実用性の改善が日々進められ, 様々な分野での応用が期待されている.
私は昔から暗号資産の取引やNFTゲームなどを楽しんできたが, その根底にあるブロックチェーンについては概念レベルの理解しかできていなかった.
トレンドのこの技術について今さらながら教養として, また, 将来に向けてお勉強しようと思った次第である.
この記事では簡易的なブロックチェーン(Blockchain)によるオリジナル暗号通貨を実装するとともに, コードレベルで主要なアルゴリズムを紹介する.


2. ブロックチェーンとは

ブロックチェーンとはデータベース技術の一種である.
ブロック単位で情報を記録し, 時系列に沿ってブロックが新たに生成される際には一つ前のブロックの内容がハッシュ値として格納される.
ハッシュ値は任意のビット列から規則性のない固定長のビット列を生成するハッシュ計算により得られる.
ハッシュ値を得ることは容易であり, 同じ入力値からは同じ結果が出力される.
しかし, ハッシュ値から元の入力値を予測することは非常に困難である.
このため過去のデータを改竄しようと試みる際は変更を加えたブロック以降のハッシュ値を計算し直す必要がある.
また, ブロックチェーンはネットワークに参加する全ての人が同一の記録を共有する分散型の台帳により管理されるため後続する全てのブロックが間違いであることを証明するのは難しい.
ブロックチェーンと一口に言っても現在は多くのモデル(パブリック型, コンソーシアム型, プライベート型 etc…)が提唱されているがいずれも対改竄性, 透明性, 追跡可能性などの特徴が見られる.
この技術を基盤とし, ユーザ間で取引履歴を記録する暗号通貨や契約や取引などを自動化するスマートコントラクトなどが成立しているのである.

前置きが長くなったがここまでがよくある概念的な説明だ.
ここからは実装を交えて説明する.
今回は元祖とも言えるBitcoinのブロックチェーンに倣い, オリジナル暗号通貨のUkawacoinを作成する.


3. Ukawacoinの実装

※現時点でUkawacoinは開発版, 勉強用であり販売目的はない.

~ブロックとハッシュ~

BitcoinはC++で作成されているが, 今回はPythonを使用する.
ハッシュ関数や公開鍵暗号署名など必要なライブラリが充実しているためお試しに良い.

まずはハッシュ値で繋がれたブロックデータ構造ついて

class BlockChain():
    def __init__(self):
        self.transactions = []
        self.block_chain = []   # ブロックチェーンデータ

    def _create_block(self, nonce, pre_hash):
        # ブロックに情報を格納しチェーンへ追加
        block = OrderedDict()
        block = {
            "timestamp": time.time(),
            "transactions": self.transactions,
            "nonce": nonce,
            "pre_hash": pre_hash,
        }
        self.block_chain.append(block)
        self.transactions = []
        return block

ブロックは4つの情報を格納している.

  1. タイムスタンプ
  2. 取引履歴
  3. nonce
  4. 直前のブロックのハッシュ値

Bitcoinでは約10分に一つ新たなブロックが生成されるよう調整がされている.
タイムスタンプはブロックが生成された時間である.
取引履歴はそのブロック内でのユーザ達の通貨送受信情報の記録である.
nonceはNumber used onceの略でそのブロックのマイニング時に発見されたランダムな32ビットの値である(詳細は後ほど).
重要なのが直前のブロックのハッシュ値である.
以下のようにブロック生成時に直前のブロックのハッシュ値を格納することでチェーンのように全てのブロックが繋がる.

    def _get_block_hash(self, block):
        # ブロックデータをSHA-256によりハッシュ化し返す
        sorted_block = json.dumps(block, sort_keys=True)
        return hashlib.sha256(sorted_block.encode()).hexdigest()

    def sample_connect_block(self):
        # ブロック生成
        self._create_block(nonce, pre_hash=self._get_block_hash(self.block_chain[-1]))

BitcoinではSHA-256というハッシュ関数が使用されている.
Secure Hash Algorithm 256-bitの略であり, その名の通り32bit長のハッシュ値が得られる.

~マイニング~

続いては, コンセンサスアルゴリズムの一種であるProof of Work:PoWについて
BitcoinではPoWを利用しブロックの生成及び結合作業であるマイニングを行なっている.
具体的にはマイナーは直前のブロックのハッシュ値にnonce加え, 出力されたハッシュ値の先頭に0が一定数並んでいるかを確認する.
もし0が一定数以上並ぶnonceを発見できればブロックが生成される.
必要な0の個数はdifficultyと呼ばれ, これによりブロック生成時間が調整される.
生成されたブロックは分散された他ノードによって正当性が検証され, 有効であれば他ノードのチェーンへ追加される.
そしてマイナーには報酬として通貨が発行されて与えられる.
BitcoinのPoWには問題があることも知られている.
計算力の半分を取られた場合データが改竄される51%攻撃や, 1MBというブロックサイズのために1秒に6~7件しか取引を処理できないスケーラビリティ問題, ハッシュ計算には高い演算能力をもつASIC, FPGA, GPUをいくつも投入する必要があり膨大な電力が消費される問題などである.
こうした問題を背景に様々なアルトコインと呼ばれる暗号通貨がこれまで誕生してきた.
例えばBitcoin Cashはブロックサイズが32MBに設定され, 多くの取引を一度に処理することが可能である.
また, Proof of Stake:PoSというコンセンサスアルゴリズムを採用する暗号通貨も増えてきた.
PoSではPoWが計算量(仕事)に対し報酬が与えられるのに対し, 通貨の保有量や保有年数(投資)に応じて報酬が与えられる.
通貨の流動性が落ちるなどの課題はあるものの51%攻撃や電力消費問題の面で利点があると言える.
有名どころというとEthereumはPoWからPoSへと移行が予定されている.

さて, これらの簡易な実装は以下のようになる.

    def _check_block_valid(
        self, transactions, pre_hash, nonce, difficulty=MINING_DIFFICULTY
    ):
        # ブロックのnonceがdifficultyの条件を満たす妥当なものか確認
        guess_block = OrderedDict()
        guess_block = {
            "transactions": transactions,
            "nonce": nonce,
            "pre_hash": pre_hash,
        }
        guess_hash = self._get_block_hash(guess_block)
        return guess_hash[:difficulty] == "0" * difficulty

    def _proof_of_work(self):
        # 適当なnonceが発見されるまで総当たりで計算
        transactions = self.transactions.copy()
        pre_hash = self._get_block_hash(self.block_chain[-1])
        nonce = 0
        while self._check_block_valid(transactions, pre_hash, nonce) is False:
            nonce += 1
        print("SUCCESS MINING")
        return nonce

    def _mining(self):
        # マイナーへの報酬を取引履歴に加えマイニング
        self._add_transaction(
            sender_address=MINING_SENDER,
            recipient_address=self.blockchain_address,
            value=MINING_REWARD,
        )
        nonce = self._proof_of_work()
        pre_hash = self._get_block_hash(self.block_chain[-1])
        self._create_block(nonce, pre_hash)
        return True

Bitcoinでは14日に一度自動的にdifficulty調整が行われるが, 今回はマシンパワーをそこまで使用したくなかったためdifficultyを固定し, マイニングが10分間隔で行われるなんちゃって実装をした.
報酬は “1.0 UKC” である.
また, ノード間での正当性検証については後ほど実装する.

~ブロックチェーンアドレスと署名~

暗号通貨の実態は通貨の取引情報記録である.
AさんがBさんへX量の通貨を送信したという記録の存在がBさんがX量のお金を保有を意味する.
では, 誰でも好き勝手に取引情報を記録し, 資産を増やすことができるのであろうか?それはできない.
ブロックチェーンでは署名という所有者であることを証明する数学的なメカニズムを利用することで, 署名が一致する所有情報のみ書き換えが可能である.

署名を用いた暗号通貨の取引には公開鍵, 秘密鍵, アドレスが必要となる.
この3つの情報を合わせてWallet呼ぶ.
公開鍵, 秘密鍵は公開鍵暗号方式によって生成され, アドレスは公開鍵から作成される.
アドレスの作成方法は通貨の種類によって異なり, 差別化に繋がっている.
例えば Bitcoinでは以下のプロセスによってアドレスが生成される.

  1. ECDSA (楕円曲線電子署名アルゴリズム)によって秘密鍵のペアとなる公開鍵を作成
  2. 公開鍵をハッシュ関数SHA-256に通す
  3. ハッシュ関数RIPEMED-160に通す
  4. 先頭にプレフィックスとして00を加える
  5. ハッシュ関数SHA-256pに2回通す
  6. 4bytesのチェックサムを後ろに加える
  7. Base58でエンコード

今回は同じ方式でアドレスを生成する.
hashlibやecdsaライブラリによって簡単に実装できる.

class Wallet():
    def __init__(self):
        self._private_key = SigningKey.generate(curve=NIST256p)
        self._public_key = self._private_key.get_verifying_key()
        self._blockchain_address = self._generate_blockchain_address()

    def _generate_blockchain_address(self):
        public_key_bytes = self._public_key.to_string()
        # 公開鍵をハッシュ関数SHA-256に通す
        sha256_bpk = hashlib.sha256(public_key_bytes)
        sha256_bpk_digest = sha256_bpk.digest()

        # ハッシュ関数RIPEMED-160に通す
        ripemed160_bpk = hashlib.new("ripemd160")
        ripemed160_bpk.update(sha256_bpk_digest)
        ripemed160_bpk_digest = ripemed160_bpk.digest()
        ripemed160_bpk_hex = codecs.encode(ripemed160_bpk_digest, "hex")

        # 先頭にプレフィックスとして00を加える
        network_byte = b"00"
        network_bitcoin_public_key = network_byte + ripemed160_bpk_hex
        network_bitcoin_public_key_bytes = codecs.decode(
            network_bitcoin_public_key, "hex"
        )

        # ハッシュ関数SHA-256pに2回通す
        sha256_bpk = hashlib.sha256(network_bitcoin_public_key_bytes)
        sha256_bpk_digest = sha256_bpk.digest()
        sha256_2_nbpk = hashlib.sha256(sha256_bpk_digest)
        sha256_2_nbpk_digest = sha256_2_nbpk.digest()
        sha256_hex = codecs.encode(sha256_2_nbpk_digest, "hex")

        # 4bytesのチェックサムを後ろに加える
        checksum = sha256_hex[:8]
        address_hex = (network_bitcoin_public_key + checksum).decode("utf-8")

        # Base58でエンコード
        blockchain_address = base58.b58encode(address_hex).decode("utf-8")
        return blockchain_address

取引情報記録と署名について
ecdsaライブラリを使い, Transactionオブジェクトに渡された取引情報に署名をしている.

class Transaction():
    def __init__(
        self,
        sender_private_key,
        sender_public_key,
        sender_address,
        recipient_address,
        value,
    ):
        self.sender_private_key = sender_private_key
        self.sender_public_key = sender_public_key
        self.sender_address = sender_address
        self.recipient_address = recipient_address
        self.value = value

    def generate_signature(self):
        # 取引情報に秘密鍵を用いて署名を行う
        sha256 = hashlib.sha256()
        transaction = OrderedDict()
        transaction = {
            "sender_address": self.sender_address,
            "recipient_address": self.recipient_address,
            "value": float(self.value),
        }
        sha256.update(str(transaction).encode("utf-8"))
        message = sha256.digest()
        private_key = SigningKey.from_string(
            bytes().fromhex(self.sender_private_key), curve=NIST256p
        )
        private_key_sign = private_key.sign(message)
        signature = private_key_sign.hex()
        return signature

署名の検証とブロックへの取引情報の追加について
先ほど同様にecdsaライブラリを使い, 署名を検証し正しい場合のみ取引情報プールへ取引を追加している.

    def _verify_signature(self, sender_public_key, signature, transaction):
        # 取引情報の署名を公開鍵で検証
        sha256 = hashlib.sha256()
        sha256.update(str(transaction).encode("utf-8"))
        message = sha256.digest()
        signature_bytes = bytes().fromhex(signature)
        verifying_key = VerifyingKey.from_string(
            bytes().fromhex(sender_public_key), curve=NIST256p
        )
        verified_key = verifying_key.verify(signature_bytes, message)
        return verified_key

    def _add_transaction(
        self,
        sender_address,
        recipient_address,
        value,
        sender_public_key=None,
        signature=None,
    ):
        transaction = OrderedDict()
        transaction = {
            "sender_address": sender_address,
            "recipient_address": recipient_address,
            "value": float(value),
        }
        if sender_address == MINING_SENDER:
            # マイニング報酬は署名検証無視
            self.transactions.append(transaction)
            return True
        if self._verify_signature(sender_public_key, signature, transaction):
            # 署名検証が正しい場合は取引情報を取引情報プールへ追加
            if self.calculate_total_amount(sender_address) < float(value):
                # 残高不足は取引不成立
                return False
            self.transactions.append(transaction)
            return True
        return False

ここまでの実装で署名を用いた取引情報の記録とPoWを利用したマイニングが実現できた.
ここからはブロックチェーンの管理とマイニングを行うノードを立ち上げネットワークを構成する.

~ブロックチェーンネットワーク~

先ほど述べたようにブロックチェーンは分散型の台帳により管理されるため各ノード及びそのネットワークを構築する必要がある.
今回, PythonのWebフレームワークFlaskにより作成したアプリをHerokuによりデプロイしネットワークを構築した.
UkawacoinのマイニングはCPUで可能であることと, ノードを増やしやすいとのことでPaaSを利用した.
本当はAWSを使いS3などにブロックチェーンデータを入れたかったが, 恒久的な無料枠の範囲では難しかったためHerokuを使わせてもらった.
Herokuの無料枠では使用量の制限やメンテナンス等で落ちることも多々あるが, 分散ノードなためどれかが無事であればUkawacoinは生きることができる.

アプリケーション実装の細かな説明は省くが, 今回は以下のような分散型のネットワークを構成した.
ブロックチェーンの管理とマイニングを行うUkawacoinサーバはP2Pでそれぞれ接続される.
ユーザにWalletを提供するインタフェースとなるWalletサーバは任意のUkawacoinサーバへと接続される.


各ノード間のブロックの検証及び同期について 
あるノードから見て他ノードのブロックチェーンが長い場合はその正当性が検証される.
そして問題がない場合は自身のブロックチェーンへとブロックを追加する.
これにより不正なブロックを除くことやノード間のデータ同期が可能となっている.

    def _valid_chain(self, chain):
        # nonceが正しいものであるかを確認しチェーンの正当性を検証
        pre_block = chain[0]
        current_index = 1
        while current_index < len(chain):
            block = chain[current_index]
            if block["pre_hash"] != self._get_block_hash(pre_block):
                return False
            if not self._check_block_valid(
                block["transactions"],
                block["pre_hash"],
                block["nonce"],
                MINING_DIFFICULTY,
            ):
                return False
            pre_block = block
            current_index += 1
        return True

    def resolve_conflicts(self):
        # 正当なチェーンでかつ自身が持つものより長ければ追加
        longest_chain = None
        max_length = len(self.block_chain)
        for node in self.neighbours:
            response = requests.get(f"{node}/viewchain")
            if response.status_code == 200:
                response_json = response.json()
                chain = response_json["chain"]
                chain_length = len(chain)
                if chain_length > max_length and self._valid_chain(chain):
                    max_length = chain_length
                    longest_chain = chain
        if longest_chain:
            self.block_chain = longest_chain
            print("find other longest chain, replaced")
            return True
        return False

今回は全てのノードで全てのデータを同期しマイニングまで行っているがBitcoinでは異なる役割を持つノードが存在する.
フルノードと呼ばれるものは要件を満たしている取引情報とブロックを承認している.
ほとんどのフルノードはサトシナカモトがリリースしたビットコインソフトウェアを実行している.
全てのブロックチェーン情報を保有したフルノードはフルアーカイブノードとして扱われる.
マイニングノードと呼ばれるものは文字通りマイニングを行うものである.
ライトノードではブロックの一部であるブロックヘッダのみを使用するものでモバイルWalletなどで使用される.
UkawacoinのWalletサーバ同様, フルノードへ依存する.

ノードを追加する場合はピアとして見つけてもらう必要がある.
BitcoinではDNSシードというBitcoinノードのIPを提供するDNSコンテンツサーバを使用する方法がある.
DNSシードはBitcoinのコミュニティメンバーによって管理され, 動的なシードと静的なシードが提供されている.
Ukawacoinではお試しということもあり, あらかじめ用意した接続候補先へのレスポンスを確認し稼働しているものをピアとして設定する.
もし, ノードを追加する場合は候補先にあるアドレスを使用する必要がある.

def check_internet(url, timeout=5):
    # 稼働し正常なレスポンスがあるかの確認
    try:
        response = urllib.request.urlopen(url).getcode()
        if response == 200:
            return True
        return False
    except urllib.error.HTTPError as ex:
        print(ex)
        return False

def find_neighbours():
    # 稼働しているノードをピアとして追加
    neighbours = []
    candidate_address_list = node_sheet.NODE_LIST
    host_name = node_sheet.MY_NODE
    print("find neighbour node process in ", host_name)
    for guess_address in candidate_address_list:
        if guess_address == host_name:
            continue
        if check_internet(guess_address):
            neighbours.append(guess_address)
    return neighbours

4. Ukawacoin -dev- 完成

Ukawacoinの開発版が完成した.
※以下のリンク情報は執筆時のものであり変更されてる可能性がある

ノードは3つ立ち上げている.

Ukawacoin fullnode
Ukawacoin 0000node
Ukawacoin 0001node

Walletノードは2つ立ち上げている.

Ukawacoin wallet1
Ukawacoin wallet2

役割的には変わらないが’Ukawacoin fullnode’がフルノードのつもりだ.
負荷対策で取引があった場合のみマイニングされるように設定してある.
‘Ukawacoin wallet1’は’Ukawacoin fullnode’と紐付き, ‘Ukawacoin wallet2’は’Ukawacoin 0001node’と紐づいてる.
もちろんどのWalletを使用してもらってもデータは同期されるため問題ない.

Walletのインタフェースについて説明する.
上部には自身のアドレスの保有資産がUKCを単位として表示される.
皆さんがアクセスした場合は”0 UKC”となっているはずだ.
Walletサーバへアクセスするたびにアドレス及び秘密鍵, 公開鍵が作成される.
これらの値は保存されないので手元に保存する必要がある.
下部には送金システムがある.
送金先のアドレスを入力し, 自身の保有資産内の量を入力しSendボタンを押すと送金される.
マイニングが完了し, 取引履歴がブロックチェーンに追加されると残高に反映されるはずだ.


今のところたった一人のマイナーである私が全てのコインを保有している状態だ.
もしUkawacoinが欲しいという人がいれば送金するのでアドレスを教えてもらいたい.
また, 知り合いでマイニングしてみたい人がいればアプリケーションを提供するので耳打ちください.

ちなみにマイニングしているWalletのアドレスが以下だ.
Walletのアドレスにコピーすることで保有量が確認できると思われる.
秘密鍵と公開鍵がないため勝手に送信はできない.

bSLfGSEnuTE8wGDZsKRqVdNEETsrvGW2X44ib97eH12ygGvEid5SeLac2edfZxXhxcJ4

あくまでこのコインはお勉強用であり開発段階だ.
正式なUkawacoinの登場をお待ちあれ.


5. おわり

ブロックチェーン技術をコードレベルで眺めることでモヤモヤしていたところがスッキリ理解できた.
一方, 今回の実装は初期のものであり最近のブロックチェーン技術については触れることができていない.
また, オリジナル暗号通貨では所々実装を省いている.
今後もお勉強を続けて実用レベルのブロックチェーンを作成したいと思う.


6. 参考

Bitcoin関連 https://bitcoin.org/

ブロックチェーン関連 https://academy.binance.com/

ブロックチェーン実装関連 https://www.udemy.com/share/1021OS3@bq1pVkFO6q03gNiNEY4--XDrYVmvl3r3q_VQX8krAe_XLDpun5KgvM6Deur94LdoEQ==/

contact

Please let me know if you need anything.