Add share cards
This commit is contained in:
51 files changed
+1193
-239
No files matched your search
@@ -37,7 +37,7 @@ type BestSharePow struct {
|
||||
|
||||
// StratumServer describes one entry in ckpool's serverurl[] array. ckpool
|
||||
// tags every client with the index of the bind it arrived on, but the index
|
||||
// alone doesn't say what that bind *is* — that depends on how the deployment
|
||||
// alone doesn't say what that bind *is*, that depends on how the deployment
|
||||
// rendered ckpool.conf. Declaring the array lets the dashboard report the
|
||||
// actual transport (plaintext, TLS with which certificate) instead of
|
||||
// hardcoding a bind order.
|
||||
@@ -77,7 +77,7 @@ type Snapshot struct {
|
||||
// Full PoW reproduction data (raw block header, coinbase, merkle
|
||||
// branches) for the best share seen since ckpool patch 0008 was
|
||||
// deployed. May describe a lower-diff share than BestDiff when the
|
||||
// all-time best predates the patch — the UI compares hashes and
|
||||
// all-time best predates the patch, the UI compares hashes and
|
||||
// shows a disclaimer in that case.
|
||||
BestSharePow *BestSharePow `json:"best_share_pow,omitempty"`
|
||||
// Last best-diff value acknowledged by the user via the UI.
|
||||
@@ -129,7 +129,7 @@ type Snapshot struct {
|
||||
// Counts of share-submit attempts ("Possible/Submitting block solve"
|
||||
// log lines) and confirmed solves ("Solved and confirmed block").
|
||||
// A growing gap means bitcoind is rejecting our submissions or
|
||||
// dropping the RPC — surface it in the UI as an alert.
|
||||
// dropping the RPC, surface it in the UI as an alert.
|
||||
BlockSubmitAttempts int64 `json:"block_submit_attempts"`
|
||||
BlockSubmitsConfirmed int64 `json:"block_submits_confirmed"`
|
||||
|
||||
@@ -186,7 +186,7 @@ const (
|
||||
|
||||
// kvCumulativeWork is versioned: "cumulative_shares" (v1) accidentally
|
||||
// summed pool.shares (raw per-share count), which is meaningless for
|
||||
// hashrate math. "cumulative_work" (v2) sums pool.accepted — the
|
||||
// hashrate math. "cumulative_work" (v2) sums pool.accepted, the
|
||||
// diff-1-normalized work, so cumulative_work * 2^32 is real hashes.
|
||||
// The old key is orphaned in the kv table on upgrade; harmless.
|
||||
kvCumulativeWork = "cumulative_work"
|
||||
@@ -264,14 +264,14 @@ type Aggregator struct {
|
||||
|
||||
// Cumulative pool work in diff-1-normalized shares, surviving
|
||||
// ckpool restarts. Maintained by re-reading pool.Shares each
|
||||
// refresh and integrating only the positive delta — a decrease
|
||||
// refresh and integrating only the positive delta, a decrease
|
||||
// means ckpool restarted and its counter reset to 0, so we reset
|
||||
// our delta baseline without losing the accumulated total.
|
||||
//
|
||||
// hasPoolSharesBaseline guards against double-counting on api
|
||||
// restart: cumulativeShares is loaded from disk, and the first
|
||||
// post-load observation of pool.Shares only establishes the
|
||||
// baseline — the current ckpool counter was already accounted for
|
||||
// baseline, the current ckpool counter was already accounted for
|
||||
// before we crashed.
|
||||
cumulativeShares float64
|
||||
lastPoolShares float64
|
||||
@@ -395,7 +395,7 @@ type Aggregator struct {
|
||||
loadOnce sync.Once
|
||||
// statsLoaded reports that the alltime share statistics have been
|
||||
// read back from the store. Persisting before that would write the
|
||||
// zero-valued in-memory counters over the accumulated history — the
|
||||
// zero-valued in-memory counters over the accumulated history, the
|
||||
// first share event triggers a save, so this is the difference
|
||||
// between a restart and permanent data loss.
|
||||
statsLoaded bool
|
||||
@@ -419,7 +419,7 @@ func New(ck *ckpool.Client, rpc *bitcoind.RPC, interval time.Duration, log *slog
|
||||
// Run calls it, but callers should invoke it directly before starting
|
||||
// any ingest goroutine: those goroutines mutate the same counters and
|
||||
// persist them on their first event, so a restore still in flight would
|
||||
// be overwritten by — and then read back as — near-empty state. Safe to
|
||||
// be overwritten by, and then read back as, near-empty state. Safe to
|
||||
// call repeatedly; only the first call does the work.
|
||||
func (a *Aggregator) LoadPersisted() {
|
||||
a.loadOnce.Do(func() {
|
||||
@@ -429,7 +429,7 @@ func (a *Aggregator) LoadPersisted() {
|
||||
}
|
||||
|
||||
// Ready returns a channel that's closed once the aggregator has
|
||||
// completed its first refresh — used by main to delay HTTP serving
|
||||
// completed its first refresh, used by main to delay HTTP serving
|
||||
// until /api/snapshot reflects real state instead of zeros.
|
||||
func (a *Aggregator) Ready() <-chan struct{} {
|
||||
return a.ready
|
||||
@@ -580,7 +580,7 @@ func (a *Aggregator) refresh(ctx context.Context) {
|
||||
// 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
|
||||
// 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.
|
||||
@@ -651,12 +651,12 @@ func (a *Aggregator) refresh(ctx context.Context) {
|
||||
|
||||
// --- cumulative work tracking ---
|
||||
// Integrate only positive deltas on pool.Accepted, which is ckpool's
|
||||
// accounted_diff_shares — the sum of each accepted share's
|
||||
// accounted_diff_shares, the sum of each accepted share's
|
||||
// difficulty, in diff-1-normalized units. pool.Shares is the raw
|
||||
// per-share count and is useless for hashrate math. A decrease
|
||||
// means ckpool restarted (or its state reset); we reset the
|
||||
// baseline without touching the accumulated total. The first
|
||||
// observation after load only establishes the baseline — the
|
||||
// observation after load only establishes the baseline, the
|
||||
// current counter was already integrated before we shut down.
|
||||
if next.Pool != nil {
|
||||
cur := float64(next.Pool.Accepted)
|
||||
@@ -687,7 +687,7 @@ func (a *Aggregator) refresh(ctx context.Context) {
|
||||
if curAcc >= a.lastPoolAcceptedRaw {
|
||||
a.allTimeAccepted += curAcc - a.lastPoolAcceptedRaw
|
||||
} else {
|
||||
// ckpool restarted — reset session share stats.
|
||||
// ckpool restarted, reset session share stats.
|
||||
a.resetSessionShareStats()
|
||||
}
|
||||
if curRej >= a.lastPoolRejectedRaw {
|
||||
@@ -866,8 +866,8 @@ func estimateRetargetPercent(inEpoch int64, elapsed float64) float64 {
|
||||
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
|
||||
// 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)
|
||||
@@ -1000,7 +1000,7 @@ func (a *Aggregator) IngestShareEvents(ctx context.Context, events <-chan logmon
|
||||
// stats that were lost or truncated: the log is the only other record of
|
||||
// per-share difficulty, and ckpool keeps its full history there.
|
||||
//
|
||||
// Deliberately never automatic — the scan walks every line of a log that
|
||||
// Deliberately never automatic, the scan walks every line of a log that
|
||||
// can hold millions, and it was a long startup scan that made the race
|
||||
// this repairs so easy to hit in the first place.
|
||||
func (a *Aggregator) RebuildShareStatsFromLog() (adopted bool, scanned int64, err error) {
|
||||
@@ -1085,7 +1085,7 @@ func (a *Aggregator) persistShareStats() {
|
||||
|
||||
// loadPersistedState restores cumulative work and hashrate history from
|
||||
// the store so they survive process restarts. Safe to call with a nil
|
||||
// Store — becomes a no-op.
|
||||
// Store, becomes a no-op.
|
||||
func (a *Aggregator) loadPersistedState() {
|
||||
if a.Store == nil {
|
||||
return
|
||||
@@ -1173,7 +1173,7 @@ func (a *Aggregator) loadPersistedState() {
|
||||
if logDiff, h := logmon.FindBestShareHash(a.LogFilePath); h != "" {
|
||||
a.mu.Lock()
|
||||
a.bestShareHash = h
|
||||
// The log may contain the true best — adopt it if higher.
|
||||
// The log may contain the true best, adopt it if higher.
|
||||
if logDiff > a.bestDiff {
|
||||
a.bestDiff = logDiff
|
||||
if a.Store != nil {
|
||||
|
||||
@@ -168,7 +168,7 @@ func (a *Aggregator) IngestBlockEvents(ctx context.Context, events <-chan logmon
|
||||
// Best-effort enrich with hash + coinbase reward + miner via bitcoind.
|
||||
// We look up the hash from height, then fetch the full block
|
||||
// (verbosity 2) to sum the coinbase outputs and extract the
|
||||
// payout address. Both are fire-and-forget — if bitcoind is
|
||||
// payout address. Both are fire-and-forget, if bitcoind is
|
||||
// down we still record the block with whatever we have.
|
||||
if a.RPC != nil {
|
||||
lookupCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
@@ -200,7 +200,7 @@ func (a *Aggregator) IngestBlockEvents(ctx context.Context, events <-chan logmon
|
||||
if err != nil {
|
||||
a.Log.Warn("block persist failed", "height", rec.Height, "err", err)
|
||||
} else if !inserted {
|
||||
a.Log.Warn("block at this height already recorded — duplicate or pre-existing entry, not counting as new",
|
||||
a.Log.Warn("block at this height already recorded, duplicate or pre-existing entry, not counting as new",
|
||||
"height", rec.Height, "hash", rec.Hash)
|
||||
isNew = false
|
||||
}
|
||||
@@ -257,7 +257,7 @@ func (a *Aggregator) reconcileOnce(ctx context.Context) {
|
||||
|
||||
// We need the current chain for both enrichment and reorg detection.
|
||||
// If the first refresh hasn't completed yet, currentChain is "" and
|
||||
// any chain comparison would be meaningless — skip until the next tick.
|
||||
// any chain comparison would be meaningless, skip until the next tick.
|
||||
a.mu.RLock()
|
||||
currentChain := ""
|
||||
if a.snap.Chain != nil {
|
||||
@@ -269,7 +269,7 @@ func (a *Aggregator) reconcileOnce(ctx context.Context) {
|
||||
}
|
||||
|
||||
// Pass 1: enrichment. Fetch hash + reward for any missing-data rows.
|
||||
// Skip blocks from a different chain — enriching a testnet block
|
||||
// Skip blocks from a different chain, enriching a testnet block
|
||||
// against mainnet RPC would overwrite its hash with the wrong value.
|
||||
missing, err := a.Store.BlocksNeedingEnrichment(since)
|
||||
if err != nil {
|
||||
@@ -350,10 +350,10 @@ func (a *Aggregator) reconcileOnce(ctx context.Context) {
|
||||
canonical, herr := a.RPC.GetBlockHash(lookupCtx, b.Height)
|
||||
cancel()
|
||||
if herr != nil {
|
||||
continue // RPC transient error — retry next sweep
|
||||
continue // RPC transient error, retry next sweep
|
||||
}
|
||||
if canonical == b.Hash {
|
||||
continue // matches the canonical chain — all good
|
||||
continue // matches the canonical chain, all good
|
||||
}
|
||||
// Hash mismatch. Determine whether it's a real reorg or a
|
||||
// cross-network block by checking if our hash exists on this chain.
|
||||
@@ -391,7 +391,7 @@ func (a *Aggregator) reconcileOnce(ctx context.Context) {
|
||||
// Pass 3: un-orphan blocks that were previously false-positived.
|
||||
// This covers two cases:
|
||||
// a) Blocks with Chain != currentChain that got orphaned before this
|
||||
// fix was deployed — un-orphan them now.
|
||||
// fix was deployed, un-orphan them now.
|
||||
// b) Same-chain blocks whose hash now matches the canonical chain
|
||||
// (e.g. the "reorg" reverted before we checked again).
|
||||
fixedAny := false
|
||||
@@ -403,7 +403,7 @@ func (a *Aggregator) reconcileOnce(ctx context.Context) {
|
||||
continue
|
||||
}
|
||||
if b.Chain != "" && b.Chain != currentChain {
|
||||
// Cross-network orphan from before the fix — un-orphan.
|
||||
// Cross-network orphan from before the fix, un-orphan.
|
||||
if err := a.Store.UnmarkOrphaned(b.Height); err != nil {
|
||||
a.Log.Warn("reconcile: unmark cross-network orphan failed", "height", b.Height, "err", err)
|
||||
continue
|
||||
@@ -418,7 +418,7 @@ func (a *Aggregator) reconcileOnce(ctx context.Context) {
|
||||
_, berr := a.RPC.GetBlock(lookupCtx, b.Hash)
|
||||
cancel()
|
||||
if berr != nil {
|
||||
// Block doesn't exist on this chain — cross-network.
|
||||
// Block doesn't exist on this chain, cross-network.
|
||||
otherChain := b.Chain
|
||||
if otherChain == "" || otherChain == currentChain {
|
||||
otherChain = inferOtherChain(currentChain)
|
||||
@@ -432,7 +432,7 @@ func (a *Aggregator) reconcileOnce(ctx context.Context) {
|
||||
fixedAny = true
|
||||
continue
|
||||
}
|
||||
// Block exists on this chain — check if it's canonical again.
|
||||
// Block exists on this chain, check if it's canonical again.
|
||||
lookupCtx2, cancel2 := context.WithTimeout(ctx, 3*time.Second)
|
||||
canonical, herr := a.RPC.GetBlockHash(lookupCtx2, b.Height)
|
||||
cancel2()
|
||||
@@ -444,7 +444,7 @@ func (a *Aggregator) reconcileOnce(ctx context.Context) {
|
||||
a.Log.Warn("reconcile: unmark reverted-reorg failed", "height", b.Height, "err", err)
|
||||
continue
|
||||
}
|
||||
a.Log.Info("reconcile: cleared orphan — hash matches canonical again",
|
||||
a.Log.Info("reconcile: cleared orphan, hash matches canonical again",
|
||||
"height", b.Height, "hash", b.Hash)
|
||||
fixedAny = true
|
||||
}
|
||||
@@ -452,9 +452,9 @@ func (a *Aggregator) reconcileOnce(ctx context.Context) {
|
||||
|
||||
// Pass 4: stamp chain on legacy/mis-stamped blocks that have a hash.
|
||||
// Handles Chain="" (never stamped), "other" (old fallback), and "test"
|
||||
// (wrong identifier — testnet4 reports as "testnet4", not "test").
|
||||
// (wrong identifier, testnet4 reports as "testnet4", not "test").
|
||||
// Uses GetBlock(hash) to determine if the block belongs to the current
|
||||
// network or a different one. No lookback restriction — this is a
|
||||
// network or a different one. No lookback restriction, this is a
|
||||
// one-time migration for pre-existing rows.
|
||||
for _, b := range recent {
|
||||
if (b.Chain != "" && b.Chain != "other" && b.Chain != "test") || b.Hash == "" {
|
||||
@@ -464,7 +464,7 @@ func (a *Aggregator) reconcileOnce(ctx context.Context) {
|
||||
_, berr := a.RPC.GetBlock(lookupCtx, b.Hash)
|
||||
cancel()
|
||||
if berr != nil {
|
||||
// Block not found on this network — infer the other chain.
|
||||
// Block not found on this network, infer the other chain.
|
||||
inferredChain := inferOtherChain(currentChain)
|
||||
if err := a.Store.StampChain(b.Height, inferredChain); err != nil {
|
||||
a.Log.Warn("reconcile: stamp legacy block failed", "height", b.Height, "err", err)
|
||||
@@ -545,7 +545,7 @@ func (a *Aggregator) appendBlock(rec BlockRecord) Snapshot {
|
||||
}
|
||||
|
||||
// loadPersistedBlocks seeds a.blocks from the store so history survives
|
||||
// restarts. Safe to call with a nil Store — becomes a no-op.
|
||||
// restarts. Safe to call with a nil Store, becomes a no-op.
|
||||
func (a *Aggregator) loadPersistedBlocks() {
|
||||
if a.Store == nil {
|
||||
return
|
||||
|
||||
@@ -90,7 +90,7 @@ func setChain(agg *Aggregator, chain string) {
|
||||
// ---- reconcileOnce tests ----
|
||||
|
||||
// TestReconcileOnce_ChainFilter verifies that a block recorded on testnet
|
||||
// is NOT orphaned when the node is now on mainnet — even if the canonical
|
||||
// is NOT orphaned when the node is now on mainnet, even if the canonical
|
||||
// hash at that height on mainnet is different.
|
||||
func TestReconcileOnce_ChainFilter_NoFalseOrphan(t *testing.T) {
|
||||
st := openTempStore(t)
|
||||
@@ -109,7 +109,7 @@ func TestReconcileOnce_ChainFilter_NoFalseOrphan(t *testing.T) {
|
||||
}
|
||||
|
||||
// Mock RPC: getblockhash returns a different mainnet hash; getblock
|
||||
// for our testnet hash returns "Block not found" — it doesn't exist
|
||||
// for our testnet hash returns "Block not found", it doesn't exist
|
||||
// on mainnet at all, confirming it's cross-network not a reorg.
|
||||
rpc := mockRPC(t, func(method string) (any, *rpcErrResp) {
|
||||
switch method {
|
||||
@@ -132,7 +132,7 @@ func TestReconcileOnce_ChainFilter_NoFalseOrphan(t *testing.T) {
|
||||
}
|
||||
for _, b := range blocks {
|
||||
if b.Height == 100 && !b.OrphanedAt.IsZero() {
|
||||
t.Error("testnet block was falsely orphaned after switching to mainnet — chain filter broken")
|
||||
t.Error("testnet block was falsely orphaned after switching to mainnet, chain filter broken")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,7 +156,7 @@ func TestReconcileOnce_GenuineReorg(t *testing.T) {
|
||||
}
|
||||
|
||||
// getblockhash returns a different canonical hash (reorg happened).
|
||||
// getblock for our hash SUCCEEDS — the block still exists in bitcoind's
|
||||
// getblock for our hash SUCCEEDS, the block still exists in bitcoind's
|
||||
// block store after a reorg, it's just no longer on the active chain.
|
||||
// This distinguishes a real reorg from a cross-network block.
|
||||
rpc := mockRPC(t, func(method string) (any, *rpcErrResp) {
|
||||
@@ -223,7 +223,7 @@ func TestReconcileOnce_RPCError_NoOrphan(t *testing.T) {
|
||||
blocks, _ := st.Recent(10)
|
||||
for _, b := range blocks {
|
||||
if b.Height == 300 && !b.OrphanedAt.IsZero() {
|
||||
t.Error("block was orphaned on RPC error — should only orphan on definitive hash mismatch")
|
||||
t.Error("block was orphaned on RPC error, should only orphan on definitive hash mismatch")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,7 +240,7 @@ func TestReconcileOnce_LegacyBlock_StillChecked(t *testing.T) {
|
||||
RewardBT: 3.125,
|
||||
FoundAt: time.Now(),
|
||||
Source: "test",
|
||||
Chain: "", // legacy — no chain recorded
|
||||
Chain: "", // legacy, no chain recorded
|
||||
})
|
||||
|
||||
// Canonical hash differs AND getblock succeeds (block exists on this
|
||||
@@ -250,7 +250,7 @@ func TestReconcileOnce_LegacyBlock_StillChecked(t *testing.T) {
|
||||
case "getblockhash":
|
||||
return "different_canonical", nil
|
||||
case "getblock":
|
||||
// Block exists in this chain's store — genuine reorg
|
||||
// Block exists in this chain's store, genuine reorg
|
||||
return map[string]any{"hash": "legacy_hash", "height": 400, "confirmations": -1}, nil
|
||||
}
|
||||
return nil, nil
|
||||
@@ -281,11 +281,11 @@ func TestReconcileOnce_LegacyBlock_CrossNetwork(t *testing.T) {
|
||||
RewardBT: 50,
|
||||
FoundAt: time.Now(),
|
||||
Source: "test",
|
||||
Chain: "", // legacy — no chain recorded
|
||||
Chain: "", // legacy, no chain recorded
|
||||
})
|
||||
|
||||
// Canonical hash differs AND getblock fails (block doesn't exist on
|
||||
// this network at all). Should NOT orphan — should stamp as "other".
|
||||
// this network at all). Should NOT orphan, should stamp as "other".
|
||||
rpc := mockRPC(t, func(method string) (any, *rpcErrResp) {
|
||||
switch method {
|
||||
case "getblockhash":
|
||||
@@ -436,6 +436,6 @@ func TestIngestBlockEvents_ChainStamping(t *testing.T) {
|
||||
t.Fatal("block not found in store")
|
||||
}
|
||||
if blocks[0].Chain != "test" {
|
||||
t.Errorf("Chain = %q, want test — chain stamping broken", blocks[0].Chain)
|
||||
t.Errorf("Chain = %q, want test, chain stamping broken", blocks[0].Chain)
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ func TestPowEndToEnd_LogToSnapshot(t *testing.T) {
|
||||
}
|
||||
|
||||
// The snapshot must expose it under best_share_pow with the flattened
|
||||
// PowData fields — this is the contract the UI reads.
|
||||
// PowData fields, this is the contract the UI reads.
|
||||
snap := Snapshot{BestSharePow: func() *BestSharePow {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
|
||||
@@ -103,8 +103,8 @@ func TestPowPairing_UnmatchedIsNotPromoted(t *testing.T) {
|
||||
|
||||
// The all-time best share may hugely outrank anything recent (found
|
||||
// before header capture existed). The PoW record must still populate
|
||||
// from the first confirmed share — it tracks "best since the feature
|
||||
// went live", never waiting for the all-time best to be beaten — while
|
||||
// from the first confirmed share, it tracks "best since the feature
|
||||
// went live", never waiting for the all-time best to be beaten, while
|
||||
// leaving the all-time bestDiff/bestShareHash untouched.
|
||||
func TestPowPairing_BelowAllTimeBestStillPopulates(t *testing.T) {
|
||||
a := New(nil, nil, time.Minute, discardLog())
|
||||
@@ -210,7 +210,7 @@ func TestPowPairing_LowerDiffDoesNotReplace(t *testing.T) {
|
||||
// statistics: the ingest goroutine runs concurrently with the restore,
|
||||
// and its first event triggers a persist. If that persist is allowed to
|
||||
// run before the counters are loaded, it writes the zero-valued
|
||||
// in-memory histogram over the stored history — which the restore then
|
||||
// in-memory histogram over the stored history, which the restore then
|
||||
// reads back, making the loss permanent.
|
||||
func TestShareStats_NotPersistedBeforeLoad(t *testing.T) {
|
||||
s := openTempStore(t)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
// The estimate must be stable from the very first blocks of an epoch:
|
||||
// three blocks on target pace should read the same ~0% as a thousand.
|
||||
// The old wall-clock measurement made early readings swing wildly
|
||||
// because the time since the last block — 600s on average — was counted
|
||||
// because the time since the last block, 600s on average, was counted
|
||||
// into the elapsed span without a matching interval to divide it by.
|
||||
func TestEstimateRetargetPercent_StableEarlyInEpoch(t *testing.T) {
|
||||
// Exactly on-pace: each interval takes the consensus target.
|
||||
@@ -82,7 +82,7 @@ func TestEstimateRetargetPercent_WallClockBiasWasWorstEarly(t *testing.T) {
|
||||
t.Errorf("mid-epoch wall-clock error = %+.2f%%, expected it to have washed out", mid)
|
||||
}
|
||||
// And with block timestamps, that same on-pace epoch reads ~0 at
|
||||
// both points — which is the actual fix.
|
||||
// both points, which is the actual fix.
|
||||
if got := estimateRetargetPercent(3, 3*onPace); math.Abs(got) > 0.01 {
|
||||
t.Errorf("block-timestamp measurement at 3 blocks = %+.4f%%, want ~0%%", got)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user