Fix network diff adjustment prediction algo
This commit is contained in:
5 files changed
+186
-28
No files matched your search
@@ -98,6 +98,12 @@ type Snapshot struct {
|
||||
// observed block interval since the current epoch started.
|
||||
NextDifficultyPercent float64 `json:"next_difficulty_percent"`
|
||||
|
||||
// Observed mean seconds per block so far this epoch, measured
|
||||
// between block timestamps. Zero before the first block of an epoch.
|
||||
// The UI uses it to project the retarget ETA at the pace the network
|
||||
// is actually keeping rather than a flat 600s.
|
||||
EpochAvgBlockSeconds float64 `json:"epoch_avg_block_seconds,omitempty"`
|
||||
|
||||
// Bitcoin Core fields
|
||||
Chain *bitcoind.BlockchainInfo `json:"chain"`
|
||||
NetworkHashrateHs float64 `json:"network_hashrate_hs"`
|
||||
@@ -172,6 +178,10 @@ type Snapshot struct {
|
||||
}
|
||||
|
||||
const (
|
||||
// Bitcoin's difficulty retarget window and its target block spacing.
|
||||
retargetInterval = 2016
|
||||
targetBlockSeconds = 600
|
||||
|
||||
maxHistoryPoints = 1440 // 24h at 1 sample/min
|
||||
|
||||
// kvCumulativeWork is versioned: "cumulative_shares" (v1) accidentally
|
||||
@@ -279,6 +289,12 @@ type Aggregator struct {
|
||||
retargetEpoch int64
|
||||
retargetStartUnix int64
|
||||
|
||||
// Chain-tip block timestamp, refetched only when the tip hash
|
||||
// changes. The retarget estimate measures between block timestamps,
|
||||
// so it needs the tip's own time rather than wall-clock time.
|
||||
tipTimeHash string
|
||||
tipTimeUnix int64
|
||||
|
||||
// ckFailStreak counts consecutive refreshes where CKPool returned an
|
||||
// error. The first failure after a streak of successes is logged at
|
||||
// DEBUG (likely a transient warm-up or lock hiccup); repeated failures
|
||||
@@ -557,44 +573,47 @@ func (a *Aggregator) refresh(ctx context.Context) {
|
||||
}
|
||||
|
||||
// --- predicted difficulty adjustment ---
|
||||
// Fetch the timestamp of the first block in the current retarget
|
||||
// epoch (height - height%2016) once per epoch and cache it, then
|
||||
// compute (expected / elapsed - 1) * 100 where expected = blocks_in *
|
||||
// 600 seconds. Bitcoin clamps the consensus adjustment factor to
|
||||
// [1/4, 4x], i.e. [-75%, +300%].
|
||||
// Project the retarget from the pace the epoch has kept so far,
|
||||
// measured strictly between block timestamps: from the epoch's first
|
||||
// block to the tip is exactly `inEpoch` inter-block intervals.
|
||||
//
|
||||
// Measuring to wall-clock time instead adds the partial, in-progress
|
||||
// interval to the elapsed span without adding an interval to divide
|
||||
// it by. That skews the estimate toward "difficulty will fall" by
|
||||
// however long it has been since the last block — which averages a
|
||||
// full 600s and, a handful of blocks into an epoch, is most of the
|
||||
// sample. It washes out only once enough blocks accumulate to dwarf
|
||||
// it, which is why the estimate used to settle down around mid-epoch.
|
||||
if next.Chain != nil && next.Chain.Blocks > 0 {
|
||||
height := next.Chain.Blocks
|
||||
epoch := height / 2016
|
||||
inEpoch := height % 2016
|
||||
epoch := height / retargetInterval
|
||||
inEpoch := height % retargetInterval
|
||||
if inEpoch > 0 {
|
||||
if a.retargetEpoch != epoch || a.retargetStartUnix == 0 {
|
||||
startHeight := epoch * 2016
|
||||
startHeight := epoch * retargetInterval
|
||||
lookupCtx, cancelLookup := context.WithTimeout(ctx, 3*time.Second)
|
||||
if hash, err := a.RPC.GetBlockHash(lookupCtx, startHeight); err == nil {
|
||||
if blk, err := a.RPC.GetBlock(lookupCtx, hash); err == nil {
|
||||
if hdr, err := a.RPC.GetBlockHeader(lookupCtx, hash); err == nil {
|
||||
a.retargetEpoch = epoch
|
||||
a.retargetStartUnix = blk.Time
|
||||
a.retargetStartUnix = hdr.Time
|
||||
}
|
||||
}
|
||||
cancelLookup()
|
||||
}
|
||||
if a.retargetStartUnix > 0 {
|
||||
elapsed := float64(time.Now().Unix() - a.retargetStartUnix)
|
||||
// Match Bitcoin Core / mempool.space: project nActualTimespan
|
||||
// by treating the elapsed window as covering (inEpoch + 1)
|
||||
// block intervals, since at retarget the consensus formula
|
||||
// uses (lastBlock.time - firstBlock.time) across all 2016
|
||||
// blocks of the epoch.
|
||||
expected := float64(inEpoch+1) * 600
|
||||
// Tip timestamp, refetched only when the tip actually moves.
|
||||
if next.Chain.BestBlockHash != "" && a.tipTimeHash != next.Chain.BestBlockHash {
|
||||
lookupCtx, cancelLookup := context.WithTimeout(ctx, 3*time.Second)
|
||||
if hdr, err := a.RPC.GetBlockHeader(lookupCtx, next.Chain.BestBlockHash); err == nil {
|
||||
a.tipTimeHash = next.Chain.BestBlockHash
|
||||
a.tipTimeUnix = hdr.Time
|
||||
}
|
||||
cancelLookup()
|
||||
}
|
||||
if a.retargetStartUnix > 0 && a.tipTimeUnix > 0 {
|
||||
elapsed := float64(a.tipTimeUnix - a.retargetStartUnix)
|
||||
next.NextDifficultyPercent = estimateRetargetPercent(inEpoch, elapsed)
|
||||
if elapsed > 0 {
|
||||
factor := expected / elapsed
|
||||
if factor > 4 {
|
||||
factor = 4
|
||||
}
|
||||
if factor < 0.25 {
|
||||
factor = 0.25
|
||||
}
|
||||
next.NextDifficultyPercent = (factor - 1) * 100
|
||||
next.EpochAvgBlockSeconds = elapsed / float64(inEpoch)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -837,6 +856,31 @@ func (a *Aggregator) ResetAckedBestDiff() {
|
||||
}
|
||||
}
|
||||
|
||||
// estimateRetargetPercent projects the next difficulty adjustment from
|
||||
// the pace of the current epoch: inEpoch inter-block intervals took
|
||||
// elapsed seconds, both measured between block timestamps. Returns the
|
||||
// percentage change, clamped to Bitcoin's consensus range of
|
||||
// [-75, +300].
|
||||
func estimateRetargetPercent(inEpoch int64, elapsed float64) float64 {
|
||||
if inEpoch <= 0 || elapsed <= 0 {
|
||||
return 0
|
||||
}
|
||||
// Consensus budgets retargetInterval*600 seconds for an epoch but
|
||||
// spans them across retargetInterval-1 inter-block intervals — the
|
||||
// long-standing off-by-one — so on-target pace is fractionally above
|
||||
// 600s per block, and a perfectly on-time epoch retargets by ~0%
|
||||
// rather than a hair negative.
|
||||
targetPerInterval := float64(retargetInterval*targetBlockSeconds) / float64(retargetInterval-1)
|
||||
factor := (float64(inEpoch) * targetPerInterval) / elapsed
|
||||
if factor > 4 {
|
||||
factor = 4
|
||||
}
|
||||
if factor < 0.25 {
|
||||
factor = 0.25
|
||||
}
|
||||
return (factor - 1) * 100
|
||||
}
|
||||
|
||||
// diffBucket returns the index [0..5] for a share difficulty:
|
||||
//
|
||||
// 0: < 1M 1: 1M–100M 2: 100M–1G
|
||||
|
||||
Reference in new issue
Block a user