node.jsでブロックチェーン周辺の基本を見ておくことにしたので、その記録。
準備
とりあえずtypescriptを入れる。
npm install typescript @types/node
ブロックチェーン
nodeで作成している記事に従ってブロックとチェーンのコードを書いてみる。
微妙に自分の趣向で書き換えてるけど、ほぼそのままパクリ。
ブロックは、前のブロックのハッシュ値+自分のデータでハッシュ値を作成するが、時刻も含めているのでそれで扱う。
import * as crypto from 'crypto';
export class Block {
readonly hash: string; // このBlockのハッシュ値
readonly timestamp: number; // 作成時刻
/**
* コンストラクタ
* @param index {number} ID
* @param previousHash {string} 前のブロックのハッシュ値
* @param data {string} アプリケーションで使用するデータ(バイト列の場合が多い?)
*/
constructor(
readonly index: number,
readonly previousHash: string,
readonly data: string
){
this.timestamp = Date.now();
this.hash = this.calculateHash(); // インスタンス作成時に自身のハッシュを作成
}
/**
* 自分自身のハッシュ値を計算
* インデックス、前のハッシュ、タイムスタンプ、データから自分のハッシュ値を計算する
* SHA256でハッシュ値を計算して16進数にする。
*/
private calculateHash(): string {
const dataConcated = this.index + this.previousHash + this.timestamp + this.data;
return crypto
.createHash('sha256')
.update(dataConcated)
.digest('hex');
}
}
チェーン
import { Block } from './Block';
/**
* Blockを配列で管理するBlockchain
*/
class Chain {
private readonly chain: Block[] = [];
/**
* この直近に追加されたBlockを返す
*/
private get latestBlock(): Block{
return this.chain[this.chain.length - 1];
}
/**
* 最初のブロックを作成
*/
constructor(){
this.chain.push(new Block(0, '0', 'Genesis First Block'));
}
/**
* Blockを追加する
* @param data 追加するBlockのdata
*/
addBlock(data: string): void {
const block = new Block(
this.latestBlock.index + 1,
this.latestBlock.hash,
data
);
this.chain.push(block);
}
}
export default Chain;
「既定のエクスポートがありませんという」エラーが出たので、
プロジェクトルートにtsconfig.json
を作成したりと試した結果、BlockのClassにexportを付けると問題なくなった。
これを使って実際にブロックチェーンを作成する。
import Chain from "./Chain";
console.log("新規ブロックチェーン作成");
let blockchain = new Chain();
console.log("最初のブロック");
blockchain.addBlock("Block1");
console.log("二番目のブロック");
blockchain.addBlock("Block2");
console.log(JSON.stringify(blockchain, null, 2));
これで実行すると
(node:2772) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
エラーが発生したので、tsconfig.jsonに"module": "commonjs"を追加して解決。
{
"chain": [
{
"index": 0,
"previousHash": "0",
"data": "Genesis First Block",
"timestamp": 1640215989067,
"hash": "d97b03898da1ac699777543ec29a9bcd6484c1fd5c0de1f136a505d6827749b4"
},
{
"index": 1,
"previousHash": "d97b03898da1ac699777543ec29a9bcd6484c1fd5c0de1f136a505d6827749b4",
"data": "Block1",
"timestamp": 1640215989076,
"hash": "3aee610e345f68c66a5c818e20ecf1b0debf76e07cf5083cf86c9228d2ee1ac3"
},
{
"index": 2,
"previousHash": "3aee610e345f68c66a5c818e20ecf1b0debf76e07cf5083cf86c9228d2ee1ac3",
"data": "Block2",
"timestamp": 1640215989077,
"hash": "8329a5182704084628a178679a58e1a89da266840ac1a2ff99b11687cf6cde20"
}
]
}
ナンス値
何らかの数字を含めてハッシュ値を計算して、その結果が何らかの条件(00000から始まる、等)に当てはまるようにする事で
改ざんしようとする場合の計算量を高めて、不正チェーンが伸び辛くする(最長チェーンが正規と判断されるため)という理屈。
ブロックにナンス値を追加して、それを正しい状態になるまでカウントアップしながら計算する、というロジックを組めば良い。
import * as crypto from 'crypto';
export class Block {
readonly hash: string; // このBlockのハッシュ値
readonly nonce:number; // ナンス値
readonly timestamp: number; // 作成時刻
/**
* コンストラクタ
* @param index {number} ID
* @param previousHash {string} 前のブロックのハッシュ値
* @param data {string} アプリケーションで使用するデータ(バイト列の場合が多い?)
*/
constructor(
readonly index: number,
readonly previousHash: string,
readonly data: string
){
this.timestamp = Date.now(); //マイニング前に確定させておく
//this.hash = this.calculateHash(); // インスタンス作成時に自身のハッシュを作成
const {p_nonce, p_hash} = this.mine(); // マイニング
this.nonce = p_nonce;
this.hash = p_hash;
}
/**
* 自分自身のハッシュ値を計算
* インデックス、前のハッシュ、タイムスタンプ、データ、ナンス値から自分のハッシュ値を計算する
* SHA256でハッシュ値を計算して16進数にする。
*/
private calculateHash(p_nonce:number): string {
const dataConcated = this.index + this.previousHash + this.timestamp + this.data + p_nonce;
return crypto
.createHash('sha256')
.update(dataConcated)
.digest('hex');
}
private mine():{p_nonce:number, p_hash:string} {
let p_nonce:number = -1;
let p_hash:string = "AAAAA";
do{
p_hash = this.calculateHash(++p_nonce);
}while(p_hash.startsWith('000')===false) // ハッシュ値の条件に当たるまでループ
return { p_nonce, p_hash};
}
}
実行するとnonceが設定されて、ハッシュの先頭が000になっている。
{
"chain": [
{
"index": 0,
"previousHash": "0",
"data": "Genesis First Block",
"timestamp": 1640220923504,
"nonce": 2233,
"hash": "0002e40c78aafdbf2ac8faaac4b95f72be399b5b864bf029f657d6e47676c867"
},
{
"index": 1,
"previousHash": "0002e40c78aafdbf2ac8faaac4b95f72be399b5b864bf029f657d6e47676c867",
"data": "Block1",
"timestamp": 1640220923529,
"nonce": 205,
"hash": "000378af5b48e24e3aea122e0debeb97775ed42fbe81b3fc3886a3bf7de80fc4"
},
{
"index": 2,
"previousHash": "000378af5b48e24e3aea122e0debeb97775ed42fbe81b3fc3886a3bf7de80fc4",
"data": "Block2",
"timestamp": 1640220923531,
"nonce": 639,
"hash": "0009ed811373333199b4d1c0c39ce7d3553b42e8ae22c296dd1afd0f2f06c08e"
}
]
}
実際の処理
単純にブロックが作れたら終わりではなく、
- チェーン台帳とやりとりするための仕組み
- 正しいチェーンと判断するための合意形成の仕組み
- チェーンを保持する台帳の仕組み(データベース)
が必要となり、nodejsだけで自前でスケーラブルに組むのは大変なので、
こういったものが例えばAWSにはAmazon Managed Blockchainとして存在している。
計算量が膨大になる=CPUを使う=電力を使う=環境に優しくない
のは言われているが、AI含めて電力はこれからも必要になるので日本も電力戦略もどうするのかというのは必要そう。
NFT
ブロックチェーン応用例としてここに説明がある。
全体的な動きはこれで、特徴として
- プログラマビリティ(付加機能の付与)
- 取引が自由
- ERC721に沿えば一定の相互運用性あり
ERC721については
ERC721では個々のトークンはユニークで代替不可能すなわち唯一無二あることから、トークンの量(_value)ではなく、トークンのID(_tokenId)を引数で指定し転送します。
その他のtransfer系の関数、approve関数でも同様にトークンの量を指定するかわりにトークンのIDを指定します。
という代替可能なもの=量指定とは違ってIDで取引する。
これらの扱いには少なくとも2018年ではOpenZeppelinというものがある。
ERC721トークンやそれを扱うスマートコントラクトを開発する場合、
ERC20同様、OpenZeppelinというSolidityで書かれたスマートコントラクトのオープンフレームワークを使うとよいでしょう
使い方はここにまとめられている。。
それ以外だとSatellitesのようなものや
ReserveBlock Foundationのもの、
Deptのもの
などがオープンソースなプラットフォームとして出てきている。
というくらいでとりあえず終わり