diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..074ab52 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,2 @@ +package-lock.json +node_modules \ No newline at end of file diff --git a/src/js/Readme.md b/src/js/Readme.md new file mode 100644 index 0000000..6f7cc8f --- /dev/null +++ b/src/js/Readme.md @@ -0,0 +1,11 @@ +# BlockChain for Node.js + +Basic implementation for blockchain in node.js, supporting pow, pos, pbft, dpos and pbft+dpos. + +Tests passed on Ubuntu and MAC. + +``` +npm install + +node test/xxx.js +``` \ No newline at end of file diff --git a/src/js/account.js b/src/js/account.js new file mode 100644 index 0000000..73f0eff --- /dev/null +++ b/src/js/account.js @@ -0,0 +1,17 @@ +'use strict'; + +class Account { + constructor(keypair, id) { + this.keypair_ = keypair; + this.id_ = id; + this.amount_ = 0; + if (!this.keypair_) { } + if (!this.id_) { } + } + + get_id() { return this.id_; } + get_key() { return this.keypair_; } + get_amount() { return this.amount_; } +} + +module.exports = Account; \ No newline at end of file diff --git a/src/js/block.js b/src/js/block.js new file mode 100644 index 0000000..c962736 --- /dev/null +++ b/src/js/block.js @@ -0,0 +1,142 @@ +'use strict'; + +var EventEmitter = require('events').EventEmitter; +var crypto = require("crypto"); +var ed = require("ed25519"); + +class Block extends EventEmitter { + constructor(data, consensus) { + super(); + // body + this.transcations_ = data ? data.transactions : []; + // header + this.version_ = 0; + this.height_ = data ? data.previous_block.height + 1 : -1; + this.previous_hash_ = data ? data.previous_block.hash : null; + this.timestamp_ = (new Date()).getTime(); + this.merkle_hash_ = data ? this.calc_merkle_hash(data.transactions) : null; + this.generator_publickey_ = data ? data.keypair.publicKey.toString('hex') : null; + this.hash_ = null; + this.block_signature_ = null; + // header extension + this.consensus_data_ = {}; + + if (consensus) { + let self = this; + setImmediate(() => { + self.make_proof(consensus, data.keypair); + }); + } + + } + + get_version() { return this.version_; } + get_height() { return this.height_; } + get_hash() { return this.hash_; } + get_previous_hash() { return this.previous_hash_; } + get_timestamp() { return this.timestamp_; } + get_signature() { return this.block_signature_; } + get_publickey() { return this.generator_publickey_; } + get_transcations() { return this.transcations_; } + get_consensus_data() { return this.consensus_data_; } + set_consensus_data(data) { this.consensus_data_ = data; } + toObject() { + let block = { + "version": this.version_, + "height": this.height_, + "previous_hash": this.previous_hash_, + "timestamp": this.timestamp_, + "merkle_hash": this.merkle_hash_, + "generator_publickey": this.generator_publickey_, + "hash": this.hash_, + "block_signature": this.block_signature_, + "consensus_data": this.consensus_data_, + "transcations": this.transcations_ + }; + return block; + } + set_data(data) { + this.version_ = data.version; + this.height_ = data.height; + this.previous_hash_ = data.previous_hash; + this.timestamp_ = data.timestamp; + this.merkle_hash_ = data.merkle_hash; + this.generator_publickey_ = data.generator_publickey; + this.hash_ = data.hash; + this.block_signature_ = data.block_signature; + this.consensus_data_ = data.consensus_data; + this.transcations_ = 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 + var hashes = []; + for (var i = 0; i < this.transcations_.length; ++i) { + hashes.push(this.calc_hash(this.transcations_.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)); + } + if (hashes.length % 2 === 1) { + tmp.push(hashes[hashes.length - 1]); + } + hashes = tmp; + } + return hashes[0] ? hashes[0] : null; + } + + prepare_data() { + let tx = ""; + for (var i = 0; i < this.transcations_.length; ++i) { + tx += this.transcations_[i].toString('utf-8'); + } + let data = this.version_.toString() + + this.height_.toString() + + this.previous_hash_ + + this.timestamp_.toString() + + this.merkle_hash_ + + this.generator_publickey_ + + JSON.stringify(this.consensus_data_) + + tx; + + return data; + } + // calc the hash of the block + calc_block_hash() { + return this.calc_hash(this.prepare_data()); + } + sign(keypair) { + var hash = this.calc_block_hash(); + return ed.Sign(Buffer.from(hash, 'utf-8'), keypair).toString('hex'); + } + make_proof(consensus, keypair) { + let self = this; + this.on('consensus completed', () => { + self.hash_ = self.calc_block_hash(); + self.block_signature_ = self.sign(keypair); + self.emit('block completed', self.toObject()); + }); + + consensus.make_consensus(this); + } + + 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) { + + } + +} + +module.exports = Block; \ No newline at end of file diff --git a/src/js/blockchain.js b/src/js/blockchain.js new file mode 100644 index 0000000..df4e19a --- /dev/null +++ b/src/js/blockchain.js @@ -0,0 +1,284 @@ +'use strict'; + +var Block = require("./block"); +const genesis_block = require("./genesis_block.json"); +var Node = require("./network"); +var Account = require("./account"); +var Transcation = require("./transcation"); +var Msg = require("./message"); +var MessageType = require("./message").type; +var Promise = require("bluebird"); + +var Pbft = require("./consensus/pbft"); +let pbft = true; +class BlockChain { + constructor(Consensus, keypair, id, is_bad = false) { + // todo + this.pending_block_ = {}; + this.chain_ = []; + + this.is_bad_ = is_bad; + this.pbft_ = new Pbft(this); + + // /////////////////////////////////////// + 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() { + this.node_ = new Node(this.get_account_id()); + this.node_.on("message", this.on_data.bind(this)); + this.node_.start(); + // start loop + var self = this; + setTimeout(function next_loop() { + self.loop(function () { + setTimeout(next_loop, 1000); + }); + }, 5000); + } + loop(cb) { + let self = this; + if (this.consensus_.prepared()) { + if (!self.is_bad_) { + this.generate_block(this.get_account_keypair(), () => { + // broadcast block + let block = self.get_last_block(); + console.log(`node: ${self.get_account_id()} generate block! block height: ${block.height} hash: ${block.hash}`); + }); + } else { + self.fork(); + } + } + cb(); + } + + 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_); + } + generate_block(keypair, cb) { + // load transcations + var tx = []; + // create block + let block = new Block({ + "keypair": keypair, + "previous_block": this.last_block_, + "transactions": tx + }, this.consensus_); + // make proof of the block/mine + let self = this; + block.on('block completed', (data) => { + if (data.height == self.last_block_.height + 1) { + console.log("block completed"); + self.commit_block(data); + + self.broadcast(Msg.block(data)); + + if (cb) cb(); + } else { + // fork or store into tmp + console.log('fork'); + // todo + self.pending_block_[data.hash] = data; + + } + }); + } + commit_block(block_data) { + if (pbft && !this.is_bad_) { + var block = new Block(); + block.set_data(block_data); + let self = this; + block.on('consensus completed', (data) => { + self.last_block_ = data; + self.save_last_block(); + }); + this.pbft_.make_consensus(block); + + } else { + this.last_block_ = block_data; + this.save_last_block(); + } + } + get_height() { + return this.last_block_.height; + } + get_block(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]; + } + } + return null; + } + get_last_block() { + return this.last_block_; + } + get_genesis_block() { + return this.generate_block_; + } + get_amount() { + // get the amount of the account + return this.account_.get_amount(); + } + get_account_id() { + // get the node id + return this.account_.get_id(); + } + get_account_keypair() { + return this.account_.get_key(); + } + broadcast(data) { + this.node_.broadcast(data); + } + list_peers() { + return this.node_.list_peers(); + } + // verify the block is valid + 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 (!Transcation.verify(tx[i])) + return false; + } + return true; + } + 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)) + return; + // verify + if (!this.verify(block)) + return; + + this.pending_block_[block.hash] = block; + + // add to chain + if (block.height == this.last_block_.height + 1) { + // console.log("on block data"); + this.commit_block(block); + // console.log("----------add block"); + } else { + // fork or store into tmp + // console.log('fork'); + // todo + } + // broadcast + this.broadcast(msg); + } + break; + case MessageType.Transcation: + { + // check if exist(pending or in chain) verify, store(into pending) and broadcast + } + break; + default: + if (pbft && !this.is_bad_) { + this.pbft_.processMessage(msg); + } + break; + } + } + print() { + // todo chain_ + let output = ''; + for (var i = 0; i < this.chain_.length; ++i) { + let height = this.chain_[i].height; + let hash = this.chain_[i].hash.substr(0, 6); + let generator_id = this.chain_[i].consensus_data.generator_id; + if (generator_id == undefined) generator_id = null; + output += `(${height}:${hash}:${generator_id}) -> `; + } + console.log(`node: ${this.get_account_id()} ${output}`); + } + async fork() { + console.log('----------fork----------'); + // load transcations + var tx1 = [{ + amount: 1000, + recipient: 'bob', + sender: 'alice' + }]; + // create block + let block1 = new Block({ + "keypair": this.get_account_keypair(), + "previous_block": this.last_block_, + "transactions": tx1 + }, this.consensus_); + // make proof of the block/mine + let self = this; + let block_data1 = await new Promise((resolve, reject) => { + block1.on('block completed', (data) => { + if (data.height == self.last_block_.height + 1) { + resolve(data); + } else { + reject('block1 failed'); + } + }); + }); + + // load transcations + var tx2 = [{ + amount: 1000, + recipient: 'cracker', + sender: 'alice' + }]; + // create block + let block2 = new Block({ + "keypair": this.get_account_keypair(), + "previous_block": this.last_block_, + "transactions": tx2 + }, this.consensus_); + let block_data2 = await new Promise((resolve, reject) => { + block2.on('block completed', (data) => { + if (data.height == self.last_block_.height + 1) { + resolve(data); + } else { + reject('block2 failed'); + } + }); + }); + + var i = 0; + for (var id in this.node_.peers_) { + let socket = this.node_.peers_[id]; + if (i % 2 == 0) { + var msg1 = Msg.block(block_data1); + this.node_.send(socket, msg1); + } else { + var msg2 = Msg.block(block_data2); + this.node_.send(socket, msg2); + } + i++; + } + console.log("fork"); + this.commit_block(block_data1); + } +} + +module.exports = BlockChain; \ No newline at end of file diff --git a/src/js/consensus/dpos.js b/src/js/consensus/dpos.js new file mode 100644 index 0000000..2c3819b --- /dev/null +++ b/src/js/consensus/dpos.js @@ -0,0 +1,60 @@ +'use strict'; +var slot = require("./slot.js"); +class DPos { + constructor(blockchain) { + this.last_slot_ = -1; + this.block_chain_ = blockchain; + } + + prepared() { + let current_slot = slot.get_slot_number(); + let current_id = current_slot % slot.delegates; + // console.log(current_slot + ' ' + current_id + ' ' + this.block_chain_.get_account_id()); + + if (current_id != this.block_chain_.get_account_id()) + return false; + + if (current_slot == this.last_slot_) + return false; + + this.last_slot_ = current_slot; + return true; + } + make_consensus(block_data) { + let self = this; + setImmediate((block) => { + let time_stamp = block.get_timestamp(); + let block_slot = slot.get_slot_number(time_stamp); + let target_id = block_slot % slot.delegates; + + let current_slot = slot.get_slot_number(); + let current_id = current_slot % slot.delegates; + + if (target_id != current_id) { + block.emit('consensus failed'); + return; + } + + if (target_id != self.block_chain_.get_account_id()) { + block.emit('consensus failed'); + return; + } + block.set_consensus_data({ + "generator_id": self.block_chain_.get_account_id() + }); + block.emit('consensus completed'); + }, block_data); + + } + verify(block) { + let time_stamp = block.timestamp; + let block_slot = slot.get_slot_number(time_stamp); + let id = block_slot % slot.delegates; + if (id != block.consensus_data.generator_id) + return false; + + return true; + } +} + +module.exports = DPos; diff --git a/src/js/consensus/pbft.js b/src/js/consensus/pbft.js new file mode 100644 index 0000000..bc76861 --- /dev/null +++ b/src/js/consensus/pbft.js @@ -0,0 +1,145 @@ +var slot = require("./slot"); +var Msg = require("../message"); +var MessageType = require("../message").type; + +var PBFT_N = slot.delegates; +var PBFT_F = Math.floor((PBFT_N - 1) / 3); + +var State = { + Idle: 0, + Prepare: 1, + Commit: 2, +}; + + +class Pbft { + constructor(blockchain) { + this.block_chain_ = blockchain; + + this.pending_block_ = {}; + this.prepare_info_ = null; + this.commit_infos_ = {}; + this.state_ = State.Idle; + + this.prepare_hash_cache_ = {}; + this.commit_hash_cache_ = {}; + this.current_slot_ = 0; + } + + make_consensus(block) { + let time_stamp = block.get_timestamp(); + let block_slot = slot.get_slot_number(time_stamp); + if (block_slot > this.current_slot_) { + this.clear_state_(); + } + this.pending_block_[block.get_hash()] = block; + + if (this.state_ === State.Idle) { + this.current_slot_ = block_slot; + this.state_ = State.Prepare; + this.prepare_info_ = { + "height": block.get_height(), + "hash": block.get_hash(), + "votesNumber": 1, + "votes": {} + }; + this.prepare_info_.votes[this.block_chain_.get_account_id()] = true; + var self = this; + setImmediate(() => { + // console.log(`node: ${self.block_chain_.get_account_id()} \t[prepared] broadcast prepare msg to peer: ${self.block_chain_.list_peers()}`); + self.block_chain_.broadcast(Msg.prepare({ + "height": block.get_height(), + "hash": block.get_hash(), + "signer": self.block_chain_.get_account_id() + })); + }); + } + } + + clear_state_() { + this.state_ = State.Idle; + this.prepare_info_ = null; + this.commit_infos_ = {}; + this.pending_block_ = {}; + } + + commit(hash) { + var block = this.pending_block_[hash]; + block.emit('consensus completed', block.toObject()); + this.clear_state_(); + } + + processMessage(msg) { + var key = msg.data.hash + ':' + msg.data.height + ':' + msg.data.signer; + switch (msg.type) { + case MessageType.Prepare: + // 如果从未收到过这个prepare消息,则转播出去,否则不处理,防止无限广播 + // hash+height其实就是这个消息的ID(n),而signer就是该消息的副本来源 i + if (!this.prepare_hash_cache_[key]) { + this.prepare_hash_cache_[key] = true; + this.block_chain_.broadcast(msg); + } else { + return; + } + // 如果当前为prepare状态,且收到的prepare消息是同一个id的消息(关于同一个block的)则这个消息的投票数加1 + // 超过2f+1票后就进入commit状态,发送commit消息 + if (this.state_ === State.Prepare && + msg.data.height === this.prepare_info_.height && + msg.data.hash === this.prepare_info_.hash && + !this.prepare_info_.votes[msg.data.signer]) { + this.prepare_info_.votes[msg.data.signer] = true; + this.prepare_info_.votesNumber++; + if (this.prepare_info_.votesNumber > 2 * PBFT_F) { + // console.log(`node: ${this.block_chain_.get_account_id()} \t[commit] broadcast commit msg to peer: ${this.block_chain_.list_peers()}`); + this.state_ = State.Commit; + var commitInfo = { + "height": this.prepare_info_.height, + "hash": this.prepare_info_.hash, + "votesNumber": 1, + "votes": {} + }; + commitInfo.votes[this.block_chain_.get_account_id()] = true; + this.commit_infos_[commitInfo.hash] = commitInfo; + this.block_chain_.broadcast(Msg.commit({ + "height": this.prepare_info_.height, + "hash": this.prepare_info_.hash, + "signer": this.block_chain_.get_account_id() + })); + } + } + break; + case MessageType.Commit: + if (!this.commit_hash_cache_[key]) { + this.commit_hash_cache_[key] = true; + this.block_chain_.broadcast(msg); + } else { + return; + } + // prepare消息是只能处理一个,但是commit消息却是可以处理多个,哪一个先达成共识就先处理哪个,然后剩下没处理的都清空 + var commit = this.commit_infos_[msg.data.hash]; + if (commit) { + if (!commit.votes[msg.data.signer]) { + commit.votes[msg.data.signer] = true; + commit.votesNumber++; + if (commit.votesNumber > 2 * PBFT_F) { + // console.log(`node: ${this.block_chain_.get_account_id()} \t[commited] do commit block}`); + this.commit(msg.data.hash); + } + } + } else { + this.commit_infos_[msg.data.hash] = { + hash: msg.data.hash, + height: msg.data.height, + votesNumber: 1, + votes: {} + }; + this.commit_infos_[msg.data.hash].votes[msg.data.signer] = true; + } + break; + default: + break; + } + } +} + +module.exports = Pbft; diff --git a/src/js/consensus/peercoin.cc b/src/js/consensus/peercoin.cc new file mode 100644 index 0000000..48a0125 --- /dev/null +++ b/src/js/consensus/peercoin.cc @@ -0,0 +1,189 @@ +bool CheckStakeKernelHash(unsigned int nBits, + const CBlockHeader &blockFrom, + unsigned int nTxPrevOffset, + const CTransaction &txPrev, + const COutPoint &prevout, + unsigned int nTimeTx, + uint256 &hashProofOfStake, + bool fPrintProofOfStake) +{ + if (nTimeTx < txPrev.nTime)// Transaction timestamp violation + return error("CheckStakeKernelHash() : nTime violation"); + + unsigned int nTimeBlockFrom = blockFrom.GetBlockTime(); + if (nTimeBlockFrom + nStakeMinAge > nTimeTx)// Min age requirement + return error("CheckStakeKernelHash() : min age violation"); + + //目标值使用nBits + CBigNum bnTargetPerCoinDay; + bnTargetPerCoinDay.SetCompact(nBits); + int64 nValueIn = txPrev.vout[prevout.n].nValue; + // v0.3 protocol kernel hash weight starts from 0 at the 30-day min age + // this change increases active coins participating the hash and helps + // to secure the network when proof-of-stake difficulty is low + int64 nTimeWeight = min((int64)nTimeTx - txPrev.nTime, (int64)STAKE_MAX_AGE) - + (IsProtocolV03(nTimeTx) ? nStakeMinAge : 0); + //计算币龄,STAKE_MAX_AGE为90天 + CBigNum bnCoinDayWeight = CBigNum(nValueIn) * nTimeWeight / COIN / (24 * 60 * 60); + // Calculate hash + CDataStream ss(SER_GETHASH, 0); + //权重修正因子 + uint64 nStakeModifier = 0; + int nStakeModifierHeight = 0; + int64 nStakeModifierTime = 0; + if (IsProtocolV03(nTimeTx))// v0.3 protocol + { + if (!GetKernelStakeModifier(blockFrom.GetHash(), nTimeTx, nStakeModifier, nStakeModifierHeight, nStakeModifierTime, fPrintProofOfStake)) + return false; + ss << nStakeModifier; + } + else// v0.2 protocol + { + ss << nBits; + } + + //计算proofhash + //即计算hash(nStakeModifier + txPrev.block.nTime + txPrev.offset + txPrev.nTime + txPrev.vout.n + nTime) + ss << nTimeBlockFrom << nTxPrevOffset << txPrev.nTime << prevout.n << nTimeTx; + hashProofOfStake = Hash(ss.begin(), ss.end()); + if (fPrintProofOfStake) + { + if (IsProtocolV03(nTimeTx)) + printf("CheckStakeKernelHash() : using modifier 0x%016" PRI64x " at height=%d timestamp=%s for block from height=%d timestamp=%s\n", + nStakeModifier, nStakeModifierHeight, + DateTimeStrFormat(nStakeModifierTime).c_str(), + mapBlockIndex[blockFrom.GetHash()]->nHeight, + DateTimeStrFormat(blockFrom.GetBlockTime()).c_str()); + printf("CheckStakeKernelHash() : check protocol=%s modifier=0x%016" PRI64x " nTimeBlockFrom=%u nTxPrevOffset=%u nTimeTxPrev=%u nPrevout=%u nTimeTx=%u hashProof=%s\n", + IsProtocolV05(nTimeTx) ? "0.5" : (IsProtocolV03(nTimeTx) ? "0.3" : "0.2"), + IsProtocolV03(nTimeTx) ? nStakeModifier : (uint64)nBits, + nTimeBlockFrom, nTxPrevOffset, txPrev.nTime, prevout.n, nTimeTx, + hashProofOfStake.ToString().c_str()); + } + + // Now check if proof-of-stake hash meets target protocol + //判断是否满足proofhash < 币龄x目标值 + if (CBigNum(hashProofOfStake) > bnCoinDayWeight * bnTargetPerCoinDay) + return false; + if (fDebug && !fPrintProofOfStake) + { + if (IsProtocolV03(nTimeTx)) + printf("CheckStakeKernelHash() : using modifier 0x%016" PRI64x " at height=%d timestamp=%s for block from height=%d timestamp=%s\n", + nStakeModifier, nStakeModifierHeight, + DateTimeStrFormat(nStakeModifierTime).c_str(), + mapBlockIndex[blockFrom.GetHash()]->nHeight, + DateTimeStrFormat(blockFrom.GetBlockTime()).c_str()); + printf("CheckStakeKernelHash() : pass protocol=%s modifier=0x%016" PRI64x " nTimeBlockFrom=%u nTxPrevOffset=%u nTimeTxPrev=%u nPrevout=%u nTimeTx=%u hashProof=%s\n", + IsProtocolV03(nTimeTx) ? "0.3" : "0.2", + IsProtocolV03(nTimeTx) ? nStakeModifier : (uint64)nBits, + nTimeBlockFrom, nTxPrevOffset, txPrev.nTime, prevout.n, nTimeTx, + hashProofOfStake.ToString().c_str()); + } + return true; +} + +unsigned int static GetNextTargetRequired(const CBlockIndex *pindexLast, bool fProofOfStake) +{ + if (pindexLast == NULL) + return bnProofOfWorkLimit.GetCompact();// genesis block + + const CBlockIndex *pindexPrev = GetLastBlockIndex(pindexLast, fProofOfStake); + if (pindexPrev->pprev == NULL) + return bnInitialHashTarget.GetCompact();// first block + const CBlockIndex *pindexPrevPrev = GetLastBlockIndex(pindexPrev->pprev, fProofOfStake); + if (pindexPrevPrev->pprev == NULL) + return bnInitialHashTarget.GetCompact();// second block + + int64 nActualSpacing = pindexPrev->GetBlockTime() - pindexPrevPrev->GetBlockTime(); + + // ppcoin: target change every block + // ppcoin: retarget with exponential moving toward target spacing + CBigNum bnNew; + bnNew.SetCompact(pindexPrev->nBits); + //STAKE_TARGET_SPACING为10分钟,即10 * 60 + //两个区块目标间隔时间即为10分钟 + int64 nTargetSpacing = fProofOfStake ? STAKE_TARGET_SPACING : min(nTargetSpacingWorkMax, (int64)STAKE_TARGET_SPACING * (1 + pindexLast->nHeight - pindexPrev->nHeight)); + //nTargetTimespan为1周,即7 * 24 * 60 * 60 + //nInterval为1008,即区块间隔为10分钟时,1周产生1008个区块 + int64 nInterval = nTargetTimespan / nTargetSpacing; + //计算当前区块目标值 + bnNew *= ((nInterval - 1) * nTargetSpacing + nActualSpacing + nActualSpacing); + bnNew /= ((nInterval + 1) * nTargetSpacing); + + if (bnNew > bnProofOfWorkLimit) + bnNew = bnProofOfWorkLimit; + + return bnNew.GetCompact(); +} + +static bool CheckStakeKernelHashV2(CBlockIndex *pindexPrev, + unsigned int nBits, + unsigned int nTimeBlockFrom, + const CTransaction &txPrev, + const COutPoint &prevout, + unsigned int nTimeTx, + uint256 &hashProofOfStake, + uint256 &targetProofOfStake, + bool fPrintProofOfStake) +{ + if (nTimeTx < txPrev.nTime)// Transaction timestamp violation + return error("CheckStakeKernelHash() : nTime violation"); + + //目标值使用nBits + CBigNum bnTarget; + bnTarget.SetCompact(nBits); + + //计算币数x目标值 + int64_t nValueIn = txPrev.vout[prevout.n].nValue; + CBigNum bnWeight = CBigNum(nValueIn); + bnTarget *= bnWeight; + + targetProofOfStake = bnTarget.getuint256(); + + //权重修正因子 + uint64_t nStakeModifier = pindexPrev->nStakeModifier; + uint256 bnStakeModifierV2 = pindexPrev->bnStakeModifierV2; + int nStakeModifierHeight = pindexPrev->nHeight; + int64_t nStakeModifierTime = pindexPrev->nTime; + + //计算哈希值 + //即计算hash(nStakeModifier + txPrev.block.nTime + txPrev.nTime + txPrev.vout.hash + txPrev.vout.n + nTime) + CDataStream ss(SER_GETHASH, 0); + if (IsProtocolV3(nTimeTx)) + ss << bnStakeModifierV2; + else + ss << nStakeModifier << nTimeBlockFrom; + ss << txPrev.nTime << prevout.hash << prevout.n << nTimeTx; + hashProofOfStake = Hash(ss.begin(), ss.end()); + + if (fPrintProofOfStake) + { + LogPrintf("CheckStakeKernelHash() : using modifier 0x%016x at height=%d timestamp=%s for block from timestamp=%s\n", + nStakeModifier, nStakeModifierHeight, + DateTimeStrFormat(nStakeModifierTime), + DateTimeStrFormat(nTimeBlockFrom)); + LogPrintf("CheckStakeKernelHash() : check modifier=0x%016x nTimeBlockFrom=%u nTimeTxPrev=%u nPrevout=%u nTimeTx=%u hashProof=%s\n", + nStakeModifier, + nTimeBlockFrom, txPrev.nTime, prevout.n, nTimeTx, + hashProofOfStake.ToString()); + } + + // Now check if proof-of-stake hash meets target protocol + //判断是否满足proofhash < 币数x目标值 + if (CBigNum(hashProofOfStake) > bnTarget) + return false; + + if (fDebug && !fPrintProofOfStake) + { + LogPrintf("CheckStakeKernelHash() : using modifier 0x%016x at height=%d timestamp=%s for block from timestamp=%s\n", + nStakeModifier, nStakeModifierHeight, + DateTimeStrFormat(nStakeModifierTime), + DateTimeStrFormat(nTimeBlockFrom)); + LogPrintf("CheckStakeKernelHash() : pass modifier=0x%016x nTimeBlockFrom=%u nTimeTxPrev=%u nPrevout=%u nTimeTx=%u hashProof=%s\n", + nStakeModifier, + nTimeBlockFrom, txPrev.nTime, prevout.n, nTimeTx, + hashProofOfStake.ToString()); + } + + return true; +} \ No newline at end of file diff --git a/src/js/consensus/pos.js b/src/js/consensus/pos.js new file mode 100644 index 0000000..79901b7 --- /dev/null +++ b/src/js/consensus/pos.js @@ -0,0 +1,74 @@ +'use strict'; + +class Pos { + constructor(blockchain) { + this.difficulty_ = 4294967295;// ~uint64(0) >> 32 + this.time_stamp_ = (new Date()).getTime(); + this.state_ = "Idle"; + this.block_chain_ = blockchain; + } + prepared() { + return this.state_ == "Idle"; + } + calc_difficulty(block) { + let prev = this.block_chain_.get_last_block(); + if (!prev) + return this.difficulty_; + let prev_prev = this.block_chain_.get_hash(prev.hash); + if (!prev_prev) + return prev.difficulty; + + let TargetSpacing = 10 * 60; + let TargetTimespan = 7 * 24 * 60 * 60; + let Interval = TargetTimespan / TargetSpacing; + let ActualSpacing = prev.timestamp - prev_prev.timestamp; + this.difficulty_ = prev.difficulty * ((Interval - 1) * TargetSpacing + 2 * ActualSpacing) / ((TargetSpacing + 1) * TargetSpacing); + } + make_consensus(block_data) { + this.state_ = "Busy"; + this.calc_difficulty(block_data); + + let self = this; + setImmediate(function make_proof(block) { + let time_period = self.time_stamp_ - block_data.get_timestamp(); + if (time_period > 3600 * 1000) { + self.state_ = "Idle"; + block.emit('consensus failed'); + return; + } + + let amount = self.block_chain_.get_amount(); + block.set_consensus_data({ + "difficulty": self.difficulty_, + "timestamp": self.time_stamp_, + "amount": amount + }); + + var hash = block.calc_block_hash(); + if (parseInt(hash, 16) < self.difficulty_ * amount) { + self.state_ = "Idle"; + block.emit('consensus completed'); + } else { + setTimeout(make_proof, 1000, block); + } + }, block_data); + } + verify(block) { + let hash = block.calc_block_hash(); + if (hash != block.get_hash()) + return false; + + let difficulty = block.get_consensus_data().difficulty; + let amount = block.get_consensus_data().amount; + + // todo: check the amount within the block timestamp + + let target = difficulty * amount; + if (parseInt(hash, 16) < target) { + return true; + } + return false; + } +} + +module.exports = Pos; diff --git a/src/js/consensus/pow.js b/src/js/consensus/pow.js new file mode 100644 index 0000000..1c66624 --- /dev/null +++ b/src/js/consensus/pow.js @@ -0,0 +1,118 @@ +'use strict'; + +class Pow { + constructor(blockchain) { + this.difficulty_ = 10000; + this.nonce_ = 0; + this.state_ = "Idle"; + this.block_chain_ = blockchain; + } + + // calc_difficulty(block) { + // this.difficulty_ = parseInt("0001000000000000000000000000000000000000000000000000000000000000", 16); + // // this.difficulty_ = parseInt("0000100000000000000000000000000000000000000000000000000000000000", 16); + // } + + // make_consensus(block_data) { + // this.state_ = "Busy"; + // this.calc_difficulty(block_data); + + // let self = this; + // setImmediate((block) => { + // self.nonce_ = 0; + // while (self.nonce_ < Number.MAX_SAFE_INTEGER) { + // block.set_consensus_data({ + // "difficulty": self.difficulty_, + // "nonce": self.nonce_ + // }); + + // var hash = block.calc_block_hash(); + // if (parseInt(hash, 16) < self.difficulty_) { + // block.emit('consensus completed'); + // self.state_ = "Idle"; + // break; + // } else + // self.nonce_ += 1; + // } + // block.emit('consensus failed'); + // self.state_ = "Idle"; + // }, block_data); + + // } + // verify(block) { + // let hash = block.calc_block_hash(); + // if (hash != block.get_hash()) + // return false; + + // let difficulty = block.get_consensus_data().difficulty; + // if (parseInt(hash, 16) < difficulty) { + // return true; + // } + // return false; + // } + + prepared() { + return this.state_ == "Idle"; + } + calc_difficulty(block) { + // adaption + let prev = this.block_chain_.get_last_block(); + if (!prev.difficulty) { + this.difficulty_ = 10000; + return; + } + var time_period = block.get_timestamp() - prev.timestamp; + if (time_period < 3000) { + this.difficulty_ = prev.difficulty + 9000; + } else if (prev.difficulty < 3000) { + this.difficulty_ = prev.difficulty + 9000; + } else { + this.difficulty_ = prev.difficulty - 3000; + } + } + make_consensus(block_data) { + this.state_ = "Busy"; + this.calc_difficulty(block_data); + + let self = this; + setImmediate((block) => { + self.nonce_ = 0; + let target = Number.MAX_SAFE_INTEGER / self.difficulty_ * 100; + while (self.nonce_ < Number.MAX_SAFE_INTEGER) { + block.set_consensus_data({ + "difficulty": self.difficulty_, + "nonce": self.nonce_ + }); + + var hash = block.calc_block_hash(); + hash = hash.toString('utf8').substring(0, 16); + if (parseInt(hash, 16) < target) { + block.emit('consensus completed'); + self.state_ = "Idle"; + break; + } else + self.nonce_ += 1; + } + block.emit('consensus failed'); + self.state_ = "Idle"; + }, block_data); + + } + verify(block) { + let hash = block.calc_block_hash(); + if (hash != block.get_hash()) + return false; + + let difficulty = block.get_consensus_data().difficulty; + let target = Number.MAX_SAFE_INTEGER / difficulty * 100; + hash = hash.toString('utf8').substring(0, 16); + + if (parseInt(hash, 16) < target) { + return true; + } + return false; + } +} + +module.exports = Pow; + diff --git a/src/js/consensus/slot.js b/src/js/consensus/slot.js new file mode 100644 index 0000000..119e83e --- /dev/null +++ b/src/js/consensus/slot.js @@ -0,0 +1,23 @@ +'use strict'; +const delegates = 20; +const interval = 3;// second + +function get_time(time) { + if (time === undefined) { + time = (new Date()).getTime(); + } + var base_time = new Date(1548988864492).getTime(); + return Math.floor((time - base_time) / 1000); +} + +function get_slot_number(time_stamp) { + time_stamp = get_time(time_stamp); + return Math.floor(time_stamp / interval); +} + +module.exports = { + interval, + delegates, + get_time, + get_slot_number +}; diff --git a/src/js/genesis_block.json b/src/js/genesis_block.json new file mode 100644 index 0000000..e2afba1 --- /dev/null +++ b/src/js/genesis_block.json @@ -0,0 +1,12 @@ +{ + "version": 0, + "height": 1, + "previous_hash": null, + "timestamp": 1550049140488, + "merkle_hash": null, + "generator_publickey": "18941c80a77f2150107cdde99486ba672b5279ddd469eeefed308540fbd46983", + "hash": "d611edb9fd86ee234cdc08d9bf382330d6ccc721cd5e59cf2a01b0a2a8decfff", + "block_signature": "603b61b14348fb7eb087fe3267e28abacadf3932f0e33958fb016ab60f825e3124bfe6c7198d38f8c91b0a3b1f928919190680e44fbe7289a4202039ffbb2109", + "consensus_data": {}, + "transcations": [] +} \ No newline at end of file diff --git a/src/js/message.js b/src/js/message.js new file mode 100644 index 0000000..b08d066 --- /dev/null +++ b/src/js/message.js @@ -0,0 +1,19 @@ +'use strict'; + +var MessageType = { + Connection: 0, + Block: 1, + Transcation: 2, + PrePrepare: 3, + Prepare: 4, + Commit: 5 +}; + +module.exports = { + type: MessageType, + connection: (data) => { return { type: MessageType.Connection, data: data }; }, + 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 }; } +}; \ No newline at end of file diff --git a/src/js/network.js b/src/js/network.js new file mode 100644 index 0000000..128dcaf --- /dev/null +++ b/src/js/network.js @@ -0,0 +1,109 @@ +'use strict'; + +var net = require("net"); +var Msg = require("./message"); +var EventEmitter = require('events').EventEmitter; +var Promise = require("bluebird"); + +var PORT = 8000; + +class Node extends EventEmitter { + constructor(id) { + super(); + + this.id_ = id; + this.peers_ = {}; + + let self = this; + this.server_ = net.createServer((socket) => { + socket.setEncoding('utf8'); + socket.on('data', (data) => { self.on_data(data, socket); }); + socket.on('end', () => { self.remove_peer(socket); }); + }); + this.server_.listen(PORT + id); + } + async start() { + for (var i = 0; i < 5; ++i) { + var remote_id = Math.floor(Math.random() * 20); // [0, 20) + if (remote_id !== this.id_ && !this.peers_[remote_id]) { + let self = this; + // console.log(`${this.id_}-->${remote_id}`); + var socket = net.createConnection({ port: (PORT + remote_id) }); + await new Promise((resolve, reject) => { + socket.on('connect', () => { + resolve(); + }); + }); + // console.log(`id: ${self.id_} connected to remote_id: ${remote_id}`); + let data = Msg.connection(self.id_); + self.send(socket, data); + self.add_peer(socket, remote_id); + + } + } + } + on_data(data, socket) { + try { + var arr = data.split("\r\n"); + for (var i = 0; i < arr.length; ++i) { + if (arr[i] == '') continue; + let obj = JSON.parse(arr[i]); + if (obj.type == Msg.type.Connection) { + // if data is connection info, add peer and response + let remote_id = obj.data; + this.add_peer(socket, remote_id); + // console.log(`node ${this.id_} receive connection: ${remote_id}`); + // console.log(`${this.id_}-->${remote_id}`); + } else { + // else emit msg to blockchain + this.emit("message", obj); + } + } + } catch (err) { + console.log("========================="); + console.log(`node: ${this.id_}\t receive msg error`); + console.log(err); + console.log(err.message); + console.log(data); + console.log(arr.length); + console.log("========================="); + throw new Error(); + } + + } + send(socket, data) { + if (typeof data === 'object') { + data = JSON.stringify(data); + } + socket.write(data + "\r\n"); + } + broadcast(data) { + for (var index in this.peers_) { + let socket = this.peers_[index]; + this.send(socket, data); + } + } + add_peer(socket, remote_id) { + if (!this.peers_[remote_id]) { + this.peers_[remote_id] = socket; + // console.log(`${this.id_}-->${remote_id}`); + } + } + remove_peer(socket) { + for (var index in this.peers_) { + if (this.peers_[index] == socket) { + delete this.peers_[index]; + break; + } + } + } + list_peers() { + let peer_ids = []; + for (var index in this.peers_) { + peer_ids.push(index); + } + return peer_ids; + } +} + +module.exports = Node; \ No newline at end of file diff --git a/src/js/package.json b/src/js/package.json new file mode 100644 index 0000000..26dcd3d --- /dev/null +++ b/src/js/package.json @@ -0,0 +1,20 @@ +{ + "name": "js", + "version": "1.0.0", + "description": "", + "main": "node.js", + "dependencies": { + "babel-eslint": "^10.0.1", + "bluebird": "^3.5.3", + "ed25519": "0.0.4", + "eslint": "^5.13.0", + "eslint-plugin-html": "^5.0.0", + "js-sha256": "^0.9.0" + }, + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC" +} diff --git a/src/js/test/create_block.js b/src/js/test/create_block.js new file mode 100644 index 0000000..4e769da --- /dev/null +++ b/src/js/test/create_block.js @@ -0,0 +1,8 @@ +'use strict'; + +var BlockChain = require("../blockchain"); +var Consensus = require("../consensus/pow"); + +let blockchain = new BlockChain(Consensus); + +console.log(blockchain.get_last_block()); \ No newline at end of file diff --git a/src/js/test/create_genesis.js b/src/js/test/create_genesis.js new file mode 100644 index 0000000..61b8eac --- /dev/null +++ b/src/js/test/create_genesis.js @@ -0,0 +1,60 @@ +'use strict'; + +var crypto = require('crypto'); +var ed = require('ed25519'); + +var password = 'I am genesis!'; + +var hash = crypto.createHash('sha256').update(password).digest(); +var keypair = ed.MakeKeypair(hash); + +let genesis = { + "version": 0, + "height": 1, + "previous_hash": null, + "timestamp": 1550049140488, + "merkle_hash": null, + "generator_publickey": keypair.publicKey.toString('hex'), + "hash": null, + "block_signature": null, + "consensus_data": {}, + "transcations": [] +}; + +function prepare_data() { + let tx = ""; + genesis.transcations.forEach(val => { + tx += val.toString('utf8'); + }); + let data = genesis.version.toString() + + genesis.height.toString() + + genesis.previous_hash + + genesis.timestamp.toString() + + genesis.merkle_hash + + genesis.generator_publickey + + JSON.stringify(genesis.consensus_data) + + tx; + + return data; +} + +function calc_hash(data) { + return crypto.createHash('sha256').update(data).digest('hex'); +} +function calc_block_hash() { + return calc_hash(prepare_data()); +} +function sign(keypair) { + var hash = calc_block_hash(); + return ed.Sign(Buffer.from(hash, 'utf-8'), keypair).toString('hex'); +} + + +genesis.hash = calc_block_hash(); +genesis.block_signature = sign(keypair); +var res = ed.Verify(Buffer.from(genesis.hash, 'utf-8'), Buffer.from(genesis.block_signature, 'hex'), keypair.publicKey); + +if (res) { + console.log(genesis); + console.log(JSON.stringify(genesis)); +} \ No newline at end of file diff --git a/src/js/test/dpos.js b/src/js/test/dpos.js new file mode 100644 index 0000000..7f4a9f4 --- /dev/null +++ b/src/js/test/dpos.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 Promise = require("bluebird"); + +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); +} +// test1 +setTimeout(() => { + for (var i = 0; i < 20; ++i) { + console.log(`${i} --> ${blockchains[i].list_peers()}`); + } +}, 3000); + +// // test2 +// function print_blockchian() { +// for (i = 0; i < 20; ++i) { +// blockchains[i].print(); +// } +// } + +// setInterval(print_blockchian, 10000); \ No newline at end of file diff --git a/src/js/test/dpos_pbft.js b/src/js/test/dpos_pbft.js new file mode 100644 index 0000000..a0260d2 --- /dev/null +++ b/src/js/test/dpos_pbft.js @@ -0,0 +1,39 @@ +'use strict'; + +var crypto = require('crypto'); +var ed = require('ed25519'); +var BlockChain = require("../blockchain"); +var Consensus = require("../consensus/dpos"); +var Promise = require("bluebird"); + +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; + if (i == 10) + blockchain = new BlockChain(Consensus, keypair, i, true); + else + blockchain = new BlockChain(Consensus, keypair, i); + blockchain.start(); + blockchains.push(blockchain); +} +// // test1 +// setTimeout(() => { +// for (var i = 0; i < 20; ++i) { +// console.log(`${i} --> ${blockchains[i].list_peers()}`); +// } +// }, 3000); + +// test2 +function print_blockchian() { + console.log("--------------------------------"); + for (i = 0; i < 20; ++i) { + blockchains[i].print(); + } +} + +setInterval(print_blockchian, 10000); diff --git a/src/js/test/network.js b/src/js/test/network.js new file mode 100644 index 0000000..2884f87 --- /dev/null +++ b/src/js/test/network.js @@ -0,0 +1,13 @@ +'use strict'; + +var Node = require("../network"); + +var nodes = []; +for (var i = 0; i < 20; ++i) { + let node = new Node(i); + nodes.push(node); +} + +for (i = 0; i < 1; ++i) { + nodes[i].start(); +} diff --git a/src/js/test/pow.js b/src/js/test/pow.js new file mode 100644 index 0000000..cdfcda7 --- /dev/null +++ b/src/js/test/pow.js @@ -0,0 +1,33 @@ +'use strict'; + +var crypto = require('crypto'); +var ed = require('ed25519'); +var BlockChain = require("../blockchain"); +var Consensus = require("../consensus/pow"); +var Promise = require("bluebird"); + +let blockchain = new BlockChain(Consensus); + +// console.log(blockchain.get_last_block().hash); + +var password = 'I am tester!'; + +var hash = crypto.createHash('sha256').update(password).digest(); +var keypair = ed.MakeKeypair(hash); + +async function create_block(prev_time) { + return new Promise((resolve, reject) => { + blockchain.generate_block(keypair, () => { + console.log(`|${blockchain.get_last_block().hash}|${blockchain.get_last_block().timestamp - prev_time}|`); + resolve(); + }); + }); +} + +(async () => { + for (var i = 0; i < 20; ++i) { + let prev_time = blockchain.get_last_block().timestamp; + await create_block(prev_time); + } +})(); + diff --git a/src/js/transcation.js b/src/js/transcation.js new file mode 100644 index 0000000..59259cb --- /dev/null +++ b/src/js/transcation.js @@ -0,0 +1,13 @@ +'use strict'; + +class Transcation { + constructor() { + + } + + static verify(tx) { + return true; + } +} + +module.exports = Transcation; \ No newline at end of file