Files
KamadoPool/api/internal/state/pow_e2e_test.go
T
2026-09-04 00:54:26 +03:00

122 lines
4.5 KiB
Go

package state
import (
"context"
"encoding/json"
"os"
"testing"
"time"
"github.com/kamadopool/kamado-api/internal/logmon"
)
// The two lines below are the verbatim output of ckpool patch 0008's
// jansson code (replicated against ckpool's in-tree jansson 2.14 with
// identical calls and JSON_COMPACT), using Bitcoin block 125552's real
// header and hash. If ckpool's encoding and this pipeline ever drift,
// this test is the tripwire.
const (
ckpoolPowLine = `[2026-08-31 18:00:01.123] Best share PoW data {"sdiff":62953.415063916997,"netdiff":136597951737045.02,"height":911234,"hash":"00000000000000001e8d6829a8a21adc5d38d0a473b144b6765798e61f98bd1d","header":"0100000081cd02ab7e569e8bcd9317e2fe99f2de44d49ab2b8851ba4a308000000000000e320b6c2fffc8d750423db8b1eb942ae710e951ed797f7affc8892b0f1fc122bc7f5d74df2b9441a42a14695","coinbase":"01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff2703e2e60d04858ee468086b616d61646f2f04a1b2c3d40011223344556677ffffffff0100f2052a010000001600140123456789abcdef0123456789abcdef0123456700000000","cb1len":41,"merklebranches":["fff2525b8931402dd09222c50775608f75787bd2b87e56995a7bdd30f79702c4","6359f0868171b1d194cbee1af2f16ea598ae8fad666d9b012c8ed2b79a236ec4"],"enonce1":"a1b2c3d4","nonce2":"0011223344556677","workername":"bc1qexample.axe1"}` + "\n"
ckpoolAcceptLine = `[2026-08-31 18:00:01.124] Accepted client 271 share diff 62953.4/512/1.02K: 00000000000000001e8d6829a8a21adc5d38d0a473b144b6765798e61f98bd1d` + "\n"
)
// End to end: real ckpool log bytes -> real Tailer.Run -> aggregator
// ingest -> snapshot JSON. Exercises the exact production path.
func TestPowEndToEnd_LogToSnapshot(t *testing.T) {
f, err := os.CreateTemp("", "ckpool_e2e*.log")
if err != nil {
t.Fatal(err)
}
f.Close()
t.Cleanup(func() { _ = os.Remove(f.Name()) })
tl := logmon.New(f.Name(), discardLog())
tl.PollWait = 10 * time.Millisecond
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go tl.Run(ctx)
a := New(nil, nil, time.Minute, discardLog())
go a.IngestShareEvents(ctx, tl.Shares)
// Give the tailer a moment to open the (empty) file and seek to the
// end, then append what ckpool would write.
time.Sleep(50 * time.Millisecond)
lf, err := os.OpenFile(f.Name(), os.O_APPEND|os.O_WRONLY, 0)
if err != nil {
t.Fatal(err)
}
if _, err := lf.WriteString(ckpoolPowLine + ckpoolAcceptLine); err != nil {
t.Fatal(err)
}
lf.Close()
deadline := time.Now().Add(5 * time.Second)
for {
a.mu.RLock()
pow := a.bestPow
a.mu.RUnlock()
if pow != nil {
if pow.Hash != "00000000000000001e8d6829a8a21adc5d38d0a473b144b6765798e61f98bd1d" {
t.Fatalf("hash = %s", pow.Hash)
}
if pow.SDiff != 62953.415063916997 || pow.NetDiff != 136597951737045.02 {
t.Fatalf("sdiff/netdiff = %v/%v", pow.SDiff, pow.NetDiff)
}
if len(pow.Header) != 160 || len(pow.MerkleBranches) != 2 {
t.Fatalf("header len %d, branches %d", len(pow.Header), len(pow.MerkleBranches))
}
break
}
if time.Now().After(deadline) {
t.Fatal("bestPow never populated from the tailed log")
}
time.Sleep(10 * time.Millisecond)
}
// The snapshot must expose it under best_share_pow with the flattened
// PowData fields, this is the contract the UI reads.
snap := Snapshot{BestSharePow: func() *BestSharePow {
a.mu.RLock()
defer a.mu.RUnlock()
return a.bestPow
}()}
data, err := json.Marshal(snap)
if err != nil {
t.Fatal(err)
}
var out map[string]any
if err := json.Unmarshal(data, &out); err != nil {
t.Fatal(err)
}
powJSON, ok := out["best_share_pow"].(map[string]any)
if !ok {
t.Fatalf("snapshot JSON missing best_share_pow: %s", data)
}
for _, key := range []string{"sdiff", "netdiff", "height", "hash", "header", "coinbase", "cb1len", "merklebranches", "enonce1", "nonce2", "workername", "seen_at"} {
if _, ok := powJSON[key]; !ok {
t.Errorf("best_share_pow JSON missing %q", key)
}
}
}
// The startup backfill must recover the same record from an existing
// log file (the path taken when live events were missed).
func TestPowEndToEnd_Backfill(t *testing.T) {
path := tempLogFile(t, ckpoolPowLine, ckpoolAcceptLine)
pow, at, _, _, err := logmon.FindBestPowData(path)
if err != nil {
t.Fatalf("scan error: %v", err)
}
if pow == nil {
t.Fatal("FindBestPowData found nothing in verbatim ckpool output")
}
if pow.SDiff != 62953.415063916997 || len(pow.MerkleBranches) != 2 {
t.Fatalf("got %+v", pow)
}
want := time.Date(2026, 8, 31, 18, 0, 1, 0, time.Local)
if !at.Equal(want) {
t.Errorf("timestamp = %v, want %v", at, want)
}
}