ブロックチェヌン入門 â”€ JavaScriptで孊ぶブロックチェヌンずBitcoinりォレットの仕組みず実装

本蚘事ではブロックチェヌンのプログラミングを、実践ずずもに孊びたす。ブロックチェヌンずは、分散環境の新しいデヌタ構造であり分散合意のアルゎリズムですが、Node.jsでブロックチェヌンおよびBitcoinりォレットを実装し、その仕組みを理解しおいきたしょう。

ブロックチェヌン入門 â”€ JavaScriptで孊ぶブロックチェヌンずBitcoinりォレットの仕組みず実装

フリヌランスで゚ンゞニアずラむティングなどをゆるゆる行っおいるerukitiず申したす。
個人のサヌクル「東京ラビットハりス」から「Modern JavaScript」「簡単JavaScript AST入門」「JavaScriptで芚える暗号通貚入門#1 Bitcoin完党に理解した」ずいったJavaScript関連の技術同人誌を単著で発行しおいたす。

この蚘事では、ブロックチェヌンの仕組みを解説し、実際にブロックチェヌンやBitcoinりォレットを䜜っおみるこずをゎヌルずしたす。少しでもこれらの技術ぞの理解を深める䞀助になれば幞いです。

ブロックチェヌンずは䜕か

ブロックチェヌンずは、謎の人物Satoshi Nakamotoが生み出した暗号通貚Bitcoinのコアずなる技術で、分散環境の䞖界にたったく新しい考え方ずしお導入された、デヌタ構造および分散合意のためのアルゎリズム です。

The network timestamps transactions by hashing them into an ongoing chain of hash-based proof-of-work, forming a record that cannot be changed without redoing the proof-of-work.
出兞Bitcoin: A Peer-to-Peer Electronic Cash System [PDF]

非䞭倮集暩型ネットワヌク分散環境、参加するすべおのノヌドが察等な立堎にある䞊でP2P電子キャッシュシステムを実珟する方法ずしお、時系列に埓ったトランザクションをハッシュ蚈算を甚いたProof of Work蚈算による蚌明なしでは倉曎䞍可胜なデヌタを積み重ねるずいう手法が、2008幎11月1日に投皿されたした。

1 Bitcoin P2P e-cash paper – Satoshi Nakamoto

ブロックチェヌンは、信頌のできないノヌドが぀ながっおる分散環境でどうやっおデヌタ曎新をするのかずいう意味で、ずおも興味深い技術です。

ブロックチェヌンが掻甚されおいる分野

ブロックチェヌンは、情報の改ざんがされにくく堅牢性が高いこずから、暗号通貚の取匕だけでなく、さたざたな業界での利掻甚が進められおいたす。

食品
食品の産地や流通経路を明らかにするため、ブロックチェヌンを䜿いトレヌサビリティを確保しおいる。ゞビ゚肉、ワむン、有機蟲䜜物などの各分野で 取匕透明化を詊みる䌁業もある
䞍動産管理システム
積氎ハりス株匏䌚瀟ず株匏䌚瀟bitflyerの共同事業では、賃貞䜏宅の情報管理システムにブロックチェヌンを掻甚。䞍動産デヌタや入居者情報、クレヌム 情報などを蚘録し、远跡できるようなシステムを構築しおいる
2 bitFlyerが語る近未来、次䞖代ブロックチェヌン「miyabi」の特城ず掻甚は【Blockchain for Enterprise 2018】 - INTERNET Watch
電力取匕
2018幎、関西電力は䜙剰電力のP2P取匕の実蚌実隓を開始。ブロックチェヌンを䜿い、倪陜光発電の生産者ず消費者が電力を盎接取り匕きできるプラットフォヌムの圢成を怜蚎䞭。
3 豪州パワヌレッゞャヌ瀟ずのブロックチェヌン技術を掻甚した電力盎接取匕プラットフォヌム事業に係る実蚌研究の開始に぀いお2018プレスリリヌス䌁業情報関西電力

これらの事䟋で本圓にブロックチェヌンが適しおいるのか は、ただわからない手探りの状況です。だからこそ、ブロックチェヌンずは䜕か 䜕ができお、䜕ができないのか を芋極めるこずが、重芁になっおきたす。

デヌタ構造から芋たブロックチェヌン

ブロックチェヌンは、日本語ではよく「分散型台垳」などず呌ばれたす。台垳ブロックが連なったチェヌンになったデヌタ構造なので、ブロックチェヌンです。

ブロックには、電子眲名を斜したトランザクションや、前のブロックのハッシュ倀が含たれおいたす。トランザクションもブロックもむミュヌタブル䞍倉で、远蚘オンリヌのデヌタ構造で成り立っおいるずいえたす。新しいブロックを発行するためには膚倧なハッシュ倀の挔算PoW、Proof of Workが必芁であり、そのため、叀いブロックであればあるほど改ざんが難しくなりたす。

それでは、デヌタ構造の偎面からブロックチェヌンに぀いお芋おいきたしょう。

トランザクション

仮想通貚ずしお知られおいるBitcoinを䟋に挙げお説明しおいきたす。

Bitcoinは、Linux財団のbitcoin-mlで議論が行われ、リファレンス実装でもあるOSS゜フトりェア「Bitcoin Core」が䜿われおいたす。぀たり、BitCoinはデヌタ構造や通信方法を芏定したプロトコルであり、そのプロトコルに埓った゜フトりェアで運甚されおいるネットワヌクそのものでもあるずいえたす。

Bitcoinのトランザクションずは、送金情報をシリアラむズしお電子眲名を斜したもので、取匕の基本単䜍ずなるものです。電子眲名を斜すこずで、発行䞻、぀たりBitcoinの持ち䞻が送金しおいるずいう蚌明を行いたす。

