5
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ブロックチェーンの基礎知識&JavaScriptでブロックチェーンを構築する

Last updated at Posted at 2020-06-09

ここでは、ブロックチェーンの基礎知識とJavaScriptを使ったブロックチェーンの構築方法を紹介します。コードを使って、基礎となるデータ構造、PoWマイニング、トランザクション処理など、ブロックチェーンを説明します。

本ブログは英語版からの翻訳です。オリジナルはこちらからご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。

#ブロックチェーンを知る
ブロックチェーンはその名の通りブロックの連鎖であるため、ブロックチェーンの基本的なデータ構造はブロックです。ブロックの中身は、タイムスタンプ、データ、ハッシュ、previousHashなどの情報が含まれています。これらの構成要素のうち、データ部分は当然のことながらデータを格納するために使われ、previousHashは前のブロックのハッシュ値を格納する役割を果たしています。以下は基本的なブロックチェーンの図です。
00.png

次に、以下のコードからわかるように、ハッシュは一般的なブロック情報を格納します。ハッシュは任意の長さの情報を固定長の文字列にマッピングすることができます。下の例では、sha256にマッピングされています。

calculateHash() {
    return SHA256(this.previousHash+ this.timestamp + JSON.stringify(this.data)).toString();
}

次に、ブロックデータの基本構造は次のようになります。

class Block {
    constructor(timestamp, data, previousHash = '') {
        this.timestamp = timestamp;
        this.data = data;
        this.previousHash = previousHash;
        // The calculation of the hash must be at the end so to ensure that all data is assigned correctly before calculation
        this.hash = this.calculateHash(); }

calculateHash() {
        return SHA256(this.previousHash + this.timestamp + JSON.stringify(this.data)).toString();
    }
}

次に、ブロックチェーンはブロックの連鎖であるため、ブロックチェーンは以下のように配列や連鎖として表現することができます。

class BlockChain {
    constructor() {
        this.chain = [];
    }
}

次に、ジェネシスブロックがありますが、これはブロックチェーンの最初のブロックで、常に手動で作成する必要があります。最初のブロックのpreviousHashは空です。

createGenesisBlock() {
    return new Block("2018-11-11 00:00:00", "Genesis block of simple chain", "");
}

また、ブロックチェーンのコンストラクタのメソッドも以下のように変更する必要があります。

class BlockChain {
    constructor() {
        this.chain = [this.createGenesisBlock()];
    }
}

次に、ブロックを追加することができます。追加した各ブロックは、元のブロックチェーンに接続されている必要があることに注意してください。

class BlockChain {
    getLatestBlock() {
        return this.chain[this.chain.length - 1];
    }
    
    addBlock(newBlock) {
        // The previous hash value of the new block is the hash value of the last block of the existing blockchain;
        newBlock.previousHash = this.getLatestBlock().hash;
        // Recalculate the hash value of the new block (because the previousHash is specified);
        newBlock.hash = newBlock.calculateHash(); 
        //Add new blocks to the chain;
        this.chain.push(newBlock); 
    }
    ...
}

これでブロックチェーンの検証ができるようになりました。ブロックチェーンデータの構造の核心は、適切な連携と耐タンパ性を確保することにあります。しかし、ブロックが改ざんされた場合、どのようにして検証し、そのブロックを見つけることができるのでしょうか?最も手間がかかりますが、最も一般的な方法は、すべてのブロックを通過する、つまりトラバースして、1つ1つ検証することです。

isChainValid() {
    //Traverse all the blocks
    for (let i = 1; i < this.chain.length; i++) {
        const currentBlock = this.chain[i];
        const previousBlock = this.chain[i - 1];
        //Recalculate the has value of the current block. If the hash value is not matched, it indicates that data of the block was changed without permission, and therefore the has value is not recalculated.
        if (currentBlock.hash !== currentBlock.calculateHash()) {
            console.error("hash not equal: " + JSON.stringify(currentBlock));
            return false;
        }
        // Determine whether the previousHash of the current block is equal to the hash of the previous block. If they are not equal to each other, this means that the previous block was changed without permission. Although the hash value is recalculated correctly, the hash value of the subsequent block is not recalculated, resulting the the whole chain breaking.
        if (currentBlock.previousHash !== previousBlock.calculateHash()) {
            console.error("previous hash not right: " + JSON.stringify(currentBlock));
            return false;
        }
    }
    return true;
}

では、以下のコードを実行します。

let simpleChain = new BlockChain();
simpleChain.addBlock(new Block("2018-11-11 00:00:01", {amount: 10}));
simpleChain.addBlock(new Block("2018-11-11 00:00:02", {amount: 20}));


console.log(JSON.stringify(simpleChain, null, 4));

console.log("is the chain valid? " + simpleChain.isChainValid());

結果は以下のようになります。

ali-186590cc4a7f:simple-chain shanyao$ node main_1.js 
{
    "chain": [
        {
            "timestamp": "2018-11-11 00:00:00",
            "data": "Genesis block of simple chain",
            "previousHash": "",
            "hash": "fd56967ff621a4090ff71ce88fdd456547d1c92d2e93766b7e8791f7a5f91f89"
        },
        {
            "timestamp": "2018-11-11 00:00:01",
            "data": {
                "amount": 10
            },
            "previousHash": "fd56967ff621a4090ff71ce88fdd456547d1c92d2e93766b7e8791f7a5f91f89",
            "hash": "150b196268a0152e9f0e719ac131a722472a809f49bd507965029a78c7400529"
        },
        {
            "timestamp": "2018-11-11 00:00:02",
            "data": {
                "amount": 20
            },
            "previousHash": "150b196268a0152e9f0e719ac131a722472a809f49bd507965029a78c7400529",
            "hash": "274a7a13ed20118e8cb745654934a7e24a4d59333ba17dfbf5d4cfe0fa8a6e34"
        }
    ]
}
is the chain valid? true

