add persistence and transaction (UTXO)

This commit is contained in:
yjjnls
2019-03-13 13:47:58 +08:00
parent 759114227f
commit 3cf616a1f3
11 changed files with 419 additions and 68 deletions

View File

@ -5,13 +5,18 @@ class Account {
this.keypair_ = keypair; this.keypair_ = keypair;
this.id_ = id; this.id_ = id;
this.amount_ = 0; this.amount_ = 0;
if (!this.keypair_) { } if (!this.keypair_) {
if (!this.id_) { } // load from file
}
if (!this.id_) {
// load from file
}
} }
get_id() { return this.id_; } get_id() { return this.id_; }
get_key() { return this.keypair_; } get_key() { return this.keypair_; }
get_amount() { return this.amount_; } get_amount() { return this.amount_; }
set_amount(amount) {this.amount_ = amount;}
} }
module.exports = Account; module.exports = Account;

View File

@ -1,14 +1,14 @@
'use strict'; 'use strict';
var EventEmitter = require('events').EventEmitter; var EventEmitter = require('events').EventEmitter;
var crypto = require("crypto"); var Crypto = require("./crypto");
var ed = require("ed25519");
class Block extends EventEmitter { class Block extends EventEmitter {
constructor(data, consensus) { constructor(data, consensus) {
super(); super();
// body // body
this.transcations_ = data ? data.transactions : []; this.transactions_ = data ? data.transactions : [];
// header // header
this.version_ = 0; this.version_ = 0;
this.height_ = data ? data.previous_block.height + 1 : -1; this.height_ = data ? data.previous_block.height + 1 : -1;
@ -37,7 +37,7 @@ class Block extends EventEmitter {
get_timestamp() { return this.timestamp_; } get_timestamp() { return this.timestamp_; }
get_signature() { return this.block_signature_; } get_signature() { return this.block_signature_; }
get_publickey() { return this.generator_publickey_; } get_publickey() { return this.generator_publickey_; }
get_transcations() { return this.transcations_; } get_transactions() { return this.transactions_; }
get_consensus_data() { return this.consensus_data_; } get_consensus_data() { return this.consensus_data_; }
set_consensus_data(data) { this.consensus_data_ = data; } set_consensus_data(data) { this.consensus_data_ = data; }
toObject() { toObject() {
@ -51,7 +51,7 @@ class Block extends EventEmitter {
"hash": this.hash_, "hash": this.hash_,
"block_signature": this.block_signature_, "block_signature": this.block_signature_,
"consensus_data": this.consensus_data_, "consensus_data": this.consensus_data_,
"transcations": this.transcations_ "transactions": this.transactions_
}; };
return block; return block;
} }
@ -65,23 +65,20 @@ class Block extends EventEmitter {
this.hash_ = data.hash; this.hash_ = data.hash;
this.block_signature_ = data.block_signature; this.block_signature_ = data.block_signature;
this.consensus_data_ = data.consensus_data; 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_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 = []; var hashes = [];
for (var i = 0; i < this.transcations_.length; ++i) { for (var i = 0; i < this.transactions_.length; ++i) {
hashes.push(this.calc_hash(this.transcations_.toString('utf-8'))); hashes.push(Crypto.calc_hash(this.transactions_.toString('utf-8')));
} }
while (hashes.length > 1) { while (hashes.length > 1) {
var tmp = []; var tmp = [];
for (var i = 0; i < hashes.length / 2; ++i) { for (var i = 0; i < hashes.length / 2; ++i) {
let data = hashes[i * 2] + hashes[i * 2 + 1]; 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) { if (hashes.length % 2 === 1) {
tmp.push(hashes[hashes.length - 1]); tmp.push(hashes[hashes.length - 1]);
@ -93,8 +90,8 @@ class Block extends EventEmitter {
prepare_data() { prepare_data() {
let tx = ""; let tx = "";
for (var i = 0; i < this.transcations_.length; ++i) { for (var i = 0; i < this.transactions_.length; ++i) {
tx += this.transcations_[i].toString('utf-8'); tx += this.transactions_[i].toString('utf-8');
} }
let data = this.version_.toString() let data = this.version_.toString()
+ this.height_.toString() + this.height_.toString()
@ -109,11 +106,11 @@ class Block extends EventEmitter {
} }
// calc the hash of the block // calc the hash of the block
calc_block_hash() { calc_block_hash() {
return this.calc_hash(this.prepare_data()); return Crypto.calc_hash(this.prepare_data());
} }
sign(keypair) { sign(keypair) {
var hash = this.calc_block_hash(); 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) { make_proof(consensus, keypair) {
let self = this; let self = this;
@ -128,13 +125,7 @@ class Block extends EventEmitter {
static verify_signature(block) { static verify_signature(block) {
var hash = block.hash; 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 Crypto.verify_signature(hash, block.block_signature, block.generator_publickey);
return res;
}
static get_address_by_publickey(publicKey) {
} }
} }

View File

@ -4,17 +4,22 @@ var Block = require("./block");
const genesis_block = require("./genesis_block.json"); const genesis_block = require("./genesis_block.json");
var Node = require("./network"); var Node = require("./network");
var Account = require("./account"); 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 Msg = require("./message");
var MessageType = require("./message").type; var MessageType = require("./message").type;
var Promise = require("bluebird"); var Promise = require("bluebird");
var level = require("level");
var Crypto = require("./crypto");
var Pbft = require("./consensus/pbft"); var Pbft = require("./consensus/pbft");
let pbft = true; let pbft = false;
class BlockChain { class BlockChain {
constructor(Consensus, keypair, id, is_bad = false) { constructor(Consensus, keypair, id, is_bad = false) {
// todo // todo
this.pending_block_ = {}; this.pending_block_ = {};
this.tx_pool = {};
this.chain_ = []; this.chain_ = [];
this.is_bad_ = is_bad; this.is_bad_ = is_bad;
@ -22,14 +27,26 @@ class BlockChain {
// /////////////////////////////////////// // ///////////////////////////////////////
this.genesis_block_ = genesis_block; this.genesis_block_ = genesis_block;
this.last_block_ = genesis_block;
this.save_last_block();
this.account_ = new Account(keypair, id); this.account_ = new Account(keypair, id);
this.consensus_ = new Consensus(this); this.consensus_ = new Consensus(this);
this.node_ = null; 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_ = new Node(this.get_account_id());
this.node_.on("message", this.on_data.bind(this)); this.node_.on("message", this.on_data.bind(this));
this.node_.start(); this.node_.start();
@ -57,18 +74,40 @@ class BlockChain {
cb(); cb();
} }
save_last_block() { async save_last_block() {
// query from db via hash // query from db via hash
// if not exist, write into db, else do nothing // if not exist, write into db, else do nothing
// todotx is also need to store?
if (this.pending_block_[this.last_block_.hash]) { if (this.pending_block_[this.last_block_.hash]) {
delete 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) { generate_block(keypair, cb) {
// load transcations // load transactions
var tx = []; 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 // create block
let block = new Block({ let block = new Block({
"keypair": keypair, "keypair": keypair,
@ -79,7 +118,7 @@ class BlockChain {
let self = this; let self = this;
block.on('block completed', (data) => { block.on('block completed', (data) => {
if (data.height == self.last_block_.height + 1) { if (data.height == self.last_block_.height + 1) {
console.log("block completed"); // console.log("block completed");
self.commit_block(data); self.commit_block(data);
self.broadcast(Msg.block(data)); self.broadcast(Msg.block(data));
@ -113,15 +152,32 @@ class BlockChain {
get_height() { get_height() {
return this.last_block_.height; return this.last_block_.height;
} }
get_block(hash) { async get_from_db(hash) {
// query block with hash value // query block with hash value
// todo try {
for (var i = 0; i < this.chain_.length; ++i) { let block_data = await this.db_.get(hash);
if (this.chain_[i] == hash) { let block = JSON.parse(block_data);
return this.chain_[i]; 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() { get_last_block() {
return this.last_block_; return this.last_block_;
@ -140,41 +196,89 @@ class BlockChain {
get_account_keypair() { get_account_keypair() {
return this.account_.get_key(); return this.account_.get_key();
} }
get_public_key() {
return this.get_account_keypair().publicKey.toString('hex');
}
broadcast(data) { broadcast(data) {
this.node_.broadcast(data); this.node_.broadcast(data);
} }
list_peers() { list_peers() {
return this.node_.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 the block is valid
verify(block) { async verify(block) {
// verify the block signature // verify the block signature
if (!Block.verify_signature(block)) if (!Block.verify_signature(block))
return false; return false;
// verify consensus // verify consensus
if (!this.consensus_.verify(block)) if (!this.consensus_.verify(block))
return false; return false;
// verify transcations // verify transactions
let tx = block.transcations; let tx = block.transactions;
for (var i = 0; i < tx.length; ++i) { if (tx) {
// todo (check tx is exist and valid) for (var i = 0; i < tx.length; ++i) {
if (!Transaction.verify(tx[i])) if (!await this.verify_transaction(tx[i]))
return false; return false;
}
} }
return true; return true;
} }
on_data(msg) { async on_data(msg) {
switch (msg.type) { switch (msg.type) {
case MessageType.Block: case MessageType.Block:
{ {
let block = msg.data; let block = msg.data;
// console.log(`node: ${this.get_account_id()} receive block: height ${block.height}`); // console.log(`node: ${this.get_account_id()} receive block: height ${block.height}`);
// check if exist // 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; return;
}
// verify // verify
if (!this.verify(block)) if (!await this.verify(block)) {
// console.log("verify failed");
return; return;
}
this.pending_block_[block.hash] = block; this.pending_block_[block.hash] = block;
@ -195,6 +299,22 @@ class BlockChain {
case MessageType.Transaction: case MessageType.Transaction:
{ {
// check if exist(pending or in chain) verify, store(into pending) and broadcast // 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; break;
default: default:
@ -218,7 +338,7 @@ class BlockChain {
} }
async fork() { async fork() {
console.log('----------fork----------'); console.log('----------fork----------');
// load transcations // load transactions
var tx1 = [{ var tx1 = [{
amount: 1000, amount: 1000,
recipient: 'bob', recipient: 'bob',
@ -242,7 +362,7 @@ class BlockChain {
}); });
}); });
// load transcations // load transactions
var tx2 = [{ var tx2 = [{
amount: 1000, amount: 1000,
recipient: 'cracker', recipient: 'cracker',
@ -279,6 +399,91 @@ class BlockChain {
console.log("fork"); console.log("fork");
this.commit_block(block_data1); 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; module.exports = BlockChain;

23
src/js/crypto.js Normal file
View File

@ -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
};

View File

@ -8,5 +8,5 @@
"hash": "d611edb9fd86ee234cdc08d9bf382330d6ccc721cd5e59cf2a01b0a2a8decfff", "hash": "d611edb9fd86ee234cdc08d9bf382330d6ccc721cd5e59cf2a01b0a2a8decfff",
"block_signature": "603b61b14348fb7eb087fe3267e28abacadf3932f0e33958fb016ab60f825e3124bfe6c7198d38f8c91b0a3b1f928919190680e44fbe7289a4202039ffbb2109", "block_signature": "603b61b14348fb7eb087fe3267e28abacadf3932f0e33958fb016ab60f825e3124bfe6c7198d38f8c91b0a3b1f928919190680e44fbe7289a4202039ffbb2109",
"consensus_data": {}, "consensus_data": {},
"transcations": [] "transactions": []
} }

View File

@ -15,5 +15,6 @@ module.exports = {
block: (data) => { return { type: MessageType.Block, data: data }; }, block: (data) => { return { type: MessageType.Block, data: data }; },
preprepare: (data) => { return { type: MessageType.PrePrepare, data: data }; }, preprepare: (data) => { return { type: MessageType.PrePrepare, data: data }; },
prepare: (data) => { return { type: MessageType.Prepare, 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 }; }
}; };

View File

@ -9,7 +9,8 @@
"ed25519": "0.0.4", "ed25519": "0.0.4",
"eslint": "^5.13.0", "eslint": "^5.13.0",
"eslint-plugin-html": "^5.0.0", "eslint-plugin-html": "^5.0.0",
"js-sha256": "^0.9.0" "js-sha256": "^0.9.0",
"level": "^4.0.0"
}, },
"devDependencies": {}, "devDependencies": {},
"scripts": { "scripts": {

View File

@ -18,12 +18,12 @@ let genesis = {
"hash": null, "hash": null,
"block_signature": null, "block_signature": null,
"consensus_data": {}, "consensus_data": {},
"transcations": [] "transactions": []
}; };
function prepare_data() { function prepare_data() {
let tx = ""; let tx = "";
genesis.transcations.forEach(val => { genesis.transactions.forEach(val => {
tx += val.toString('utf8'); tx += val.toString('utf8');
}); });
let data = genesis.version.toString() let data = genesis.version.toString()

34
src/js/test/db.js Normal file
View File

@ -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);

View File

@ -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);

View File

@ -1,13 +1,64 @@
'use strict'; 'use strict';
var Crypto = require("./crypto");
class Transaction { class TxOutput {
constructor() { constructor(amount, ScriptPubKey) {
this.amount_ = amount;
this.script_pubkey_ = ScriptPubKey;
} }
toObject() {
static verify(tx) { let output = {
return true; "amount": this.amount_,
"ScriptPubKey": this.script_pubkey_
};
return output;
} }
} }
module.exports = Transaction; 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
};