Skip to content

Commit

Permalink
new EstimatedTimetoStake and related functions
Browse files Browse the repository at this point in the history
Corrects netweight -- replaces the old GetPosKernelPS() with a
new function GetEstimatedNetworkWeight(). (The old function does not
have anything to do with network protocol and was misleading.)

Implements new EstimatedTimetoStake() with a better algorithm and
replaces denormalized code in both GUI and RPC areas.

Implements new GetAverageDifficulty().

Adds two overview screen fields for Estimated TTS and research
rewards (RR) per day.
  • Loading branch information
jamescowens committed Apr 7, 2018
1 parent f5c8161 commit 12d8484
Show file tree
Hide file tree
Showing 6 changed files with 339 additions and 58 deletions.
289 changes: 264 additions & 25 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -386,10 +386,10 @@ bool FullSyncWithDPORNodes()
const int64_t nLookback = 30 * 6 * 86400;
const int64_t iStartTime = (iEndTime - nLookback) - ( (iEndTime - nLookback) % BLOCK_GRANULARITY);
std::string cpiddata = GetListOf("beacon", iStartTime, iEndTime);
std::string sWhitelist = GetListOf("project");
std::string sWhitelist = GetListOf("project");
int64_t superblock_time = ReadCache("superblock", "magnitudes").timestamp;
int64_t superblock_age = GetAdjustedTime() - superblock_time;
LogPrintf(" list of cpids %s \n",cpiddata);
LogPrintf(" list of cpids %s \n",cpiddata);
double popularity = 0;
std::string consensus_hash = GetNeuralNetworkSupermajorityHash(popularity);
std::string sAge = ToString(superblock_age);
Expand All @@ -403,39 +403,275 @@ bool FullSyncWithDPORNodes()
return true;
}

double GetPoSKernelPS()
double GetEstimatedNetworkWeight(unsigned int nPoSInterval)
{
// The number of stakes to include in the average has been reduced to 40 (default) from 72. 72 stakes represented 1.8 hours at
// standard spacing. This is too long. 40 blocks is nominally 1 hour.
double result;

// The constant below comes from (MaxHash / StandardDifficultyTarget) * 16 sec / 90 sec. If you divide it by 80 to convert to GRC you
// get the familiar 9544517.40667
result = 763561392.533 * GetAverageDifficulty(nPoSInterval);
if (fDebug10) LogPrintf("Network Weight = %f", result);
if (fDebug10) LogPrintf("Network Weight in GRC = %f", result / 80.0);

return result;
}

double GetAverageDifficulty(unsigned int nPoSInterval)
{
int nPoSInterval = 72;
double dStakeKernelsTriedAvg = 0;
int nStakesHandled = 0, nStakesTime = 0;
/*
* Diff is an inverse metric. It is essentially the reciprocal of the probability of staking
* (compared to standard target instead of maxhash). The diff is recorded in each block, and
* its inverse is a proxy for the coinweight selected by the staker. Therefore averaging the
* reciprocal of the diffs and then reinverting to determine the average diff is the correct way
* to do this calculation. This is called a harmonic mean in math.
*
* Also... The number of stakes to include in the average has been reduced to 40 (default) from 72.
* 72 stakes represented 1.8 hours at standard spacing. This is too long. 40 blocks is nominally 1 hour.
*/

double dDiff = 1.0;
double dReciprocalDiffSum = 0.0;
unsigned int nStakesHandled = 0;
double result;

CBlockIndex* pindex = pindexBest;;
CBlockIndex* pindexPrevStake = NULL;
CBlockIndex* pindex = pindexBest;

while (pindex && nStakesHandled < nPoSInterval)
{
if (pindex->IsProofOfStake())
{
dStakeKernelsTriedAvg += GetDifficulty(pindex) * 4294967296.0;
nStakesTime += pindexPrevStake ? (pindexPrevStake->nTime - pindex->nTime) : 0;
pindexPrevStake = pindex;
nStakesHandled++;
dDiff = GetDifficulty(pindex);
// dDiff should never be zero, but just in case, skip the block and move to the next one.
if (dDiff)
{
dReciprocalDiffSum += 1 / dDiff;
nStakesHandled++;
if (fDebug10) LogPrintf("dDiff = %f", dDiff);
if (fDebug10) LogPrintf("nStakesHandled = %u", nStakesHandled);
}
}

pindex = pindex->pprev;
}

double result = 0;
result = nStakesHandled ? nStakesHandled / dReciprocalDiffSum : 0;
if (fDebug10) LogPrintf("Average dDiff = %f", result);

return result;
}