beforeHashとhashに注意してください。現在のブロックのpreviousHashが実は前のブロックのhashになっていることがわかります。

では、ブロックを改ざんしてみましょう。よくある主張として、ブロックチェーンは改ざん防止になっているというものがあります。それは本当でしょうか?では、2つ目のブロックを使って、どうなるか見てみましょう。

let simpleChain = new BlockChain();
simpleChain.addBlock(new Block("2018-11-11 00:00:01", {amount: 10}));
simpleChain.addBlock(new Block("2018-11-11 00:00:02", {amount: 20}));

console.log("is the chain valid? " + simpleChain.isChainValid());

// Change the data of the second block from 10 to 15
simpleChain.chain[1].data = {amount: 15};

console.log("is the chain still valid? " + simpleChain.isChainValid());
console.log(JSON.stringify(simpleChain, null, 4));

結果は以下のようになります。

ali-186590cc4a7f:simple-chain shanyao$ node main_1.js 
is the chain valid? true
hash not equal: {"timestamp":"2018-11-11 00:00:01","data":{"amount":15},"previousHash":"fd56967ff621a4090ff71ce88fdd456547d1c92d2e93766b7e8791f7a5f91f89","hash":"150b196268a0152e9f0e719ac131a722472a809f49bd507965029a78c7400529"}
is the chain still valid? false
{
    "chain": [
        {
            "timestamp": "2018-11-11 00:00:00",
            "data": "Genesis block of simple chain",
            "previousHash": "",
            "hash": "fd56967ff621a4090ff71ce88fdd456547d1c92d2e93766b7e8791f7a5f91f89"
        },
        {
            "timestamp": "2018-11-11 00:00:01",
            "data": {
                "amount": 15
            },
            "previousHash": "fd56967ff621a4090ff71ce88fdd456547d1c92d2e93766b7e8791f7a5f91f89",
            "hash": "150b196268a0152e9f0e719ac131a722472a809f49bd507965029a78c7400529"
        },
        {
            "timestamp": "2018-11-11 00:00:02",
            "data": {
                "amount": 20
            },
            "previousHash": "150b196268a0152e9f0e719ac131a722472a809f49bd507965029a78c7400529",
            "hash": "274a7a13ed20118e8cb745654934a7e24a4d59333ba17dfbf5d4cfe0fa8a6e34"
        }
    ]
}

上記の結果から、データを改ざんした後、ハッシュ値の再計算が行われず、ブロックのハッシュ値にミスマッチが発生していることがわかります。

では、別のブロックを改ざんしてみましょう。スマートな方法で、ブロックを改ざんした後にハッシュ値を再計算したらどうでしょうか?

let simpleChain = new BlockChain();
simpleChain.addBlock(new Block("2018-11-11 00:00:01", {amount: 10}));
simpleChain.addBlock(new Block("2018-11-11 00:00:02", {amount: 20}));

console.log("is the chain valid? " + simpleChain.isChainValid());
// Recalculate the hash value after tampering with it
simpleChain.chain[1].data = {amount: 15};
simpleChain.chain[1].hash = simpleChain.chain[1].calculateHash();
console.log("is the chain still valid? " + simpleChain.isChainValid());
console.log(JSON.stringify(simpleChain, null, 4));

結果は以下のようになります。

ali-186590cc4a7f:simple-chain shanyao$ node main_1.js 
is the chain valid? true
previous hash not right: {"timestamp":"2018-11-11 00:00:02","data":{"amount":20},"previousHash":"150b196268a0152e9f0e719ac131a722472a809f49bd507965029a78c7400529","hash":"274a7a13ed20118e8cb745654934a7e24a4d59333ba17dfbf5d4cfe0fa8a6e34"}
is the chain still valid? false
{
    "chain": [
        {
            "timestamp": "2018-11-11 00:00:00",
            "data": "Genesis block of simple chain",
            "previousHash": "",
            "hash": "fd56967ff621a4090ff71ce88fdd456547d1c92d2e93766b7e8791f7a5f91f89"
        },
        {
            "timestamp": "2018-11-11 00:00:01",
            "data": {
                "amount": 15
            },
            "previousHash": "fd56967ff621a4090ff71ce88fdd456547d1c92d2e93766b7e8791f7a5f91f89",
            "hash": "74d139274fb692495b7c805dd5822faa0c5b5e6058b6beef96e87e18ab83a6b1"
        },
        {
            "timestamp": "2018-11-11 00:00:02",
            "data": {
                "amount": 20
            },
            "previousHash": "150b196268a0152e9f0e719ac131a722472a809f49bd507965029a78c7400529",
            "hash": "274a7a13ed20118e8cb745654934a7e24a4d59333ba17dfbf5d4cfe0fa8a6e34"
        }
    ]
}

3つ目のブロックのpreviousHashは、2つ目のhashの「hash」ではありません。

