Add tests for the critical mining path
Tests cover the four subsystems that sit between a share submission and
a confirmed block reward:
bitcoind/rpc_test.go — isRetryable classification, 3-attempt backoff
on 503, no retry on semantic RPC errors (-5), retry on warm-up (-28),
CoinbaseReward calculation.
logmon/tailer_test.go — handleLine regex parsing, share-diff carry from
attempt line to block event, diff reset after use, unrelated-line
silence, file-read via goroutine, cursor save/resume across restart,
log rotation re-open.
store/blocks_test.go — InsertBlock roundtrip (all fields including
chain), idempotent dedup, MarkOrphaned, UpdateEnrichment,
BlocksNeedingEnrichment, KV get/set/overwrite, and migration of an
existing DB that lacks the chain column.
state/blocks_test.go — reconcileOnce chain-filter (testnet block not
orphaned on mainnet), genuine reorg detection (different canonical
hash → orphaned), transient RPC error (no orphan on -5), legacy
empty-chain block still checked for reorgs, IngestBlockEvents new
block persisted + confirmed counter incremented, dedup skips counter
and OnRefresh, chain stamped from current snapshot at ingest time.
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
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); 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)
|
||||
}
|
||||
}
|
||||
|
||||
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"})
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user