Fix cross-network block detection and remove false orphan artifacts

- Change OrphanedAt from time.Time to *time.Time so JSON omitempty
  correctly omits zero values (Go serializes zero time.Time as
  "0001-01-01T00:00:00Z" which JS treats as truthy)
- Rewrite reconcileOnce with 4-pass architecture: enrichment, reorg
  detection via GetBlock verification, false-positive un-orphaning,
  and legacy block stamping
- Use inferOtherChain() to stamp cross-network blocks with the correct
  chain identifier (testnet4 reports as "testnet4", not "test")
- Move tailer startup after aggregator's first refresh so blocks are
  always stamped with the current chain
- Add StampChain store method, debug-blocks admin endpoint, and
  comprehensive reconcile tests for cross-network scenarios
This commit is contained in:
satoshi
2026-05-10 17:22:56 +03:00
parent 92fcb70d93
commit 0787f092ff
5 changed files with 391 additions and 62 deletions
+49
View File
@@ -37,6 +37,7 @@ func (s *Server) Handler() http.Handler {
mux.HandleFunc("GET /api/blocks", s.blocks)
mux.HandleFunc("GET /api/snapshot", s.snapshot)
mux.HandleFunc("GET /api/ws", s.handleWS)
mux.HandleFunc("GET /api/admin/debug-blocks", s.debugBlocks)
mux.Handle("/", spaHandler(webui.FS()))
return mux
}
@@ -159,3 +160,51 @@ func (s *Server) clients(w http.ResponseWriter, r *http.Request) {
func (s *Server) blocks(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, s.Agg.Blocks())
}
// debugBlocks exposes raw block state from both the in-memory snapshot
// (what the UI sees) and the DB (ground truth) for troubleshooting.
func (s *Server) debugBlocks(w http.ResponseWriter, r *http.Request) {
type row struct {
Height int64 `json:"height"`
Hash string `json:"hash"`
Chain string `json:"chain"`
OrphanedAt string `json:"orphaned_at"`
FoundAt string `json:"found_at"`
RewardBTC float64 `json:"reward_btc"`
Source string `json:"source"`
}
fmtBlock := func(b state.BlockRecord) row {
orphaned := ""
if b.OrphanedAt != nil {
orphaned = b.OrphanedAt.Format("2006-01-02T15:04:05Z")
}
return row{
Height: b.Height,
Hash: b.Hash,
Chain: b.Chain,
OrphanedAt: orphaned,
FoundAt: b.FoundAt.Format("2006-01-02T15:04:05Z"),
RewardBTC: b.RewardBT,
Source: b.Source,
}
}
// In-memory blocks (what /api/blocks and /api/snapshot serve)
memBlocks := s.Agg.Blocks()
mem := make([]row, 0, len(memBlocks))
for _, b := range memBlocks {
mem = append(mem, fmtBlock(b))
}
// DB blocks (ground truth)
dbBlocks := s.Agg.BlocksFromStore()
db := make([]row, 0, len(dbBlocks))
for _, b := range dbBlocks {
db = append(db, fmtBlock(b))
}
writeJSON(w, http.StatusOK, map[string]any{
"memory": mem,
"db": db,
})
}