git.gammaspectra.live/P2Pool/consensus@v0.0.0-20240403173234-a039820b20c9/p2pool/mainchain/mainchain.go (about) 1 package mainchain 2 3 import ( 4 "context" 5 "encoding/hex" 6 "fmt" 7 mainblock "git.gammaspectra.live/P2Pool/consensus/monero/block" 8 "git.gammaspectra.live/P2Pool/consensus/monero/client" 9 "git.gammaspectra.live/P2Pool/consensus/monero/client/zmq" 10 "git.gammaspectra.live/P2Pool/consensus/monero/randomx" 11 "git.gammaspectra.live/P2Pool/consensus/monero/transaction" 12 "git.gammaspectra.live/P2Pool/consensus/p2pool/mempool" 13 "git.gammaspectra.live/P2Pool/consensus/p2pool/sidechain" 14 p2pooltypes "git.gammaspectra.live/P2Pool/consensus/p2pool/types" 15 "git.gammaspectra.live/P2Pool/consensus/types" 16 "git.gammaspectra.live/P2Pool/consensus/utils" 17 "github.com/dolthub/swiss" 18 "slices" 19 "sync" 20 "sync/atomic" 21 "time" 22 ) 23 24 const TimestampWindow = 60 25 const BlockHeadersRequired = 720 26 27 type MainChain struct { 28 p2pool P2PoolInterface 29 lock sync.RWMutex 30 sidechain *sidechain.SideChain 31 32 highest uint64 33 mainchainByHeight *swiss.Map[uint64, *sidechain.ChainMain] 34 mainchainByHash *swiss.Map[types.Hash, *sidechain.ChainMain] 35 36 tip atomic.Pointer[sidechain.ChainMain] 37 tipMinerData atomic.Pointer[p2pooltypes.MinerData] 38 39 medianTimestamp atomic.Uint64 40 } 41 42 type P2PoolInterface interface { 43 ClientRPC() *client.Client 44 ClientZMQ() *zmq.Client 45 Context() context.Context 46 Started() bool 47 UpdateMainData(data *sidechain.ChainMain) 48 UpdateMinerData(data *p2pooltypes.MinerData) 49 UpdateMempoolData(data mempool.Mempool) 50 UpdateBlockFound(data *sidechain.ChainMain, block *sidechain.PoolBlock) 51 } 52 53 func NewMainChain(s *sidechain.SideChain, p2pool P2PoolInterface) *MainChain { 54 m := &MainChain{ 55 sidechain: s, 56 p2pool: p2pool, 57 mainchainByHeight: swiss.NewMap[uint64, *sidechain.ChainMain](BlockHeadersRequired + 3), 58 mainchainByHash: swiss.NewMap[types.Hash, *sidechain.ChainMain](BlockHeadersRequired + 3), 59 } 60 61 return m 62 } 63 64 func (c *MainChain) Listen() error { 65 ctx := c.p2pool.Context() 66 err := c.p2pool.ClientZMQ().Listen(ctx, 67 func(fullChainMain *zmq.FullChainMain) { 68 if len(fullChainMain.MinerTx.Inputs) < 1 { 69 return 70 } 71 d := &sidechain.ChainMain{ 72 Difficulty: types.ZeroDifficulty, 73 Height: fullChainMain.MinerTx.Inputs[0].Gen.Height, 74 Timestamp: uint64(fullChainMain.Timestamp), 75 Reward: 0, 76 Id: types.ZeroHash, 77 } 78 for _, o := range fullChainMain.MinerTx.Outputs { 79 d.Reward += o.Amount 80 } 81 82 outputs := make(transaction.Outputs, 0, len(fullChainMain.MinerTx.Outputs)) 83 var totalReward uint64 84 for i, o := range fullChainMain.MinerTx.Outputs { 85 if o.ToKey != nil { 86 outputs = append(outputs, transaction.Output{ 87 Index: uint64(i), 88 Reward: o.Amount, 89 Type: transaction.TxOutToKey, 90 EphemeralPublicKey: o.ToKey.Key, 91 ViewTag: 0, 92 }) 93 } else if o.ToTaggedKey != nil { 94 tk, _ := hex.DecodeString(o.ToTaggedKey.ViewTag) 95 outputs = append(outputs, transaction.Output{ 96 Index: uint64(i), 97 Reward: o.Amount, 98 Type: transaction.TxOutToTaggedKey, 99 EphemeralPublicKey: o.ToTaggedKey.Key, 100 ViewTag: tk[0], 101 }) 102 } else { 103 //error 104 break 105 } 106 totalReward += o.Amount 107 } 108 109 if len(outputs) != len(fullChainMain.MinerTx.Outputs) { 110 return 111 } 112 113 extraDataRaw, _ := hex.DecodeString(fullChainMain.MinerTx.Extra) 114 extraTags := transaction.ExtraTags{} 115 if err := extraTags.UnmarshalBinary(extraDataRaw); err != nil { 116 //TODO: err 117 extraTags = nil 118 } 119 120 blockData := &mainblock.Block{ 121 MajorVersion: uint8(fullChainMain.MajorVersion), 122 MinorVersion: uint8(fullChainMain.MinorVersion), 123 Timestamp: uint64(fullChainMain.Timestamp), 124 PreviousId: fullChainMain.PrevID, 125 Nonce: uint32(fullChainMain.Nonce), 126 Coinbase: transaction.CoinbaseTransaction{ 127 Version: uint8(fullChainMain.MinerTx.Version), 128 UnlockTime: uint64(fullChainMain.MinerTx.UnlockTime), 129 InputCount: uint8(len(fullChainMain.MinerTx.Inputs)), 130 InputType: transaction.TxInGen, 131 GenHeight: fullChainMain.MinerTx.Inputs[0].Gen.Height, 132 Outputs: outputs, 133 OutputsBlobSize: 0, 134 TotalReward: totalReward, 135 Extra: extraTags, 136 ExtraBaseRCT: 0, 137 }, 138 Transactions: fullChainMain.TxHashes, 139 TransactionParentIndices: nil, 140 } 141 c.HandleMainBlock(blockData) 142 }, func(txs []zmq.FullTxPoolAdd) { 143 144 }, func(fullMinerData *zmq.FullMinerData) { 145 pool := make(mempool.Mempool, len(fullMinerData.TxBacklog)) 146 for i := range fullMinerData.TxBacklog { 147 pool[i] = &mempool.MempoolEntry{ 148 Id: fullMinerData.TxBacklog[i].Id, 149 BlobSize: fullMinerData.TxBacklog[i].BlobSize, 150 Weight: fullMinerData.TxBacklog[i].Weight, 151 Fee: fullMinerData.TxBacklog[i].Fee, 152 } 153 } 154 c.HandleMinerData(&p2pooltypes.MinerData{ 155 MajorVersion: fullMinerData.MajorVersion, 156 Height: fullMinerData.Height, 157 PrevId: fullMinerData.PrevId, 158 SeedHash: fullMinerData.SeedHash, 159 Difficulty: fullMinerData.Difficulty, 160 MedianWeight: fullMinerData.MedianWeight, 161 AlreadyGeneratedCoins: fullMinerData.AlreadyGeneratedCoins, 162 MedianTimestamp: fullMinerData.MedianTimestamp, 163 TxBacklog: pool, 164 TimeReceived: time.Now(), 165 }) 166 }, func(chainMain *zmq.MinimalChainMain) { 167 168 }, func(txs []zmq.TxMempoolData) { 169 m := make(mempool.Mempool, len(txs)) 170 for i := range txs { 171 m[i] = &mempool.MempoolEntry{ 172 Id: txs[i].Id, 173 BlobSize: txs[i].BlobSize, 174 Weight: txs[i].Weight, 175 Fee: txs[i].Fee, 176 } 177 } 178 c.p2pool.UpdateMempoolData(m) 179 }) 180 if err != nil { 181 return err 182 } 183 return nil 184 } 185 186 func (c *MainChain) getTimestamps(timestamps []uint64) bool { 187 _ = timestamps[TimestampWindow-1] 188 if c.mainchainByHeight.Count() <= TimestampWindow { 189 return false 190 } 191 192 for i := 0; i < TimestampWindow; i++ { 193 h, ok := c.mainchainByHeight.Get(c.highest - uint64(i)) 194 if !ok { 195 break 196 } 197 timestamps[i] = h.Timestamp 198 } 199 return true 200 } 201 202 func (c *MainChain) updateMedianTimestamp() { 203 var timestamps [TimestampWindow]uint64 204 if !c.getTimestamps(timestamps[:]) { 205 c.medianTimestamp.Store(0) 206 return 207 } 208 209 slices.Sort(timestamps[:]) 210 211 // Shift it +1 block compared to Monero's code because we don't have the latest block yet when we receive new miner data 212 ts := (timestamps[TimestampWindow/2] + timestamps[TimestampWindow/2+1]) / 2 213 utils.Logf("MainChain", "Median timestamp updated to %d", ts) 214 c.medianTimestamp.Store(ts) 215 } 216 217 func (c *MainChain) HandleMainHeader(mainHeader *mainblock.Header) { 218 c.lock.Lock() 219 defer c.lock.Unlock() 220 221 mainData := &sidechain.ChainMain{ 222 Difficulty: mainHeader.Difficulty, 223 Height: mainHeader.Height, 224 Timestamp: mainHeader.Timestamp, 225 Reward: mainHeader.Reward, 226 Id: mainHeader.Id, 227 } 228 c.mainchainByHeight.Put(mainHeader.Height, mainData) 229 c.mainchainByHash.Put(mainHeader.Id, mainData) 230 231 if mainData.Height > c.highest { 232 c.highest = mainData.Height 233 } 234 235 utils.Logf("MainChain", "new main chain block: height = %d, id = %s, timestamp = %d, reward = %s", mainData.Height, mainData.Id.String(), mainData.Timestamp, utils.XMRUnits(mainData.Reward)) 236 237 c.updateMedianTimestamp() 238 } 239 240 func (c *MainChain) HandleMainBlock(b *mainblock.Block) { 241 mainData := &sidechain.ChainMain{ 242 Difficulty: types.ZeroDifficulty, 243 Height: b.Coinbase.GenHeight, 244 Timestamp: b.Timestamp, 245 Reward: b.Coinbase.TotalReward, 246 Id: b.Id(), 247 } 248 249 func() { 250 c.lock.Lock() 251 defer c.lock.Unlock() 252 253 if h, ok := c.mainchainByHeight.Get(mainData.Height); ok { 254 mainData.Difficulty = h.Difficulty 255 } else { 256 return 257 } 258 c.mainchainByHash.Put(mainData.Id, mainData) 259 c.mainchainByHeight.Put(mainData.Height, mainData) 260 261 if mainData.Height > c.highest { 262 c.highest = mainData.Height 263 } 264 265 utils.Logf("MainChain", "new main chain block: height = %d, id = %s, timestamp = %d, reward = %s", mainData.Height, mainData.Id.String(), mainData.Timestamp, utils.XMRUnits(mainData.Reward)) 266 267 c.updateMedianTimestamp() 268 }() 269 270 extraMergeMiningTag := b.Coinbase.Extra.GetTag(transaction.TxExtraTagMergeMining) 271 if extraMergeMiningTag == nil { 272 return 273 } 274 sidechainHashData := extraMergeMiningTag.Data 275 if len(sidechainHashData) != types.HashSize { 276 return 277 } 278 279 sidechainId := types.HashFromBytes(sidechainHashData) 280 281 if block := c.sidechain.GetPoolBlockByTemplateId(sidechainId); block != nil { 282 c.p2pool.UpdateBlockFound(mainData, block) 283 } else { 284 c.sidechain.WatchMainChainBlock(mainData, sidechainId) 285 } 286 287 c.updateTip() 288 } 289 290 func (c *MainChain) GetChainMainByHeight(height uint64) *sidechain.ChainMain { 291 c.lock.RLock() 292 defer c.lock.RUnlock() 293 m, _ := c.mainchainByHeight.Get(height) 294 return m 295 } 296 297 func (c *MainChain) GetChainMainByHash(hash types.Hash) *sidechain.ChainMain { 298 c.lock.RLock() 299 defer c.lock.RUnlock() 300 b, _ := c.mainchainByHash.Get(hash) 301 return b 302 } 303 304 func (c *MainChain) GetChainMainTip() *sidechain.ChainMain { 305 return c.tip.Load() 306 } 307 308 func (c *MainChain) GetMinerDataTip() *p2pooltypes.MinerData { 309 return c.tipMinerData.Load() 310 } 311 312 func (c *MainChain) updateTip() { 313 if minerData := c.tipMinerData.Load(); minerData != nil { 314 if d := c.GetChainMainByHash(minerData.PrevId); d != nil { 315 c.tip.Store(d) 316 } 317 } 318 } 319 320 func (c *MainChain) Cleanup() { 321 if tip := c.GetChainMainTip(); tip != nil { 322 c.lock.Lock() 323 defer c.lock.Unlock() 324 c.cleanup(tip.Height) 325 } 326 } 327 328 func (c *MainChain) cleanup(height uint64) { 329 // Expects m_mainchainLock to be already locked here 330 // Deletes everything older than 720 blocks, except for the 3 latest RandomX seed heights 331 332 const PruneDistance = BlockHeadersRequired 333 334 seedHeight := randomx.SeedHeight(height) 335 336 seedHeights := []uint64{seedHeight, seedHeight - randomx.SeedHashEpochBlocks, seedHeight - randomx.SeedHashEpochBlocks*2} 337 338 c.mainchainByHeight.Iter(func(h uint64, m *sidechain.ChainMain) (stop bool) { 339 if (h + PruneDistance) >= height { 340 return false 341 } 342 343 if !slices.Contains(seedHeights, h) { 344 c.mainchainByHash.Delete(m.Id) 345 c.mainchainByHeight.Delete(h) 346 } 347 return false 348 }) 349 350 } 351 352 func (c *MainChain) DownloadBlockHeaders(currentHeight uint64) error { 353 seedHeight := randomx.SeedHeight(currentHeight) 354 355 var prevSeedHeight uint64 356 357 if seedHeight > randomx.SeedHashEpochBlocks { 358 prevSeedHeight = seedHeight - randomx.SeedHashEpochBlocks 359 } 360 361 // First download 2 RandomX seeds 362 363 for _, h := range []uint64{prevSeedHeight, seedHeight} { 364 if err := c.getBlockHeader(h); err != nil { 365 return err 366 } 367 } 368 369 var startHeight uint64 370 if currentHeight > BlockHeadersRequired { 371 startHeight = currentHeight - BlockHeadersRequired 372 } 373 374 if rangeResult, err := c.p2pool.ClientRPC().GetBlockHeadersRangeResult(startHeight, currentHeight-1, c.p2pool.Context()); err != nil { 375 return fmt.Errorf("couldn't download block headers range for height %d to %d: %s", startHeight, currentHeight-1, err) 376 } else { 377 for _, header := range rangeResult.Headers { 378 prevHash, _ := types.HashFromString(header.PrevHash) 379 h, _ := types.HashFromString(header.Hash) 380 c.HandleMainHeader(&mainblock.Header{ 381 MajorVersion: uint8(header.MajorVersion), 382 MinorVersion: uint8(header.MinorVersion), 383 Timestamp: uint64(header.Timestamp), 384 PreviousId: prevHash, 385 Height: header.Height, 386 Nonce: uint32(header.Nonce), 387 Reward: header.Reward, 388 Id: h, 389 Difficulty: types.DifficultyFrom64(header.Difficulty), 390 }) 391 } 392 utils.Logf("MainChain", "Downloaded headers for range %d to %d", startHeight, currentHeight-1) 393 } 394 395 c.updateMedianTimestamp() 396 397 return nil 398 } 399 400 func (c *MainChain) HandleMinerData(minerData *p2pooltypes.MinerData) { 401 var missingHeights []uint64 402 func() { 403 c.lock.Lock() 404 defer c.lock.Unlock() 405 406 mainData := &sidechain.ChainMain{ 407 Difficulty: minerData.Difficulty, 408 Height: minerData.Height, 409 } 410 411 if existingMainData, ok := c.mainchainByHeight.Get(mainData.Height); !ok { 412 c.mainchainByHeight.Put(mainData.Height, mainData) 413 } else { 414 existingMainData.Difficulty = mainData.Difficulty 415 mainData = existingMainData 416 } 417 418 prevMainData := &sidechain.ChainMain{ 419 Height: minerData.Height - 1, 420 Id: minerData.PrevId, 421 } 422 423 if existingPrevMainData, ok := c.mainchainByHeight.Get(prevMainData.Height); !ok { 424 c.mainchainByHeight.Put(prevMainData.Height, prevMainData) 425 } else { 426 existingPrevMainData.Id = prevMainData.Id 427 428 prevMainData = existingPrevMainData 429 } 430 431 c.mainchainByHash.Put(prevMainData.Id, prevMainData) 432 433 c.cleanup(minerData.Height) 434 435 minerData.TimeReceived = time.Now() 436 c.tipMinerData.Store(minerData) 437 438 c.updateMedianTimestamp() 439 440 utils.Logf("MainChain", "new miner data: major_version = %d, height = %d, prev_id = %s, seed_hash = %s, difficulty = %s", minerData.MajorVersion, minerData.Height, minerData.PrevId.String(), minerData.SeedHash.String(), minerData.Difficulty.StringNumeric()) 441 442 // Tx secret keys from all miners change every block, so cache can be cleared here 443 if c.sidechain.PreCalcFinished() { 444 c.sidechain.DerivationCache().Clear() 445 } 446 447 if c.p2pool.Started() { 448 for h := minerData.Height; h > 0 && (h+BlockHeadersRequired) > minerData.Height; h-- { 449 if d, ok := c.mainchainByHeight.Get(h); !ok || d.Difficulty.Equals(types.ZeroDifficulty) { 450 utils.Logf("MainChain", "Main chain data for height = %d is missing, requesting from monerod again", h) 451 missingHeights = append(missingHeights, h) 452 } 453 } 454 } 455 }() 456 457 c.p2pool.UpdateMinerData(minerData) 458 459 var wg sync.WaitGroup 460 for _, h := range missingHeights { 461 wg.Add(1) 462 go func(height uint64) { 463 wg.Done() 464 if err := c.getBlockHeader(height); err != nil { 465 utils.Errorf("MainChain", "%s", err) 466 } 467 }(h) 468 } 469 wg.Wait() 470 471 c.updateTip() 472 473 } 474 475 func (c *MainChain) getBlockHeader(height uint64) error { 476 if header, err := c.p2pool.ClientRPC().GetBlockHeaderByHeight(height, c.p2pool.Context()); err != nil { 477 return fmt.Errorf("couldn't download block header for height %d: %s", height, err) 478 } else { 479 prevHash, _ := types.HashFromString(header.BlockHeader.PrevHash) 480 h, _ := types.HashFromString(header.BlockHeader.Hash) 481 c.HandleMainHeader(&mainblock.Header{ 482 MajorVersion: uint8(header.BlockHeader.MajorVersion), 483 MinorVersion: uint8(header.BlockHeader.MinorVersion), 484 Timestamp: uint64(header.BlockHeader.Timestamp), 485 PreviousId: prevHash, 486 Height: header.BlockHeader.Height, 487 Nonce: uint32(header.BlockHeader.Nonce), 488 Reward: header.BlockHeader.Reward, 489 Id: h, 490 Difficulty: types.DifficultyFrom64(header.BlockHeader.Difficulty), 491 }) 492 } 493 494 return nil 495 }