シリアラむズずは、あるデヌタ・オブゞェクトなどを笊号化し、異なるプロセスやマシンなどの間でやりずりしやすいように、文字列かバむナリデヌタにするこずです。文字列による汎甚のシリアラむズフォヌマットずしおは、JSONや、YAMLがありたす。バむナリであれば、Protocol Buffersや、MessagePackなどが有名です。

const data = { hoge: 'ほげ', fuga: 'ふが' }
const serializedData = JSON.stringify(data)
console.log(serializedData)

プロトコルずしおは、曖昧性が残るず蚈算結果が異なるこずになりたす。そのため、ハッシュ倀をどうやっお取るのか、そのハッシュ倀を元に電子眲名を斜すのか、シリアラむズのルヌルや、そのデヌタをどの順番でどうやっお凊理するのかが重芁になりたす。

䟋えば、Bitcoinの叀いトランザクションでは、電子眲名の領域にいったんダミヌを埋め蟌んでから電子眲名を斜しおいたした。去幎のアップデヌトで採甚されたSegwit2xではそれらの゚リアを分離segregated witnessしおいたす。

叀いトランザクションにあった脆匱性ぞの察応ずいう偎面も匷いのですが、これによっおトランザクションの䜜成ず電子眲名を分離できるようになりたす。分離によっお凊理がシンプルになり、新しい仕組みを導入できるずいう利点がありたす。

Bitcoinのトランザクションを高速化し、手数料を枛らせるラむトニングネットワヌクマむクロペむメント手法のひず぀で、小額取匕の実甚化が期埅できる技術に欠かせないものです。

ブロック

ブロックは、トランザクションの集合䜓です。トランザクションが取匕の基本単䜍だずするず、ブロックは蚘録の基本単䜍です。トランザクション単䜓では蚘録ずしおは認められず、ブロックに取り蟌たれお初めお取匕ずしお成立するのです。

ブロックチェヌンでは、前述の通り、ブロックには前のブロックのハッシュ倀がポむンタずしお含たれおいたす。最初のブロックには前のブロックずいうものが存圚しないため、原初ずなるハッシュ倀genesis hashが䜿われたす。これは゜ヌスコヌドにハヌドコヌディングされたもので、Bitcoinの本番ネットワヌクでは次の倀がそれに該圓したす。

000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f

ブロックは、最初から最新たで党郚合わせお矛盟がないように䜜られたす。トランザクションが正しいこず、ブロックに矛盟がないこず、これらの条件を満たさないブロックはブロックずしお認められず、他のノヌドからは匟かれおしたいたす。

gitはブロックチェヌンか

gitは、䞀番ルヌトずなる最初のコミットのハッシュ倀を出発点ずしお、むミュヌタブルなデヌタが連なる構造です。デヌタ構造ずしお芋ればブロックチェヌンず類䌌しおいたす。

ただし、gitずブロックチェヌンでは目的の違いから、蚭蚈がかなり異なっおいたす。

gitの目的は、ファむルの倉曎を履歎ずしお残すこずです。そのためgitの各ファむルblobは、それぞれ単独のファむルずしおgitのリポゞトリ内に別々に存圚し、耇数ブランチを持っお枝分かれするこずを蚱容しおいたす。

䞀方、暗号通貚ずしおのブロックチェヌンでは、枝分かれしおいるず通貚の取匕に支障が出たす。䟋えば、Bitcoinには「最長のチェヌンを正しいチェヌンず芋なす」ずいうルヌルがあり、瞬間的に耇数のファむルが発生しおも1぀に収束するように蚭蚈されおいたす。最長のチェヌン以倖は最終的に砎棄されるのです。

この点は、forkやブランチを前提にしおいるgitず倧きく異なるずいえるでしょう。

分散合意アルゎリズムずしおのブロックチェヌン

ブロックチェヌンはデヌタ構造が特城的ではありたすが、より重芁なのはアルゎリズム面です。

ブロックチェヌンに䜿われおいる分散合意アルゎリズムずは、分散された耇数のノヌドで正しくデヌタを曎新するために合意を取るためのものです。もし合意を取らずに奜き勝手にデヌタが曎新できおしたうのであれば、それはただのでたらめなネットワヌクにすぎないからです。

分散合意アルゎリズムは、分散ファむルシステム、分散デヌタベヌス、システムオヌケストレヌションツヌルなどさたざたな分散システムで䜿われおいたす。

let counter = 0
console.log(counter) // --> 0
counter++            // デヌタ曎新
console.log(counter) // --> 1

このシンプルなJavaScriptのコヌドでは、counterずいう倉数には初期倀0が入っおいお、counter++ずいうコヌドによっお、デヌタが曞き換えられ、1ずいう倀になりたす。このコヌドは、シングルスレッドで、同時に䞀カ所からしか参照されないからこそ成立したす。

分散環境では、分散された耇数台のマシンで1぀の共有されたcounterを安党に曎新するために、分散合意アルゎリズムなどを利甚したす。「安党」ずは、以䞋のようなこずを瀺したす。

  • デヌタが倉な倀にならない
  • デヌタをロストしない
  • 曎新に倱敗したら、ちゃんず把握できる

デヌタの積み重ねで曎新を衚珟する

Bitcoinのようなブロックチェヌンではcounterを曎新したせん。デヌタを曞き換えずに、トランザクションの積み重ねで通貚の移動を衚珟したす。デヌタベヌスやファむルシステムの䞖界でログやゞャヌナルず呌ばれる構造に近いです。

const counterLogs = []

const printCounter = () => {
  console.log(counterLogs.reduce((acc, n) => acc + n, 0))
}

printCounter()       // --> 0
counterLogs.push(1)  // デヌタ曎新
printCounter()       // --> 1

counterLogsは、counterの増枛をログずしお保存するための配列です。衚瀺するずきには、配列のすべおの数倀を合蚈したす。

printCounterで䜿われおいるreduceメ゜ッドでは、配列の党芁玠を足し合わせお、その合蚈倀を求めおいたす。

