git.gammaspectra.live/P2Pool/consensus/v3@v3.8.0/p2pool/stratum/stratum.go (about) 1 package stratum 2 3 import ( 4 "encoding/binary" 5 "errors" 6 "fmt" 7 "git.gammaspectra.live/P2Pool/consensus/v3/monero" 8 "git.gammaspectra.live/P2Pool/consensus/v3/monero/address" 9 "git.gammaspectra.live/P2Pool/consensus/v3/monero/block" 10 "git.gammaspectra.live/P2Pool/consensus/v3/monero/crypto" 11 "git.gammaspectra.live/P2Pool/consensus/v3/monero/transaction" 12 "git.gammaspectra.live/P2Pool/consensus/v3/p2pool/mempool" 13 "git.gammaspectra.live/P2Pool/consensus/v3/p2pool/sidechain" 14 p2pooltypes "git.gammaspectra.live/P2Pool/consensus/v3/p2pool/types" 15 "git.gammaspectra.live/P2Pool/consensus/v3/types" 16 "git.gammaspectra.live/P2Pool/consensus/v3/utils" 17 gojson "git.gammaspectra.live/P2Pool/go-json" 18 "github.com/dolthub/swiss" 19 fasthex "github.com/tmthrgd/go-hex" 20 "math" 21 unsafeRandom "math/rand/v2" 22 "net" 23 "net/netip" 24 "slices" 25 "sync" 26 "time" 27 ) 28 29 // HighFeeValue 0.006 XMR 30 const HighFeeValue uint64 = 6000000000 31 const TimeInMempool = time.Second * 5 32 33 type ephemeralPubKeyCacheKey [crypto.PublicKeySize*2 + 8]byte 34 35 type ephemeralPubKeyCacheEntry struct { 36 PublicKey crypto.PublicKeyBytes 37 ViewTag uint8 38 } 39 40 type NewTemplateData struct { 41 PreviousTemplateId types.Hash 42 SideHeight uint64 43 Difficulty types.Difficulty 44 CumulativeDifficulty types.Difficulty 45 TransactionPrivateKeySeed types.Hash 46 // TransactionPrivateKey Generated from TransactionPrivateKeySeed 47 TransactionPrivateKey crypto.PrivateKeyBytes 48 TransactionPublicKey crypto.PublicKeySlice 49 Timestamp uint64 50 TotalReward uint64 51 Transactions []types.Hash 52 MaxRewardAmountsWeight uint64 53 ShareVersion sidechain.ShareVersion 54 Uncles []types.Hash 55 Ready bool 56 Window struct { 57 ReservedShareIndex int 58 Shares sidechain.Shares 59 ShuffleMapping [2][]int 60 EphemeralPubKeyCache map[ephemeralPubKeyCacheKey]*ephemeralPubKeyCacheEntry 61 } 62 } 63 64 type Server struct { 65 SubmitFunc func(block *sidechain.PoolBlock) error 66 67 refreshDuration time.Duration 68 69 minerData *p2pooltypes.MinerData 70 tip *sidechain.PoolBlock 71 newTemplateData NewTemplateData 72 lock sync.RWMutex 73 sidechain *sidechain.SideChain 74 75 mempool *MiningMempool 76 lastMempoolRefresh time.Time 77 78 preAllocatedDifficultyData []sidechain.DifficultyData 79 preAllocatedDifficultyDifferences []uint32 80 preAllocatedSharesPool *sidechain.PreAllocatedSharesPool 81 82 preAllocatedBufferLock sync.Mutex 83 preAllocatedBuffer []byte 84 85 minersLock sync.RWMutex 86 miners map[address.PackedAddress]*MinerTrackingEntry 87 88 bansLock sync.RWMutex 89 bans map[[16]byte]BanEntry 90 91 clientsLock sync.RWMutex 92 clients []*Client 93 94 incomingChanges chan func() bool 95 } 96 97 type Client struct { 98 Lock sync.RWMutex 99 Conn *net.TCPConn 100 encoder *gojson.Encoder 101 decoder *gojson.Decoder 102 Agent string 103 Login bool 104 Address address.PackedAddress 105 Password string 106 RigId string 107 buf []byte 108 RpcId uint32 109 } 110 111 func (c *Client) Write(b []byte) (int, error) { 112 if err := c.Conn.SetWriteDeadline(time.Now().Add(time.Second * 5)); err != nil { 113 return 0, err 114 } 115 return c.Conn.Write(b) 116 } 117 118 func NewServer(s *sidechain.SideChain, submitFunc func(block *sidechain.PoolBlock) error) *Server { 119 server := &Server{ 120 SubmitFunc: submitFunc, 121 sidechain: s, 122 preAllocatedDifficultyData: make([]sidechain.DifficultyData, s.Consensus().ChainWindowSize*2), 123 preAllocatedDifficultyDifferences: make([]uint32, s.Consensus().ChainWindowSize*2), 124 preAllocatedSharesPool: sidechain.NewPreAllocatedSharesPool(s.Consensus().ChainWindowSize * 2), 125 preAllocatedBuffer: make([]byte, 0, sidechain.PoolBlockMaxTemplateSize), 126 miners: make(map[address.PackedAddress]*MinerTrackingEntry), 127 mempool: (*MiningMempool)(swiss.NewMap[types.Hash, *mempool.Entry](512)), 128 // buffer 4 at a time for non-blocking source 129 incomingChanges: make(chan func() bool, 4), 130 131 //refresh every n seconds 132 refreshDuration: time.Duration(s.Consensus().TargetBlockTime) * time.Second, 133 } 134 return server 135 } 136 137 func (s *Server) CleanupMiners() { 138 s.minersLock.Lock() 139 defer s.minersLock.Unlock() 140 141 cleanupTime := time.Now() 142 for k, e := range s.miners { 143 if cleanupTime.Sub(e.LastJob) > time.Minute*5 { 144 delete(s.miners, k) 145 } else { 146 if len(e.Templates) > 0 { 147 var templateSideHeight uint64 148 for _, tpl := range e.Templates { 149 if tpl.SideHeight > templateSideHeight { 150 templateSideHeight = tpl.SideHeight 151 } 152 } 153 154 tipHeight := uint64(0) 155 if templateSideHeight > sidechain.UncleBlockDepth { 156 tipHeight = templateSideHeight - sidechain.UncleBlockDepth + 1 157 } 158 //Delete old templates further than uncle depth 159 for key, tpl := range e.Templates { 160 if tpl.SideHeight < tipHeight { 161 delete(e.Templates, key) 162 } 163 } 164 165 // TODO: Prevent long-term leaks by re-allocating templates if capacity grew too much 166 } 167 } 168 } 169 } 170 171 func (s *Server) fillNewTemplateData(currentDifficulty types.Difficulty) error { 172 173 s.newTemplateData.Ready = false 174 175 if s.minerData == nil { 176 return errors.New("no main data present") 177 } 178 179 if s.minerData.MajorVersion > monero.HardForkSupportedVersion { 180 return fmt.Errorf("unsupported hardfork version %d", s.minerData.MajorVersion) 181 } 182 183 oldPubKeyCache := s.newTemplateData.Window.EphemeralPubKeyCache 184 185 s.newTemplateData.Timestamp = uint64(time.Now().Unix()) 186 187 s.newTemplateData.ShareVersion = sidechain.P2PoolShareVersion(s.sidechain.Consensus(), s.newTemplateData.Timestamp) 188 189 // Do not allow mining on old chains, as they are not optimal for CPU usage 190 if s.newTemplateData.ShareVersion < sidechain.ShareVersion_V2 { 191 return errors.New("unsupported sidechain version") 192 } 193 // no merge mining nor merkle proof support yet 194 if s.newTemplateData.ShareVersion > sidechain.ShareVersion_V2 { 195 return errors.New("unsupported sidechain version") 196 } 197 198 if s.tip != nil { 199 s.newTemplateData.PreviousTemplateId = s.tip.SideTemplateId(s.sidechain.Consensus()) 200 s.newTemplateData.SideHeight = s.tip.Side.Height + 1 201 202 oldSeed := s.newTemplateData.TransactionPrivateKeySeed 203 204 s.newTemplateData.TransactionPrivateKeySeed = s.tip.Side.CoinbasePrivateKeySeed 205 if s.tip.Main.PreviousId != s.minerData.PrevId { 206 s.newTemplateData.TransactionPrivateKeySeed = s.tip.CalculateTransactionPrivateKeySeed() 207 } 208 209 //TODO: check this 210 if s.newTemplateData.TransactionPrivateKeySeed != oldSeed { 211 oldPubKeyCache = nil 212 } 213 214 if currentDifficulty != types.ZeroDifficulty { 215 //difficulty is set from caller 216 s.newTemplateData.Difficulty = currentDifficulty 217 oldPubKeyCache = nil 218 } 219 220 s.newTemplateData.CumulativeDifficulty = s.tip.Side.CumulativeDifficulty.Add(s.newTemplateData.Difficulty) 221 222 s.newTemplateData.Uncles = s.sidechain.GetPossibleUncles(s.tip, s.newTemplateData.SideHeight) 223 } else { 224 s.newTemplateData.PreviousTemplateId = types.ZeroHash 225 s.newTemplateData.TransactionPrivateKeySeed = s.sidechain.Consensus().Id 226 s.newTemplateData.Difficulty = types.DifficultyFrom64(s.sidechain.Consensus().MinimumDifficulty) 227 s.newTemplateData.CumulativeDifficulty = types.DifficultyFrom64(s.sidechain.Consensus().MinimumDifficulty) 228 } 229 230 kP := s.sidechain.DerivationCache().GetDeterministicTransactionKey(s.newTemplateData.TransactionPrivateKeySeed, s.minerData.PrevId) 231 s.newTemplateData.TransactionPrivateKey = kP.PrivateKey.AsBytes() 232 s.newTemplateData.TransactionPublicKey = kP.PublicKey.AsSlice() 233 234 fakeTemplateTipBlock := &sidechain.PoolBlock{ 235 Main: block.Block{ 236 MajorVersion: s.minerData.MajorVersion, 237 MinorVersion: monero.HardForkSupportedVersion, 238 Timestamp: s.newTemplateData.Timestamp, 239 PreviousId: s.minerData.PrevId, 240 Nonce: 0, 241 Coinbase: transaction.CoinbaseTransaction{ 242 GenHeight: s.minerData.Height, 243 }, 244 //TODO: 245 Transactions: nil, 246 }, 247 Side: sidechain.SideData{ 248 //Zero Spend/View key 249 PublicKey: address.PackedAddress{}, 250 CoinbasePrivateKeySeed: s.newTemplateData.TransactionPrivateKeySeed, 251 CoinbasePrivateKey: s.newTemplateData.TransactionPrivateKey, 252 Parent: s.newTemplateData.PreviousTemplateId, 253 Uncles: s.newTemplateData.Uncles, 254 Height: s.newTemplateData.SideHeight, 255 Difficulty: s.newTemplateData.Difficulty, 256 CumulativeDifficulty: s.newTemplateData.CumulativeDifficulty, 257 }, 258 Metadata: sidechain.PoolBlockReceptionMetadata{ 259 LocalTime: time.Now().UTC(), 260 }, 261 CachedShareVersion: s.newTemplateData.ShareVersion, 262 } 263 264 preAllocatedShares := s.preAllocatedSharesPool.Get() 265 defer s.preAllocatedSharesPool.Put(preAllocatedShares) 266 shares, _ := sidechain.GetSharesOrdered(fakeTemplateTipBlock, s.sidechain.Consensus(), s.sidechain.Server().GetDifficultyByHeight, s.sidechain.GetPoolBlockByTemplateId, preAllocatedShares) 267 268 if shares == nil { 269 return errors.New("could not get outputs") 270 } 271 272 s.newTemplateData.Window.Shares = slices.Clone(shares) 273 s.newTemplateData.Window.ReservedShareIndex = s.newTemplateData.Window.Shares.Index(fakeTemplateTipBlock.GetAddress()) 274 275 if s.newTemplateData.Window.ReservedShareIndex == -1 { 276 return errors.New("could not find reserved share index") 277 } 278 279 // Only choose transactions that were received 5 or more seconds ago, or high fee (>= 0.006 XMR) transactions 280 selectedMempool := s.mempool.Select(HighFeeValue, TimeInMempool) 281 282 //TODO: limit max Monero block size 283 284 baseReward := block.GetBaseReward(s.minerData.AlreadyGeneratedCoins) 285 286 totalWeight, totalFees := selectedMempool.WeightAndFees() 287 288 maxReward := baseReward + totalFees 289 290 rewards := sidechain.SplitRewardAllocate(maxReward, s.newTemplateData.Window.Shares) 291 292 s.newTemplateData.MaxRewardAmountsWeight = uint64(utils.UVarInt64SliceSize(rewards)) 293 294 tx, err := s.createCoinbaseTransaction(fakeTemplateTipBlock.GetTransactionOutputType(), s.newTemplateData.Window.Shares, rewards, s.newTemplateData.MaxRewardAmountsWeight, false) 295 if err != nil { 296 return err 297 } 298 coinbaseTransactionWeight := uint64(tx.BufferLength()) 299 300 var pickedMempool mempool.Mempool 301 302 if totalWeight+coinbaseTransactionWeight <= s.minerData.MedianWeight { 303 // if a block doesn't get into the penalty zone, just pick all transactions 304 pickedMempool = selectedMempool 305 } else { 306 pickedMempool = selectedMempool.Pick(baseReward, coinbaseTransactionWeight, s.minerData.MedianWeight) 307 } 308 309 //shuffle transactions 310 unsafeRandom.Shuffle(len(pickedMempool), func(i, j int) { 311 pickedMempool[i], pickedMempool[j] = pickedMempool[j], pickedMempool[i] 312 }) 313 314 s.newTemplateData.Transactions = make([]types.Hash, len(pickedMempool)) 315 316 for i, entry := range pickedMempool { 317 s.newTemplateData.Transactions[i] = entry.Id 318 } 319 320 finalReward := mempool.GetBlockReward(baseReward, s.minerData.MedianWeight, pickedMempool.Fees(), coinbaseTransactionWeight+pickedMempool.Weight()) 321 322 if finalReward < baseReward { 323 return errors.New("final reward < base reward, should never happen") 324 } 325 s.newTemplateData.TotalReward = finalReward 326 327 s.newTemplateData.Window.ShuffleMapping = BuildShuffleMapping(len(s.newTemplateData.Window.Shares), s.newTemplateData.ShareVersion, s.newTemplateData.TransactionPrivateKeySeed) 328 329 s.newTemplateData.Window.EphemeralPubKeyCache = make(map[ephemeralPubKeyCacheKey]*ephemeralPubKeyCacheEntry) 330 331 txPrivateKeySlice := s.newTemplateData.TransactionPrivateKey.AsSlice() 332 txPrivateKeyScalar := s.newTemplateData.TransactionPrivateKey.AsScalar() 333 334 //TODO: parallelize this 335 hasher := crypto.GetKeccak256Hasher() 336 defer crypto.PutKeccak256Hasher(hasher) 337 for i, m := range PossibleIndicesForShuffleMapping(s.newTemplateData.Window.ShuffleMapping) { 338 if i == 0 { 339 // Skip zero key 340 continue 341 } 342 share := s.newTemplateData.Window.Shares[i] 343 var k ephemeralPubKeyCacheKey 344 copy(k[:], share.Address.Bytes()) 345 346 for _, index := range m { 347 if index == -1 { 348 continue 349 } 350 binary.LittleEndian.PutUint64(k[crypto.PublicKeySize*2:], uint64(index)) 351 if e, ok := oldPubKeyCache[k]; ok { 352 s.newTemplateData.Window.EphemeralPubKeyCache[k] = e 353 } else { 354 var e ephemeralPubKeyCacheEntry 355 e.PublicKey, e.ViewTag = s.sidechain.DerivationCache().GetEphemeralPublicKey(&share.Address, txPrivateKeySlice, txPrivateKeyScalar, uint64(index), hasher) 356 s.newTemplateData.Window.EphemeralPubKeyCache[k] = &e 357 } 358 } 359 } 360 361 s.newTemplateData.Ready = true 362 363 return nil 364 365 } 366 367 func BuildShuffleMapping(n int, shareVersion sidechain.ShareVersion, transactionPrivateKeySeed types.Hash) (mappings [2][]int) { 368 if n <= 1 { 369 return [2][]int{{0}, {0}} 370 } 371 shuffleSequence1 := make([]int, n) 372 for i := range shuffleSequence1 { 373 shuffleSequence1[i] = i 374 } 375 shuffleSequence2 := make([]int, n-1) 376 for i := range shuffleSequence2 { 377 shuffleSequence2[i] = i 378 } 379 380 sidechain.ShuffleSequence(shareVersion, transactionPrivateKeySeed, n, func(i, j int) { 381 shuffleSequence1[i], shuffleSequence1[j] = shuffleSequence1[j], shuffleSequence1[i] 382 }) 383 sidechain.ShuffleSequence(shareVersion, transactionPrivateKeySeed, n-1, func(i, j int) { 384 shuffleSequence2[i], shuffleSequence2[j] = shuffleSequence2[j], shuffleSequence2[i] 385 }) 386 387 mappings[0] = make([]int, n) 388 mappings[1] = make([]int, n-1) 389 390 //Flip 391 for i := range shuffleSequence1 { 392 mappings[0][shuffleSequence1[i]] = i 393 } 394 for i := range shuffleSequence2 { 395 mappings[1][shuffleSequence2[i]] = i 396 } 397 398 return mappings 399 } 400 401 func ApplyShuffleMapping[T any](v []T, mappings [2][]int) []T { 402 n := len(v) 403 404 result := make([]T, n) 405 406 if n == len(mappings[0]) { 407 for i := range v { 408 result[mappings[0][i]] = v[i] 409 } 410 } else if n == len(mappings[1]) { 411 for i := range v { 412 result[mappings[1][i]] = v[i] 413 } 414 } 415 return result 416 } 417 418 func PossibleIndicesForShuffleMapping(mappings [2][]int) [][3]int { 419 n := len(mappings[0]) 420 result := make([][3]int, n) 421 for i := 0; i < n; i++ { 422 // Count with all + miner 423 result[i][0] = mappings[0][i] 424 if i > 0 { 425 // Count with all + miner shifted to a slot before 426 result[i][1] = mappings[0][i-1] 427 428 // Count with all miners minus one 429 result[i][2] = mappings[1][i-1] 430 } else { 431 result[i][1] = -1 432 result[i][2] = -1 433 } 434 } 435 436 return result 437 } 438 439 func (s *Server) BuildTemplate(addr address.PackedAddress, forceNewTemplate bool) (tpl *Template, jobCounter uint64, difficultyTarget types.Difficulty, seedHash types.Hash, err error) { 440 441 var zeroAddress address.PackedAddress 442 if addr == zeroAddress { 443 return nil, 0, types.ZeroDifficulty, types.ZeroHash, errors.New("nil address") 444 } 445 446 e, ok := func() (*MinerTrackingEntry, bool) { 447 s.minersLock.RLock() 448 defer s.minersLock.RUnlock() 449 e, ok := s.miners[addr] 450 return e, ok 451 }() 452 453 tpl, jobCounter, targetDiff, seedHash, err := func() (tpl *Template, jobCounter uint64, difficultyTarget types.Difficulty, seedHash types.Hash, err error) { 454 s.lock.RLock() 455 defer s.lock.RUnlock() 456 457 if s.minerData == nil { 458 return nil, 0, types.ZeroDifficulty, types.ZeroHash, errors.New("nil miner data") 459 } 460 461 if !s.newTemplateData.Ready { 462 return nil, 0, types.ZeroDifficulty, types.ZeroHash, errors.New("template data not ready") 463 } 464 465 if !forceNewTemplate && e != nil && ok { 466 if tpl, jobCounter := func() (*Template, uint64) { 467 e.Lock.RLock() 468 defer e.Lock.RUnlock() 469 470 jobCounter := e.LastTemplate.Load() 471 472 if tpl, ok := e.Templates[jobCounter]; ok && tpl.SideParent == s.newTemplateData.PreviousTemplateId && tpl.MainParent == s.minerData.PrevId { 473 return tpl, jobCounter 474 } 475 return nil, 0 476 }(); tpl != nil { 477 e.Lock.Lock() 478 defer e.Lock.Unlock() 479 e.LastJob = time.Now() 480 481 targetDiff := tpl.SideDifficulty 482 if s.minerData.Difficulty.Cmp(targetDiff) < 0 { 483 targetDiff = s.minerData.Difficulty 484 } 485 486 return tpl, jobCounter, targetDiff, s.minerData.SeedHash, nil 487 } 488 } 489 490 blockTemplate := &sidechain.PoolBlock{ 491 Main: block.Block{ 492 MajorVersion: s.minerData.MajorVersion, 493 MinorVersion: monero.HardForkSupportedVersion, 494 Timestamp: s.newTemplateData.Timestamp, 495 PreviousId: s.minerData.PrevId, 496 Nonce: 0, 497 Transactions: s.newTemplateData.Transactions, 498 }, 499 Side: sidechain.SideData{ 500 PublicKey: addr, 501 CoinbasePrivateKeySeed: s.newTemplateData.TransactionPrivateKeySeed, 502 CoinbasePrivateKey: s.newTemplateData.TransactionPrivateKey, 503 Parent: s.newTemplateData.PreviousTemplateId, 504 Uncles: s.newTemplateData.Uncles, 505 Height: s.newTemplateData.SideHeight, 506 Difficulty: s.newTemplateData.Difficulty, 507 CumulativeDifficulty: s.newTemplateData.CumulativeDifficulty, 508 ExtraBuffer: sidechain.SideDataExtraBuffer{ 509 SoftwareId: p2pooltypes.CurrentSoftwareId, 510 SoftwareVersion: p2pooltypes.CurrentSoftwareVersion, 511 RandomNumber: 0, 512 SideChainExtraNonce: 0, 513 }, 514 }, 515 CachedShareVersion: s.newTemplateData.ShareVersion, 516 } 517 518 preAllocatedShares := s.preAllocatedSharesPool.Get() 519 defer s.preAllocatedSharesPool.Put(preAllocatedShares) 520 521 shares := s.newTemplateData.Window.Shares.Clone() 522 523 // It exists, replace 524 if i := shares.Index(addr); i != -1 { 525 shares[i] = &sidechain.Share{ 526 Address: addr, 527 Weight: shares[i].Weight.Add(shares[s.newTemplateData.Window.ReservedShareIndex].Weight), 528 } 529 shares = slices.Delete(shares, s.newTemplateData.Window.ReservedShareIndex, s.newTemplateData.Window.ReservedShareIndex+1) 530 } else { 531 // Replace reserved address 532 shares[s.newTemplateData.Window.ReservedShareIndex] = &sidechain.Share{ 533 Weight: shares[s.newTemplateData.Window.ReservedShareIndex].Weight, 534 Address: addr, 535 } 536 } 537 shares = shares.Compact() 538 539 // Apply consensus shuffle 540 shares = ApplyShuffleMapping(shares, s.newTemplateData.Window.ShuffleMapping) 541 542 // Allocate rewards 543 { 544 preAllocatedRewards := make([]uint64, 0, len(shares)) 545 rewards := sidechain.SplitReward(preAllocatedRewards, s.newTemplateData.TotalReward, shares) 546 547 if rewards == nil || len(rewards) != len(shares) { 548 return nil, 0, types.ZeroDifficulty, types.ZeroHash, errors.New("could not calculate rewards") 549 } 550 551 if blockTemplate.Main.Coinbase, err = s.createCoinbaseTransaction(blockTemplate.GetTransactionOutputType(), shares, rewards, s.newTemplateData.MaxRewardAmountsWeight, true); err != nil { 552 return nil, 0, types.ZeroDifficulty, types.ZeroHash, err 553 } 554 } 555 556 tpl, err = TemplateFromPoolBlock(blockTemplate) 557 if err != nil { 558 return nil, 0, types.ZeroDifficulty, types.ZeroHash, err 559 } 560 561 targetDiff := tpl.SideDifficulty 562 if s.minerData.Difficulty.Cmp(targetDiff) < 0 { 563 targetDiff = s.minerData.Difficulty 564 } 565 566 return tpl, 0, targetDiff, s.minerData.SeedHash, nil 567 }() 568 569 if err != nil { 570 return nil, 0, types.ZeroDifficulty, types.ZeroHash, err 571 } 572 573 if forceNewTemplate || jobCounter != 0 { 574 return tpl, jobCounter, targetDiff, seedHash, nil 575 } 576 577 if e != nil && ok { 578 e.Lock.Lock() 579 defer e.Lock.Unlock() 580 var newJobCounter uint64 581 for newJobCounter == 0 { 582 newJobCounter = e.Counter.Add(1) 583 } 584 e.Templates[newJobCounter] = tpl 585 e.LastTemplate.Store(newJobCounter) 586 e.LastJob = time.Now() 587 return tpl, newJobCounter, targetDiff, seedHash, nil 588 } else { 589 s.minersLock.Lock() 590 defer s.minersLock.Unlock() 591 592 e = &MinerTrackingEntry{ 593 Templates: make(map[uint64]*Template), 594 } 595 var newJobCounter uint64 596 for newJobCounter == 0 { 597 newJobCounter = e.Counter.Add(1) 598 } 599 e.Templates[newJobCounter] = tpl 600 e.LastTemplate.Store(newJobCounter) 601 e.LastJob = time.Now() 602 s.miners[addr] = e 603 return tpl, newJobCounter, targetDiff, seedHash, nil 604 } 605 } 606 607 func (s *Server) createCoinbaseTransaction(txType uint8, shares sidechain.Shares, rewards []uint64, maxRewardsAmountsWeight uint64, final bool) (tx transaction.CoinbaseTransaction, err error) { 608 609 //TODO: v3 610 mergeMineTag := slices.Clone(types.ZeroHash[:]) 611 612 tx = transaction.CoinbaseTransaction{ 613 Version: 2, 614 UnlockTime: s.minerData.Height + monero.MinerRewardUnlockTime, 615 InputCount: 1, 616 InputType: transaction.TxInGen, 617 GenHeight: s.minerData.Height, 618 AuxiliaryData: transaction.CoinbaseTransactionAuxiliaryData{ 619 TotalReward: func() (v uint64) { 620 for i := range rewards { 621 v += rewards[i] 622 } 623 return 624 }(), 625 }, 626 Extra: transaction.ExtraTags{ 627 transaction.ExtraTag{ 628 Tag: transaction.TxExtraTagPubKey, 629 VarInt: 0, 630 Data: types.Bytes(s.newTemplateData.TransactionPublicKey), 631 }, 632 transaction.ExtraTag{ 633 Tag: transaction.TxExtraTagNonce, 634 VarInt: sidechain.SideExtraNonceSize, 635 HasVarInt: true, 636 Data: make(types.Bytes, sidechain.SideExtraNonceSize), 637 }, 638 transaction.ExtraTag{ 639 //TODO: fix this for V3 640 Tag: transaction.TxExtraTagMergeMining, 641 VarInt: uint64(len(mergeMineTag)), 642 HasVarInt: true, 643 Data: mergeMineTag, 644 }, 645 }, 646 ExtraBaseRCT: 0, 647 } 648 649 tx.Outputs = make(transaction.Outputs, len(shares)) 650 651 if final { 652 txPrivateKeySlice := s.newTemplateData.TransactionPrivateKey.AsSlice() 653 txPrivateKeyScalar := s.newTemplateData.TransactionPrivateKey.AsScalar() 654 655 hasher := crypto.GetKeccak256Hasher() 656 defer crypto.PutKeccak256Hasher(hasher) 657 658 var k ephemeralPubKeyCacheKey 659 for i := range tx.Outputs { 660 outputIndex := uint64(i) 661 tx.Outputs[outputIndex].Index = outputIndex 662 tx.Outputs[outputIndex].Type = txType 663 tx.Outputs[outputIndex].Reward = rewards[outputIndex] 664 copy(k[:], shares[outputIndex].Address.Bytes()) 665 binary.LittleEndian.PutUint64(k[crypto.PublicKeySize*2:], outputIndex) 666 if e, ok := s.newTemplateData.Window.EphemeralPubKeyCache[k]; ok { 667 tx.Outputs[outputIndex].EphemeralPublicKey, tx.Outputs[outputIndex].ViewTag = e.PublicKey, e.ViewTag 668 } else { 669 tx.Outputs[outputIndex].EphemeralPublicKey, tx.Outputs[outputIndex].ViewTag = s.sidechain.DerivationCache().GetEphemeralPublicKey(&shares[outputIndex].Address, txPrivateKeySlice, txPrivateKeyScalar, outputIndex, hasher) 670 } 671 } 672 } else { 673 for i := range tx.Outputs { 674 outputIndex := uint64(i) 675 tx.Outputs[outputIndex].Index = outputIndex 676 tx.Outputs[outputIndex].Type = txType 677 tx.Outputs[outputIndex].Reward = rewards[outputIndex] 678 } 679 } 680 681 rewardAmountsWeight := uint64(utils.UVarInt64SliceSize(rewards)) 682 683 if !final { 684 if rewardAmountsWeight != maxRewardsAmountsWeight { 685 return transaction.CoinbaseTransaction{}, fmt.Errorf("incorrect miner rewards during the dry run, %d != %d", rewardAmountsWeight, maxRewardsAmountsWeight) 686 } 687 } else if rewardAmountsWeight > maxRewardsAmountsWeight { 688 return transaction.CoinbaseTransaction{}, fmt.Errorf("incorrect miner rewards during the dry run, %d > %d", rewardAmountsWeight, maxRewardsAmountsWeight) 689 } 690 691 correctedExtraNonceSize := sidechain.SideExtraNonceSize + maxRewardsAmountsWeight - rewardAmountsWeight 692 693 if correctedExtraNonceSize > sidechain.SideExtraNonceSize { 694 if correctedExtraNonceSize > sidechain.SideExtraNonceMaxSize { 695 return transaction.CoinbaseTransaction{}, fmt.Errorf("corrected extra_nonce size is too large, %d > %d", correctedExtraNonceSize, sidechain.SideExtraNonceMaxSize) 696 } 697 //Increase size to maintain transaction weight 698 tx.Extra[1].Data = make(types.Bytes, correctedExtraNonceSize) 699 tx.Extra[1].VarInt = correctedExtraNonceSize 700 } 701 702 return tx, nil 703 } 704 705 func (s *Server) HandleMempoolData(data mempool.Mempool) { 706 s.incomingChanges <- func() bool { 707 timeReceived := time.Now() 708 709 s.lock.Lock() 710 defer s.lock.Unlock() 711 712 var highFeeReceived bool 713 for _, tx := range data { 714 715 if s.mempool.Add(tx) { 716 if tx.Fee >= HighFeeValue { 717 //prevent a lot of calls if not needed 718 if utils.GlobalLogLevel&utils.LogLevelDebug > 0 { 719 utils.Debugf("Stratum", "new tx id = %s, size = %d, weight = %d, fee = %s XMR", tx.Id, tx.BlobSize, tx.Weight, utils.XMRUnits(tx.Fee)) 720 } 721 722 highFeeReceived = true 723 utils.Noticef("Stratum", "high fee tx received: %s, %s XMR - updating template", tx.Id, utils.XMRUnits(tx.Fee)) 724 } 725 } 726 } 727 728 // Refresh if 10 seconds have passed between templates and new transactions arrived, or a high fee was received 729 if highFeeReceived || timeReceived.Sub(s.lastMempoolRefresh) >= s.refreshDuration { 730 s.lastMempoolRefresh = timeReceived 731 if err := s.fillNewTemplateData(types.ZeroDifficulty); err != nil { 732 utils.Errorf("Stratum", "Error building new template data: %s", err) 733 return false 734 } 735 return true 736 } 737 return false 738 } 739 } 740 741 func (s *Server) HandleMinerData(minerData *p2pooltypes.MinerData) { 742 s.incomingChanges <- func() bool { 743 s.lock.Lock() 744 defer s.lock.Unlock() 745 746 if s.minerData == nil || s.minerData.Height <= minerData.Height { 747 s.minerData = minerData 748 s.mempool.Swap(minerData.TxBacklog) 749 s.lastMempoolRefresh = time.Now() 750 if err := s.fillNewTemplateData(types.ZeroDifficulty); err != nil { 751 utils.Errorf("Stratum", "Error building new template data: %s", err) 752 return false 753 } 754 return true 755 } 756 return false 757 } 758 } 759 760 func (s *Server) HandleTip(tip *sidechain.PoolBlock) { 761 currentDifficulty := s.sidechain.Difficulty() 762 s.incomingChanges <- func() bool { 763 s.lock.Lock() 764 defer s.lock.Unlock() 765 766 if s.tip == nil || s.tip.Side.Height <= tip.Side.Height { 767 s.tip = tip 768 s.lastMempoolRefresh = time.Now() 769 if err := s.fillNewTemplateData(currentDifficulty); err != nil { 770 utils.Errorf("Stratum", "Error building new template data: %s", err) 771 return false 772 } 773 return true 774 } 775 return false 776 } 777 } 778 779 func (s *Server) HandleBroadcast(block *sidechain.PoolBlock) { 780 s.incomingChanges <- func() bool { 781 s.lock.Lock() 782 defer s.lock.Unlock() 783 784 if s.tip != nil && block != s.tip && block.Side.Height <= s.tip.Side.Height { 785 //probably a new block was added as alternate 786 if err := s.fillNewTemplateData(types.ZeroDifficulty); err != nil { 787 utils.Errorf("Stratum", "Error building new template data: %s", err) 788 return false 789 } 790 return true 791 } 792 return false 793 } 794 } 795 796 func (s *Server) Update() { 797 var closeClients []*Client 798 defer func() { 799 for _, c := range closeClients { 800 s.CloseClient(c) 801 } 802 }() 803 s.clientsLock.RLock() 804 defer s.clientsLock.RUnlock() 805 806 if len(s.clients) > 0 { 807 utils.Logf("Stratum", "Sending new job to %d connection(s)", len(s.clients)) 808 for _, c := range s.clients { 809 if err := s.SendTemplate(c); err != nil { 810 utils.Noticef("Stratum", "Closing connection %s: %s", c.Conn.RemoteAddr().String(), err) 811 closeClients = append(closeClients, c) 812 } 813 } 814 } 815 } 816 817 type BanEntry struct { 818 Expiration uint64 819 Error error 820 } 821 822 func (s *Server) CleanupBanList() { 823 s.bansLock.Lock() 824 defer s.bansLock.Unlock() 825 826 currentTime := uint64(time.Now().Unix()) 827 828 for k, b := range s.bans { 829 if currentTime >= b.Expiration { 830 delete(s.bans, k) 831 } 832 } 833 } 834 835 func (s *Server) IsBanned(ip netip.Addr) (bool, *BanEntry) { 836 if ip.IsLoopback() { 837 return false, nil 838 } 839 ip = ip.Unmap() 840 var prefix netip.Prefix 841 if ip.Is6() { 842 //ban the /64 843 prefix, _ = ip.Prefix(64) 844 } else if ip.Is4() { 845 //ban only a single ip, /32 846 prefix, _ = ip.Prefix(32) 847 } 848 849 if !prefix.IsValid() { 850 return false, nil 851 } 852 853 k := prefix.Addr().As16() 854 855 if b, ok := func() (entry BanEntry, ok bool) { 856 s.bansLock.RLock() 857 defer s.bansLock.RUnlock() 858 entry, ok = s.bans[k] 859 return entry, ok 860 }(); ok == false { 861 return false, nil 862 } else if uint64(time.Now().Unix()) >= b.Expiration { 863 return false, nil 864 } else { 865 return true, &b 866 } 867 } 868 869 func (s *Server) Ban(ip netip.Addr, duration time.Duration, err error) { 870 if ok, _ := s.IsBanned(ip); ok { 871 return 872 } 873 874 utils.Noticef("Stratum", "Banned %s for %s: %s", ip.String(), duration.String(), err.Error()) 875 if !ip.IsLoopback() { 876 ip = ip.Unmap() 877 var prefix netip.Prefix 878 if ip.Is6() { 879 //ban the /64 880 prefix, _ = ip.Prefix(64) 881 } else if ip.Is4() { 882 //ban only a single ip, /32 883 prefix, _ = ip.Prefix(32) 884 } 885 886 if prefix.IsValid() { 887 func() { 888 s.bansLock.Lock() 889 defer s.bansLock.Unlock() 890 s.bans[prefix.Addr().As16()] = BanEntry{ 891 Error: err, 892 Expiration: uint64(time.Now().Unix()) + uint64(duration.Seconds()), 893 } 894 }() 895 } 896 } 897 898 } 899 900 func (s *Server) processIncoming() { 901 go func() { 902 defer close(s.incomingChanges) 903 904 ctx := s.sidechain.Server().Context() 905 for { 906 select { 907 case <-ctx.Done(): 908 return 909 case f := <-s.incomingChanges: 910 if f() { 911 s.Update() 912 } 913 } 914 } 915 }() 916 } 917 918 func (s *Server) Listen(listen string) error { 919 920 ctx := s.sidechain.Server().Context() 921 go func() { 922 for range utils.ContextTick(ctx, time.Second*15) { 923 s.CleanupMiners() 924 } 925 }() 926 927 s.processIncoming() 928 929 if listener, err := (&net.ListenConfig{}).Listen(ctx, "tcp", listen); err != nil { 930 return err 931 } else if tcpListener, ok := listener.(*net.TCPListener); !ok { 932 return errors.New("not a tcp listener") 933 } else { 934 defer tcpListener.Close() 935 936 addressNetwork, _ := s.sidechain.Consensus().NetworkType.AddressNetwork() 937 938 for { 939 if conn, err := tcpListener.AcceptTCP(); err != nil { 940 return err 941 } else { 942 if err = func() error { 943 if addrPort, err := netip.ParseAddrPort(conn.RemoteAddr().String()); err != nil { 944 return err 945 } else if !addrPort.Addr().IsLoopback() { 946 addr := addrPort.Addr().Unmap() 947 948 if ok, b := s.IsBanned(addr); ok { 949 return fmt.Errorf("peer is banned: %w", b.Error) 950 } 951 } 952 953 return nil 954 }(); err != nil { 955 go func() { 956 defer conn.Close() 957 utils.Noticef("Stratum", "Connection from %s rejected (%s)", conn.RemoteAddr().String(), err.Error()) 958 }() 959 continue 960 } 961 962 func() { 963 utils.Noticef("Stratum", "Incoming connection from %s", conn.RemoteAddr().String()) 964 965 var rpcId uint32 966 for rpcId == 0 { 967 rpcId = unsafeRandom.Uint32() 968 } 969 client := &Client{ 970 RpcId: rpcId, 971 Conn: conn, 972 decoder: utils.NewJSONDecoder(conn), 973 Address: address.FromBase58(types.DonationAddress).ToPackedAddress(), 974 } 975 // Use deadline 976 client.encoder = utils.NewJSONEncoder(client) 977 978 func() { 979 s.clientsLock.Lock() 980 defer s.clientsLock.Unlock() 981 s.clients = append(s.clients, client) 982 }() 983 go func() { 984 var err error 985 defer s.CloseClient(client) 986 defer func() { 987 if err != nil { 988 utils.Noticef("Stratum", "Connection %s closed with error: %s", client.Conn.RemoteAddr().String(), err) 989 } else { 990 utils.Noticef("Stratum", "Connection %s closed", client.Conn.RemoteAddr().String()) 991 } 992 }() 993 defer func() { 994 if e := recover(); e != nil { 995 if err = e.(error); err == nil { 996 err = fmt.Errorf("panic called: %v", e) 997 } 998 s.CloseClient(client) 999 } 1000 }() 1001 1002 for client.decoder.More() { 1003 var msg JsonRpcMessage 1004 if err = client.decoder.Decode(&msg); err != nil { 1005 return 1006 } 1007 1008 switch msg.Method { 1009 case "login": 1010 1011 if err = func() error { 1012 client.Lock.Lock() 1013 defer client.Lock.Unlock() 1014 if client.Login { 1015 return errors.New("already logged in") 1016 } 1017 if m, ok := msg.Params.(map[string]any); ok { 1018 if str, ok := m["agent"].(string); ok { 1019 client.Agent = str 1020 } 1021 if str, ok := m["pass"].(string); ok { 1022 client.Password = str 1023 } 1024 if str, ok := m["rig-id"].(string); ok { 1025 client.RigId = str 1026 } 1027 if str, ok := m["login"].(string); ok { 1028 a := address.FromBase58(str) 1029 if a != nil && a.Network == addressNetwork { 1030 client.Address = a.ToPackedAddress() 1031 } else { 1032 return errors.New("invalid address in user") 1033 } 1034 } 1035 var hasRx0 bool 1036 if algos, ok := m["algo"].([]any); ok { 1037 for _, v := range algos { 1038 if str, ok := v.(string); !ok { 1039 return errors.New("invalid algo") 1040 } else if str == "rx/0" { 1041 hasRx0 = true 1042 break 1043 } 1044 } 1045 } 1046 1047 if !hasRx0 { 1048 return errors.New("algo rx/0 not found") 1049 } 1050 1051 utils.Debugf("Stratum", "Connection %s address = %s, agent = \"%s\", pass = \"%s\"", client.Conn.RemoteAddr().String(), client.Address.ToAddress(addressNetwork).ToBase58(), client.Agent, client.Password) 1052 1053 client.Login = true 1054 return nil 1055 } else { 1056 return errors.New("could not read login params") 1057 } 1058 }(); err != nil { 1059 _ = client.encoder.Encode(JsonRpcResult{ 1060 Id: msg.Id, 1061 JsonRpcVersion: "2.0", 1062 Error: map[string]any{ 1063 "code": int(-1), 1064 "message": err.Error(), 1065 }, 1066 }) 1067 return 1068 } else if err = s.SendTemplateResponse(client, msg.Id); err != nil { 1069 _ = client.encoder.Encode(JsonRpcResult{ 1070 Id: msg.Id, 1071 JsonRpcVersion: "2.0", 1072 Error: map[string]any{ 1073 "code": int(-1), 1074 "message": err.Error(), 1075 }, 1076 }) 1077 } 1078 1079 case "submit": 1080 if submitError, ban := func() (error, bool) { 1081 client.Lock.RLock() 1082 defer client.Lock.RUnlock() 1083 if !client.Login { 1084 return errors.New("unauthenticated"), true 1085 } 1086 var err error 1087 var jobId JobIdentifier 1088 var resultHash types.Hash 1089 var nonce uint32 1090 if m, ok := msg.Params.(map[string]any); ok { 1091 if str, ok := m["job_id"].(string); ok { 1092 if jobId, err = JobIdentifierFromString(str); err != nil { 1093 return err, true 1094 } 1095 } else { 1096 return errors.New("no job_id specified"), true 1097 } 1098 if str, ok := m["nonce"].(string); ok { 1099 var nonceBuf []byte 1100 if nonceBuf, err = fasthex.DecodeString(str); err != nil { 1101 return err, true 1102 } 1103 if len(nonceBuf) != 4 { 1104 return errors.New("invalid nonce size"), true 1105 } 1106 nonce = binary.LittleEndian.Uint32(nonceBuf) 1107 } else { 1108 return errors.New("no nonce specified"), true 1109 } 1110 if str, ok := m["result"].(string); ok { 1111 if resultHash, err = types.HashFromString(str); err != nil { 1112 return err, true 1113 } 1114 } else { 1115 return errors.New("no result specified"), true 1116 } 1117 1118 if err, ban := func() (error, bool) { 1119 if e, ok := func() (*MinerTrackingEntry, bool) { 1120 s.minersLock.RLock() 1121 defer s.minersLock.RUnlock() 1122 e, ok := s.miners[client.Address] 1123 return e, ok 1124 }(); ok { 1125 b := &sidechain.PoolBlock{} 1126 if blob := e.GetJobBlob(jobId, nonce); blob == nil { 1127 return errors.New("invalid job id"), true 1128 } else if err := b.UnmarshalBinary(s.sidechain.Consensus(), s.sidechain.DerivationCache(), blob); err != nil { 1129 return err, true 1130 } else { 1131 if b.Side.Difficulty.CheckPoW(resultHash) { 1132 //passes difficulty 1133 if err := s.SubmitFunc(b); err != nil { 1134 return fmt.Errorf("submit error: %w", err), true 1135 } 1136 } else { 1137 1138 return errors.New("low difficulty share"), true 1139 } 1140 } 1141 } else { 1142 return errors.New("unknown miner"), true 1143 } 1144 return nil, false 1145 }(); err != nil { 1146 return err, ban 1147 } 1148 return nil, false 1149 } else { 1150 return errors.New("could not read submit params"), true 1151 } 1152 }(); submitError != nil { 1153 err = client.encoder.Encode(JsonRpcResult{ 1154 Id: msg.Id, 1155 JsonRpcVersion: "2.0", 1156 Error: map[string]any{ 1157 "code": int(-1), 1158 "message": submitError.Error(), 1159 }, 1160 }) 1161 if err != nil || ban { 1162 return 1163 } 1164 } else { 1165 if err = client.encoder.Encode(JsonRpcResult{ 1166 Id: msg.Id, 1167 JsonRpcVersion: "2.0", 1168 Error: nil, 1169 Result: map[string]any{ 1170 "status": "OK", 1171 }, 1172 }); err != nil { 1173 return 1174 } 1175 } 1176 case "keepalived": 1177 if err = client.encoder.Encode(JsonRpcResult{ 1178 Id: msg.Id, 1179 JsonRpcVersion: "2.0", 1180 Error: nil, 1181 Result: map[string]any{ 1182 "status": "KEEPALIVED", 1183 }, 1184 }); err != nil { 1185 return 1186 } 1187 default: 1188 err = fmt.Errorf("unknown command %s", msg.Method) 1189 _ = client.encoder.Encode(JsonRpcResult{ 1190 Id: msg.Id, 1191 JsonRpcVersion: "2.0", 1192 Error: map[string]any{ 1193 "code": int(-1), 1194 "message": err.Error(), 1195 }, 1196 }) 1197 return 1198 } 1199 } 1200 }() 1201 }() 1202 } 1203 1204 } 1205 } 1206 1207 return nil 1208 } 1209 1210 func (s *Server) SendTemplate(c *Client) (err error) { 1211 c.Lock.Lock() 1212 defer c.Lock.Unlock() 1213 tpl, jobCounter, targetDifficulty, seedHash, err := s.BuildTemplate(c.Address, false) 1214 1215 if err != nil { 1216 return err 1217 } 1218 1219 job := copyBaseJob() 1220 bufLen := tpl.HashingBlobBufferLength() 1221 if cap(c.buf) < bufLen { 1222 c.buf = make([]byte, 0, bufLen) 1223 } 1224 1225 sideRandomNumber := unsafeRandom.Uint32() 1226 extraNonce := unsafeRandom.Uint32() 1227 sideExtraNonce := extraNonce 1228 1229 hasher := crypto.GetKeccak256Hasher() 1230 defer crypto.PutKeccak256Hasher(hasher) 1231 1232 var templateId types.Hash 1233 tpl.TemplateId(hasher, c.buf, s.sidechain.Consensus(), sideRandomNumber, sideExtraNonce, &templateId) 1234 1235 job.Params.Blob = fasthex.EncodeToString(tpl.HashingBlob(hasher, c.buf, 0, extraNonce, templateId)) 1236 1237 jobId := JobIdentifierFromValues(jobCounter, extraNonce, sideRandomNumber, sideExtraNonce, templateId) 1238 1239 job.Params.JobId = jobId.String() 1240 1241 target := targetDifficulty.Target() 1242 job.Params.Target = TargetHex(target) 1243 job.Params.Height = tpl.MainHeight 1244 job.Params.SeedHash = seedHash 1245 1246 if err = c.encoder.EncodeWithOption(job, utils.JsonEncodeOptions...); err != nil { 1247 return 1248 } 1249 return nil 1250 } 1251 1252 func (s *Server) SendTemplateResponse(c *Client, id any) (err error) { 1253 c.Lock.Lock() 1254 defer c.Lock.Unlock() 1255 tpl, jobCounter, targetDifficulty, seedHash, err := s.BuildTemplate(c.Address, false) 1256 1257 if err != nil { 1258 return 1259 } 1260 1261 job := copyBaseResponseJob() 1262 bufLen := tpl.HashingBlobBufferLength() 1263 if cap(c.buf) < bufLen { 1264 c.buf = make([]byte, 0, bufLen) 1265 } 1266 var hexBuf [4]byte 1267 binary.LittleEndian.PutUint32(hexBuf[:], c.RpcId) 1268 1269 sideRandomNumber := unsafeRandom.Uint32() 1270 extraNonce := unsafeRandom.Uint32() 1271 sideExtraNonce := extraNonce 1272 1273 hasher := crypto.GetKeccak256Hasher() 1274 defer crypto.PutKeccak256Hasher(hasher) 1275 1276 var templateId types.Hash 1277 tpl.TemplateId(hasher, c.buf, s.sidechain.Consensus(), sideRandomNumber, sideExtraNonce, &templateId) 1278 1279 job.Id = id 1280 job.Result.Id = fasthex.EncodeToString(hexBuf[:]) 1281 job.Result.Job.Blob = fasthex.EncodeToString(tpl.HashingBlob(hasher, c.buf, 0, extraNonce, templateId)) 1282 1283 jobId := JobIdentifierFromValues(jobCounter, extraNonce, sideRandomNumber, sideExtraNonce, templateId) 1284 1285 job.Result.Job.JobId = jobId.String() 1286 1287 target := targetDifficulty.Target() 1288 job.Result.Job.Target = TargetHex(target) 1289 job.Result.Job.Height = tpl.MainHeight 1290 job.Result.Job.SeedHash = seedHash 1291 1292 if err = c.encoder.EncodeWithOption(job, utils.JsonEncodeOptions...); err != nil { 1293 return 1294 } 1295 return nil 1296 } 1297 1298 func (s *Server) CloseClient(c *Client) { 1299 c.Conn.Close() 1300 1301 s.clientsLock.Lock() 1302 defer s.clientsLock.Unlock() 1303 if i := slices.Index(s.clients, c); i != -1 { 1304 s.clients = slices.Delete(s.clients, i, i+1) 1305 } 1306 } 1307 1308 // Target4BytesLimit Use short target format (4 bytes) for diff <= 4 million 1309 const Target4BytesLimit = math.MaxUint64 / 4000001 1310 1311 func TargetHex(target uint64) string { 1312 var buf [8]byte 1313 binary.LittleEndian.PutUint64(buf[:], target) 1314 result := fasthex.EncodeToString(buf[:]) 1315 if target >= Target4BytesLimit { 1316 return result[4*2:] 1317 } else { 1318 return result 1319 } 1320 }