では、ブロックチェーンは本当にタンパープルーフなのでしょうか?実際には、ブロックチェーンは完全にタンパープルーフではありません。お見せしましょう。もっと賢くなって、後続のすべてのブロックのハッシュ値を再計算することができたらどうでしょうか?それは次のようになります。

let simpleChain = new BlockChain();
simpleChain.addBlock(new Block("2018-11-11 00:00:01", {amount: 10}));
simpleChain.addBlock(new Block("2018-11-11 00:00:02", {amount: 20}));

console.log("is the chain valid? " + simpleChain.isChainValid());
// Tamper with the second block
simpleChain.chain[1].data = {amount: 15};
simpleChain.chain[1].hash = simpleChain.chain[1].calculateHash();
// Then, recalculate the third block
simpleChain.chain[2].previousHash = simpleChain.chain[1].hash;
simpleChain.chain[2].hash = simpleChain.chain[2].calculateHash();
console.log("is the chain still valid? " + simpleChain.isChainValid());
console.log(JSON.stringify(simpleChain, null, 4));

結果は以下のようになります。

ali-186590cc4a7f:simple-chain shanyao$ node main_1.js 
is the chain valid? true
is the chain still valid? true
{
    "chain": [
        {
            "timestamp": "2018-11-11 00:00:00",
            "data": "Genesis block of simple chain",
            "previousHash": "",
            "hash": "fd56967ff621a4090ff71ce88fdd456547d1c92d2e93766b7e8791f7a5f91f89"
        },
        {
            "timestamp": "2018-11-11 00:00:01",
            "data": {
                "amount": 15
            },
            "previousHash": "fd56967ff621a4090ff71ce88fdd456547d1c92d2e93766b7e8791f7a5f91f89",
            "hash": "74d139274fb692495b7c805dd5822faa0c5b5e6058b6beef96e87e18ab83a6b1"
        },
        {
            "timestamp": "2018-11-11 00:00:02",
            "data": {
                "amount": 20
            },
            "previousHash": "74d139274fb692495b7c805dd5822faa0c5b5e6058b6beef96e87e18ab83a6b1",
            "hash": "cc294e763c51e9357bf22d96073e643f4d51e07dd0de6e9b15d1d4f6bf6b45a8"
        }
    ]
}

これでブロックチェーン全体が改ざんされたことになります。実際には、あるブロックを改ざんした後、そのブロック以降のすべてのブロックを改ざんすることで、さらにブロックチェーン全体を改ざんすることができます。しかし、改ざんされたブロックの後のブロックの数が非常に多い場合、ブロックチェーン全体を改ざんするためには、後続のブロックを素早く改ざんしてバレないようにする必要があります。また、この場合、改ざんにかかるコストも非常に高くなります。したがって、ご覧のように、ブロックチェーンは完全に改ざんされないわけではありませんが、同時にAブロックを改ざんすることは、通常、ブロックチェーンの改ざんによる潜在的な利益よりも改ざんコストの方が大きくなるように設計されています。この点では、ブロックチェーン全体が安全であり、本質的に改ざん防止であると考えることができます。

#ブロックチェーンの方が安全性が高い理由
前のセクションで説明したように、ブロックチェーンは実際には完全な改ざん防止ではありませんが、それでもかなり安全です。ブロックチェーンが改ざんされる可能性を避けるためには、ブロックチェーンを改ざんするのにかかる潜在的なコストを増やす必要があります。しかし、どのようにしてこのコストを増やすことができるのでしょうか?そうですね、最も手間のかかる方法は、人為的に障壁を作ることかもしれません。では、あなたはブロックチェーン会計に参加したいと思いますか?もしそうなら、あなたの能力と意欲を証明するために、まずこの難しい数学の問題を解いてみましょう。これには、非常にシンプルな概念であるPoWが含まれています。

その前に、まずはこの数学の問題を解いてみましょう。この数学の問題は何でしょうか?それはそれほど複雑ではありませんが、実際には少しばかげて見えるかもしれません。ハッシュ値の最初の10桁が0であることを確認してください。 この数学の問題を解くには、推測して運を試すしかありません。ハッシュ計算には次のような特徴があります。

  • 情報の長さは、文章であれ、記事であれ、歌であれ、どのような長さであってもよく、一意の数字の文字列として計算することができます。
  • この数字の文字列は長さが決まっています。
  • その計算過程は不可逆的で、つまり、文章の段落のハッシュ値は簡単に計算できますが、ハッシュ値が対応する元の情報を知ることはできません。

テキストの段落を与えられ、テキストの最後に nonce を追加することを許可されている場合、テキストと nonce の最初の 10 のハッシュ値が 0 であることを確実にする方法は、異なる数字を試し続け、できるだけ早く正しく推測するのに十分な幸運があることを期待する以外には見つけることができません。

さて、このすべてのコーディングの部分に入りましょう。まず最初に ブロックにnonceを追加してみましょう。まず、前のブロックのハッシュ値が固定されていることに注意してください。

calculateHash() {
        return SHA256(this.previousHash + this.timestamp + JSON.stringify(this.data)).toString();
  }

この値は変更できません。この数学の問題を解くには、手動でブロックに nonce を追加する必要があります。

constructor(timestamp, data, previousHash = '') {
    this.timestamp = timestamp;
    this.data = data;
    this.previousHash = previousHash;
    this.nonce = 0;
    this.hash = this.calculateHash();
}

「nonce」には特別な意味はありません。ハッシュ値が要件を満たすように生成されたハッシュ値を変更するために追加されます。

