diff --git a/cmd/p2pool/p2pool.go b/cmd/p2pool/p2pool.go index e775c44..bb0484a 100644 --- a/cmd/p2pool/p2pool.go +++ b/cmd/p2pool/p2pool.go @@ -8,6 +8,7 @@ import ( p2poolinstance "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool" "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/p2p" "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain" + "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/stratum" "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/types" "git.gammaspectra.live/P2Pool/p2pool-observer/utils" "golang.org/x/exp/slices" @@ -51,6 +52,9 @@ func main() { addSelf := flag.Bool("add-self-peer", false, "Adds itself to the peer list regularly, based on found local interfaces for IPv4/IPv6") peerList := flag.String("peer-list", "p2pool_peers.txt", "Either a path or an URL to obtain peer lists from. If it is a path, new peers will be saved to this path. Set to empty to disable") + //stratum related + stratumListen := flag.String("stratum", "", "IP:port for stratum server to listen on. Empty to disable.") + //other settings lightMode := flag.Bool("light-mode", false, "Don't allocate RandomX dataset, saves 2GB of RAM") noDns := flag.Bool("no-dns", false, "Disable DNS queries, use only IP addresses to connect to peers (seed node DNS will be unavailable too)") @@ -178,6 +182,27 @@ func main() { }() } + if *stratumListen != "" { + stratumServer := stratum.NewServer(instance.SideChain(), func(block *sidechain.PoolBlock) error { + block.WantBroadcast.Store(true) + block.LocalTimestamp = uint64(time.Now().Unix()) + if _, err, _ := instance.SideChain().AddPoolBlockExternal(block); err != nil { + log.Printf("[Stratum] ERROR: Submitted block template id = %s, side height = %d, main height = %d, main_id = %s, nonce = %d, extra_nonce = %d add_pool_block_external error: %s", block.SideTemplateId(instance.Consensus()), block.Side.Height, block.Main.Coinbase.GenHeight, block.MainId(), block.Main.Nonce, block.ExtraNonce(), err) + return err + } else { + log.Printf("[Stratum] SUCCESS: Submitted block template id = %s, side height = %d, main height = %d, main_id = %s, nonce = %d, extra_nonce = %d", block.SideTemplateId(instance.Consensus()), block.Side.Height, block.Main.Coinbase.GenHeight, block.MainId(), block.Main.Nonce, block.ExtraNonce()) + } + return nil + }) + listenerId := instance.AddListener(stratumServer.HandleTip, stratumServer.HandleBroadcast, nil, nil, stratumServer.HandleMinerData, stratumServer.HandleMempoolData) + go func() { + defer instance.RemoveListener(listenerId) + if err := stratumServer.Listen(*stratumListen); err != nil { + log.Panic(err) + } + }() + } + var connectList []netip.AddrPort for _, peerAddr := range strings.Split(*addPeers, ",") { if peerAddr == "" { diff --git a/p2pool/stratum/message.go b/p2pool/stratum/message.go new file mode 100644 index 0000000..a2df713 --- /dev/null +++ b/p2pool/stratum/message.go @@ -0,0 +1,89 @@ +package stratum + +import "git.gammaspectra.live/P2Pool/p2pool-observer/types" + +type JsonRpcMessage struct { + // Id set by client + Id any `json:"id,omitempty"` + // JsonRpcVersion Always "2.0" + JsonRpcVersion string `json:"jsonrpc"` + Method string `json:"method"` + Params any `json:"params,omitempty"` +} +type JsonRpcResult struct { + // Id set by client + Id any `json:"id,omitempty"` + // JsonRpcVersion Always "2.0" + JsonRpcVersion string `json:"jsonrpc"` + Result any `json:"result,omitempty"` + Error any `json:"error"` +} + +type JsonRpcJob struct { + // JsonRpcVersion Always "2.0" + JsonRpcVersion string `json:"jsonrpc"` + // Method always "job" + Method string `json:"method"` + Params jsonRpcJobParams `json:"params"` +} + +type jsonRpcJobParams struct { + // Blob HashingBlob, in hex + Blob string `json:"blob"` + + // JobId anything? + JobId string `json:"job_id"` + + // Target uint64 target in hex + Target string `json:"target"` + + // Algo always "rx/0" + Algo string `json:"algo"` + + // Height main height + Height uint64 `json:"height"` + + // SeedHash + SeedHash types.Hash `json:"seed_hash"` +} +type JsonRpcResponseJob struct { + // Id set by client + Id any `json:"id,omitempty"` + // JsonRpcVersion Always "2.0" + JsonRpcVersion string `json:"jsonrpc"` + Result jsonRpcResponseJobResult `json:"result"` +} + +type jsonRpcResponseJobResult struct { + Id string `json:"id,omitempty"` + Job jsonRpcJobParams `json:"job"` + Extensions []string `json:"extensions"` + Status string `json:"status"` +} + +var baseRpcJob = JsonRpcJob{ + JsonRpcVersion: "2.0", + Method: "job", + Params: jsonRpcJobParams{ + Algo: "rx/0", + }, +} + +var baseRpcResponseJob = JsonRpcResponseJob{ + JsonRpcVersion: "2.0", + Result: jsonRpcResponseJobResult{ + Job: jsonRpcJobParams{ + Algo: "rx/0", + }, + Extensions: []string{"algo"}, + Status: "OK", + }, +} + +func copyBaseJob() JsonRpcJob { + return baseRpcJob +} + +func copyBaseResponseJob() JsonRpcResponseJob { + return baseRpcResponseJob +} diff --git a/p2pool/stratum/miner.go b/p2pool/stratum/miner.go new file mode 100644 index 0000000..7adc603 --- /dev/null +++ b/p2pool/stratum/miner.go @@ -0,0 +1,86 @@ +package stratum + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "errors" + "git.gammaspectra.live/P2Pool/p2pool-observer/types" + "sync" + "sync/atomic" + "time" +) + +type MinerTrackingEntry struct { + Lock sync.RWMutex + Counter atomic.Uint64 + LastTemplate atomic.Uint64 + Templates map[uint64]*Template + LastJob time.Time +} + +const JobIdentifierSize = 8 + 4 + 4 + 4 + types.HashSize + +type JobIdentifier [JobIdentifierSize]byte + +func JobIdentifierFromString(s string) (JobIdentifier, error) { + var h JobIdentifier + if buf, err := hex.DecodeString(s); err != nil { + return h, err + } else { + if len(buf) != JobIdentifierSize { + return h, errors.New("wrong job id size") + } + copy(h[:], buf) + return h, nil + } +} +func JobIdentifierFromValues(templateCounter uint64, extraNonce, sideRandomNumber, sideExtraNonce uint32, templateId types.Hash) JobIdentifier { + var h JobIdentifier + binary.LittleEndian.PutUint64(h[:], templateCounter) + binary.LittleEndian.PutUint32(h[8:], extraNonce) + binary.LittleEndian.PutUint32(h[8+4:], sideRandomNumber) + binary.LittleEndian.PutUint32(h[8+4+4:], sideExtraNonce) + copy(h[8+4+4+4:], templateId[:]) + return h +} + +func (id JobIdentifier) TemplateCounter() uint64 { + return binary.LittleEndian.Uint64(id[:]) +} + +func (id JobIdentifier) ExtraNonce() uint32 { + return binary.LittleEndian.Uint32(id[8:]) +} + +func (id JobIdentifier) SideRandomNumber() uint32 { + return binary.LittleEndian.Uint32(id[8+4:]) +} + +func (id JobIdentifier) SideExtraNonce() uint32 { + return binary.LittleEndian.Uint32(id[8+4+4:]) +} + +func (id JobIdentifier) TemplateId() types.Hash { + return types.HashFromBytes(id[8+4+4+4 : 8+4+4+4+types.HashSize]) +} + +func (id JobIdentifier) String() string { + return hex.EncodeToString(id[:]) +} + +// GetJobBlob Gets old job data based on returned id +func (e *MinerTrackingEntry) GetJobBlob(jobId JobIdentifier, nonce uint32) []byte { + e.Lock.RLock() + defer e.Lock.RUnlock() + + if t, ok := e.Templates[jobId.TemplateCounter()]; ok { + buffer := bytes.NewBuffer(make([]byte, 0, len(t.Buffer))) + if err := t.Write(buffer, nonce, jobId.ExtraNonce(), jobId.SideRandomNumber(), jobId.SideExtraNonce(), jobId.TemplateId()); err != nil { + return nil + } + return buffer.Bytes() + } else { + return nil + } +} diff --git a/p2pool/stratum/server.go b/p2pool/stratum/server.go new file mode 100644 index 0000000..3920563 --- /dev/null +++ b/p2pool/stratum/server.go @@ -0,0 +1,1276 @@ +package stratum + +import ( + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "git.gammaspectra.live/P2Pool/p2pool-observer/monero" + "git.gammaspectra.live/P2Pool/p2pool-observer/monero/address" + "git.gammaspectra.live/P2Pool/p2pool-observer/monero/block" + "git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto" + "git.gammaspectra.live/P2Pool/p2pool-observer/monero/transaction" + "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/mempool" + "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain" + p2pooltypes "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/types" + "git.gammaspectra.live/P2Pool/p2pool-observer/types" + "git.gammaspectra.live/P2Pool/p2pool-observer/utils" + gojson "github.com/goccy/go-json" + "golang.org/x/exp/slices" + "log" + "math" + unsafeRandom "math/rand" + "net" + "net/netip" + "sync" + "time" +) + +type ephemeralPubKeyCacheKey [crypto.PublicKeySize*2 + 8]byte + +type ephemeralPubKeyCacheEntry struct { + PublicKey crypto.PublicKeyBytes + ViewTag uint8 +} + +type NewTemplateData struct { + PreviousTemplateId types.Hash + SideHeight uint64 + Difficulty types.Difficulty + CumulativeDifficulty types.Difficulty + TransactionPrivateKeySeed types.Hash + // TransactionPrivateKey Generated from TransactionPrivateKeySeed + TransactionPrivateKey crypto.PrivateKeyBytes + TransactionPublicKey crypto.PublicKeySlice + Timestamp uint64 + TotalReward uint64 + Transactions []types.Hash + MaxRewardAmountsWeight uint64 + ShareVersion sidechain.ShareVersion + Uncles []types.Hash + Ready bool + Window struct { + ReservedShareIndex int + Shares sidechain.Shares + ShuffleMapping [2][]int + EphemeralPubKeyCache map[ephemeralPubKeyCacheKey]*ephemeralPubKeyCacheEntry + } +} + +type Server struct { + SubmitFunc func(block *sidechain.PoolBlock) error + + minerData *p2pooltypes.MinerData + tip *sidechain.PoolBlock + newTemplateData NewTemplateData + lock sync.RWMutex + sidechain *sidechain.SideChain + + mempool mempool.Mempool + lastMempoolRefresh time.Time + + preAllocatedDifficultyData []sidechain.DifficultyData + preAllocatedDifficultyDifferences []uint32 + preAllocatedSharesPool *sidechain.PreAllocatedSharesPool + + preAllocatedBufferLock sync.Mutex + preAllocatedBuffer []byte + + minersLock sync.RWMutex + miners map[address.PackedAddress]*MinerTrackingEntry + + bansLock sync.RWMutex + bans map[[16]byte]BanEntry + + clientsLock sync.RWMutex + clients []*Client + + incomingChanges chan func() bool +} + +type Client struct { + Lock sync.RWMutex + Conn *net.TCPConn + encoder *gojson.Encoder + decoder *gojson.Decoder + Agent string + Login bool + Address address.PackedAddress + Password string + RigId string + buf []byte + RpcId uint32 +} + +func (c *Client) Write(b []byte) (int, error) { + if err := c.Conn.SetWriteDeadline(time.Now().Add(time.Second * 5)); err != nil { + return 0, err + } + return c.Conn.Write(b) +} + +func NewServer(s *sidechain.SideChain, submitFunc func(block *sidechain.PoolBlock) error) *Server { + server := &Server{ + SubmitFunc: submitFunc, + sidechain: s, + preAllocatedDifficultyData: make([]sidechain.DifficultyData, s.Consensus().ChainWindowSize*2), + preAllocatedDifficultyDifferences: make([]uint32, s.Consensus().ChainWindowSize*2), + preAllocatedSharesPool: sidechain.NewPreAllocatedSharesPool(s.Consensus().ChainWindowSize * 2), + preAllocatedBuffer: make([]byte, 0, sidechain.PoolBlockMaxTemplateSize), + miners: make(map[address.PackedAddress]*MinerTrackingEntry), + // buffer 4 at a time for non-blocking source + incomingChanges: make(chan func() bool, 4), + } + return server +} + +func (s *Server) CleanupMiners() { + s.minersLock.Lock() + defer s.minersLock.Unlock() + + cleanupTime := time.Now() + for k, e := range s.miners { + if cleanupTime.Sub(e.LastJob) > time.Minute*5 { + delete(s.miners, k) + } else { + if len(e.Templates) > 0 { + var templateSideHeight uint64 + for _, tpl := range e.Templates { + if tpl.SideHeight > templateSideHeight { + templateSideHeight = tpl.SideHeight + } + } + + tipHeight := uint64(0) + if templateSideHeight > sidechain.UncleBlockDepth { + tipHeight = templateSideHeight - sidechain.UncleBlockDepth + 1 + } + //Delete old templates further than uncle depth + for key, tpl := range e.Templates { + if tpl.SideHeight < tipHeight { + delete(e.Templates, key) + } + } + + // TODO: Prevent long-term leaks by re-allocating templates if capacity grew too much + } + } + } +} + +func (s *Server) fillNewTemplateData(currentDifficulty types.Difficulty) error { + + s.newTemplateData.Ready = false + + if s.minerData == nil { + return errors.New("no main data present") + } + + if s.minerData.MajorVersion > monero.HardForkSupportedVersion { + return fmt.Errorf("unsupported hardfork version %d", s.minerData.MajorVersion) + } + + oldPubKeyCache := s.newTemplateData.Window.EphemeralPubKeyCache + + s.newTemplateData.Timestamp = uint64(time.Now().Unix()) + + s.newTemplateData.ShareVersion = sidechain.P2PoolShareVersion(s.sidechain.Consensus(), s.newTemplateData.Timestamp) + + // Do not allow mining on old chains, as they are not optimal for CPU usage + if s.newTemplateData.ShareVersion < sidechain.ShareVersion_V2 { + return errors.New("unsupported sidechain version") + } + + if s.tip != nil { + s.newTemplateData.PreviousTemplateId = s.tip.SideTemplateId(s.sidechain.Consensus()) + s.newTemplateData.SideHeight = s.tip.Side.Height + 1 + + oldSeed := s.newTemplateData.TransactionPrivateKeySeed + + s.newTemplateData.TransactionPrivateKeySeed = s.tip.Side.CoinbasePrivateKeySeed + if s.tip.Main.PreviousId != s.minerData.PrevId { + s.newTemplateData.TransactionPrivateKeySeed = s.tip.CalculateTransactionPrivateKeySeed() + } + + //TODO: check this + if s.newTemplateData.TransactionPrivateKeySeed != oldSeed { + oldPubKeyCache = nil + } + + if currentDifficulty != types.ZeroDifficulty { + //difficulty is set from caller + s.newTemplateData.Difficulty = currentDifficulty + oldPubKeyCache = nil + } + + s.newTemplateData.CumulativeDifficulty = s.tip.Side.CumulativeDifficulty.Add(s.newTemplateData.Difficulty) + + s.newTemplateData.Uncles = s.sidechain.GetPossibleUncles(s.tip, s.newTemplateData.SideHeight) + } else { + s.newTemplateData.PreviousTemplateId = types.ZeroHash + s.newTemplateData.TransactionPrivateKeySeed = s.sidechain.Consensus().Id + s.newTemplateData.Difficulty = types.DifficultyFrom64(s.sidechain.Consensus().MinimumDifficulty) + s.newTemplateData.CumulativeDifficulty = types.DifficultyFrom64(s.sidechain.Consensus().MinimumDifficulty) + } + + kP := s.sidechain.DerivationCache().GetDeterministicTransactionKey(s.newTemplateData.TransactionPrivateKeySeed, s.minerData.PrevId) + s.newTemplateData.TransactionPrivateKey = kP.PrivateKey.AsBytes() + s.newTemplateData.TransactionPublicKey = kP.PublicKey.AsSlice() + + fakeTemplateTipBlock := &sidechain.PoolBlock{ + Main: block.Block{ + MajorVersion: s.minerData.MajorVersion, + MinorVersion: monero.HardForkSupportedVersion, + Timestamp: s.newTemplateData.Timestamp, + PreviousId: s.minerData.PrevId, + Nonce: 0, + Coinbase: &transaction.CoinbaseTransaction{ + GenHeight: s.minerData.Height, + }, + //TODO: + Transactions: nil, + }, + Side: sidechain.SideData{ + //Zero Spend/View key + PublicSpendKey: crypto.PublicKeyBytes{}, + PublicViewKey: crypto.PublicKeyBytes{}, + CoinbasePrivateKeySeed: s.newTemplateData.TransactionPrivateKeySeed, + CoinbasePrivateKey: s.newTemplateData.TransactionPrivateKey, + Parent: s.newTemplateData.PreviousTemplateId, + Uncles: s.newTemplateData.Uncles, + Height: s.newTemplateData.SideHeight, + Difficulty: s.newTemplateData.Difficulty, + CumulativeDifficulty: s.newTemplateData.CumulativeDifficulty, + }, + CachedShareVersion: s.newTemplateData.ShareVersion, + } + + preAllocatedShares := s.preAllocatedSharesPool.Get() + defer s.preAllocatedSharesPool.Put(preAllocatedShares) + shares, _ := sidechain.GetSharesOrdered(fakeTemplateTipBlock, s.sidechain.Consensus(), s.sidechain.Server().GetDifficultyByHeight, s.sidechain.GetPoolBlockByTemplateId, preAllocatedShares) + + if shares == nil { + return errors.New("could not get outputs") + } + + s.newTemplateData.Window.Shares = slices.Clone(shares) + s.newTemplateData.Window.ReservedShareIndex = s.newTemplateData.Window.Shares.Index(fakeTemplateTipBlock.GetAddress()) + + if s.newTemplateData.Window.ReservedShareIndex == -1 { + return errors.New("could not find reserved share index") + } + + baseReward := block.GetBaseReward(s.minerData.AlreadyGeneratedCoins) + + totalWeight, totalFees := s.mempool.WeightAndFees() + + maxReward := baseReward + totalFees + + rewards := sidechain.SplitReward(maxReward, s.newTemplateData.Window.Shares) + + s.newTemplateData.MaxRewardAmountsWeight = uint64(utils.UVarInt64SliceSize(rewards)) + + tx, err := s.createCoinbaseTransaction(fakeTemplateTipBlock.GetTransactionOutputType(), s.newTemplateData.Window.Shares, rewards, s.newTemplateData.MaxRewardAmountsWeight, false) + if err != nil { + return err + } + coinbaseTransactionWeight := uint64(tx.BufferLength()) + + var selectedMempool mempool.Mempool + + if totalWeight+coinbaseTransactionWeight <= s.minerData.MedianWeight { + // if a block doesn't get into the penalty zone, just pick all transactions + selectedMempool = s.mempool + } else { + selectedMempool = s.mempool.Pick(baseReward, coinbaseTransactionWeight, s.minerData.MedianWeight) + } + + s.newTemplateData.Transactions = make([]types.Hash, len(selectedMempool)) + + for i, entry := range selectedMempool { + s.newTemplateData.Transactions[i] = entry.Id + } + + finalReward := mempool.GetBlockReward(baseReward, s.minerData.MedianWeight, selectedMempool.Fees(), coinbaseTransactionWeight+selectedMempool.Weight()) + + if finalReward < baseReward { + return errors.New("final reward < base reward, should never happen") + } + s.newTemplateData.TotalReward = finalReward + + s.newTemplateData.Window.ShuffleMapping = BuildShuffleMapping(len(s.newTemplateData.Window.Shares), s.newTemplateData.ShareVersion, s.newTemplateData.TransactionPrivateKeySeed) + + s.newTemplateData.Window.EphemeralPubKeyCache = make(map[ephemeralPubKeyCacheKey]*ephemeralPubKeyCacheEntry) + + txPrivateKeySlice := s.newTemplateData.TransactionPrivateKey.AsSlice() + txPrivateKeyScalar := s.newTemplateData.TransactionPrivateKey.AsScalar() + + //TODO: parallelize this + hasher := crypto.GetKeccak256Hasher() + defer crypto.PutKeccak256Hasher(hasher) + for i, m := range PossibleIndicesForShuffleMapping(s.newTemplateData.Window.ShuffleMapping) { + if i == 0 { + // Skip zero key + continue + } + share := s.newTemplateData.Window.Shares[i] + var k ephemeralPubKeyCacheKey + copy(k[:], share.Address.Bytes()) + + for _, index := range m { + if index == -1 { + continue + } + binary.LittleEndian.PutUint64(k[crypto.PublicKeySize*2:], uint64(index)) + if e, ok := oldPubKeyCache[k]; ok { + s.newTemplateData.Window.EphemeralPubKeyCache[k] = e + } else { + var e ephemeralPubKeyCacheEntry + e.PublicKey, e.ViewTag = s.sidechain.DerivationCache().GetEphemeralPublicKey(&share.Address, txPrivateKeySlice, txPrivateKeyScalar, uint64(index), hasher) + s.newTemplateData.Window.EphemeralPubKeyCache[k] = &e + } + } + } + + s.newTemplateData.Ready = true + + return nil + +} + +func BuildShuffleMapping(n int, shareVersion sidechain.ShareVersion, transactionPrivateKeySeed types.Hash) (mappings [2][]int) { + if n <= 1 { + return [2][]int{{0}, {0}} + } + shuffleSequence1 := make([]int, n) + for i := range shuffleSequence1 { + shuffleSequence1[i] = i + } + shuffleSequence2 := make([]int, n-1) + for i := range shuffleSequence2 { + shuffleSequence2[i] = i + } + + sidechain.ShuffleSequence(shareVersion, transactionPrivateKeySeed, n, func(i, j int) { + shuffleSequence1[i], shuffleSequence1[j] = shuffleSequence1[j], shuffleSequence1[i] + }) + sidechain.ShuffleSequence(shareVersion, transactionPrivateKeySeed, n-1, func(i, j int) { + shuffleSequence2[i], shuffleSequence2[j] = shuffleSequence2[j], shuffleSequence2[i] + }) + + mappings[0] = make([]int, n) + mappings[1] = make([]int, n-1) + + //Flip + for i := range shuffleSequence1 { + mappings[0][shuffleSequence1[i]] = i + } + for i := range shuffleSequence2 { + mappings[1][shuffleSequence2[i]] = i + } + + return mappings +} + +func ApplyShuffleMapping[T any](v []T, mappings [2][]int) []T { + n := len(v) + + result := make([]T, n) + + if n == len(mappings[0]) { + for i := range v { + result[mappings[0][i]] = v[i] + } + } else if n == len(mappings[1]) { + for i := range v { + result[mappings[1][i]] = v[i] + } + } + return result +} + +func PossibleIndicesForShuffleMapping(mappings [2][]int) [][3]int { + n := len(mappings[0]) + result := make([][3]int, n) + for i := 0; i < n; i++ { + // Count with all + miner + result[i][0] = mappings[0][i] + if i > 0 { + // Count with all + miner shifted to a slot before + result[i][1] = mappings[0][i-1] + + // Count with all miners minus one + result[i][2] = mappings[1][i-1] + } else { + result[i][1] = -1 + result[i][2] = -1 + } + } + + return result +} + +func (s *Server) BuildTemplate(addr address.PackedAddress, forceNewTemplate bool) (tpl *Template, jobCounter uint64, difficultyTarget types.Difficulty, seedHash types.Hash, err error) { + + var zeroAddress address.PackedAddress + if addr == zeroAddress { + return nil, 0, types.ZeroDifficulty, types.ZeroHash, errors.New("nil address") + } + + e, ok := func() (*MinerTrackingEntry, bool) { + s.minersLock.RLock() + defer s.minersLock.RUnlock() + e, ok := s.miners[addr] + return e, ok + }() + + tpl, jobCounter, targetDiff, seedHash, err := func() (tpl *Template, jobCounter uint64, difficultyTarget types.Difficulty, seedHash types.Hash, err error) { + s.lock.RLock() + defer s.lock.RUnlock() + + if s.minerData == nil { + return nil, 0, types.ZeroDifficulty, types.ZeroHash, errors.New("nil miner data") + } + + if !s.newTemplateData.Ready { + return nil, 0, types.ZeroDifficulty, types.ZeroHash, errors.New("template data not ready") + } + + if !forceNewTemplate && e != nil && ok { + if tpl, jobCounter := func() (*Template, uint64) { + e.Lock.RLock() + defer e.Lock.RUnlock() + + jobCounter := e.LastTemplate.Load() + + if tpl, ok := e.Templates[jobCounter]; ok && tpl.SideParent == s.newTemplateData.PreviousTemplateId && tpl.MainParent == s.minerData.PrevId { + return tpl, jobCounter + } + return nil, 0 + }(); tpl != nil { + e.Lock.Lock() + defer e.Lock.Unlock() + e.LastJob = time.Now() + + targetDiff := tpl.SideDifficulty + if s.minerData.Difficulty.Cmp(targetDiff) < 0 { + targetDiff = s.minerData.Difficulty + } + + return tpl, jobCounter, targetDiff, s.minerData.SeedHash, nil + } + } + + blockTemplate := &sidechain.PoolBlock{ + Main: block.Block{ + MajorVersion: s.minerData.MajorVersion, + MinorVersion: monero.HardForkSupportedVersion, + Timestamp: s.newTemplateData.Timestamp, + PreviousId: s.minerData.PrevId, + Nonce: 0, + Transactions: s.newTemplateData.Transactions, + }, + Side: sidechain.SideData{ + PublicSpendKey: *addr.SpendPublicKey(), + PublicViewKey: *addr.ViewPublicKey(), + CoinbasePrivateKeySeed: s.newTemplateData.TransactionPrivateKeySeed, + CoinbasePrivateKey: s.newTemplateData.TransactionPrivateKey, + Parent: s.newTemplateData.PreviousTemplateId, + Uncles: s.newTemplateData.Uncles, + Height: s.newTemplateData.SideHeight, + Difficulty: s.newTemplateData.Difficulty, + CumulativeDifficulty: s.newTemplateData.CumulativeDifficulty, + ExtraBuffer: struct { + SoftwareId p2pooltypes.SoftwareId + SoftwareVersion p2pooltypes.SoftwareVersion + RandomNumber uint32 + SideChainExtraNonce uint32 + }{ + SoftwareId: p2pooltypes.CurrentSoftwareId, + SoftwareVersion: p2pooltypes.CurrentSoftwareVersion, + RandomNumber: 0, + SideChainExtraNonce: 0, + }, + }, + CachedShareVersion: s.newTemplateData.ShareVersion, + } + + preAllocatedShares := s.preAllocatedSharesPool.Get() + defer s.preAllocatedSharesPool.Put(preAllocatedShares) + + shares := s.newTemplateData.Window.Shares.Clone() + + // It exists, replace + if i := shares.Index(addr); i != -1 { + shares[i] = &sidechain.Share{ + Address: addr, + Weight: shares[i].Weight.Add(shares[s.newTemplateData.Window.ReservedShareIndex].Weight), + } + shares = slices.Delete(shares, s.newTemplateData.Window.ReservedShareIndex, s.newTemplateData.Window.ReservedShareIndex+1) + } else { + // Replace reserved address + shares[s.newTemplateData.Window.ReservedShareIndex] = &sidechain.Share{ + Weight: shares[s.newTemplateData.Window.ReservedShareIndex].Weight, + Address: addr, + } + } + shares = shares.Compact() + + // Apply consensus shuffle + shares = ApplyShuffleMapping(shares, s.newTemplateData.Window.ShuffleMapping) + + // Allocate rewards + { + preAllocatedRewards := make([]uint64, 0, len(shares)) + rewards := sidechain.SplitRewardNoAllocate(preAllocatedRewards, s.newTemplateData.TotalReward, shares) + + if rewards == nil || len(rewards) != len(shares) { + return nil, 0, types.ZeroDifficulty, types.ZeroHash, errors.New("could not calculate rewards") + } + + if blockTemplate.Main.Coinbase, err = s.createCoinbaseTransaction(blockTemplate.GetTransactionOutputType(), shares, rewards, s.newTemplateData.MaxRewardAmountsWeight, true); err != nil { + return nil, 0, types.ZeroDifficulty, types.ZeroHash, err + } + } + + tpl, err = TemplateFromPoolBlock(blockTemplate) + if err != nil { + return nil, 0, types.ZeroDifficulty, types.ZeroHash, err + } + + targetDiff := tpl.SideDifficulty + if s.minerData.Difficulty.Cmp(targetDiff) < 0 { + targetDiff = s.minerData.Difficulty + } + + return tpl, 0, targetDiff, s.minerData.SeedHash, nil + }() + + if err != nil { + return nil, 0, types.ZeroDifficulty, types.ZeroHash, err + } + + if forceNewTemplate || jobCounter != 0 { + return tpl, jobCounter, targetDiff, seedHash, nil + } + + if e != nil && ok { + e.Lock.Lock() + defer e.Lock.Unlock() + var newJobCounter uint64 + for newJobCounter == 0 { + newJobCounter = e.Counter.Add(1) + } + e.Templates[newJobCounter] = tpl + e.LastTemplate.Store(newJobCounter) + e.LastJob = time.Now() + return tpl, newJobCounter, targetDiff, seedHash, nil + } else { + s.minersLock.Lock() + defer s.minersLock.Unlock() + + e = &MinerTrackingEntry{ + Templates: make(map[uint64]*Template), + } + var newJobCounter uint64 + for newJobCounter == 0 { + newJobCounter = e.Counter.Add(1) + } + e.Templates[newJobCounter] = tpl + e.LastTemplate.Store(newJobCounter) + e.LastJob = time.Now() + s.miners[addr] = e + return tpl, newJobCounter, targetDiff, seedHash, nil + } +} + +func (s *Server) createCoinbaseTransaction(txType uint8, shares sidechain.Shares, rewards []uint64, maxRewardsAmountsWeight uint64, final bool) (tx *transaction.CoinbaseTransaction, err error) { + tx = &transaction.CoinbaseTransaction{ + Version: 2, + UnlockTime: s.minerData.Height + monero.MinerRewardUnlockTime, + InputCount: 1, + InputType: transaction.TxInGen, + GenHeight: s.minerData.Height, + TotalReward: func() (v uint64) { + for i := range rewards { + v += rewards[i] + } + return + }(), + Extra: transaction.ExtraTags{ + transaction.ExtraTag{ + Tag: transaction.TxExtraTagPubKey, + VarInt: 0, + Data: types.Bytes(s.newTemplateData.TransactionPublicKey), + }, + transaction.ExtraTag{ + Tag: transaction.TxExtraTagNonce, + VarInt: sidechain.SideExtraNonceSize, + HasVarInt: true, + Data: make(types.Bytes, sidechain.SideExtraNonceSize), + }, + transaction.ExtraTag{ + Tag: transaction.TxExtraTagMergeMining, + VarInt: 32, + HasVarInt: true, + Data: slices.Clone(types.ZeroHash[:]), + }, + }, + ExtraBaseRCT: 0, + } + + tx.Outputs = make(transaction.Outputs, len(shares)) + + if final { + txPrivateKeySlice := s.newTemplateData.TransactionPrivateKey.AsSlice() + txPrivateKeyScalar := s.newTemplateData.TransactionPrivateKey.AsScalar() + + hasher := crypto.GetKeccak256Hasher() + defer crypto.PutKeccak256Hasher(hasher) + + var k ephemeralPubKeyCacheKey + for i := range tx.Outputs { + outputIndex := uint64(i) + tx.Outputs[outputIndex].Index = outputIndex + tx.Outputs[outputIndex].Type = txType + tx.Outputs[outputIndex].Reward = rewards[outputIndex] + copy(k[:], shares[outputIndex].Address.Bytes()) + binary.LittleEndian.PutUint64(k[crypto.PublicKeySize*2:], outputIndex) + if e, ok := s.newTemplateData.Window.EphemeralPubKeyCache[k]; ok { + tx.Outputs[outputIndex].EphemeralPublicKey, tx.Outputs[outputIndex].ViewTag = e.PublicKey, e.ViewTag + } else { + tx.Outputs[outputIndex].EphemeralPublicKey, tx.Outputs[outputIndex].ViewTag = s.sidechain.DerivationCache().GetEphemeralPublicKey(&shares[outputIndex].Address, txPrivateKeySlice, txPrivateKeyScalar, outputIndex, hasher) + } + } + } else { + for i := range tx.Outputs { + outputIndex := uint64(i) + tx.Outputs[outputIndex].Index = outputIndex + tx.Outputs[outputIndex].Type = txType + tx.Outputs[outputIndex].Reward = rewards[outputIndex] + } + } + + rewardAmountsWeight := uint64(utils.UVarInt64SliceSize(rewards)) + + if !final { + if rewardAmountsWeight != maxRewardsAmountsWeight { + return nil, fmt.Errorf("incorrect miner rewards during the dry run, %d != %d", rewardAmountsWeight, maxRewardsAmountsWeight) + } + } else if rewardAmountsWeight > maxRewardsAmountsWeight { + return nil, fmt.Errorf("incorrect miner rewards during the dry run, %d > %d", rewardAmountsWeight, maxRewardsAmountsWeight) + } + + correctedExtraNonceSize := sidechain.SideExtraNonceSize + maxRewardsAmountsWeight - rewardAmountsWeight + + if correctedExtraNonceSize > sidechain.SideExtraNonceSize { + if correctedExtraNonceSize > sidechain.SideExtraNonceMaxSize { + return nil, fmt.Errorf("corrected extra_nonce size is too large, %d > %d", correctedExtraNonceSize, sidechain.SideExtraNonceMaxSize) + } + //Increase size to maintain transaction weight + tx.Extra[1].Data = make(types.Bytes, correctedExtraNonceSize) + tx.Extra[1].VarInt = correctedExtraNonceSize + } + + return tx, nil +} + +func (s *Server) HandleMempoolData(data mempool.Mempool) { + s.incomingChanges <- func() bool { + s.lock.Lock() + defer s.lock.Unlock() + + s.mempool = append(s.mempool, data...) + + // Refresh if 10 seconds have passed between templates and new transactions arrived + if time.Now().Sub(s.lastMempoolRefresh) >= time.Second*10 { + s.lastMempoolRefresh = time.Now() + if err := s.fillNewTemplateData(types.ZeroDifficulty); err != nil { + log.Printf("[Stratum] Error building new template data: %s", err) + return false + } + return true + } + return false + } +} + +func (s *Server) HandleMinerData(minerData *p2pooltypes.MinerData) { + s.incomingChanges <- func() bool { + s.lock.Lock() + defer s.lock.Unlock() + + if s.minerData == nil || s.minerData.Height <= minerData.Height { + s.minerData = minerData + s.mempool = minerData.TxBacklog + s.lastMempoolRefresh = time.Now() + if err := s.fillNewTemplateData(types.ZeroDifficulty); err != nil { + log.Printf("[Stratum] Error building new template data: %s", err) + return false + } + return true + } + return false + } +} + +func (s *Server) HandleTip(tip *sidechain.PoolBlock) { + currentDifficulty := s.sidechain.Difficulty() + s.incomingChanges <- func() bool { + s.lock.Lock() + defer s.lock.Unlock() + + if s.tip == nil || s.tip.Side.Height <= tip.Side.Height { + s.tip = tip + s.lastMempoolRefresh = time.Now() + if err := s.fillNewTemplateData(currentDifficulty); err != nil { + log.Printf("[Stratum] Error building new template data: %s", err) + return false + } + return true + } + return false + } +} + +func (s *Server) HandleBroadcast(block *sidechain.PoolBlock) { + s.incomingChanges <- func() bool { + s.lock.Lock() + defer s.lock.Unlock() + + if s.tip != nil && block != s.tip && block.Side.Height <= s.tip.Side.Height { + //probably a new block was added as alternate + if err := s.fillNewTemplateData(types.ZeroDifficulty); err != nil { + log.Printf("[Stratum] Error building new template data: %s", err) + return false + } + return true + } + return false + } +} + +func (s *Server) Update() { + var closeClients []*Client + defer func() { + for _, c := range closeClients { + s.CloseClient(c) + } + }() + s.clientsLock.RLock() + defer s.clientsLock.RUnlock() + + if len(s.clients) > 0 { + log.Printf("[Stratum] Sending new job to %d connection(s)", len(s.clients)) + for _, c := range s.clients { + if err := s.SendTemplate(c); err != nil { + log.Printf("[Stratum] Closing connection %s: %s", c.Conn.RemoteAddr().String(), err) + closeClients = append(closeClients, c) + } + } + } +} + +type BanEntry struct { + Expiration uint64 + Error error +} + +func (s *Server) CleanupBanList() { + s.bansLock.Lock() + defer s.bansLock.Unlock() + + currentTime := uint64(time.Now().Unix()) + + for k, b := range s.bans { + if currentTime >= b.Expiration { + delete(s.bans, k) + } + } +} + +func (s *Server) IsBanned(ip netip.Addr) (bool, *BanEntry) { + if ip.IsLoopback() { + return false, nil + } + ip = ip.Unmap() + var prefix netip.Prefix + if ip.Is6() { + //ban the /64 + prefix, _ = ip.Prefix(64) + } else if ip.Is4() { + //ban only a single ip, /32 + prefix, _ = ip.Prefix(32) + } + + if !prefix.IsValid() { + return false, nil + } + + k := prefix.Addr().As16() + + if b, ok := func() (entry BanEntry, ok bool) { + s.bansLock.RLock() + defer s.bansLock.RUnlock() + entry, ok = s.bans[k] + return entry, ok + }(); ok == false { + return false, nil + } else if uint64(time.Now().Unix()) >= b.Expiration { + return false, nil + } else { + return true, &b + } +} + +func (s *Server) Ban(ip netip.Addr, duration time.Duration, err error) { + if ok, _ := s.IsBanned(ip); ok { + return + } + + log.Printf("[Stratum] Banned %s for %s: %s", ip.String(), duration.String(), err.Error()) + if !ip.IsLoopback() { + ip = ip.Unmap() + var prefix netip.Prefix + if ip.Is6() { + //ban the /64 + prefix, _ = ip.Prefix(64) + } else if ip.Is4() { + //ban only a single ip, /32 + prefix, _ = ip.Prefix(32) + } + + if prefix.IsValid() { + func() { + s.bansLock.Lock() + defer s.bansLock.Unlock() + s.bans[prefix.Addr().As16()] = BanEntry{ + Error: err, + Expiration: uint64(time.Now().Unix()) + uint64(duration.Seconds()), + } + }() + } + } + +} + +func (s *Server) processIncoming() { + go func() { + defer close(s.incomingChanges) + + ctx := s.sidechain.Server().Context() + for { + select { + case <-ctx.Done(): + return + case f := <-s.incomingChanges: + if f() { + s.Update() + } + } + } + }() +} + +func (s *Server) Listen(listen string) error { + + ctx := s.sidechain.Server().Context() + go func() { + for range utils.ContextTick(ctx, time.Second*15) { + s.CleanupMiners() + } + }() + + s.processIncoming() + + if listener, err := (&net.ListenConfig{}).Listen(ctx, "tcp", listen); err != nil { + return err + } else if tcpListener, ok := listener.(*net.TCPListener); !ok { + return errors.New("not a tcp listener") + } else { + defer tcpListener.Close() + + addressNetwork, _ := s.sidechain.Consensus().NetworkType.AddressNetwork() + + for { + if conn, err := tcpListener.AcceptTCP(); err != nil { + return err + } else { + if err = func() error { + if addrPort, err := netip.ParseAddrPort(conn.RemoteAddr().String()); err != nil { + return err + } else if !addrPort.Addr().IsLoopback() { + addr := addrPort.Addr().Unmap() + + if ok, b := s.IsBanned(addr); ok { + return fmt.Errorf("peer is banned: %w", b.Error) + } + } + + return nil + }(); err != nil { + go func() { + defer conn.Close() + log.Printf("[Stratum] Connection from %s rejected (%s)", conn.RemoteAddr().String(), err.Error()) + }() + continue + } + + func() { + log.Printf("[Stratum] Incoming connection from %s", conn.RemoteAddr().String()) + + var rpcId uint32 + for rpcId == 0 { + rpcId = unsafeRandom.Uint32() + } + client := &Client{ + RpcId: rpcId, + Conn: conn, + decoder: utils.NewJSONDecoder(conn), + Address: address.FromBase58(types.DonationAddress).ToPackedAddress(), + } + // Use deadline + client.encoder = utils.NewJSONEncoder(client) + + func() { + s.clientsLock.Lock() + defer s.clientsLock.Unlock() + s.clients = append(s.clients, client) + }() + go func() { + var err error + defer s.CloseClient(client) + defer func() { + if err != nil { + log.Printf("[Stratum] Connection %s closed with error: %s", client.Conn.RemoteAddr().String(), err) + } else { + log.Printf("[Stratum] Connection %s closed", client.Conn.RemoteAddr().String()) + } + }() + defer func() { + if e := recover(); e != nil { + err = errors.New("panic called") + log.Print(e) + s.CloseClient(client) + } + }() + + for client.decoder.More() { + var msg JsonRpcMessage + if err = client.decoder.Decode(&msg); err != nil { + return + } + + switch msg.Method { + case "login": + + if err = func() error { + client.Lock.Lock() + defer client.Lock.Unlock() + if client.Login { + return errors.New("already logged in") + } + if m, ok := msg.Params.(map[string]any); ok { + if str, ok := m["agent"].(string); ok { + client.Agent = str + } + if str, ok := m["pass"].(string); ok { + client.Password = str + } + if str, ok := m["rig-id"].(string); ok { + client.RigId = str + } + if str, ok := m["login"].(string); ok { + a := address.FromBase58(str) + if a != nil && a.Network == addressNetwork { + client.Address = a.ToPackedAddress() + } else { + return errors.New("invalid address in user") + } + } + var hasRx0 bool + if algos, ok := m["algo"].([]any); ok { + for _, v := range algos { + if str, ok := v.(string); !ok { + return errors.New("invalid algo") + } else if str == "rx/0" { + hasRx0 = true + break + } + } + } + + if !hasRx0 { + return errors.New("algo rx/0 not found") + } + + log.Printf("[Stratum] Connection %s address = %s, agent = \"%s\", pass = \"%s\"", client.Conn.RemoteAddr().String(), client.Address.ToAddress(addressNetwork).ToBase58(), client.Agent, client.Password) + + client.Login = true + return nil + } else { + return errors.New("could not read login params") + } + }(); err != nil { + _ = client.encoder.Encode(JsonRpcResult{ + Id: msg.Id, + JsonRpcVersion: "2.0", + Error: map[string]any{ + "code": int(-1), + "message": err.Error(), + }, + }) + return + } else if err = s.SendTemplateResponse(client, msg.Id); err != nil { + _ = client.encoder.Encode(JsonRpcResult{ + Id: msg.Id, + JsonRpcVersion: "2.0", + Error: map[string]any{ + "code": int(-1), + "message": err.Error(), + }, + }) + } + + case "submit": + if submitError, ban := func() (error, bool) { + client.Lock.RLock() + defer client.Lock.RUnlock() + if !client.Login { + return errors.New("unauthenticated"), true + } + var err error + var jobId JobIdentifier + var resultHash types.Hash + var nonce uint32 + if m, ok := msg.Params.(map[string]any); ok { + if str, ok := m["job_id"].(string); ok { + if jobId, err = JobIdentifierFromString(str); err != nil { + return err, true + } + } else { + return errors.New("no job_id specified"), true + } + if str, ok := m["nonce"].(string); ok { + var nonceBuf []byte + if nonceBuf, err = hex.DecodeString(str); err != nil { + return err, true + } + if len(nonceBuf) != 4 { + return errors.New("invalid nonce size"), true + } + nonce = binary.LittleEndian.Uint32(nonceBuf) + } else { + return errors.New("no nonce specified"), true + } + if str, ok := m["result"].(string); ok { + if resultHash, err = types.HashFromString(str); err != nil { + return err, true + } + } else { + return errors.New("no result specified"), true + } + + if err, ban := func() (error, bool) { + if e, ok := func() (*MinerTrackingEntry, bool) { + s.minersLock.RLock() + defer s.minersLock.RUnlock() + e, ok := s.miners[client.Address] + return e, ok + }(); ok { + b := &sidechain.PoolBlock{} + if blob := e.GetJobBlob(jobId, nonce); blob == nil { + return errors.New("invalid job id"), true + } else if err := b.UnmarshalBinary(s.sidechain.Consensus(), s.sidechain.DerivationCache(), blob); err != nil { + return err, true + } else { + if b.Side.Difficulty.CheckPoW(resultHash) { + //passes difficulty + if err := s.SubmitFunc(b); err != nil { + return fmt.Errorf("submit error: %w", err), true + } + } else { + + return errors.New("low difficulty share"), true + } + } + } else { + return errors.New("unknown miner"), true + } + return nil, false + }(); err != nil { + return err, ban + } + return nil, false + } else { + return errors.New("could not read submit params"), true + } + }(); submitError != nil { + err = client.encoder.Encode(JsonRpcResult{ + Id: msg.Id, + JsonRpcVersion: "2.0", + Error: map[string]any{ + "code": int(-1), + "message": submitError.Error(), + }, + }) + if err != nil || ban { + return + } + } else { + if err = client.encoder.Encode(JsonRpcResult{ + Id: msg.Id, + JsonRpcVersion: "2.0", + Error: nil, + Result: map[string]any{ + "status": "OK", + }, + }); err != nil { + return + } + } + case "keepalived": + if err = client.encoder.Encode(JsonRpcResult{ + Id: msg.Id, + JsonRpcVersion: "2.0", + Error: nil, + Result: map[string]any{ + "status": "KEEPALIVED", + }, + }); err != nil { + return + } + default: + err = fmt.Errorf("unknown command %s", msg.Method) + _ = client.encoder.Encode(JsonRpcResult{ + Id: msg.Id, + JsonRpcVersion: "2.0", + Error: map[string]any{ + "code": int(-1), + "message": err.Error(), + }, + }) + return + } + } + }() + }() + } + + } + } + + return nil +} + +func (s *Server) SendTemplate(c *Client) (err error) { + c.Lock.Lock() + defer c.Lock.Unlock() + tpl, jobCounter, targetDifficulty, seedHash, err := s.BuildTemplate(c.Address, false) + + if err != nil { + return err + } + + job := copyBaseJob() + bufLen := tpl.HashingBlobBufferLength() + if cap(c.buf) < bufLen { + c.buf = make([]byte, 0, bufLen) + } + + sideRandomNumber := unsafeRandom.Uint32() + extraNonce := unsafeRandom.Uint32() + sideExtraNonce := extraNonce + + hasher := crypto.GetKeccak256Hasher() + defer crypto.PutKeccak256Hasher(hasher) + + var templateId types.Hash + tpl.TemplateId(hasher, c.buf, s.sidechain.Consensus(), sideRandomNumber, sideExtraNonce, &templateId) + + job.Params.Blob = hex.EncodeToString(tpl.HashingBlob(hasher, c.buf, 0, extraNonce, templateId)) + + jobId := JobIdentifierFromValues(jobCounter, extraNonce, sideRandomNumber, sideExtraNonce, templateId) + + job.Params.JobId = jobId.String() + + target := targetDifficulty.Target() + job.Params.Target = TargetHex(target) + job.Params.Height = tpl.MainHeight + job.Params.SeedHash = seedHash + + if err = c.encoder.EncodeWithOption(job, utils.JsonEncodeOptions...); err != nil { + return + } + return nil +} + +func (s *Server) SendTemplateResponse(c *Client, id any) (err error) { + c.Lock.Lock() + defer c.Lock.Unlock() + tpl, jobCounter, targetDifficulty, seedHash, err := s.BuildTemplate(c.Address, false) + + if err != nil { + return + } + + job := copyBaseResponseJob() + bufLen := tpl.HashingBlobBufferLength() + if cap(c.buf) < bufLen { + c.buf = make([]byte, 0, bufLen) + } + var hexBuf [4]byte + binary.LittleEndian.PutUint32(hexBuf[:], c.RpcId) + + sideRandomNumber := unsafeRandom.Uint32() + extraNonce := unsafeRandom.Uint32() + sideExtraNonce := extraNonce + + hasher := crypto.GetKeccak256Hasher() + defer crypto.PutKeccak256Hasher(hasher) + + var templateId types.Hash + tpl.TemplateId(hasher, c.buf, s.sidechain.Consensus(), sideRandomNumber, sideExtraNonce, &templateId) + + job.Id = id + job.Result.Id = hex.EncodeToString(hexBuf[:]) + job.Result.Job.Blob = hex.EncodeToString(tpl.HashingBlob(hasher, c.buf, 0, extraNonce, templateId)) + + jobId := JobIdentifierFromValues(jobCounter, extraNonce, sideRandomNumber, sideExtraNonce, templateId) + + job.Result.Job.JobId = jobId.String() + + target := targetDifficulty.Target() + job.Result.Job.Target = TargetHex(target) + job.Result.Job.Height = tpl.MainHeight + job.Result.Job.SeedHash = seedHash + + if err = c.encoder.EncodeWithOption(job, utils.JsonEncodeOptions...); err != nil { + return + } + return nil +} + +func (s *Server) CloseClient(c *Client) { + c.Conn.Close() + + s.clientsLock.Lock() + defer s.clientsLock.Unlock() + if i := slices.Index(s.clients, c); i != -1 { + s.clients = slices.Delete(s.clients, i, i+1) + } +} + +// Target4BytesLimit Use short target format (4 bytes) for diff <= 4 million +const Target4BytesLimit = math.MaxUint64 / 4000001 + +func TargetHex(target uint64) string { + var buf [8]byte + binary.LittleEndian.PutUint64(buf[:], target) + result := hex.EncodeToString(buf[:]) + if target >= Target4BytesLimit { + return result[4*2:] + } else { + return result + } +} diff --git a/p2pool/stratum/server_test.go b/p2pool/stratum/server_test.go new file mode 100644 index 0000000..ba6eb81 --- /dev/null +++ b/p2pool/stratum/server_test.go @@ -0,0 +1,246 @@ +package stratum + +import ( + "fmt" + "git.gammaspectra.live/P2Pool/p2pool-observer/monero/address" + "git.gammaspectra.live/P2Pool/p2pool-observer/monero/client" + "git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto" + "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain" + p2pooltypes "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/types" + "git.gammaspectra.live/P2Pool/p2pool-observer/types" + "golang.org/x/exp/slices" + unsafeRandom "math/rand" + "os" + "path" + "runtime" + "testing" + "time" +) + +var preLoadedMiniSideChain *sidechain.SideChain + +var preLoadedPoolBlock *sidechain.PoolBlock + +func init() { + _, filename, _, _ := runtime.Caller(0) + // The ".." may change depending on you folder structure + dir := path.Join(path.Dir(filename), "../..") + err := os.Chdir(dir) + if err != nil { + panic(err) + } +} + +func getMinerData() *p2pooltypes.MinerData { + if d, err := client.GetDefaultClient().GetMinerData(); err != nil { + return nil + } else { + return &p2pooltypes.MinerData{ + MajorVersion: d.MajorVersion, + Height: d.Height, + PrevId: types.MustHashFromString(d.PrevId), + SeedHash: types.MustHashFromString(d.SeedHash), + Difficulty: types.MustDifficultyFromString(d.Difficulty), + MedianWeight: d.MedianWeight, + AlreadyGeneratedCoins: d.AlreadyGeneratedCoins, + MedianTimestamp: d.MedianTimestamp, + TimeReceived: time.Now(), + TxBacklog: nil, + } + } +} + +func TestMain(m *testing.M) { + if buf, err := os.ReadFile("testdata/block.dat"); err != nil { + panic(err) + } else { + preLoadedPoolBlock = &sidechain.PoolBlock{} + if err = preLoadedPoolBlock.UnmarshalBinary(sidechain.ConsensusDefault, &sidechain.NilDerivationCache{}, buf); err != nil { + panic(err) + } + } + + _ = sidechain.ConsensusMini.InitHasher(2) + client.SetDefaultClientSettings(os.Getenv("MONEROD_RPC_URL")) + + preLoadedMiniSideChain = sidechain.NewSideChain(sidechain.GetFakeTestServer(sidechain.ConsensusMini)) + + f, err := os.Open("testdata/sidechain_dump_mini.dat") + if err != nil { + panic(err) + } + defer f.Close() + + if err = preLoadedMiniSideChain.LoadTestData(f); err != nil { + panic(err) + } + + code := m.Run() + + os.Exit(code) +} + +func TestStratumServer(t *testing.T) { + stratumServer := NewServer(preLoadedMiniSideChain, func(block *sidechain.PoolBlock) error { + return nil + }) + minerData := getMinerData() + tip := preLoadedMiniSideChain.GetChainTip() + stratumServer.HandleMinerData(minerData) + stratumServer.HandleTip(tip) + + func() { + //Process all incoming changes first + for { + select { + case f := <-stratumServer.incomingChanges: + if f() { + stratumServer.Update() + } + default: + return + } + } + }() + + tpl, _, _, seedHash, err := stratumServer.BuildTemplate(address.FromBase58(types.DonationAddress).ToPackedAddress(), false) + if err != nil { + t.Fatal(err) + } + + if seedHash != minerData.SeedHash { + t.Fatal() + } + + if tpl.MainHeight != minerData.Height { + t.Fatal() + } + + if tpl.MainParent != minerData.PrevId { + t.Fatal() + } + + if tpl.SideHeight != (tip.Side.Height + 1) { + t.Fatal() + } + + if tpl.SideParent != tip.SideTemplateId(preLoadedMiniSideChain.Consensus()) { + t.Fatal() + } +} + +func TestShuffleMapping(t *testing.T) { + const n = 16 + const shareVersion = sidechain.ShareVersion_V2 + var seed = zeroExtraBaseRCTHash + mappings := BuildShuffleMapping(n, shareVersion, seed) + + seq := make([]int, n) + for i := range seq { + seq[i] = i + } + + seq1 := slices.Clone(seq) + + sidechain.ShuffleShares(seq1, shareVersion, seed) + seq2 := ApplyShuffleMapping(seq, mappings) + + if slices.Compare(seq1, seq2) != 0 { + for i := range seq1 { + if seq1[i] != seq2[i] { + t.Logf("%d %d *** @ %d", seq1[i], seq2[i], i) + } else { + t.Logf("%d %d @ %d", seq1[i], seq2[i], i) + } + } + + t.Fatal() + } + +} + +func BenchmarkServer_FillTemplate(b *testing.B) { + stratumServer := NewServer(preLoadedMiniSideChain, func(block *sidechain.PoolBlock) error { + return nil + }) + minerData := getMinerData() + tip := preLoadedMiniSideChain.GetChainTip() + stratumServer.minerData = minerData + stratumServer.tip = tip + + b.ResetTimer() + + b.Run("New", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if err := stratumServer.fillNewTemplateData(minerData.Difficulty); err != nil { + b.Fatal(err) + } + } + b.ReportAllocs() + }) + + b.Run("Cached", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if err := stratumServer.fillNewTemplateData(types.ZeroDifficulty); err != nil { + b.Fatal(err) + } + } + b.ReportAllocs() + }) + +} + +func BenchmarkServer_BuildTemplate(b *testing.B) { + stratumServer := NewServer(preLoadedMiniSideChain, func(block *sidechain.PoolBlock) error { + return nil + }) + minerData := getMinerData() + tip := preLoadedMiniSideChain.GetChainTip() + stratumServer.minerData = minerData + stratumServer.tip = tip + + if err := stratumServer.fillNewTemplateData(minerData.Difficulty); err != nil { + b.Fatal(err) + } + + const randomPoolSize = 512 + var randomKeys [randomPoolSize]address.PackedAddress + + //generate random keys deterministically + for i := range randomKeys { + spendPriv, viewPriv := crypto.DeterministicScalar([]byte(fmt.Sprintf("BenchmarkBuildTemplate_%d_spend", i))), crypto.DeterministicScalar([]byte(fmt.Sprintf("BenchmarkBuildTemplate_%d_spend", i))) + randomKeys[i][0] = (*crypto.PrivateKeyScalar)(spendPriv).PublicKey().AsBytes() + randomKeys[i][1] = (*crypto.PrivateKeyScalar)(viewPriv).PublicKey().AsBytes() + } + + b.ResetTimer() + b.ReportAllocs() + + b.Run("Cached", func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + counter := unsafeRandom.Intn(randomPoolSize) + for pb.Next() { + a := randomKeys[counter%randomPoolSize] + if _, _, _, _, err := stratumServer.BuildTemplate(a, false); err != nil { + b.Fatal(err) + } + counter++ + } + }) + b.ReportAllocs() + }) + + b.Run("Forced", func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + counter := unsafeRandom.Intn(randomPoolSize) + for pb.Next() { + a := randomKeys[counter%randomPoolSize] + if _, _, _, _, err := stratumServer.BuildTemplate(a, true); err != nil { + b.Fatal(err) + } + counter++ + } + }) + b.ReportAllocs() + }) +} diff --git a/p2pool/stratum/template.go b/p2pool/stratum/template.go new file mode 100644 index 0000000..f0c054b --- /dev/null +++ b/p2pool/stratum/template.go @@ -0,0 +1,257 @@ +package stratum + +import ( + "encoding/binary" + "errors" + "git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto" + "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain" + "git.gammaspectra.live/P2Pool/p2pool-observer/types" + "git.gammaspectra.live/P2Pool/p2pool-observer/utils" + "git.gammaspectra.live/P2Pool/sha3" + "io" +) + +type Template struct { + Buffer []byte + + // NonceOffset offset of an uint32 + NonceOffset int + + CoinbaseOffset int + + // ExtraNonceOffset offset of an uint32 + ExtraNonceOffset int + + // TemplateIdOffset offset of a types.Hash + TemplateIdOffset int + + // TransactionsOffset Start of transactions section + TransactionsOffset int + + // TemplateExtraBufferOffset offset of 4*uint32 + TemplateExtraBufferOffset int + + MainHeight uint64 + MainParent types.Hash + + SideHeight uint64 + SideParent types.Hash + SideDifficulty types.Difficulty + + MerkleTreeMainBranch []types.Hash +} + +func (tpl *Template) Write(writer io.Writer, nonce, extraNonce, sideRandomNumber, sideExtraNonce uint32, templateId types.Hash) error { + var uint32Buf [4]byte + + if _, err := writer.Write(tpl.Buffer[:tpl.NonceOffset]); err != nil { + return err + } + + binary.LittleEndian.PutUint32(uint32Buf[:], nonce) + if _, err := writer.Write(uint32Buf[:]); err != nil { + return err + } + + if _, err := writer.Write(tpl.Buffer[tpl.NonceOffset+4 : tpl.ExtraNonceOffset]); err != nil { + return err + } + + binary.LittleEndian.PutUint32(uint32Buf[:], extraNonce) + if _, err := writer.Write(uint32Buf[:]); err != nil { + return err + } + + if _, err := writer.Write(tpl.Buffer[tpl.ExtraNonceOffset+4 : tpl.TemplateIdOffset]); err != nil { + return err + } + if _, err := writer.Write(templateId[:]); err != nil { + return err + } + if _, err := writer.Write(tpl.Buffer[tpl.TemplateIdOffset+types.HashSize : tpl.TemplateExtraBufferOffset+4*2]); err != nil { + return err + } + + binary.LittleEndian.PutUint32(uint32Buf[:], sideRandomNumber) + if _, err := writer.Write(uint32Buf[:]); err != nil { + return err + } + + binary.LittleEndian.PutUint32(uint32Buf[:], sideExtraNonce) + if _, err := writer.Write(uint32Buf[:]); err != nil { + return err + } + + return nil +} + +func (tpl *Template) Blob(preAllocatedBuffer []byte, nonce, extraNonce, sideRandomNumber, sideExtraNonce uint32, templateId types.Hash) []byte { + buf := append(preAllocatedBuffer, tpl.Buffer...) + + // Overwrite nonce + binary.LittleEndian.PutUint32(buf[tpl.NonceOffset:], nonce) + // Overwrite extra nonce + binary.LittleEndian.PutUint32(buf[tpl.ExtraNonceOffset:], extraNonce) + // Overwrite template id + copy(buf[tpl.TemplateIdOffset:], templateId[:]) + // Overwrite sidechain random number + binary.LittleEndian.PutUint32(buf[tpl.TemplateExtraBufferOffset+4*2:], sideRandomNumber) + // Overwrite sidechain extra nonce number + binary.LittleEndian.PutUint32(buf[tpl.TemplateExtraBufferOffset+4*3:], sideExtraNonce) + + return buf +} + +func (tpl *Template) TemplateId(hasher *sha3.HasherState, preAllocatedBuffer []byte, consensus *sidechain.Consensus, sideRandomNumber, sideExtraNonce uint32, result *types.Hash) { + buf := tpl.Blob(preAllocatedBuffer, 0, 0, sideRandomNumber, sideExtraNonce, types.ZeroHash) + + _, _ = hasher.Write(buf) + _, _ = hasher.Write(consensus.Id[:]) + crypto.HashFastSum(hasher, (*result)[:]) + hasher.Reset() +} + +func (tpl *Template) Timestamp() uint64 { + t, _ := binary.Uvarint(tpl.Buffer[2:]) + return t +} + +func (tpl *Template) CoinbaseBufferLength() int { + return tpl.TransactionsOffset - tpl.CoinbaseOffset +} + +func (tpl *Template) CoinbaseBlob(preAllocatedBuffer []byte, extraNonce uint32, templateId types.Hash) []byte { + buf := append(preAllocatedBuffer, tpl.Buffer[tpl.CoinbaseOffset:tpl.TransactionsOffset]...) + + // Overwrite extra nonce + binary.LittleEndian.PutUint32(buf[tpl.ExtraNonceOffset-tpl.CoinbaseOffset:], extraNonce) + // Overwrite template id + copy(buf[tpl.TemplateIdOffset-tpl.CoinbaseOffset:], templateId[:]) + + return buf +} + +func (tpl *Template) CoinbaseBlobId(hasher *sha3.HasherState, preAllocatedBuffer []byte, extraNonce uint32, templateId types.Hash, result *types.Hash) { + + buf := tpl.CoinbaseBlob(preAllocatedBuffer, extraNonce, templateId) + _, _ = hasher.Write(buf[:len(buf)-1]) + crypto.HashFastSum(hasher, (*result)[:]) + hasher.Reset() + + CoinbaseIdHash(hasher, *result, result) +} + +func (tpl *Template) CoinbaseId(hasher *sha3.HasherState, extraNonce uint32, templateId types.Hash, result *types.Hash) { + + var extraNonceBuf [4]byte + + _, _ = hasher.Write(tpl.Buffer[tpl.CoinbaseOffset:tpl.ExtraNonceOffset]) + // extra nonce + binary.LittleEndian.PutUint32(extraNonceBuf[:], extraNonce) + _, _ = hasher.Write(extraNonceBuf[:]) + + _, _ = hasher.Write(tpl.Buffer[tpl.ExtraNonceOffset+4 : tpl.TemplateIdOffset]) + // template id + _, _ = hasher.Write(templateId[:]) + + _, _ = hasher.Write(tpl.Buffer[tpl.TemplateIdOffset+types.HashSize : tpl.TransactionsOffset-1]) + + crypto.HashFastSum(hasher, (*result)[:]) + hasher.Reset() + + CoinbaseIdHash(hasher, *result, result) +} + +var zeroExtraBaseRCTHash = crypto.PooledKeccak256([]byte{0}) + +func CoinbaseIdHash(hasher *sha3.HasherState, coinbaseBlobMinusBaseRTC types.Hash, result *types.Hash) { + _, _ = hasher.Write(coinbaseBlobMinusBaseRTC[:]) + // Base RCT, single 0 byte in miner tx + _, _ = hasher.Write(zeroExtraBaseRCTHash[:]) + // Prunable RCT, empty in miner tx + _, _ = hasher.Write(types.ZeroHash[:]) + crypto.HashFastSum(hasher, (*result)[:]) + hasher.Reset() +} + +func (tpl *Template) HashingBlobBufferLength() int { + _, n := binary.Uvarint(tpl.Buffer[tpl.TransactionsOffset:]) + + return tpl.NonceOffset + 4 + types.HashSize + n +} + +func (tpl *Template) HashingBlob(hasher *sha3.HasherState, preAllocatedBuffer []byte, nonce, extraNonce uint32, templateId types.Hash) []byte { + + var rootHash types.Hash + tpl.CoinbaseId(hasher, extraNonce, templateId, &rootHash) + + buf := append(preAllocatedBuffer, tpl.Buffer[:tpl.NonceOffset]...) + buf = binary.LittleEndian.AppendUint32(buf, nonce) + + numTransactions, n := binary.Uvarint(tpl.Buffer[tpl.TransactionsOffset:]) + + if numTransactions < 1 { + } else if numTransactions < 2 { + _, _ = hasher.Write(rootHash[:]) + _, _ = hasher.Write(tpl.Buffer[tpl.TransactionsOffset+n : tpl.TransactionsOffset+n+types.HashSize]) + crypto.HashFastSum(hasher, rootHash[:]) + hasher.Reset() + } else { + for i := range tpl.MerkleTreeMainBranch { + _, _ = hasher.Write(rootHash[:]) + _, _ = hasher.Write(tpl.MerkleTreeMainBranch[i][:]) + crypto.HashFastSum(hasher, rootHash[:]) + hasher.Reset() + } + } + + buf = append(buf, rootHash[:]...) + buf = binary.AppendUvarint(buf, numTransactions+1) + return buf +} + +func TemplateFromPoolBlock(b *sidechain.PoolBlock) (tpl *Template, err error) { + if b.ShareVersion() < sidechain.ShareVersion_V1 { + return nil, errors.New("unsupported share version") + } + totalLen := b.BufferLength() + buf := make([]byte, 0, b.BufferLength()) + if buf, err = b.AppendBinaryFlags(buf, false, false); err != nil { + return nil, err + } + + tpl = &Template{ + Buffer: buf, + } + + mainBufferLength := b.Main.BufferLength() + coinbaseLength := b.Main.Coinbase.BufferLength() + tpl.NonceOffset = mainBufferLength - (4 + coinbaseLength + utils.UVarInt64Size(len(b.Main.Transactions)) + types.HashSize*len(b.Main.Transactions)) + + tpl.CoinbaseOffset = mainBufferLength - (coinbaseLength + utils.UVarInt64Size(len(b.Main.Transactions)) + types.HashSize*len(b.Main.Transactions)) + + tpl.TransactionsOffset = mainBufferLength - (utils.UVarInt64Size(len(b.Main.Transactions)) + types.HashSize*len(b.Main.Transactions)) + + tpl.ExtraNonceOffset = tpl.NonceOffset + 4 + (coinbaseLength - (b.Main.Coinbase.Extra[1].BufferLength() + b.Main.Coinbase.Extra[2].BufferLength() + 1)) + 1 + utils.UVarInt64Size(b.Main.Coinbase.Extra[1].VarInt) + + tpl.TemplateIdOffset = tpl.NonceOffset + 4 + (coinbaseLength - (b.Main.Coinbase.Extra[2].BufferLength() + 1)) + 1 + utils.UVarInt64Size(b.Main.Coinbase.Extra[2].VarInt) + tpl.TemplateExtraBufferOffset = totalLen - 4*4 + + // Set places to zeroes where necessary + tpl.Buffer = tpl.Blob(make([]byte, 0, len(tpl.Buffer)), 0, 0, 0, 0, types.ZeroHash) + + if len(b.Main.Transactions) > 1 { + merkleTree := make(crypto.BinaryTreeHash, len(b.Main.Transactions)+1) + copy(merkleTree[1:], b.Main.Transactions) + tpl.MerkleTreeMainBranch = merkleTree.MainBranch() + } + + tpl.MainHeight = b.Main.Coinbase.GenHeight + tpl.MainParent = b.Main.PreviousId + + tpl.SideHeight = b.Side.Height + tpl.SideParent = b.Side.Parent + tpl.SideDifficulty = b.Side.Difficulty + + return tpl, nil +} diff --git a/p2pool/stratum/template_test.go b/p2pool/stratum/template_test.go new file mode 100644 index 0000000..051d888 --- /dev/null +++ b/p2pool/stratum/template_test.go @@ -0,0 +1,188 @@ +package stratum + +import ( + "bytes" + "encoding/hex" + "git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto" + "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain" + "git.gammaspectra.live/P2Pool/p2pool-observer/types" + unsafeRandom "math/rand" + "testing" +) + +func TestTemplate(t *testing.T) { + b := preLoadedPoolBlock + buf, _ := b.MarshalBinary() + tpl, err := TemplateFromPoolBlock(b) + if err != nil { + t.Fatal(err) + } + + preAllocatedBuffer := make([]byte, 0, len(tpl.Buffer)) + + blockTemplateId := types.HashFromBytes(b.CoinbaseExtra(sidechain.SideTemplateId)) + + if tplBuf := tpl.Blob(preAllocatedBuffer, b.Main.Nonce, b.ExtraNonce(), b.Side.ExtraBuffer.RandomNumber, b.Side.ExtraBuffer.SideChainExtraNonce, blockTemplateId); bytes.Compare(tplBuf, buf) != 0 { + if len(tplBuf) == len(buf) { + for i := range buf { + if buf[i] != tplBuf[i] { + t.Logf("%s %s *** @ %d", hex.EncodeToString(buf[i:i+1]), hex.EncodeToString(tplBuf[i:i+1]), i) + } else { + t.Logf("%s %s @ %d", hex.EncodeToString(buf[i:i+1]), hex.EncodeToString(tplBuf[i:i+1]), i) + } + } + } + t.Fatal("not matching blob buffers") + } + + writer := bytes.NewBuffer(nil) + + if err := tpl.Write(writer, b.Main.Nonce, b.ExtraNonce(), b.Side.ExtraBuffer.RandomNumber, b.Side.ExtraBuffer.SideChainExtraNonce, blockTemplateId); err != nil { + t.Fatal(err) + } else if bytes.Compare(writer.Bytes(), buf) != 0 { + t.Fatal("not matching writer buffers") + } + + hasher := crypto.GetKeccak256Hasher() + defer crypto.PutKeccak256Hasher(hasher) + + bHashingBlob := b.Main.HashingBlob(nil) + if tplHashingBlob := tpl.HashingBlob(hasher, preAllocatedBuffer, b.Main.Nonce, b.ExtraNonce(), blockTemplateId); bytes.Compare(tplHashingBlob, bHashingBlob) != 0 { + if len(tplHashingBlob) == len(bHashingBlob) { + for i := range buf { + if bHashingBlob[i] != tplHashingBlob[i] { + t.Logf("%s %s *** @ %d", hex.EncodeToString(bHashingBlob[i:i+1]), hex.EncodeToString(tplHashingBlob[i:i+1]), i) + } else { + t.Logf("%s %s @ %d", hex.EncodeToString(bHashingBlob[i:i+1]), hex.EncodeToString(tplHashingBlob[i:i+1]), i) + } + } + } + t.Fatal("not matching hashing blob buffers") + } + + bCoinbaseBlob, _ := b.Main.Coinbase.MarshalBinary() + if tplCoinbaseBlob := tpl.CoinbaseBlob(preAllocatedBuffer, b.ExtraNonce(), blockTemplateId); bytes.Compare(tplCoinbaseBlob, bCoinbaseBlob) != 0 { + if len(tplCoinbaseBlob) == len(bCoinbaseBlob) { + for i := range buf { + if bCoinbaseBlob[i] != tplCoinbaseBlob[i] { + t.Logf("%s %s *** @ %d", hex.EncodeToString(bCoinbaseBlob[i:i+1]), hex.EncodeToString(tplCoinbaseBlob[i:i+1]), i) + } else { + t.Logf("%s %s @ %d", hex.EncodeToString(bCoinbaseBlob[i:i+1]), hex.EncodeToString(tplCoinbaseBlob[i:i+1]), i) + } + } + } + t.Fatal("not matching coinbase blob buffers") + } + + var coinbaseId types.Hash + + if tpl.CoinbaseId(hasher, b.ExtraNonce(), blockTemplateId, &coinbaseId); coinbaseId != b.Main.Coinbase.Id() { + t.Fatal("different coinbase ids") + } + + if tpl.CoinbaseBlobId(hasher, preAllocatedBuffer, b.ExtraNonce(), blockTemplateId, &coinbaseId); coinbaseId != b.Main.Coinbase.Id() { + t.Fatal("different coinbase blob ids") + } + + var templateId types.Hash + if tpl.TemplateId(hasher, preAllocatedBuffer, sidechain.ConsensusDefault, b.Side.ExtraBuffer.RandomNumber, b.Side.ExtraBuffer.SideChainExtraNonce, &templateId); templateId != blockTemplateId { + t.Fatal("different template ids") + } + + if tpl.Timestamp() != b.Main.Timestamp { + t.Fatal("different timestamps") + } + + if tpl.HashingBlobBufferLength() != b.Main.HashingBlobBufferLength() { + t.Fatal("different hashing blob buffer length") + } + + if tpl.HashingBlobBufferLength() != len(tpl.HashingBlob(hasher, preAllocatedBuffer, 0, 0, types.ZeroHash)) { + t.Fatal("different hashing blob buffer length from blob") + } + +} + +func BenchmarkTemplate_CoinbaseId(b *testing.B) { + tpl, err := TemplateFromPoolBlock(preLoadedPoolBlock) + if err != nil { + b.Fatal(err) + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + hasher := crypto.GetKeccak256Hasher() + defer crypto.PutKeccak256Hasher(hasher) + var counter = unsafeRandom.Uint32() + var coinbaseId types.Hash + for pb.Next() { + tpl.CoinbaseId(hasher, counter, types.ZeroHash, &coinbaseId) + counter++ + } + }) + b.ReportAllocs() +} + +func BenchmarkTemplate_CoinbaseBlobId(b *testing.B) { + tpl, err := TemplateFromPoolBlock(preLoadedPoolBlock) + if err != nil { + b.Fatal(err) + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + preAllocatedBuffer := make([]byte, 0, tpl.CoinbaseBufferLength()) + hasher := crypto.GetKeccak256Hasher() + defer crypto.PutKeccak256Hasher(hasher) + var counter = unsafeRandom.Uint32() + var coinbaseId types.Hash + for pb.Next() { + tpl.CoinbaseBlobId(hasher, preAllocatedBuffer, counter, types.ZeroHash, &coinbaseId) + counter++ + } + }) + b.ReportAllocs() +} + +func BenchmarkTemplate_HashingBlob(b *testing.B) { + tpl, err := TemplateFromPoolBlock(preLoadedPoolBlock) + if err != nil { + b.Fatal(err) + } + + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + preAllocatedBuffer := make([]byte, 0, tpl.HashingBlobBufferLength()) + hasher := crypto.GetKeccak256Hasher() + defer crypto.PutKeccak256Hasher(hasher) + var counter = unsafeRandom.Uint32() + for pb.Next() { + tpl.HashingBlob(hasher, preAllocatedBuffer, counter, counter, types.ZeroHash) + counter++ + } + }) + b.ReportAllocs() +} + +func BenchmarkTemplate_TemplateId(b *testing.B) { + tpl, err := TemplateFromPoolBlock(preLoadedPoolBlock) + if err != nil { + b.Fatal(err) + } + + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + preAllocatedBuffer := make([]byte, 0, len(tpl.Buffer)) + hasher := crypto.GetKeccak256Hasher() + defer crypto.PutKeccak256Hasher(hasher) + var counter = unsafeRandom.Uint32() + var templateId types.Hash + for pb.Next() { + tpl.TemplateId(hasher, preAllocatedBuffer, sidechain.ConsensusDefault, counter, counter+1, &templateId) + counter++ + } + }) + b.ReportAllocs() +}