double GetEstimatedTimetoStake(unsigned int nPoSInterval, double dConfidence)
{
/*
* The algorithm below is an attempt to come up with a more accurate way of estimating Time to Stake (ETTS) based on
* the actual situation of the miner and UTXO's. A simple equation will not provide good results, because in mainnet,
* the cooldown period is 16 hours, and depending on how many UTXO's and where they are with respect to getting out of
* cooldown has a lot to do with the expected time to stake.
*
* The way to conceptualize the approach below is to think of the UTXO's as bars on a Gantt Chart. It is a negative Gantt
* chart, meaning that each UTXO bar is cooldown period long, and while the current time is in that bar, the staking probability
* for the UTXO is zero, and UnitStakingProbability elsewhere. A timestamp mask of 16x the normal mask is used to reduce
* the work in the nested loop, so that a 16 hour interval will have a maximum of 225 events, and most likely far less.
* This is important, because the inner loop will be the number of UTXO's. A future improvement to this algorithm would
* also be to quantize (group) the UTXO's themselves (the Gantt bars) so that the work would be further reduced.
* You will see that once the UTXO's are sorted in ascending order based on the time of the end of each of their cooldowns, this
* becomes a manageable, but irritating algorithm to piece the probabilities together.
*
* You will note that I use the compound Poisson (geometric) recursive probability relation, since you cannot simply add
* the probabilities due to consideration of high confidence (CDF) values of 80% or more.
*
* I have also elected to use thin local data structures to hold the UTXO stuff. This minimizes the amount of time
* that locks on the wallet need to be held at the expense of a little memory consumption.
*/

double result = 0.0;
bool staking = MinerStatus.nLastCoinStakeSearchInterval && MinerStatus.WeightSum;
// Get out early if not staking and set return value of 0.
if (!staking)
{
if (fDebug10) LogPrintf("ETTS debug: Not staking: ETTS = %f", result);
return result;
}

int64_t nValue = 0;
int64_t nCurrentTime = GetAdjustedTime();
if (fDebug10) LogPrintf("ETTS debug: nCurrentTime = %i", nCurrentTime);

CTxDB txdb("r");
// Here I am defining a time mask 16 times as long as the normal stake time mask. This is to quantize the UTXO's into a maximum of
// 16 hours * 3600 / 256 = 225 time bins for evaluation. Otherwise for a large number of UTXO's, this algorithm could become
// really expensive.
const int ETTS_TIMESTAMP_MASK = 16 * STAKE_TIMESTAMP_MASK;

int64_t BalanceAvailForStaking = 0;
vector<COutput> vCoins;

{
LOCK2(cs_main, pwalletMain->cs_wallet);

BalanceAvailForStaking = pwalletMain->GetBalance() - nReserveBalance;

if (fDebug10) LogPrintf("ETTS debug: BalanceAvailForStaking = %u", BalanceAvailForStaking);

// Get out early if no balance available and set return value of 0. This should already have happened above, because with no
// balance left after reserve, staking should be disabled; however, just to be safe...
if (BalanceAvailForStaking <= 0)
{
if (fDebug10) LogPrintf("ETTS debug: No balance available: ETTS = %f", result);
return result;
}

//reminder... void AvailableCoins(std::vector<COutput>& vCoins, bool fOnlyConfirmed=true, const CCoinControl *coinControl=NULL, bool fIncludeStakingCoins=false) const;
pwalletMain->AvailableCoins(vCoins, true, NULL, true);
}


// An efficient local structure to store the UTXO's with the bare minimum info we need.
typedef vector< std::pair<int64_t, int64_t> > vCoinsExt;
vCoinsExt vUTXO;
// A local ordered set to store the unique "bins" corresponding to the UTXO transaction times. We are going to use this
// for the outer loop.
std::set<int64_t> UniqueUTXOTimes;
// We want the first "event" to be the CurrentTime. This does not have to be quantized.
UniqueUTXOTimes.insert(nCurrentTime);


// Debug output cooldown...
if (fDebug10) LogPrintf("ETTS debug: nStakeMinAge = %i", nStakeMinAge);

// Derive a smoothed difficulty over desired PoSInterval of blocks by calling GetPoSKernelPS() which is netweight and reverse engineering...
double dDiff = GetAverageDifficulty(nPoSInterval);
if (fDebug10) LogPrintf("ETTS debug: dDiff = %f", dDiff);

// The stake probability per "throw" of 1 weight unit = target value at diff of 1.0 / (maxhash * diff). This happens effectively every STAKE_TIMESTAMP_MASK+1 sec.
double dUnitStakeProbability = 1 / (4295032833.0 * dDiff);
if (fDebug10) LogPrintf("ETTS debug: dUnitStakeProbability = %e", dUnitStakeProbability);


int64_t nTime = 0;
for (const auto& out : vCoins)
{
CTxIndex txindex;
CBlock CoinBlock; //Block which contains CoinTx
//{
// LOCK2(cs_main, pwalletMain->cs_wallet);

if (!txdb.ReadTxIndex(out.tx->GetHash(), txindex))
continue; //error?

if (!CoinBlock.ReadFromDisk(txindex.pos.nFile, txindex.pos.nBlockPos, false))
continue;
//}

// We are going to store as an event the time that the UTXO matures (is available for staking again.)
nTime = (CoinBlock.GetBlockTime() & ~ETTS_TIMESTAMP_MASK) + nStakeMinAge;

nValue = out.tx->vout[out.i].nValue;

// Only consider UTXO's that are actually stakeable - which means that each one must be less than the available balance
// subtracting the reserve. Each UTXO also has to be greater than 1/80 GRC to result in a weight greater than zero in the CreateCoinStake loop,
// so eliminate UTXO's with less than 0.0125 GRC balances right here.
if(BalanceAvailForStaking >= nValue && nValue / 1250000 > 0)
{
vUTXO.push_back(std::pair<int64_t, int64_t>( nTime, nValue));
if (fDebug10) LogPrintf("ETTS debug: pair (relative to current time: <%i, %i>", nTime - nCurrentTime, nValue);
// Only record a time below if it is after nCurrentTime, because UTXO's that have matured already are already stakeable and can be grouped (will be found)
// by the nCurrentTime record that was already injected above.
if(nTime > nCurrentTime) UniqueUTXOTimes.insert(nTime);
}
}

//The below line is only needed for debug. The order is actually only important in the outer loop, which is maintained in the UniqueUTXOTimes set.
//sort(vUTXO.begin(),vUTXO.end());


int64_t nTimePrev = nCurrentTime;
int64_t nDeltaTime = 0;
int64_t nThrows = 0;
int64_t nCoinWeight = 0;
double dProbAccumulator = 0;
double dCumulativeProbability = 0;
// Note: Even though this is a compound Poisson process leading to a compound geometric distribution, and the individual probabilities are
// small, we are mounting to high CDFs. This means to be reasonably accurate, we cannot just add the probabilities, because the intersections
// become significant. The CDF of a compound geometric distribution as you do tosses with different probabilities follows the
// recursion relation... CDF.i = 1 - (1 - CDF.i-1)(1 - p.i). If all probabilities are the same, this reduces to the familiar
// CDF.k = 1 - (1 - p)^k where ^ is exponentiation.
for(const auto& itertime : UniqueUTXOTimes)
{

nTime = itertime;
dProbAccumulator = 0;

for( auto& iterUTXO : vUTXO)
{

if (fDebug10) LogPrintf("ETTS debug: Unique UTXO Time: %u, vector pair <%u, %u>", nTime, iterUTXO.first, iterUTXO.second);

// If the "negative Gantt chart bar" is ending or has ended for a UTXO, it now accumulates probability. (I.e. the event time being checked
// is greater than or equal to the cooldown expiration of the UTXO.)
// accumulation for that UTXO.
if(nTime >= iterUTXO.first)
{
// The below weight calculation is just like the CalculateStakeWeightV8 in kernel.cpp.
nCoinWeight = iterUTXO.second / 1250000;

dProbAccumulator = 1 - ((1 - dProbAccumulator) * (1 - (dUnitStakeProbability * nCoinWeight)));
if (fDebug10) LogPrintf("ETTS debug: dProbAccumulator = %e", dProbAccumulator);
}

}
nDeltaTime = nTime - nTimePrev;
nThrows = nDeltaTime / (STAKE_TIMESTAMP_MASK + 1);
if (fDebug10) LogPrintf("ETTS debug: nThrows = %i", nThrows);
dCumulativeProbability = 1 - ((1 - dCumulativeProbability) * pow((1 - dProbAccumulator), nThrows));
if (fDebug10) LogPrintf("ETTS debug: dCumulativeProbability = %e", dCumulativeProbability);

if(dCumulativeProbability >= dConfidence)
break;

nTimePrev = nTime;
}

if (nStakesTime)
result = dStakeKernelsTriedAvg / nStakesTime;
// If (dConfidence - dCumulativeProbability) > 0, it means we exited the negative Gantt chart area and the desired confidence level
// has not been reached. All of the eligible UTXO's are contributing probability, and this is the final dProbAccumulator value.
// If the loop above is degenerate (i.e. only the current time pass through), then dCumulativeProbability will be zero.
// If it was not degenerate and the positive reqions in the Gantt chart area contributed some probability, then dCumulativeProbability will
// be greater than zero. We must compute the amount of time beyond nTime that is required to bridge the gap between
// dCumulativeProbability and dConfidence. If (dConfidence - dCumulativeProbability) <= 0 then we overshot during the Gantt chart area,
// and we will back off by nThrows amount, which will now be negative.
if (fDebug10) LogPrintf("ETTS debug: dProbAccumulator = %e", dProbAccumulator);

// Shouldn't happen because if we are down here, we are staking, and there have to be eligible UTXO's, but just in case...
if (dProbAccumulator == 0.0)
{
if (fDebug10) LogPrintf("ETTS debug: ERROR in dProbAccumulator calculations");
return result;
}

if (fDebug10) LogPrintf("ETTS debug: dConfidence = %f", dConfidence);
// If nThrows is negative, this just means we overshot in the Gantt chart loop and have to backtrack by nThrows.
nThrows = (int64_t)((log(1 - dConfidence) - log(1 - dCumulativeProbability)) / log(1 - dProbAccumulator));
if (fDebug10) LogPrintf("ETTS debug: nThrows = %i", nThrows);

nDeltaTime = nThrows * (STAKE_TIMESTAMP_MASK + 1);
if (fDebug10) LogPrintf("ETTS debug: nDeltaTime = %i", nDeltaTime);

// Because we are looking at the delta time required past nTime, which is where we exited the Gantt chart loop.
result = nDeltaTime + nTime - nCurrentTime;
if (fDebug10) LogPrintf("ETTS debug: ETTS at %d confidence = %i", dConfidence, result);

if (IsProtocolV2(nBestHeight))
result *= STAKE_TIMESTAMP_MASK + 1;
// The old calculation for comparative purposes...
double oldETTS = 0;

oldETTS = GetTargetSpacing(nBestHeight) * GetEstimatedNetworkWeight(nPoSInterval) / MinerStatus.WeightSum;
if (fDebug10) LogPrintf("ETTS debug: oldETTS = %f", oldETTS);

return result/100;
return result;
}