それに応じてハッシュ値の計算処理を変更する必要があります。

calculateHash() {
    return SHA256(this.previousHash + this.timestamp + JSON.stringify(this.data) + this.nonce).toString();
}

そこで、前に説明したように、数学の問題は、ハッシュの最初の10桁が0になるようにnonceを変更してみることです。 そのために、対応するコードは以下のようになります。

this.hash.substring(0, 10) === Array(10 + 1).join("0")

ハッシュの最初の10桁が0であることは、これにより最初の10桁または最初の5桁が0であることが確実になるので好ましい。 まあ、桁数によって難易度は異なる。ハッシュの最初の10桁が0であることを保証するのは、最初の5桁よりもはるかに難しいです。桁数は難易度として見ることができます。マイニングとは、ハッシュ値を準拠させるために、さまざまなnonceを試すことです。

mineBlock(difficulty) {
    while (this.hash.substring(0, difficulty) !== Array(difficulty + 1).join("0")) {
        this.nonce++;
        this.hash = this.calculateHash();
    }
    console.log("Block mined, nonce: " + this.nonce + ", hash: " + this.hash);
}

シンプルにするために、難易度はブロックチェーンの固定パラメータとして使用することができます。
注:実際には、ビットコインの難易度は、ブロック生成時間を約10分に保つように動的に調整されています。

constructor() {
    this.chain = [this.createGenesisBlock()];
    this.difficulty = 2;
}

ブロックを追加することは、もはや直接ブロックを追加することではありません。その代わり、ブロックの追加処理はマイニングになりました。

addBlock(newBlock) {
    newBlock.previousHash = this.getLatestBlock().hash;
    newBlock.mineBlock(this.difficulty);
    this.chain.push(newBlock);
}

準拠したブロックのみを追加することができます。では、以下のコードを実行してみましょう。

let simpleChain = new BlockChain();
console.log("Mining block 1...");
simpleChain.addBlock(new Block("2018-11-11 00:00:01", {amount: 10}));
console.log("Mining block 2...");
simpleChain.addBlock(new Block("2018-11-11 00:00:02", {amount: 20}));


console.log(JSON.stringify(simpleChain, null, 4));

ブロックの生成がかなり遅くなっていることがわかります。

ali-186590cc4a7f:simple-chain shanyao$ node main_2.js 
Mining block 1...
Block mined, nonce: 464064, hash: 0000e7e1aae4fae9d245f8d4b8ce030ffe13270218c362511db6840a824a1cdb
Mining block 2...
Block mined, nonce: 4305, hash: 000047b449537483d7f2861a12b53a59c971d3a928b2c0110a5945bff1a82616
{
    "chain": [
        {
            "timestamp": 0,
            "data": "2018-11-11 00:00:00",
            "previousHash": "Genesis block of simple chain",
            "nonce": 0,
            "hash": "8a7b66d194b1b0b795b0c45b3f11b60e8aa97d3668c831f39ec3343c83ae41c0"
        },
        {
            "timestamp": "2018-11-11 00:00:01",
            "data": {
                "amount": 10
            },
            "previousHash": "8a7b66d194b1b0b795b0c45b3f11b60e8aa97d3668c831f39ec3343c83ae41c0",
            "nonce": 464064,
            "hash": "0000e7e1aae4fae9d245f8d4b8ce030ffe13270218c362511db6840a824a1cdb"
        },
        {
            "timestamp": "2018-11-11 00:00:02",
            "data": {
                "amount": 20
            },
            "previousHash": "0000e7e1aae4fae9d245f8d4b8ce030ffe13270218c362511db6840a824a1cdb",
            "nonce": 4305,
            "hash": "000047b449537483d7f2861a12b53a59c971d3a928b2c0110a5945bff1a82616"
        }
    ],
    "difficulty": 4
}

この例では、難易度は4なので、ブロックハッシュ値の最初の4桁が0になります。

#プルーフ・オブ・ワーク(PoW)とは
上記は、Proof of Work(通常はPoWと略される)という概念の一例です。この概念は単純なように見えますが、いくつかのブロックチェーンで非常に効果的で安全であることが証明されています。例えば、ビットコインのPoWシステムは、人間の介入なしに数百万台のマシン間の一貫性を保証しており、10年以内に一度もエラーが発生したことがありません。これは間違いなく奇跡的なことだと言えるでしょう。実際には、このシンプルなコンセプトの背後にはもっと多くのことがあります。PoWの核となっているのは、CPU(パワー)を使って自分の意見や見解を表明する「one-cpu-one-vote方式」です。なぜone-cpu-one-voteではなく、one-cpu-one-voteなのでしょうか?それは、非常に安価に大量のIPアドレスをシミュレーションできるからですね。当時のPoWではCPUが使われていました(2008年の例)が、CPUのリソースはかなり高価で、マイニングの難易度や公平性を確保できるからです(サトシ・ナカモト氏のビットコインホワイトペーパー参照)。もちろん、その時に中本聡が気づかなかったのは、ASIC(Application Specific Integrated Circuit)のような特定のアルゴリズムチップの誕生により、通常のCPUによるマイニングがますます困難になってきているということです。これについては、スペースの都合上、この記事ではこれ以上説明は致しません。