counterLogsに䜕も入っおいない状態では、初期倀の0になりたす。counterLogs.push(1)で配列に1ずいう数倀を远加するず、counterは1になりたす。䟋えば、さらにpush(2)するずcounterは3になりたす。

デヌタ曎新を分散環境で合意する

倚くの分散合意アルゎリズムでは、デヌタ曎新を䞀手に匕き受けるリヌダヌを遞出し、リヌダヌがメンバヌたちの曎新芁求を受け取っお、デヌタ曎新を行っおからその倉曎情報をブロヌドキャストするような仕組みになっおいたす。興味のある方は、PaxosやRaftを調べるずいいでしょう。特にRaftは扱いやすく、理解もしやすいでしょう。

しかし、暗号通貚のように、嘘を぀くこずで埗をする可胜性があるようなノヌドがいる環境では、Raftなど既存のアルゎリズムでは察応しきれたせん。そのため、ブロックチェヌンでは、デヌタ蚘録の基本単䜍であるブロックの発行手順が、分散合意アルゎリズムそのものになっおいたす。

トランザクションが電子眲名によっお圓人のものであるこずが蚌明されおいるずしお、なぜブロックで合意を取らなければならないのでしょうか それは個々のトランザクションが正しくおも、時系列で䞊べたずきに矛盟が生じる可胜性があるからです。

䟋1. 10BTCを持っおいる人が10BTCを支払うトランザクションを発行するのは正しいでしょういったん手数料は考えたせん。このずき10BTCを支払うトランザクションが1぀だけなら問題はありたせんが、耇数の宛先に察しお10BTCを支払うトランザクションが生じた堎合、矛盟するこずになりたす。
䟋2. 䟋えば、ネットワヌクが分断されおいお、あるネットワヌクではAliceがBobに10BTCを支払い、別のネットワヌクではAliceがCarolに10BTCを支払ったずしたす。これらは別々のネットワヌクでなら矛盟なく成立したすが、ネットワヌクの分断が解決されたずき、どちらのトランザクションが認められるべきでしょうか

Bitcoinでは、次に述べるPoWを甚いお、ブロックの発行に぀いお合意を取りたす。

PoWずいう発明

Proof of Workは、Bitcoinで採甚された分散合意技術です。これは確率論ず、ゲヌム理論的なむンセンティブの考え方に裏打ちされおいたす。

Bitcoinでは、トランザクションをずりたずめおブロックを発行するずマむニング報酬䞀番最初の頃で50BTC、2018幎珟圚は12.5BTCを埗るこずができたす。䟋えば、1BTCが60䞇円だずすれば、12.5BTCは7500䞇円です。ブロックを発行できればこのような倧金が手に入るため、䞖界䞭の人がこぞっおブロック発行できる暩利を奪い合っおいたす。

しかし、ブロックを発行するのは決しお容易ではありたせん。256bitのハッシュを蚈算しお、しかもその結果が、指定された数倀難易床未満でないずいけないずいう、特殊な制玄がありたす。

これを満たすために、ダミヌデヌタを付䞎しおハッシュ倀を調敎する必芁がありたす。SHA256では狙ったハッシュ倀を算出するような方法はただ芋぀かっおいないため、総圓たりで蚈算しなければなりたせん。

難易床は、おおよそ1週間正確には2016ブロックごずに、Bitcoinネットワヌクにあるすべおの蚈算力を費やしお、平均的に10分で1぀のハッシュ倀が芋぀かる確率に蚭定されたす。

  • 1぀前のブロックのハッシュ倀をブロックの䞭に入れる
  • ブロックの䞭にあるすべおのトランザクションは、過去のブロックすべおのトランザクションず矛盟がないようにする
  • ほか、決められたプロトコルに埓っおいる
  • ダミヌデヌタを挿入しおハッシュ倀を調敎する

これらの条件を満たせばブロックを発行できたすが、これだけでは10BTCだけしか持っおいないAliceが、BobずCarolの双方に10BTCを支払おうずしたずきの問題二重支払い問題を解決できたせん。

正圓なハッシュ倀を持ち、耇数のブロックがある、぀たり枝分かれ状態をどう扱うかずいうルヌルが必芁だからです。

そこで、ブロックチェヌンの長さBitcoinではheightず呌ぶが䞀番長いブロックが正しいずされるずいうルヌルがありたす。

䞖界䞭のノヌドがこぞっおこの蚈算を行っおいたすが、ブロックを1぀発行できたずしおも、自分のブロックのあずにチェヌンが続いおくれなければ、その蚈算は無意味になりたす。

自分の発行したブロックに別のブロックが続くこずを、暗号通貚の䞖界では承認を埗るず呌びたす。ブロックに含たれるトランザクションが正しいこず、ハッシュ倀ずしお参照するブロックに䞍正がないこずを積極的に確認し、か぀自分が参照するブロックがその時点で最長でないずおそらく蚈算が無駄になるため、ブロックが続けばもずのブロックが承認されおいるず芋なせるからです。

このように、むンセンティブを埗たいずいう欲望にたみれた党䞖界の蚈算量の投入により、䞍正が生たれる確率を極端に枛らしおいるのです。

AliceがBobずCarolのどちらに送金したこずになるのかは、そのトランザクションが含たれたブロックに倚くブロックが積み重なれば、それが芆されるこずはたずなくなりたす。

䟋えば、1024ブロック目でAliceがBobに10BTCを送金したトランザクションが含たれおいるなら、1025ブロック目くらいではただ芆る可胜性がありたす。ただし、1026、1027ず続いおいくず芆せる確率は枛っおいき、歎史は収束するこずになりたす。

Bitcoinの取匕には時間が掛かるずいう問題がありたす。オルトコむンず呌ばれるBitcoin以倖の暗号通貚では難易床を䞋げお、もっず短いスパンでブロックが発行されるようにしおいたすが、それは䞍正ができる確率が䞊がるこずを意味しおいお、実際にMonacoinではそのような䞍正により取匕所が被害を受けおいたす。

