Add best share analysis page, miner column, and UI improvements
- Best share page: hex + binary hash comparison against network target, per-bit coloring showing exactly which bits prevented a valid block, toggle between network diff at time of finding vs current diff - Capture best share hash from ckpool logs with one-time backfill - Persist network difficulty at time of best share for historical accuracy - Add miner (worker) column to blocks table via coinbase address matching - Truncate block hashes in table with full hash on hover - Increase hashrate chart Y-axis to 7 ticks for better readability
This commit is contained in:
@@ -3,6 +3,7 @@ package state
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kamadopool/kamado-api/internal/logmon"
|
||||
@@ -24,6 +25,7 @@ type BlockRecord struct {
|
||||
ShareDiff float64 `json:"share_diff,omitempty"`
|
||||
OrphanedAt *time.Time `json:"orphaned_at,omitempty"`
|
||||
Chain string `json:"chain,omitempty"` // "main", "test", "signet"
|
||||
Miner string `json:"miner,omitempty"` // workername (address.worker) who found the block
|
||||
}
|
||||
|
||||
// timePtr returns a pointer to t if non-zero, nil otherwise.
|
||||
@@ -157,17 +159,18 @@ func (a *Aggregator) IngestBlockEvents(ctx context.Context, events <-chan logmon
|
||||
ShareDiff: ev.ShareDiff,
|
||||
Chain: currentChain,
|
||||
}
|
||||
// Best-effort enrich with hash + coinbase reward via bitcoind.
|
||||
// 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. Both are fire-
|
||||
// and-forget — if bitcoind is down we still record the block
|
||||
// with whatever we have.
|
||||
// (verbosity 2) to sum the coinbase outputs and extract the
|
||||
// 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)
|
||||
if hash, err := a.RPC.GetBlockHash(lookupCtx, ev.Height); err == nil {
|
||||
rec.Hash = hash
|
||||
if blk, err := a.RPC.GetBlock(lookupCtx, hash); err == nil {
|
||||
rec.RewardBT = blk.CoinbaseReward()
|
||||
rec.Miner = a.minerFromCoinbase(blk.CoinbaseAddress())
|
||||
} else {
|
||||
a.Log.Warn("bitcoind getblock failed", "hash", hash, "err", err)
|
||||
}
|
||||
@@ -186,6 +189,7 @@ func (a *Aggregator) IngestBlockEvents(ctx context.Context, events <-chan logmon
|
||||
Source: rec.Source,
|
||||
ShareDiff: rec.ShareDiff,
|
||||
Chain: rec.Chain,
|
||||
Miner: rec.Miner,
|
||||
})
|
||||
if err != nil {
|
||||
a.Log.Warn("block persist failed", "height", rec.Height, "err", err)
|
||||
@@ -273,6 +277,7 @@ func (a *Aggregator) reconcileOnce(ctx context.Context) {
|
||||
lookupCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
hash := b.Hash
|
||||
reward := b.RewardBT
|
||||
miner := b.Miner
|
||||
if hash == "" {
|
||||
if h, herr := a.RPC.GetBlockHash(lookupCtx, b.Height); herr == nil {
|
||||
hash = h
|
||||
@@ -281,18 +286,23 @@ func (a *Aggregator) reconcileOnce(ctx context.Context) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if reward == 0 && hash != "" {
|
||||
if (reward == 0 || miner == "") && hash != "" {
|
||||
if blk, berr := a.RPC.GetBlock(lookupCtx, hash); berr == nil {
|
||||
reward = blk.CoinbaseReward()
|
||||
if reward == 0 {
|
||||
reward = blk.CoinbaseReward()
|
||||
}
|
||||
if miner == "" {
|
||||
miner = a.minerFromCoinbase(blk.CoinbaseAddress())
|
||||
}
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
if hash != b.Hash || reward != b.RewardBT {
|
||||
if err := a.Store.UpdateEnrichment(b.Height, hash, reward); err != nil {
|
||||
if hash != b.Hash || reward != b.RewardBT || miner != b.Miner {
|
||||
if err := a.Store.UpdateEnrichment(b.Height, hash, reward, miner); err != nil {
|
||||
a.Log.Warn("reconcile: update enrichment failed", "height", b.Height, "err", err)
|
||||
continue
|
||||
}
|
||||
a.Log.Info("reconcile: enriched block", "height", b.Height, "hash", hash, "reward", reward)
|
||||
a.Log.Info("reconcile: enriched block", "height", b.Height, "hash", hash, "reward", reward, "miner", miner)
|
||||
enrichedAny = true
|
||||
}
|
||||
}
|
||||
@@ -480,6 +490,32 @@ func (a *Aggregator) reconcileOnce(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// minerFromCoinbase matches a coinbase payout address to a full workername
|
||||
// from the current worker list. In solo mode, stratum usernames are
|
||||
// "address" or "address.label", and the coinbase pays the address portion.
|
||||
// Returns the first matching workername (address.worker), or just the
|
||||
// address if no worker match is found.
|
||||
func (a *Aggregator) minerFromCoinbase(addr string) string {
|
||||
if addr == "" {
|
||||
return ""
|
||||
}
|
||||
a.mu.RLock()
|
||||
workers := a.snap.Workers
|
||||
a.mu.RUnlock()
|
||||
for _, w := range workers {
|
||||
// Worker.User is "address.workername" in ckpool. The address
|
||||
// portion is everything before the first dot.
|
||||
wAddr := w.User
|
||||
if dot := strings.IndexByte(wAddr, '.'); dot >= 0 {
|
||||
wAddr = wAddr[:dot]
|
||||
}
|
||||
if wAddr == addr {
|
||||
return w.User + "." + w.Worker
|
||||
}
|
||||
}
|
||||
return addr
|
||||
}
|
||||
|
||||
// maxBlockHistory caps in-memory block history. Persistence comes in
|
||||
// Phase 2b.5 via SQLite; for now recent blocks survive only this
|
||||
// process's lifetime.
|
||||
@@ -526,6 +562,7 @@ func (a *Aggregator) loadPersistedBlocks() {
|
||||
ShareDiff: r.ShareDiff,
|
||||
OrphanedAt: timePtr(r.OrphanedAt),
|
||||
Chain: r.Chain,
|
||||
Miner: r.Miner,
|
||||
})
|
||||
}
|
||||
a.mu.Lock()
|
||||
@@ -581,6 +618,7 @@ func (a *Aggregator) BlocksFromStore() []BlockRecord {
|
||||
ShareDiff: r.ShareDiff,
|
||||
OrphanedAt: timePtr(r.OrphanedAt),
|
||||
Chain: r.Chain,
|
||||
Miner: r.Miner,
|
||||
})
|
||||
}
|
||||
return out
|
||||
|
||||
Reference in New Issue
Block a user