PoWの根本的な部分は何でしょうか?それは、PoWがアンカーを提供することです。つまり、PoWは仮想世界のビットコインと物理世界のCPUの間にアンカーを実装し、物理世界の高価なリソースを使ってビットコインのセキュリティを確保しています。ビットコインのマイニングは多くの電力を消費し、無駄な作業だと言う人もいるかもしれません。しかし、この見方はかなり偏っています。実際には、ビットコインは世界で最も安価な通貨システムかもしれません。結局のところ、世界中の多くの通貨の循環は、その通貨の価値を守り、維持するために、膨大な費用をかけた行政機関に支えられてきました。しかし、ビットコインはコンピューティングパワーと電気を消費するだけで、コンピューティングパワーが大きければ大きいほど安全なビットコインシステムになるので、この消費は全く意味がないわけではありません。

実際、PoWに加えて、一般的なコンセンサスメカニズムとしては、DPOS(delegate-proof-of-stake)、プライベートブロックチェーンで広く使われているPBFT(Practical Byzantine Fault Tolerance)、Raftなどがあります。これらのコンセンサスメカニズムは、この記事の範囲内ではありません。

#利益重視のブロックチェーン開発
上で説明したブロックチェーンの仕組みはあまりにも単純で、以下のような大きな問題点があります。

  • 各ブロックには1つのトランザクションしか含まれていません。これは、高いトランザクションコストにつながる。実際のシナリオでは、ブロックチェーンの各ブロックには複数のトランザクションが含まれており、それらは同時に1つのブロックにまとめられています。
  • マイニングに報酬が提供されない マイニングに報酬が提供されないとしたら、誰がマイニングマシンを購入し、電気代を負担し、取引を検証してパッケージ化してくれるだろうか?考えてみてください。世界には、多少の犠牲を払うことを厭わない無私の人々が必要なのは間違いありませんが、自己犠牲だけでは一定の進歩を維持することはできませんよね?完全に正直に言うと、世界の進歩は明らかに利益によっても駆動されています。合理的なブロックチェーンのシステム設計とインセンティブ制度は、ブロックチェーンの安定性を保つための基本です。実際、多くのPoWベースの暗号通貨で使われているのはマイニングという手法だけです。例えば、可能なビットコインは全部で2100万個しかありません。これらのビットコインは、マイニングされた後、セカンダリーマーケットでのみ流通することができます。

以下では、この2つの大きな問題を解決するための解決策を見ていきます。

まず、トランザクションを定義することです。トランザクションに含まれる基本的な情報は、取引金額と当事者2人です。

class Transaction {
    constructor(fromAddress, toAddress, amount) {
        this.fromAddress = fromAddress;
        this.toAddress = toAddress;
        this.amount = amount;
    }
}

各ブロックには複数のトランザクションが含まれている必要がありますので、トランザクションと正確に一致するように前のデータを更新します。

class Block {
    constructor(timestamp, transactions, previousHash = '') {
        this.timestamp = timestamp;
        this.transactions = transactions;
        this.previousHash = previousHash;
        this.nonce = 0;
        this.hash = this.calculateHash();
    }
    ....
}

ブロックチェーンのデータ構造もそれに応じてアップグレードする必要があります。処理すべき取引と、マイニング作業ごとの報酬額を追加するべきです。

class BlockChain {
    constructor() {
        this.chain = [this.createGenesisBlock()];
        this.difficulty = 3;
        this.pendingTransactions = [];
        this.miningReward = 100;
    }
    ....
}

構造的な関係に注意してください。

  • 1つのチェーンには複数のブロックが含まれています。
  • 1つのブロックには複数のトランザクションが含まれています。
    次に、マイニングについて見てみましょう。したがって、先ほどのaddBlockメソッドをminePendingTransactionsに変更する必要がありますが、これは主に以下の点で前者とは異なります。
  • 追加された新しいブロックは、通常のブロックではなく、すべての保留中のトランザクションに関する情報を含むブロックです。単純化のために、すべての保留中のトランザクションは1つのブロックにパッケージ化されています。しかし、実際のシナリオではそうではありません。例えば、元のビットコインブロックは、すべての保留中のトランザクションを含むには小さすぎます(1 MB)。1つのパッケージ化操作の後に残ったトランザクションは、別のブロックにパッケージ化されなければなりません。さらに、採掘者は通常、採掘者手数料の高い取引を優先します。
  • 鉱夫報酬を支払う。一般的に、鉱夫がブロックを採掘した後、鉱夫アドレスへの転送トランザクションのバッチが生成され、転送トランザクションは次のブロックにパッケージ化されます。
    例えば、以下のようなものです。
//Incoming miner address
minePendingTransactions(miningRewardAddress) {
    // Pakcage all pending transactions together in the same block
    let block = new Block(Date.now(), this.pendingTransactions);
    // Mining, that is, constantly trying nonce to make the hash Vluw meet the requirements
    block.mineBlock(this.difficulty);

    console.log('Block successfully mined!');
    this.chain.push(block);
    
    // Put the miner fee transaction into pendingTransactions for the next processing operation. The miner fee transaction is characterized by the source account being empty.
    this.pendingTransactions = [
        new Transaction(null, miningRewardAddress, this.miningReward)
    ];
}
// Create a transaction, put the transaction into the pending transaction pool.
createTransaction(transaction) {
    this.pendingTransactions.push(transaction);
}