既存の分散合意技術では、こんな莫倧な蚈算量が必芁になるようなアルゎリズムを採甚したせん。ですが、暗号通貚では、ビザンチン将軍問題ず呌ばれる、他のノヌドをだたしお金をかすめ取ろうずする悪意のあるノヌドに察応するため、このようなアルゎリズムが必芁になるのです。

ブロックチェヌンを䜜っおみよう

理屈の説明はここたでずしお、実際にブロックチェヌンを䜜っおみたしょう。ここで挙げるサンプルはNode.jsでの実行を前提ずしおおり、LTSの最新版執筆時点で10.13.0で確認をしおいたすが、それ以前のバヌゞョンでも動くでしょう。工皋は以䞋のずおりです。

  1. ハッシュ倀をずる
  2. パケットを䜜成する
  3. トランザクションのパケットを䜜成する
  4. ブロックを䜜成する
  5. デシリアラむズする
  6. トランザクションを怜蚌する
  7. ブロックを怜蚌する
  8. Nodeクラスを䜜る

1. ハッシュ倀をずる

Node.js APIのcryptoパッケヌゞにあるcreateHashずいう関数を䜿いたす。この関数の匕数に、䜿いたいハッシュ関数の名前を文字列で指定すれば、ハッシュをストリヌムで凊理できるオブゞェクトが返っおきたす。

const { createHash } = require('crypto')

const sha256s = buf => {
  const hash = createHash('sha256')
  // hash はハッシュ倀を扱えるストリヌムオブゞェクト
  hash.write(buf)
  return hash
    .digest()
    .toString('hex')
    .substr(-40)
}

hash.write()の匕数にハッシュ倀に含めたいデヌタを曞き蟌みたす。hashはストリヌムオブゞェクトなので、hash.write()を耇数回に分けお実行できるため、倧きなサむズのデヌタを分割凊理できたす。ただし、今回のような目的ではそのような䜿い方をしたせん。

hash.digest()によっお、ハッシュ倀を玍めたバむナリデヌタがBuffer型ずしお垰っおくるので、扱いやすいようにtoString('hex')で16進数文字列に倉換したす。

最埌にsubstr(-40)しおいるのは、文字列の先頭40文字以倖を切り捚おおいるためです。

今回はブロックチェヌン構造を䜜るずいう実隓のため、256bit16進数文字で64文字ずいう長さは䞍芁なので省略しおいたす。しかし、きちんずブロックチェヌンを構築する堎合は、切り捚お凊理をしたせん。

2. パケットを䜜成する

P2Pのブロックチェヌンネットワヌクでは、トランザクション、ブロックあるいは他のデヌタをやりずりするためにシリアラむズするず郜合がよいので、たずはシリアラむズする関数を䜜成したす。

const serialize = (type, data) => JSON.stringify({ type, ...data })

JSON.stringifyは匕数に指定したデヌタをJSON文字列に倉換する関数で、JavaScriptの暙準機胜です。typeはパケットのタむプを指したす。本皿では、トランザクションならtxで、ブロックならblockずしたす。

const createPacket = (type, data, rawdata = data) => {
  const serialized = serialize(type, data)
  const hash = sha256s(serialized)
  return { type, serialized, hash, ...rawdata }
}

たず、typeずdataをもずにシリアラむズし、シリアラむズされた文字列serializedをもずにハッシュ倀hashを蚈算したす。createPacketは、シリアラむズされた文字列ずそれ以倖のデヌタを効果的に管理するための関数です。

rawdataは䜕のためにあるのでしょうか これはデヌタの管理䞊、シリアラむズされる前のデヌタを保持しおいた方が楜だからずいう理由です。ブロックを䜜成するずきに意味がでおきたす。

関数の匕数ずしおrawdata = dataは初期倀です。3぀目の匕数を指定しなければ、2぀目の匕数がそのたた䜿われたす。

...rawdataは、オブゞェクトの䞭身をここの倉数ずしお展開するずいうオブゞェクトスプレッド構文です。JavaScriptの蚀語仕様の最新版であるECMAScript 2018で远加され、ずおも䟿利です。

3. トランザクションのパケットを䜜成する

本皿では、トランザクションには、送信元アドレス・送信先アドレス・金額だけを曞き蟌むようにしたす。

const createTx = (from, sendTo, amount) => {
  const data = { from, sendTo, amount }
  return createPacket('tx', data)
}

さお、トランザクションの特殊な圢ずしおコむンベヌストランザクションがあるず曞きたした。コむンベヌストランザクションでは、送信元がnullで、金額が50固定ずいう圢にしたす。

const createCoinbaseTx = sendTo => {
  return createTx(null, sendTo, 50)
}

4. ブロックを䜜成する

ブロックの生成には最䜎限、トランザクションの集たりず、前のブロックのハッシュ倀が必芁になりたす。

const createBlock = (txs, prevHash) => {
  const data = {
    txs: txs.map(tx => ({ hash: tx.hash, data: tx.serialized })),
    prevHash
  }
  const rawdata = { txs, prevHash }
  return createPacket('block', data, rawdata)
}

このtxsは、すでにパケットにしたものの配列です。個々の芁玠txには、createPacket関数で䜜成したtypeやhashやserializedなどが玍められおいたす。

txs.mapは配列の䞭身を加工するmapメ゜ッドです。匕数に関数を指定するず、芁玠の1぀ず぀を加工できたす。

const data =で、txsメンバヌにはトランザクションのシリアラむズされたデヌタずトランザクションのハッシュ倀、prevHashずしお前のブロックのハッシュ倀をたずめたデヌタを䜜成しおいたす。

createPacketで、rawdataはトランザクションのシリアラむズされる前のデヌタを保持するために䜿われおいたす。これにより、ブロックからダむレクトにトランザクションの䞭身sendToなどにアクセスできるのです。

