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.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;

View File

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

View File

@ -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
// todotx 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,16 +152,33 @@ 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;
}
}
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;
// verify transactions
let tx = block.transactions;
if (tx) {
for (var i = 0; i < tx.length; ++i) {
// todo (check tx is exist and valid)
if (!Transaction.verify(tx[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;

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",
"block_signature": "603b61b14348fb7eb087fe3267e28abacadf3932f0e33958fb016ab60f825e3124bfe6c7198d38f8c91b0a3b1f928919190680e44fbe7289a4202039ffbb2109",
"consensus_data": {},
"transcations": []
"transactions": []
}

View File

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

View File

@ -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": {

View File

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

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';
var Crypto = require("./crypto");
class TxOutput {
constructor(amount, ScriptPubKey) {
this.amount_ = amount;
this.script_pubkey_ = ScriptPubKey;
}
toObject() {
let output = {
"amount": this.amount_,
"ScriptPubKey": this.script_pubkey_
};
return output;
}
}
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() {
constructor(input, output) {
this.input_ = [];
for (i = 0; i < input.length; ++i) {
this.input_.push(input[i].toObject());
}
static verify(tx) {
return true;
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 = Transaction;
module.exports = {
TxOutput,
TxInput,
Transaction
};