次に、残高を照会することができます。転送取引以外にも、人によっては口座の残高を確認したいと思うことがあるかもしれません。しかし、ブロックチェーンには実在する口座は存在しません。一般的なアカウントモデルには、ビットコインのUTXOモデルやEthereumのアカウント/残高モデルがあります。どうやら、私たちの例では、本物のアカウントも存在しないようです。この例では、ブロックチェーンのトランザクションは、トランザクションの金額と2つのトランザクション当事者を記録するだけです。口座残高は記録されていません。では、このような場合、どのようにして口座の残高を知ることができるのでしょうか?最も手間のかかる方法は、ブロックチェーン上のすべてのトランザクション情報を反復処理し、from、to、金額の情報から口座の残高を推測することです。

getBalanceOfAddress(address) {
    let balance = 0;
    for (const block of this.chain) {
        for (const transaction of block.transactions) {
            //账户转出,余额减少
            if (transaction.fromAddress === address) {
                balance -= transaction.amount;
            }
            //账户转入,余额增加
            if (transaction.toAddress === address) {
                balance += transaction.amount;
            }
        }
    }
    return balance;

さて、譲渡取引は成功したのでしょうか?鉱夫は鉱夫報酬を受け取りましたか?コードを実行してどうなるか見てみましょう。

let simpleCoin = new BlockChain();
// First, create two transactions, one where address1 first transfers 100 to address2 and another where address2 transfers 60 to address1.
simpleCoin.createTransaction(new Transaction('address1', 'address2', 100));
simpleCoin.createTransaction(new Transaction('address2', 'address1', 60));

console.log('starting the miner...');
simpleCoin.minePendingTransactions('worker1-address');
// Then, if these actions succeed, this means that address2 will have 40.
console.log('Balance of address2 is: ', simpleCoin.getBalanceOfAddress('address2'));
// How much should a miner account then? For this transaction, the appropriate miner fee is 100.
console.log('Balance of miner is: ', simpleCoin.getBalanceOfAddress('worker1-address'));

// Create another transaction where address2 transfers 10 to address1.
simpleCoin.createTransaction(new Transaction('address2', 'address1', 10));

console.log('starting the miner again...');
simpleCoin.minePendingTransactions('worker1-address');
// Then, if this transfer was successful, then address2 will have 30.
console.log('Balance of address2 is: ', simpleCoin.getBalanceOfAddress('address2'));
// So now how much should a miner fee be then? Having handled two blocks, it should be set at 200, right?
console.log('Balance of miner is: ', simpleCoin.getBalanceOfAddress('worker1-address'));

結果は以下のようになります。

ali-186590cc4a7f:simple-chain shanyao$ node main_3.js 
starting the miner...
Block mined, nonce: 2121, hash: 000cd629157ee59494dfc08329d4cf265180c26010935993171b6881f9bae578
Block successfully mined!
Balance of address2 is:  40
Balance of miner is:  0
starting the miner again...
Block mined, nonce: 1196, hash: 000d5f8278ea9bf4f30c9cc05b4cc36aab8831dc5860e42c775360eb85bc238e
Block successfully mined!
Balance of address2 is:  30
Balance of miner is:  100

address2のバランスは予想通りです。しかし、ブロックの追加に成功した後もマイナの残高は0のままです。マイナーが手数料を受け取っていないのはなぜでしょうか?これは、マイナーの手数料は次のブロックに投入され、現在のブロックのマイナーは、ブロックが追加された後でなければ、マイナーの手数料を受け取ることができないからです。

#トランザクションの署名と検証
前述の通り、誰でも取引を開始できるようです。例えば、中本聡さんの口座から私の口座に100枚のコインを振り込む取引を始めたいとします。これは実現可能でしょうか?上で使ったモデルを使えば、誰でも取引を開始できるようです。しかし、これは完全に現実に沿ったものではありません。これまでのところ、重要なことを一つだけ説明していませんが、それは取引のサインをすることです。

そこで、パスワード、さらに重要なのは取り返しのつかないパスワードについて説明しましょう。パスワードは現実の世界では一般的なものです。例えば、クレジットカードやソーシャルメディア、Eコマースサイトのアカウントのパスワードを持っていたり、自動ゲートコントロールのパスワードを持っていたりします。これらのパスワードは安全に管理しなければなりません。そうでなければ、これらのパスワードがたまたま漏れてしまった場合、資産の損失や個人情報が流出してしまう可能性があります。クレジットカードのパスワードが盗まれたと思ったら、すぐにパスワードを変更する必要があります。パスワードを忘れてしまった場合は、IDカードを持って銀行に行き、新しいパスワードを設定することができます。

しかし、ブロックチェーンの世界では、単純にパスワードを修正したり、取得したりすることはできません。さらに、ブロックチェーンの世界にはIDカードは存在しません。むしろ、パスワードそのもの、つまり秘密鍵がブロックチェーンではあなたのIDカードとみなされます。

では、非対称暗号化について話しましょう。ブロックチェーンにおける唯一のパスワードは、秘密鍵と呼ばれています。では、秘密鍵とは何か、秘密鍵はどのように生成されるのでしょうか?秘密鍵は非対称暗号化によって生成されます。非対称暗号化では、暗号化と復号に異なる鍵を使用します。複雑に聞こえますが、原理はとてもシンプルです。

  • 非対称暗号化アルゴリズムは、一対の鍵を生成します。1つは公開鍵で、もう1つは秘密鍵です。2 つの鍵は、データを暗号化したり復号化したりすることができます。対応する秘密鍵のみが公開鍵で暗号化されたデータを復号化でき、対応する公開鍵のみがユーザの秘密鍵で署名され暗号化されたデータを復号化できる。
  • 秘密鍵は対応する公開鍵から推論することはできませんが、公開鍵は対応する秘密鍵から推論することができます。ほとんどのRivest, Shamir, and Adelman (RSA)暗号システムの実装は、公開鍵暗号化標準(PKCS)に従っていますが、これは公開鍵は秘密鍵から推論できますが、秘密鍵は公開鍵から推論できないことを意味しています。
  • 公開鍵は暗号化に、秘密鍵は復号に使用されます。公開鍵で暗号化されたデータは、対応する秘密鍵でのみ復号化することができます。公開鍵はメールアドレスのようなもので、誰もが知ることができ、誰もが電子メールを送信することができます。しかし、電子メールの所有者だけがパスワードを持ち、電子メールアカウントにログオンすることができます。
  • 秘密鍵は署名に、公開鍵は検証に使われます。例えば、AがBから手紙を受け取ったとき、この手紙が本当にBが書いたものであることをAはどうやって確認できるでしょうか?それは、Bが秘密鍵を使って手紙に署名したからです。(これが実際の暗号化処理です)。Aが手紙を受け取ると、AはBが公開した公開鍵を使って手紙を復号化します。復号化に成功すれば、Aはこの手紙がBから送られてきたものであることを確認できます。

公開鍵と秘密鍵のペアを生成してみましょう。

const EC = require('elliptic').ec;
const ec = new EC('secp256k1');

const key = ec.genKeyPair();
const publicKey = key.getPublic('hex');
const privateKey = key.getPrivate('hex');

console.log('Public key', publicKey);
console.log('Private key', privateKey);

その結果、以下のようになりました。

Public key 04f1aa4d934e7f2035a6c2a2ebc9daf0e9ca7d13855c2a0fb8696ab8763e5ee263c803dfa7ac5ae23b25fb98151c99f91c55e89586717965758e6663772ebccd1b
Private key 1c258d67b50bda9377c1badddd33bc815eeac8fcb9aee5d097ad6cedc3d2310c

秘密鍵は32バイトで、あなただけのパスワードです。公開鍵の方が長いようです。しかし、なぜビットコインのアドレスは短いのが普通なのでしょうか?公開鍵は65バイトで、最初のバイトは固定(0x04)です。残りの64バイトのうち、最初の32バイトが楕円曲線のX座標、最後の32バイトが楕円曲線のY座標となっています。ビットコインアドレスは、1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNaのようなBase58アドレスが生成される前に、文字列がSHA256暗号化、RIPEMD160暗号化、Base58エンコーディングのような一連の変換を経験するため、より短くなります。簡単にするために、この記事ではアドレス生成については説明しません。一般的には、公開鍵とは形式が異なるだけで、逆変換が可能な自分の口座アドレスと考えることができます。

これで、トランザクションに署名することができます。そのためには、まず、取引が本当に自分で始めたものであることを証明するために、自分の秘密鍵を使って取引に署名する必要があります。

class Transaction {
    // Calculate the hash in order to do the signature. You need to do this because the hash value will be signed instead of the original information directly. 
    calculateHash() {
        return SHA256(this.fromAddress + this.toAddress + this.amount).toString();
    }

    // Incoming key
    signTransaction(signingKey) {
        // Verify if the source account is the person's address, or more specifically, verify whether the source address is the public key corresponding to the private key.
        if (signingKey.getPublic('hex') !== this.fromAddress) {
            throw new Error('You cannot sign transactions for other wallets!')
        }
        const txHash = this.calculateHash();
        // Sign the transaction hash with the private key.
        const sig = signingKey.sign(txHash, 'base64');
        // Convert the signature to the DER format.
        this.signature = sig.toDER('hex');
        console.log("signature: "+this.signature)
    }
    ...
}

次に、トランザクションの検証を行います。トランザクション情報を受け取った相手側は、署名が正しいことを確認するために送信元アカウントの公開鍵を使用して、fromAddressからこのトランザクションが有効であるかどうかを検証する必要があります。

class Transaction {
    isValid() {
        // The miner fee transaction fromAddress is empty, verification cannot be completed.
        if (this.fromAddress === null) return true;
        // Determine if the signature exists
        if (!this.signature || this.signature.length === 0) {
            throw new Error('No signature in this transaction');
        }
        // Transcode fromAddress to get the public key (this process is reversible, as it is just a format conversion process.)
        const publicKey = ec.keyFromPublic(this.fromAddress, 'hex');
        // Use the public key to verify if the signature is correct, or more specifically if the transaction was actually initiated from fromAddress.
        return publicKey.verify(this.calculateHash(), this.signature);
    }
    ...
}

前述のサンプルでは、1 つのトランザクションの有効性を検証しています。ブロックには複数のトランザクションが含まれているため、ブロック内のすべてのトランザクションを検証するメソッドを追加する必要があります。

class Block {
    hasValidTransactions() {
        // Traverse all transactions within the block, verifying them one by one
        for (const tx of this.transactions) {
            if (!tx.isValid()) {
                return false;
            }
        }
        return true;
    }
    ...
}

これに対応して、createTransactionはaddTransactionに置き換える必要があります。つまり、トランザクションを直接作成することはできなくなりました。その代わりに、最初に署名されたトランザクションを検証してから、有効なトランザクションのみを送信する必要があります。例を示します。

class BlockChain {
    addTransaction(transaction) {
        if (!transaction.fromAddress || !transaction.toAddress) {
            throw new Error('Transaction must include from and to address');
        }
        // Verify that the transaction is valid and valid before it can be submitted to the trading pool.
        if (!transaction.isValid()) {
            throw new Error('Cannot add invalid transaction to the chain');
        }
        this.pendingTransactions.push(transaction);
    }
    ...
}

これに対応して、ブロックチェーンの isChainValid メソッドは、ブロック内のすべてのトランザクションを検証するために使用する必要があります。

class BlockChain {
    isChainValid() {
        for (let i = 1; i < this.chain.length; i++) {
            const currentBlock = this.chain[i];
            const previousBlock = this.chain[i - 1];
            // Check if all transactions in the block are valid.
            if (!currentBlock.hasValidTransactions()) {
                return false;
            }
            if (currentBlock.hash !== currentBlock.calculateHash()) {
                console.error("hash not equal: " + JSON.stringify(currentBlock));
                return false;
            }
            if (currentBlock.previousHash !== previousBlock.calculateHash()) {
                console.error("previous hash not right: " + JSON.stringify(currentBlock));
                return false;
            }
        }
        return true;
    }
    ...
}

今すぐ以下のコードを実行してください。

const {BlockChain, Transaction} = require('./blockchain');
const EC = require('elliptic').ec;
const ec = new EC('secp256k1');
// Generate a pair of private and public keys with tools.
const myPrivateKey = '1c258d67b50bda9377c1badddd33bc815eeac8fcb9aee5d097ad6cedc3d2310c';
const myPublicKey = '04f1aa4d934e7f2035a6c2a2ebc9daf0e9ca7d13855c2a0fb8696ab8763e5ee263c803dfa7ac5ae23b25fb98151c99f91c55e89586717965758e6663772ebccd1b';

const myKey = ec.keyFromPrivate(myPrivateKey);
// Derive the public key from the private key
const myWalletAddress = myKey.getPublic('hex');
// Looking at the output, I did get the public key from the private key.
console.log("is the myWalletAddress from privateKey equals to publicKey?", myWalletAddress === myPublicKey);

let simpleCoin = new BlockChain();

const alicePublicKey = '047058e794dcd7d9fb0a256349a5e2d4d724b50ab8cfba2258e1759e5bd4c81bb6ac1b0490518287ac48f0f10a58dc00cda03ffd6d03d67158f8923847c8ad4e7d';
// Initiate a transaction and transfer 60 from your own account to Alice's account.
const tx1 = new Transaction(myWalletAddress, alicePublicKey, 60);
// Sign with private key
tx1.signTransaction(myKey);
// Submit a transaction
simpleCoin.addTransaction(tx1);

console.log('starting the miner...');
simpleCoin.minePendingTransactions(myWalletAddress);
// If the transfer is successful, the balance of Alice's account will be 60.
console.log('Balance of Alice's account is: ', simpleCoin.getBalanceOfAddress(alicePublicKey));


// Initiate a transaction and transfer back to your account from Alice's account
const tx2 = new Transaction(alicePublicKey, myWalletAddress, 20);
// You still sign with your private key, but doing so will cause an error. Because you don't know Alice's key, you cannot operate your account. That is, your key cannot open Alice's account.
tx2.signTransaction(myKey);
simpleCoin.minePendingTransactions(myWalletAddress);
console.log('Balance of Alice's account is: ', simpleCoin.getBalanceOfAddress(alicePublicKey));

結果は以下のようになります。

ali-186590cc4a7f:simple-chain shanyao$ node main.js 
is the myWalletAddress from privateKey equals to publicKey? true
signature: 3045022100b87a9199c2b3fa31ac4092b27a41a616d99df884732dfd65972dc9eacd12da7702201f7957ef25d42c17cb2f6fb2888e6a0d5c521225d9b8851ba2d228f96d878f85
starting the miner...
Block mined, nonce: 15812, hash: 00081837c2ae46a1310a0873f5e3d6a1b14b072e3d32a538748fac71e0bfd91e
Block successfully mined!
Balance of trump is:  60
/Users/shanyao/front/simple-chain/blockchain.js:22
            throw new Error('You cannot sign transactions for other wallets!')

最初の取引では、自分のアカウントを操作して署名します。あなたは秘密鍵を持っているので、トランザクションは成功しています。2番目の保留中のトランザクションでは、あなたは相手のアカウントを操作し、秘密鍵が間違っているため、転送トランザクションを送信することができませんでした。秘密鍵はあなたの唯一のパスワードとパスポートです。秘密鍵を持っていないと対応する資産を所有することができません。これが本当の意味での私有財産の不可侵性なのかもしれません。

#概要
さて、記事を読んでいただいた方は、ブロックチェーンはどれくらいシンプルなものだと思いますか?ブロックチェーンは思っているほど複雑ではありません。しかし、とても単純なものではないのも事実です。実際、コアとなるコンセンサス機構(または分散型コンセンサス)や分散型ブロックチェーンガバナンスについては、この記事では解説していません。この記事では、ブロックチェーンの基本的な構造、基本的な概念、一般的なトランザクションプロセスについて簡単に説明し、ブロックチェーンについての予備的な理解を得ることができるようにしているに過ぎません。ブロックチェーン自体は幅広いトピックであり、より多くの研究と探求が必要です。

アリババクラウドは日本に2つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
アリババクラウドジャパン公式ページ

5
9
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?