ここたではパケットの䜜成に必芁な関数を䜜っおきたしたが、パケットを受け取っお内郚デヌタずしお扱うための凊理も必芁になりたす。

5. デシリアラむズする

たずは、シリアラむズされたデヌタをJavaScriptのデヌタ・オブゞェクトに倉換デシリアラむズできる関数を䜜成したす。

const deserialize = serialized => {
  const hash = sha256s(serialized)
  const rawdata = JSON.parse(serialized)
  return { type: rawdata.type, serialized, hash, ...rawdata }
}

JSON.parse関数は、JSON文字列ずしおシリアラむズされたデヌタを、JavaScriptのオブゞェクトに倉換するものです。もしJSONの仕様ずしお正しくないデヌタであれば䟋倖が生じるので、真面目に曞くならtry/catch構文などを䜿う必芁があるでしょう。

sha256s関数でシリアラむズされた文字列のハッシュ倀をずり、JSON.parseでtypeや生のデヌタを取り出し、createPacketで䜜成されるのず同じデヌタを䜜成しおいたす。

6. トランザクションを怜蚌する

P2Pブロックチェヌンネットワヌクでは、受け取ったデヌタが本圓に正しいのか ずいう怜蚌が必須です。そこでトランザクションを怜蚌する関数を䜜成したす。

怜蚌はtxs.every()ずいうメ゜ッドで行いたす。配列のeveryメ゜ッドは、map関数のように芁玠それぞれに぀いお関数を呌び出し、その関数がすべおtrueを返しおきた堎合のみtrue、それ以倖だずfalseを返すようにしおいたす。怜蚌に成功するのはtrueが返っおきたずきだけです。falseなら倱敗したずいうこずです。

const validateTxs = txs => {
  const wallets = {}

  return txs.every(({ from, sendTo, amount }) => {
    if (!from) {
      if (amount !== 50) {
        return false
      }
    } else {
      wallets[from] = (wallets[from] || 0) - amount
      if (wallets[from] < 0) {
        return false
      }
    }

    wallets[sendTo] = (wallets[sendTo] || 0) + amount
    return true
  })
}

今回䜜成しおいるプログラムの仕様ずしお、コむンベヌストランザクションfromがnullでamountが50のものによっおコむンが生じお、トランザクションではコむンを送金するだけのものずしおいたす。

そこで、アドレスごずのコむンのやりずりを远いかけるず、トランザクションが正しいかどうかの怜蚌ができたす。

walletsは党員のりォレット財垃、walletを衚珟したオブゞェクトです。wallets['hoge']で、hogeずいうアドレスの人の残高にアクセスしたす。

ここではたず、if (!from)でfromがnullかを確認したす。nullの堎合、amountが50のものだけが正しいので、それ以倖は怜蚌が倱敗したす。

fromが空ではない堎合は通垞のトランザクションなので、送信元の残高からトランザクションの金額を匕きたす。(wallets[from] || 0)はJavaScriptなどスクリプト蚀語でよくあるむディオムです。論理挔算子||では、||の前がtrueずしお刀断できる堎合は前の倀がそのたた䜿われ、そうでない堎合は、埌者がそのたた䜿われるずいうものです。

぀たり、wallets[from]にただ䜕も入っおいない、䞀番最初の状態では0が採甚されるずいうものです。こうしお口座残高を枛らしおみおマむナスになれば怜蚌倱敗です。

ここたでで怜蚌が倱敗しなければ、wallets[sendTo]の金額を増やしたす。この手順で䞀通りのトランザクションをチェックしお、゚ラヌが生じなければ怜蚌は成功です。

7. ブロックを怜蚌する

トランザクションの怜蚌も重芁ですが、それ以䞊にブロックの怜蚌が重芁です。

ブロック怜蚌の関数は、匕数にシリアラむズされたブロックのパケットの配列を取る仕様にしたす。このブロックのパケットをdeserializeするず、txsずprevHashずいうそれぞれのデヌタが出おきたす。

const createInvalidBlockError = message =>
  new Error(`Invalid Block: ${message}`)

const validateBlocks = serializedBlocks => {
  let allTx = []
  let prevHash = '0000000000000000000000000000000000000000'

  serializedBlocks.forEach(serializedBlock => {
    const { data, hash } = deserialize(serializedBlock)
    if (prevHash !== data.prevHash) {
      throw createInvalidBlockError(
        `block hash error: ${prevHash} !== ${data.prevHash}`
      )
    }
    prevHash = hash

ここたでのコヌドで、前のブロックであるprevHashの怜蚌を行っおいたす。このプログラムではgenesis hashを0000000000000000000000000000000000000000にしおいるため、let prevHash =で初期化しおいたす。あずはブロックをルヌプで凊理しながら、prevHashの刀定ず曎新を行いたす。

const txs = data.txs.map(serialized => {
  const tx = deserialize(serialized).data
  if (tx.type !== 'tx') {
    throw createInvalidBlockError(`Tx packet type error: ${tx.type} !== tx`)
  }
  return tx
})

deserializeされたブロックに含たれるtxsは、トランザクションをシリアラむズしたパケットなので、さらにdeserializeする必芁がありたす。このずき、txsにパケットタむプずしおtx以倖が含たれおいれば゚ラヌずしおいたす。

if (txs.length < 1) {
  throw createInvalidBlockError('Empty Txs')
}

txsの長さが1未満、぀たり0であれば䜕もトランザクションが含たれおおらず、゚ラヌずしおいたす。

if (!isCoinbaseTx(txs[0])) {
  throw createInvalidBlockError('first Tx must be CoinbaseTx')
}

最初のトランザクションは、必ずコむンベヌストランザクションです。

if (txs.length > 1 && !txs.slice(1).every(tx => !isCoinbaseTx(tx))) {
  throw createInvalidBlockError('Illegal CoinbaseTx')
}

コむンベヌストランザクション以倖のトランザクションがある堎合、それらはすべおコむンベヌスじゃない通垞のトランザクションです。

    allTx = allTx.concat(txs)
  })

  evaluateTxs(allTx)
}

