Consensus module for nodejs. Centralized, PoW, PoS, dPoS, dPoW.
Look example in folder "example".
This application implements consensus algorithm work. You can reimplement classess, objects, methods for your application.
Have four modules, based on app.MODULE:
- Peer
- Data
- Validator
- PeerManager
- DataManager
- RoundManager
- Consensus
This modules is native and make consensus algorithm. Must be reimplemented, they are abstract.
Define connected node entry
class Peer extends app.MODULE {
constructor(data);//create new peer
getId(); //get peer id
send(peerId, msg); //must implement peer sending data algorithm to another peerId
relay(msg); //sending message to All connected peers
newMessage(message); //invoke this method when new message recived from peer
close(code); //invoke this method when peer disconnected or error
error(code, message); //invoke this method when have some error
}
Define validator node entry
class Validator extends app.MODULE {
constructor(data);//create new peer
getId(); //get validator public key
getVolume(); //must implement validator volume
getPriority(); //must implement priority of validator
updateVolume(volume); //update validator volume
updatePriority(priority); //update validator priority
}
Define Mapper of Peer entries. Storing, sorting, searching, etc...
class PeerMapper extends app.MODULE {
getPeersList();//get list of all peers
addPeer(peer);//add peer to list
removePeerById(peerId);//remove peer by id
removePeer(peer);//remove Peer
}
Define Mapper of proof of stake round
class RoundMapper extends app.MODULE {
getValidatorsList();//sorting active validator list for round
check(peer, data);//check data for round consensus
checkBlockTime(peer, data);//check block time for round
update();//update round info, or start new round
decrementPriority();//decrement priority for cursor
incrementPriority();//increment priority for cursor
getRoundValidators();//get active validators
addValidator(publickey, priority, volume);//add validators to list
removeValidator(publickey);//remove validators from list
validatorCount();//get validator count
getValidator(key);//get validator instance `app.VALIDATOR`
getCursorId();//get active cursor for round
isValidator(key);//check public key for validator
isActiveValidator(key);//check public key for active validator
}
Define blockchain data message (blocks)
class Data extends app.MODULE {
constructor(data);//create new Data
static IsDataMessage(message);//check message for data-message fields
static getIdFieldName();//get field id name from message
static getPrevIdFieldName();//get field name prev block id from message
static getTimeFieldName();//get block time field name
static getBitsFieldName()//get bits filed name
static getVersionFieldName()//get version filed name
getId();//get data id value (hash)
getVersion();//get version of block
getBits();//get bits value
getPrevId();//get prevblock hash value
getKey();//get public key of block (used for pos consensus, need sign coinbase block too)
getTime();//get timme value
isValid();//verify block for syntax and value validity
getStakeValue(height);//used for PoS. Get stake value of block-sender (public key of block)
isDelegateMessage();//used for delegates. Check block-sender for delegate.
}
Define Mapper of Data entries. Storing, sorting, searching, etc...
class DataMapper extends app.MODULE {
getDataList();//get all data
_addDataToBlockChain(data);//add data to list, without verify (init from local version of blockchain)
addData(data);//add data to list with verify (recive from network)
getData(id);//get data by id
getDataFromHeight(h);//get Data by height
getDataHeight(dataId);//get height by data id
getHeight();//get blockchain info
getDataSlice(a, b)//Get slice of data. From a to b, if b is not defined b = 0. b<a. Top block at list[0]
replaceDataById(dataId, newData);//replace data by hash
replaceData(oldData, newData);//replace data by obj
getTopInfo();//get blockchain top info
isDataLinked(data);//Verify data-linking to chain. Check previd and search it in blockchain
getGenesis();//get genesis section from config
getSideList();//get side chain list
getSideData(id);//get data from sidechain
getOrphanList();//get orphan chain list
getOrphanData(id);//get data from orphan chain
seekBlockNetwork(id);//request block from network (for orphan block parent), MUST be implemented in your code
inMainChainData(id);//check block for contains in main chain
}
Describe consensus algorithm.
class AbstractConsensus extends app.MODULE {
constructor(consensus_name, consensus_config_field);//consensus_name - is name for logs,consensus_config_field - config section, with params for this consensus algorithm. See more: config sections
getConfig(field, defaultValue);//get config section param (or all if first param is null);
init()//init consensus
applyData(peer, data);//add data from peer to consensus
isPeerCanSendData(peer);//In this method we can check ability of peer to send data in network
/**
*Check that data is match to consensus.
For centralized: match all data, sended from mainNode.
For delegate proof: match all data, sended from delegate
For PoW: match data, difficulty > avgDifficultyNetwork
etc
* return true if yes, else false.
*/
isDataMatch(data, peer);
/**
* In this mode enabled: only delegate node can send data to network, used for dPos and dPoW consensus.
*/
isDelegateMode();
checkHash(hash, target);//Check that hash is valid for this target
inConsensusVersionRange(data);//check validity of block for version and forks
getForks();//gets forks config for consensus
}
Any class in application can be redefined before start, for example we have app methods:
definePeerClass(man);
definePeerManagerClass(man);
setPeerManager(man);
defineDataClass(man);
defineDataManagerClass(man);
setDataManager(man);
defineConsensusClass(man);
defineValidatorClass(man);
defineRoundManagerClass(man);
setRoundManager(man);
To redefine class you need create you own class and extends one from default modules:
- app.PEER
- app.DATA
- app.VALIDATOR
- app.PEERMANAGER
- app.DATAMANAGER
- app.ROUNDMANAGER
- app.CONSENSUS
For example we can create new consensus algorithm with another target-hash verify:
module.exports = function(app) {
class NewConsensus extends app.CONSENSUS {
constructor(){
super('New consensus', 'newconsensus_config_field')
}
checkHash(hash, target) {
return hash * target < 5;
}
}
return NewConsensus
}
Put this code to file newcons.js, and require it:
const newconst = require('newcons.js');
const ConsensusJS = require("consensusjs");
let config = {
"newconsensus_config_field":{
"extends":"pow"//read about config extending in config section
}
"genesis":{ //required section
///...
}
};
let app = new ConsensusJS(config);
app.defineConsensusClass(newconst(app));
//now you can start app with new consensus
app.start();
You can create many config sections:
"section1": {
"param1": "value1",
"param2": "value2"
//etc
}
and use it in application: app.config.section1.param1
Any consensus have default config section, defined in second param of constructor, for example:
constructor(){
super('New consensus', 'newconsensus_config_field')
}
in this example we must have section newconsensus_config_field
in config!
Genesis section is required for application. Its describe data of default first block in chain.
"genesis": {
"id": 'genesis hash',
"prev": -1,
"bits": 1,
"time": 0,
"nonce": 0,
},
Config sections can be extending by existing sections:
"section1": {
"param1":'value1',
"param2":'value2',
},
"section2":{
"extends":"section1",
"param3":"value3"
}
In this example section2 have next params:
"param1":'value1',
"param2":'value2',
"param3":"value3"
Extending can be nested:
"section1": {
"param1":'value1',
"param2":'value2',
},
"section2":{
"extends":"section1",
"param3":"value3"
},
"section3": {
"extends": "section2",
"params5": 1,
}
it will be:
"param1":'value1',
"param2":'value2',
"param3":"value3",
"params5": 1,
Extending config sections is used for default consensus algorithms, see below.
Config default parameters, that can be extend and rewrite defined in app.getDefaultConfig()
:
getDefaultConfig() {
return {
'centralized': {
'mainNode': '',//public key, only this node can emit new blocks, if you need make centralized with more then 1 node - use delegateMode
'ignorePrevChilds': true,//ignore childs of prev block when add new block. If this param is false - add new block to side if childs of parent is more then 1
},
'pow': {
"premine": 24,//number of height - when premine will stop
"blockcount": 12, ///number of blocks in target calculation
"blocktime": 300, //time of one block in seconds
"maxtarget": 1, //min difficulty
"excludeFirst": 1, //dont use this numbers blocks in calculation of new target
"diffWindow": 120, //window of data, used for target
"diffCut": 6,
"changeBranchDelay": 0,//The number of blocks that we ignore when sidechain length is bigger then main chain,
"removeOrphanCount": 100,// the number of blocks after which we remove the old blocks from the lost ones
'ignorePrevChilds': true,//ignore childs of prev block when add new block. If this param is false - add new block to side if childs of parent is more then 1
},
'pos': {
'extends': 'pow',//extending config params from pow section
'shareStake': 0.1,//max share of stake value that we can decrease from target
},
'dpos': {
'extends': 'pos',
'delegateMode': true,//new block can emit only public keys from this array
'delegates': []//if this param is empty - we can make dynamic delegates
},
'dpow': {
'extends': 'pow',
'delegateMode': true,
'delegates': []//if this param is empty - we can make dynamic delegates
},
'ddpos': {
'extends': 'dpos',
'validatorCount': 60,
'timeout': 60,//timeout in seconds for sending block for validator. If timedout - decrement priority and set cursor to next
'staticDelegatesLimit': 5,//enable static delegates from config if connected validator count less then this parameter
'delegates': [],//if this param is empty - we can make dynamic delegates
"timeout": 60, //max block time after prev block
"pause": 20,//min block time after prev block
},
"genesis": { //need to be rediclared on yours config
"id": 'genesis',
"prev": -1,
"bits": 1,
"time": 0,
"nonce": 0,
}
};
}
ConsensusJs have 5 default consensus algoritm:
- Centralized
- ProofOfWorkConsensus
- ProofOfStakeConsensus
- DelegatedProofOfWorkConsensus
- DelegatedProofOfStakeConsensus
- DynamicDelegateProofOfStakeConsensus
and config sections:
- centralized
- pow
- pos (actually pow+pos)
- dpow
- dpos (actually dpos+pos)
- ddpos
You can extend you own consensus algoritm any default algorithm, for example:
class myConsensus extends app.CONSENSUS.ProofOfStakeConsensus {
constructor () {
super("My first consensus", "myConsensus");
}
}
and config sections:
"myConsensus":{
"extends": "pos"
}
Only one node, defined in config - can send new blocks to chain. If config.delegateMode
is enabled, public keys from config.delegates
can send data too.
Block to network can send everyone, who have hashrate and can mine blocks
Extending pow config and use mining for search new blocks, but if user have stakeValue (bigger percent of coins), he can reduse up to 10% from target and make faster hash searching. Have new method in implementation:
class ProofOfStakeConsensus extends app.CONSENSUS.ProofOfWorkConsensus {
...
getStakeToTargetTransform(publicKey, stake, target) {
//make some algo, to change target, if stakeValue is not null
//if user have make coins - target must be lower
//by default just decrease diff on 10%, nut we can calculate usercoins/allcoins value, and use this param instead shareStake
return (stake > 0) ? target * (1 - this.getConfig('shareStake', 0.1)) : target;
}
...
}
Same as pow, but only defined in config.delegates
publicKeys can send new blocks to network, or messages for data.isDelegateMessage
method returns true
.
Same pos and dpow implementation.
Essence of this consensus comes down to the Round Robin procedure. The network goes rounds of adding blocks. A general list of validators is set, and at the beginning of each round, a list of active validators for a given round is selected by sorting by the parameters of the total number of coins in delegation and the priority described below. The number of validators active for a round is set by the validatorCount parameter.
At the beginning of the round, a cursor is formed that defines the active validator, which, and only that has the right to generate a block and add it to the network at the moment. After successfully adding a block, the cursor moves to the next validator in the list of active ones until the round ends. After this, the procedure of creating a new round is repeated, by selecting new active validators and moving the cursor to position 0. In the network, you can set a pause for blocks, using the pause parameter (in seconds), at which the block will not be added to the network if the time is between the new and previous block less than that number of seconds.
If the active validator with the cursor does not manage to place the block on the network at the timeout specified by the parameter (in seconds), the cursor moves to the next validator, and it must change the information about the previous validator and add it to the network on its behalf, decreasing the priority parameter by 1.
On the contrary, if the previous validator published the block to the network at the right time and without errors, the next validator should increase the priority parameter by 1 if the parameter is less than zero.
If the total number of validators in the network is less than the staticDelegatesLimit parameter, the network switches to staticDelegates mode, i.e. only validators can publish blocks with public keys described in the delegates array parameter.
List of events:
- app.config
- app.peermanager
- app.datamanager
- app.roundmanager
- app.selected_consensus
- app.consensus.create
- app.consensus.init
- app.data.seek
- app.data.chainchain
- app.data.new
- app.data{someDataId}
- app.data.tx{someTxId}
- app.data.removeOld
Config inited, with 1 parameter - config
PeerManager loaded
DataManager loaded
RoundManager loaded
Consensus selected (before creation), with 1 parameter consensus_name
After creation of consensus, params:
data.config
config of consensus (with extended fields)
data.name
- name of consensus
data.field
- config field of consensus
After initialization of consensus, params:
data.id
- hash of top block after init
data.height
- height of storage
Try to search data in network, local storages dont have dataId (childs in orphan)
Param: dataId
change slice of side chain to main chain, params:
data.items
count of tree items
data.data
(app.DATA object), change from tree
data.oldheight
old height
data.newheight
new height
data.nowInMain
now in main (arr with dataId items, replaced to main chain)
data.dataId
id of data
data.chain
(main|side|orphan)
data.height
added only if chain = main
emit this event when dataId added in main chain
emit this event when data with txId added in main chain
emited when remove old data from side/orphan
data.chain
side|orphan
data.dataId
data id
data.delta
seconds timeout of data