みなさん、改ざんに強く信頼性が高いと言われるブロックチェーンですが、それではブロックチェーンのブロックを検証したことがありますか?
僕はあります
ぜひ、みなさんにも体験して欲しいです。
なので今日はNEM Catapultエンジンを搭載したSymbolブロックチェーンのテストネットを使ってブロックを検証してみましょう。
必要なもの
ネットワーク環境
Chromeブラウザ
前準備
適当なノード(例えば http://api-01.ap-southeast-1.0.10.0.x.symboldev.network:3000/node/info )にブラウザでアクセスし、デベロッパーツールを起動します。F12 あるいは fn+F12 などでコンソール画面を開いてください。以下のような画面が表示されます。
>
という入力受付モードになっているかと思います。ここにjavascriptを入力すると、対話式に動作結果を確認することができます。この表示になっていない場合は Console タブを選択し直してみてださい。
次にsymbol-sdkを読み込みます。githubにsymbol-sdk-typescriptをbrowserify化したものを置いているのでそれを読み込みにいきます。(現在の最新バージョンは0.22.2です)
(script = document.createElement('script')).src = 'https://xembook.github.io/nem2-browserify/symbol-sdk-pack-0.22.2.js';
document.getElementsByTagName('head')[0].appendChild(script);
symbol-sdkのライブラリをインポートします。
nem = require("/node_modules/symbol-sdk");
固定値としてノードURLとジェネレーションハッシュ値を定義します。
NODE = 'http://api-01.ap-southeast-1.0.10.0.x.symboldev.network:3000'
GENERATION_HASH = '6C1B92391CCB41C96478471C2634C111D9E989DECD66130C0430B5B8D20117CD';
これでブロックチェーンにアクセスする準備ができました。
では検証してみたいブロックを表示してみましょう。今回はブロック高=2のブロックを表示してみます。
repo = new nem.RepositoryFactoryHttp(NODE, nem.NetworkType.TEST_NET,GENERATION_HASH);
blockRepo = repo.createBlockRepository();
block = await blockRepo.getBlockByHeight(2).toPromise();
console.log(block);
以下のように出力されました。これが、キャッシュされていない「生のブロックチェーン」です。
署名検証
それでは署名検証に必要な情報を集めていきましょう。何が必要かはSymbolテクニカルリファレンスの 7.1ブロックフィールドに記載されています。
NEM Symbol Technical Reference
(b)のVerifiable dataが検証に必要なデータです。これらをSignerの秘密鍵で署名するとSignatureが生成されます。そして、Signerの公開鍵を使うとSignatureの署名者が検証できます。
V,N,Tは
version,networkType,EntityTypeでそれぞれ1,152,33091になります。
そのほかはそのまま該当するキー値があります。
検証にはそれぞれが固定幅を持つデータ列にシリアライズする必要があります。
そこで固定幅のサイズをcatbufferで調べてみます。
catbuffer とはおそらく Catapult Protocol Buffersの略と思われます。
catbuffer/schemas/entity.cats
enum EntityType : uint16
enum NetworkType : uint8
version = uint8
catbuffer/schemas/types.cats
using BlockFeeMultiplier = uint32
using Difficulty = uint64
using Height = uint64
using Timestamp = uint64
その他の項目についてはhash値なのでそのままシリアライズします。
次にシリアライズするためにcatbufferというライブラリを読み込みます。
インポート
cat = require("/node_modules/catbuffer-typescript");
cat.GeneratorUtils.uintToBuffer
cat.GeneratorUtils.uint64ToBuffer
という関数が使えるようになります。
シリアライズ時に配列の箱に情報を詰め込んで行くのですが、8箱使うものについてはuint64ToBuffer([lower,heigher])をそれ以外のものはuintToBuffer(,箱数)を使いましょう。hash値のシリアライズ変換についてはsymbol-sdkのConvert.hexToUint8を使用します。
ブロック高=2のデータについては以下のように詰め込むことができました。
v = cat.GeneratorUtils.uintToBuffer( block.version, 1) //version:8
n = cat.GeneratorUtils.uintToBuffer( block.networkType, 1) //network:8
t = cat.GeneratorUtils.uintToBuffer( block.type, 2) //type:16
h = cat.GeneratorUtils.uint64ToBuffer([block.height.lower ,block.height.higher]) //height
ts = cat.GeneratorUtils.uint64ToBuffer([block.timestamp.lower ,block.timestamp.higher]) //timestamp
df = cat.GeneratorUtils.uint64ToBuffer([block.difficulty.lower,block.difficulty.higher]) //difficulty
pg = nem.Convert.hexToUint8(block.proofGamma) //proofGamma
ps = nem.Convert.hexToUint8(block.proofScalar) //proofScalar
pv = nem.Convert.hexToUint8(block.proofVerificationHash) //proofVerificationHash
ph = nem.Convert.hexToUint8(block.previousBlockHash) //prehash
th = nem.Convert.hexToUint8(block.blockTransactionsHash) //txhash
rh = nem.Convert.hexToUint8(block.blockReceiptsHash) //rcpthash
sh = nem.Convert.hexToUint8(block.stateHash) //stathash
ba = nem.RawAddress.stringToAddress(block.beneficiaryAddress.address) //beneficiaryAddress
fm = cat.GeneratorUtils.uintToBuffer( block.feeMultiplier, 4)
次にそれぞれを配列結合します。
buffer = Array.from(v)
.concat(Array.from(n))
.concat(Array.from(t))
.concat(Array.from(h))
.concat(Array.from(ts))
.concat(Array.from(df))
.concat(Array.from(pg))
.concat(Array.from(pv))
.concat(Array.from(ps))
.concat(Array.from(ph))
.concat(Array.from(th))
.concat(Array.from(rh))
.concat(Array.from(sh))
.concat(Array.from(ba))
.concat(Array.from(fm))
264個の配列に結合されました。
署名者が署名したかどうか公開鍵で検証します。
> signer = nem.PublicAccount.createFromPublicKey(block.signer.publicKey,nem.NetworkType.TEST_NET);
> signer.verifySignature(buffer,block.signature);
< true
公開鍵で署名者のアカウントを生成しsymbol-sdkのverifySignatureで署名されるデータと署名データを公開鍵で比較します。
trueと出力されました。検証完了です。間違いなくこのアカウントによって署名されたことが証明できました。
ハッシュ生成
ハッシュライブラリをインポートして、sha3のハッシュクラスを生成します。
sha3_256 = require('/node_modules/js-sha3').sha3_256;
hasher = sha3_256.create();
hashの生成順序はここに記載されています。
catapult-server/src/catapult/model/EntityHasher.cpp
sha3.update(entity.Signature);
sha3.update(entity.SignerPublicKey);
sha3.update(buffer);
sha3.final(entityHash);
最後のbufferについてはどの範囲のデータか悩みましたが、結果として検証データと同じもので大丈夫でした。
最初に署名でハッシュ更新します
hasher.update(nem.Convert.hexToUint8(block.signature)); //signature
次に公開鍵で更新
hasher.update(nem.Convert.hexToUint8(block.signer.publicKey)); //publicKey
最後に署名データで更新
hash = hasher.update(buffer).hex().toUpperCase();
< "8DB5757CCF024CA04FC7B27B40A0801615864D661CCC488528D86054A5CFB25C"
出力された値と最初に表示したblock高=2のハッシュ値と比較して同じであれば、ハッシュ値はデータを正しく要約していると考えられます。
なお、最新のバージョンではブロックヘッダーはNormalBlockとImportanceBlockの2種類があります。ImportanceBlockが取得できた場合は、以下のようにbufferを生成しましょう。
v = cat.GeneratorUtils.uintToBuffer( block.version, 1) //version:8
n = cat.GeneratorUtils.uintToBuffer( block.networkType, 1) //network:8
t = cat.GeneratorUtils.uintToBuffer( block.type, 2) //type:16
h = cat.GeneratorUtils.uint64ToBuffer([block.height.lower ,block.height.higher]) //height
ts = cat.GeneratorUtils.uint64ToBuffer([block.timestamp.lower ,block.timestamp.higher]) //timestamp
df = cat.GeneratorUtils.uint64ToBuffer([block.difficulty.lower,block.difficulty.higher]) //difficulty
pg = nem.Convert.hexToUint8(block.proofGamma) //proofGamma
ps = nem.Convert.hexToUint8(block.proofScalar) //proofScalar
pv = nem.Convert.hexToUint8(block.proofVerificationHash) //proofVerificationHash
ph = nem.Convert.hexToUint8(block.previousBlockHash) //prehash
th = nem.Convert.hexToUint8(block.blockTransactionsHash) //txhash
rh = nem.Convert.hexToUint8(block.blockReceiptsHash) //rcpthash
sh = nem.Convert.hexToUint8(block.stateHash) //stathash
ba = nem.RawAddress.stringToAddress(block.beneficiaryAddress.address) //beneficiaryAddress
fm = cat.GeneratorUtils.uintToBuffer( block.feeMultiplier, 4) //feeMultiplier:32
ve = cat.GeneratorUtils.uintToBuffer(block.votingEligibleAccountsCount,4)
he = cat.GeneratorUtils.uint64ToBuffer([block.harvestingEligibleAccountsCount.lower ,block.harvestingEligibleAccountsCount.higher])
tv = cat.GeneratorUtils.uint64ToBuffer([block.totalVotingBalance.lower ,block.totalVotingBalance.higher])
pi = nem.Convert.hexToUint8(block.previousImportanceBlockHash)
buffer = Array.from(v)
.concat(Array.from(n))
.concat(Array.from(t))
.concat(Array.from(h))
.concat(Array.from(ts))
.concat(Array.from(df))
.concat(Array.from(pg))
.concat(Array.from(pv))
.concat(Array.from(ps))
.concat(Array.from(ph))
.concat(Array.from(th))
.concat(Array.from(rh))
.concat(Array.from(sh))
.concat(Array.from(ba))
.concat(Array.from(fm))
.concat(Array.from(ve))
.concat(Array.from(he))
.concat(Array.from(tv))
.concat(Array.from(pi));
これで、ブロックチェーンのブロックの署名検証とハッシュ生成を追体験することができました。
ブロックチェーンが改ざんに強いと言われるのはこれらの仕組みがうまく機能し、ノード間がお互いを検証し合っているからです。Catapultエンジンを搭載したブロックチェーンではなクライアントからでも簡単に検証することができます。将来、IoTがブロックチェーンと深く結びついてくるとブロックヘッダーを保存したクライアントが活躍するようになると思いますが、これはまた別の機会にお話します。
さあ、あなたも今日からこう言えます。
「キミ、ブロックチェーンを熱く語ってとても信頼しているようだけど、検証したことある?」