ここたで䞀通りチェックすれば、ブロックの怜蚌は完了です。

allTxは、トランザクションをすべお連結したもので、さっき䜜ったevaluateTxsでさらにトランザクションの怜蚌を行いたす。

8. Nodeクラスを䜜る

今回はP2P通信するコヌドたでは䜜りたせんが、各ノヌドピアを実隓するためのクラスを䜜りたす。

class Node {
  constructor(seed = null) {
    this.address = sha256s(seed || randomBytes(32))
    this.pendingTxs = []
    this.blocks = []
    this.peers = []
    this.prevHash = '0000000000000000000000000000000000000000'
  }

Nodeクラスのメンバヌには、送金甚のラベルであるaddressず、ブロックにただ入っおいないpendingTxsず、既に発行されおいるblocksず、接続䞀芧であるpeersず、prevHashを持ちたす。

  _getAllTx() {
    return [].concat(
      ...this.blocks.map(block => {
        return deserialize(block).data.txs.map(tx => deserialize(tx).data)
      }),
      this.pendingTxs.map(tx => deserialize(tx).data)
    )
  }
  getBalance(address = this.address) {
    const txs = this._getAllTx()
    const wallets = evaluateTxs(txs)
    return wallets[address] || 0
  }

たずは、珟時点での各アドレスの残高を確認するgetBalanceメ゜ッドです。匕数省略時には自分自身の残高を返したす。

_getAllTxは、this.blockに含たれるトランザクションずthis.pendingTxsをすべおdeserializeしお1぀の配列に入れるものです。JavaScriptでは慣習ずしお、_で始たるメンバヌはプラむベヌト扱いにするずいうものがありたす。TypeScriptであれば、private修食子が䜿えたす。

evaluateTxsはトランザクションの怜蚌で、各りォレットの資金移動を順に远いかけおいるため、最終結果は各アドレスの残高ずいうこずになりたす。ただ送信されおいないアドレスの堎合は、先ほど玹介した||によるむディオムを䜿っお0を返したす。

  generate() {
    const coinbaseTx = createCoinbaseTx(this.address)
    const txs = [coinbaseTx.serialized, ...this.pendingTxs]
    const block = createBlock(txs, this.prevHash)
    this.pendingTxs = []
    this.prevHash = block.hash
    this.blocks.push(block.serialized)
    this.broadcast(block.serialized)
  }

generateメ゜ッドは、マむニングが成功したずいうこずにしおブロックを生成したす。

ブロックに含たれるトランザクションは、自分宛のコむンベヌストランザクションず、this.pendingTxsです。

ブロックを䜜成したら、this.pendingTxsを空に戻しおthis.prevHashを曎新し、this.blocksにブロックを远加し、埌ほど説明するbroadcastメ゜ッドで他のノヌドにブロックを流したす。

  send(sendTo, amount) {
    if (amount > this.getBalance()) {
      throw new Error('Wallet error: Insufficient funds')
    }
    const tx = createTx(this.address, sendTo, amount)
    this.pendingTxs.push(tx.serialized)
    this.broadcast(tx.serialized)
  }

sendメ゜ッドで送金を行いたす。残高が足りない堎合ぱラヌになりたす。

  connect(peer) {
    this.peers.push(peer)
    peer.peers.push(this)
  }

connectメ゜ッドは、別のノヌドピアをリストに远加し、盞手のピアに自分を登録したす。

  broadcast(packet) {
    this.peers.forEach(peer => {
      peer.recv(packet)
    })
  }

broadcastメ゜ッドは、接続しおいるピアすべおのパケットを送信したす。実際のコヌドずしおは、盞手のrecvメ゜ッドを叩いおいるだけです。

  recv(packet) {
    const { type } = deserialize(packet).data
    switch (type) {
      case 'tx': {
        this._receiveTx(packet)
        break
      }
      case 'block': {
        this._receiveBlock(packet)
        break
      }
    }
  }

recvメ゜ッドでは、受け取ったパケットのtypeを芋お、txずblockで凊理を分けおいたす。

  _receiveTx(packet) {
    const { from } = deserialize(packet).data
    if (from === null) {
      throw new Error('Invalid Tx: reject CoinbaseTx')
    }
    const txs = this._getAllTx()
    txs.push(deserialize(packet).data)
    evaluateTxs(txs)

    this.pendingTxs.push(packet)
  }

トランザクションの堎合、たず受け取ったトランザクションがコむンベヌスなら゚ラヌずしたす。実際のP2Pプログラムの堎合、recvが゚ラヌをthrowするのは良くないため、ログに残し぀぀パケットを無芖するずいう挙動になるでしょう。

トランザクションをすべお怜蚌しお゚ラヌが出なければ、this.pendingTxsにパケットを远加したす。

  _receiveBlock(packet) {
    const blocks = [...this.blocks, packet]
    validateBlocks(blocks)

    this.blocks.push(packet)
  }

ブロックの堎合も、新しいブロックを加えおvalidateBlocksで怜蚌しお゚ラヌが出なければ、this.blockにパケットを远加したす。

Bitcoinりォレットを䜜っおみよう

最埌に実際のBitcoinのりォレットプログラムを䜜っおみたしょう。

Bitcoinの通信党郚を実装するのは倧倉なため、Bitcoin公匏りォレットのBitcoin Coreを動かしお、JSON-RPC経由で制埡するずいうパタヌンでやりたす。

mkdir bitcoin-wallet
cd bitcoin-wallet

今回のプログラムは「bitcoin-wallet」ずいう名前で䜜りたす。倧たかな手順は以䞋のずおりです。

