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  }