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