Commit bd024116 authored by Tom Bradshaw's avatar Tom Bradshaw
Browse files

Merge branch 'mn_pos_testnet' into 'mn_pos'

Mn pos testnet

See merge request presstab/crown-core!6
parents aa85667c 153897dd
Showing with 376 additions and 165 deletions
+376 -165
......@@ -207,6 +207,7 @@ libbitcoin_server_a_SOURCES = \
miner.cpp \
mn-pos/kernel.cpp \
mn-pos/stakeminer.cpp \
mn-pos/stakepointer.cpp \
mn-pos/stakevalidation.cpp \
net.cpp \
noui.cpp \
......
......@@ -31,7 +31,19 @@ void CActiveMasternode::ManageStatus()
pmn = mnodeman.Find(pubKeyMasternode);
if(pmn != NULL) {
pmn->Check();
if(pmn->IsEnabled() && pmn->protocolVersion == PROTOCOL_VERSION) EnableHotColdMasterNode(pmn->vin, pmn->addr);
if (pmn->IsEnabled() && pmn->protocolVersion == PROTOCOL_VERSION) {
EnableHotColdMasterNode(pmn->vin, pmn->addr);
if (!pmn->vchSignover.empty()) {
if (pmn->pubkey.Verify(pubKeyMasternode.GetHash(), pmn->vchSignover)) {
LogPrintf("%s: Verified pubkey2 signover for staking\n", __func__);
activeMasternode.vchSigSignover = pmn->vchSignover;
} else {
LogPrintf("%s: Failed to verify pubkey on signover!\n", __func__);
}
} else {
LogPrintf("%s: NOT SIGNOVER!\n", __func__);
}
}
}
}
......@@ -111,7 +123,8 @@ void CActiveMasternode::ManageStatus()
}
CMasternodeBroadcast mnb;
if(!CMasternodeBroadcast::Create(vin, service, keyCollateralAddress, pubKeyCollateralAddress, keyMasternode, pubKeyMasternode, errorMessage, mnb)) {
bool fSignOver = true;
if(!CMasternodeBroadcast::Create(vin, service, keyCollateralAddress, pubKeyCollateralAddress, keyMasternode, pubKeyMasternode, fSignOver, errorMessage, mnb)) {
notCapableReason = "Error on CreateBroadcast: " + errorMessage;
LogPrintf("Register::ManageStatus() - %s\n", notCapableReason);
return;
......
......@@ -34,6 +34,9 @@ public:
// Keys for the main Masternode
CPubKey pubKeyMasternode;
// Signature signing over staking priviledge
std::vector<unsigned char> vchSigSignover;
// Initialized while registering Masternode
CTxIn vin;
CService service;
......
......@@ -29,11 +29,23 @@ void CActiveSystemnode::ManageStatus()
if(status == ACTIVE_SYSTEMNODE_SYNC_IN_PROCESS) status = ACTIVE_SYSTEMNODE_INITIAL;
if(status == ACTIVE_SYSTEMNODE_INITIAL) {
CSystemnode *pmn;
pmn = snodeman.Find(pubKeySystemnode);
if(pmn != NULL) {
pmn->Check();
if(pmn->IsEnabled() && pmn->protocolVersion == PROTOCOL_VERSION) EnableHotColdSystemNode(pmn->vin, pmn->addr);
CSystemnode *psn;
psn = snodeman.Find(pubKeySystemnode);
if(psn != NULL) {
psn->Check();
if(psn->IsEnabled() && psn->protocolVersion == PROTOCOL_VERSION) {
EnableHotColdSystemNode(psn->vin, psn->addr);
if (!psn->vchSignover.empty()) {
if (psn->pubkey.Verify(pubKeySystemnode.GetHash(), psn->vchSignover)) {
LogPrintf("%s: Verified pubkey2 signover for staking\n", __func__);
activeSystemnode.vchSigSignover = psn->vchSignover;
} else {
LogPrintf("%s: Failed to verify pubkey on signover!\n", __func__);
}
} else {
LogPrintf("%s: NOT SIGNOVER!\n", __func__);
}
}
}
}
......@@ -113,7 +125,8 @@ void CActiveSystemnode::ManageStatus()
}
CSystemnodeBroadcast mnb;
if(!CSystemnodeBroadcast::Create(vin, service, keyCollateralAddress, pubKeyCollateralAddress, keySystemnode, pubKeySystemnode, errorMessage, mnb)) {
bool fSignOver = true;
if(!CSystemnodeBroadcast::Create(vin, service, keyCollateralAddress, pubKeyCollateralAddress, keySystemnode, pubKeySystemnode, fSignOver, errorMessage, mnb)) {
notCapableReason = "Error on CreateBroadcast: " + errorMessage;
LogPrintf("Register::ManageStatus() - %s\n", notCapableReason);
return;
......
......@@ -39,7 +39,10 @@ public:
// Keys for the main Systemnode
CPubKey pubKeySystemnode;
// Initialized while registering Systemnode
// Signature signing over staking priviledge
std::vector<unsigned char> vchSigSignover;
// Initialized while registering Systemnode
CTxIn vin;
CService service;
......
......@@ -138,6 +138,7 @@ public:
unsigned int nTime;
unsigned int nBits;
unsigned int nNonce;
bool fProofOfStake;
//! (memory only) Sequential id assigned to distinguish order in which blocks are received.
uint32_t nSequenceId;
......@@ -162,6 +163,7 @@ public:
nTime = 0;
nBits = 0;
nNonce = 0;
fProofOfStake = false;
}
CBlockIndex()
......@@ -169,7 +171,7 @@ public:
SetNull();
}
CBlockIndex(const CBlockHeader& block)
CBlockIndex(const CBlock& block)
{
SetNull();
......@@ -178,6 +180,7 @@ public:
nTime = block.nTime;
nBits = block.nBits;
nNonce = block.nNonce;
fProofOfStake = block.IsProofOfStake();
}
CDiskBlockPos GetBlockPos() const {
......@@ -271,6 +274,7 @@ public:
//! Efficiently find an ancestor of this block.
CBlockIndex* GetAncestor(int height);
const CBlockIndex* GetAncestor(int height) const;
bool IsProofOfStake() const;
};
/** Used to marshal pointers into hashes for db storage. */
......@@ -311,6 +315,7 @@ public:
READWRITE(nTime);
READWRITE(nBits);
READWRITE(nNonce);
READWRITE(fProofOfStake);
}
uint256 GetBlockHash() const
......
......@@ -409,6 +409,7 @@ public:
fDefaultConsistencyChecks = true;
fAllowMinDifficultyBlocks = false;
fMineBlocksOnDemand = true;
nBlockPoSStart = 9999999;
}
const Checkpoints::CCheckpointData& Checkpoints() const
......
......@@ -30,6 +30,7 @@
#include "util.h"
#include "spork.h"
#include "utilmoneystr.h"
#include "mn-pos/stakevalidation.h"
#include <sstream>
......@@ -975,7 +976,7 @@ bool CheckTransaction(const CTransaction& tx, CValidationState &state)
}
else if (tx.IsCoinStake())
{
if (tx.vin[0].scriptSig.size() != 1)
if (tx.vin[0].scriptSig.size() > 100)
return state.DoS(100, error("CheckTransactions() : coinstake script size"),
REJECT_INVALID, "bad-cs-length");
}
......@@ -1554,7 +1555,7 @@ bool WriteBlockToDisk(CBlock& block, CDiskBlockPos& pos)
both a block and its header. */
template<typename T>
static bool ReadBlockOrHeader(T& block, const CDiskBlockPos& pos)
static bool ReadBlockOrHeader(T& block, const CDiskBlockPos& pos, bool fProofOfStake)
{
block.SetNull();
......@@ -1572,7 +1573,7 @@ static bool ReadBlockOrHeader(T& block, const CDiskBlockPos& pos)
}
// Check the header
if (!CheckProofOfWork(block))
if (!fProofOfStake && !CheckProofOfWork(block))
return error("ReadBlockFromDisk : Errors in block header");
return true;
......@@ -1581,7 +1582,7 @@ static bool ReadBlockOrHeader(T& block, const CDiskBlockPos& pos)
template<typename T>
static bool ReadBlockOrHeader(T& block, const CBlockIndex* pindex)
{
if (!ReadBlockOrHeader(block, pindex->GetBlockPos()))
if (!ReadBlockOrHeader(block, pindex->GetBlockPos(), pindex->IsProofOfStake()))
return false;
if (block.GetHash() != pindex->GetBlockHash())
return error("ReadBlockFromDisk(CBlock&, CBlockIndex*) : GetHash() doesn't match index");
......@@ -1590,7 +1591,7 @@ static bool ReadBlockOrHeader(T& block, const CBlockIndex* pindex)
bool ReadBlockFromDisk(CBlock& block, const CDiskBlockPos& pos)
{
return ReadBlockOrHeader(block, pos);
return ReadBlockOrHeader(block, pos, block.IsProofOfStake());
}
bool ReadBlockFromDisk(CBlock& block, const CBlockIndex* pindex)
......@@ -1974,8 +1975,11 @@ bool DisconnectBlock(CBlock& block, CValidationState& state, CBlockIndex* pindex
if (!blockUndo.ReadFromDisk(pos, pindex->pprev->GetBlockHash()))
return error("DisconnectBlock() : failure reading undo data");
if (blockUndo.vtxundo.size() + 1 != block.vtx.size())
return error("DisconnectBlock() : block and undo data inconsistent");
int nSizeCheck = blockUndo.vtxundo.size() + 1;
if (block.IsProofOfStake())
nSizeCheck++;
if (nSizeCheck != block.vtx.size())
return error("DisconnectBlock() : block and undo data inconsistent, check=%d size=%d", nSizeCheck, block.vtx.size());
// undo transactions in reverse order
for (int i = block.vtx.size() - 1; i >= 0; i--) {
......@@ -2005,10 +2009,10 @@ bool DisconnectBlock(CBlock& block, CValidationState& state, CBlockIndex* pindex
}
// restore inputs
if (i > 0) { // not coinbases
if (i > 0 && !tx.IsCoinStake()) { // not coinbases
const CTxUndo &txundo = blockUndo.vtxundo[i-1];
if (txundo.vprevout.size() != tx.vin.size())
return error("DisconnectBlock() : transaction and undo data inconsistent");
return error("DisconnectBlock() : transaction and undo data inconsistent prevout=%d vin=%d", txundo.vprevout.size(), blockUndo.vtxundo.size());
for (unsigned int j = tx.vin.size(); j-- > 0;) {
const COutPoint &out = tx.vin[j].prevout;
const CTxInUndo &undo = txundo.vprevout[j];
......@@ -2092,7 +2096,7 @@ bool CheckBlockProofPointer(const CBlock& block, CPubKey& pubkeyMasternode, COut
return error("%s: Failed to read block from disk", __func__);
bool found = false;
for (const auto& tx : block.vtx) {
for (const auto& tx : blockFrom.vtx) {
if (tx.GetHash() == stakePointer.txid) {
if (tx.vout.size() <= stakePointer.nPos)
return error("%s: vout too small", __func__);
......@@ -2101,11 +2105,25 @@ bool CheckBlockProofPointer(const CBlock& block, CPubKey& pubkeyMasternode, COut
if (!ExtractDestination(tx.vout[stakePointer.nPos].scriptPubKey, dest))
return error("%s: failed to get destination from scriptPubKey", __func__);
// if (CBitcoinAddress(stakePointer.hashPubKey).ToString() != CBitcoinAddress(dest).ToString())
// return error("%s: Pubkeyhash from proof does not match stakepointer", __func__);
// The block can either be signed by the collateral key, or the masternode key if it has a sig with it verifying sign over
CBitcoinAddress addressProof(stakePointer.pubKeyProofOfStake.GetID());
CBitcoinAddress addressReward(dest);
CBitcoinAddress addressCollateralCheck(stakePointer.pubKeyCollateral.GetID());
outpoint = COutPoint(stakePointer.txid, stakePointer.nPos);
if (addressCollateralCheck.ToString() != addressReward.ToString())
return error("%s: Wrong pubkeys: Pubkey Collateral in proof pointer = %s, pubkey in reward payment = %s", __func__, addressCollateralCheck.ToString(), addressReward.ToString());
pubkeyMasternode = stakePointer.pubKeyCollateral;
if (addressProof.ToString() != addressReward.ToString()) {
//Check if the key was signed over to another privkey
if (!stakePointer.VerifyCollateralSignOver())
return error("%s: Collateral signover is not validated!", __func__);
pubkeyMasternode = stakePointer.pubKeyProofOfStake;
}
outpoint = COutPoint(stakePointer.txid, stakePointer.nPos);
found = true;
break;
}
......@@ -2226,7 +2244,7 @@ bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockIndex* pin
}
CTxUndo undoDummy;
if (i > 0) {
if (i > 0 && !tx.IsCoinStake()) {
blockundo.vtxundo.push_back(CTxUndo());
}
UpdateCoins(tx, state, view, i == 0 ? undoDummy : blockundo.vtxundo.back(), pindex->nHeight);
......@@ -2882,7 +2900,7 @@ bool ReconsiderBlock(CValidationState& state, CBlockIndex *pindex) {
return true;
}
CBlockIndex* AddToBlockIndex(const CBlockHeader& block)
CBlockIndex* AddToBlockIndex(const CBlockHeader& block, bool fProofOfStake)
{
// Check for duplicate
uint256 hash = block.GetHash();
......@@ -2911,6 +2929,8 @@ CBlockIndex* AddToBlockIndex(const CBlockHeader& block)
if (pindexBestHeader == NULL || pindexBestHeader->nChainWork < pindexNew->nChainWork)
pindexBestHeader = pindexNew;
pindexNew->fProofOfStake = fProofOfStake;
setDirtyBlockIndex.insert(pindexNew);
return pindexNew;
......@@ -3044,9 +3064,9 @@ bool FindUndoPos(CValidationState &state, int nFile, CDiskBlockPos &pos, unsigne
bool CheckBlockHeader(const CBlockHeader& block, CValidationState& state, bool fCheckPOW)
{
// Check proof of work matches claimed amount
if (fCheckPOW && !CheckProofOfWork(block))
return state.DoS(50, error("CheckBlockHeader() : proof of work failed"),
REJECT_INVALID, "high-hash");
// if (fCheckPOW && !CheckProofOfWork(block))
// return state.DoS(50, error("CheckBlockHeader() : proof of work failed"),
// REJECT_INVALID, "high-hash");
// Check timestamp
if (block.GetBlockTime() > GetAdjustedTime() + 2 * 60 * 60)
......@@ -3065,7 +3085,8 @@ bool CheckBlock(const CBlock& block, CValidationState& state, bool fCheckPOW, bo
// Check that the header is valid (particularly PoW). This is mostly
// redundant with the call in AcceptBlockHeader.
if (!CheckBlockHeader(block, state, fCheckPOW))
bool fCheck = block.IsProofOfWork() && fCheckPOW;
if (!CheckBlockHeader(block, state, fCheck))
return false;
// Check the merkle root.
......@@ -3267,7 +3288,7 @@ bool ContextualCheckBlock(const CBlock& block, CValidationState& state, CBlockIn
return true;
}
bool AcceptBlockHeader(const CBlockHeader& block, CValidationState& state, CBlockIndex** ppindex)
bool AcceptBlockHeader(const CBlockHeader& block, bool fProofOfStake, CValidationState& state, CBlockIndex** ppindex)
{
AssertLockHeld(cs_main);
// Check for duplicate
......@@ -3286,7 +3307,7 @@ bool AcceptBlockHeader(const CBlockHeader& block, CValidationState& state, CBloc
return true;
}
if (!CheckBlockHeader(block, state))
if (!CheckBlockHeader(block, state, fProofOfStake))
return false;
// Get prev block index
......@@ -3304,7 +3325,7 @@ bool AcceptBlockHeader(const CBlockHeader& block, CValidationState& state, CBloc
return false;
if (pindex == NULL)
pindex = AddToBlockIndex(block);
pindex = AddToBlockIndex(block, fProofOfStake);
if (ppindex)
*ppindex = pindex;
......@@ -3318,7 +3339,7 @@ bool AcceptBlock(CBlock& block, CValidationState& state, CBlockIndex** ppindex,
CBlockIndex *&pindex = *ppindex;
if (!AcceptBlockHeader(block, state, &pindex))
if (!AcceptBlockHeader(block, block.IsProofOfStake(), state, &pindex))
return false;
if (pindex->nStatus & BLOCK_HAVE_DATA) {
......@@ -3413,6 +3434,11 @@ const CBlockIndex* CBlockIndex::GetAncestor(int height) const
return const_cast<CBlockIndex*>(this)->GetAncestor(height);
}
bool CBlockIndex::IsProofOfStake() const
{
return fProofOfStake;
}
void CBlockIndex::BuildSkip()
{
if (pprev)
......@@ -3422,7 +3448,7 @@ void CBlockIndex::BuildSkip()
bool ProcessNewBlock(CValidationState &state, CNode* pfrom, CBlock* pblock, CDiskBlockPos *dbp)
{
// Preliminary checks
bool checked = CheckBlock(*pblock, state);
bool checked = CheckBlock(*pblock, state, pblock->IsProofOfWork());
while(true) {
TRY_LOCK(cs_main, lockMain);
......@@ -3802,7 +3828,7 @@ bool InitBlockIndex() {
return error("LoadBlockIndex() : FindBlockPos failed");
if (!WriteBlockToDisk(block, blockPos))
return error("LoadBlockIndex() : writing genesis block to disk failed");
CBlockIndex *pindex = AddToBlockIndex(block);
CBlockIndex *pindex = AddToBlockIndex(block, block.IsProofOfStake());
if (!ReceivedBlockTransactions(block, state, pindex, blockPos))
return error("LoadBlockIndex() : genesis block not accepted");
if (!ActivateBestChain(state, &block))
......@@ -4711,7 +4737,7 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv,
bool fAlreadyHave = AlreadyHave(inv);
LogPrint("net", "got inv: %s %s peer=%d\n", inv.ToString(), fAlreadyHave ? "have" : "new", pfrom->id);
if (!fAlreadyHave && !fImporting && !fReindex && inv.type != MSG_BLOCK)
if (!fAlreadyHave && !fImporting && !fReindex)
pfrom->AskFor(inv);
......@@ -4726,7 +4752,7 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv,
// time the block arrives, the header chain leading up to it is already validated. Not
// doing this will result in the received block being rejected as an orphan in case it is
// not a direct successor.
pfrom->PushMessage("getheaders", chainActive.GetLocator(pindexBestHeader), inv.hash);
pfrom->PushMessage("getblocks", chainActive.GetLocator(pindexBestHeader), inv.hash);
CNodeState *nodestate = State(pfrom->GetId());
if (chainActive.Tip()->GetBlockTime() > GetAdjustedTime() - Params().TargetSpacing() * 20 &&
nodestate->nBlocksInFlight < MAX_BLOCKS_IN_TRANSIT_PER_PEER) {
......@@ -4976,64 +5002,64 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv,
else if (strCommand == "headers" && !fImporting && !fReindex) // Ignore headers received while importing
{
std::vector<CBlockHeader> headers;
// Bypass the normal CBlock deserialization, as we don't want to risk deserializing 2000 full blocks.
unsigned int nCount = ReadCompactSize(vRecv);
if (nCount > MAX_HEADERS_RESULTS) {
Misbehaving(pfrom->GetId(), 20);
return error("headers message size = %u", nCount);
}
headers.resize(nCount);
for (unsigned int n = 0; n < nCount; n++) {
vRecv >> headers[n];
ReadCompactSize(vRecv); // ignore tx count; assume it is 0.
}
LOCK(cs_main);
if (nCount == 0) {
// Nothing interesting. Stop asking this peers for more headers.
return true;
}
// If we already know the last header in the message, then it contains
// no new information for us. In this case, we do not request
// more headers later. This prevents multiple chains of redundant
// getheader requests from running in parallel if triggered by incoming
// blocks while the node is still in initial headers sync.
const bool hasNewHeaders = (mapBlockIndex.count(headers.back().GetHash()) == 0);
CBlockIndex *pindexLast = NULL;
BOOST_FOREACH(const CBlockHeader& header, headers) {
CValidationState state;
if (pindexLast != NULL && header.hashPrevBlock != pindexLast->GetBlockHash()) {
Misbehaving(pfrom->GetId(), 20);
return error("non-continuous headers sequence");
}
if (!AcceptBlockHeader(header, state, &pindexLast)) {
int nDoS;
if (state.IsInvalid(nDoS)) {
if (nDoS > 0)
Misbehaving(pfrom->GetId(), nDoS);
std::string strError = "invalid header received " + header.GetHash().ToString();
return error(strError.c_str());
}
}
}
if (pindexLast)
UpdateBlockAvailability(pfrom->GetId(), pindexLast->GetBlockHash());
if (nCount == MAX_HEADERS_RESULTS && pindexLast && hasNewHeaders) {
// Headers message had its maximum size; the peer may have more headers.
// TODO: optimize: if pindexLast is an ancestor of chainActive.Tip or pindexBestHeader, continue
// from there instead.
LogPrint("net", "more getheaders (%d) to end to peer=%d (startheight:%d)\n", pindexLast->nHeight, pfrom->id, pfrom->nStartingHeight);
pfrom->PushMessage("getheaders", chainActive.GetLocator(pindexLast), uint256());
}
CheckBlockIndex();
// std::vector<CBlockHeader> headers;
//
// // Bypass the normal CBlock deserialization, as we don't want to risk deserializing 2000 full blocks.
// unsigned int nCount = ReadCompactSize(vRecv);
// if (nCount > MAX_HEADERS_RESULTS) {
// Misbehaving(pfrom->GetId(), 20);
// return error("headers message size = %u", nCount);
// }
// headers.resize(nCount);
// for (unsigned int n = 0; n < nCount; n++) {
// vRecv >> headers[n];
// ReadCompactSize(vRecv); // ignore tx count; assume it is 0.
// }
//
// LOCK(cs_main);
//
// if (nCount == 0) {
// // Nothing interesting. Stop asking this peers for more headers.
// return true;
// }
//
// // If we already know the last header in the message, then it contains
// // no new information for us. In this case, we do not request
// // more headers later. This prevents multiple chains of redundant
// // getheader requests from running in parallel if triggered by incoming
// // blocks while the node is still in initial headers sync.
// const bool hasNewHeaders = (mapBlockIndex.count(headers.back().GetHash()) == 0);
//
// CBlockIndex *pindexLast = NULL;
// BOOST_FOREACH(const CBlockHeader& header, headers) {
// CValidationState state;
// if (pindexLast != NULL && header.hashPrevBlock != pindexLast->GetBlockHash()) {
// Misbehaving(pfrom->GetId(), 20);
// return error("non-continuous headers sequence");
// }
// if (!AcceptBlockHeader(header, state, &pindexLast)) {
// int nDoS;
// if (state.IsInvalid(nDoS)) {
// if (nDoS > 0)
// Misbehaving(pfrom->GetId(), nDoS);
// std::string strError = "invalid header received " + header.GetHash().ToString();
// return error(strError.c_str());
// }
// }
// }
//
// if (pindexLast)
// UpdateBlockAvailability(pfrom->GetId(), pindexLast->GetBlockHash());
//
// if (nCount == MAX_HEADERS_RESULTS && pindexLast && hasNewHeaders) {
// // Headers message had its maximum size; the peer may have more headers.
// // TODO: optimize: if pindexLast is an ancestor of chainActive.Tip or pindexBestHeader, continue
// // from there instead.
// LogPrint("net", "more getheaders (%d) to end to peer=%d (startheight:%d)\n", pindexLast->nHeight, pfrom->id, pfrom->nStartingHeight);
// pfrom->PushMessage("getheaders", chainActive.GetLocator(pindexLast), uint256());
// }
//
// CheckBlockIndex();
}
else if (strCommand == "block" && !fImporting && !fReindex) // Ignore blocks received while importing
......@@ -5536,7 +5562,7 @@ bool SendMessages(CNode* pto, bool fSendTrickle)
nSyncStarted++;
CBlockIndex *pindexStart = pindexBestHeader->pprev ? pindexBestHeader->pprev : pindexBestHeader;
LogPrint("net", "initial getheaders (%d) to peer=%d (startheight:%d)\n", pindexStart->nHeight, pto->id, pto->nStartingHeight);
pto->PushMessage("getheaders", chainActive.GetLocator(pindexStart), uint256());
pto->PushMessage("getblocks", chainActive.GetLocator(pindexStart), uint256());
}
}
......
......@@ -430,7 +430,7 @@ bool TestBlockValidity(CValidationState &state, const CBlock& block, CBlockIndex
/** Store block on disk. If dbp is provided, the file is known to already reside on disk */
bool AcceptBlock(CBlock& block, CValidationState& state, CBlockIndex **pindex, CDiskBlockPos* dbp = NULL);
bool AcceptBlockHeader(const CBlockHeader& block, CValidationState& state, CBlockIndex **ppindex= NULL);
bool AcceptBlockHeader(const CBlockHeader& block, bool fProofOfStake, CValidationState& state, CBlockIndex **ppindex= NULL);
......
......@@ -62,6 +62,7 @@ CMasternode::CMasternode()
pubkey = CPubKey();
pubkey2 = CPubKey();
sig = std::vector<unsigned char>();
vchSignover = std::vector<unsigned char>();
activeState = MASTERNODE_ENABLED;
sigTime = GetAdjustedTime();
lastPing = CMasternodePing();
......@@ -84,6 +85,7 @@ CMasternode::CMasternode(const CMasternode& other)
pubkey = other.pubkey;
pubkey2 = other.pubkey2;
sig = other.sig;
vchSignover = other.vchSignover;
activeState = other.activeState;
sigTime = other.sigTime;
lastPing = other.lastPing;
......@@ -106,6 +108,7 @@ CMasternode::CMasternode(const CMasternodeBroadcast& mnb)
pubkey = mnb.pubkey;
pubkey2 = mnb.pubkey2;
sig = mnb.sig;
vchSignover = mnb.vchSignover;
activeState = MASTERNODE_ENABLED;
sigTime = mnb.sigTime;
lastPing = mnb.lastPing;
......@@ -291,34 +294,29 @@ int64_t CMasternode::GetLastPaid() const
bool CMasternode::GetRecentPaymentBlocks(std::vector<const CBlockIndex*>& vPaymentBlocks, bool limitMostRecent) const
{
vPaymentBlocks.clear();
if (chainActive.Tip() == NULL) return false;
const CBlockIndex *BlockReading = chainActive.Tip();
int nBlockLimit = BlockReading->nHeight - PAYMENT_BLOCK_DEPTH;
if (nBlockLimit < 1)
nBlockLimit = 1;
int nMinimumValidBlockHeight = chainActive.Height() - PAYMENT_BLOCK_DEPTH;
if (nMinimumValidBlockHeight < 1)
nMinimumValidBlockHeight = 1;
CBlockIndex* pindex = chainActive[nMinimumValidBlockHeight];
CScript mnpayee;
mnpayee = GetScriptForDestination(pubkey.GetID());
bool fBlockFound = false;
while (BlockReading && BlockReading->nHeight >= nBlockLimit) {
if(masternodePayments.mapMasternodeBlocks.count(BlockReading->nHeight)){
/*
Search for this payee, with at least 2 votes. This will aid in consensus allowing the network
to converge on the same payees quickly, then keep the same schedule.
*/
if(masternodePayments.mapMasternodeBlocks[BlockReading->nHeight].HasPayeeWithVotes(mnpayee, 2)){
vPaymentBlocks.emplace_back(BlockReading);
fBlockFound = true;
if (limitMostRecent)
return fBlockFound;
}
while (chainActive.Next(pindex)) {
CBlock block;
if (!ReadBlockFromDisk(block, pindex))
continue;
if (block.vtx[0].vout.size() > 1 && block.vtx[0].vout[1].scriptPubKey == mnpayee) {
vPaymentBlocks.emplace_back(pindex);
fBlockFound = true;
if (limitMostRecent)
return fBlockFound;
}
if (BlockReading->pprev == NULL) { assert(BlockReading); break; }
BlockReading = BlockReading->pprev;
pindex = chainActive.Next(pindex);
}
return fBlockFound;
......@@ -331,6 +329,7 @@ CMasternodeBroadcast::CMasternodeBroadcast()
pubkey = CPubKey();
pubkey2 = CPubKey();
sig = std::vector<unsigned char>();
vchSignover = std::vector<unsigned char>();
activeState = MASTERNODE_ENABLED;
sigTime = GetAdjustedTime();
lastPing = CMasternodePing();
......@@ -351,6 +350,7 @@ CMasternodeBroadcast::CMasternodeBroadcast(CService newAddr, CTxIn newVin, CPubK
pubkey = newPubkey;
pubkey2 = newPubkey2;
sig = std::vector<unsigned char>();
vchSignover = std::vector<unsigned char>();
activeState = MASTERNODE_ENABLED;
sigTime = GetAdjustedTime();
lastPing = CMasternodePing();
......@@ -371,6 +371,7 @@ CMasternodeBroadcast::CMasternodeBroadcast(const CMasternode& mn)
pubkey = mn.pubkey;
pubkey2 = mn.pubkey2;
sig = mn.sig;
vchSignover = mn.vchSignover;
activeState = mn.activeState;
sigTime = mn.sigTime;
lastPing = mn.lastPing;
......@@ -433,10 +434,11 @@ bool CMasternodeBroadcast::Create(std::string strService, std::string strKeyMast
return false;
}
return Create(txin, CService(strService), keyCollateralAddress, pubKeyCollateralAddress, keyMasternodeNew, pubKeyMasternodeNew, strErrorMessage, mnb);
bool fSignOver = true;
return Create(txin, CService(strService), keyCollateralAddress, pubKeyCollateralAddress, keyMasternodeNew, pubKeyMasternodeNew, fSignOver, strErrorMessage, mnb);
}
bool CMasternodeBroadcast::Create(CTxIn txin, CService service, CKey keyCollateralAddress, CPubKey pubKeyCollateralAddress, CKey keyMasternodeNew, CPubKey pubKeyMasternodeNew, std::string &strErrorMessage, CMasternodeBroadcast &mnb) {
bool CMasternodeBroadcast::Create(CTxIn txin, CService service, CKey keyCollateralAddress, CPubKey pubKeyCollateralAddress, CKey keyMasternodeNew, CPubKey pubKeyMasternodeNew, bool fSignOver, std::string &strErrorMessage, CMasternodeBroadcast &mnb) {
// wait for reindex and/or import to finish
if (fImporting || fReindex) return false;
......@@ -465,6 +467,16 @@ bool CMasternodeBroadcast::Create(CTxIn txin, CService service, CKey keyCollater
return false;
}
//Additional signature for use in proof of stake
if (fSignOver) {
if (!keyCollateralAddress.Sign(pubKeyMasternodeNew.GetHash(), mnb.vchSignover)) {
LogPrintf("CMasternodeBroadcast::Create failed signover\n");
mnb = CMasternodeBroadcast();
return false;
}
LogPrintf("%s: Signed over to key %s\n", __func__, pubKeyMasternodeNew.GetID().GetHex());
}
return true;
}
......@@ -678,6 +690,16 @@ bool CMasternodeBroadcast::CheckInputsAndAdd(int& nDoS) const
// if it matches our Masternode privkey, then we've been remotely activated
if(pubkey2 == activeMasternode.pubKeyMasternode && protocolVersion == PROTOCOL_VERSION){
activeMasternode.EnableHotColdMasterNode(vin, addr);
if (!vchSignover.empty()) {
if (pubkey.Verify(pubkey2.GetHash(), vchSignover)) {
LogPrintf("%s: Verified pubkey2 signover for staking, added to activemasternode\n", __func__);
activeMasternode.vchSigSignover = vchSignover;
} else {
LogPrintf("%s: Failed to verify pubkey on signover!\n", __func__);
}
} else {
LogPrintf("%s: NOT SIGNOVER!\n", __func__);
}
}
bool isLocal = addr.IsRFC1918() || addr.IsLocal();
......
......@@ -136,6 +136,7 @@ public:
int nScanningErrorCount;
int nLastScanningErrorBlockHeight;
CMasternodePing lastPing;
std::vector<unsigned char> vchSignover;
CMasternode();
CMasternode(const CMasternode& other);
......@@ -165,6 +166,7 @@ public:
swap(first.nLastDsq, second.nLastDsq);
swap(first.nScanningErrorCount, second.nScanningErrorCount);
swap(first.nLastScanningErrorBlockHeight, second.nLastScanningErrorBlockHeight);
swap(first.vchSignover, second.vchSignover);
}
CMasternode& operator=(CMasternode from)
......@@ -188,24 +190,25 @@ public:
template <typename Stream, typename Operation>
inline void SerializationOp(Stream& s, Operation ser_action, int nType, int nVersion)
{
LOCK(cs);
READWRITE(vin);
READWRITE(addr);
READWRITE(pubkey);
READWRITE(pubkey2);
READWRITE(sig);
READWRITE(sigTime);
READWRITE(protocolVersion);
READWRITE(activeState);
READWRITE(lastPing);
READWRITE(cacheInputAge);
READWRITE(cacheInputAgeBlock);
READWRITE(unitTest);
READWRITE(allowFreeTx);
READWRITE(nLastDsq);
READWRITE(nScanningErrorCount);
READWRITE(nLastScanningErrorBlockHeight);
LOCK(cs);
READWRITE(vin);
READWRITE(addr);
READWRITE(pubkey);
READWRITE(pubkey2);
READWRITE(sig);
READWRITE(sigTime);
READWRITE(protocolVersion);
READWRITE(activeState);
READWRITE(lastPing);
READWRITE(cacheInputAge);
READWRITE(cacheInputAgeBlock);
READWRITE(unitTest);
READWRITE(allowFreeTx);
READWRITE(nLastDsq);
READWRITE(nScanningErrorCount);
READWRITE(nLastScanningErrorBlockHeight);
READWRITE(vchSignover);
}
int64_t SecondsSincePayment() const;
......@@ -281,7 +284,7 @@ public:
CMasternodeBroadcast(const CMasternode& mn);
/// Create Masternode broadcast, needs to be relayed manually after that
static bool Create(CTxIn txin, CService service, CKey keyCollateral, CPubKey pubKeyCollateral, CKey keyMasternodeNew, CPubKey pubKeyMasternodeNew, std::string &strErrorMessage, CMasternodeBroadcast &mnb);
static bool Create(CTxIn txin, CService service, CKey keyCollateral, CPubKey pubKeyCollateral, CKey keyMasternodeNew, CPubKey pubKeyMasternodeNew, bool fSignOver, std::string &strErrorMessage, CMasternodeBroadcast &mnb);
static bool Create(std::string strService, std::string strKey, std::string strTxHash, std::string strOutputIndex, std::string& strErrorMessage, CMasternodeBroadcast &mnb, bool fOffline = false);
......@@ -305,6 +308,7 @@ public:
READWRITE(protocolVersion);
READWRITE(lastPing);
READWRITE(nLastDsq);
READWRITE(vchSignover);
}
uint256 GetHash() const
......
......@@ -22,9 +22,13 @@
#endif
#include "masternode-payments.h"
#include "systemnode-payments.h"
#include "legacysigner.h"
#include "masternodeconfig.h"
#include "mn-pos/stakepointer.h"
#include <boost/thread.hpp>
#include <boost/tuple/tuple.hpp>
#include <mn-pos/stakevalidation.h>
using namespace std;
......@@ -114,7 +118,7 @@ CBlockTemplate* CreateNewBlock(const CScript& scriptPubKeyIn, CWallet* pwallet,
pblock->nVersion.SetBaseVersion(CBlockHeader::CURRENT_VERSION, nChainId);
if (fProofOfStake) {
if (fProofOfStake && chainActive.Height() + 1 >= Params().PoSStartHeight()) {
pblock->nTime = GetAdjustedTime();
CBlockIndex* pindexPrev = chainActive.Tip();
pblock->nBits = GetNextWorkRequired(pindexPrev, pblock);
......@@ -124,12 +128,15 @@ CBlockTemplate* CreateNewBlock(const CScript& scriptPubKeyIn, CWallet* pwallet,
int nHeight = pindexPrev->nHeight + 1;
uint32_t nTime = pblock->nTime;
uint32_t nBits = pblock->nBits;
if (pwallet->CreateCoinStake(nHeight, nBits, nTime, txCoinStake, nTxNewTime)) {
StakePointer stakePointer;
if (pwallet->CreateCoinStake(nHeight, nBits, nTime, txCoinStake, nTxNewTime, stakePointer)) {
pblock->nTime = nTxNewTime;
pblock->vtx.clear();
txCoinbase.vout[0].scriptPubKey = CScript();
pblock->vtx.emplace_back(txCoinbase);
pblock->vtx.emplace_back(CTransaction(txCoinbase));
txCoinStake.vin[0].scriptSig << nHeight << OP_0;
pblock->vtx.emplace_back(CTransaction(txCoinStake));
pblock->stakePointer = stakePointer;
fStakeFound = true;
}
......@@ -378,33 +385,79 @@ CBlockTemplate* CreateNewBlock(const CScript& scriptPubKeyIn, CWallet* pwallet,
pblock->hashPrevBlock = pindexPrev->GetBlockHash();
if (!fProofOfStake)
UpdateTime(pblock, pindexPrev);
pblock->nBits = GetNextWorkRequired(pindexPrev, pblock);
pblock->nNonce = 0;
pblocktemplate->vTxSigOps[0] = GetLegacySigOpCount(pblock->vtx[0]);
// Sign Block
if (fProofOfStake) {
StakePointer stakePointer;
if (!pwallet->GetRecentStakePointer(stakePointer)) {
LogPrintf("CreateNewBlock() : Failed to find stake pointer\n");
return NULL;
}
pblock->stakePointer = stakePointer;
CTxIn txInCollateralAddress;
CPubKey pubKeyCollateralAddress;
CKey keyCollateralAddress;
CPubKey pubKeyNode;
CKey keyNode;
std::vector<unsigned char> vchSig;
std::string strPrivKey;
if (fMasterNode) {
strPrivKey = strMasterNodePrivKey;
std::string strErrorMessage;
if (!legacySigner.SetKey(strPrivKey, strErrorMessage, keyNode, pubKeyNode)) {
strErrorMessage = strprintf("Can't find keys for masternode - %s", strErrorMessage);
LogPrintf("CMasternodeBroadcast::Create -- %s\n", strErrorMessage);
return NULL;
}
if (fMasterNode && pwallet->GetMasternodeVinAndKeys(txInCollateralAddress, pubKeyCollateralAddress,
keyCollateralAddress)) {
if (!keyCollateralAddress.Sign(pblock->GetHash(), vchSig)) {
// Switch keys if using signed over staking key
if (!activeMasternode.vchSigSignover.empty()) {
pblock->stakePointer.pubKeyCollateral = pblock->stakePointer.pubKeyProofOfStake;
pblock->stakePointer.pubKeyProofOfStake = pubKeyNode;
pblock->stakePointer.vchSigCollateralSignOver = activeMasternode.vchSigSignover;
LogPrintf("%s signover set\n", __func__);
} else {
LogPrintf("%s signover NOT set\n", __func__);
}
if (pubKeyNode != pblock->stakePointer.pubKeyProofOfStake) {
LogPrintf("%s: using wrong pubkey. pointer=%s pubkeynode=%s\n", __func__,
pblock->stakePointer.pubKeyProofOfStake.GetHash().GetHex(),
pubKeyNode.GetHash().GetHex());
return NULL;
}
pblock->hashMerkleRoot = pblock->BuildMerkleTree();
if (!keyNode.Sign(pblock->GetHash(), vchSig)) {
LogPrintf("CreateNewBlock() : Failed to sign block as masternode\n");
return NULL;
}
} else if (fSystemNode && pwallet->GetSystemnodeVinAndKeys(txInCollateralAddress, pubKeyCollateralAddress,
keyCollateralAddress)) {
if (!keyCollateralAddress.Sign(pblock->GetHash(), vchSig)) {
} else if (fSystemNode) {
strPrivKey = strSystemNodePrivKey;
std::string strErrorMessage;
if (!legacySigner.SetKey(strPrivKey, strErrorMessage, keyNode, pubKeyNode)) {
strErrorMessage = strprintf("Can't find keys for systemnode - %s", strErrorMessage);
LogPrintf("CSystemnodeBroadcast::Create -- %s\n", strErrorMessage);
return NULL;
}
// Switch keys if using signed over staking key
if (!activeSystemnode.vchSigSignover.empty()) {
pblock->stakePointer.pubKeyCollateral = pblock->stakePointer.pubKeyProofOfStake;
pblock->stakePointer.pubKeyProofOfStake = pubKeyNode;
pblock->stakePointer.vchSigCollateralSignOver = activeSystemnode.vchSigSignover;
LogPrintf("%s signover set\n", __func__);
} else {
LogPrintf("%s signover NOT set\n", __func__);
}
if (pubKeyNode != pblock->stakePointer.pubKeyProofOfStake) {
LogPrintf("%s: using wrong pubkey. pointer=%s pubkeynode=%s\n", __func__,
pblock->stakePointer.pubKeyProofOfStake.GetHash().GetHex(),
pubKeyNode.GetHash().GetHex());
return NULL;
}
pblock->hashMerkleRoot = pblock->BuildMerkleTree();
if (!keyNode.Sign(pblock->GetHash(), vchSig)) {
LogPrintf("CreateNewBlock() : Failed to sign block as systemnode\n");
return NULL;
}
......@@ -412,12 +465,17 @@ CBlockTemplate* CreateNewBlock(const CScript& scriptPubKeyIn, CWallet* pwallet,
LogPrintf("CreateNewBlock() : Failed to obtain key for block signature\n");
return NULL;
}
pblock->vchBlockSig = vchSig;
if (!CheckBlockSignature(*pblock, pblock->stakePointer.pubKeyProofOfStake)) {
LogPrintf("%s: Block signature is not valid\n", __func__);
return NULL;
}
}
CValidationState state;
if (!TestBlockValidity(state, *pblock, pindexPrev, false, false)) {
LogPrintf("CreateNewBlock() : TestBlockValidity failed");
LogPrintf("CreateNewBlock() : TestBlockValidity failed\n");
return NULL;
}
}
......@@ -537,6 +595,11 @@ void BitcoinMiner(CWallet *pwallet, bool fProofOfStake)
try {
while (true) {
if (fProofOfStake && chainActive.Height() + 1 < Params().PoSStartHeight() || !(fMasterNode || (fSystemNode && fProofOfStake)) || chainActive.Tip()->GetBlockTime() > GetAdjustedTime()) {
MilliSleep(1000);
continue;
}
if (Params().MiningRequiresPeers()) {
// Busy-wait for the network to come online so we don't waste time mining
// on an obsolete chain. In regtest mode we expect to fly solo.
......@@ -562,10 +625,12 @@ void BitcoinMiner(CWallet *pwallet, bool fProofOfStake)
if (!pblocktemplate)
{
LogPrintf("Error in CrownnMiner: Keypool ran out, please call keypoolrefill before restarting the mining thread\n");
return;
MilliSleep(1000);
continue;
}
CBlock *pblock = &pblocktemplate->block;
IncrementExtraNonce(pblock, pindexPrev, nExtraNonce);
if (!fProofOfStake)
IncrementExtraNonce(pblock, pindexPrev, nExtraNonce);
//Proof of Stake Miner
if (fProofOfStake) {
......
......@@ -5,6 +5,7 @@
#include "../streams.h"
#include "../hash.h"
#include "stakepointer.h"
#include "../tinyformat.h"
/*
* A 'proof hash' (also referred to as a 'kernel') is comprised of the following:
......@@ -55,4 +56,10 @@ bool Kernel::IsValidProof(const uint256& nTarget)
void Kernel::SetStakeTime(uint64_t nTime)
{
m_nTimeStake = nTime;
}
std::string Kernel::ToString()
{
return strprintf("OutPoint: %s:%d Modifier=%s timeblockfrom=%d time=%d amount=%d", m_outpoint.first.GetHex(),
m_outpoint.second, m_nStakeModifier.GetHex(), m_nTimeBlockFrom, m_nTimeStake, m_nAmount);
}
\ No newline at end of file
......@@ -17,6 +17,7 @@ public:
bool IsValidProof(const uint256& nTarget);
bool IsValidStakePointer(const StakePointer& stakePointer);
void SetStakeTime(uint64_t nTime);
std::string ToString();
static bool CheckProof(const arith_uint256& target, const arith_uint256& hash, const uint64_t nAmount);
......
#include "stakepointer.h"
bool StakePointer::VerifyCollateralSignOver() const
{
return pubKeyCollateral.Verify(pubKeyProofOfStake.GetHash(), vchSigCollateralSignOver);
}
\ No newline at end of file
#ifndef CROWNCOIN_PROOFPOINTER_H
#define CROWNCOIN_PROOFPOINTER_H
#include <pubkey.h>
#include "uint256.h"
#include "serialize.h"
......@@ -16,21 +17,26 @@
* @param hashPubKey The CKeyID (or pubkeyhash) of the masternode's payment
*/
struct StakePointer
class StakePointer
{
public:
uint256 hashBlock;
uint256 txid;
unsigned int nPos;
uint160 hashPubKey;
CPubKey pubKeyProofOfStake;
CPubKey pubKeyCollateral; // The public key to the collateral address
std::vector<unsigned char> vchSigCollateralSignOver; // Signature signed from collateral pubkey giving permission to pubKeyProofOfStake
void SetNull()
{
hashBlock = uint256();
txid = uint256();
hashPubKey = uint160();
pubKeyProofOfStake = CPubKey();
nPos = 0;
}
bool VerifyCollateralSignOver() const;
ADD_SERIALIZE_METHODS;
template <typename Stream, typename Operation>
......@@ -38,7 +44,9 @@ struct StakePointer
READWRITE(hashBlock);
READWRITE(txid);
READWRITE(nPos);
READWRITE(hashPubKey);
READWRITE(pubKeyProofOfStake);
READWRITE(pubKeyCollateral);
READWRITE(vchSigCollateralSignOver);
}
};
......
......@@ -4,12 +4,14 @@
#include <primitives/block.h>
#include <chain.h>
#include <pubkey.h>
#include <wallet.h>
#include <util.h>
#include <arith_uint256.h>
bool CheckBlockSignature(const CBlock& block, const CPubKey& pubkeyMasternode)
{
uint256 hashBlock = block.GetHash();
return pubkeyMasternode.Verify(hashBlock, block.vchBlockSig);
}
......@@ -24,8 +26,11 @@ bool CheckProofOfStake(const CBlock& block, const CBlockIndex* prevBlock, const
CAmount nSNPayment = txPayment.vout[2].nValue;
auto pairOut = std::make_pair(outpoint.hash, outpoint.n);
CAmount nAmountCollateral = (outpoint.n == 1 ? MASTERNODE_COLLATERAL : SYSTEMNODE_COLLATERAL);
uint256 nStakeModifier = prevBlock->GetAncestor(prevBlock->nHeight - 100)->GetBlockHash();
Kernel kernel(pairOut, (outpoint.n == 1 ? nMNPayment : nSNPayment), uint256(), prevBlock->GetBlockTime(), block.nTime);
Kernel kernel(pairOut, nAmountCollateral, nStakeModifier, prevBlock->GetBlockTime(), block.nTime);
bool fNegative;
bool fOverflow;
......@@ -37,5 +42,7 @@ bool CheckProofOfStake(const CBlock& block, const CBlockIndex* prevBlock, const
if (fNegative || bnTarget == 0 || fOverflow)
return error("CheckProofOfStake() : nBits below minimum stake");
LogPrintf("%s : %s\n", __func__, kernel.ToString());
return kernel.IsValidProof(ArithToUint256(bnTarget));
}
\ No newline at end of file
......@@ -143,6 +143,9 @@ unsigned int GetNextWorkRequired(const CBlockIndex* pindexLast, const CBlockHead
if (pindexLast->nHeight + 1 >= 1059780) retarget = DIFF_DGW;
else retarget = DIFF_BTC;
if (Params().NetworkID() == CBaseChainParams::TESTNET && pindexLast->nHeight >= 10000)
retarget = DIFF_DGW;
// Default Bitcoin style retargeting
if (retarget == DIFF_BTC)
{
......
......@@ -66,6 +66,28 @@ Value getpoolinfo(const Array& params, bool fHelp)
return obj;
}
Value getstakepointers(const Array& params, bool fHelp)
{
if (fHelp || params.size() != 0)
throw runtime_error(
"getstakepointers\n"
"Returns stake pointers");
std::vector<StakePointer> vStakePointers;
pwalletMain->GetRecentStakePointers(vStakePointers);
Array ret;
for (auto p : vStakePointers) {
Object obj;
obj.push_back(Pair("blockhash", p.hashBlock.GetHex()));
obj.push_back(Pair("hashpubkey", p.pubKeyProofOfStake.GetID().GetHex()));
obj.push_back(Pair("pos", (int64_t)p.nPos));
obj.push_back(Pair("txid", p.txid.GetHex()));
ret.push_back(obj);
}
return ret;
}
Value masternode(const Array& params, bool fHelp)
{
......
......@@ -336,6 +336,7 @@ static const CRPCCommand vRPCCommands[] =
{ "crown", "systemnodelist", &systemnodelist, true, true, false },
{ "crown", "systemnodebroadcast", &systemnodebroadcast, true, true, false },
{ "crown", "node", &node, true, true, false },
{ "crown", "getstakepointers", &getstakepointers, true, true, false },
/* API features */
{ "api", "service", &service, true, true, false },
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment