Files
KamadoPool/api/internal/store/blocks_test.go
T
satoshi 1157f3501a 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
2026-05-18 17:19:35 +03:00

274 lines
6.8 KiB
Go

package store
import (
"database/sql"
"os"
"testing"
"time"
_ "modernc.org/sqlite"
)
func openTemp(t *testing.T) *BlockStore {
t.Helper()
f, err := os.CreateTemp("", "kamado*.db")
if err != nil {
t.Fatal(err)
}
f.Close()
t.Cleanup(func() { _ = os.Remove(f.Name()) })
s, err := Open(f.Name())
if err != nil {
t.Fatalf("Open: %v", err)
}
t.Cleanup(func() { _ = s.Close() })
return s
}
func TestInsertBlock_Roundtrip(t *testing.T) {
s := openTemp(t)
now := time.Now().UTC().Truncate(time.Second)
b := Block{
Height: 840000,
Hash: "000000000000000000024bead8df69990852c202db0e0097c1a12ea637d7e96d",
RewardBT: 3.125,
FoundAt: now,
Source: "logmon",
ShareDiff: 8_000_000,
Chain: "main",
}
inserted, err := s.InsertBlock(b)
if err != nil {
t.Fatalf("InsertBlock: %v", err)
}
if !inserted {
t.Fatal("expected inserted=true for a new block")
}
blocks, err := s.Recent(10)
if err != nil {
t.Fatalf("Recent: %v", err)
}
if len(blocks) != 1 {
t.Fatalf("Recent returned %d blocks, want 1", len(blocks))
}
got := blocks[0]
if got.Height != b.Height {
t.Errorf("Height = %d, want %d", got.Height, b.Height)
}
if got.Hash != b.Hash {
t.Errorf("Hash = %q, want %q", got.Hash, b.Hash)
}
if got.RewardBT != b.RewardBT {
t.Errorf("RewardBT = %v, want %v", got.RewardBT, b.RewardBT)
}
if !got.FoundAt.Equal(b.FoundAt) {
t.Errorf("FoundAt = %v, want %v", got.FoundAt, b.FoundAt)
}
if got.Chain != "main" {
t.Errorf("Chain = %q, want \"main\"", got.Chain)
}
if !got.OrphanedAt.IsZero() {
t.Errorf("OrphanedAt should be zero, got %v", got.OrphanedAt)
}
}
func TestInsertBlock_Dedup(t *testing.T) {
s := openTemp(t)
b := Block{Height: 100, FoundAt: time.Now(), Chain: "main"}
inserted1, err := s.InsertBlock(b)
if err != nil || !inserted1 {
t.Fatalf("first insert: err=%v inserted=%v", err, inserted1)
}
inserted2, err := s.InsertBlock(b)
if err != nil {
t.Fatalf("second insert: %v", err)
}
if inserted2 {
t.Error("second insert at same height should return inserted=false")
}
blocks, _ := s.Recent(10)
if len(blocks) != 1 {
t.Errorf("store has %d blocks after dedup, want 1", len(blocks))
}
}
func TestMarkOrphaned_RoundTrip(t *testing.T) {
s := openTemp(t)
b := Block{Height: 200, Hash: "abc123", FoundAt: time.Now(), Chain: "main"}
s.InsertBlock(b)
orphanTime := time.Now().UTC().Truncate(time.Second)
if err := s.MarkOrphaned(200, orphanTime); err != nil {
t.Fatalf("MarkOrphaned: %v", err)
}
blocks, _ := s.Recent(10)
if len(blocks) == 0 {
t.Fatal("no blocks after MarkOrphaned")
}
if blocks[0].OrphanedAt.IsZero() {
t.Error("OrphanedAt should be set after MarkOrphaned")
}
if !blocks[0].OrphanedAt.Equal(orphanTime) {
t.Errorf("OrphanedAt = %v, want %v", blocks[0].OrphanedAt, orphanTime)
}
}
func TestUpdateEnrichment(t *testing.T) {
s := openTemp(t)
s.InsertBlock(Block{Height: 300, FoundAt: time.Now(), Chain: "main"})
if err := s.UpdateEnrichment(300, "newhash", 3.125, "bc1qtest.rig1"); err != nil {
t.Fatalf("UpdateEnrichment: %v", err)
}
blocks, _ := s.Recent(10)
if blocks[0].Hash != "newhash" {
t.Errorf("Hash = %q, want newhash", blocks[0].Hash)
}
if blocks[0].RewardBT != 3.125 {
t.Errorf("RewardBT = %v, want 3.125", blocks[0].RewardBT)
}
if blocks[0].Miner != "bc1qtest.rig1" {
t.Errorf("Miner = %q, want bc1qtest.rig1", blocks[0].Miner)
}
}
func TestBlocksNeedingEnrichment(t *testing.T) {
s := openTemp(t)
now := time.Now()
// Block missing hash → needs enrichment.
s.InsertBlock(Block{Height: 1, FoundAt: now, Chain: "main"})
// Block with hash but no reward → needs enrichment.
s.InsertBlock(Block{Height: 2, Hash: "abc", RewardBT: 0, FoundAt: now, Chain: "main"})
// Block fully enriched → does NOT need enrichment.
s.InsertBlock(Block{Height: 3, Hash: "def", RewardBT: 3.125, FoundAt: now, Chain: "main", Miner: "bc1q.rig1"})
since := now.Add(-time.Hour)
missing, err := s.BlocksNeedingEnrichment(since)
if err != nil {
t.Fatalf("BlocksNeedingEnrichment: %v", err)
}
if len(missing) != 2 {
t.Errorf("got %d blocks needing enrichment, want 2", len(missing))
}
for _, b := range missing {
if b.Height == 3 {
t.Error("fully enriched block should not appear in missing list")
}
}
}
// TestMigrate_AddsChainColumn creates a database with the old schema
// (no chain column) and verifies Open() migrates it transparently.
func TestMigrate_AddsChainColumn(t *testing.T) {
f, err := os.CreateTemp("", "kamado_old*.db")
if err != nil {
t.Fatal(err)
}
f.Close()
t.Cleanup(func() { _ = os.Remove(f.Name()) })
// Build old-style schema without the chain column.
db, err := sql.Open("sqlite", f.Name())
if err != nil {
t.Fatal(err)
}
_, err = db.Exec(`CREATE TABLE blocks (
height INTEGER PRIMARY KEY,
hash TEXT NOT NULL DEFAULT '',
reward_btc REAL NOT NULL DEFAULT 0,
found_at INTEGER NOT NULL,
source TEXT NOT NULL DEFAULT '',
share_diff REAL NOT NULL DEFAULT 0,
orphaned_at INTEGER NOT NULL DEFAULT 0
)`)
if err != nil {
db.Close()
t.Fatalf("create old schema: %v", err)
}
_, err = db.Exec(`INSERT INTO blocks(height, found_at) VALUES (999, ?)`, time.Now().Unix())
if err != nil {
db.Close()
t.Fatalf("insert old row: %v", err)
}
db.Close()
// Open via our store — migrate() must add the chain column.
s, err := Open(f.Name())
if err != nil {
t.Fatalf("Open after migration: %v", err)
}
defer s.Close()
// Insert a block with a chain value — would fail if migration didn't add
// the column.
inserted, err := s.InsertBlock(Block{
Height: 1000,
FoundAt: time.Now(),
Chain: "test",
})
if err != nil {
t.Fatalf("InsertBlock after migration: %v", err)
}
if !inserted {
t.Error("expected inserted=true")
}
blocks, err := s.Recent(10)
if err != nil {
t.Fatalf("Recent: %v", err)
}
// Row 999 (old, no chain) and row 1000 (new, chain="test").
if len(blocks) != 2 {
t.Fatalf("got %d blocks, want 2", len(blocks))
}
// Newest first; row 1000 is first.
if blocks[0].Chain != "test" {
t.Errorf("new block chain = %q, want test", blocks[0].Chain)
}
if blocks[1].Chain != "" {
t.Errorf("old block chain = %q, want \"\" (empty default)", blocks[1].Chain)
}
}
func TestKVRoundTrip(t *testing.T) {
s := openTemp(t)
if err := s.SetKV("foo", "bar"); err != nil {
t.Fatalf("SetKV: %v", err)
}
v, err := s.GetKV("foo")
if err != nil {
t.Fatalf("GetKV: %v", err)
}
if v != "bar" {
t.Errorf("GetKV = %q, want bar", v)
}
// Overwrite.
if err := s.SetKV("foo", "baz"); err != nil {
t.Fatalf("SetKV overwrite: %v", err)
}
v2, _ := s.GetKV("foo")
if v2 != "baz" {
t.Errorf("after overwrite GetKV = %q, want baz", v2)
}
// Missing key returns empty string.
v3, err := s.GetKV("nonexistent")
if err != nil {
t.Fatalf("GetKV missing: %v", err)
}
if v3 != "" {
t.Errorf("missing key GetKV = %q, want empty", v3)
}
}