  1. パッケヌゞのむンストヌルず起動
  2. 関数を䜜成
  3. Walletの䜜成

Bitcoin Coreをむンストヌルする

macOSでパッケヌゞ管理システムHomebrewを䜿っおいれば、bitcoindパッケヌゞをむンストヌルするだけです。

brew install bitcoind

bitcoindを起動する

デヌタ甚のディレクトリを䜜成しおおいお、bitcoindを起動したす。

$ mkdir data
$ bitcoind -testnet -datadir=data -txindex -server -rpcuser=u -rpcpassword=p

bitcoindを起動するずきの泚意点ずしお、-testnetでテスト甚のネットワヌクに接続したしょう。testnetでは、Bitcoin testnet3 faucetのようなサむトで、無料でtestnet専甚のBitcoinを受け取れたす。このコむンを䜿っお、実際のネットワヌク䞊でのテストをするのです。

-datadir=dataでデヌタディレクトリの䜍眮を指定したす。ちなみに、珟時点で26GBもの容量を消費するので、テストを終えお䞍芁になったら削陀するのをおすすめしたす。

JSON-RPCでは、ナヌザヌ名やパスワヌドをコマンドラむンもしくは蚭定ファむルで定矩したすが、他人に芋られる可胜性があるものずしお泚意しおください。

たた、本運甚で䜿う堎合、りォレットのパスワヌドロックなど、気を぀けないずいけないこずがありたす。

必芁なパッケヌゞをむンストヌルする

npm init -y
npm i request-promise

今回は、JSON-RPCを叩くために、request-promiseずいうnpmパッケヌゞを䜿いたす。

怜玢すれば、Bitcoin CoreのJSON-RPCを叩くためのパッケヌゞがいく぀か芋぀かりたすが、どれも叀く、メンテナンスもされおいたせん。そもそも自前で叩いおもそんなに長いコヌドにならないため、今回は自䜜しおしたいたす。

Bitcoin Coreを叩く

JSON-RPCでは、HTTP POSTでmethodずparamsずいうそれぞれのパラメヌタを送り぀けたす。methodは、helpやgetnewaddressなどの名前を持ちたす。

ちなみにBitcoinでの開発では、bitcoin-cliずいうCLIでBitcoin Coreを制埡するコマンドを叩くのが定番ですが、やっおいるこずはたったく同じです。

const rp = require('request-promise')

rp(`http://localhost:18332`, {
  method: 'POST',
  body: JSON.stringify({ method, params }),
  auth: { user, pass }
})

最近のJavaScriptでは、非同期凊理はPromiseが定番です。rp関数を叩くずPromiseが返っおきたす。

rp(...).then(response => console.log(response))

Promiseではthenメ゜ッドを叩くこずで非同期凊理の続きを曞くこずができたす。぀たり、request-promiseの堎合、HTTP POSTを実行したあずthenの䞭身が実行されたす。

thenの戻り倀はPromiseオブゞェクトなため、さらにthenを぀なげられたす。

ここでは、さらに䞀歩螏み蟌んで非同期凊理のasync/awaitを䜿っおみたす。

const dispatch = async (user, pass, method, ...params) => {
  const { result, error } = JSON.parse(await rp(...))
}

async宣蚀された関数の䞭では、awaitずいうキヌワヌドを぀けるこずでPromiseを同期的に扱えたす。具䜓的にはawaitのあずに続くコヌドは、thenの䞭身が呌び出されるたで埅機したす。

const { result, error } = JSON.parse(await rp())ずいうコヌドは、rp().then(({ result, error }) => {....})ず同じ意味を持ちたす。

thenをひたすら連鎖させるのはそれなりに面倒ですが、awaitを䞊べるだけなら曞きやすく、理解しやすいコヌドになりたす。

Promiseはthenだけではなく、catchずいうメ゜ッドも持ちたす。これぱラヌ時のリカバリヌをどうするかずいうものです。

今回の事䟋では、HTTPの接続に゚ラヌが生じた、あるいはJSON-RPCでmethod名が正しくないなどの゚ラヌが生じた堎合の凊理が必芁になりたす。

catch(e => {
  if (e.statusCode) {
    return JSON.stringify({ error: JSON.parse(e.error).error })
  } else {
    return JSON.stringify({ error: e.error })
  }
})

通信自䜓は成功するものの、Bitcoin JSON-RPCのプロトコル的な問題があるずきにはe.statusCodeに500などがセットされおいたす。たた、その堎合、e.errorにJSONで゚ンコヌドされた゚ラヌ情報が入っおいるずいう埮劙にややこしいこずになっおいたす。

クラむアントを䜜る関数を䜜る

さきほどたでのコヌドでは、接続先がハヌドコヌディングされおいるこずず、dispatch関数で、毎回userずpassを指定しおいたす。

const createClient = ({ host, rpcport, user, pass }) => {
  // dispatch関数にあたるものを返す
}

そのため、host・rpcport・user・passずいう匕数でたず初期化しお、dispatchにあたる関数を返すようにしたす。

以䞋、client.jsで定矩したす。

const rp = require('request-promise')

const createClient = ({ host, rpcport, user, pass }) => {
  return async (method, ...params) => {
    const { result, error } = JSON.parse(
      await rp(`http://${host}:${rpcport}`, {
        method: 'POST',
        body: JSON.stringify({ method, params }),
        auth: { user, pass }
      }).catch(e => {
        if (e.statusCode) {
          return JSON.stringify({ error: JSON.parse(e.error).error })
        } else {
          return JSON.stringify({ error: e.error })
        }
      })
    )
    if (error) {
      throw error
    } else {
      return result
    }
  }
}

module.exports = { createClient }

りォレットの䜜成

さお、りォレットを䜜りたす。コマンドラむンで動くツヌルずしお䜜るので、コマンドラむン匕数を扱いたす。

Node.jsでは、process.argvでコマンドラむン匕数にアクセスできたす。 process.argv[0]にはNode.jsのコマンドそのものが入り、process.argv[1]にはスクリプト名が入りたす。そのため、匕数はprocess.argv[2]から始たりたす。

const { createClient } = require('./client')

const wallet = async () => {
  if (process.argv.length < 3) {
    console.log('usage: wallet <command> [option...]')
    process.exit(1)
  }

  const command = process.argv[2]
  if (!(command in commands)) {
    console.log(`unknown command: ${command}`)
    process.exit(1)
  }

  const conf = {
    host: 'localhost',
    rpcport: 18332,
    user: 'u',
    pass: 'p'
  }
  const cl = createClient(conf)
  await commands[command](cl, ...process.argv.slice(3))
}

wallet().catch(err => console.error(err))

process.argv.lengthが3未満の堎合、usageを衚瀺しお終了したす。たた、匕数で指定するcommandが、埌ほど説明するコマンド配列にない堎合も、unknown commandを衚瀺しお終了したす。

client.jsで定矩したcreateClientでクラむアントを䜜成したす。このずき、蚭定はtestnet向けに決め打ちで曞いおいたす。

りォレットのコマンド定矩

コマンドはこのように定矩しおいたす。

const newAddress = async cl => {
  const address = await cl('getnewaddress', 'my address')

  console.log(address)
}
// äž­ç•¥
const commands = { newAddress, info, send, dump, importPriv }
newAddressコマンド
$ node src/wallet/cli.js newAddress
2N7wqmAYRhiDUXhnZKtMbWBDSeXG4ddYvSy

起動した盎埌は、りォレットずしお䜿うためのアドレスがありたせん。たずはnewAddressコマンドでアドレスを䜜成したす。

const newAddress = async cl => {
  const address = await cl('getnewaddress', 'my address')

  console.log(address)
}

getnewaddressずいうJSON-RPCメ゜ッドは、匕数に文字列を枡すずラベルずしお扱いたす。凊理の郜合䞊、ラベルはmy address決め打ちずしたす。

infoコマンド
$ node src/wallet/cli.js info
addresses: [2N7wqmAYRhiDUXhnZKtMbWBDSeXG4ddYvSy]
balance: 0
block height: 273905

infoコマンドで、自分の持っおいるアドレスず、残高、ブロック長情報を衚瀺したす。

const info = async cl => {
  const addresses = await cl('getaddressesbylabel', 'my address')
  const balance = await cl('getbalance')
  const blockchainInfo = await cl('getblockchaininfo')

  console.log(`addresses: [${Object.keys(addresses).join(', ')}]`)
  console.log(`balance: ${balance}`)
  console.log(`block height: ${blockchainInfo.blocks}`)
}

前述のmy addressずいうラベルは、getaddressesbylabelJSON-RPCメ゜ッドで䜿っおいたす。

getblockchaininfoは、Bitcoinネットワヌクの状況を知るためのJSON-RPCメ゜ッドです。

sendコマンド
$ node src/wallet/cli.js send 2N6PaxqoUFbWYiumFq3i6nuQPQ1LG8VJrE1 0.04
bef4f254851ff8cff12c398978f45bb3d73c713c38238330f2869898e0547151

sendコマンドは、送金するコマンドです。送金に成功すれば送金トランザクションのIDが衚瀺されたす。

トランザクションを芋るこずができるサむト、䟋えばBlockCypherのBitcoin Testnet Block Explorerで、確認できたす。

const send = async (cl, address, amount) => {
  const txid = await cl('sendtoaddress', address, amount)
  console.log(txid)
}

sendtoaddressJSON-RPCメ゜ッドで送金を行いたす。

dumpコマンド
$ node src/wallet/cli.js dump 2N6PaxqoUFbWYiumFq3i6nuQPQ1LG8VJrE1
<プラむベヌトキヌ>

dumpは、自分の持っおいるアドレスに察応する秘密鍵を出力するコマンドです。この秘密鍵の文字列を他人に芋られるず、Bitcoinを盗たれ攟題なのでご泚意ください。

const dump = async (cl, address) => {
  const priv = await cl('dumpprivkey', address)
  console.log(priv)
}

dumpprivkeyJSON-RPCメ゜ッドは、指定したアドレスの秘密鍵がある堎合に、それを埗るものです。

importPrivコマンド
$ node src/wallet/cli.js importPriv <プラむベヌトキヌ>

importPrivコマンドは、秘密鍵をむンポヌトするものです。これができるため、秘密鍵を他人に知られおはいけないのです。

const importPriv = async (cl, priv) => {
  await cl('importprivkey', priv, 'my address')
}

importprivkeyJSON-RPCメ゜ッドは、指定した秘密鍵をむンポヌトしたす。オプションで第2匕数にラベルを指定できたす。

最埌に

ここたで、ブロックチェヌンの仕組みを解説し、実際にブロックチェヌンやBitcoinりォレットのサンプルプログラムを芋おきたした。サンプルの党゜ヌスは、GitHubの次のアドレスからダりンロヌドできたす。ぜひ自分で詊しおみおください。

4 GitHub - erukiti/bitcoin-samples

erukitiえるきち 5 erukiti 6 erukiti 7 erukiti 8 erukiti

9
バック゚ンド・フロント゚ンド・䞀郚むンフラなどやったりしおいるフリヌランス゚ンゞニャヌ。メタプログラミングやブロックチェヌンをやったり、それらの同人誌を曞いたり、それを商業化したりしおいる。VSCodeTypeScript最高。
䞻な著曞に、同人誌をNextPublishing技術曞兞シリヌズで商業化した「最新JavaScript開発」「JavaScript AST入門」がある。ほか同人では共著の合同誌に「ワンストップ技術同人誌を曞こう」「Alchemist Vol.1」「OneStop転職」があり、「ワンストップ」制䜜のノりハりや知芋を「本文212Pの分厚い薄い本の共同執筆を支える技術」や「コミケの技術評論島で2日間で200冊売った話する」でも公開しおいる。

線集薄井千春ZINE

若手ハむキャリアのスカりト転職