consensus/p2pool/sidechain/sidechain.go

962 lines
30 KiB
Go
Raw Normal View History

2022-11-03 11:32:07 +00:00
package sidechain
import (
"bytes"
"errors"
"fmt"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero"
mainblock "git.gammaspectra.live/P2Pool/p2pool-observer/monero/block"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/transaction"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"golang.org/x/exp/slices"
2022-11-06 10:58:19 +00:00
"log"
2022-11-03 11:32:07 +00:00
"math"
"sync"
"sync/atomic"
)
type Cache interface {
GetBlob(key []byte) (blob []byte, err error)
SetBlob(key, blob []byte) (err error)
RemoveBlob(key []byte) (err error)
}
type P2PoolInterface interface {
2022-11-03 11:32:07 +00:00
ConsensusProvider
Cache
2022-11-03 11:32:07 +00:00
Broadcast(block *PoolBlock)
}
type SideChain struct {
cache *DerivationCache
server P2PoolInterface
2022-11-03 11:32:07 +00:00
2022-11-06 12:17:26 +00:00
sidechainLock sync.RWMutex
sharesCache Shares
2022-11-03 11:32:07 +00:00
blocksByTemplateId map[types.Hash]*PoolBlock
blocksByHeight map[uint64][]*PoolBlock
chainTip atomic.Pointer[PoolBlock]
currentDifficulty atomic.Pointer[types.Difficulty]
}
func NewSideChain(server P2PoolInterface) *SideChain {
2022-11-03 11:32:07 +00:00
return &SideChain{
cache: NewDerivationCache(),
server: server,
blocksByTemplateId: make(map[types.Hash]*PoolBlock),
blocksByHeight: make(map[uint64][]*PoolBlock),
2022-11-06 12:17:26 +00:00
sharesCache: make(Shares, 0, server.Consensus().ChainWindowSize*2),
2022-11-03 11:32:07 +00:00
}
}
func (c *SideChain) Consensus() *Consensus {
return c.server.Consensus()
}
func (c *SideChain) PreprocessBlock(block *PoolBlock) (err error) {
2022-11-06 12:17:26 +00:00
c.sidechainLock.RLock()
defer c.sidechainLock.RUnlock()
2022-11-03 11:32:07 +00:00
if len(block.Main.Coinbase.Outputs) == 0 {
if outputs := c.getOutputs(block); outputs == nil {
return errors.New("nil transaction outputs")
} else {
block.Main.Coinbase.Outputs = outputs
}
if outputBlob, err := block.Main.Coinbase.OutputsBlob(); err != nil {
return err
} else if uint64(len(outputBlob)) != block.Main.Coinbase.OutputsBlobSize {
return fmt.Errorf("invalid output blob size, got %d, expected %d", block.Main.Coinbase.OutputsBlobSize, len(outputBlob))
2022-11-03 11:32:07 +00:00
}
}
return nil
}
func (c *SideChain) isPoolBlockTransactionKeyIsDeterministic(block *PoolBlock) bool {
kP := c.cache.GetDeterministicTransactionKey(block.GetAddress(), block.Main.PreviousId)
return bytes.Compare(block.CoinbaseExtra(SideCoinbasePublicKey), kP.PublicKey.AsSlice()) == 0 && bytes.Compare(block.Side.CoinbasePrivateKey[:], kP.PrivateKey.AsSlice()) == 0
}
2022-11-06 10:58:19 +00:00
func (c *SideChain) AddPoolBlockExternal(block *PoolBlock) (missingBlocks []types.Hash, err error) {
2022-11-03 11:32:07 +00:00
// Technically some p2pool node could keep stuffing block with transactions until reward is less than 0.6 XMR
// But default transaction picking algorithm never does that. It's better to just ban such nodes
if block.Main.Coinbase.TotalReward < monero.TailEmissionReward {
2022-11-06 10:58:19 +00:00
return nil, errors.New("block reward too low")
2022-11-03 11:32:07 +00:00
}
// Enforce deterministic tx keys starting from v15
if block.Main.MajorVersion >= monero.HardForkViewTagsVersion {
if !c.isPoolBlockTransactionKeyIsDeterministic(block) {
2022-11-06 10:58:19 +00:00
return nil, errors.New("invalid deterministic transaction keys")
2022-11-03 11:32:07 +00:00
}
}
// Both tx types are allowed by Monero consensus during v15 because it needs to process pre-fork mempool transactions,
// but P2Pool can switch to using only TXOUT_TO_TAGGED_KEY for miner payouts starting from v15
expectedTxType := c.GetTransactionOutputType(block.Main.MajorVersion)
if err = c.PreprocessBlock(block); err != nil {
2022-11-06 10:58:19 +00:00
return nil, err
2022-11-03 11:32:07 +00:00
}
for _, o := range block.Main.Coinbase.Outputs {
if o.Type != expectedTxType {
2022-11-06 10:58:19 +00:00
return nil, errors.New("unexpected transaction type")
2022-11-03 11:32:07 +00:00
}
}
templateId := c.Consensus().CalculateSideTemplateId(&block.Main, &block.Side)
if templateId != block.SideTemplateId(c.Consensus()) {
2022-11-06 10:58:19 +00:00
return nil, fmt.Errorf("invalid template id %s, expected %s", templateId.String(), block.SideTemplateId(c.Consensus()).String())
2022-11-03 11:32:07 +00:00
}
if block.Side.Difficulty.Cmp64(c.Consensus().MinimumDifficulty) < 0 {
2022-11-06 10:58:19 +00:00
return nil, fmt.Errorf("block mined by %s has invalid difficulty %s, expected >= %d", block.GetAddress().ToBase58(), block.Side.Difficulty.StringNumeric(), c.Consensus().MinimumDifficulty)
2022-11-03 11:32:07 +00:00
}
//TODO: cache?
2022-11-06 10:58:19 +00:00
//expectedDifficulty := c.GetDifficulty(block)
//tooLowDiff := block.Side.Difficulty.Cmp(expectedDifficulty) < 0
tooLowDiff := false
var expectedDifficulty types.Difficulty
2022-11-03 11:32:07 +00:00
if otherBlock := c.GetPoolBlockByTemplateId(templateId); otherBlock != nil {
//already added
//TODO: specifically check Main id for nonce changes! p2pool does not do this
2022-11-06 10:58:19 +00:00
return nil, nil
2022-11-03 11:32:07 +00:00
}
// This is mainly an anti-spam measure, not an actual verification step
if tooLowDiff {
// Reduce required diff by 50% (by doubling this block's diff) to account for alternative chains
diff2 := block.Side.Difficulty.Mul64(2)
tip := c.GetChainTip()
for tmp := tip; tmp != nil && (tmp.Side.Height+c.Consensus().ChainWindowSize > tip.Side.Height); tmp = c.GetParent(tmp) {
if diff2.Cmp(tmp.Side.Difficulty) >= 0 {
tooLowDiff = false
break
}
}
}
//TODO log
if tooLowDiff {
2022-11-06 10:58:19 +00:00
return nil, fmt.Errorf("block mined by %s has too low difficulty %s, expected >= %s", block.GetAddress().ToBase58(), block.Side.Difficulty.StringNumeric(), expectedDifficulty.StringNumeric())
2022-11-03 11:32:07 +00:00
}
// This check is not always possible to perform because of mainchain reorgs
//TODO: cache current miner data?
if data := mainblock.GetBlockHeaderByHash(block.Main.PreviousId); data != nil {
if data.Height+1 != block.Main.Coinbase.GenHeight {
2022-11-06 10:58:19 +00:00
return nil, fmt.Errorf("wrong mainchain height %d, expected %d", block.Main.Coinbase.GenHeight, data.Height+1)
2022-11-03 11:32:07 +00:00
}
} else {
//TODO warn unknown block, reorg
}
if _, err := block.PowHashWithError(); err != nil {
2022-11-06 10:58:19 +00:00
return nil, err
2022-11-03 11:32:07 +00:00
} else {
//TODO: fast monero submission
if isHigher, err := block.IsProofHigherThanDifficultyWithError(); err != nil {
2022-11-06 10:58:19 +00:00
return nil, err
2022-11-03 11:32:07 +00:00
} else if !isHigher {
2022-11-06 10:58:19 +00:00
return nil, fmt.Errorf("not enough PoW for height = %d, mainchain height %d", block.Side.Height, block.Main.Coinbase.GenHeight)
2022-11-03 11:32:07 +00:00
}
}
//TODO: block found section
2022-11-06 10:58:19 +00:00
return func() []types.Hash {
2022-11-06 12:17:26 +00:00
c.sidechainLock.RLock()
defer c.sidechainLock.RUnlock()
2022-11-06 10:58:19 +00:00
missing := make([]types.Hash, 0, 4)
if block.Side.Parent != types.ZeroHash && c.getPoolBlockByTemplateId(block.Side.Parent) == nil {
missing = append(missing, block.Side.Parent)
}
for _, uncleId := range block.Side.Uncles {
if uncleId != types.ZeroHash && c.getPoolBlockByTemplateId(uncleId) == nil {
missing = append(missing, uncleId)
}
}
return missing
}(), c.AddPoolBlock(block)
2022-11-03 11:32:07 +00:00
}
func (c *SideChain) AddPoolBlock(block *PoolBlock) (err error) {
2022-11-06 12:17:26 +00:00
c.sidechainLock.Lock()
defer c.sidechainLock.Unlock()
2022-11-03 11:32:07 +00:00
if _, ok := c.blocksByTemplateId[block.SideTemplateId(c.Consensus())]; ok {
//already inserted
//TODO WARN
return nil
}
c.blocksByTemplateId[block.SideTemplateId(c.Consensus())] = block
log.Printf("[SideChain] add_block: height = %d, id = %s, mainchain height = %d, verified = %t", block.Side.Height, block.SideTemplateId(c.Consensus()), block.Main.Coinbase.GenHeight, block.Verified.Load())
2022-11-03 11:32:07 +00:00
if l, ok := c.blocksByHeight[block.Side.Height]; ok {
c.blocksByHeight[block.Side.Height] = append(l, block)
} else {
c.blocksByHeight[block.Side.Height] = []*PoolBlock{block}
}
c.updateDepths(block)
if block.Verified.Load() {
if !block.Invalid.Load() {
c.updateChainTip(block)
}
2022-11-06 10:58:19 +00:00
return nil
2022-11-03 11:32:07 +00:00
} else {
2022-11-06 10:58:19 +00:00
return c.verifyLoop(block)
2022-11-03 11:32:07 +00:00
}
}
2022-11-06 10:58:19 +00:00
func (c *SideChain) verifyLoop(blockToVerify *PoolBlock) (err error) {
2022-11-03 11:32:07 +00:00
// PoW is already checked at this point
blocksToVerify := make([]*PoolBlock, 1, 8)
2022-11-06 10:58:19 +00:00
blocksToVerify[0] = blockToVerify
2022-11-03 11:32:07 +00:00
var highestBlock *PoolBlock
for len(blocksToVerify) != 0 {
2022-11-06 10:58:19 +00:00
block := blocksToVerify[len(blocksToVerify)-1]
2022-11-03 11:32:07 +00:00
blocksToVerify = blocksToVerify[:len(blocksToVerify)-1]
if block.Verified.Load() {
continue
}
2022-11-06 10:58:19 +00:00
if verification, invalid := c.verifyBlock(block); invalid != nil {
log.Printf("[SideChain] block at height = %d, id = %s, mainchain height = %d, mined by %s is invalid: %s", block.Side.Height, block.SideTemplateId(c.Consensus()), block.Main.Coinbase.GenHeight, block.GetAddress().ToBase58(), invalid.Error())
block.Invalid.Store(true)
block.Verified.Store(verification == nil)
if block == blockToVerify {
//Save error for return
err = invalid
}
} else if verification != nil {
//log.Printf("[SideChain] can't verify block at height = %d, id = %s, mainchain height = %d, mined by %s: %s", block.Side.Height, block.SideTemplateId(c.Consensus()), block.Main.Coinbase.GenHeight, block.GetAddress().ToBase58(), verification.Error())
2022-11-06 10:58:19 +00:00
block.Verified.Store(false)
block.Invalid.Store(false)
2022-11-03 11:32:07 +00:00
} else {
2022-11-06 10:58:19 +00:00
block.Verified.Store(true)
block.Invalid.Store(false)
log.Printf("[SideChain] verified block at height = %d, depth = %d, id = %s, mainchain height = %d, mined by %s", block.Side.Height, block.Depth.Load(), block.SideTemplateId(c.Consensus()), block.Main.Coinbase.GenHeight, block.GetAddress().ToBase58())
2022-11-03 11:32:07 +00:00
// This block is now verified
2022-11-06 12:17:26 +00:00
if isLongerChain, _ := c.isLongerChain(highestBlock, block); isLongerChain {
2022-11-03 11:32:07 +00:00
highestBlock = block
} else if highestBlock != nil && highestBlock.Side.Height > block.Side.Height {
2022-11-06 10:58:19 +00:00
log.Printf("[SideChain] block at height = %d, id = %s, is not a longer chain than height = %d, id = %s", block.Side.Height, block.SideTemplateId(c.Consensus()), highestBlock.Side.Height, highestBlock.SideTemplateId(c.Consensus()))
2022-11-03 11:32:07 +00:00
}
if block.WantBroadcast.Load() && !block.Broadcasted.Swap(true) {
if block.Depth.Load() < UncleBlockDepth {
c.server.Broadcast(block)
}
}
//store for faster startup
c.saveBlock(block)
2022-11-03 11:32:07 +00:00
// Try to verify blocks on top of this one
for i := uint64(1); i <= UncleBlockDepth; i++ {
blocksToVerify = append(blocksToVerify, c.blocksByHeight[block.Side.Height+i]...)
}
}
}
if highestBlock != nil {
c.updateChainTip(highestBlock)
}
2022-11-06 10:58:19 +00:00
return
2022-11-03 11:32:07 +00:00
}
2022-11-06 10:58:19 +00:00
func (c *SideChain) verifyBlock(block *PoolBlock) (verification error, invalid error) {
2022-11-03 11:32:07 +00:00
// Genesis
if block.Side.Height == 0 {
if block.Side.Parent != types.ZeroHash ||
len(block.Side.Uncles) != 0 ||
block.Side.Difficulty.Cmp64(c.Consensus().MinimumDifficulty) != 0 ||
block.Side.CumulativeDifficulty.Cmp64(c.Consensus().MinimumDifficulty) != 0 {
2022-11-06 10:58:19 +00:00
return nil, errors.New("genesis block has invalid parameters")
2022-11-03 11:32:07 +00:00
}
//this does not verify coinbase outputs, but that's fine
2022-11-06 10:58:19 +00:00
return nil, nil
2022-11-03 11:32:07 +00:00
}
// Deep block
//
// Blocks in PPLNS window (m_chainWindowSize) require up to m_chainWindowSize earlier blocks to verify
// If a block is deeper than m_chainWindowSize * 2 - 1 it can't influence blocks in PPLNS window
// Also, having so many blocks on top of this one means it was verified by the network at some point
// We skip checks in this case to make pruning possible
if block.Depth.Load() >= c.Consensus().ChainWindowSize*2 {
2022-11-06 10:58:19 +00:00
log.Printf("[SideChain] block at height = %d, id = %s skipped verification", block.Side.Height, block.SideTemplateId(c.Consensus()))
return nil, nil
2022-11-03 11:32:07 +00:00
}
//Regular block
//Must have parent
if block.Side.Parent == types.ZeroHash {
2022-11-06 10:58:19 +00:00
return nil, errors.New("block must have a parent")
2022-11-03 11:32:07 +00:00
}
if parent := c.getParent(block); parent != nil {
// If it's invalid then this block is also invalid
2022-11-06 10:58:19 +00:00
if !parent.Verified.Load() {
return errors.New("parent is not verified"), nil
}
2022-11-03 11:32:07 +00:00
if parent.Invalid.Load() {
2022-11-06 10:58:19 +00:00
return nil, errors.New("parent is invalid")
2022-11-03 11:32:07 +00:00
}
expectedHeight := parent.Side.Height + 1
if expectedHeight != block.Side.Height {
2022-11-06 10:58:19 +00:00
return nil, fmt.Errorf("wrong height, expected %d", expectedHeight)
2022-11-03 11:32:07 +00:00
}
// Uncle hashes must be sorted in the ascending order to prevent cheating when the same hash is repeated multiple times
for i, uncleId := range block.Side.Uncles {
2022-11-05 09:10:04 +00:00
if i == 0 {
2022-11-03 11:32:07 +00:00
continue
}
2022-11-07 07:37:39 +00:00
if block.Side.Uncles[i-1].Compare(uncleId) != -1 {
2022-11-06 10:58:19 +00:00
return nil, errors.New("invalid uncle order")
2022-11-03 11:32:07 +00:00
}
}
2022-11-05 09:10:04 +00:00
expectedCumulativeDifficulty := parent.Side.CumulativeDifficulty.Add(block.Side.Difficulty)
2022-11-03 11:32:07 +00:00
//check uncles
minedBlocks := make([]types.Hash, 0, len(block.Side.Uncles)*UncleBlockDepth*2+1)
{
tmp := parent
n := utils.Min(UncleBlockDepth, block.Side.Height+1)
for i := uint64(0); tmp != nil && i < n; i++ {
minedBlocks = append(minedBlocks, tmp.SideTemplateId(c.Consensus()))
for _, uncleId := range tmp.Side.Uncles {
minedBlocks = append(minedBlocks, uncleId)
}
tmp = c.getParent(tmp)
}
}
for _, uncleId := range block.Side.Uncles {
// Empty hash is only used in the genesis block and only for its parent
// Uncles can't be empty
if uncleId == types.ZeroHash {
2022-11-06 10:58:19 +00:00
return nil, errors.New("empty uncle hash")
2022-11-03 11:32:07 +00:00
}
// Can't mine the same uncle block twice
if slices.Index(minedBlocks, uncleId) != -1 {
2022-11-06 10:58:19 +00:00
return nil, fmt.Errorf("uncle %s has already been mined", uncleId.String())
2022-11-03 11:32:07 +00:00
}
if uncle := c.getPoolBlockByTemplateId(uncleId); uncle == nil {
2022-11-06 10:58:19 +00:00
return errors.New("uncle does not exist"), nil
} else if !uncle.Verified.Load() {
// If it's invalid then this block is also invalid
return errors.New("uncle is not verified"), nil
2022-11-03 11:32:07 +00:00
} else if uncle.Invalid.Load() {
// If it's invalid then this block is also invalid
2022-11-06 10:58:19 +00:00
return nil, errors.New("uncle is invalid")
2022-11-03 11:32:07 +00:00
} else if uncle.Side.Height >= block.Side.Height || (uncle.Side.Height+UncleBlockDepth < block.Side.Height) {
2022-11-06 10:58:19 +00:00
return nil, fmt.Errorf("uncle at the wrong height (%d)", uncle.Side.Height)
2022-11-03 11:32:07 +00:00
} else {
// Check that uncle and parent have the same ancestor (they must be on the same chain)
tmp := parent
for tmp.Side.Height > uncle.Side.Height {
tmp = c.getParent(tmp)
if tmp == nil {
2022-11-06 10:58:19 +00:00
return nil, errors.New("uncle from different chain (check 1)")
2022-11-03 11:32:07 +00:00
}
}
if tmp.Side.Height < uncle.Side.Height {
2022-11-06 10:58:19 +00:00
return nil, errors.New("uncle from different chain (check 2)")
2022-11-03 11:32:07 +00:00
}
if sameChain := func() bool {
tmp2 := uncle
for j := uint64(0); j < UncleBlockDepth && tmp != nil && tmp2 != nil && (tmp.Side.Height+UncleBlockDepth >= block.Side.Height); j++ {
if tmp.Side.Parent == tmp2.Side.Parent {
return true
}
tmp = c.getParent(tmp)
tmp2 = c.getParent(tmp2)
}
return false
}(); !sameChain {
2022-11-06 10:58:19 +00:00
return nil, errors.New("uncle from different chain (check 3)")
2022-11-03 11:32:07 +00:00
}
2022-11-05 09:10:04 +00:00
expectedCumulativeDifficulty = expectedCumulativeDifficulty.Add(uncle.Side.Difficulty)
2022-11-03 11:32:07 +00:00
}
}
// We can verify this block now (all previous blocks in the window are verified and valid)
// It can still turn out to be invalid
2022-11-05 09:10:04 +00:00
if !block.Side.CumulativeDifficulty.Equals(expectedCumulativeDifficulty) {
2022-11-06 10:58:19 +00:00
return nil, fmt.Errorf("wrong cumulative difficulty, got %s, expected %s", block.Side.CumulativeDifficulty.StringNumeric(), expectedCumulativeDifficulty.StringNumeric())
2022-11-03 11:32:07 +00:00
}
// Verify difficulty and miner rewards only for blocks in PPLNS window
if block.Depth.Load() >= c.Consensus().ChainWindowSize {
2022-11-06 10:58:19 +00:00
log.Printf("[SideChain] block at height = %d, id = %s skipped diff/reward verification", block.Side.Height, block.SideTemplateId(c.Consensus()))
2022-11-03 11:32:07 +00:00
return
}
if diff := c.getDifficulty(parent); diff == types.ZeroDifficulty {
2022-11-06 10:58:19 +00:00
return nil, errors.New("could not get difficulty")
2022-11-03 11:32:07 +00:00
} else if diff != block.Side.Difficulty {
2022-11-06 10:58:19 +00:00
return nil, fmt.Errorf("wrong difficulty, got %s, expected %s", block.Side.Difficulty.StringNumeric(), diff.StringNumeric())
2022-11-03 11:32:07 +00:00
}
2022-11-06 12:17:26 +00:00
if c.sharesCache = c.getShares(block, c.sharesCache); len(c.sharesCache) == 0 {
2022-11-06 10:58:19 +00:00
return nil, errors.New("could not get outputs")
2022-11-06 12:17:26 +00:00
} else if len(c.sharesCache) != len(block.Main.Coinbase.Outputs) {
return nil, fmt.Errorf("invalid number of outputs, got %d, expected %d", len(block.Main.Coinbase.Outputs), len(c.sharesCache))
2022-11-03 11:32:07 +00:00
} else if totalReward := func() (result uint64) {
for _, o := range block.Main.Coinbase.Outputs {
result += o.Reward
}
return
}(); totalReward != block.Main.Coinbase.TotalReward {
2022-11-06 10:58:19 +00:00
return nil, fmt.Errorf("invalid total reward, got %d, expected %d", block.Main.Coinbase.TotalReward, totalReward)
2022-11-06 12:17:26 +00:00
} else if rewards := c.SplitReward(totalReward, c.sharesCache); len(rewards) != len(block.Main.Coinbase.Outputs) {
2022-11-06 10:58:19 +00:00
return nil, fmt.Errorf("invalid number of outputs, got %d, expected %d", len(block.Main.Coinbase.Outputs), len(rewards))
2022-11-03 11:32:07 +00:00
} else {
results := utils.SplitWork(-2, uint64(len(rewards)), func(workIndex uint64, workerIndex int) error {
out := block.Main.Coinbase.Outputs[workIndex]
if rewards[workIndex] != out.Reward {
return fmt.Errorf("has invalid reward at index %d, got %d, expected %d", workIndex, out.Reward, rewards[workIndex])
}
2022-11-06 12:17:26 +00:00
if ephPublicKey, viewTag := c.cache.GetEphemeralPublicKey(&c.sharesCache[workIndex].Address, &block.Side.CoinbasePrivateKey, workIndex); ephPublicKey != out.EphemeralPublicKey {
return fmt.Errorf("has incorrect eph_public_key at index %d, got %s, expected %s", workIndex, out.EphemeralPublicKey.String(), ephPublicKey.String())
} else if out.Type == transaction.TxOutToTaggedKey && viewTag != out.ViewTag {
return fmt.Errorf("has incorrect view tag at index %d, got %d, expected %d", workIndex, out.ViewTag, viewTag)
}
return nil
})
2022-11-06 12:17:26 +00:00
for i := range results {
if results[i] != nil {
return nil, results[i]
2022-11-03 11:32:07 +00:00
}
}
}
// All checks passed
2022-11-06 10:58:19 +00:00
return nil, nil
2022-11-03 11:32:07 +00:00
} else {
2022-11-06 10:58:19 +00:00
return errors.New("parent does not exist"), nil
2022-11-03 11:32:07 +00:00
}
}
func (c *SideChain) updateDepths(block *PoolBlock) {
for i := uint64(1); i <= UncleBlockDepth; i++ {
for _, child := range c.blocksByHeight[block.Side.Height+i] {
if child.Side.Parent.Equals(block.SideTemplateId(c.Consensus())) {
if i != 1 {
//TODO: error
} else {
block.Depth.Store(utils.Max(block.Depth.Load(), child.Depth.Load()+1))
}
}
if ix := slices.Index(child.Side.Uncles, block.SideTemplateId(c.Consensus())); ix != 1 {
block.Depth.Store(utils.Max(block.Depth.Load(), child.Depth.Load()+1))
}
}
}
blocksToUpdate := make([]*PoolBlock, 1, 8)
blocksToUpdate[0] = block
for len(blocksToUpdate) != 0 {
block = blocksToUpdate[len(blocksToUpdate)-1]
blocksToUpdate = blocksToUpdate[:len(blocksToUpdate)-1]
blockDepth := block.Depth.Load()
// Verify this block and possibly other blocks on top of it when we're sure it will get verified
if !block.Verified.Load() && (blockDepth >= c.Consensus().ChainWindowSize*2 || block.Side.Height == 0) {
c.verifyLoop(block)
}
if parent := c.getParent(block); parent != nil {
if parent.Side.Height+1 != block.Side.Height {
//TODO error
}
if parent.Depth.Load() < blockDepth+1 {
parent.Depth.Store(blockDepth + 1)
blocksToUpdate = append(blocksToUpdate, parent)
}
}
for _, uncleId := range block.Side.Uncles {
if uncle := c.getPoolBlockByTemplateId(uncleId); uncle != nil {
if uncle.Side.Height >= block.Side.Height || (uncle.Side.Height+UncleBlockDepth < block.Side.Height) {
//TODO: error
}
d := block.Side.Height - uncle.Side.Height
if uncle.Depth.Load() < blockDepth+d {
uncle.Depth.Store(blockDepth + d)
blocksToUpdate = append(blocksToUpdate, uncle)
}
}
}
}
}
func (c *SideChain) updateChainTip(block *PoolBlock) {
if !block.Verified.Load() || block.Invalid.Load() {
//todo err
return
}
if block.Depth.Load() >= c.Consensus().ChainWindowSize {
//TODO err
return
}
tip := c.GetChainTip()
2022-11-06 12:17:26 +00:00
if isLongerChain, isAlternative := c.isLongerChain(tip, block); isLongerChain {
2022-11-03 11:32:07 +00:00
if diff := c.getDifficulty(block); diff != types.ZeroDifficulty {
c.chainTip.Store(block)
c.currentDifficulty.Store(&diff)
//TODO log
block.WantBroadcast.Store(true)
if isAlternative {
c.cache.Clear()
}
c.pruneOldBlocks()
}
} else if block.Side.Height > tip.Side.Height {
//TODO log
} else if block.Side.Height+UncleBlockDepth > tip.Side.Height {
//TODO: log
}
if block.WantBroadcast.Load() && !block.Broadcasted.Swap(true) {
c.server.Broadcast(block)
}
}
func (c *SideChain) pruneOldBlocks() {
//TODO
}
func (c *SideChain) GetTransactionOutputType(majorVersion uint8) uint8 {
// Both tx types are allowed by Monero consensus during v15 because it needs to process pre-fork mempool transactions,
// but P2Pool can switch to using only TXOUT_TO_TAGGED_KEY for miner payouts starting from v15
expectedTxType := uint8(transaction.TxOutToKey)
if majorVersion >= monero.HardForkViewTagsVersion {
expectedTxType = transaction.TxOutToTaggedKey
}
return expectedTxType
}
2022-11-05 10:28:10 +00:00
func (c *SideChain) getOutputs(block *PoolBlock) transaction.Outputs {
//cannot use SideTemplateId() as it might not be proper to calculate yet. fetch from coinbase only here
if b := c.getPoolBlockByTemplateId(types.HashFromBytes(block.CoinbaseExtra(SideTemplateId))); b != nil {
2022-11-03 11:32:07 +00:00
return b.Main.Coinbase.Outputs
}
return c.calculateOutputs(block)
}
func (c *SideChain) calculateOutputs(block *PoolBlock) transaction.Outputs {
//TODO: buffer
tmpShares := c.getShares(block, make(Shares, 0, c.Consensus().ChainWindowSize*2))
2022-11-03 11:32:07 +00:00
tmpRewards := c.SplitReward(block.Main.Coinbase.TotalReward, tmpShares)
if tmpShares == nil || tmpRewards == nil || len(tmpRewards) != len(tmpShares) {
return nil
}
n := uint64(len(tmpShares))
2022-11-03 11:32:07 +00:00
2022-11-05 10:28:10 +00:00
outputs := make(transaction.Outputs, n)
2022-11-03 11:32:07 +00:00
txType := c.GetTransactionOutputType(block.Main.MajorVersion)
_ = utils.SplitWork(-2, n, func(workIndex uint64, workerIndex int) error {
output := &transaction.Output{
Index: workIndex,
Type: txType,
}
output.Reward = tmpRewards[output.Index]
output.EphemeralPublicKey, output.ViewTag = c.cache.GetEphemeralPublicKey(&tmpShares[output.Index].Address, &block.Side.CoinbasePrivateKey, output.Index)
2022-11-03 11:32:07 +00:00
outputs[output.Index] = output
2022-11-03 11:32:07 +00:00
return nil
})
2022-11-03 11:32:07 +00:00
return outputs
}
func (c *SideChain) SplitReward(reward uint64, shares Shares) (rewards []uint64) {
var totalWeight uint64
for i := range shares {
totalWeight += shares[i].Weight
}
if totalWeight == 0 {
//TODO: err
return nil
}
rewards = make([]uint64, len(shares))
var w uint64
var rewardGiven uint64
for i := range shares {
w += shares[i].Weight
nextValue := types.DifficultyFrom64(w).Mul64(reward).Div64(totalWeight).Lo
rewards[i] = nextValue - rewardGiven
rewardGiven = nextValue
}
// Double check that we gave out the exact amount
rewardGiven = 0
for _, r := range rewards {
rewardGiven += r
}
if rewardGiven != reward {
return nil
}
return rewards
}
2022-11-06 12:17:26 +00:00
func (c *SideChain) getShares(tip *PoolBlock, shares Shares) Shares {
shares = shares[:0]
2022-11-03 11:32:07 +00:00
var blockDepth uint64
cur := tip
2022-11-06 12:17:26 +00:00
2022-11-03 11:32:07 +00:00
for {
2022-11-06 12:17:26 +00:00
curShare := &Share{Weight: cur.Side.Difficulty.Lo, Address: *cur.GetAddress()}
2022-11-03 11:32:07 +00:00
for _, uncleId := range cur.Side.Uncles {
2022-11-06 10:58:19 +00:00
if uncle := c.getPoolBlockByTemplateId(uncleId); uncle == nil {
2022-11-03 11:32:07 +00:00
//cannot find uncles
return nil
} else {
// Skip uncles which are already out of PPLNS window
if (tip.Side.Height - uncle.Side.Height) >= c.Consensus().ChainWindowSize {
continue
}
// Take some % of uncle's weight into this share
product := uncle.Side.Difficulty.Mul64(c.Consensus().UnclePenalty)
unclePenalty := product.Div64(100)
curShare.Weight += unclePenalty.Lo
2022-11-06 12:17:26 +00:00
shares = append(shares, &Share{Weight: uncle.Side.Difficulty.Sub(unclePenalty).Lo, Address: *uncle.GetAddress()})
2022-11-03 11:32:07 +00:00
}
}
shares = append(shares, curShare)
blockDepth++
if blockDepth >= c.Consensus().ChainWindowSize {
break
}
// Reached the genesis block so we're done
if cur.Side.Height == 0 {
break
}
2022-11-06 10:58:19 +00:00
cur = c.getParent(cur)
2022-11-03 11:32:07 +00:00
if cur == nil {
return nil
}
}
// Combine shares with the same wallet addresses
2022-11-06 12:17:26 +00:00
slices.SortFunc(shares, func(a *Share, b *Share) bool {
return a.Address.Compare(&b.Address) < 0
2022-11-03 11:32:07 +00:00
})
k := 0
2022-11-06 10:58:19 +00:00
for i := 1; i < len(shares); i++ {
if shares[i].Address.Compare(&shares[k].Address) == 0 {
2022-11-03 11:32:07 +00:00
shares[k].Weight += shares[i].Weight
} else {
k++
shares[k].Address = shares[i].Address
shares[k].Weight = shares[i].Weight
}
}
2022-11-06 10:58:19 +00:00
return shares[:k+1]
2022-11-03 11:32:07 +00:00
}
type DifficultyData struct {
CumulativeDifficulty types.Difficulty
Timestamp uint64
}
func (c *SideChain) GetDifficulty(tip *PoolBlock) types.Difficulty {
2022-11-06 12:17:26 +00:00
c.sidechainLock.RLock()
defer c.sidechainLock.RUnlock()
2022-11-03 11:32:07 +00:00
return c.getDifficulty(tip)
}
func (c *SideChain) getDifficulty(tip *PoolBlock) types.Difficulty {
difficultyData := make([]DifficultyData, 0, c.Consensus().ChainWindowSize*2)
cur := tip
var blockDepth uint64
var oldestTimestamp uint64 = math.MaxUint64
for {
oldestTimestamp = utils.Min(oldestTimestamp, cur.Main.Timestamp)
difficultyData = append(difficultyData, DifficultyData{CumulativeDifficulty: cur.Side.CumulativeDifficulty, Timestamp: cur.Main.Timestamp})
for _, uncleId := range cur.Side.Uncles {
if uncle := c.getPoolBlockByTemplateId(uncleId); uncle == nil {
//cannot find uncles
return types.ZeroDifficulty
} else {
// Skip uncles which are already out of PPLNS window
if (tip.Side.Height - uncle.Side.Height) >= c.Consensus().ChainWindowSize {
continue
}
oldestTimestamp = utils.Min(oldestTimestamp, uncle.Main.Timestamp)
difficultyData = append(difficultyData, DifficultyData{CumulativeDifficulty: uncle.Side.CumulativeDifficulty, Timestamp: uncle.Main.Timestamp})
}
}
blockDepth++
if blockDepth >= c.Consensus().ChainWindowSize {
break
}
// Reached the genesis block so we're done
if cur.Side.Height == 0 {
break
}
2022-11-06 10:58:19 +00:00
cur = c.getParent(cur)
2022-11-03 11:32:07 +00:00
if cur == nil {
return types.ZeroDifficulty
}
}
// Discard 10% oldest and 10% newest (by timestamp) blocks
tmpTimestamps := make([]uint32, 0, len(difficultyData))
for i := range difficultyData {
tmpTimestamps = append(tmpTimestamps, uint32(difficultyData[i].Timestamp-oldestTimestamp))
}
cutSize := (len(difficultyData) + 9) / 10
index1 := cutSize - 1
index2 := len(difficultyData) - cutSize
//TODO: replace this with introspective selection, use order for now
slices.Sort(tmpTimestamps)
timestamp1 := oldestTimestamp + uint64(tmpTimestamps[index1])
timestamp2 := oldestTimestamp + uint64(tmpTimestamps[index2])
deltaT := uint64(1)
if timestamp2 > timestamp1 {
deltaT = timestamp2 - timestamp1
}
var diff1 = types.Difficulty{Hi: math.MaxUint64, Lo: math.MaxUint64}
var diff2 types.Difficulty
for i := range difficultyData {
d := &difficultyData[i]
if timestamp1 <= d.Timestamp && d.Timestamp <= timestamp2 {
if d.CumulativeDifficulty.Cmp(diff1) < 0 {
diff1 = d.CumulativeDifficulty
}
if diff2.Cmp(d.CumulativeDifficulty) < 0 {
diff2 = d.CumulativeDifficulty
}
}
}
//TODO: p2pool uses uint64 instead of full range here: This is correct as long as the difference between two 128-bit difficulties is less than 2^64, even if it wraps
deltaDiff := diff2.Sub(diff1)
product := deltaDiff.Mul64(c.Consensus().TargetBlockTime)
2022-11-06 10:58:19 +00:00
if product.Hi >= deltaT {
2022-11-03 11:32:07 +00:00
//TODO: error, calculated difficulty too high
return types.ZeroDifficulty
}
curDifficulty := product.Div64(deltaT)
if curDifficulty.Cmp64(c.Consensus().MinimumDifficulty) < 0 {
curDifficulty = types.DifficultyFrom64(c.Consensus().MinimumDifficulty)
}
return curDifficulty
}
func (c *SideChain) GetParent(block *PoolBlock) *PoolBlock {
2022-11-06 12:17:26 +00:00
c.sidechainLock.RLock()
defer c.sidechainLock.RUnlock()
2022-11-03 11:32:07 +00:00
return c.getParent(block)
}
func (c *SideChain) getParent(block *PoolBlock) *PoolBlock {
return c.getPoolBlockByTemplateId(block.Side.Parent)
}
func (c *SideChain) GetPoolBlockByTemplateId(id types.Hash) *PoolBlock {
2022-11-06 12:17:26 +00:00
c.sidechainLock.RLock()
defer c.sidechainLock.RUnlock()
2022-11-03 11:32:07 +00:00
return c.getPoolBlockByTemplateId(id)
}
2022-11-06 10:58:19 +00:00
func (c *SideChain) GetPoolBlockCount() int {
2022-11-06 12:17:26 +00:00
c.sidechainLock.RLock()
defer c.sidechainLock.RUnlock()
2022-11-06 10:58:19 +00:00
return len(c.blocksByTemplateId)
}
2022-11-03 11:32:07 +00:00
func (c *SideChain) getPoolBlockByTemplateId(id types.Hash) *PoolBlock {
return c.blocksByTemplateId[id]
}
func (c *SideChain) GetChainTip() *PoolBlock {
return c.chainTip.Load()
}
func (c *SideChain) IsLongerChain(block, candidate *PoolBlock) (isLonger, isAlternative bool) {
2022-11-06 12:17:26 +00:00
c.sidechainLock.RLock()
defer c.sidechainLock.RUnlock()
return c.isLongerChain(block, candidate)
}
func (c *SideChain) isLongerChain(block, candidate *PoolBlock) (isLonger, isAlternative bool) {
if candidate == nil || !candidate.Verified.Load() || candidate.Invalid.Load() {
2022-11-03 11:32:07 +00:00
return false, false
}
// Switching from an empty to a non-empty chain
if block == nil {
return true, true
}
// If these two blocks are on the same chain, they must have a common ancestor
blockAncestor := block
for blockAncestor != nil && blockAncestor.Side.Height > candidate.Side.Height {
2022-11-06 12:17:26 +00:00
blockAncestor = c.getParent(blockAncestor)
2022-11-03 11:32:07 +00:00
//TODO: err on blockAncestor nil
}
if blockAncestor != nil {
candidateAncestor := candidate
for candidateAncestor != nil && candidateAncestor.Side.Height > blockAncestor.Side.Height {
2022-11-06 12:17:26 +00:00
candidateAncestor = c.getParent(candidateAncestor)
2022-11-03 11:32:07 +00:00
//TODO: err on candidateAncestor nil
}
for blockAncestor != nil && candidateAncestor != nil {
if blockAncestor.Side.Parent.Equals(candidateAncestor.Side.Parent) {
return block.Side.CumulativeDifficulty.Cmp(candidate.Side.CumulativeDifficulty) < 0, false
}
2022-11-06 12:17:26 +00:00
blockAncestor = c.getParent(blockAncestor)
candidateAncestor = c.getParent(candidateAncestor)
2022-11-03 11:32:07 +00:00
}
}
// They're on totally different chains. Compare total difficulties over the last m_chainWindowSize blocks
var blockTotalDiff, candidateTotalDiff types.Difficulty
oldChain := block
newChain := candidate
var candidateMainchainHeight, candidateMainchainMinHeight uint64
var mainchainPrevId types.Hash
for i := uint64(0); i < c.Consensus().ChainWindowSize && (oldChain != nil || newChain != nil); i++ {
if oldChain != nil {
blockTotalDiff = blockTotalDiff.Add(oldChain.Side.Difficulty)
2022-11-06 12:17:26 +00:00
oldChain = c.getParent(oldChain)
2022-11-03 11:32:07 +00:00
}
if newChain != nil {
if candidateMainchainMinHeight != 0 {
candidateMainchainMinHeight = utils.Min(candidateMainchainMinHeight, newChain.Main.Coinbase.GenHeight)
} else {
candidateMainchainMinHeight = newChain.Main.Coinbase.GenHeight
}
candidateTotalDiff = candidateTotalDiff.Add(newChain.Side.Difficulty)
if !newChain.Main.PreviousId.Equals(mainchainPrevId) {
if data := mainblock.GetBlockHeaderByHash(newChain.Main.PreviousId); data != nil {
mainchainPrevId = data.Id
candidateMainchainHeight = utils.Max(candidateMainchainHeight, data.Height)
}
}
2022-11-06 12:17:26 +00:00
newChain = c.getParent(newChain)
2022-11-03 11:32:07 +00:00
}
}
if blockTotalDiff.Cmp(candidateTotalDiff) >= 0 {
return false, true
}
// Final check: candidate chain must be built on top of recent mainchain blocks
if data := mainblock.GetLastBlockHeader(); data != nil {
if candidateMainchainHeight+10 < data.Height {
//TODO: warn received a longer alternative chain but it's stale: height
return false, true
}
limit := c.Consensus().ChainWindowSize * 4 * c.Consensus().TargetBlockTime / monero.BlockTime
if candidateMainchainMinHeight+limit < data.Height {
//TODO: warn received a longer alternative chain but it's stale: min height
return false, true
}
return true, true
} else {
return false, true
}
}