void GetGlobalStatus()
{
//Populate overview
Expand All @@ -460,10 +696,12 @@ void GetGlobalStatus()
{ LOCK(MinerStatus.lock);
GlobalStatusStruct.blocks = ToString(nBestHeight);
GlobalStatusStruct.difficulty = RoundToString(PORDiff,3);
GlobalStatusStruct.netWeight = RoundToString(GetPoSKernelPS(),2);
GlobalStatusStruct.netWeight = RoundToString(GetEstimatedNetworkWeight() / 80.0,2);
//todo: use the real weight from miner status (requires scaling)
GlobalStatusStruct.coinWeight = sWeight;
GlobalStatusStruct.magnitude = RoundToString(boincmagnitude,2);
GlobalStatusStruct.ETTS = RoundToString(GetEstimatedTimetoStake()/86400.0,3);
GlobalStatusStruct.ERRperday = RoundToString(boincmagnitude * GRCMagnitudeUnit(GetAdjustedTime()),2);
GlobalStatusStruct.project = msMiningProject;
GlobalStatusStruct.cpid = GlobalCPUMiningCPID.cpid;
GlobalStatusStruct.poll = msPoll;
Expand Down Expand Up @@ -3092,11 +3330,11 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex, bool fJustCheck, boo

}

if (bb.lastblockhash != pindex->pprev->GetBlockHash().GetHex())
{
std::string sNarr = "ConnectBlock[ResearchAge] : Historical DPOR Replay attack : lastblockhash != actual last block hash.";
LogPrintf("\n ****** %s ***** \n",sNarr);
}
if (bb.lastblockhash != pindex->pprev->GetBlockHash().GetHex())
{
std::string sNarr = "ConnectBlock[ResearchAge] : Historical DPOR Replay attack : lastblockhash != actual last block hash.";
LogPrintf("\n ****** %s ***** \n",sNarr);
}

if (IsResearchAgeEnabled(pindex->nHeight)
&& (BlockNeedsChecked(nTime) || nVersion>=9))
Expand Down Expand Up @@ -5448,7 +5686,7 @@ bool GetEarliestStakeTime(std::string grcaddress, std::string cpid)
}
else
{
myCPID = pblockindex->GetCPID();
myCPID = pblockindex->GetCPID();
}
if (cpid == myCPID && nCPIDTime==0 && IsResearcher(myCPID))
{
Expand Down Expand Up @@ -8724,3 +8962,4 @@ bool IsResearcher(const std::string& cpid)
{
return cpid.length() == 32;
}

6 changes: 5 additions & 1 deletion src/main.h
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ struct globalStatusType
std::string netWeight;
std::string coinWeight;
std::string magnitude;
std::string ETTS;
std::string ERRperday;
std::string project;
std::string cpid;
std::string status;
Expand Down Expand Up @@ -283,7 +285,9 @@ std::string UnpackBinarySuperblock(std::string sBlock);
bool IsSuperBlock(CBlockIndex* pIndex);
bool LoadSuperblock(std::string data, int64_t nTime, int height);

double GetPoSKernelPS();
double GetEstimatedNetworkWeight(unsigned int nPoSInterval = 40);
double GetAverageDifficulty(unsigned int nPoSInterval = 40);
double GetEstimatedTimetoStake(unsigned int nPoSInterval = 40, double dConfidence = 0.8);

unsigned int ComputeMinWork(unsigned int nBase, int64_t nTime);
unsigned int ComputeMinStake(unsigned int nBase, int64_t nTime, unsigned int nBlockTime);
Expand Down
8 changes: 4 additions & 4 deletions src/qt/bitcoingui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1898,15 +1898,15 @@ void BitcoinGUI::updateStakingIcon()
uint64_t nWeight, nLastInterval;
std::string ReasonNotStaking;
{ LOCK(MinerStatus.lock);
// not using real weigh to not break calculation
nWeight = MinerStatus.ValueSum;
// not using real weight to not break calculation - fixed - but retaining GRC units for instead of internal weight units.
nWeight = MinerStatus.WeightSum / 80.0;
nLastInterval = MinerStatus.nLastCoinStakeSearchInterval;
ReasonNotStaking = MinerStatus.ReasonNotStaking;
}

uint64_t nNetworkWeight = GetPoSKernelPS();
uint64_t nNetworkWeight = GetEstimatedNetworkWeight() / 80.0;
bool staking = nLastInterval && nWeight;
uint64_t nEstimateTime = staking ? (GetTargetSpacing(nBestHeight) * nNetworkWeight / nWeight) : 0;
uint64_t nEstimateTime = GetEstimatedTimetoStake();

if (staking)
{
Expand Down
Loading

0 comments on commit 12d8484

Please sign in to comment.