diff --git a/src/js/account.js b/src/js/account.js index 73f0eff..9ccd65b 100644 --- a/src/js/account.js +++ b/src/js/account.js @@ -5,13 +5,18 @@ class Account { this.keypair_ = keypair; this.id_ = id; this.amount_ = 0; - if (!this.keypair_) { } - if (!this.id_) { } + if (!this.keypair_) { + // load from file + } + if (!this.id_) { + // load from file + } } get_id() { return this.id_; } get_key() { return this.keypair_; } get_amount() { return this.amount_; } + set_amount(amount) {this.amount_ = amount;} } module.exports = Account; \ No newline at end of file diff --git a/src/js/block.js b/src/js/block.js index c962736..593c470 100644 --- a/src/js/block.js +++ b/src/js/block.js @@ -1,14 +1,14 @@ 'use strict'; var EventEmitter = require('events').EventEmitter; -var crypto = require("crypto"); -var ed = require("ed25519"); +var Crypto = require("./crypto"); + class Block extends EventEmitter { constructor(data, consensus) { super(); // body - this.transcations_ = data ? data.transactions : []; + this.transactions_ = data ? data.transactions : []; // header this.version_ = 0; this.height_ = data ? data.previous_block.height + 1 : -1; @@ -37,7 +37,7 @@ class Block extends EventEmitter { get_timestamp() { return this.timestamp_; } get_signature() { return this.block_signature_; } get_publickey() { return this.generator_publickey_; } - get_transcations() { return this.transcations_; } + get_transactions() { return this.transactions_; } get_consensus_data() { return this.consensus_data_; } set_consensus_data(data) { this.consensus_data_ = data; } toObject() { @@ -51,7 +51,7 @@ class Block extends EventEmitter { "hash": this.hash_, "block_signature": this.block_signature_, "consensus_data": this.consensus_data_, - "transcations": this.transcations_ + "transactions": this.transactions_ }; return block; } @@ -65,23 +65,20 @@ class Block extends EventEmitter { this.hash_ = data.hash; this.block_signature_ = data.block_signature; this.consensus_data_ = data.consensus_data; - this.transcations_ = data.transactions; + this.transactions_ = data.transactions; } - calc_hash(data) { - return crypto.createHash('sha256').update(data).digest('hex'); - } calc_merkle_hash() { - // calc merkle root hash according to the transcations in the block + // calc merkle root hash according to the transactions in the block var hashes = []; - for (var i = 0; i < this.transcations_.length; ++i) { - hashes.push(this.calc_hash(this.transcations_.toString('utf-8'))); + for (var i = 0; i < this.transactions_.length; ++i) { + hashes.push(Crypto.calc_hash(this.transactions_.toString('utf-8'))); } while (hashes.length > 1) { var tmp = []; for (var i = 0; i < hashes.length / 2; ++i) { let data = hashes[i * 2] + hashes[i * 2 + 1]; - tmp.push(this.calc_hash(data)); + tmp.push(Crypto.calc_hash(data)); } if (hashes.length % 2 === 1) { tmp.push(hashes[hashes.length - 1]); @@ -93,8 +90,8 @@ class Block extends EventEmitter { prepare_data() { let tx = ""; - for (var i = 0; i < this.transcations_.length; ++i) { - tx += this.transcations_[i].toString('utf-8'); + for (var i = 0; i < this.transactions_.length; ++i) { + tx += this.transactions_[i].toString('utf-8'); } let data = this.version_.toString() + this.height_.toString() @@ -109,11 +106,11 @@ class Block extends EventEmitter { } // calc the hash of the block calc_block_hash() { - return this.calc_hash(this.prepare_data()); + return Crypto.calc_hash(this.prepare_data()); } sign(keypair) { var hash = this.calc_block_hash(); - return ed.Sign(Buffer.from(hash, 'utf-8'), keypair).toString('hex'); + return Crypto.sign(keypair, hash); } make_proof(consensus, keypair) { let self = this; @@ -128,13 +125,7 @@ class Block extends EventEmitter { static verify_signature(block) { var hash = block.hash; - var res = ed.Verify(Buffer.from(hash, 'utf8'), Buffer.from(block.block_signature, 'hex'), Buffer.from(block.generator_publickey, 'hex')); - return res; - - } - - static get_address_by_publickey(publicKey) { - + return Crypto.verify_signature(hash, block.block_signature, block.generator_publickey); } } diff --git a/src/js/blockchain.js b/src/js/blockchain.js index 222a748..6886ce2 100644 --- a/src/js/blockchain.js +++ b/src/js/blockchain.js @@ -4,17 +4,22 @@ var Block = require("./block"); const genesis_block = require("./genesis_block.json"); var Node = require("./network"); var Account = require("./account"); -var Transaction = require("./transaction"); +var Transaction = require("./transaction").Transaction; +var TxInput = require("./transaction").TxInput; +var TxOutput = require("./transaction").TxOutput; var Msg = require("./message"); var MessageType = require("./message").type; var Promise = require("bluebird"); +var level = require("level"); +var Crypto = require("./crypto"); var Pbft = require("./consensus/pbft"); -let pbft = true; +let pbft = false; class BlockChain { constructor(Consensus, keypair, id, is_bad = false) { // todo this.pending_block_ = {}; + this.tx_pool = {}; this.chain_ = []; this.is_bad_ = is_bad; @@ -22,14 +27,26 @@ class BlockChain { // /////////////////////////////////////// this.genesis_block_ = genesis_block; - this.last_block_ = genesis_block; - this.save_last_block(); + this.account_ = new Account(keypair, id); this.consensus_ = new Consensus(this); this.node_ = null; } - start() { + async start() { + this.db_ = level(`/tmp/data_${this.get_account_id()}`); + try { + // load blocks + let last = await this.db_.get("last_block"); + this.last_block_ = JSON.parse(last); + console.log(`node: ${this.get_account_id()} last block: ${this.last_block_.height}`); + } catch (err) { + // empty chain + this.last_block_ = genesis_block; + this.save_last_block(); + console.log(`node: ${this.get_account_id()} empty`); + } + this.node_ = new Node(this.get_account_id()); this.node_.on("message", this.on_data.bind(this)); this.node_.start(); @@ -57,18 +74,40 @@ class BlockChain { cb(); } - save_last_block() { + async save_last_block() { // query from db via hash // if not exist, write into db, else do nothing - // todo(tx is also need to store?) if (this.pending_block_[this.last_block_.hash]) { delete this.pending_block_[this.last_block_.hash]; } - this.chain_.push(this.last_block_); + await this.db_.put(this.last_block_.hash, JSON.stringify(this.last_block_)); + await this.db_.put("last_block", JSON.stringify(this.last_block_)); + // console.log(`save block: ${this.last_block_.hash} to db`); + + // tx + if (!this.last_block_.transactions) { + return; + } + for (var i = 0; i < this.last_block_.transactions.length; ++i) { + let tx = this.last_block_.transactions[i]; + if (this.tx_pool[tx.id]) { + delete this.tx_pool[tx.id]; + // console.log(`node ${this.get_account_id()} delete tx ${tx.id}`); + } + await this.db_.put(tx.id, JSON.stringify(tx)); + } } generate_block(keypair, cb) { - // load transcations - var tx = []; + // load transactions + var tx = [this.create_coinbase()]; + var i = 0; + for (let key in this.tx_pool) { + if (i == 10) + break; + tx.push(this.tx_pool[key]); + i++; + console.log(`node ${this.get_account_id()} load tx ${key}`); + } // create block let block = new Block({ "keypair": keypair, @@ -79,7 +118,7 @@ class BlockChain { let self = this; block.on('block completed', (data) => { if (data.height == self.last_block_.height + 1) { - console.log("block completed"); + // console.log("block completed"); self.commit_block(data); self.broadcast(Msg.block(data)); @@ -113,15 +152,32 @@ class BlockChain { get_height() { return this.last_block_.height; } - get_block(hash) { + async get_from_db(hash) { // query block with hash value - // todo - for (var i = 0; i < this.chain_.length; ++i) { - if (this.chain_[i] == hash) { - return this.chain_[i]; - } + try { + let block_data = await this.db_.get(hash); + let block = JSON.parse(block_data); + return block; + } catch (err) { + return null; } - return null; + } + async iterator_back(cb, hash) { + if (!hash) { + return; + } + let block = await this.get_from_db(hash); + let res = cb(block); + if (res) + await this.iterator_back(cb, block.previous_hash); + } + async iterator_forward(cb, hash) { + if (!hash) { + return; + } + let block = await this.get_from_db(hash); + await this.iterator_forward(cb, block.previous_hash); + cb(block); } get_last_block() { return this.last_block_; @@ -140,41 +196,89 @@ class BlockChain { get_account_keypair() { return this.account_.get_key(); } + get_public_key() { + return this.get_account_keypair().publicKey.toString('hex'); + } broadcast(data) { this.node_.broadcast(data); } list_peers() { return this.node_.list_peers(); } + async verify_transaction(tx) { + let input_amount = 0; + for (var i = 0; i < tx.input.length; ++i) { + let input = tx.input[i]; + // coinbase + if (input.id == null) { + // todo check milestone + if (tx.output[0].amount == 50) { + return true; + } else { + return false; + } + } + let vout = null; + if (this.tx_pool[input.id]) { + vout = this.tx.tx_pool[input.id]; + } else { + vout = await this.get_from_db(input.id); + } + if (!vout) { + // invalid vout + return false; + } + vout = vout.output[input.index]; + let res = Crypto.verify_signature(JSON.stringify(vout), input.ScriptSig, vout.ScriptPubKey); + if (!res) { + return false; + } + input_amount += vout.amount; + } + let output_amount = 0; + for (i = 0; i < tx.output.length; ++i) { + output_amount += tx.output[i].amount; + } + if (input_amount < output_amount) { + return false; + } + return true; + } // verify the block is valid - verify(block) { + async verify(block) { // verify the block signature if (!Block.verify_signature(block)) return false; // verify consensus if (!this.consensus_.verify(block)) return false; - // verify transcations - let tx = block.transcations; - for (var i = 0; i < tx.length; ++i) { - // todo (check tx is exist and valid) - if (!Transaction.verify(tx[i])) - return false; + // verify transactions + let tx = block.transactions; + if (tx) { + for (var i = 0; i < tx.length; ++i) { + if (!await this.verify_transaction(tx[i])) + return false; + } } return true; } - on_data(msg) { + async on_data(msg) { switch (msg.type) { case MessageType.Block: { let block = msg.data; // console.log(`node: ${this.get_account_id()} receive block: height ${block.height}`); // check if exist - if (this.pending_block_[block.hash] || this.get_block(block.hash)) + let query = await this.get_from_db(block.hash); + if (this.pending_block_[block.hash] || query) { + // console.log("block already exists"); return; + } // verify - if (!this.verify(block)) + if (!await this.verify(block)) { + // console.log("verify failed"); return; + } this.pending_block_[block.hash] = block; @@ -195,6 +299,22 @@ class BlockChain { case MessageType.Transaction: { // check if exist(pending or in chain) verify, store(into pending) and broadcast + let tx = msg.data; + if (this.tx_pool[tx.id]) { + // already exists + return; + } + this.tx_pool[tx.id] = tx; + // verify transaction + let res = await this.verify_transaction(tx); + if (!res) { + delete this.tx_pool[tx.id]; + } else { + // console.log(`node ${this.get_account_id()} store tx ${tx.id}`); + } + + // broadcast + this.broadcast(msg); } break; default: @@ -218,7 +338,7 @@ class BlockChain { } async fork() { console.log('----------fork----------'); - // load transcations + // load transactions var tx1 = [{ amount: 1000, recipient: 'bob', @@ -242,7 +362,7 @@ class BlockChain { }); }); - // load transcations + // load transactions var tx2 = [{ amount: 1000, recipient: 'cracker', @@ -279,6 +399,91 @@ class BlockChain { console.log("fork"); this.commit_block(block_data1); } + create_coinbase() { + let input = new TxInput(null, -1, `${new Date()} node: ${this.get_account_id()} coinbase tx`); + let output = new TxOutput(50, this.get_public_key()); + let tx = new Transaction([input], [output]); + return tx; + } + + async get_utxo(cb) { + let publicKey = this.get_public_key(); + let spentTXOs = {}; + await this.iterator_back((block) => { + let txs = block.transactions; + // tx + for (var i = 0; i < txs.length; ++i) { + let tx = txs[i]; + let transaction_id = tx.id; + // output + for (var j = 0; j < tx.output.length; ++j) { + let output = tx.output[j]; + // owns + if (output.ScriptPubKey == publicKey) { + // not spent + if (spentTXOs.hasOwnProperty(transaction_id) && + spentTXOs[transaction_id].hasOwnProperty(j)) { + continue; + } else { + if (!cb(transaction_id, j, output)) return false; + } + } + } + // input + for (j = 0; j < tx.input.length; ++j) { + let input = tx.input[j]; + // not coinbase + if (input.id != null && input.index != -1) { + if (!spentTXOs[input.id]) { + spentTXOs[input.id] = []; + } + spentTXOs[input.id].push(input.index); + } + } + } + return true; + }, + this.get_last_block().hash); + } + async get_balance() { + let value = 0; + await this.get_utxo((transaction_id, index, vout) => { + value += vout.amount; + return true; + }); + return value; + } + async create_transaction(to, amount) { + let value = 0; + let input = []; + let output = []; + let self = this; + let tx = null; + await this.get_utxo((transaction_id, index, vout) => { + value += vout.amount; + let signature = Crypto.sign(self.get_account_keypair(), JSON.stringify(vout)); + input.push(new TxInput(transaction_id, index, signature)); + if (value >= amount) { + output.push(new TxOutput(amount, to)); + if (value > amount) + output.push(new TxOutput(value - amount, self.get_public_key())); + tx = new Transaction(input, output); + // stop + return false; + } + return true; + }); + if (value < amount) { + throw new Error("amount is not enough!"); + } + if (tx == null) { + throw new Error("create transaction failed!"); + } + this.tx_pool[tx.id] = tx; + this.broadcast(Msg.transaction(tx)); + + return tx; + } } module.exports = BlockChain; \ No newline at end of file diff --git a/src/js/crypto.js b/src/js/crypto.js new file mode 100644 index 0000000..5293a3c --- /dev/null +++ b/src/js/crypto.js @@ -0,0 +1,23 @@ +'use strict'; + +var crypto = require("crypto"); +var ed = require("ed25519"); + +function calc_hash(data) { + return crypto.createHash('sha256').update(data).digest('hex'); +} + +function sign(keypair, data) { + return ed.Sign(Buffer.from(data, 'utf-8'), keypair).toString('hex'); +} + +function verify_signature(data, signature, publickey) { + var res = ed.Verify(Buffer.from(data, 'utf-8'), Buffer.from(signature, 'hex'), Buffer.from(publickey, 'hex')); + return res; +} + +module.exports = { + calc_hash, + sign, + verify_signature +}; \ No newline at end of file diff --git a/src/js/genesis_block.json b/src/js/genesis_block.json index e2afba1..0ac5c6f 100644 --- a/src/js/genesis_block.json +++ b/src/js/genesis_block.json @@ -8,5 +8,5 @@ "hash": "d611edb9fd86ee234cdc08d9bf382330d6ccc721cd5e59cf2a01b0a2a8decfff", "block_signature": "603b61b14348fb7eb087fe3267e28abacadf3932f0e33958fb016ab60f825e3124bfe6c7198d38f8c91b0a3b1f928919190680e44fbe7289a4202039ffbb2109", "consensus_data": {}, - "transcations": [] + "transactions": [] } \ No newline at end of file diff --git a/src/js/message.js b/src/js/message.js index 7e9e58c..96ca6eb 100644 --- a/src/js/message.js +++ b/src/js/message.js @@ -15,5 +15,6 @@ module.exports = { block: (data) => { return { type: MessageType.Block, data: data }; }, preprepare: (data) => { return { type: MessageType.PrePrepare, data: data }; }, prepare: (data) => { return { type: MessageType.Prepare, data: data }; }, - commit: (data) => { return { type: MessageType.Commit, data: data }; } + commit: (data) => { return { type: MessageType.Commit, data: data }; }, + transaction: (data)=>{return { type: MessageType.Transaction, data: data }; } }; \ No newline at end of file diff --git a/src/js/package.json b/src/js/package.json index 26dcd3d..9308771 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -9,7 +9,8 @@ "ed25519": "0.0.4", "eslint": "^5.13.0", "eslint-plugin-html": "^5.0.0", - "js-sha256": "^0.9.0" + "js-sha256": "^0.9.0", + "level": "^4.0.0" }, "devDependencies": {}, "scripts": { diff --git a/src/js/test/create_genesis.js b/src/js/test/create_genesis.js index 61b8eac..a79e3ea 100644 --- a/src/js/test/create_genesis.js +++ b/src/js/test/create_genesis.js @@ -18,12 +18,12 @@ let genesis = { "hash": null, "block_signature": null, "consensus_data": {}, - "transcations": [] + "transactions": [] }; function prepare_data() { let tx = ""; - genesis.transcations.forEach(val => { + genesis.transactions.forEach(val => { tx += val.toString('utf8'); }); let data = genesis.version.toString() diff --git a/src/js/test/db.js b/src/js/test/db.js new file mode 100644 index 0000000..01e7db8 --- /dev/null +++ b/src/js/test/db.js @@ -0,0 +1,34 @@ +'use strict'; + +var crypto = require('crypto'); +var ed = require('ed25519'); +var BlockChain = require("../blockchain"); +var Consensus = require("../consensus/dpos"); + +var password = 'I am tester!'; + +var hash = crypto.createHash('sha256').update(password).digest(); +var keypair = ed.MakeKeypair(hash); + +let blockchains = []; +for (var i = 0; i < 20; ++i) { + let blockchain = new BlockChain(Consensus, keypair, i); + blockchain.start(); + blockchains.push(blockchain); +} + +// setTimeout(() => { +// for (var i = 0; i < 20; ++i) { +// console.log(`${i} --> ${blockchains[i].list_peers()}`); +// } +// }, 3000); + +setTimeout(async () => { + console.log("================="); + await blockchains[0].iterator_forward((block) => { + console.log("-----------------"); + console.log(block.height); + console.log(block.hash); + return true; + }, blockchains[0].get_last_block().hash); +}, 5000); \ No newline at end of file diff --git a/src/js/test/transaction.js b/src/js/test/transaction.js new file mode 100644 index 0000000..7ff8dbb --- /dev/null +++ b/src/js/test/transaction.js @@ -0,0 +1,40 @@ +'use strict'; + +var crypto = require('crypto'); +var ed = require('ed25519'); +var BlockChain = require("../blockchain"); +var Consensus = require("../consensus/dpos"); + + +let blockchains = []; +for (var i = 0; i < 20; ++i) { + + var password = `I am tester ${i}!`; + var hash = crypto.createHash('sha256').update(password).digest(); + var keypair = ed.MakeKeypair(hash); + console.log(`node ${i} address: ${keypair.publicKey.toString('hex')}`); + + let blockchain = new BlockChain(Consensus, keypair, i); + blockchain.start(); + blockchains.push(blockchain); +} + +// setTimeout(() => { +// for (var i = 0; i < 20; ++i) { +// console.log(`${i} --> ${blockchains[i].list_peers()}`); +// } +// }, 3000); + +setTimeout(() => { + let address = blockchains[6].get_public_key(); + blockchains[0].create_transaction(address, 30); +}, 3000); + +async function get_balance() { + let amount = await blockchains[0].get_balance(); + console.log(`node 0 balance: ${amount}`); + amount = await blockchains[6].get_balance(); + console.log(`node 6 balance: ${amount}`); +} + +setInterval(get_balance, 10000); diff --git a/src/js/transaction.js b/src/js/transaction.js index 0616fde..1e271c5 100644 --- a/src/js/transaction.js +++ b/src/js/transaction.js @@ -1,13 +1,64 @@ 'use strict'; +var Crypto = require("./crypto"); -class Transaction { - constructor() { - +class TxOutput { + constructor(amount, ScriptPubKey) { + this.amount_ = amount; + this.script_pubkey_ = ScriptPubKey; } - - static verify(tx) { - return true; + toObject() { + let output = { + "amount": this.amount_, + "ScriptPubKey": this.script_pubkey_ + }; + return output; } } -module.exports = Transaction; \ No newline at end of file +class TxInput { + constructor(id, index, ScriptSig) { + this.id_ = id; + this.index_ = index; + this.script_sig_ = ScriptSig; + } + toObject() { + let input = { + "id": this.id_, + "index": this.index_, + "ScriptSig": this.script_sig_ + }; + return input; + } +} + +class Transaction { + constructor(input, output) { + this.input_ = []; + for (i = 0; i < input.length; ++i) { + this.input_.push(input[i].toObject()); + } + this.output_ = []; + for (var i = 0; i < output.length; ++i) { + this.output_.push(output[i].toObject()); + } + this.id_ = Crypto.calc_hash(JSON.stringify(this.input_) + JSON.stringify(this.output_)); + return this.toObject(); + } + get_id() { return this.id_; } + get_input() { return this.input_; } + get_output() { return this.output_; } + toObject() { + let tx = { + "id": this.id_, + "input": this.input_, + "output": this.output_ + }; + return tx; + } +} + +module.exports = { + TxOutput, + TxInput, + Transaction +}; \ No newline at end of file