package main import ( "flag" "git.gammaspectra.live/P2Pool/p2pool-observer/database" "git.gammaspectra.live/P2Pool/p2pool-observer/monero/client" "git.gammaspectra.live/P2Pool/p2pool-observer/monero/randomx" p2poolapi "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/api" "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain" "git.gammaspectra.live/P2Pool/p2pool-observer/types" "git.gammaspectra.live/P2Pool/p2pool-observer/utils" "log" "os" "time" ) func blockId(b *sidechain.PoolBlock) types.Hash { return types.HashFromBytes(b.CoinbaseExtra(sidechain.SideTemplateId)) } func main() { startFromHeight := flag.Uint64("from", 0, "Start sync from this height") dbString := flag.String("db", "", "") p2poolApiHost := flag.String("api-host", "", "Host URL for p2pool go observer consensus") flag.Parse() client.SetDefaultClientSettings(os.Getenv("MONEROD_RPC_URL")) db, err := database.NewDatabase(*dbString) if err != nil { log.Panic(err) } defer db.Close() p2api := p2poolapi.NewP2PoolApi(*p2poolApiHost) api, err := p2poolapi.New(db, p2api) if err != nil { log.Panic(err) } //TODO: force-insert section tip := db.GetChainTip() isFresh := tip == nil var tipHeight uint64 if tip != nil { tipHeight = tip.Height } log.Printf("[CHAIN] Last known database tip is %d\n", tipHeight) for status := p2api.Status(); !p2api.Status().Synchronized; status = p2api.Status() { log.Printf("[API] Not synchronized (height %d, id %s), waiting five seconds", status.Height, status.Id) time.Sleep(time.Second * 5) } log.Printf("[CHAIN] Consensus id = %s\n", p2api.Consensus().Id()) getSeedByHeight := func(height uint64) (hash types.Hash) { seedHeight := randomx.SeedHeight(height) if h := p2api.MainHeaderByHeight(seedHeight); h == nil { return types.ZeroHash } else { return h.Id } } tipChain, tipUncles := p2api.StateFromTip() if len(tipChain) == 0 || len(tipChain) < int(p2api.Consensus().ChainWindowSize*2) { log.Panicf("[CHAIN] Too small length %d", len(tipChain)) } p2poolTip := tipChain[0].Side.Height log.Printf("[CHAIN] Last known p2pool tip is %d\n", p2poolTip) startFrom := utils.Max(tipHeight, tipChain[len(tipChain)-1].Side.Height) //TODO: archive if *startFromHeight != 0 { log.Printf("[CHAIN] Forcing start tip to %d\n", *startFromHeight) startFrom = *startFromHeight } if isFresh || startFrom != p2poolTip { if startFrom > p2poolTip { startFrom = p2poolTip } if isFresh { currentIndex := int(p2poolTip - startFrom) if currentIndex > (len(tipChain) - 1) { currentIndex = len(tipChain) - 1 } for ; currentIndex >= 0; currentIndex-- { b := tipChain[currentIndex] if block, uncles, err := database.NewBlockFromBinaryBlock(getSeedByHeight, p2api.MainDifficultyByHeight, db, b, tipUncles, true); err != nil { log.Panicf("[CHAIN] Could not find share %s to insert at height %d. Check disk or uncles: %s\n", blockId(b).String(), b.Side.Height, err) } else { log.Printf("[CHAIN] Inserting share %s at height %d\n", blockId(b).String(), b.Side.Height) if err = db.InsertBlock(block, nil); err != nil { log.Panic(err) } for _, uncle := range uncles { log.Printf("[CHAIN] Inserting uncle share %s at height %d\n", uncle.Block.Id.String(), b.Side.Height) if err = db.InsertUncleBlock(uncle, nil); err != nil { log.Panic(err) } } } startFrom = b.Side.Height } } } //TODO: handle jumps in blocks (missing data) knownTip := startFrom log.Printf("[CHAIN] Starting tip from height %d\n", knownTip) runs := 0 //Fix blocks without height for b := range db.GetBlocksByQuery("WHERE miner_main_difficulty = 'ffffffffffffffffffffffffffffffff' ORDER BY main_height ASC;") { cacheHeightDifficulty(b.Main.Height) if diff, ok := getHeightDifficulty(b.Main.Height); ok { log.Printf("[CHAIN] Filling main difficulty for share %d, main height %d\n", b.Height, b.Main.Height) _ = db.SetBlockMainDifficulty(b.Id, diff) b = db.GetBlockById(b.Id) if !b.Main.Found && b.IsProofHigherThanDifficulty() { log.Printf("[CHAIN] BLOCK FOUND! Main height %d, main id %s\n", b.Main.Height, b.Main.Id.String()) if tx, _ := client.GetDefaultClient().GetCoinbaseTransaction(b.Coinbase.Id); tx != nil { _ = db.SetBlockFound(b.Id, true) processFoundBlockWithTransaction(api, b, tx) } } } } //Fix uncle without height for u := range db.GetUncleBlocksByQuery("WHERE miner_main_difficulty = 'ffffffffffffffffffffffffffffffff' ORDER BY main_height ASC;") { cacheHeightDifficulty(u.Block.Main.Height) if diff, ok := getHeightDifficulty(u.Block.Main.Height); ok { log.Printf("[CHAIN] Filling main difficulty for uncle share %d, main height %d\n", u.Block.Height, u.Block.Main.Height) _ = db.SetBlockMainDifficulty(u.Block.Id, diff) u = db.GetUncleById(u.Block.Id) if !u.Block.Main.Found && u.Block.IsProofHigherThanDifficulty() { log.Printf("[CHAIN] BLOCK FOUND! Main height %d, main id %s\n", u.Block.Main.Height, u.Block.Main.Id.String()) if tx, _ := client.GetDefaultClient().GetCoinbaseTransaction(u.Block.Coinbase.Id); tx != nil { _ = db.SetBlockFound(u.Block.Id, true) processFoundBlockWithTransaction(api, u, tx) } } } } var chain sidechain.UniquePoolBlockSlice for { toStart: if len(chain) == 0 { time.Sleep(time.Second * 1) } runs++ var p2tip *sidechain.PoolBlock if len(chain) > 0 { p2tip = chain[len(chain)-1] chain = chain[:len(chain)-1] } else { p2tip = p2api.Tip() } if p2tip == nil { log.Panicf("[CHAIN] could not find tip, at %d", knownTip) } if p2tip.Side.Height < knownTip { log.Panicf("[CHAIN] tip went backwards! %d -> %d", knownTip, p2tip.Side.Height) } else if p2tip.Side.Height > (knownTip + 1) { log.Printf("[CHAIN] tip went forwards! Rolling back %d -> %d", knownTip, p2tip.Side.Height) for i := p2tip.Side.Height - (knownTip + 1); i > 0 && p2tip != nil; i-- { chain = append(chain, p2tip) p2tip = p2api.ByTemplateId(p2tip.Side.Parent) } } if p2tip == nil { log.Panicf("[CHAIN] could not find tip after depth, at %d", knownTip) } dbTip := db.GetBlockByHeight(knownTip) if dbTip != nil && dbTip.Id == blockId(p2tip) { // no changes continue } if dbTip != nil && dbTip.Id != p2tip.Side.Parent { //Reorg has happened, delete old values log.Printf("[REORG] Reorg happened, deleting blocks to match from height %d.\n", dbTip.Height) diskBlock := p2tip for h := knownTip; h > 0; h-- { dbBlock := db.GetBlockByHeight(h) diskBlock = p2api.ByTemplateId(diskBlock.Side.Parent) if dbBlock.Id == blockId(diskBlock) { log.Printf("[REORG] Found matching head %s at height %d\n", dbBlock.PreviousId.String(), dbBlock.Height-1) deleted, err := db.DeleteBlockById(dbBlock.Id) if err != nil { log.Panic(err) } log.Printf("[REORG] Deleted %d shares(s).\n", deleted) log.Printf("[REORG] Next tip %s : %d.\n", blockId(diskBlock), diskBlock.Side.Height) knownTip = dbBlock.Height - 1 break } } continue } tipUncles = tipUncles[:0] for _, uncleId := range p2tip.Side.Uncles { if u := p2api.ByTemplateId(uncleId); u == nil { goto toStart } else { tipUncles = append(tipUncles, u) } } diskBlock, uncles, err := database.NewBlockFromBinaryBlock(getSeedByHeight, p2api.MainDifficultyByHeight, db, p2tip, tipUncles, true) if err != nil { log.Printf("[CHAIN] Could not find share %s to insert at height %d. Check disk or uncles\n", blockId(p2tip), p2tip.Side.Height) continue } prevBlock := db.GetBlockByHeight(p2tip.Side.Height - 1) if prevBlock != nil && diskBlock.PreviousId != prevBlock.Id { log.Printf("[CHAIN] Possible reorg occurred, aborting insertion at height %d: prev id %s != id %s\n", p2tip.Side.Height, diskBlock.PreviousId.String(), prevBlock.Id.String()) continue } log.Printf("[CHAIN] Inserting share %s at height %d\n", diskBlock.Id.String(), diskBlock.Height) cacheHeightDifficulty(diskBlock.Main.Height) diff, ok := getHeightDifficulty(diskBlock.Main.Height) if ok { err = db.InsertBlock(diskBlock, &diff) } else { err = db.InsertBlock(diskBlock, nil) } if err == nil { for _, uncle := range uncles { log.Printf("[CHAIN] Inserting uncle %s @ %s at %d", uncle.Block.Main.Id.String(), diskBlock.Id.String(), diskBlock.Height) diff, ok := getHeightDifficulty(uncle.Block.Main.Height) if ok { err = db.InsertUncleBlock(uncle, &diff) } else { err = db.InsertUncleBlock(uncle, nil) } if uncle.Block.Main.Found { log.Printf("[CHAIN] BLOCK FOUND! (uncle) Main height %d, main id %s", uncle.Block.Main.Height, uncle.Block.Main.Id.String()) if b := tipUncles.Get(uncle.Block.Id); b != nil { processFoundBlockWithTransaction(api, uncle, b.Main.Coinbase) } } } knownTip = diskBlock.Height } if diskBlock.Main.Found { log.Printf("[CHAIN] BLOCK FOUND! Main height %d, main id %s", diskBlock.Main.Height, diskBlock.Main.Id.String()) if b := p2api.ByTemplateId(diskBlock.Id); b != nil { processFoundBlockWithTransaction(api, diskBlock, b.Main.Coinbase) } } if runs%10 == 0 { //Every 10 seconds or so for foundBlock := range db.GetAllFound(10, 0) { //Scan last 10 found blocks and set status accordingly if found/not found // Look between +1 block and +4 blocks if (p2tip.Main.Coinbase.GenHeight-1) > foundBlock.GetBlock().Main.Height && (p2tip.Main.Coinbase.GenHeight-5) < foundBlock.GetBlock().Main.Height || db.GetCoinbaseTransaction(foundBlock.GetBlock()) == nil { if tx, _ := client.GetDefaultClient().GetCoinbaseTransaction(foundBlock.GetBlock().Coinbase.Id); tx == nil { // If more than two minutes have passed before we get utxo, remove from found log.Printf("[CHAIN] Block that was found at main height %d, cannot find output, marking not found\n", foundBlock.GetBlock().Main.Height) _ = db.SetBlockFound(foundBlock.GetBlock().Id, false) } else { processFoundBlockWithTransaction(api, foundBlock, tx) } } } } if isFresh { //TODO: Do migration tasks isFresh = false } } }