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:
@@ -21,6 +21,7 @@ import (
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -57,6 +58,17 @@ type LatencyEvent struct {
|
||||
LatencyMs int64
|
||||
}
|
||||
|
||||
// ShareEvent is emitted for every individual share submission logged by
|
||||
// ckpool (both accepted and rejected). Used to build rejection-reason
|
||||
// breakdowns and accepted-share difficulty distributions.
|
||||
type ShareEvent struct {
|
||||
SeenAt time.Time
|
||||
Diff float64 // share difficulty (always set for accepted; 0 for rejected)
|
||||
Hash string // block header hash (hex, accepted shares only)
|
||||
Rejected bool
|
||||
Reason string // rejection reason (e.g. "Stale", "Duplicate"); empty for accepted
|
||||
}
|
||||
|
||||
// Tailer follows a log file, surviving rotation/truncation, and emits
|
||||
// parsed events. Create with New, then Run in a goroutine.
|
||||
type Tailer struct {
|
||||
@@ -64,6 +76,7 @@ type Tailer struct {
|
||||
Events chan BlockEvent
|
||||
Attempts chan AttemptEvent
|
||||
Latencies chan LatencyEvent
|
||||
Shares chan ShareEvent
|
||||
Log *slog.Logger
|
||||
PollWait time.Duration // how long to sleep between EOF polls
|
||||
|
||||
@@ -90,6 +103,7 @@ func New(path string, log *slog.Logger) *Tailer {
|
||||
Events: make(chan BlockEvent, 16),
|
||||
Attempts: make(chan AttemptEvent, 16),
|
||||
Latencies: make(chan LatencyEvent, 16),
|
||||
Shares: make(chan ShareEvent, 64),
|
||||
Log: log,
|
||||
PollWait: 500 * time.Millisecond,
|
||||
}
|
||||
@@ -107,6 +121,19 @@ var (
|
||||
// Matches the latency line from our patch 0005:
|
||||
// "Block update latency: 92ms (ZMQ trigger to mining.notify broadcast)"
|
||||
latencyRE = regexp.MustCompile(`Block update latency:\s+(\d+)ms`)
|
||||
|
||||
// Share events from ckpool's stratifier.c (v1.0 / cfb0f83b):
|
||||
// "Accepted client 42 share diff 1234.5/65536/1.234G: <hexhash>"
|
||||
// "Rejected client 42 dupe diff 1234.5/65536/1.234G: <hexhash>"
|
||||
// "Rejected client 42 high diff 1234.5/65536/1.234G: <hexhash>"
|
||||
// "Rejected client 42 invalid share Stale"
|
||||
// The first number after "diff " is sdiff (share difficulty solved).
|
||||
acceptedShareRE = regexp.MustCompile(`Accepted client \S+ share diff ([0-9.]+)/[^:]+:\s*([0-9a-fA-F]+)`)
|
||||
// Rejected shares come in two forms:
|
||||
// 1. "dupe diff" / "high diff" — share was valid but duplicate or below target
|
||||
// 2. "invalid share <reason>" — share was structurally invalid (Stale, etc.)
|
||||
rejectedDiffRE = regexp.MustCompile(`Rejected client \S+ (\w+) diff [0-9.]+/`)
|
||||
rejectedInvalRE = regexp.MustCompile(`Rejected client \S+ invalid share (.+)`)
|
||||
)
|
||||
|
||||
// Run blocks until ctx is cancelled. It opens the file, seeks to either
|
||||
@@ -118,6 +145,7 @@ func (t *Tailer) Run(ctx context.Context) {
|
||||
defer close(t.Events)
|
||||
defer close(t.Attempts)
|
||||
defer close(t.Latencies)
|
||||
defer close(t.Shares)
|
||||
|
||||
var (
|
||||
f *os.File
|
||||
@@ -256,6 +284,35 @@ func (t *Tailer) Run(ctx context.Context) {
|
||||
}
|
||||
|
||||
func (t *Tailer) handleLine(line string) {
|
||||
// Accepted share: extract share difficulty and block header hash.
|
||||
if m := acceptedShareRE.FindStringSubmatch(line); m != nil {
|
||||
if d, err := strconv.ParseFloat(m[1], 64); err == nil {
|
||||
select {
|
||||
case t.Shares <- ShareEvent{SeenAt: time.Now(), Diff: d, Hash: m[2]}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
// Don't return — an accepted share may also be a block solve,
|
||||
// so let the solve-diff regex below get a chance to match too.
|
||||
}
|
||||
// Rejected share with diff info: "Rejected client <id> dupe|high diff ..."
|
||||
if m := rejectedDiffRE.FindStringSubmatch(line); m != nil {
|
||||
reason := rejectKeyword(m[1])
|
||||
select {
|
||||
case t.Shares <- ShareEvent{SeenAt: time.Now(), Rejected: true, Reason: reason}:
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
// Rejected share without diff: "Rejected client <id> invalid share <reason>"
|
||||
if m := rejectedInvalRE.FindStringSubmatch(line); m != nil {
|
||||
reason := strings.TrimSpace(m[1])
|
||||
select {
|
||||
case t.Shares <- ShareEvent{SeenAt: time.Now(), Rejected: true, Reason: reason}:
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
if m := latencyRE.FindStringSubmatch(line); m != nil {
|
||||
if ms, err := strconv.ParseInt(m[1], 10, 64); err == nil {
|
||||
select {
|
||||
@@ -301,6 +358,55 @@ func (t *Tailer) handleLine(line string) {
|
||||
}
|
||||
}
|
||||
|
||||
// rejectKeyword maps the short keyword ckpool uses in "Rejected client
|
||||
// <id> <keyword> diff ..." log lines to a human-readable reason.
|
||||
func rejectKeyword(kw string) string {
|
||||
switch strings.ToLower(kw) {
|
||||
case "dupe":
|
||||
return "Duplicate"
|
||||
case "high":
|
||||
return "Above target"
|
||||
default:
|
||||
return kw
|
||||
}
|
||||
}
|
||||
|
||||
// FindBestShareHash scans the log file for the accepted share with the
|
||||
// highest difficulty and returns its diff and block header hash. Used as
|
||||
// a one-time backfill when bestDiff is persisted but the hash is not
|
||||
// (pre-upgrade shares). Returns (0, "") if no accepted shares are found.
|
||||
func FindBestShareHash(path string) (float64, string) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return 0, ""
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var bestDiff float64
|
||||
var bestHash string
|
||||
scanner := bufio.NewScanner(f)
|
||||
scanner.Buffer(make([]byte, 0, 64*1024), 256*1024)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if !strings.Contains(line, "Accepted client") {
|
||||
continue
|
||||
}
|
||||
m := acceptedShareRE.FindStringSubmatch(line)
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
d, err := strconv.ParseFloat(m[1], 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if d >= bestDiff {
|
||||
bestDiff = d
|
||||
bestHash = m[2]
|
||||
}
|
||||
}
|
||||
return bestDiff, bestHash
|
||||
}
|
||||
|
||||
// sleep returns false if ctx was cancelled during the wait.
|
||||
func sleep(ctx context.Context, d time.Duration) bool {
|
||||
select {
|
||||
|
||||
Reference in New Issue
Block a user