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  }