├── .deepsource.toml ├── .gitignore ├── .idea ├── modules.xml └── vcs.xml ├── LICENSE ├── README.md ├── algorithm ├── algorithm.go └── algorithm_test.go ├── api └── server.go ├── bans └── banning.go ├── config.bitcoin-qt.json ├── config.btcd.json ├── config.dash.json ├── config.example.json ├── config.litecoin.json ├── config ├── algorithm.go ├── api.go ├── banning.go ├── coin.go ├── daemon.go ├── options.go ├── p2p.go ├── payment.go ├── redis.go ├── stratum.go ├── tls.go └── vardiff.go ├── daemons ├── getbalance.go ├── getblock.go ├── getblockchaininfo.go ├── getblocktemplate.go ├── getdifficulty.go ├── getinfo.go ├── getmininginfo.go ├── getnetworkinfo.go ├── getwalletinfo.go ├── jsonrpc.go ├── manager.go ├── submitblock.go └── validateaddress.go ├── go.mod ├── go.sum ├── jobs ├── extraNonce1Generator.go ├── job.go ├── jobManager.go └── job_test.go ├── main.go ├── merkletree ├── mtree.go └── mtree_test.go ├── p2p ├── peer.go └── peer_test.go ├── payments ├── contrib.go ├── payer.go └── recipient.go ├── pool ├── pool.go └── stats.go ├── resources ├── logo.png └── logo.svg ├── storage └── redis.go ├── stratum ├── client.go ├── server.go ├── shares.go └── subscriptionCounter.go ├── transactions ├── transactions.go └── transactions_test.go ├── types ├── block.go ├── errors.go ├── share.go └── stats.go ├── utils ├── utils.go └── utils_test.go └── vardiff ├── ringbuffer.go ├── vardiff.go └── vardiff_test.go /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "go" 5 | enabled = true 6 | 7 | [analyzers.meta] 8 | import_path = "github.com/mining-pool/not-only-mining-pool" 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | config.json 3 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Command M 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Not-Only-Mining-Pool(NOMP) 2 | 3 | BitcoinCore(bitcoind)-variants' pool written in golang 4 | 5 | ## Difference from NOMP(node-open-mining-portal) 6 | 7 | This pool software is not a portal, but a standalone stratum server with high performance. 8 | 9 | If you want, you can implement the portal page in frontend web. 10 | 11 | ## Why standalone? 12 | 13 | Keep standalone is better to assemble new algorithm/coin to your pool services, without dealing with C lib conflicts or restart the whole site to append new pool, what's more, most pool operators don't need a portal, they just get benefit from few different coins with different algorithms. 14 | 15 | So it's obviously that standalone one with more advantages on deploying and maintaining. 16 | 17 | In other words, "大人,時代變了!". 18 | 19 | ## How to use it? 20 | 21 | ### 0x00 Check 22 | 23 | Make sure your algorithm in support, if not, take an issue. 24 | 25 | ### 0x01 Configuration 26 | 27 | Read `config.example.json` and modify the configurations 28 | 29 | Then rename it to `config.json` 30 | 31 | ### 0x02 Build 32 | 33 | ```bash 34 | go build . 35 | 36 | ``` 37 | 38 | ### 0x03 Deploy 39 | 40 | Copy `go-stratum-pool` or `go-stratum-pool.exe` and `config.json` to your VPS server and 41 | 42 | And then 43 | 44 | ```bash 45 | $ ./go-stratum-pool 46 | 47 | ``` 48 | 49 | or 50 | 51 | ```cmd 52 | > go-stratum-pool.exe 53 | 54 | ``` 55 | 56 | ## TODO 57 | 58 | - API(half done) 59 | - More algorithms 60 | - Web page 61 | - ... 62 | 63 | ## Donation 64 | 65 | **LTC**: LXxqHY4StG79nqRurdNNt1wF2yCf4Mc986 66 | 67 | ## Tested Network 68 | - LTC Testnet 69 | -------------------------------------------------------------------------------- /algorithm/algorithm.go: -------------------------------------------------------------------------------- 1 | package algorithm 2 | 3 | import ( 4 | "math/big" 5 | "strings" 6 | 7 | logging "github.com/ipfs/go-log/v2" 8 | "github.com/mining-pool/not-only-mining-pool/utils" 9 | "github.com/samli88/go-x11-hash" 10 | "golang.org/x/crypto/scrypt" 11 | ) 12 | 13 | var log = logging.Logger("algorithm") 14 | 15 | // difficulty = MAX_TARGET / current_target. 16 | var ( 17 | MaxTargetTruncated, _ = new(big.Int).SetString("00000000FFFF0000000000000000000000000000000000000000000000000000", 16) 18 | MaxTarget, _ = new(big.Int).SetString("00000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 16) 19 | ) 20 | 21 | func GetHashFunc(hashName string) func([]byte) []byte { 22 | switch strings.ToLower(hashName) { 23 | case "scrypt": 24 | return ScryptHash 25 | case "x11": 26 | return X11Hash 27 | case "sha256d": 28 | return DoubleSha256Hash 29 | default: 30 | log.Panic(hashName, " is not officially supported yet, but you can easily add it with cgo binding by yourself") 31 | return nil 32 | } 33 | } 34 | 35 | // ScryptHash is the algorithm which litecoin uses as its PoW mining algorithm 36 | func ScryptHash(data []byte) []byte { 37 | b, _ := scrypt.Key(data, data, 1024, 1, 1, 32) 38 | 39 | return b 40 | } 41 | 42 | // X11Hash is the algorithm which dash uses as its PoW mining algorithm 43 | func X11Hash(data []byte) []byte { 44 | dst := make([]byte, 32) 45 | x11.New().Hash(data, dst) 46 | return dst 47 | } 48 | 49 | // DoubleSha256Hash is the algorithm which litecoin uses as its PoW mining algorithm 50 | func DoubleSha256Hash(b []byte) []byte { 51 | return utils.Sha256d(b) 52 | } 53 | -------------------------------------------------------------------------------- /algorithm/algorithm_test.go: -------------------------------------------------------------------------------- 1 | package algorithm 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | "github.com/mining-pool/not-only-mining-pool/utils" 8 | ) 9 | 10 | func TestHash(t *testing.T) { 11 | t.Log(MaxTargetTruncated) 12 | } 13 | 14 | func TestScryptHash(t *testing.T) { 15 | headerHex := "01000000f615f7ce3b4fc6b8f61e8f89aedb1d0852507650533a9e3b10b9bbcc30639f279fcaa86746e1ef52d3edb3c4ad8259920d509bd073605c9bf1d59983752a6b06b817bb4ea78e011d012d59d4" 16 | headerBytes, err := hex.DecodeString(headerHex) 17 | if err != nil { 18 | t.Log(err) 19 | } 20 | result := hex.EncodeToString(utils.ReverseBytes(GetHashFunc("scrypt")(headerBytes))) 21 | if result != "0000000110c8357966576df46f3b802ca897deb7ad18b12f1c24ecff6386ebd9" { 22 | t.Log(result) 23 | t.Fail() 24 | } 25 | } 26 | 27 | func TestX11Hash(t *testing.T) { 28 | if hex.EncodeToString(X11Hash([]byte("The great experiment continues."))) != "4da3b7c5ff698c6546564ebc72204f31885cd87b75b2b3ca5a93b5d75db85b8c" { 29 | t.Log(hex.EncodeToString(GetHashFunc("x11")([]byte("The great experiment continues.")))) 30 | t.Fail() 31 | } 32 | 33 | // Test Dash Tx 34 | raw, _ := hex.DecodeString("0200000001ac7d18f0103f17c44b5b2b1352617735cc3a3a52381a28e923dffa4ac78e1560000000006b483045022100c56b739271efc559d63b04a01c15fddf7a74008b9afbd432c6260c24bde3b0cf02206ce80233e5af953f7e6f4b55427afa86aac6cbf3047c3cf90fcc248c8d3338f9012103e544bf462f31edad02b3d8134f60d20d7180208df68b0d95f8e0cacee880bc93ffffffff013d6c6d02000000001976a91404ed220f5b5bfd1c61becf0d76e21773ed204ac188ac00000000") 35 | hash := hex.EncodeToString(DoubleSha256Hash(raw)) 36 | 37 | if hash != hex.EncodeToString(utils.Uint256BytesFromHash("498a7a14586da86d98a26ee00aecb7f8fb61a6160453186c88108e4873beaaff")) { 38 | t.Log(hash) 39 | t.Fail() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /api/server.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/gorilla/mux" 9 | logging "github.com/ipfs/go-log/v2" 10 | "github.com/mining-pool/not-only-mining-pool/config" 11 | "github.com/mining-pool/not-only-mining-pool/storage" 12 | ) 13 | 14 | var log = logging.Logger("api") 15 | 16 | // TODO 17 | type Server struct { 18 | *mux.Router 19 | 20 | apiConf *config.APIOptions 21 | storage *storage.DB 22 | 23 | availablePaths []string 24 | config map[string]interface{} 25 | } 26 | 27 | // NewAPIServer creates a Server which follows [standard api](https://github.com/mining-pool/mining-pool-api) 28 | func NewAPIServer(options *config.Options, storage *storage.DB) *Server { 29 | s := &Server{ 30 | Router: mux.NewRouter(), 31 | 32 | apiConf: options.API, 33 | storage: storage, 34 | 35 | availablePaths: make([]string, 0), 36 | config: make(map[string]interface{}), 37 | } 38 | 39 | s.ConvertConf(options) 40 | 41 | s.RegisterFunc("/", s.indexFunc) 42 | 43 | s.RegisterFunc("/pool", s.poolFunc) 44 | 45 | s.RegisterFunc("/config", s.configIndexFunc) 46 | s.RegisterFunc("/config/{key}", s.configFunc) 47 | 48 | s.RegisterFunc("/miner/{miner}", s.minerFunc) 49 | s.RegisterFunc("/miner/{miner}/rig/{rig}", s.rigFunc) 50 | s.Use(mux.CORSMethodMiddleware(s.Router)) 51 | 52 | http.Handle("/", s) 53 | 54 | return s 55 | } 56 | 57 | func (s *Server) ConvertConf(options *config.Options) { 58 | s.config["ports"] = options.Ports 59 | s.config["algorithm"] = options.Algorithm 60 | s.config["coin"] = options.Coin 61 | s.config["api"] = options.API 62 | } 63 | 64 | func (s *Server) RegisterFunc(path string, fn func(http.ResponseWriter, *http.Request)) { 65 | s.HandleFunc(path, fn) 66 | s.availablePaths = append(s.availablePaths, path) 67 | } 68 | 69 | func (s *Server) Serve() { 70 | addr := s.apiConf.Addr() 71 | log.Warn("API server listening on ", addr) 72 | go func() { 73 | err := http.ListenAndServe(addr, nil) 74 | if err != nil { 75 | panic(err) 76 | } 77 | }() 78 | } 79 | 80 | func (s *Server) indexFunc(writer http.ResponseWriter, _ *http.Request) { 81 | raw, _ := json.Marshal(s.availablePaths) 82 | _, _ = writer.Write(raw) 83 | } 84 | 85 | func (s *Server) configIndexFunc(writer http.ResponseWriter, _ *http.Request) { 86 | keys := make([]string, 0) 87 | for k := range s.config { 88 | keys = append(keys, "/config/"+k) 89 | } 90 | 91 | raw, _ := json.Marshal(keys) 92 | _, _ = writer.Write(raw) 93 | } 94 | 95 | func (s *Server) configFunc(writer http.ResponseWriter, r *http.Request) { 96 | vars := mux.Vars(r) 97 | 98 | raw, _ := json.Marshal(s.config[vars["key"]]) 99 | _, _ = writer.Write(raw) 100 | } 101 | 102 | type PoolInfo struct { 103 | CoinName string `json:"coinName"` 104 | 105 | Hashrate1Min float64 `json:"hashrate1min"` 106 | Hashrate30Min float64 `json:"hashrate30min"` 107 | Hashrate1H float64 `json:"hashrate1h"` 108 | Hashrate6H float64 `json:"hashrate6h"` 109 | Hashrate1D float64 `json:"hashrate1d"` 110 | 111 | Miners []string `json:"miners"` 112 | } 113 | 114 | func (s *Server) poolFunc(w http.ResponseWriter, _ *http.Request) { 115 | now := time.Now().Unix() // unit: sec 116 | 117 | hs1M, err := s.storage.GetPoolHashrate(now-60, now) 118 | if err != nil { 119 | log.Error(err) 120 | } 121 | hs30M, err := s.storage.GetPoolHashrate(now-30*60, now) 122 | if err != nil { 123 | log.Error(err) 124 | } 125 | hs1H, err := s.storage.GetPoolHashrate(now-60*60, now) 126 | if err != nil { 127 | log.Error(err) 128 | } 129 | hs6H, err := s.storage.GetPoolHashrate(now-6*60*60, now) 130 | if err != nil { 131 | log.Error(err) 132 | } 133 | hs1D, err := s.storage.GetPoolHashrate(now-24*60*60, now) 134 | if err != nil { 135 | log.Error(err) 136 | } 137 | 138 | miners, err := s.storage.GetMinerIndex() 139 | if err != nil { 140 | log.Error(err) 141 | } 142 | 143 | miner := PoolInfo{ 144 | Hashrate1Min: hs1M, 145 | Hashrate30Min: hs30M, 146 | Hashrate1H: hs1H, 147 | Hashrate6H: hs6H, 148 | Hashrate1D: hs1D, 149 | 150 | Miners: miners, 151 | } 152 | 153 | raw, err := json.Marshal(&miner) 154 | if err != nil { 155 | log.Error(err) 156 | } 157 | 158 | _, _ = w.Write(raw) 159 | } 160 | 161 | type MinerInfo struct { 162 | Name string `json:"name"` 163 | 164 | Hashrate1Min float64 `json:"hashrate1min"` 165 | Hashrate30Min float64 `json:"hashrate30min"` 166 | Hashrate1H float64 `json:"hashrate1h"` 167 | Hashrate6H float64 `json:"hashrate6h"` 168 | Hashrate1D float64 `json:"hashrate1d"` 169 | 170 | RoundContrib float64 `json:"roundContrib"` 171 | Rigs []string `json:"rigs"` 172 | } 173 | 174 | func (s *Server) minerFunc(w http.ResponseWriter, r *http.Request) { 175 | vars := mux.Vars(r) 176 | minerName := vars["miner"] 177 | 178 | contrib, err := s.storage.GetMinerCurrentRoundContrib(minerName) 179 | if err != nil { 180 | log.Error(err) 181 | } 182 | 183 | now := time.Now().Unix() // unit: sec 184 | 185 | hs1M, err := s.storage.GetMinerHashrate(minerName, now-60, now) 186 | if err != nil { 187 | log.Error(err) 188 | } 189 | hs30M, err := s.storage.GetMinerHashrate(minerName, now-30*60, now) 190 | if err != nil { 191 | log.Error(err) 192 | } 193 | hs1H, err := s.storage.GetMinerHashrate(minerName, now-60*60, now) 194 | if err != nil { 195 | log.Error(err) 196 | } 197 | hs6H, err := s.storage.GetMinerHashrate(minerName, now-6*60*60, now) 198 | if err != nil { 199 | log.Error(err) 200 | } 201 | hs1D, err := s.storage.GetMinerHashrate(minerName, now-24*60*60, now) 202 | if err != nil { 203 | log.Error(err) 204 | } 205 | 206 | rigs, err := s.storage.GetRigIndex(minerName) 207 | if err != nil { 208 | log.Error(err) 209 | } 210 | 211 | miner := MinerInfo{ 212 | Name: minerName, 213 | 214 | Hashrate1Min: hs1M, 215 | Hashrate30Min: hs30M, 216 | Hashrate1H: hs1H, 217 | Hashrate6H: hs6H, 218 | Hashrate1D: hs1D, 219 | 220 | RoundContrib: contrib, 221 | Rigs: rigs, 222 | } 223 | 224 | raw, err := json.Marshal(&miner) 225 | if err != nil { 226 | log.Error(err) 227 | } 228 | 229 | _, _ = w.Write(raw) 230 | } 231 | 232 | type RigInfo struct { 233 | Name string `json:"name"` 234 | MinerName string `json:"minerName"` 235 | Hashrate1Min float64 `json:"hashrate1min"` 236 | Hashrate30Min float64 `json:"hashrate30min"` 237 | Hashrate1H float64 `json:"hashrate1h"` 238 | Hashrate6H float64 `json:"hashrate6h"` 239 | Hashrate1D float64 `json:"hashrate1d"` 240 | } 241 | 242 | func (s *Server) rigFunc(w http.ResponseWriter, r *http.Request) { 243 | vars := mux.Vars(r) 244 | minerName := vars["miner"] 245 | rigName := vars["rig"] 246 | 247 | now := time.Now().Unix() // unit: sec 248 | 249 | hs1M, err := s.storage.GetRigHashrate(minerName, rigName, now-60, now) 250 | if err != nil { 251 | log.Error(err) 252 | } 253 | hs30M, err := s.storage.GetRigHashrate(minerName, rigName, now-30*60, now) 254 | if err != nil { 255 | log.Error(err) 256 | } 257 | hs1H, err := s.storage.GetRigHashrate(minerName, rigName, now-60*60, now) 258 | if err != nil { 259 | log.Error(err) 260 | } 261 | hs6H, err := s.storage.GetRigHashrate(minerName, rigName, now-6*60*60, now) 262 | if err != nil { 263 | log.Error(err) 264 | } 265 | hs1D, err := s.storage.GetRigHashrate(minerName, rigName, now-24*60*60, now) 266 | if err != nil { 267 | log.Error(err) 268 | } 269 | 270 | miner := RigInfo{ 271 | Name: rigName, 272 | MinerName: minerName, 273 | 274 | Hashrate1Min: hs1M, 275 | Hashrate30Min: hs30M, 276 | Hashrate1H: hs1H, 277 | Hashrate6H: hs6H, 278 | Hashrate1D: hs1D, 279 | } 280 | 281 | raw, err := json.Marshal(&miner) 282 | if err != nil { 283 | log.Error(err) 284 | } 285 | 286 | _, _ = w.Write(raw) 287 | } 288 | -------------------------------------------------------------------------------- /bans/banning.go: -------------------------------------------------------------------------------- 1 | package bans 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/mining-pool/not-only-mining-pool/config" 7 | ) 8 | 9 | type BanningManager struct { 10 | Options *config.BanningOptions 11 | BannedIPList map[string]*time.Time 12 | } 13 | 14 | func NewBanningManager(options *config.BanningOptions) *BanningManager { 15 | return &BanningManager{ 16 | Options: options, 17 | BannedIPList: make(map[string]*time.Time), 18 | } 19 | } 20 | 21 | func (bm *BanningManager) Init() { 22 | go func() { 23 | ticker := time.NewTicker(time.Duration(bm.Options.PurgeInterval) * time.Second) 24 | defer ticker.Stop() 25 | 26 | for { 27 | <-ticker.C 28 | for ip, banTime := range bm.BannedIPList { 29 | if time.Since(*banTime) > time.Duration(bm.Options.Time)*time.Second { 30 | delete(bm.BannedIPList, ip) 31 | } 32 | } 33 | } 34 | }() 35 | } 36 | 37 | func (bm *BanningManager) CheckBan(strRemoteAddr string) (shouldCloseSocket bool) { 38 | if bm.BannedIPList[strRemoteAddr] != nil { 39 | bannedTime := bm.BannedIPList[strRemoteAddr] 40 | bannedTimeAgo := time.Since(*bannedTime) 41 | timeLeft := time.Duration(bm.Options.Time)*time.Second - bannedTimeAgo 42 | if timeLeft > 0 { 43 | return true 44 | // client.Socket.Close() 45 | // kickedBannedIP 46 | } else { 47 | delete(bm.BannedIPList, strRemoteAddr) 48 | // forgaveBannedIP 49 | } 50 | } 51 | 52 | return false 53 | } 54 | 55 | func (bm *BanningManager) AddBannedIP(strRemoteAddr string) { 56 | now := time.Now() 57 | bm.BannedIPList[strRemoteAddr] = &now 58 | } 59 | -------------------------------------------------------------------------------- /config.bitcoin-qt.json: -------------------------------------------------------------------------------- 1 | { 2 | "coin": { 3 | "name": "Bitcoin", 4 | "symbol": "BTC" 5 | }, 6 | "algorithm": { 7 | "name": "sha256d", 8 | "multiplier": 0, 9 | "sha256dBlockHasher": true 10 | }, 11 | "poolAddress": { 12 | "address": "76a914892b70294fcea4be8bf41b6c8e69fe79743b08f388ac", 13 | "type": "script" 14 | }, 15 | "rewardRecipients": [ 16 | { 17 | "address": "76a914892b70294fcea4be8bf41b6c8e69fe79743b08f388ac", 18 | "type": "script", 19 | "percent": 0.01 20 | } 21 | ], 22 | "blockRefreshInterval": 10, 23 | "jobRebroadcastTimeout": 55, 24 | "connectionTimeout": 600, 25 | "emitInvalidBlockHashes": false, 26 | "tcpProxyProtocol": false, 27 | "banning": { 28 | "time": 600, 29 | "invalidPercent": 50, 30 | "checkThreshold": 500, 31 | "purgeInterval": 300 32 | }, 33 | "ports": { 34 | "3032": { 35 | "diff": 1, 36 | "varDiff": { 37 | "minDiff": 1, 38 | "maxDiff": 100000, 39 | "targetTime": 15, 40 | "retargetTime": 90, 41 | "variancePercent": 30 42 | } 43 | } 44 | }, 45 | "daemons": [ 46 | { 47 | "host": "127.0.0.1", 48 | "port": 18332, 49 | "user": "bitcoinrpc", 50 | "password": "testnet" 51 | } 52 | ], 53 | "api": { 54 | "host": "0.0.0.0", 55 | "port": 20000 56 | }, 57 | "storage": { 58 | "network": "tcp", 59 | "host": "127.0.0.1", 60 | "port": 6379, 61 | "tls": null 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /config.btcd.json: -------------------------------------------------------------------------------- 1 | { 2 | "disablePayment": true, 3 | "coin": { 4 | "name": "Bitcoin", 5 | "symbol": "BTC" 6 | }, 7 | "algorithm": { 8 | "name": "sha256d", 9 | "multiplier": 0, 10 | "sha256dBlockHasher": true 11 | }, 12 | "poolAddress": { 13 | "address": "bc1qlr36tpdwctfndcdgzx5ukjkqazl5hclkurw08q", 14 | "type": "p2wsh" 15 | }, 16 | "rewardRecipients": [ 17 | { 18 | "address": "bc1qlr36tpdwctfndcdgzx5ukjkqazl5hclkurw08q", 19 | "type": "p2wsh", 20 | "percent": 0.01 21 | } 22 | ], 23 | "blockRefreshInterval": 10, 24 | "jobRebroadcastTimeout": 55, 25 | "connectionTimeout": 600, 26 | "emitInvalidBlockHashes": false, 27 | "tcpProxyProtocol": false, 28 | "banning": { 29 | "time": 600, 30 | "invalidPercent": 50, 31 | "checkThreshold": 500, 32 | "purgeInterval": 300 33 | }, 34 | "ports": { 35 | "3032": { 36 | "diff": 1, 37 | "varDiff": { 38 | "minDiff": 1, 39 | "maxDiff": 100000, 40 | "targetTime": 15, 41 | "retargetTime": 90, 42 | "variancePercent": 30 43 | } 44 | } 45 | }, 46 | "daemons": [ 47 | { 48 | "host": "127.0.0.1", 49 | "port": 18334, 50 | "user": "bitcoinrpc", 51 | "password": "testnet", 52 | "tls":{} 53 | } 54 | ], 55 | "api": { 56 | "host": "0.0.0.0", 57 | "port": 20000 58 | }, 59 | "storage": { 60 | "network": "tcp", 61 | "host": "127.0.0.1", 62 | "port": 6379, 63 | "tls": null 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /config.dash.json: -------------------------------------------------------------------------------- 1 | { 2 | "coin": { 3 | "name": "Dash", 4 | "symbol": "DASH" 5 | }, 6 | "algorithm": { 7 | "name": "x11", 8 | "multiplier": 30, 9 | "sha256dBlockHasher": false 10 | }, 11 | "poolAddress": { 12 | "address": "yYtPmC5TGJZrKsJ5HcmEuhabG5UjU5U6kq", 13 | "type": "p2pkh" 14 | }, 15 | "rewardRecipients": [ 16 | { 17 | "address": "yadZJQPAaQkfKj8Md7zz1N2FqLvRHcF5CL", 18 | "type": "p2pkh", 19 | "percent": 0.01 20 | } 21 | ], 22 | "blockRefreshInterval": 10, 23 | "jobRebroadcastTimeout": 55, 24 | "connectionTimeout": 600, 25 | "emitInvalidBlockHashes": false, 26 | "tcpProxyProtocol": false, 27 | "banning": { 28 | "time": 600, 29 | "invalidPercent": 50, 30 | "checkThreshold": 500, 31 | "purgeInterval": 300 32 | }, 33 | "ports": { 34 | "3032": { 35 | "diff": 0.0001, 36 | "varDiff": { 37 | "minDiff": 0.0001, 38 | "maxDiff": 100000, 39 | "targetTime": 15, 40 | "retargetTime": 90, 41 | "variancePercent": 30 42 | } 43 | } 44 | }, 45 | "daemons": [ 46 | { 47 | "host": "127.0.0.1", 48 | "port": 19998, 49 | "user": "dashrpc", 50 | "password": "testnet" 51 | } 52 | ], 53 | "p2p": null, 54 | "api": { 55 | "host": "0.0.0.0", 56 | "port": 20000 57 | }, 58 | "storage": { 59 | "network": "tcp", 60 | "host": "127.0.0.1", 61 | "port": 6379, 62 | "tls": null 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "coin": { 3 | "name": "Litecoin", 4 | "symbol": "LTC" 5 | }, 6 | "algorithm": { 7 | "name": "scrypt", 8 | "multiplier": 16, 9 | "sha256dBlockHasher": true 10 | }, 11 | "poolAddress": { 12 | "address": "QPxrDq3sorCk8DWaYX2GeCkxoePhm1asyY", 13 | "type": "p2sh" 14 | }, 15 | "rewardRecipients": [ 16 | { 17 | "address": "QPxrDq3sorCk8DWaYX2GeCkxoePhm1asyY", 18 | "type": "p2sh", 19 | "percent": 0.01 20 | } 21 | ], 22 | "blockRefreshInterval": 10, 23 | "jobRebroadcastTimeout": 55, 24 | "connectionTimeout": 600, 25 | "emitInvalidBlockHashes": false, 26 | "tcpProxyProtocol": false, 27 | "banning": { 28 | "time": 600, 29 | "invalidPercent": 50, 30 | "checkThreshold": 500, 31 | "purgeInterval": 300 32 | }, 33 | "ports": { 34 | "3032": { 35 | "diff": 15, 36 | "varDiff": { 37 | "minDiff": 1, 38 | "maxDiff": 100000, 39 | "targetTime": 15, 40 | "retargetTime": 90, 41 | "variancePercent": 30 42 | }, 43 | "tls": null 44 | } 45 | }, 46 | "daemons": [ 47 | { 48 | "host": "127.0.0.1", 49 | "port": 19332, 50 | "user": "litecoinrpc", 51 | "password": "testnet" 52 | } 53 | ], 54 | "p2p": { 55 | "host": "127.0.0.1", 56 | "port": 19335, 57 | "magic": "fdd2c8f1", 58 | "disableTransactions": true 59 | }, 60 | "api": { 61 | "host": "0.0.0.0", 62 | "port": 20000 63 | }, 64 | "storage": { 65 | "network": "tcp", 66 | "host": "127.0.0.1", 67 | "port": 6379, 68 | "tls": { 69 | "certFile": "cert.pem", 70 | "keyFile": "server.key" 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /config.litecoin.json: -------------------------------------------------------------------------------- 1 | { 2 | "coin": { 3 | "name": "Litecoin", 4 | "symbol": "LTC" 5 | }, 6 | "algorithm": { 7 | "name": "scrypt", 8 | "multiplier": 16, 9 | "sha256dBlockHasher": true 10 | }, 11 | "poolAddress": { 12 | "address": "QPxrDq3sorCk8DWaYX2GeCkxoePhm1asyY", 13 | "type": "p2sh" 14 | }, 15 | "rewardRecipients": [ 16 | { 17 | "address": "QPxrDq3sorCk8DWaYX2GeCkxoePhm1asyY", 18 | "type": "p2sh", 19 | "percent": 0.01 20 | } 21 | ], 22 | "blockRefreshInterval": 10, 23 | "jobRebroadcastTimeout": 55, 24 | "connectionTimeout": 600, 25 | "emitInvalidBlockHashes": false, 26 | "tcpProxyProtocol": false, 27 | "banning": { 28 | "time": 600, 29 | "invalidPercent": 50, 30 | "checkThreshold": 500, 31 | "purgeInterval": 300 32 | }, 33 | "ports": { 34 | "3032": { 35 | "diff": 15, 36 | "varDiff": { 37 | "minDiff": 1, 38 | "maxDiff": 100000, 39 | "targetTime": 15, 40 | "retargetTime": 90, 41 | "variancePercent": 30 42 | } 43 | } 44 | }, 45 | "daemons": [ 46 | { 47 | "host": "127.0.0.1", 48 | "port": 19332, 49 | "user": "litecoinrpc", 50 | "password": "testnet" 51 | } 52 | ], 53 | "p2p": { 54 | "host": "127.0.0.1", 55 | "port": 19335, 56 | "magic": "fdd2c8f1", 57 | "disableTransactions": true 58 | }, 59 | "api": { 60 | "host": "0.0.0.0", 61 | "port": 20000 62 | }, 63 | "storage": { 64 | "network": "tcp", 65 | "host": "127.0.0.1", 66 | "port": 6379, 67 | "tls": null 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /config/algorithm.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type AlgorithmOptions struct { 4 | Name string `json:"name"` 5 | Multiplier int `json:"multiplier"` 6 | SHA256dBlockHasher bool `json:"sha256dBlockHasher"` 7 | } 8 | -------------------------------------------------------------------------------- /config/api.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "strconv" 4 | 5 | type APIOptions struct { 6 | Host string `json:"host"` 7 | Port int `json:"port"` 8 | } 9 | 10 | func (api *APIOptions) Addr() string { 11 | return api.Host + ":" + strconv.FormatInt(int64(api.Port), 10) 12 | } 13 | -------------------------------------------------------------------------------- /config/banning.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type BanningOptions struct { 4 | Time int `json:"time"` 5 | InvalidPercent float64 `json:"invalidPercent"` 6 | CheckThreshold uint64 `json:"checkThreshold"` 7 | PurgeInterval int `json:"purgeInterval"` // unit seconds 8 | } 9 | -------------------------------------------------------------------------------- /config/coin.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type CoinOptions struct { 4 | Name string `json:"name"` 5 | Symbol string `json:"symbol"` 6 | TxMessages bool `json:"txMessages"` 7 | 8 | // auto-filled from rpc 9 | Reward string `json:"reward"` 10 | NoSubmitBlock bool `json:"noSubmitBlock"` 11 | Testnet bool `json:"testnet"` 12 | } 13 | -------------------------------------------------------------------------------- /config/daemon.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | type DaemonOptions struct { 8 | Host string `json:"host"` 9 | Port int `json:"port"` 10 | User string `json:"user"` 11 | Password string `json:"password"` 12 | TLS *TLSClientOptions `json:"tls"` 13 | } 14 | 15 | func (d *DaemonOptions) String() string { 16 | return d.User + ":" + d.Password + "@" + d.Host + strconv.FormatInt(int64(d.Port), 10) 17 | } 18 | 19 | func (d *DaemonOptions) URL() string { 20 | if d.TLS != nil { 21 | return "https://" + d.Host + ":" + strconv.FormatInt(int64(d.Port), 10) 22 | } 23 | 24 | return "http://" + d.Host + ":" + strconv.FormatInt(int64(d.Port), 10) 25 | } 26 | -------------------------------------------------------------------------------- /config/options.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import logging "github.com/ipfs/go-log/v2" 4 | 5 | var log = logging.Logger("config") 6 | 7 | type Options struct { 8 | DisablePayment bool `json:"disablePayment"` 9 | Coin *CoinOptions `json:"coin"` 10 | 11 | PoolAddress *Recipient `json:"poolAddress"` 12 | RewardRecipients []*Recipient `json:"rewardRecipients"` 13 | 14 | BlockRefreshInterval int `json:"blockRefreshInterval"` 15 | JobRebroadcastTimeout int `json:"jobRebroadcastTimeout"` 16 | ConnectionTimeout int `json:"connectionTimeout"` 17 | EmitInvalidBlockHashes bool `json:"emitInvalidBlockHashes"` 18 | TCPProxyProtocol bool `json:"tcpProxyProtocol"` // http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt 19 | 20 | API *APIOptions `json:"api"` 21 | Banning *BanningOptions `json:"banning"` 22 | Ports map[int]*PortOptions `json:"ports"` 23 | Daemons []*DaemonOptions `json:"daemons"` 24 | P2P *P2POptions `json:"p2p"` 25 | Storage *RedisOptions `json:"storage"` 26 | Algorithm *AlgorithmOptions `json:"algorithm"` 27 | } 28 | 29 | func (o *Options) TotalFeePercent() float64 { 30 | var totalFeePercent float64 31 | for i := range o.RewardRecipients { 32 | totalFeePercent = totalFeePercent + o.RewardRecipients[i].Percent 33 | } 34 | 35 | return totalFeePercent 36 | } 37 | -------------------------------------------------------------------------------- /config/p2p.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "strconv" 4 | 5 | type P2POptions struct { 6 | Host string `json:"host"` 7 | Port int `json:"port"` 8 | Magic string `json:"magic"` 9 | DisableTransactions bool `json:"disableTransactions"` 10 | } 11 | 12 | func (p2p *P2POptions) Addr() string { 13 | return p2p.Host + ":" + strconv.FormatInt(int64(p2p.Port), 10) 14 | } 15 | -------------------------------------------------------------------------------- /config/payment.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/mining-pool/not-only-mining-pool/utils" 7 | ) 8 | 9 | type Recipient struct { 10 | Address string `json:"address"` 11 | Type string `json:"type"` 12 | Percent float64 `json:"percent"` 13 | 14 | script []byte 15 | } 16 | 17 | func (r *Recipient) GetScript() []byte { 18 | if r.script == nil { 19 | switch strings.ToLower(r.Type) { 20 | case "p2sh": 21 | r.script = utils.P2SHAddressToScript(r.Address) 22 | case "p2pkh": 23 | r.script = utils.P2PKHAddressToScript(r.Address) 24 | case "p2wsh": 25 | r.script = utils.P2WSHAddressToScript(r.Address) 26 | case "pk", "publickey": 27 | r.script = utils.PublicKeyToScript(r.Address) 28 | case "script": 29 | r.script = utils.ScriptPubKeyToScript(r.Address) 30 | case "": 31 | log.Error(r.Address, " has no type!") 32 | default: 33 | log.Error(r.Address, " uses an unsupported type: ", r.Type) 34 | 35 | } 36 | } 37 | 38 | return r.script 39 | } 40 | -------------------------------------------------------------------------------- /config/redis.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "crypto/tls" 5 | "strconv" 6 | 7 | "github.com/go-redis/redis/v8" 8 | ) 9 | 10 | type RedisOptions struct { 11 | // The network type, either tcp or unix. 12 | // Default is tcp. 13 | Network string `json:"network"` 14 | 15 | Host string `json:"host"` 16 | Port int `json:"port"` 17 | 18 | Password string `json:"password"` 19 | DB int `json:"db"` 20 | 21 | TLS *TLSClientOptions `json:"tls"` 22 | } 23 | 24 | func (ro *RedisOptions) Addr() string { 25 | return ro.Host + ":" + strconv.Itoa(ro.Port) 26 | } 27 | 28 | func (ro *RedisOptions) ToRedisOptions() *redis.Options { 29 | var tlsConfig *tls.Config 30 | 31 | if ro.TLS != nil { 32 | tlsConfig = ro.TLS.ToTLSConfig() 33 | } 34 | 35 | return &redis.Options{ 36 | Network: ro.Network, 37 | Addr: ro.Addr(), 38 | Password: ro.Password, 39 | DB: ro.DB, 40 | TLSConfig: tlsConfig, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /config/stratum.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type PortOptions struct { 4 | Diff float64 `json:"diff"` 5 | VarDiff *VarDiffOptions `json:"varDiff"` 6 | TLS *TLSServerOptions `json:"tls"` 7 | } 8 | -------------------------------------------------------------------------------- /config/tls.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "crypto/tls" 5 | ) 6 | 7 | type TLSClientOptions struct { 8 | CertFile string `json:"certFile"` 9 | KeyFile string `json:"keyFile"` 10 | } 11 | 12 | func (to *TLSClientOptions) ToTLSConfig() *tls.Config { 13 | certs := make([]tls.Certificate, 0) 14 | if len(to.CertFile) > 0 && len(to.KeyFile) > 0 { 15 | cert, err := tls.LoadX509KeyPair(to.CertFile, to.KeyFile) 16 | if err != nil { 17 | log.Panic(err) 18 | } 19 | certs = append(certs, cert) 20 | } 21 | 22 | return &tls.Config{ 23 | Certificates: certs, 24 | InsecureSkipVerify: true, 25 | } 26 | } 27 | 28 | type TLSServerOptions struct { 29 | CertFile string `json:"certFile"` 30 | KeyFile string `json:"keyFile"` 31 | } 32 | 33 | func (to *TLSServerOptions) ToTLSConfig() *tls.Config { 34 | certs := make([]tls.Certificate, 0) 35 | if len(to.CertFile) > 0 && len(to.KeyFile) > 0 { 36 | cert, err := tls.LoadX509KeyPair(to.CertFile, to.KeyFile) 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | certs = append(certs, cert) 41 | } 42 | 43 | return &tls.Config{ 44 | Certificates: certs, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /config/vardiff.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type VarDiffOptions struct { 4 | MinDiff float64 `json:"minDiff"` 5 | MaxDiff float64 `json:"maxDiff"` 6 | TargetTime int64 `json:"targetTime"` 7 | RetargetTime int64 `json:"retargetTime"` 8 | VariancePercent float64 `json:"variancePercent"` 9 | X2Mode bool `json:"x2mode"` 10 | } 11 | -------------------------------------------------------------------------------- /daemons/getbalance.go: -------------------------------------------------------------------------------- 1 | package daemons 2 | 3 | type GetBalance float64 4 | -------------------------------------------------------------------------------- /daemons/getblock.go: -------------------------------------------------------------------------------- 1 | package daemons 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type GetBlock struct { 9 | Hash string `json:"hash"` 10 | Confirmations int `json:"confirmations"` 11 | Strippedsize int `json:"strippedsize"` 12 | Size int `json:"size"` 13 | Weight int `json:"weight"` 14 | Height int `json:"height"` 15 | Version int `json:"version"` 16 | VersionHex string `json:"versionHex"` 17 | Merkleroot string `json:"merkleroot"` 18 | Tx []string `json:"tx"` 19 | Time int `json:"time"` 20 | Mediantime int `json:"mediantime"` 21 | Nonce int `json:"nonce"` 22 | Bits string `json:"bits"` 23 | Difficulty float64 `json:"difficulty"` 24 | Chainwork string `json:"chainwork"` 25 | NTx int `json:"nTx"` 26 | Previousblockhash string `json:"previousblockhash"` 27 | } 28 | 29 | func BytesToGetBlock(b []byte) *GetBlock { 30 | var getBlock GetBlock 31 | err := json.Unmarshal(b, &getBlock) 32 | if err != nil { 33 | log.Fatal(fmt.Sprint("getblock call failed with error ", err)) 34 | } 35 | 36 | return &getBlock 37 | } 38 | -------------------------------------------------------------------------------- /daemons/getblockchaininfo.go: -------------------------------------------------------------------------------- 1 | package daemons 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type GetBlockchainInfo struct { 9 | Chain string `json:"chain"` 10 | Blocks int `json:"blocks"` 11 | Headers int `json:"headers"` 12 | Bestblockhash string `json:"bestblockhash"` 13 | Difficulty float64 `json:"difficulty"` 14 | Mediantime int `json:"mediantime"` 15 | Verificationprogress float64 `json:"verificationprogress"` 16 | Chainwork string `json:"chainwork"` 17 | Pruned bool `json:"pruned"` 18 | Softforks interface{} // having difference between LTC & BTC, so use interface to suppress the error 19 | Bip9Softforks struct { 20 | Csv struct { 21 | Status string `json:"status"` 22 | StartTime int `json:"startTime"` 23 | Timeout int `json:"timeout"` 24 | Since int `json:"since"` 25 | } `json:"csv"` 26 | Dip0001 struct { 27 | Status string `json:"status"` 28 | StartTime int `json:"startTime"` 29 | Timeout int `json:"timeout"` 30 | Since int `json:"since"` 31 | } `json:"dip0001"` 32 | Dip0003 struct { 33 | Status string `json:"status"` 34 | StartTime int `json:"startTime"` 35 | Timeout int `json:"timeout"` 36 | Since int `json:"since"` 37 | } `json:"dip0003"` 38 | Dip0008 struct { 39 | Status string `json:"status"` 40 | StartTime int `json:"startTime"` 41 | Timeout int `json:"timeout"` 42 | Since int `json:"since"` 43 | } `json:"dip0008"` 44 | Bip147 struct { 45 | Status string `json:"status"` 46 | StartTime int `json:"startTime"` 47 | Timeout int `json:"timeout"` 48 | Since int `json:"since"` 49 | } `json:"bip147"` 50 | } `json:"bip9_softforks"` 51 | } 52 | 53 | func BytesToGetBlockchainInfo(b []byte) *GetBlockchainInfo { 54 | var getBlockchainInfo GetBlockchainInfo 55 | err := json.Unmarshal(b, &getBlockchainInfo) 56 | if err != nil { 57 | log.Panic(fmt.Sprint("getblockchaininfo call failed with error: ", err)) 58 | } 59 | 60 | return &getBlockchainInfo 61 | } 62 | -------------------------------------------------------------------------------- /daemons/getblocktemplate.go: -------------------------------------------------------------------------------- 1 | package daemons 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | ) 8 | 9 | type MasternodeParams struct { 10 | Payee string `json:"payee"` 11 | Script string `json:"script"` 12 | Amount uint64 `json:"amount"` 13 | } 14 | 15 | type SuperblockParams struct { 16 | Payee string `json:"payee"` 17 | Script string `json:"script"` 18 | Amount uint64 `json:"amount"` 19 | } 20 | 21 | type TxParams struct { 22 | Data string `json:"data"` 23 | Hash string `json:"hash"` 24 | Depends []interface{} `json:"depends"` 25 | Fee uint64 `json:"fee"` 26 | Sigops int `json:"sigops"` 27 | TxId string `json:"txid"` 28 | } 29 | 30 | type GetBlockTemplate struct { 31 | // Base fields from BIP 0022. CoinbaseAux is optional. One of 32 | // CoinbaseTxn or CoinbaseValue must be specified, but not both. 33 | Version int32 `json:"version"` 34 | Bits string `json:"bits"` 35 | CurTime uint32 `json:"curtime"` 36 | Height int64 `json:"height"` 37 | // Rules []string `json:"rules"` 38 | PreviousBlockHash string `json:"previousblockhash"` 39 | // PreviousBits string `json:"previousbits"` 40 | // SigOpLimit int64 `json:"sigoplimit,omitempty"` 41 | // SizeLimit int64 `json:"sizelimit,omitempty"` 42 | // WeightLimit int64 `json:"weightlimit,omitempty"` 43 | // WorkID string `json:"workid,omitempty"` 44 | Transactions []*TxParams `json:"transactions"` 45 | // CoinbaseTxn *TxParams `json:"coinbasetxn,omitempty"` // Bitcoin does not produce the coinbasetxn for you, you will have to build it manually. 46 | CoinbaseAux struct { 47 | Flags string `json:"flags"` 48 | } `json:"coinbaseaux"` 49 | CoinbaseValue uint64 `json:"coinbasevalue"` 50 | 51 | // Block proposal from BIP 0023. 52 | // Capabilities []string `json:"capabilities,omitempty"` 53 | // RejectReason string `json:"reject-reason,omitempty"` 54 | 55 | //Vbavailable struct { 56 | //} `json:"vbavailable"` 57 | //Vbrequired int `json:"vbrequired"` 58 | 59 | // Witness commitment defined in BIP 0141. 60 | DefaultWitnessCommitment string `json:"default_witness_commitment,omitempty"` 61 | 62 | // Optional long polling from BIP 0022. 63 | // LongPollID string `json:"longpollid,omitempty"` 64 | // LongPollURI string `json:"longpolluri,omitempty"` 65 | // SubmitOld *bool `json:"submitold,omitempty"` 66 | 67 | // Basic pool extension from BIP 0023. 68 | Target string `json:"target,omitempty"` 69 | // Expires int64 `json:"expires,omitempty"` 70 | 71 | // Mutations from BIP 0023 72 | // MaxTime int64 `json:"maxtime,omitempty"` 73 | // MinTime int64 `json:"mintime,omitempty"` 74 | // Mutable []string `json:"mutable,omitempty"` 75 | // NonceRange string `json:"noncerange,omitempty"` 76 | 77 | // Dash 78 | Masternode []MasternodeParams `json:"masternode"` 79 | // MasternodePaymentsStarted bool `json:"masternode_payments_started"` 80 | // MasternodePaymentsEnforced bool `json:"masternode_payments_enforced"` 81 | 82 | Superblock []SuperblockParams `json:"superblock"` 83 | // SuperblocksStarted bool `json:"superblocks_started"` 84 | // SuperblocksEnabled bool `json:"superblocks_enabled"` 85 | CoinbasePayload string `json:"coinbase_payload"` 86 | 87 | // unknown 88 | Votes []string 89 | MasternodePayments interface{} 90 | Payee interface{} 91 | PayeeAmount interface{} 92 | } 93 | 94 | // then JobManager.ProcessTemplate(rpcData) 95 | func (dm *DaemonManager) GetBlockTemplate() (getBlockTemplate *GetBlockTemplate, err error) { 96 | instance, result, _ := dm.Cmd("getblocktemplate", 97 | []interface{}{map[string]interface{}{"capabilities": []string{"coinbasetxn", "workid", "coinbase/append"}, "rules": []string{"segwit"}}}, 98 | ) 99 | 100 | if result.Error != nil { 101 | return nil, errors.New(fmt.Sprint("getblocktemplate call failed for daemon instance ", instance, " with error ", result.Error)) 102 | } 103 | 104 | getBlockTemplate = BytesToGetBlockTemplate(result.Result) 105 | if getBlockTemplate == nil { 106 | return nil, errors.New(fmt.Sprint("getblocktemplate call failed for daemon instance ", instance, " with error ", getBlockTemplate)) 107 | } 108 | 109 | return getBlockTemplate, nil 110 | } 111 | 112 | func BytesToGetBlockTemplate(b []byte) *GetBlockTemplate { 113 | var getBlockTemplate GetBlockTemplate 114 | err := json.Unmarshal(b, &getBlockTemplate) 115 | if err != nil { 116 | log.Fatal(fmt.Sprint("getblocktemplate call failed with error ", err)) 117 | } 118 | 119 | return &getBlockTemplate 120 | } 121 | -------------------------------------------------------------------------------- /daemons/getdifficulty.go: -------------------------------------------------------------------------------- 1 | package daemons 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // type GetDifficulty interface {} 9 | 10 | func BytesToGetDifficulty(b []byte) interface{} { 11 | var getDifficulty interface{} 12 | err := json.Unmarshal(b, &getDifficulty) 13 | if err != nil { 14 | log.Fatal(fmt.Sprint("getDifficulty call failed with error ", err)) 15 | } 16 | 17 | return getDifficulty 18 | } 19 | -------------------------------------------------------------------------------- /daemons/getinfo.go: -------------------------------------------------------------------------------- 1 | package daemons 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type GetInfo struct { 9 | Version int `json:"version"` 10 | Protocolversion int `json:"protocolversion"` 11 | Walletversion int `json:"walletversion"` 12 | Balance float64 `json:"balance"` 13 | PrivatesendBalance float64 `json:"privatesend_balance"` 14 | Blocks int `json:"blocks"` 15 | Timeoffset int `json:"timeoffset"` 16 | Connections int `json:"connections"` 17 | Proxy string `json:"proxy"` 18 | Difficulty float64 `json:"difficulty"` 19 | Testnet bool `json:"testnet"` 20 | Keypoololdest int `json:"keypoololdest"` 21 | Keypoolsize int `json:"keypoolsize"` 22 | Paytxfee float64 `json:"paytxfee"` 23 | Relayfee float64 `json:"relayfee"` 24 | Errors string `json:"errors"` 25 | } 26 | 27 | func BytesToGetInfo(b []byte) *GetInfo { 28 | var getInfo GetInfo 29 | err := json.Unmarshal(b, &getInfo) 30 | if err != nil { 31 | log.Fatal(fmt.Sprint("getDifficulty call failed with error ", err)) 32 | } 33 | 34 | return &getInfo 35 | } 36 | -------------------------------------------------------------------------------- /daemons/getmininginfo.go: -------------------------------------------------------------------------------- 1 | package daemons 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type GetMiningInfo struct { 9 | Blocks int `json:"blocks"` 10 | Currentblocksize int `json:"currentblocksize"` 11 | Currentblocktx int `json:"currentblocktx"` 12 | Difficulty float64 `json:"difficulty"` 13 | Errors string `json:"errors"` 14 | Networkhashps float64 `json:"networkhashps"` 15 | Pooledtx int `json:"pooledtx"` 16 | Chain string `json:"chain"` 17 | } 18 | 19 | func BytesToGetMiningInfo(b []byte) *GetMiningInfo { 20 | var getMiningInfo GetMiningInfo 21 | err := json.Unmarshal(b, &getMiningInfo) 22 | if err != nil { 23 | log.Fatal(fmt.Sprint("getDifficulty call failed with error ", err)) 24 | } 25 | 26 | return &getMiningInfo 27 | } 28 | -------------------------------------------------------------------------------- /daemons/getnetworkinfo.go: -------------------------------------------------------------------------------- 1 | package daemons 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type GetNetworkInfo struct { 9 | Version int `json:"version"` 10 | Subversion string `json:"subversion"` 11 | Protocolversion int `json:"protocolversion"` 12 | Localservices string `json:"localservices"` 13 | Localrelay bool `json:"localrelay"` 14 | Timeoffset int `json:"timeoffset"` 15 | Networkactive bool `json:"networkactive"` 16 | Connections int `json:"connections"` 17 | Networks []struct { 18 | Name string `json:"name"` 19 | Limited bool `json:"limited"` 20 | Reachable bool `json:"reachable"` 21 | Proxy string `json:"proxy"` 22 | ProxyRandomizeCredentials bool `json:"proxy_randomize_credentials"` 23 | } `json:"networks"` 24 | Relayfee float64 `json:"relayfee"` 25 | Incrementalfee float64 `json:"incrementalfee"` 26 | Localaddresses []interface{} `json:"localaddresses"` 27 | Warnings string `json:"warnings"` 28 | } 29 | 30 | func BytesToGetNetworkInfo(b []byte) *GetNetworkInfo { 31 | var getNetworkInfo GetNetworkInfo 32 | err := json.Unmarshal(b, &getNetworkInfo) 33 | if err != nil { 34 | log.Fatal(fmt.Sprint("getDifficulty call failed with error ", err)) 35 | } 36 | 37 | return &getNetworkInfo 38 | } 39 | -------------------------------------------------------------------------------- /daemons/getwalletinfo.go: -------------------------------------------------------------------------------- 1 | package daemons 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type GetWalletInfo struct { 9 | Walletversion int `json:"walletversion"` 10 | Balance float64 `json:"balance"` 11 | PrivatesendBalance float64 `json:"privatesend_balance"` 12 | UnconfirmedBalance float64 `json:"unconfirmed_balance"` 13 | ImmatureBalance float64 `json:"immature_balance"` 14 | Txcount int `json:"txcount"` 15 | Keypoololdest int `json:"keypoololdest"` 16 | Keypoolsize int `json:"keypoolsize"` 17 | KeysLeft int `json:"keys_left"` 18 | Paytxfee float64 `json:"paytxfee"` 19 | } 20 | 21 | func BytesToGetWalletInfo(b []byte) *GetWalletInfo { 22 | var getWalletInfo GetWalletInfo 23 | err := json.Unmarshal(b, &getWalletInfo) 24 | if err != nil { 25 | log.Fatal(fmt.Sprint("getDifficulty call failed with error ", err)) 26 | } 27 | 28 | return &getWalletInfo 29 | } 30 | -------------------------------------------------------------------------------- /daemons/jsonrpc.go: -------------------------------------------------------------------------------- 1 | package daemons 2 | 3 | import "encoding/json" 4 | 5 | type JsonRpc interface { 6 | GetJsonRpcId() int64 7 | Json() []byte 8 | } 9 | 10 | type JsonRpcResponse struct { 11 | Id interface{} `json:"id"` // be int64 or null 12 | Result json.RawMessage `json:"result,omitempty"` 13 | Error *JsonRpcError `json:"error,omitempty"` 14 | } 15 | 16 | func (j *JsonRpcResponse) GetJsonRpcId() int64 { 17 | if j.Id == nil { 18 | return 0 19 | } 20 | 21 | return j.Id.(int64) 22 | } 23 | 24 | func (j *JsonRpcResponse) Json() []byte { 25 | raw, _ := json.Marshal(j) 26 | return raw 27 | } 28 | 29 | type JsonRpcRequest struct { 30 | Id interface{} `json:"id"` 31 | Method string `json:"method"` 32 | Params []json.RawMessage `json:"params"` 33 | } 34 | 35 | func (j *JsonRpcRequest) GetJsonRpcId() int64 { 36 | if j.Id == nil { 37 | return 0 38 | } 39 | 40 | return j.Id.(int64) 41 | } 42 | 43 | func (j *JsonRpcRequest) Json() []byte { 44 | raw, _ := json.Marshal(j) 45 | return raw 46 | } 47 | 48 | type JsonRpcError struct { 49 | Code int `json:"code"` 50 | Message string `json:"message"` 51 | } 52 | 53 | //type Method string 54 | // 55 | //const ( 56 | // MethodSubmitBlock Method = "getsubmitblock" 57 | // MethodGetBlockTemplate Method = "getblocktemplate" 58 | // MethodGetBlock Method = "getblock" 59 | // MethodGetBalance Method = "getbalance" 60 | // MethodValidateAddress Method = "validateaddress" 61 | // ) 62 | -------------------------------------------------------------------------------- /daemons/manager.go: -------------------------------------------------------------------------------- 1 | package daemons 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "io/ioutil" 8 | "net/http" 9 | "strconv" 10 | "sync" 11 | 12 | logging "github.com/ipfs/go-log/v2" 13 | "github.com/mining-pool/not-only-mining-pool/config" 14 | "github.com/mining-pool/not-only-mining-pool/utils" 15 | ) 16 | 17 | var log = logging.Logger("daemons") 18 | 19 | type DaemonManager struct { 20 | Daemons []*config.DaemonOptions 21 | clients map[string]*http.Client 22 | Coin *config.CoinOptions 23 | } 24 | 25 | func NewDaemonManager(daemons []*config.DaemonOptions, coin *config.CoinOptions) *DaemonManager { 26 | if daemons == nil || coin == nil { 27 | log.Fatal("new daemon with empty options!") 28 | } 29 | 30 | clients := make(map[string]*http.Client) 31 | for _, daemon := range daemons { 32 | transport := &http.Transport{} 33 | if daemon.TLS != nil { 34 | transport.TLSClientConfig = daemon.TLS.ToTLSConfig() 35 | } 36 | 37 | client := &http.Client{Transport: transport} 38 | clients[daemon.String()] = client 39 | } 40 | 41 | return &DaemonManager{ 42 | Daemons: daemons, 43 | Coin: coin, 44 | clients: clients, 45 | } 46 | } 47 | 48 | func (dm *DaemonManager) Check() { 49 | if !dm.IsAllOnline() { 50 | log.Fatal("daemons are not all online!") 51 | } 52 | } 53 | 54 | func (dm *DaemonManager) IsAllOnline() bool { 55 | responses, _ := dm.CmdAll("getpeerinfo", []interface{}{}) 56 | for _, res := range responses { 57 | if res.StatusCode/100 != 2 { 58 | return false 59 | } 60 | 61 | var jsonRes JsonRpcResponse 62 | err := json.NewDecoder(res.Body).Decode(&jsonRes) 63 | if err != nil { 64 | log.Error(err) 65 | return false 66 | } 67 | 68 | if jsonRes.Error != nil { 69 | log.Error(jsonRes.Error) 70 | return false 71 | } 72 | 73 | } 74 | 75 | return true 76 | } 77 | 78 | func (dm *DaemonManager) DoHttpRequest(daemon *config.DaemonOptions, reqRawData []byte) (*http.Response, error) { 79 | client := dm.clients[daemon.String()] 80 | 81 | req, err := http.NewRequest("POST", daemon.URL(), bytes.NewReader(reqRawData)) 82 | if err != nil { 83 | log.Panic(err) 84 | } 85 | if daemon.User != "" { 86 | req.SetBasicAuth(daemon.User, daemon.Password) 87 | } 88 | 89 | return client.Do(req) 90 | } 91 | 92 | func (dm *DaemonManager) BatchCmd(commands []interface{}) (*config.DaemonOptions, []*JsonRpcResponse, error) { 93 | requestJson := make([]map[string]interface{}, len(commands)) 94 | for i := range commands { 95 | requestJson[i] = map[string]interface{}{ 96 | "id": utils.RandPositiveInt64(), 97 | "method": commands[i].([]interface{})[0], 98 | "params": commands[i].([]interface{})[1], 99 | } 100 | } 101 | 102 | for i := range dm.Daemons { 103 | raw, _ := json.Marshal(requestJson) 104 | res, err := dm.DoHttpRequest(dm.Daemons[i], raw) 105 | if err != nil { 106 | return dm.Daemons[i], nil, err 107 | } 108 | var rpcResponses []*JsonRpcResponse 109 | err = json.NewDecoder(res.Body).Decode(&rpcResponses) 110 | if err != nil { 111 | return dm.Daemons[i], nil, err 112 | } 113 | 114 | return dm.Daemons[i], rpcResponses, err 115 | } 116 | 117 | return nil, nil, nil 118 | } 119 | 120 | // CmdAll sends the rpc call to all daemon, and never break because of any error. 121 | // So the elem in responses may be nil 122 | func (dm *DaemonManager) CmdAll(method string, params []interface{}) (responses []*http.Response, results []*JsonRpcResponse) { 123 | responses = make([]*http.Response, len(dm.Daemons)) 124 | results = make([]*JsonRpcResponse, len(dm.Daemons)) 125 | 126 | msg := map[string]interface{}{ 127 | "id": utils.RandPositiveInt64(), 128 | "method": method, 129 | "params": params, 130 | } 131 | 132 | reqRawData, err := json.Marshal(msg) 133 | if err != nil { 134 | log.Errorf("failed marshaling %v: %s", msg, err) 135 | return // all elem are nil 136 | } 137 | 138 | log.Debug(string(reqRawData)) 139 | 140 | wg := sync.WaitGroup{} 141 | for i := range dm.Daemons { 142 | wg.Add(1) 143 | go func(i int) { 144 | res, err := dm.DoHttpRequest(dm.Daemons[i], reqRawData) 145 | if err != nil { 146 | log.Errorf("failed on daemon %s: %s", dm.Daemons[i].String(), err) 147 | return 148 | } 149 | 150 | //if err := dm.CheckStatusCode(res.StatusCode); err != nil { 151 | // log.Println(err) 152 | //} 153 | 154 | responses[i] = res 155 | 156 | var result JsonRpcResponse 157 | raw, err := ioutil.ReadAll(res.Body) 158 | if err != nil { 159 | log.Error(err) 160 | } 161 | res.Body = ioutil.NopCloser(bytes.NewBuffer(raw)) 162 | 163 | err = json.Unmarshal(raw, &result) 164 | if err != nil { 165 | log.Panicf("failed to unmarshal response body: %s", raw) 166 | } 167 | 168 | results[i] = &result 169 | 170 | wg.Done() 171 | }(i) 172 | } 173 | 174 | wg.Wait() 175 | 176 | return responses, results 177 | } 178 | 179 | func (dm *DaemonManager) CheckStatusCode(statusCode int) error { 180 | switch statusCode / 100 { 181 | case 2: 182 | return nil 183 | case 4: 184 | switch statusCode % 100 { 185 | case 0: 186 | return errors.New("daemon cannot understand the request") 187 | case 1: 188 | return errors.New("daemon requires authorization (have to login before request)") 189 | case 3: 190 | return errors.New("daemon rejected the request") 191 | 192 | case 4: 193 | return errors.New("daemon cannot find the resource requested") 194 | case 13: 195 | return errors.New("daemon cannot deal this large request") 196 | 197 | } 198 | 199 | case 5: 200 | return errors.New("daemon internal error") 201 | } 202 | 203 | return errors.New("unknown status code:" + strconv.Itoa(statusCode)) 204 | } 205 | 206 | // Cmd will call daemons one by one and return the first answer 207 | // one by one not all is to try fetching from the same one not random one 208 | func (dm *DaemonManager) Cmd(method string, params []interface{}) (*config.DaemonOptions, *JsonRpcResponse, *http.Response) { 209 | reqRawData, err := json.Marshal(map[string]interface{}{ 210 | "id": utils.RandPositiveInt64(), 211 | "method": method, 212 | "params": params, 213 | }) 214 | if err != nil { 215 | log.Error(err) 216 | } 217 | 218 | var result JsonRpcResponse 219 | var res *http.Response 220 | for i := range dm.Daemons { 221 | var err error 222 | res, err = dm.DoHttpRequest(dm.Daemons[i], reqRawData) 223 | if err != nil { 224 | log.Error(err) 225 | } 226 | 227 | //if err := dm.CheckStatusCode(res.StatusCode); err != nil { 228 | // log.Println(err) 229 | //} 230 | 231 | raw, err := ioutil.ReadAll(res.Body) 232 | if err != nil { 233 | log.Error(err) 234 | } 235 | res.Body = ioutil.NopCloser(bytes.NewBuffer(raw)) 236 | 237 | err = json.Unmarshal(raw, &result) 238 | if err != nil { 239 | log.Error(err) 240 | } 241 | 242 | return dm.Daemons[i], &result, res 243 | } 244 | 245 | log.Error("failed getting GBT from all daemons!") 246 | return nil, nil, nil 247 | } 248 | -------------------------------------------------------------------------------- /daemons/submitblock.go: -------------------------------------------------------------------------------- 1 | package daemons 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/mining-pool/not-only-mining-pool/utils" 7 | ) 8 | 9 | // submitblock has no result 10 | func (dm *DaemonManager) SubmitBlock(blockHex string) { 11 | var results []*JsonRpcResponse 12 | if dm.Coin.NoSubmitBlock { 13 | _, results = dm.CmdAll("getblocktemplate", []interface{}{map[string]interface{}{"mode": "submit", "data": blockHex}}) 14 | } else { 15 | _, results = dm.CmdAll("submitblock", []interface{}{blockHex}) 16 | } 17 | 18 | for i := range results { 19 | if results[i] == nil { 20 | log.Errorf("failed submitting to daemon %s, see log above for details", dm.Daemons[i].String()) 21 | continue 22 | } 23 | 24 | if results[i].Error != nil { 25 | log.Error("rpc error with daemon when submitting block: " + string(utils.Jsonify(results[i].Error))) 26 | } else { 27 | var result string 28 | err := json.Unmarshal(results[i].Result, &result) 29 | if err == nil && result == "rejected" { 30 | log.Error("Daemon instance rejected a supposedly valid block") 31 | } 32 | 33 | if err == nil && result == "invalid" { 34 | log.Error("Daemon instance rejected an invalid block") 35 | } 36 | 37 | if err == nil && result == "inconclusive" { 38 | log.Warn("Daemon instance warns an inconclusive block") 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /daemons/validateaddress.go: -------------------------------------------------------------------------------- 1 | package daemons 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type ValidateAddress struct { 9 | Isvalid bool `json:"isvalid"` 10 | Address string `json:"address"` 11 | ScriptPubKey string `json:"scriptPubKey"` 12 | Ismine bool `json:"ismine"` 13 | Iswatchonly bool `json:"iswatchonly"` 14 | Isscript bool `json:"isscript"` 15 | Iswitness bool `json:"iswitness"` 16 | Script string `json:"script"` 17 | Hex string `json:"hex"` 18 | Pubkey string `json:"pubkey"` 19 | Embedded struct { 20 | Isscript bool `json:"isscript"` 21 | Iswitness bool `json:"iswitness"` 22 | WitnessVersion int `json:"witness_version"` 23 | WitnessProgram string `json:"witness_program"` 24 | Pubkey string `json:"pubkey"` 25 | Address string `json:"address"` 26 | ScriptPubKey string `json:"scriptPubKey"` 27 | } `json:"embedded"` 28 | Addresses []string `json:"addresses"` 29 | Label string `json:"label"` 30 | Timestamp int `json:"timestamp"` 31 | Hdkeypath string `json:"hdkeypath"` 32 | Hdseedid string `json:"hdseedid"` 33 | Hdmasterkeyid string `json:"hdmasterkeyid"` 34 | Labels []struct { 35 | Name string `json:"name"` 36 | Purpose string `json:"purpose"` 37 | } `json:"labels"` 38 | } 39 | 40 | func BytesToValidateAddress(b []byte) *ValidateAddress { 41 | var validateAddress ValidateAddress 42 | err := json.Unmarshal(b, &validateAddress) 43 | if err != nil { 44 | log.Fatal(fmt.Sprint("validateAddress call failed with error ", err)) 45 | } 46 | 47 | return &validateAddress 48 | } 49 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mining-pool/not-only-mining-pool 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/c0mm4nd/go-bech32 v0.0.0-20201015031713-6bb434e0ac5d 7 | github.com/go-redis/redis/v8 v8.3.1 8 | github.com/gorilla/mux v1.8.0 9 | github.com/ipfs/go-log/v2 v2.1.1 10 | github.com/mr-tron/base58 v1.2.0 11 | github.com/samli88/go-x11-hash v0.0.0-20180604013825-c37c1282f506 12 | golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= 4 | github.com/btcsuite/btcd v0.20.1-beta h1:Ik4hyJqN8Jfyv3S4AGBOmyouMsYE3EdYODkMbQjwPGw= 5 | github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= 6 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= 7 | github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= 8 | github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts= 9 | github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= 10 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= 11 | github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= 12 | github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= 13 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= 14 | github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= 15 | github.com/c0mm4nd/go-bech32 v0.0.0-20201015031713-6bb434e0ac5d h1:nQf3TD5A+VPDemt6zG0MQWMhlCx9aaNooC4+JONwlXs= 16 | github.com/c0mm4nd/go-bech32 v0.0.0-20201015031713-6bb434e0ac5d/go.mod h1:D08DIm3/NJ0pLz+OWLYfGtVqVLTns8Y5sA3vHhcMkww= 17 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= 18 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 19 | github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495 h1:6IyqGr3fnd0tM3YxipK27TUskaOVUjU2nG45yzwcQKY= 20 | github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 22 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 23 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 25 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 26 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 27 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 28 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 29 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 30 | github.com/go-redis/redis/v8 v8.3.1 h1:jEPCgHQopfNaABun3NVN9pv2K7RjstY/7UJD6UEKFEY= 31 | github.com/go-redis/redis/v8 v8.3.1/go.mod h1:a2xkpBM7NJUN5V5kiF46X5Ltx4WeXJ9757X/ScKUBdE= 32 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 33 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 34 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 35 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 36 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 37 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 38 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 39 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 40 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 41 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 42 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 43 | github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= 44 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 45 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 46 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 47 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 48 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 49 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 50 | github.com/ipfs/go-log/v2 v2.1.1 h1:G4TtqN+V9y9HY9TA6BwbCVyyBZ2B9MbCjR2MtGx8FR0= 51 | github.com/ipfs/go-log/v2 v2.1.1/go.mod h1:2v2nsGfZsvvAJz13SyFzf9ObaqwHiHxsPLEHntrv9KM= 52 | github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 53 | github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= 54 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 55 | github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= 56 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 57 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 58 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 59 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 60 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 61 | github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 62 | github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 63 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= 64 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 65 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 66 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 67 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 68 | github.com/onsi/ginkgo v1.14.1 h1:jMU0WaQrP0a/YAEq8eJmJKjBoMs+pClEr1vDMlM/Do4= 69 | github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 70 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 71 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 72 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 73 | github.com/onsi/gomega v1.10.2 h1:aY/nuoWlKJud2J6U0E3NWsjlg+0GtwXxgEqthRdzlcs= 74 | github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 75 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 76 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 77 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 78 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 79 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 80 | github.com/samli88/go-x11-hash v0.0.0-20180604013825-c37c1282f506 h1:BBdYETqJwXeC8LpzMyuDajo4qLU7l87dhB59IUqV0+c= 81 | github.com/samli88/go-x11-hash v0.0.0-20180604013825-c37c1282f506/go.mod h1:AN54awRQDkyEDfZ7XlAz1GaXRdenWUoJijL17HHIsh4= 82 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 83 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 84 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 85 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 86 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 87 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 88 | go.opentelemetry.io/otel v0.13.0 h1:2isEnyzjjJZq6r2EKMsFj4TxiQiexsM04AVhwbR/oBA= 89 | go.opentelemetry.io/otel v0.13.0/go.mod h1:dlSNewoRYikTkotEnxdmuBHgzT+k/idJSfDv/FxEnOY= 90 | go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= 91 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 92 | go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= 93 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 94 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= 95 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 96 | go.uber.org/zap v1.14.1 h1:nYDKopTbvAPq/NrUVZwT15y2lpROBiLLyoRTbXOYWOo= 97 | go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= 98 | golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 99 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 100 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 101 | golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 102 | golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee h1:4yd7jl+vXjalO5ztz6Vc1VADv+S/80LGJmyl1ROJ2AI= 103 | golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 104 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 105 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 106 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 107 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 108 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 109 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 110 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 111 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0= 112 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 113 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 114 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 115 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 116 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 117 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 118 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 119 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 120 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 121 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 122 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4= 123 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 124 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 125 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 126 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 127 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 128 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 129 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 130 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 131 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= 132 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 133 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 134 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 135 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 136 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 137 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 138 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 139 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 140 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 141 | google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= 142 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 143 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 144 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 145 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 146 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 147 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 148 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 149 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 150 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 151 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 152 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 153 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 154 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 155 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 156 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 157 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 158 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 159 | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= 160 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 161 | -------------------------------------------------------------------------------- /jobs/extraNonce1Generator.go: -------------------------------------------------------------------------------- 1 | package jobs 2 | 3 | import "crypto/rand" 4 | 5 | type ExtraNonce1Generator struct { 6 | Size int 7 | } 8 | 9 | func NewExtraNonce1Generator() *ExtraNonce1Generator { 10 | return &ExtraNonce1Generator{ 11 | Size: 4, 12 | } 13 | } 14 | 15 | func (eng *ExtraNonce1Generator) GetExtraNonce1() []byte { 16 | extraNonce := make([]byte, eng.Size) 17 | _, _ = rand.Read(extraNonce) 18 | 19 | return extraNonce 20 | } 21 | -------------------------------------------------------------------------------- /jobs/job.go: -------------------------------------------------------------------------------- 1 | package jobs 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "math/big" 8 | "time" 9 | 10 | "github.com/mining-pool/not-only-mining-pool/algorithm" 11 | "github.com/mining-pool/not-only-mining-pool/config" 12 | "github.com/mining-pool/not-only-mining-pool/daemons" 13 | "github.com/mining-pool/not-only-mining-pool/merkletree" 14 | "github.com/mining-pool/not-only-mining-pool/transactions" 15 | "github.com/mining-pool/not-only-mining-pool/utils" 16 | ) 17 | 18 | type Job struct { 19 | GetBlockTemplate *daemons.GetBlockTemplate 20 | Submits []string 21 | GenerationTransaction [][]byte 22 | JobId string 23 | PrevHashReversed string 24 | MerkleBranch []string 25 | Target *big.Int 26 | Difficulty *big.Float 27 | TransactionData []byte 28 | Reward string 29 | MerkleTree *merkletree.MerkleTree 30 | } 31 | 32 | func NewJob(jobId string, rpcData *daemons.GetBlockTemplate, poolAddressScript, extraNoncePlaceholder []byte, reward string, txMessages bool, recipients []*config.Recipient) *Job { 33 | var bigTarget *big.Int 34 | 35 | if rpcData.Target != "" { 36 | bigTarget, _ = new(big.Int).SetString(rpcData.Target, 16) 37 | } else { 38 | utils.BigIntFromBitsHex(rpcData.Bits) 39 | } 40 | 41 | bigDiff := new(big.Float).Quo( 42 | new(big.Float).SetInt(algorithm.MaxTargetTruncated), 43 | new(big.Float).SetInt(bigTarget), 44 | ) 45 | 46 | bPreviousBlockHash, err := hex.DecodeString(rpcData.PreviousBlockHash) 47 | if err != nil { 48 | log.Error(err) 49 | } 50 | prevHashReversed := hex.EncodeToString(utils.ReverseByteOrder(bPreviousBlockHash)) 51 | 52 | transactionData := make([][]byte, len(rpcData.Transactions)) 53 | for i := 0; i < len(rpcData.Transactions); i++ { 54 | transactionData[i], err = hex.DecodeString(rpcData.Transactions[i].Data) 55 | if err != nil { 56 | log.Error(err) 57 | } 58 | } 59 | 60 | txsBytes := GetTransactionBytes(rpcData.Transactions) 61 | merkleTree := merkletree.NewMerkleTree(txsBytes) 62 | merkleBranch := merkletree.GetMerkleHashes(merkleTree.Steps) 63 | generationTransaction := transactions.CreateGeneration( 64 | rpcData, 65 | poolAddressScript, 66 | extraNoncePlaceholder, 67 | reward, 68 | txMessages, 69 | recipients, 70 | ) 71 | 72 | txData := make([][]byte, len(rpcData.Transactions)) 73 | for i := 0; i < len(rpcData.Transactions); i++ { 74 | data, err := hex.DecodeString(rpcData.Transactions[i].Data) 75 | if err != nil { 76 | log.Panic("failed to decode tx: ", rpcData.Transactions[i]) 77 | } 78 | 79 | txData[i] = data 80 | } 81 | 82 | log.Info("New Job, diff: ", bigDiff) 83 | 84 | return &Job{ 85 | GetBlockTemplate: rpcData, 86 | Submits: nil, 87 | GenerationTransaction: generationTransaction, 88 | JobId: jobId, 89 | PrevHashReversed: prevHashReversed, 90 | MerkleBranch: merkleBranch, 91 | Target: bigTarget, 92 | Difficulty: bigDiff, 93 | TransactionData: bytes.Join(txData, nil), 94 | Reward: "", 95 | MerkleTree: merkleTree, 96 | } 97 | } 98 | 99 | func (j *Job) SerializeCoinbase(extraNonce1, extraNonce2 []byte) []byte { 100 | if j.GenerationTransaction[0] == nil || j.GenerationTransaction[1] == nil { 101 | log.Warn("empty generation transaction", j.GenerationTransaction) 102 | } 103 | 104 | return bytes.Join([][]byte{ 105 | j.GenerationTransaction[0], 106 | extraNonce1, 107 | extraNonce2, 108 | j.GenerationTransaction[1], 109 | }, nil) 110 | } 111 | 112 | func (j *Job) SerializeBlock(header, coinbase []byte) []byte { 113 | // POS coins require a zero byte appended to block which the daemon replaces with the signature 114 | var suffix []byte 115 | if j.Reward == "POS" { 116 | suffix = []byte{0} 117 | } else { 118 | suffix = []byte{} 119 | } 120 | 121 | if j.TransactionData == nil { 122 | log.Warn("transaction data is empty") 123 | } 124 | 125 | voteData := j.GetVoteData() 126 | if voteData == nil { 127 | log.Warn("no vote data") 128 | } 129 | 130 | return bytes.Join([][]byte{ 131 | header, 132 | 133 | utils.VarIntBytes(uint64(len(j.GetBlockTemplate.Transactions) + 1)), // coinbase(generation) + txs 134 | coinbase, 135 | j.TransactionData, 136 | 137 | j.GetVoteData(), 138 | 139 | suffix, 140 | }, nil) 141 | } 142 | 143 | //https://en.bitcoin.it/wiki/Protocol_specification#Block_Headers 144 | func (j *Job) SerializeHeader(merkleRoot, nTime, nonce []byte) []byte { 145 | header := make([]byte, 80) 146 | 147 | bits, _ := hex.DecodeString(j.GetBlockTemplate.Bits) 148 | prevHash, _ := hex.DecodeString(j.GetBlockTemplate.PreviousBlockHash) 149 | 150 | pos := 0 151 | copy(header[pos:], nonce) // 4 152 | pos += len(nonce) 153 | copy(header[pos:], bits) // 4 154 | pos += len(bits) 155 | copy(header[pos:], nTime) // 4 156 | pos += len(nTime) 157 | copy(header[pos:], merkleRoot) // 32 158 | pos += len(merkleRoot) 159 | copy(header[pos:], prevHash) // 32 160 | pos += len(prevHash) 161 | binary.BigEndian.PutUint32(header[pos:], uint32(j.GetBlockTemplate.Version)) // 4 162 | pos += 4 163 | 164 | return utils.ReverseBytes(header) 165 | } 166 | 167 | // record the submit times and contents => check duplicate 168 | func (j *Job) RegisterSubmit(extraNonce1, extraNonce2, nTime, nonce string) bool { 169 | submission := extraNonce1 + extraNonce2 + nTime + nonce 170 | 171 | if utils.StringsIndexOf(j.Submits, submission) == -1 { 172 | j.Submits = append(j.Submits, submission) 173 | return true 174 | } 175 | 176 | return false 177 | } 178 | 179 | func (j *Job) GetJobParams(forceUpdate bool) []interface{} { 180 | return []interface{}{ 181 | j.JobId, 182 | j.PrevHashReversed, 183 | hex.EncodeToString(j.GenerationTransaction[0]), 184 | hex.EncodeToString(j.GenerationTransaction[1]), 185 | j.MerkleBranch, 186 | hex.EncodeToString(utils.PackInt32BE(j.GetBlockTemplate.Version)), 187 | j.GetBlockTemplate.Bits, 188 | hex.EncodeToString(utils.PackUint32BE(uint32(time.Now().Unix()))), // Updated: implement time rolling 189 | forceUpdate, 190 | } 191 | } 192 | 193 | func GetTransactionBytes(txs []*daemons.TxParams) [][]byte { 194 | txHashes := make([][]byte, len(txs)) 195 | for i := 0; i < len(txs); i++ { 196 | if txs[i].TxId != "" { 197 | txHashes[i] = utils.Uint256BytesFromHash(txs[i].TxId) 198 | continue 199 | } 200 | 201 | if txs[i].Hash != "" { 202 | txHashes[i] = utils.Uint256BytesFromHash(txs[i].Hash) 203 | continue 204 | } 205 | 206 | log.Panic("no hash or txid in transactions params") 207 | } 208 | 209 | return append([][]byte{nil}, txHashes...) 210 | } 211 | 212 | func (j *Job) GetVoteData() []byte { 213 | if j.GetBlockTemplate.MasternodePayments == nil { 214 | return nil 215 | } 216 | 217 | hexVotes := make([][]byte, len(j.GetBlockTemplate.Votes)) 218 | for i := 0; i < len(j.GetBlockTemplate.Votes); i++ { 219 | hexVotes[i], _ = hex.DecodeString(j.GetBlockTemplate.Votes[i]) 220 | } 221 | 222 | return bytes.Join([][]byte{ 223 | utils.VarIntBytes(uint64(len(j.GetBlockTemplate.Votes))), 224 | }, nil) 225 | } 226 | -------------------------------------------------------------------------------- /jobs/jobManager.go: -------------------------------------------------------------------------------- 1 | package jobs 2 | 3 | import ( 4 | "encoding/hex" 5 | "math/big" 6 | "net" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | logging "github.com/ipfs/go-log/v2" 12 | "github.com/mining-pool/not-only-mining-pool/algorithm" 13 | "github.com/mining-pool/not-only-mining-pool/config" 14 | "github.com/mining-pool/not-only-mining-pool/daemons" 15 | "github.com/mining-pool/not-only-mining-pool/storage" 16 | "github.com/mining-pool/not-only-mining-pool/types" 17 | "github.com/mining-pool/not-only-mining-pool/utils" 18 | ) 19 | 20 | var log = logging.Logger("jobMgr") 21 | 22 | type JobCounter struct { 23 | Counter *big.Int 24 | } 25 | 26 | type JobManager struct { 27 | PoolAddress *config.Recipient 28 | 29 | Storage *storage.DB 30 | Options *config.Options 31 | JobCounter *JobCounter 32 | ExtraNonce1Generator *ExtraNonce1Generator 33 | ExtraNoncePlaceholder []byte 34 | ExtraNonce2Size int 35 | 36 | CurrentJob *Job 37 | ValidJobs map[string]*Job 38 | 39 | CoinbaseHasher func([]byte) []byte 40 | 41 | DaemonManager *daemons.DaemonManager 42 | 43 | NewBlockEvent chan *Job 44 | } 45 | 46 | func NewJobManager(options *config.Options, dm *daemons.DaemonManager, storage *storage.DB) *JobManager { 47 | placeholder, _ := hex.DecodeString("f000000ff111111f") 48 | extraNonce1Generator := NewExtraNonce1Generator() 49 | 50 | return &JobManager{ 51 | PoolAddress: options.PoolAddress, 52 | 53 | Options: options, 54 | ExtraNonce1Generator: extraNonce1Generator, 55 | ExtraNoncePlaceholder: placeholder, 56 | ExtraNonce2Size: len(placeholder) - extraNonce1Generator.Size, 57 | CurrentJob: nil, 58 | ValidJobs: make(map[string]*Job), 59 | CoinbaseHasher: utils.Sha256d, 60 | Storage: storage, 61 | DaemonManager: dm, 62 | } 63 | } 64 | 65 | func (jm *JobManager) Init(gbt *daemons.GetBlockTemplate) { 66 | jm.ProcessTemplate(gbt) 67 | } 68 | 69 | func (jm *JobManager) ProcessShare(share *types.Share) { 70 | // isValidBlock 71 | var isAccepted bool 72 | var tx string 73 | if share.BlockHex != "" { 74 | log.Info("submitting new Block: ", share.BlockHex) 75 | jm.DaemonManager.SubmitBlock(share.BlockHex) 76 | 77 | isAccepted, tx = jm.CheckBlockAccepted(share.BlockHash) 78 | share.TxHash = tx 79 | if isAccepted { 80 | log.Info("Block ", share.BlockHash, " Accepted! generation tx: ", share.TxHash, ". Wait for pendding!") 81 | } 82 | 83 | gbt, err := jm.DaemonManager.GetBlockTemplate() 84 | if err != nil { 85 | log.Panic("failed fetching GBT: ", err) 86 | } 87 | jm.ProcessTemplate(gbt) 88 | } 89 | 90 | // notValidBlock but isValidShare 91 | go jm.Storage.PutShare(share, isAccepted) 92 | 93 | } 94 | 95 | func (jm *JobManager) CheckBlockAccepted(blockHash string) (isAccepted bool, tx string) { 96 | _, results := jm.DaemonManager.CmdAll("getblock", []interface{}{blockHash}) 97 | 98 | isAccepted = true 99 | for i := range results { 100 | isAccepted = isAccepted && results[i] != nil && results[i].Error == nil 101 | } 102 | 103 | for i := range results { 104 | if results[i] == nil { 105 | continue 106 | } 107 | 108 | gb := daemons.BytesToGetBlock(results[i].Result) 109 | if gb.Tx != nil { 110 | return isAccepted, gb.Tx[0] 111 | } 112 | } 113 | 114 | return isAccepted, "" 115 | } 116 | 117 | // UpdateCurrentJob updates the job when mining the same height but tx changes 118 | func (jm *JobManager) UpdateCurrentJob(rpcData *daemons.GetBlockTemplate) { 119 | tmpBlockTemplate := NewJob( 120 | jm.CurrentJob.JobId, 121 | rpcData, 122 | jm.PoolAddress.GetScript(), 123 | jm.ExtraNoncePlaceholder, 124 | jm.Options.Coin.Reward, 125 | jm.Options.Coin.TxMessages, 126 | jm.Options.RewardRecipients, 127 | ) 128 | 129 | jm.CurrentJob = tmpBlockTemplate 130 | jm.ValidJobs[tmpBlockTemplate.JobId] = tmpBlockTemplate 131 | 132 | log.Debug("Job updated") 133 | } 134 | 135 | // CreateNewJob creates a new job when mining new height 136 | func (jm *JobManager) CreateNewJob(rpcData *daemons.GetBlockTemplate) { 137 | // creates a new job when mining new height 138 | 139 | tmpBlockTemplate := NewJob( 140 | utils.RandHexUint64(), 141 | rpcData, 142 | jm.PoolAddress.GetScript(), 143 | jm.ExtraNoncePlaceholder, 144 | jm.Options.Coin.Reward, 145 | jm.Options.Coin.TxMessages, 146 | jm.Options.RewardRecipients, 147 | ) 148 | 149 | jm.CurrentJob = tmpBlockTemplate 150 | jm.ValidJobs[tmpBlockTemplate.JobId] = tmpBlockTemplate 151 | 152 | log.Info("New Job (Block) from block template") 153 | } 154 | 155 | // ProcessTemplate handles the template 156 | func (jm *JobManager) ProcessTemplate(rpcData *daemons.GetBlockTemplate) { 157 | if jm.CurrentJob != nil && rpcData.Height < jm.CurrentJob.GetBlockTemplate.Height { 158 | return 159 | } 160 | 161 | if jm.CurrentJob != nil && rpcData.Height == jm.CurrentJob.GetBlockTemplate.Height { 162 | jm.UpdateCurrentJob(rpcData) 163 | return 164 | } 165 | 166 | jm.CreateNewJob(rpcData) 167 | } 168 | 169 | func (jm *JobManager) ProcessSubmit(jobId string, prevDiff, diff *big.Float, extraNonce1 []byte, hexExtraNonce2, hexNTime, hexNonce string, ipAddr net.Addr, workerName string) (share *types.Share) { 170 | submitTime := time.Now() 171 | 172 | var miner, rig string 173 | names := strings.Split(workerName, ".") 174 | if len(names) < 2 { 175 | miner = names[0] 176 | rig = "unknown" 177 | } else { 178 | miner = names[0] 179 | rig = names[1] 180 | } 181 | 182 | job, exists := jm.ValidJobs[jobId] 183 | if !exists || job == nil || job.JobId != jobId { 184 | return &types.Share{ 185 | JobId: jobId, 186 | RemoteAddr: ipAddr, 187 | Miner: miner, 188 | Rig: rig, 189 | 190 | ErrorCode: types.ErrJobNotFound, 191 | } 192 | } 193 | 194 | extraNonce2, err := hex.DecodeString(hexExtraNonce2) 195 | if err != nil { 196 | log.Error(err) 197 | } 198 | 199 | if len(extraNonce2) != jm.ExtraNonce2Size { 200 | return &types.Share{ 201 | JobId: jobId, 202 | RemoteAddr: ipAddr, 203 | Miner: miner, 204 | Rig: rig, 205 | 206 | ErrorCode: types.ErrIncorrectExtraNonce2Size, 207 | } 208 | } 209 | 210 | if len(hexNTime) != 8 { 211 | return &types.Share{ 212 | JobId: jobId, 213 | RemoteAddr: ipAddr, 214 | Miner: miner, 215 | Rig: rig, 216 | 217 | ErrorCode: types.ErrIncorrectNTimeSize, 218 | } 219 | } 220 | 221 | // allowed nTime range [GBT's CurTime, submitTime+7s] 222 | nTimeInt, err := strconv.ParseInt(hexNTime, 16, 64) 223 | if err != nil { 224 | log.Error(err) 225 | } 226 | if uint32(nTimeInt) < job.GetBlockTemplate.CurTime || nTimeInt > submitTime.Unix()+7 { 227 | log.Error("nTime incorrect: expect from ", job.GetBlockTemplate.CurTime, " to ", submitTime.Unix()+7, ", got ", uint32(nTimeInt)) 228 | return &types.Share{ 229 | JobId: jobId, 230 | RemoteAddr: ipAddr, 231 | Miner: miner, 232 | Rig: rig, 233 | 234 | ErrorCode: types.ErrNTimeOutOfRange, 235 | } 236 | } 237 | 238 | if len(hexNonce) != 8 { 239 | return &types.Share{ 240 | JobId: jobId, 241 | RemoteAddr: ipAddr, 242 | Miner: miner, 243 | Rig: rig, 244 | 245 | ErrorCode: types.ErrIncorrectNonceSize, 246 | } 247 | } 248 | 249 | if !job.RegisterSubmit(hex.EncodeToString(extraNonce1), hexExtraNonce2, hexNTime, hexNonce) { 250 | return &types.Share{ 251 | JobId: jobId, 252 | RemoteAddr: ipAddr, 253 | Miner: miner, 254 | Rig: rig, 255 | 256 | ErrorCode: types.ErrDuplicateShare, 257 | } 258 | } 259 | 260 | coinbaseBytes := job.SerializeCoinbase(extraNonce1, extraNonce2) 261 | coinbaseHash := jm.CoinbaseHasher(coinbaseBytes) 262 | merkleRoot := utils.ReverseBytes(job.MerkleTree.WithFirst(coinbaseHash)) 263 | 264 | nonce, err := hex.DecodeString(hexNonce) // in big-endian 265 | if err != nil { 266 | log.Error(err) 267 | } 268 | 269 | nTimeBytes, err := hex.DecodeString(hexNTime) // in big-endian 270 | if err != nil { 271 | log.Error(err) 272 | } 273 | 274 | headerBytes := job.SerializeHeader(merkleRoot, nTimeBytes, nonce) // in LE 275 | headerHash := algorithm.GetHashFunc(jm.Options.Algorithm.Name)(headerBytes) 276 | headerHashBigInt := new(big.Int).SetBytes(utils.ReverseBytes(headerHash)) 277 | 278 | bigShareDiff := new(big.Float).Quo( 279 | new(big.Float).SetInt(new(big.Int).Mul(algorithm.MaxTargetTruncated, big.NewInt(1< 0 { 286 | blockHex := hex.EncodeToString(job.SerializeBlock(headerBytes, coinbaseBytes)) 287 | var blockHash string 288 | if jm.Options.Algorithm.SHA256dBlockHasher { 289 | // LTC 290 | blockHash = hex.EncodeToString(utils.ReverseBytes(utils.Sha256d(headerBytes))) 291 | } else { 292 | // DASH 293 | blockHash = hex.EncodeToString(utils.ReverseBytes(algorithm.GetHashFunc(jm.Options.Algorithm.Name)(headerBytes))) 294 | } 295 | 296 | log.Warn("Found Block: ", blockHash) 297 | return &types.Share{ 298 | JobId: jobId, 299 | RemoteAddr: ipAddr, 300 | Miner: miner, 301 | Rig: rig, 302 | 303 | BlockHeight: job.GetBlockTemplate.Height, 304 | BlockReward: job.GetBlockTemplate.CoinbaseValue, 305 | Diff: shareDiff, 306 | BlockHash: blockHash, 307 | BlockHex: blockHex, 308 | } 309 | } 310 | 311 | // Check if share didn't reached the miner's difficulty) 312 | if new(big.Float).Quo(bigShareDiff, diff).Cmp(big.NewFloat(0.99)) < 0 { 313 | // Check if share matched a previous difficulty from before a vardiff retarget 314 | if prevDiff != nil && bigShareDiff.Cmp(prevDiff) >= 0 { 315 | return &types.Share{ 316 | JobId: jobId, 317 | RemoteAddr: ipAddr, 318 | Miner: miner, 319 | Rig: rig, 320 | 321 | BlockHeight: job.GetBlockTemplate.Height, 322 | BlockReward: job.GetBlockTemplate.CoinbaseValue, 323 | Diff: shareDiff, 324 | } 325 | } else { 326 | return &types.Share{ 327 | JobId: jobId, 328 | RemoteAddr: ipAddr, 329 | Miner: workerName, 330 | 331 | ErrorCode: types.ErrLowDiffShare, 332 | } 333 | } 334 | } 335 | 336 | // share reaches the miner's difficulty but doesnt not reach the block's 337 | return &types.Share{ 338 | JobId: jobId, 339 | RemoteAddr: ipAddr, 340 | Miner: miner, 341 | Rig: rig, 342 | 343 | Diff: shareDiff, 344 | } 345 | } 346 | 347 | //func GetPoolAddressScript(reward string, validateAddress *daemons.ValidateAddress) []byte { 348 | // switch reward { 349 | // case "POS": 350 | // return utils.PublicKeyToScript(validateAddress.Pubkey) 351 | // case "POW": 352 | // if validateAddress.Isscript { 353 | // return utils.P2SHAddressToScript(validateAddress.Address) 354 | // } 355 | // return utils.P2PKHAddressToScript(validateAddress.Address) 356 | // default: 357 | // // as POW 358 | // log.Fatal("unknown reward type: " + reward) 359 | // return nil 360 | // } 361 | //} 362 | -------------------------------------------------------------------------------- /jobs/job_test.go: -------------------------------------------------------------------------------- 1 | package jobs 2 | 3 | import ( 4 | "encoding/hex" 5 | "encoding/json" 6 | "fmt" 7 | "math/big" 8 | "testing" 9 | 10 | "github.com/mining-pool/not-only-mining-pool/algorithm" 11 | "github.com/mining-pool/not-only-mining-pool/daemons" 12 | "github.com/mining-pool/not-only-mining-pool/merkletree" 13 | "github.com/mining-pool/not-only-mining-pool/utils" 14 | ) 15 | 16 | func TestNewBlockTemplate(t *testing.T) { 17 | // 1e06109b bits 18 | // 000006109b000000000000000000000000000000000000000000000000000000 target 19 | // 0.0006395185894153062 diff 20 | 21 | target, _ := hex.DecodeString("000006109b000000000000000000000000000000000000000000000000000000") 22 | bigTarget := new(big.Float).SetInt(new(big.Int).SetBytes(target)) 23 | diff := big.NewFloat(0.0006395185894153062) 24 | fmt.Println(0.0006395185894153062) 25 | maxTarget := new(big.Float).SetInt(algorithm.MaxTarget) 26 | fmt.Println(algorithm.MaxTarget) 27 | fmt.Println(maxTarget) 28 | fmt.Println(new(big.Float).SetInt(new(big.Int).SetBytes(target))) 29 | fmt.Println(new(big.Float).Mul(bigTarget, diff)) 30 | fmt.Println(utils.BigIntFromBitsHex("1e06109b")) 31 | 32 | fmt.Println(algorithm.MaxTarget) 33 | } 34 | 35 | func TestGetTransactionBytes(t *testing.T) { 36 | //txs := []*daemons.TxParams{ 37 | // &daemons.TxParams{ 38 | // Data: "", 39 | // Hash: "", 40 | // Depends: nil, 41 | // Fee: 0, 42 | // Sigops: 0, 43 | // TxId: "", 44 | // }, 45 | //} 46 | 47 | rawTxs := ` 48 | [ 49 | { 50 | "data": "01000000012f8975c900f56662f35c317a0669fecc5fe0e1fb8ee53f4de72f1cb68c07e606010000008a473044022061a9ac17f269f3c69e18b5d67dfa6bf8b6a5a60eb7f9b0c992ffaeb66b5b88fb02202bb6fd7eb539302d97f4b8604bc822c91747e1cb365fecc91b37526b6b8c2c25014104fe67366f857106ee7b4cc48abb4dabd46302e12fe4140f4c933b92bd3ce75b1f4ae45055312f9a6c5ddc1f8d94d4f6d11e2a13372bcd6bfd651e48997b0f767effffffff02e8030000000000001976a914dffec839eba107e556d6c4f25f90765b3d10583288acbb60da04000000001976a914bdd83cf3ab8b7a57ff9b841752c1ae764f2a02ee88ac00000000", 51 | "txid": "f9b8b0bdd0dc38b2a707faf89acf064f543c3a88d39f54fb126cbd084ffb5ed9", 52 | "hash": "f9b8b0bdd0dc38b2a707faf89acf064f543c3a88d39f54fb126cbd084ffb5ed9", 53 | "depends": [ 54 | ], 55 | "fee": 6450, 56 | "sigops": 8, 57 | "weight": 1028 58 | }, 59 | { 60 | "data": "0200000001979a795a82096fc375487778939d9193bb284c58525e5df9c3a404c81c9220ef01000000d9004730440220086f0b09ded442c84e602520f5a8b38b41a1bc860fb595bd47834c20fa8db39402200a40cb86c15198302cabfd5c620c24fa6ac9ac5d946394e37dd3f9960b65a0e701473044022049bc0be153a4535196f73455bf82667956f2089019db4eeb57cb35649d8f69b202206ddd411917cb3e54f7b9a89c0693c971a6499eb6e421dce1d5fa9358300525d301475221025ad7eedea4c87b98463b8c7316c139f94c0e75fe4c849f42dab112479e1a1bb7210257591ace4d6a9fc94b8114cffd84df9bd0349c974a792580f7f5afb74f5ba94952ae0000000002102700000000000017a914b75a640760f2caae367c0e0cd6bfb85e8d80755987e17608000000000017a914ef20c4471b54fc47c93d587a318d351e93fbc13b8700000000", 61 | "txid": "620c724890f76b802714d786d5d3fe13a89106d81e93b74c4eafd6dc04179f37", 62 | "hash": "620c724890f76b802714d786d5d3fe13a89106d81e93b74c4eafd6dc04179f37", 63 | "depends": [ 64 | ], 65 | "fee": 3493, 66 | "sigops": 8, 67 | "weight": 1328 68 | } 69 | ] 70 | ` 71 | var txs []*daemons.TxParams 72 | err := json.Unmarshal([]byte(rawTxs), &txs) 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | 77 | b := GetTransactionBytes(txs) 78 | for i := 0; i < len(b); i++ { 79 | t.Log(hex.EncodeToString(b[i])) 80 | } 81 | 82 | merkleTree := merkletree.NewMerkleTree(GetTransactionBytes(txs)) 83 | t.Log(merkleTree.Steps) 84 | merkleBranch := merkletree.GetMerkleHashes(merkleTree.Steps) 85 | t.Log(merkleBranch) 86 | } 87 | 88 | func TestJob_SerializeHeader(t *testing.T) { 89 | rawTxs := ` 90 | [ 91 | { 92 | "data": "01000000000101899522d55c2fdf575cfb549f15f6c7c34e43e98b17300ae34edc63f8272311410100000000ffffffff020000000000000000136a0c0701007ac01b000140000000530345d471abcc1e0b000000001600148d5ce2c127c03ad8d1f9fc9bbe3fad1023bb2aa202473044022029ba3fbd618616a48ff211450008d0ced9654ffdd3f46640e51535322cef122c022017db76be63a080580144cebdefceee4032d4c481f2961a8dc67f23f2d0ccfc70012103df64a645452a26c559adeff4825e6ab5a4935a0627b4b13b5f5d716e818906a700000000", 93 | "hash": "d12deb5951b796a3c478ae446598f5591b3ee571188980eba4c8bb3276260c0d", 94 | "depends": [], 95 | "fee": 3450, 96 | "sigops": 1, 97 | "txid": "78c1505e1312bdc723e4a86aad3e2e798cd6761d0a589b87de248b33f2c5773f" 98 | }, 99 | { 100 | "data": "0100000000010112d8e244e6daa7b4d04d1724318b335d63291e60a80bda9760f76b79462c37470000000000ffffffff020000000000000000146a0d07080003a67427265646564640530345d4716db4eb0b000000001600142aeb9224a6491f75aa3a8fdaa60b17cae735b8ce0247304402207ed0d9b0e044f5cc3d68129931c8cb8e8ec1f941d4f820d1595b75b384e753bf022024fade5ed065e8e6916c2fe9e8a5e585c6ce9d9b7a831406250f9ac11a7a1b660121028d20c4336c742ef18bd958b2db1289e93f7f158d1d0e462b958fe2f3e2018ac100000000", 101 | "hash": "ef7f84c048d8cb4f55bc824264199a0e752aba47d54c897e46f2a240ffa58386", 102 | "depends": [], 103 | "fee": 3475, 104 | "sigops": 1, 105 | "txid": "03bef03f26f28fe5bd2b5ef89d3dc1fe498da2b215b1570e924810079dc7fd42" 106 | }, 107 | { 108 | "data": "010000000001015af7409fdc3f99f8b5ec2638a16f4f249d0dc2dd13b42fe5ced9b4af8bb6620a0100000000ffffffff020000000000000000136a0c0701000c4001000080000000530345d47139b3810b00000000160014de9d9a2c541fce2c54d7bf7e88bb038071a329ad0247304402201a668bf396d01b29055fa7e1d996643a6df5da74a9b0000674f44bfea8c7bdbd022006615dfd0ac4dcd3036948e672107f8241f257e4ba4b6ca2f7bf5e2227eea442012102d788d048abf4325a4ac018f5b369aa8ea9b252cb9239dd6abc5270673041a7b100000000", 109 | "hash": "ec2857e3e6874ceed835e7b6949d98e383a929c9bff5951ab3c8a9d0067a0a4c", 110 | "depends": [], 111 | "fee": 3450, 112 | "sigops": 1, 113 | "txid": "18133eb939f15814826de97ace1cff26de7d0378666989c59c00beb0c9cb4151" 114 | }, 115 | { 116 | "data": "0100000000010134d0cbd4f023d9aaa5aab78b81fd1ed17ea2ac512e9b462936a4eaaa3ae5f0de0100000000ffffffff030000000000000000226a1b0701027ac001000083001000003000b0003000f0000c001c000000530345d471a0252600000000001976a914b66c71c784d8804ec8591011f3d78f3e1e23c2f288ac61f49e0b00000000160014bb471c71724b39beadf55fc1b334b75f45aa8b4c0247304402200dd0adaa9949c4e0fd269d1472b556145eec542f914ede27fffe87586c2eb9c90220542797fac36d7e1b041837d85e688396f0e7b9ae2283fe1c60f9486e0a8e693801210317993020134466e9db343f04f56f3c53c65e7984565ddaf1e0ebe4cde521861000000000", 117 | "hash": "a0127b166709395c3c0fdf919c4563d9f2d8fdb7a81367c63932cc8088b4760a", 118 | "depends": [], 119 | "fee": 4675, 120 | "sigops": 5, 121 | "txid": "3b94cc39ddb80b052f7135b09c791c34fdcc726ade24ed9d0d7369167ef1a66d" 122 | }, 123 | { 124 | "data": "010000000001013f77c5f2338b24de879b580a1d76d68c792e3ead6aa8e423c7bd12135e50c1780100000000ffffffff020000000000000000136a0c0701007ac0010000c0000000530345d47131bf1e0b000000001600148d5ce2c127c03ad8d1f9fc9bbe3fad1023bb2aa2024730440220359e9d5d96dadf5bddf276ab5f0d5d5ceed433b57039dc5d1f572568b465a7ec022011c1379815a50a58e73cfd69ec590540f31342b1cbac1f533e0900bd670df1d7012103df64a645452a26c559adeff4825e6ab5a4935a0627b4b13b5f5d716e818906a700000000", 125 | "hash": "864359eeabe597f57dbfb65975fde5b6ecbd191baddc2f29d5d738728583c840", 126 | "depends": [ 127 | 1 128 | ], 129 | "fee": 3450, 130 | "sigops": 1, 131 | "txid": "d4bda2d38acd3815a45636f4d06db1acc0ec9b713226b131989f057b9bd8a988" 132 | }, 133 | { 134 | "data": "010000000001015141cbc9b0be009cc589696678037dde26ff1cce7ae96d821458f139b93e13180100000000ffffffff020000000000000000136a0c0701000c4001000040000000530345d471bfa5810b00000000160014de9d9a2c541fce2c54d7bf7e88bb038071a329ad0247304402201569f50e32a337e06214398756d4517612e757e82f185d8c28f891b07a9fa3480220770e8e2ee2c25022eb6289e9504fca132040e207369a45c30c5e2a3fa4d7e137012102d788d048abf4325a4ac018f5b369aa8ea9b252cb9239dd6abc5270673041a7b100000000", 135 | "hash": "799c4a377661d3d2cdd2f5ab225413fa54e0cc58a3028fe7cab7bc8ce4626abd", 136 | "depends": [ 137 | 3 138 | ], 139 | "fee": 3450, 140 | "sigops": 1, 141 | "txid": "2c5f761385fe6c79901b39a8ef09af38c544214805ac3f6eebb07dcffc122abb" 142 | }, 143 | { 144 | "data": "0100000000010188a9d89b7b059f9831b12632719becc0acb16dd0f43656a41538cd8ad3a2bdd40100000000ffffffff020000000000000000136a0c0701007ac001800080000000530345d471b7b11e0b000000001600148d5ce2c127c03ad8d1f9fc9bbe3fad1023bb2aa2024730440220322d0a9466f5b1af4ecf9c549748b0377825433589c55f16fcde8c34d825e804022009f60c98c18425495d7acd081dc2b81649d5ca4821f17d52ffe16e0f5db76b26012103df64a645452a26c559adeff4825e6ab5a4935a0627b4b13b5f5d716e818906a700000000", 145 | "hash": "b53eb9b0af014e1903d4689d1a7d6cc6d07eeb3ef027956dfb862fbd9ba4020f", 146 | "depends": [ 147 | 5 148 | ], 149 | "fee": 3450, 150 | "sigops": 1, 151 | "txid": "85fbc84d63b76346fbac28c005cba5bbe5153e6851d0432963d3651f4559e5dd" 152 | } 153 | ] 154 | ` 155 | var txs []*daemons.TxParams 156 | err := json.Unmarshal([]byte(rawTxs), &txs) 157 | if err != nil { 158 | t.Fatal(err) 159 | } 160 | 161 | txsBytes := GetTransactionBytes(txs) 162 | //for i:=0; i 1 { 31 | steps = append(steps, L[1]) 32 | 33 | if Ll%2 != 0 { 34 | L = append(L, L[len(L)-1]) 35 | } 36 | 37 | r := utils.Range(StartL, Ll, 2) 38 | Ld := make([][]byte, len(r)) 39 | 40 | for i := 0; i < len(r); i++ { 41 | Ld[i] = MerkleJoin(L[r[i]], L[r[i]+1]) 42 | } 43 | L = append(PreL, Ld...) 44 | Ll = len(L) 45 | } 46 | 47 | return steps 48 | } 49 | 50 | func MerkleJoin(h1, h2 []byte) []byte { 51 | return utils.Sha256d(bytes.Join([][]byte{h1, h2}, nil)) 52 | } 53 | 54 | func (mt *MerkleTree) WithFirst(f []byte) []byte { 55 | for i := 0; i < len(mt.Steps); i++ { 56 | f = utils.Sha256d(bytes.Join([][]byte{f, mt.Steps[i]}, nil)) 57 | } 58 | return f 59 | } 60 | 61 | func GetMerkleHashes(steps [][]byte) []string { 62 | hashes := make([]string, len(steps)) 63 | for i := 0; i < len(steps); i++ { 64 | // hash := make([]byte, 32) 65 | // copy(hash, steps[i]) 66 | hashes[i] = hex.EncodeToString(steps[i]) 67 | } 68 | 69 | return hashes 70 | } 71 | -------------------------------------------------------------------------------- /merkletree/mtree_test.go: -------------------------------------------------------------------------------- 1 | package merkletree 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | ) 7 | 8 | func TestNewMerkleTree(t *testing.T) { 9 | mt1 := NewMerkleTree([][]byte{[]byte("hello"), []byte("world")}) 10 | 11 | if hex.EncodeToString(mt1.WithFirst([]byte("first"))) != "11f206ce3848f46083c5f30d01b95a8dd75194ef5781b24202d34720b2b4c12f" { 12 | t.Fail() 13 | } 14 | 15 | if GetMerkleHashes(mt1.Steps)[0] != "776f726c64" { 16 | t.Log(GetMerkleHashes(mt1.Steps)[0]) 17 | t.Fail() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /p2p/peer.go: -------------------------------------------------------------------------------- 1 | package p2p 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "encoding/binary" 7 | "encoding/hex" 8 | "io" 9 | "net" 10 | "time" 11 | 12 | logging "github.com/ipfs/go-log/v2" 13 | "github.com/mining-pool/not-only-mining-pool/config" 14 | "github.com/mining-pool/not-only-mining-pool/utils" 15 | ) 16 | 17 | var log = logging.Logger("p2p") 18 | 19 | type Peer struct { 20 | Magic []byte 21 | 22 | VerAck bool 23 | ValidConnectionConfig bool 24 | 25 | NetworkServices []byte 26 | EmptyNetAddress []byte 27 | UserAgent []byte 28 | BlockStartHeight []byte 29 | RelayTransactions []byte 30 | 31 | InvCodes map[string]uint32 32 | Commands map[string][]byte 33 | Options *config.P2POptions 34 | Conn net.Conn 35 | ProtocolVersion int 36 | 37 | BlockNotifyCh chan string 38 | } 39 | 40 | func NewPeer(protocolVersion int, options *config.P2POptions) *Peer { 41 | magic, err := hex.DecodeString(options.Magic) 42 | if err != nil { 43 | log.Fatal("magic hex string is incorrect") 44 | } 45 | 46 | networkServices, _ := hex.DecodeString("0100000000000000") // NODE_NETWORK services (value 1 packed as uint64) 47 | emptyNetAddress, _ := hex.DecodeString("010000000000000000000000000000000000ffff000000000000") 48 | userAgent := utils.VarStringBytes("/node-stratum/") 49 | blockStartHeight, _ := hex.DecodeString("00000000") // block start_height, can be empty 50 | 51 | //If protocol version is new enough, add do not relay transactions flag byte, outlined in BIP37 52 | //https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki#extensions-to-existing-messages 53 | var relayTransactions []byte 54 | if options.DisableTransactions { 55 | relayTransactions = []byte{0} 56 | } else { 57 | relayTransactions = []byte{} 58 | } 59 | 60 | return &Peer{ 61 | Magic: magic, 62 | VerAck: false, 63 | ValidConnectionConfig: true, 64 | 65 | Options: options, 66 | ProtocolVersion: protocolVersion, 67 | 68 | NetworkServices: networkServices, 69 | EmptyNetAddress: emptyNetAddress, 70 | UserAgent: userAgent, 71 | BlockStartHeight: blockStartHeight, 72 | RelayTransactions: relayTransactions, 73 | 74 | InvCodes: map[string]uint32{ 75 | "error": 0, 76 | "tx": 1, 77 | "block": 2, 78 | }, 79 | 80 | Commands: map[string][]byte{ 81 | "version": utils.CommandStringBytes("version"), 82 | "inv": utils.CommandStringBytes("inv"), 83 | "verack": utils.CommandStringBytes("verack"), 84 | "addr": utils.CommandStringBytes("addr"), 85 | "getblocks": utils.CommandStringBytes("getblocks"), 86 | }, 87 | 88 | BlockNotifyCh: make(chan string), 89 | } 90 | } 91 | 92 | func (p *Peer) Init() { 93 | p.Connect() 94 | } 95 | 96 | func (p *Peer) Connect() { 97 | var err error 98 | p.Conn, err = net.Dial("tcp", p.Options.Addr()) 99 | if err != nil { 100 | log.Fatal("failed to connect to coin's p2p port: ", err) 101 | } 102 | 103 | p.SetupMessageParser() 104 | } 105 | 106 | func (p *Peer) SetupMessageParser() { 107 | go func() { 108 | p.SendVersion() 109 | 110 | header := make([]byte, 24) 111 | for { 112 | n, err := p.Conn.Read(header) 113 | if err == io.EOF { 114 | continue 115 | } 116 | 117 | if err != nil || n < 24 { 118 | log.Error(err) 119 | continue 120 | } 121 | 122 | if !bytes.Equal(header[0:4], p.Magic) { 123 | continue 124 | } 125 | 126 | payload := make([]byte, binary.LittleEndian.Uint32(header[16:20])) 127 | _, err = p.Conn.Read(payload) 128 | if err != nil { 129 | log.Error(err) 130 | continue 131 | } 132 | 133 | if bytes.Equal(utils.Sha256d(payload)[0:4], header[20:24]) { 134 | go p.HandleMessage(header[4:16], payload) 135 | } 136 | } 137 | }() 138 | } 139 | 140 | func (p *Peer) HandleMessage(command, payload []byte) { 141 | log.Info("handling: ", command, payload) 142 | switch string(command) { 143 | case string(p.Commands["inv"]): 144 | p.HandleInv(payload) 145 | case string(p.Commands["verack"]): 146 | if !p.VerAck { 147 | p.VerAck = true 148 | // connected 149 | } 150 | case string(p.Commands["version"]): 151 | p.SendMessage(p.Commands["verack"], make([]byte, 0)) 152 | default: 153 | break 154 | } 155 | } 156 | 157 | // Parsing inv message https://en.bitcoin.it/wiki/Protocol_specification#inv 158 | func (p *Peer) HandleInv(payload []byte) { 159 | // sloppy varint decoding 160 | var count int 161 | var buf []byte 162 | if payload[0] < 0xFD { 163 | count = int(payload[0]) 164 | buf = payload[1:] 165 | } else { 166 | count = int(binary.LittleEndian.Uint16(payload[0:2])) 167 | buf = payload[2:] 168 | } 169 | 170 | for count--; count != 0; count-- { 171 | switch binary.LittleEndian.Uint32(buf) { 172 | case p.InvCodes["error"]: 173 | case p.InvCodes["tx"]: 174 | // tx := hex.EncodeToString(buf[4:36]) 175 | case p.InvCodes["block"]: 176 | block := hex.EncodeToString(buf[4:36]) 177 | log.Warn("block found: ", block) 178 | // block found 179 | p.ProcessBlockNotify(block) 180 | } 181 | buf = buf[36:] 182 | } 183 | } 184 | 185 | func (p *Peer) SendMessage(command, payload []byte) { 186 | log.Info("sending: ", command, payload) 187 | if p.Conn == nil { 188 | p.Connect() 189 | } 190 | 191 | message := bytes.Join([][]byte{ 192 | p.Magic, 193 | command, 194 | utils.PackUint32LE(uint32(len(payload))), 195 | utils.Sha256d(payload)[0:4], 196 | payload, 197 | }, nil) 198 | 199 | _, err := p.Conn.Write(message) 200 | if err != nil { 201 | log.Error(err) 202 | } 203 | 204 | log.Info(string(message)) 205 | } 206 | 207 | func (p *Peer) SendVersion() { 208 | nonce := make([]byte, 8) 209 | rand.Read(nonce) 210 | payload := bytes.Join([][]byte{ 211 | utils.PackUint32LE(uint32(p.ProtocolVersion)), 212 | p.NetworkServices, 213 | utils.PackUint64LE(uint64(time.Now().Unix())), 214 | p.EmptyNetAddress, // addr_recv, can be empty 215 | 216 | p.EmptyNetAddress, // addr_from, can be empty 217 | nonce, // nonce, random unique ID 218 | p.UserAgent, 219 | p.BlockStartHeight, 220 | 221 | p.RelayTransactions, 222 | }, nil) 223 | 224 | p.SendMessage(p.Commands["version"], payload) 225 | } 226 | 227 | func (p *Peer) ProcessBlockNotify(blockHash string) { 228 | log.Info("Block notification via p2p") 229 | // if p.JobManager.CurrentJob != nil && blockHash != p.JobManager.CurrentJob.GetBlockTemplate.PreviousBlockHash { 230 | p.BlockNotifyCh <- blockHash 231 | //} 232 | } 233 | -------------------------------------------------------------------------------- /p2p/peer_test.go: -------------------------------------------------------------------------------- 1 | package p2p 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | 8 | "github.com/mining-pool/not-only-mining-pool/config" 9 | ) 10 | 11 | func TestNewPeer(t *testing.T) { 12 | var options config.P2POptions 13 | json.Unmarshal([]byte(` 14 | { 15 | "host": "0.0.0.0", 16 | "port": 19335, 17 | "magic": "fdd2c8f1", 18 | "disableTransactions": true 19 | } 20 | `), &options) 21 | peer := NewPeer(70015, &options) 22 | peer.Init() 23 | 24 | c := time.After(time.Minute) 25 | for { 26 | select { 27 | case <-c: 28 | return 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /payments/contrib.go: -------------------------------------------------------------------------------- 1 | package payments 2 | -------------------------------------------------------------------------------- /payments/payer.go: -------------------------------------------------------------------------------- 1 | package payments 2 | 3 | import ( 4 | logging "github.com/ipfs/go-log/v2" 5 | "github.com/mining-pool/not-only-mining-pool/daemons" 6 | ) 7 | 8 | var log = logging.Logger("payments") 9 | 10 | type PayMode int 11 | 12 | const ( 13 | PayOnManual PayMode = iota 14 | PayPPLNS // calc rewards on block found 15 | PayPPS // calc rewards every day 16 | ) 17 | 18 | type PaymentManager struct { 19 | pay chan struct{} 20 | dm *daemons.DaemonManager 21 | } 22 | 23 | func NewPaymentManager(mode PayMode, dm *daemons.DaemonManager) *PaymentManager { 24 | return &PaymentManager{ 25 | pay: make(chan struct{}), 26 | dm: dm, 27 | } 28 | } 29 | 30 | func (pm *PaymentManager) Serve() { 31 | func() { 32 | for { 33 | <-pm.pay 34 | pm.doPay() 35 | } 36 | }() 37 | } 38 | 39 | func (pm *PaymentManager) doPay() { 40 | // PPLNS solution: 41 | // 42 | // for range // not wg => keep balance safe 43 | // validateaddress 44 | // getbalance 45 | // ['hgetall', coin + ':balances'], 46 | // ['smembers', coin + ':blocksPending'] 47 | // 48 | // { 49 | // blockHash: details[0], 50 | // txHash: details[1], 51 | // height: details[2], 52 | // }; 53 | // 54 | // gettransaction generatiion tx & getaccount pooladdress -> chech tx detail -> kick/orphan/confirm 55 | // 56 | // ['hgetall', coin + ':shares:round' + r.height] 57 | // var reward = parseInt(round.reward * magnitude); 58 | // 59 | // sendmany [addressAccount, addressAmounts] 60 | // 61 | // ['hincrbyfloat', coin + ':balances', w, satoshisToCoins(worker.balanceChange)] 62 | // ['hincrbyfloat', coin + ':payouts', w, worker.sent] 63 | // 64 | // smove => move block from pending to kick/orphan/confirm 65 | } 66 | -------------------------------------------------------------------------------- /payments/recipient.go: -------------------------------------------------------------------------------- 1 | package payments 2 | -------------------------------------------------------------------------------- /pool/pool.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | logging "github.com/ipfs/go-log/v2" 13 | 14 | "github.com/mining-pool/not-only-mining-pool/api" 15 | "github.com/mining-pool/not-only-mining-pool/bans" 16 | "github.com/mining-pool/not-only-mining-pool/config" 17 | "github.com/mining-pool/not-only-mining-pool/daemons" 18 | "github.com/mining-pool/not-only-mining-pool/jobs" 19 | "github.com/mining-pool/not-only-mining-pool/p2p" 20 | "github.com/mining-pool/not-only-mining-pool/storage" 21 | "github.com/mining-pool/not-only-mining-pool/stratum" 22 | "github.com/mining-pool/not-only-mining-pool/utils" 23 | ) 24 | 25 | var log = logging.Logger("pool") 26 | 27 | type Pool struct { 28 | DaemonManager *daemons.DaemonManager 29 | JobManager *jobs.JobManager 30 | P2PManager *p2p.Peer 31 | 32 | StratumServer *stratum.Server 33 | 34 | Options *config.Options 35 | Magnitude uint64 36 | CoinPrecision int 37 | HasGetInfo bool 38 | Stats *Stats 39 | BlockPollingIntervalTicker *time.Ticker 40 | Recipients []*config.Recipient 41 | ProtocolVersion int 42 | APIServer *api.Server 43 | } 44 | 45 | func NewPool(options *config.Options) *Pool { 46 | dm := daemons.NewDaemonManager(options.Daemons, options.Coin) 47 | dm.Check() 48 | 49 | if options.PoolAddress.GetScript() == nil { 50 | log.Panicf("failed to get poolAddress' script, check the address and type") 51 | } 52 | 53 | for _, addr := range options.RewardRecipients { 54 | if addr.GetScript() == nil { 55 | log.Panicf("failed to get addr %s' script, check the address and type", addr.Address) 56 | } 57 | } 58 | 59 | var magnitude int64 = 100000000 //sat 60 | if !options.DisablePayment { 61 | _, getBalance, _ := dm.Cmd("getbalance", []interface{}{}) 62 | 63 | if getBalance.Error != nil { 64 | log.Fatal(errors.New(fmt.Sprint(getBalance.Error))) 65 | } 66 | 67 | split0 := bytes.Split(utils.Jsonify(getBalance), []byte(`result":`)) 68 | split2 := bytes.Split(split0[1], []byte(",")) 69 | split3 := bytes.Split(split2[0], []byte(".")) 70 | d := split3[1] 71 | 72 | var err error 73 | magnitude, err = strconv.ParseInt("10"+strconv.Itoa(len(d))+"0", 10, 64) 74 | if err != nil { 75 | log.Fatal("ErrorCode detecting number of satoshis in a coin, cannot do payments processing. Tried parsing: ", string(utils.Jsonify(getBalance))) 76 | } 77 | } 78 | 79 | db := storage.NewStorage(options.Coin.Name, options.Storage) 80 | 81 | jm := jobs.NewJobManager(options, dm, db) 82 | bm := bans.NewBanningManager(options.Banning) 83 | s := api.NewAPIServer(options, db) 84 | 85 | return &Pool{ 86 | Options: options, 87 | DaemonManager: dm, 88 | JobManager: jm, 89 | APIServer: s, 90 | 91 | StratumServer: stratum.NewStratumServer(options, jm, bm), 92 | Magnitude: uint64(magnitude), 93 | CoinPrecision: len(strconv.FormatUint(uint64(magnitude), 10)) - 1, 94 | Stats: NewStats(), 95 | } 96 | } 97 | 98 | // 99 | func (p *Pool) Init() { 100 | p.CheckAllReady() 101 | p.DetectCoinData() 102 | 103 | initGBT, err := p.DaemonManager.GetBlockTemplate() 104 | if err != nil { 105 | log.Fatal(err) 106 | } 107 | 108 | p.SetupP2PBlockNotify() 109 | p.SetupBlockPolling() 110 | 111 | p.JobManager.Init(initGBT) 112 | 113 | p.StartStratumServer() 114 | p.APIServer.Serve() 115 | 116 | p.OutputPoolInfo() 117 | } 118 | 119 | func (p *Pool) SetupP2PBlockNotify() { 120 | if p.Options.P2P == nil { 121 | return 122 | } 123 | 124 | p.P2PManager = p2p.NewPeer(p.ProtocolVersion, p.Options.P2P) 125 | p.Init() 126 | 127 | go func() { 128 | for { 129 | blockHash, ok := <-p.P2PManager.BlockNotifyCh 130 | if !ok { 131 | log.Warn("Block notify is stopped!") 132 | return 133 | } 134 | 135 | if p.JobManager.CurrentJob != nil && blockHash != p.JobManager.CurrentJob.GetBlockTemplate.PreviousBlockHash { 136 | gbt, err := p.DaemonManager.GetBlockTemplate() 137 | if err != nil { 138 | log.Error("p2p block notify failed getting block template: ", err) 139 | } 140 | p.JobManager.ProcessTemplate(gbt) 141 | } 142 | } 143 | }() 144 | } 145 | 146 | func (p *Pool) AttachMiners(miners []*stratum.Client) { 147 | for i := range miners { 148 | p.StratumServer.ManuallyAddStratumClient(miners[i]) 149 | } 150 | 151 | p.StratumServer.BroadcastCurrentMiningJob(p.JobManager.CurrentJob.GetJobParams(true)) 152 | } 153 | 154 | func (p *Pool) StartStratumServer() { 155 | portStarted := p.StratumServer.Init() 156 | p.Stats.StratumPorts = portStarted 157 | } 158 | 159 | // enrich the config options from rpc 160 | func (p *Pool) DetectCoinData() { 161 | var diff float64 162 | 163 | // getdifficulty 164 | _, rpcResponse, _ := p.DaemonManager.Cmd("getdifficulty", []interface{}{}) 165 | if rpcResponse.Error != nil || rpcResponse == nil { 166 | log.Error("Could not start pool, error with init batch RPC call: " + string(utils.Jsonify(rpcResponse))) 167 | return 168 | } 169 | getDifficulty := daemons.BytesToGetDifficulty(rpcResponse.Result) 170 | switch reflect.ValueOf(getDifficulty).Kind() { 171 | case reflect.Float64: 172 | diff = getDifficulty.(float64) 173 | p.Options.Coin.Reward = "POW" 174 | case reflect.Array: 175 | diff = getDifficulty.(map[string]interface{})["proof-of-work"].(float64) 176 | if p.Options.Coin.Reward == "" { 177 | if bytes.Contains(rpcResponse.Result, []byte("proof-of-stake")) { 178 | p.Options.Coin.Reward = "POS" 179 | } else { 180 | p.Options.Coin.Reward = "POW" 181 | } 182 | } 183 | default: 184 | log.Error(reflect.ValueOf(getDifficulty).Kind()) 185 | } 186 | 187 | // getmininginfo 188 | _, rpcResponse, _ = p.DaemonManager.Cmd("getmininginfo", []interface{}{}) 189 | if rpcResponse.Error != nil || rpcResponse == nil { 190 | log.Error("Could not start pool, error with init batch RPC call: " + string(utils.Jsonify(rpcResponse))) 191 | return 192 | } 193 | getMiningInfo := daemons.BytesToGetMiningInfo(rpcResponse.Result) 194 | p.Stats.NetworkHashrate = getMiningInfo.Networkhashps 195 | 196 | _, rpcResponse, _ = p.DaemonManager.Cmd("submitblock", []interface{}{}) 197 | if rpcResponse == nil || rpcResponse.Error == nil { 198 | log.Error("Could not start pool, error with init batch RPC call: " + utils.JsonifyIndentString(rpcResponse)) 199 | return 200 | } 201 | 202 | if rpcResponse.Error.Message == "Method not found" { 203 | p.Options.Coin.NoSubmitBlock = true 204 | } else if rpcResponse.Error.Code == -1 { 205 | p.Options.Coin.NoSubmitBlock = false 206 | } else { 207 | log.Fatal("Could not detect block submission RPC method, " + utils.JsonifyIndentString(rpcResponse)) 208 | } 209 | 210 | _, rpcResponse, _ = p.DaemonManager.Cmd("getwalletinfo", []interface{}{}) 211 | if rpcResponse.Error != nil || rpcResponse == nil { 212 | log.Error("Could not start pool, error with init batch RPC call: " + string(utils.Jsonify(rpcResponse))) 213 | return 214 | } 215 | 216 | _, rpcResponse, _ = p.DaemonManager.Cmd("getinfo", []interface{}{}) 217 | if rpcResponse.Error == nil && rpcResponse != nil { 218 | getInfo := daemons.BytesToGetInfo(rpcResponse.Result) 219 | 220 | p.Options.Coin.Testnet = getInfo.Testnet 221 | p.ProtocolVersion = getInfo.Protocolversion 222 | // diff = getInfo.Difficulty 223 | 224 | p.Stats.Connections = getInfo.Connections 225 | } else { 226 | _, rpcResponse, _ := p.DaemonManager.Cmd("getnetworkinfo", []interface{}{}) 227 | if rpcResponse.Error != nil || rpcResponse == nil { 228 | log.Error("Could not start pool, error with init batch RPC call: " + string(utils.Jsonify(rpcResponse))) 229 | return 230 | } 231 | getNetworkInfo := daemons.BytesToGetNetworkInfo(rpcResponse.Result) 232 | 233 | _, rpcResponse, _ = p.DaemonManager.Cmd("getblockchaininfo", []interface{}{}) 234 | if rpcResponse.Error != nil || rpcResponse == nil { 235 | log.Error("Could not start pool, error with init batch RPC call: " + string(utils.Jsonify(rpcResponse))) 236 | return 237 | } 238 | getBlockchainInfo := daemons.BytesToGetBlockchainInfo(rpcResponse.Result) 239 | p.Options.Coin.Testnet = strings.Contains(getBlockchainInfo.Chain, "test") 240 | p.ProtocolVersion = getNetworkInfo.Protocolversion 241 | // diff = getBlockchainInfo.Difficulty 242 | 243 | p.Stats.Connections = getNetworkInfo.Connections 244 | } 245 | 246 | mul := 1 << p.Options.Algorithm.Multiplier 247 | p.Stats.Difficulty = diff * float64(mul) 248 | } 249 | 250 | func (p *Pool) OutputPoolInfo() { 251 | startMessage := "Stratum Pool Server Started for " + p.Options.Coin.Name + " [" + strings.ToUpper(p.Options.Coin.Symbol) + "] " 252 | 253 | var network string 254 | if p.Options.Coin.Testnet { 255 | network = "Testnet" 256 | } else { 257 | network = "Mainnet" 258 | } 259 | 260 | diff, _ := p.JobManager.CurrentJob.Difficulty.Float64() 261 | mul := 1 << p.Options.Algorithm.Multiplier 262 | 263 | infoLines := []string{ 264 | startMessage, 265 | "Network Connected:\t" + network, 266 | "Detected Reward Type:\t" + p.Options.Coin.Reward, 267 | "Current Block Height:\t" + strconv.FormatInt(p.JobManager.CurrentJob.GetBlockTemplate.Height, 10), 268 | "Current Connect Peers:\t" + strconv.Itoa(p.Stats.Connections), 269 | "Current Block Diff:\t" + strconv.FormatFloat(diff*float64(mul), 'f', 7, 64), 270 | "Network Difficulty:\t" + strconv.FormatFloat(p.Stats.Difficulty, 'f', 7, 64), 271 | "Network Hash Rate:\t" + utils.GetReadableHashRateString(p.Stats.NetworkHashrate), 272 | "Stratum Port(s):\t" + string(utils.Jsonify(p.Stats.StratumPorts)), 273 | "Total Pool Fee Percent:\t" + strconv.FormatFloat(p.Options.TotalFeePercent(), 'f', 7, 64) + "%", 274 | } 275 | 276 | fmt.Println(strings.Join(infoLines, "\n\t")) 277 | } 278 | 279 | func (p *Pool) CheckAllReady() { 280 | _, results := p.DaemonManager.CmdAll("getblocktemplate", []interface{}{map[string]interface{}{"capabilities": []string{"coinbasetxn", "workid", "coinbase/append"}, "rules": []string{"segwit"}}}) 281 | for i := range results { 282 | if results[i] == nil { 283 | log.Fatalf("daemon %s is not available", p.DaemonManager.Daemons[i]) 284 | } 285 | 286 | if results[i].Error != nil { 287 | log.Fatalf("daemon %s is not ready for mining: %s", p.DaemonManager.Daemons[i], results[i].Error.Message) 288 | } 289 | } 290 | } 291 | 292 | func (p *Pool) SetupBlockPolling() { 293 | if p.Options.BlockRefreshInterval <= 0 { 294 | log.Warn("Block template polling has been disabled") 295 | return 296 | } 297 | 298 | pollingInterval := time.Duration(p.Options.BlockRefreshInterval) * time.Second 299 | p.BlockPollingIntervalTicker = time.NewTicker(pollingInterval) 300 | 301 | go func() { 302 | for { 303 | _, ok := <-p.BlockPollingIntervalTicker.C 304 | if !ok { 305 | log.Warn("Block polling is stopped!") 306 | p.BlockPollingIntervalTicker.Stop() 307 | return 308 | } 309 | 310 | gbt, err := p.DaemonManager.GetBlockTemplate() 311 | if err != nil { 312 | log.Error("Block notify error getting block template for ", p.Options.Coin.Name, err) 313 | } 314 | 315 | if gbt != nil { 316 | p.JobManager.ProcessTemplate(gbt) 317 | } 318 | } 319 | }() 320 | } 321 | -------------------------------------------------------------------------------- /pool/stats.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | type Stats struct { 4 | Connections int 5 | Difficulty float64 6 | NetworkHashrate float64 7 | StratumPorts []int 8 | } 9 | 10 | func NewStats() *Stats { 11 | return &Stats{ 12 | Connections: 0, 13 | Difficulty: 0.0, 14 | NetworkHashrate: 0, 15 | StratumPorts: []int{}, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /resources/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mining-pool/not-only-mining-pool/2a6ccb11a1ed6cbc7be849c7611c178ff2163da9/resources/logo.png -------------------------------------------------------------------------------- /resources/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 52 | 60 | 65 | 70 | 82 | 83 | -------------------------------------------------------------------------------- /storage/redis.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/go-redis/redis/v8" 10 | logging "github.com/ipfs/go-log/v2" 11 | "github.com/mining-pool/not-only-mining-pool/config" 12 | "github.com/mining-pool/not-only-mining-pool/types" 13 | ) 14 | 15 | var log = logging.Logger("storage") 16 | 17 | type DB struct { 18 | *redis.Client 19 | coin string 20 | } 21 | 22 | func NewStorage(coinName string, options *config.RedisOptions) *DB { 23 | client := redis.NewClient(options.ToRedisOptions()) 24 | if client == nil { 25 | log.Panic("failed to connect to the redis server. If you dont wanna db storage please delete redis config in config file") 26 | return nil 27 | } 28 | 29 | result, err := client.Ping(context.Background()).Result() 30 | if err != nil || strings.ToLower(result) != "pong" { 31 | log.Panicf("failed to connect to the redis server: %s %s", result, err) 32 | } 33 | 34 | return &DB{ 35 | Client: client, 36 | coin: coinName, 37 | } 38 | } 39 | 40 | func (s *DB) PutShare(share *types.Share, accepted bool) { 41 | now := time.Now().Unix() 42 | strNow := strconv.FormatInt(now, 10) 43 | 44 | ppl := s.Pipeline() 45 | ctx := context.Background() 46 | 47 | strDiff := strconv.FormatFloat(share.Diff, 'f', 5, 64) 48 | ppl.SAdd(ctx, s.coin+":pool:miners", share.Miner) // miner index 49 | ppl.SAdd(ctx, s.coin+":miner:"+share.Miner+":rigs", share.Rig) // rig index 50 | 51 | if share.ErrorCode == 0 { 52 | log.Warn("recording valid share") 53 | ppl.HIncrByFloat(ctx, s.coin+":pool:contrib", share.Miner, share.Diff) 54 | ppl.HIncrBy(ctx, s.coin+":miners:validShares", share.Miner, 1) 55 | 56 | ppl.HIncrBy(ctx, s.coin+":pool", "validShares", 1) 57 | 58 | // cost storage for speed, dont use for range to replace this 59 | ppl.ZAdd(ctx, s.coin+":pool:shares", &redis.Z{ 60 | Score: float64(now), 61 | Member: strDiff, 62 | }) 63 | 64 | ppl.ZAdd(ctx, s.coin+":miner:"+share.Miner+":hashes", &redis.Z{ 65 | Score: float64(now), 66 | Member: strDiff, 67 | }) 68 | 69 | ppl.ZAdd(ctx, s.coin+":miner:"+share.Miner+":rig:"+share.Rig+":hashes", &redis.Z{ 70 | Score: float64(now), 71 | Member: strDiff, 72 | }) 73 | 74 | } else { 75 | log.Warn("recording invalid share") 76 | ppl.HIncrBy(ctx, s.coin+":miners:invalidShares", share.Miner, 1) 77 | 78 | ppl.HIncrBy(ctx, s.coin+":pool", "invalidShares", 1) 79 | } 80 | 81 | // when mined one => seal roundCount, 82 | // BlockHex is not accuracy, maybe out of date 83 | if len(share.BlockHex) > 0 { 84 | // share is valid but block from share can be also invalid 85 | if accepted { 86 | log.Warn("recording valid block") 87 | ppl.Rename(ctx, s.coin+":pool:contrib", s.coin+":pool:contrib:"+strconv.FormatInt(share.BlockHeight, 10)) 88 | ppl.SAdd(ctx, s.coin+":blocks:pending", share.BlockHash) 89 | ppl.HSetNX(ctx, s.coin+":blocks", share.BlockHash, strings.Join([]string{ 90 | share.TxHash, 91 | strconv.FormatInt(share.BlockHeight, 10), 92 | share.Miner, 93 | strNow, 94 | }, ":")) 95 | 96 | ppl.HIncrBy(ctx, s.coin+":pool", "validBlocks", 1) 97 | } else { 98 | log.Warn("recording invalid block") 99 | ppl.HIncrBy(ctx, s.coin+":pool", "invalidBlocks", 1) 100 | } 101 | } 102 | 103 | _, err := ppl.Exec(ctx) 104 | if err != nil { 105 | log.Error(err) 106 | } 107 | } 108 | 109 | func (s *DB) GetMinerIndex() ([]string, error) { 110 | return s.SMembers(context.Background(), s.coin+":pool:miners").Result() 111 | } 112 | 113 | func (s *DB) GetRigIndex(minerName string) ([]string, error) { 114 | return s.SMembers(context.Background(), s.coin+":miner:"+minerName+":rigs").Result() 115 | } 116 | 117 | // GetCurrentRoundCount will return a total diff of shares the miner submitted 118 | func (s *DB) GetMinerCurrentRoundContrib(minerName string) (float64, error) { 119 | return s.HGet(context.Background(), s.coin+":shares:contrib", minerName).Float64() 120 | } 121 | 122 | // GetMinerTotalShares will return the number of all valid shares 123 | func (s *DB) GetPoolTotalValidShares() (uint64, error) { 124 | return s.HGet(context.Background(), s.coin+":pool", "validShares").Uint64() 125 | } 126 | 127 | // GetMinerTotalShares will return the number of all valid blocks 128 | func (s *DB) GetPoolTotalValidBlocks() (uint64, error) { 129 | return s.HGet(context.Background(), s.coin+":pool", "validBlocks").Uint64() 130 | } 131 | 132 | // GetMinerTotalShares will return the number of all invalid shares 133 | func (s *DB) GetPoolTotalInvalidShares() (uint64, error) { 134 | return s.HGet(context.Background(), s.coin+":pool", "validShares").Uint64() 135 | } 136 | 137 | // GetMinerTotalShares will return the number of all invalid blocks 138 | func (s *DB) GetPoolTotalInvalidBlocks() (uint64, error) { 139 | return s.HGet(context.Background(), s.coin+":pool", "invalidBlocks").Uint64() 140 | } 141 | 142 | // GetMinerTotalShares will return the number of all invalid blocks 143 | func (s *DB) GetRigHashrate(minerName, rigName string, from, to int64) (hashrate float64, err error) { 144 | slice, err := s.ZRange(context.Background(), s.coin+":miner:"+minerName+":rig:"+rigName+":hashes", from, to).Result() 145 | if err != nil { 146 | return 0.0, err 147 | } 148 | 149 | var totalDiff float64 150 | for i := range slice { 151 | diff, err := strconv.ParseFloat(slice[i], 64) 152 | if err != nil { 153 | return 0.0, err 154 | } 155 | 156 | totalDiff += diff 157 | } 158 | 159 | return totalDiff / float64(to-from), nil 160 | } 161 | 162 | // GetMinerTotalShares will return the number of all invalid blocks 163 | func (s *DB) GetMinerHashrate(minerName string, from, to int64) (hashrate float64, err error) { 164 | slice, err := s.ZRange(context.Background(), s.coin+":miner:"+minerName+":shares", from, to).Result() 165 | if err != nil { 166 | return 0.0, err 167 | } 168 | 169 | var totalDiff float64 170 | for i := range slice { 171 | diff, err := strconv.ParseFloat(slice[i], 64) 172 | if err != nil { 173 | return 0.0, err 174 | } 175 | 176 | totalDiff += diff 177 | } 178 | 179 | return totalDiff / float64(to-from), nil 180 | } 181 | 182 | // GetMinerTotalShares will return the number of all invalid blocks 183 | func (s *DB) GetPoolHashrate(from, to int64) (float64, error) { 184 | slice, err := s.ZRange(context.Background(), s.coin+":pool:shares", from, to).Result() 185 | if err != nil { 186 | return 0.0, err 187 | } 188 | 189 | var totalDiff float64 190 | for i := range slice { 191 | diff, err := strconv.ParseFloat(slice[i], 64) 192 | if err != nil { 193 | return 0.0, err 194 | } 195 | 196 | totalDiff += diff 197 | } 198 | 199 | return totalDiff / float64(to-from), nil 200 | } 201 | 202 | // GetCurrentRoundCount will return a total diff of shares the miner submitted 203 | func (s *DB) GetMinerRigs(minerName string) (float64, error) { 204 | return s.HGet(context.Background(), s.coin+":shares:contrib", minerName).Float64() 205 | } 206 | 207 | // ConfirmBlock alt one pending block to confirmed 208 | func (s *DB) ConfirmBlock(blockHash string) (ok bool, err error) { 209 | return s.SMove(context.Background(), s.coin+":blocks:pending", s.coin+":blocks:confirmed", blockHash).Result() 210 | } 211 | 212 | // KickBlock alt one pending block to kicked 213 | func (s *DB) KickBlock(blockHash string) (ok bool, err error) { 214 | return s.SMove(context.Background(), s.coin+":blocks:pending", s.coin+":blocks:kicked", blockHash).Result() 215 | } 216 | -------------------------------------------------------------------------------- /stratum/client.go: -------------------------------------------------------------------------------- 1 | package stratum 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/binary" 7 | "encoding/hex" 8 | "encoding/json" 9 | "io" 10 | "math/big" 11 | "net" 12 | "strconv" 13 | "time" 14 | 15 | "github.com/mining-pool/not-only-mining-pool/types" 16 | 17 | "github.com/mining-pool/not-only-mining-pool/bans" 18 | "github.com/mining-pool/not-only-mining-pool/config" 19 | "github.com/mining-pool/not-only-mining-pool/daemons" 20 | "github.com/mining-pool/not-only-mining-pool/jobs" 21 | "github.com/mining-pool/not-only-mining-pool/utils" 22 | "github.com/mining-pool/not-only-mining-pool/vardiff" 23 | ) 24 | 25 | type Client struct { 26 | SubscriptionId []byte 27 | Options *config.Options 28 | RemoteAddress net.Addr 29 | 30 | Socket net.Conn 31 | SocketBufIO *bufio.ReadWriter 32 | 33 | LastActivity time.Time 34 | Shares *Shares 35 | 36 | IsAuthorized bool 37 | SubscriptionBeforeAuth bool 38 | 39 | ExtraNonce1 []byte 40 | 41 | VarDiff *vardiff.VarDiff 42 | 43 | WorkerName string 44 | WorkerPass string 45 | 46 | PendingDifficulty *big.Float 47 | CurrentDifficulty *big.Float 48 | PreviousDifficulty *big.Float 49 | 50 | BanningManager *bans.BanningManager 51 | JobManager *jobs.JobManager 52 | SocketClosedEvent chan struct{} 53 | } 54 | 55 | func NewStratumClient(subscriptionId []byte, socket net.Conn, options *config.Options, jm *jobs.JobManager, bm *bans.BanningManager) *Client { 56 | var varDiff *vardiff.VarDiff 57 | if options.Ports[socket.LocalAddr().(*net.TCPAddr).Port] != nil && options.Ports[socket.LocalAddr().(*net.TCPAddr).Port].VarDiff != nil { 58 | varDiff = vardiff.NewVarDiff(options.Ports[socket.LocalAddr().(*net.TCPAddr).Port].VarDiff) 59 | } 60 | 61 | return &Client{ 62 | SubscriptionId: subscriptionId, 63 | PendingDifficulty: big.NewFloat(0), 64 | Options: options, 65 | RemoteAddress: socket.RemoteAddr(), 66 | Socket: socket, 67 | SocketBufIO: bufio.NewReadWriter(bufio.NewReader(socket), bufio.NewWriter(socket)), 68 | LastActivity: time.Now(), 69 | Shares: &Shares{ 70 | Valid: 0, 71 | Invalid: 0, 72 | }, 73 | IsAuthorized: false, 74 | SubscriptionBeforeAuth: false, 75 | ExtraNonce1: jm.ExtraNonce1Generator.GetExtraNonce1(), 76 | 77 | VarDiff: varDiff, 78 | JobManager: jm, 79 | BanningManager: bm, 80 | } 81 | } 82 | 83 | func (sc *Client) ShouldBan(shareValid bool) bool { 84 | if shareValid { 85 | sc.Shares.Valid++ 86 | } else { 87 | sc.Shares.Invalid++ 88 | if sc.Shares.TotalShares() >= sc.Options.Banning.CheckThreshold { 89 | if sc.Shares.BadPercent() < sc.Options.Banning.InvalidPercent { 90 | sc.Shares.Reset() 91 | } else { 92 | // sc.TriggerBanEvent <- strconv.FormatUint(sc.Shares.Invalid, 10) + " out of the last " + strconv.FormatUint(sc.Shares.TotalShares(), 10) + " shares were invalid" 93 | log.Info(strconv.FormatUint(sc.Shares.Invalid, 10) + " out of the last " + strconv.FormatUint(sc.Shares.TotalShares(), 10) + " shares were invalid") 94 | sc.BanningManager.AddBannedIP(sc.RemoteAddress.String()) 95 | log.Warn("closed socket", sc.WorkerName, " due to shares bad percent reached the banning invalid percent threshold") 96 | sc.SocketClosedEvent <- struct{}{} 97 | _ = sc.Socket.Close() 98 | return true 99 | } 100 | } 101 | } 102 | 103 | return false 104 | } 105 | 106 | func (sc *Client) Init() { 107 | sc.SetupSocket() 108 | } 109 | 110 | func (sc *Client) HandleMessage(message *daemons.JsonRpcRequest) { 111 | switch message.Method { 112 | case "mining.subscribe": 113 | sc.HandleSubscribe(message) 114 | case "mining.authorize": 115 | sc.HandleAuthorize(message, true) 116 | case "mining.submit": 117 | sc.LastActivity = time.Now() 118 | sc.HandleSubmit(message) 119 | case "mining.get_transactions": 120 | sc.SendJsonRPC(&daemons.JsonRpcResponse{ 121 | Id: 0, 122 | Result: nil, 123 | Error: nil, // TODO: Support this 124 | }) 125 | default: 126 | log.Warn("unknown stratum method: ", string(utils.Jsonify(message))) 127 | } 128 | } 129 | 130 | func (sc *Client) HandleSubscribe(message *daemons.JsonRpcRequest) { 131 | log.Info("handling subscribe") 132 | if !sc.IsAuthorized { 133 | sc.SubscriptionBeforeAuth = true 134 | } 135 | 136 | extraNonce2Size := sc.JobManager.ExtraNonce2Size 137 | 138 | // TODO 139 | //var err error 140 | //if err != nil { 141 | // sc.SendJson(&daemons.JsonRpcResponse{ 142 | // Id: message.Id, 143 | // Result: nil, 144 | // ErrorCode: &daemons.JsonRpcError{ 145 | // Code: 20, 146 | // Message: err.ErrorCode(), 147 | // }, 148 | // }) 149 | // 150 | // return 151 | //} 152 | 153 | sc.SendJsonRPC(&daemons.JsonRpcResponse{ 154 | Id: message.Id, 155 | Result: utils.Jsonify([]interface{}{ 156 | [][]string{ 157 | {"mining.set_difficulty", strconv.FormatUint(binary.LittleEndian.Uint64(sc.SubscriptionId), 10)}, 158 | {"mining.notify", strconv.FormatUint(binary.LittleEndian.Uint64(sc.SubscriptionId), 10)}, 159 | }, 160 | hex.EncodeToString(sc.ExtraNonce1), 161 | extraNonce2Size, 162 | }), 163 | Error: nil, 164 | }) 165 | } 166 | 167 | func (sc *Client) HandleAuthorize(message *daemons.JsonRpcRequest, replyToSocket bool) { 168 | log.Info("handling authorize") 169 | 170 | sc.WorkerName = string(message.Params[0]) 171 | sc.WorkerPass = string(message.Params[1]) 172 | 173 | authorized, disconnect, err := sc.AuthorizeFn(sc.RemoteAddress, sc.Socket.LocalAddr().(*net.TCPAddr).Port, sc.WorkerName, sc.WorkerPass) 174 | sc.IsAuthorized = err == nil && authorized 175 | 176 | if replyToSocket { 177 | if sc.IsAuthorized { 178 | sc.SendJsonRPC(&daemons.JsonRpcResponse{ 179 | Id: message.Id, 180 | Result: utils.Jsonify(sc.IsAuthorized), 181 | Error: nil, 182 | }) 183 | } else { 184 | sc.SendJsonRPC(&daemons.JsonRpcResponse{ 185 | Id: message.Id, 186 | Result: utils.Jsonify(sc.IsAuthorized), 187 | Error: &daemons.JsonRpcError{ 188 | Code: 20, 189 | Message: string(utils.Jsonify(err)), 190 | }, 191 | }) 192 | } 193 | } 194 | 195 | if disconnect { 196 | log.Warn("closed socket", sc.WorkerName, "due to failed to authorize the miner") 197 | _ = sc.Socket.Close() 198 | sc.SocketClosedEvent <- struct{}{} 199 | } 200 | 201 | // the init Diff for miners 202 | log.Info("sending init difficulty: ", sc.Options.Ports[sc.Socket.LocalAddr().(*net.TCPAddr).Port].Diff) 203 | sc.SendDifficulty(big.NewFloat(sc.Options.Ports[sc.Socket.LocalAddr().(*net.TCPAddr).Port].Diff)) 204 | sc.SendMiningJob(sc.JobManager.CurrentJob.GetJobParams(true)) 205 | } 206 | 207 | // TODO: Can be DIY 208 | func (sc *Client) AuthorizeFn(ip net.Addr, port int, workerName string, password string) (authorized bool, disconnect bool, err error) { 209 | log.Info("Authorize " + workerName + ": " + password + "@" + ip.String()) 210 | return true, false, nil 211 | } 212 | 213 | func (sc *Client) HandleSubmit(message *daemons.JsonRpcRequest) { 214 | /* Avoid hash flood */ 215 | if !sc.IsAuthorized { 216 | sc.SendJsonRPC(&daemons.JsonRpcResponse{ 217 | Id: message.Id, 218 | Result: nil, 219 | Error: &daemons.JsonRpcError{ 220 | Code: 24, 221 | Message: "unauthorized worker", 222 | }, 223 | }) 224 | sc.ShouldBan(false) 225 | return 226 | } 227 | 228 | if sc.ExtraNonce1 == nil { 229 | sc.SendJsonRPC(&daemons.JsonRpcResponse{ 230 | Id: message.Id, 231 | Result: nil, 232 | Error: &daemons.JsonRpcError{ 233 | Code: 25, 234 | Message: "not subscribed", 235 | }, 236 | }) 237 | sc.ShouldBan(false) 238 | return 239 | } 240 | 241 | share := sc.JobManager.ProcessSubmit( 242 | utils.RawJsonToString(message.Params[1]), 243 | sc.PreviousDifficulty, 244 | sc.CurrentDifficulty, 245 | sc.ExtraNonce1, 246 | utils.RawJsonToString(message.Params[2]), 247 | utils.RawJsonToString(message.Params[3]), 248 | utils.RawJsonToString(message.Params[4]), 249 | sc.RemoteAddress, 250 | utils.RawJsonToString(message.Params[0]), 251 | ) 252 | 253 | sc.JobManager.ProcessShare(share) 254 | 255 | if share.ErrorCode == types.ErrLowDiffShare { 256 | // warn the miner with current diff 257 | log.Error("Error on handling submit: sending new diff ", string(utils.Jsonify([]json.RawMessage{utils.Jsonify(sc.CurrentDifficulty)})), " to miner") 258 | f, _ := sc.CurrentDifficulty.Float64() 259 | sc.SendJsonRPC(&daemons.JsonRpcRequest{ 260 | Id: nil, 261 | Method: "mining.set_difficulty", 262 | Params: []json.RawMessage{utils.Jsonify(f)}, 263 | }) 264 | } 265 | 266 | if share.ErrorCode == types.ErrNTimeOutOfRange { 267 | sc.SendMiningJob(sc.JobManager.CurrentJob.GetJobParams(true)) 268 | } 269 | 270 | // vardiff 271 | if sc.VarDiff != nil { 272 | diff, _ := sc.CurrentDifficulty.Float64() 273 | if nextDiff := sc.VarDiff.CalcNextDiff(diff); nextDiff != diff && nextDiff != 0 { 274 | sc.EnqueueNextDifficulty(nextDiff) 275 | } 276 | } 277 | 278 | if sc.PendingDifficulty != nil && sc.PendingDifficulty.Cmp(big.NewFloat(0)) != 0 { 279 | diff := sc.PendingDifficulty 280 | log.Info("sending new difficulty: ", diff) 281 | ok := sc.SendDifficulty(diff) 282 | sc.PendingDifficulty = nil 283 | if ok { 284 | // difficultyChanged 285 | // -> difficultyUpdate client.workerName, diff 286 | displayDiff, _ := diff.Float64() 287 | log.Info("Difficulty update to diff:", displayDiff, "&workerName:", sc.WorkerName) 288 | } 289 | } 290 | 291 | if sc.ShouldBan(share.ErrorCode == 0) { 292 | return 293 | } 294 | 295 | var errParams *daemons.JsonRpcError 296 | if share.ErrorCode != 0 { 297 | errParams = &daemons.JsonRpcError{ 298 | Code: int(share.ErrorCode), 299 | Message: share.ErrorCode.String(), 300 | } 301 | 302 | log.Error(sc.WorkerName, "'s share is invalid: ", errParams.Message) 303 | sc.SendJsonRPC(&daemons.JsonRpcResponse{ 304 | Id: message.Id, 305 | Result: utils.Jsonify(false), 306 | Error: errParams, 307 | }) 308 | } 309 | 310 | log.Info(sc.WorkerName, " submitted a valid share") 311 | sc.SendJsonRPC(&daemons.JsonRpcResponse{ 312 | Id: message.Id, 313 | Result: utils.Jsonify(true), 314 | }) 315 | } 316 | 317 | func (sc *Client) SendJsonRPC(jsonRPCs daemons.JsonRpc) { 318 | raw := jsonRPCs.Json() 319 | 320 | message := make([]byte, 0, len(raw)+1) 321 | message = append(raw, '\n') 322 | _, err := sc.SocketBufIO.Write(message) 323 | if err != nil { 324 | log.Error("failed inputting", string(raw), err) 325 | } 326 | 327 | err = sc.SocketBufIO.Flush() 328 | if err != nil { 329 | log.Error("failed sending data", err) 330 | } 331 | 332 | log.Debug("sent raw bytes: ", string(raw)) 333 | } 334 | 335 | func (sc *Client) SendSubscriptionFirstResponse() { 336 | } 337 | 338 | func (sc *Client) SetupSocket() { 339 | sc.BanningManager.CheckBan(sc.RemoteAddress.String()) 340 | once := true 341 | 342 | go func() { 343 | for { 344 | select { 345 | case <-sc.SocketClosedEvent: 346 | return 347 | default: 348 | raw, err := sc.SocketBufIO.ReadBytes('\n') 349 | if err != nil { 350 | if err == io.EOF { 351 | sc.SocketClosedEvent <- struct{}{} 352 | return 353 | } 354 | e, ok := err.(net.Error) 355 | 356 | if !ok { 357 | log.Error("failed to ready bytes from socket due to non-network error:", err) 358 | return 359 | } 360 | 361 | if ok && e.Timeout() { 362 | log.Error("socket is timeout:", err) 363 | return 364 | } 365 | 366 | if ok && e.Temporary() { 367 | log.Error("failed to ready bytes from socket due to temporary error:", err) 368 | continue 369 | } 370 | 371 | log.Error("failed to ready bytes from socket:", err) 372 | return 373 | } 374 | 375 | if len(raw) > 10240 { 376 | // socketFlooded 377 | log.Warn("Flooding message from", sc.GetLabel(), ":", string(raw)) 378 | _ = sc.Socket.Close() 379 | sc.SocketClosedEvent <- struct{}{} 380 | return 381 | } 382 | 383 | if len(raw) == 0 { 384 | continue 385 | } 386 | 387 | var message daemons.JsonRpcRequest 388 | err = json.Unmarshal(raw, &message) 389 | if err != nil { 390 | if !sc.Options.TCPProxyProtocol { 391 | log.Error("Malformed message from", sc.GetLabel(), ":", string(raw)) 392 | _ = sc.Socket.Close() 393 | sc.SocketClosedEvent <- struct{}{} 394 | } 395 | 396 | return 397 | } 398 | 399 | if once && sc.Options.TCPProxyProtocol { 400 | once = false 401 | if bytes.HasPrefix(raw, []byte("PROXY")) { 402 | sc.RemoteAddress, err = net.ResolveTCPAddr("tcp", string(bytes.Split(raw, []byte(" "))[2])) 403 | if err != nil { 404 | log.Error("failed to resolve tcp addr behind proxy:", err) 405 | } 406 | } else { 407 | log.Error("Client IP detection failed, tcpProxyProtocol is enabled yet did not receive proxy protocol message, instead got data:", raw) 408 | } 409 | } 410 | 411 | sc.BanningManager.CheckBan(sc.RemoteAddress.String()) 412 | 413 | if &message != nil { 414 | log.Debug("handling message: ", string(message.Json())) 415 | sc.HandleMessage(&message) 416 | } 417 | } 418 | } 419 | }() 420 | } 421 | 422 | func (sc *Client) GetLabel() string { 423 | if sc.WorkerName != "" { 424 | return sc.WorkerName + " [" + sc.RemoteAddress.String() + "]" 425 | } else { 426 | return "(unauthorized)" + " [" + sc.RemoteAddress.String() + "]" 427 | } 428 | } 429 | 430 | func (sc *Client) EnqueueNextDifficulty(nextDiff float64) bool { 431 | log.Info("Enqueue next difficulty:", nextDiff) 432 | sc.PendingDifficulty = big.NewFloat(nextDiff) 433 | return true 434 | } 435 | 436 | func (sc *Client) SendDifficulty(diff *big.Float) bool { 437 | if diff == nil { 438 | log.Fatal("trying to send empty diff!") 439 | } 440 | if sc.CurrentDifficulty != nil && diff.Cmp(sc.CurrentDifficulty) == 0 { 441 | return false 442 | } 443 | 444 | sc.PreviousDifficulty = sc.CurrentDifficulty 445 | sc.CurrentDifficulty = diff 446 | 447 | f, _ := diff.Float64() 448 | sc.SendJsonRPC(&daemons.JsonRpcRequest{ 449 | Id: 0, 450 | Method: "mining.set_difficulty", 451 | Params: []json.RawMessage{utils.Jsonify(f)}, 452 | }) 453 | 454 | return true 455 | } 456 | 457 | func (sc *Client) SendMiningJob(jobParams []interface{}) { 458 | log.Info("sending job: ", string(utils.Jsonify(jobParams))) 459 | 460 | lastActivityAgo := time.Since(sc.LastActivity) 461 | if lastActivityAgo > time.Duration(sc.Options.ConnectionTimeout)*time.Second { 462 | log.Info("closed socket", sc.WorkerName, "due to activity timeout") 463 | _ = sc.Socket.Close() 464 | sc.SocketClosedEvent <- struct{}{} 465 | return 466 | } 467 | 468 | if sc.PendingDifficulty != nil && sc.PendingDifficulty.Cmp(big.NewFloat(0)) != 0 { 469 | diff := sc.PendingDifficulty 470 | ok := sc.SendDifficulty(diff) 471 | sc.PendingDifficulty = nil 472 | if ok { 473 | // difficultyChanged 474 | // -> difficultyUpdate client.workerName, diff 475 | displayDiff, _ := diff.Float64() 476 | log.Info("Difficulty update to diff:", displayDiff, "&workerName:", sc.WorkerName) 477 | } 478 | } 479 | 480 | params := make([]json.RawMessage, len(jobParams)) 481 | for i := range jobParams { 482 | params[i] = utils.Jsonify(jobParams[i]) 483 | } 484 | 485 | sc.SendJsonRPC(&daemons.JsonRpcRequest{ 486 | Id: nil, 487 | Method: "mining.notify", 488 | Params: params, 489 | }) 490 | } 491 | 492 | func (sc *Client) ManuallyAuthClient(username, password string) { 493 | sc.HandleAuthorize(&daemons.JsonRpcRequest{ 494 | Id: 1, 495 | Method: "", 496 | Params: []json.RawMessage{utils.Jsonify(username), utils.Jsonify(password)}, 497 | }, false) 498 | } 499 | 500 | func (sc *Client) ManuallySetValues(otherClient *Client) { 501 | sc.ExtraNonce1 = otherClient.ExtraNonce1 502 | sc.PreviousDifficulty = otherClient.PreviousDifficulty 503 | sc.CurrentDifficulty = otherClient.CurrentDifficulty 504 | } 505 | -------------------------------------------------------------------------------- /stratum/server.go: -------------------------------------------------------------------------------- 1 | package stratum 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "encoding/binary" 7 | "net" 8 | "strconv" 9 | "time" 10 | 11 | logging "github.com/ipfs/go-log/v2" 12 | 13 | "github.com/mining-pool/not-only-mining-pool/bans" 14 | "github.com/mining-pool/not-only-mining-pool/config" 15 | "github.com/mining-pool/not-only-mining-pool/daemons" 16 | "github.com/mining-pool/not-only-mining-pool/jobs" 17 | "github.com/mining-pool/not-only-mining-pool/vardiff" 18 | ) 19 | 20 | var log = logging.Logger("stratum") 21 | 22 | type Server struct { 23 | Options *config.Options 24 | Listener net.Listener 25 | 26 | DaemonManager *daemons.DaemonManager 27 | VarDiff *vardiff.VarDiff 28 | JobManager *jobs.JobManager 29 | StratumClients map[uint64]*Client 30 | SubscriptionCounter *SubscriptionCounter 31 | BanningManager *bans.BanningManager 32 | 33 | rebroadcastTicker *time.Ticker 34 | } 35 | 36 | func NewStratumServer(options *config.Options, jm *jobs.JobManager, bm *bans.BanningManager) *Server { 37 | return &Server{ 38 | Options: options, 39 | BanningManager: bm, 40 | SubscriptionCounter: NewSubscriptionCounter(), 41 | 42 | JobManager: jm, 43 | StratumClients: make(map[uint64]*Client), 44 | } 45 | } 46 | 47 | func (ss *Server) Init() (portStarted []int) { 48 | if ss.Options.Banning != nil { 49 | ss.BanningManager.Init() 50 | } 51 | 52 | for port, options := range ss.Options.Ports { 53 | var err error 54 | if options.TLS != nil { 55 | ss.Listener, err = tls.Listen("tcp", ":"+strconv.Itoa(port), options.TLS.ToTLSConfig()) 56 | } else { 57 | ss.Listener, err = net.Listen("tcp", ":"+strconv.Itoa(port)) 58 | } 59 | 60 | if err != nil { 61 | log.Error(err) 62 | continue 63 | } 64 | 65 | portStarted = append(portStarted, port) 66 | //if len(portStarted) == len(ss.Options.Ports) { 67 | // // emit started 68 | //} 69 | } 70 | 71 | if len(portStarted) == 0 { 72 | log.Panic("No port listened") 73 | } 74 | 75 | go func() { 76 | var id string 77 | var txs []byte 78 | ss.rebroadcastTicker = time.NewTicker(time.Duration(ss.Options.JobRebroadcastTimeout) * time.Second) 79 | defer log.Warn("broadcaster stopped") 80 | defer ss.rebroadcastTicker.Stop() 81 | for { 82 | <-ss.rebroadcastTicker.C 83 | go ss.BroadcastCurrentMiningJob(ss.JobManager.CurrentJob.GetJobParams( 84 | id != ss.JobManager.CurrentJob.JobId || !bytes.Equal(txs, ss.JobManager.CurrentJob.TransactionData), 85 | )) 86 | 87 | id = ss.JobManager.CurrentJob.JobId 88 | txs = ss.JobManager.CurrentJob.TransactionData 89 | } 90 | }() 91 | 92 | go func() { 93 | for { 94 | conn, err := ss.Listener.Accept() 95 | if err != nil { 96 | log.Error(err) 97 | continue 98 | } 99 | 100 | if conn != nil { 101 | log.Info("new conn from ", conn.RemoteAddr().String()) 102 | go ss.HandleNewClient(conn) 103 | } 104 | } 105 | }() 106 | 107 | return portStarted 108 | } 109 | 110 | // HandleNewClient converts the conn to an underlying client instance and finally return its unique subscriptionID 111 | func (ss *Server) HandleNewClient(socket net.Conn) []byte { 112 | subscriptionID := ss.SubscriptionCounter.Next() 113 | client := NewStratumClient(subscriptionID, socket, ss.Options, ss.JobManager, ss.BanningManager) 114 | ss.StratumClients[binary.LittleEndian.Uint64(subscriptionID)] = client 115 | // client.connected 116 | 117 | go func() { 118 | for { 119 | <-client.SocketClosedEvent 120 | log.Warn("a client socket closed") 121 | ss.RemoveStratumClientBySubscriptionId(subscriptionID) 122 | // client.disconnected 123 | } 124 | }() 125 | 126 | client.Init() 127 | 128 | return subscriptionID 129 | } 130 | 131 | func (ss *Server) BroadcastCurrentMiningJob(jobParams []interface{}) { 132 | log.Info("broadcasting job params") 133 | for clientId := range ss.StratumClients { 134 | ss.StratumClients[clientId].SendMiningJob(jobParams) 135 | } 136 | } 137 | 138 | func (ss *Server) RemoveStratumClientBySubscriptionId(subscriptionId []byte) { 139 | delete(ss.StratumClients, binary.LittleEndian.Uint64(subscriptionId)) 140 | } 141 | 142 | func (ss *Server) ManuallyAddStratumClient(client *Client) { 143 | subscriptionId := ss.HandleNewClient(client.Socket) 144 | if subscriptionId != nil { 145 | ss.StratumClients[binary.LittleEndian.Uint64(subscriptionId)].ManuallyAuthClient(client.WorkerName, client.WorkerPass) 146 | ss.StratumClients[binary.LittleEndian.Uint64(subscriptionId)].ManuallySetValues(client) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /stratum/shares.go: -------------------------------------------------------------------------------- 1 | package stratum 2 | 3 | import "sync/atomic" 4 | 5 | type Shares struct { 6 | Valid uint64 7 | Invalid uint64 8 | } 9 | 10 | func (s *Shares) TotalShares() uint64 { 11 | return s.Valid + s.Invalid 12 | } 13 | 14 | func (s *Shares) BadPercent() float64 { 15 | return float64(s.Invalid*100) / float64(s.TotalShares()) 16 | } 17 | 18 | func (s *Shares) Reset() { 19 | atomic.StoreUint64(&s.Invalid, 0) 20 | atomic.StoreUint64(&s.Valid, 0) 21 | } 22 | -------------------------------------------------------------------------------- /stratum/subscriptionCounter.go: -------------------------------------------------------------------------------- 1 | package stratum 2 | 3 | import ( 4 | "encoding/binary" 5 | "math" 6 | ) 7 | 8 | // support 18446744073709551615 max conn 9 | type SubscriptionCounter struct { 10 | Count uint64 11 | Padding []byte 12 | } 13 | 14 | func NewSubscriptionCounter() *SubscriptionCounter { 15 | return &SubscriptionCounter{ 16 | Count: 0, 17 | Padding: nil, 18 | } 19 | } 20 | 21 | func (sc *SubscriptionCounter) Next() []byte { 22 | sc.Count++ 23 | if sc.Count == math.MaxUint64 { 24 | sc.Count = 0 25 | } 26 | 27 | b := make([]byte, 8) 28 | binary.LittleEndian.PutUint64(b, sc.Count) 29 | return append(sc.Padding, b...) 30 | } 31 | -------------------------------------------------------------------------------- /transactions/transactions.go: -------------------------------------------------------------------------------- 1 | package transactions 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "math" 7 | "time" 8 | 9 | logging "github.com/ipfs/go-log/v2" 10 | "github.com/mining-pool/not-only-mining-pool/config" 11 | "github.com/mining-pool/not-only-mining-pool/daemons" 12 | "github.com/mining-pool/not-only-mining-pool/utils" 13 | ) 14 | 15 | var log = logging.Logger("tx") 16 | 17 | //type struct { 18 | // CoinbaseValue uint64 // unit: Satoshis 19 | // MasterNode *MasternodeParams 20 | // Superblock []SuperBlockTemplate 21 | // DefaultWitnessCommitment string 22 | //} 23 | 24 | func GenerateOutputTransactions(poolRecipient []byte, recipients []*config.Recipient, rpcData *daemons.GetBlockTemplate) []byte { 25 | reward := rpcData.CoinbaseValue 26 | rewardToPool := reward 27 | txOutputBuffers := make([][]byte, 0) 28 | 29 | if rpcData.Masternode != nil && len(rpcData.Masternode) > 0 { 30 | log.Info("handling dash's masternode") 31 | for i := range rpcData.Masternode { 32 | payeeReward := rpcData.Masternode[i].Amount 33 | reward -= payeeReward 34 | rewardToPool -= payeeReward 35 | 36 | var payeeScript []byte 37 | if len(rpcData.Masternode[i].Script) > 0 { 38 | payeeScript, _ = hex.DecodeString(rpcData.Masternode[i].Script) 39 | } else { 40 | payeeScript = utils.P2PKHAddressToScript(rpcData.Masternode[i].Payee) 41 | } 42 | txOutputBuffers = append(txOutputBuffers, bytes.Join([][]byte{ 43 | utils.PackUint64BE(payeeReward), 44 | utils.VarIntBytes(uint64(len(payeeScript))), 45 | }, nil)) 46 | } 47 | } 48 | 49 | if rpcData.Superblock != nil && len(rpcData.Superblock) > 0 { 50 | log.Info("handling dash's superblock") 51 | for i := range rpcData.Superblock { 52 | payeeReward := rpcData.Superblock[i].Amount 53 | reward -= payeeReward 54 | rewardToPool -= payeeReward 55 | 56 | var payeeScript []byte 57 | if len(rpcData.Superblock[i].Script) > 0 { 58 | payeeScript, _ = hex.DecodeString(rpcData.Superblock[i].Script) 59 | } else { 60 | payeeScript = utils.P2PKHAddressToScript(rpcData.Superblock[i].Payee) 61 | } 62 | 63 | txOutputBuffers = append(txOutputBuffers, bytes.Join([][]byte{ 64 | utils.PackUint64LE(payeeReward), 65 | utils.VarIntBytes(uint64(len(payeeScript))), 66 | payeeScript, 67 | }, nil)) 68 | } 69 | } 70 | 71 | if rpcData.Payee != nil { 72 | var payeeReward uint64 73 | if rpcData.PayeeAmount != nil { 74 | payeeReward = rpcData.PayeeAmount.(uint64) 75 | } else { 76 | payeeReward = uint64(math.Ceil(float64(reward) / 5)) 77 | } 78 | 79 | reward -= payeeReward 80 | rewardToPool -= payeeReward 81 | 82 | payeeScript := utils.P2PKHAddressToScript(rpcData.Payee.(string)) 83 | txOutputBuffers = append(txOutputBuffers, bytes.Join([][]byte{ 84 | utils.PackUint64LE(payeeReward), 85 | utils.VarIntBytes(uint64(len(payeeScript))), 86 | payeeScript, 87 | }, nil)) 88 | } 89 | 90 | for i := range recipients { 91 | script := recipients[i].GetScript() 92 | 93 | recipientReward := uint64(math.Floor(recipients[i].Percent * float64(reward))) 94 | rewardToPool -= recipientReward 95 | 96 | txOutputBuffers = append(txOutputBuffers, bytes.Join([][]byte{ 97 | utils.PackUint64LE(recipientReward), 98 | utils.VarIntBytes(uint64(len(script))), 99 | script, 100 | }, nil)) 101 | } 102 | 103 | txOutputBuffers = append([][]byte{bytes.Join([][]byte{ 104 | utils.PackUint64LE(rewardToPool), 105 | utils.VarIntBytes(uint64(len(poolRecipient))), 106 | poolRecipient, 107 | }, nil)}, txOutputBuffers...) 108 | 109 | if rpcData.DefaultWitnessCommitment != "" { 110 | witnessCommitment, err := hex.DecodeString(rpcData.DefaultWitnessCommitment) 111 | if err != nil { 112 | log.Error(err) 113 | } 114 | 115 | txOutputBuffers = append([][]byte{bytes.Join([][]byte{ 116 | utils.PackUint64LE(0), 117 | utils.VarIntBytes(uint64(len(witnessCommitment))), 118 | witnessCommitment, 119 | }, nil)}, txOutputBuffers...) 120 | } 121 | 122 | return bytes.Join([][]byte{ 123 | utils.VarIntBytes(uint64(len(txOutputBuffers))), 124 | bytes.Join(txOutputBuffers, nil), 125 | }, nil) 126 | } 127 | 128 | func CreateGeneration(rpcData *daemons.GetBlockTemplate, publicKey, extraNoncePlaceholder []byte, reward string, txMessages bool, recipients []*config.Recipient) [][]byte { 129 | var txVersion int 130 | var txComment []byte 131 | txType := 0 132 | var txExtraPayload []byte 133 | if txMessages { 134 | txVersion = 2 135 | txComment = utils.SerializeString("by Command") 136 | } else { 137 | txVersion = 1 138 | txComment = make([]byte, 0) 139 | } 140 | txLockTime := 0 141 | 142 | if rpcData.CoinbasePayload != "" && len(rpcData.CoinbasePayload) > 0 { 143 | txVersion = 3 144 | txType = 5 145 | txExtraPayload, _ = hex.DecodeString(rpcData.CoinbasePayload) 146 | } 147 | 148 | txVersion = txVersion + (txType << 16) 149 | 150 | txInPrevOutHash := "" 151 | txInPrevOutIndex := 1<<32 - 1 152 | txInSequence := 0 153 | 154 | txTimestamp := make([]byte, 0) 155 | if reward == "POS" { 156 | txTimestamp = utils.PackUint32LE(rpcData.CurTime) 157 | } 158 | 159 | bCoinbaseAuxFlags, err := hex.DecodeString(rpcData.CoinbaseAux.Flags) 160 | if err != nil { 161 | log.Error(err) 162 | } 163 | scriptSigPart1 := bytes.Join([][]byte{ 164 | utils.SerializeNumber(uint64(rpcData.Height)), 165 | bCoinbaseAuxFlags, 166 | utils.SerializeNumber(uint64(time.Now().Unix())), 167 | {byte(len(extraNoncePlaceholder))}, 168 | }, nil) 169 | 170 | scriptSigPart2 := utils.SerializeString("/by Command/") 171 | 172 | p1 := bytes.Join([][]byte{ 173 | utils.PackUint32LE(uint32(txVersion)), 174 | txTimestamp, 175 | 176 | // transaction input 177 | utils.VarIntBytes(1), // only one txIn 178 | utils.Uint256BytesFromHash(txInPrevOutHash), 179 | utils.PackUint32LE(uint32(txInPrevOutIndex)), 180 | utils.VarIntBytes(uint64(len(scriptSigPart1) + len(extraNoncePlaceholder) + len(scriptSigPart2))), 181 | scriptSigPart1, 182 | }, nil) 183 | 184 | outputTransactions := GenerateOutputTransactions(publicKey, recipients, rpcData) 185 | 186 | p2 := bytes.Join([][]byte{ 187 | scriptSigPart2, 188 | utils.PackUint32LE(uint32(txInSequence)), 189 | // end transaction input 190 | 191 | // transaction output 192 | outputTransactions, 193 | // end transaction ouput 194 | 195 | utils.PackUint32LE(uint32(txLockTime)), 196 | txComment, 197 | }, nil) 198 | 199 | if len(txExtraPayload) > 0 { 200 | p2 = bytes.Join([][]byte{ 201 | p2, 202 | utils.VarIntBytes(uint64(len(txExtraPayload))), 203 | txExtraPayload, 204 | }, nil) 205 | } 206 | 207 | return [][]byte{p1, p2} 208 | } 209 | -------------------------------------------------------------------------------- /transactions/transactions_test.go: -------------------------------------------------------------------------------- 1 | package transactions 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "encoding/json" 7 | "testing" 8 | 9 | "github.com/mining-pool/not-only-mining-pool/config" 10 | "github.com/mining-pool/not-only-mining-pool/daemons" 11 | "github.com/mining-pool/not-only-mining-pool/utils" 12 | ) 13 | 14 | func TestCreateGeneration(t *testing.T) { 15 | data := ` 16 | { 17 | "capabilities": [ 18 | "proposal" 19 | ], 20 | "version": 536870912, 21 | "rules": [ 22 | "csv", 23 | "segwit" 24 | ], 25 | "vbavailable": { 26 | }, 27 | "vbrequired": 0, 28 | "previousblockhash": "b83b698bed0897ac94819041aec857d1a26a567bf7bf046d60849d5ccf24155e", 29 | "transactions": [ 30 | { 31 | "data": "01000000012f8975c900f56662f35c317a0669fecc5fe0e1fb8ee53f4de72f1cb68c07e606010000008a473044022061a9ac17f269f3c69e18b5d67dfa6bf8b6a5a60eb7f9b0c992ffaeb66b5b88fb02202bb6fd7eb539302d97f4b8604bc822c91747e1cb365fecc91b37526b6b8c2c25014104fe67366f857106ee7b4cc48abb4dabd46302e12fe4140f4c933b92bd3ce75b1f4ae45055312f9a6c5ddc1f8d94d4f6d11e2a13372bcd6bfd651e48997b0f767effffffff02e8030000000000001976a914dffec839eba107e556d6c4f25f90765b3d10583288acbb60da04000000001976a914bdd83cf3ab8b7a57ff9b841752c1ae764f2a02ee88ac00000000", 32 | "txid": "f9b8b0bdd0dc38b2a707faf89acf064f543c3a88d39f54fb126cbd084ffb5ed9", 33 | "hash": "f9b8b0bdd0dc38b2a707faf89acf064f543c3a88d39f54fb126cbd084ffb5ed9", 34 | "depends": [ 35 | ], 36 | "fee": 6450, 37 | "sigops": 8, 38 | "weight": 1028 39 | }, 40 | { 41 | "data": "0200000001979a795a82096fc375487778939d9193bb284c58525e5df9c3a404c81c9220ef01000000d9004730440220086f0b09ded442c84e602520f5a8b38b41a1bc860fb595bd47834c20fa8db39402200a40cb86c15198302cabfd5c620c24fa6ac9ac5d946394e37dd3f9960b65a0e701473044022049bc0be153a4535196f73455bf82667956f2089019db4eeb57cb35649d8f69b202206ddd411917cb3e54f7b9a89c0693c971a6499eb6e421dce1d5fa9358300525d301475221025ad7eedea4c87b98463b8c7316c139f94c0e75fe4c849f42dab112479e1a1bb7210257591ace4d6a9fc94b8114cffd84df9bd0349c974a792580f7f5afb74f5ba94952ae0000000002102700000000000017a914b75a640760f2caae367c0e0cd6bfb85e8d80755987e17608000000000017a914ef20c4471b54fc47c93d587a318d351e93fbc13b8700000000", 42 | "txid": "620c724890f76b802714d786d5d3fe13a89106d81e93b74c4eafd6dc04179f37", 43 | "hash": "620c724890f76b802714d786d5d3fe13a89106d81e93b74c4eafd6dc04179f37", 44 | "depends": [ 45 | ], 46 | "fee": 3493, 47 | "sigops": 8, 48 | "weight": 1328 49 | } 50 | ], 51 | "coinbaseaux": { 52 | "flags": "" 53 | }, 54 | "coinbasevalue": 2500009943, 55 | "longpollid": "b83b698bed0897ac94819041aec857d1a26a567bf7bf046d60849d5ccf24155e19620", 56 | "target": "00000000a9490000000000000000000000000000000000000000000000000000", 57 | "mintime": 1581747579, 58 | "mutable": [ 59 | "time", 60 | "transactions", 61 | "prevblock" 62 | ], 63 | "noncerange": "00000000ffffffff", 64 | "sigoplimit": 80000, 65 | "sizelimit": 4000000, 66 | "weightlimit": 4000000, 67 | "curtime": 1581749398, 68 | "bits": "1d00a949", 69 | "height": 1369986 70 | } 71 | 72 | ` 73 | 74 | var rpcData daemons.GetBlockTemplate 75 | _ = json.Unmarshal([]byte(data), &rpcData) 76 | 77 | pk := utils.P2PKHAddressToScript("QPxrDq3sorCk8DWaYX2GeCkxoePhm1asyY") 78 | placeholder, _ := hex.DecodeString("f000000ff111111f") 79 | 80 | t.Log(hex.EncodeToString(utils.PackUint32LE(uint32(0)))) 81 | 82 | gens := CreateGeneration(&rpcData, pk, placeholder, "POW", true, []*config.Recipient{}) 83 | 84 | t.Log("0: ", hex.EncodeToString(gens[0])) 85 | t.Log("1: ", hex.EncodeToString(gens[1])) 86 | 87 | t1, _ := hex.DecodeString("02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff1f0382e71404") 88 | if !bytes.Contains(gens[0], t1) { 89 | t.Fail() 90 | } 91 | 92 | if hex.EncodeToString(gens[1]) != "0c2f627920436f6d6d616e642f0000000001d71f0395000000001976a91424da8749fde8fcdcde60ba1c5afea8d2bd4a4f2688ac000000000a627920436f6d6d616e64" { 93 | t.Fail() 94 | } 95 | } 96 | 97 | func TestGenerateOutputTransactions(t *testing.T) { 98 | publicKey := utils.P2PKHAddressToScript("QPxrDq3sorCk8DWaYX2GeCkxoePhm1asyY") 99 | recipients := make([]*config.Recipient, 0) 100 | data := ` 101 | { 102 | "capabilities": [ 103 | "proposal" 104 | ], 105 | "version": 536870912, 106 | "rules": [ 107 | "csv", 108 | "segwit" 109 | ], 110 | "vbavailable": { 111 | }, 112 | "vbrequired": 0, 113 | "previousblockhash": "b83b698bed0897ac94819041aec857d1a26a567bf7bf046d60849d5ccf24155e", 114 | "transactions": [ 115 | { 116 | "data": "01000000012f8975c900f56662f35c317a0669fecc5fe0e1fb8ee53f4de72f1cb68c07e606010000008a473044022061a9ac17f269f3c69e18b5d67dfa6bf8b6a5a60eb7f9b0c992ffaeb66b5b88fb02202bb6fd7eb539302d97f4b8604bc822c91747e1cb365fecc91b37526b6b8c2c25014104fe67366f857106ee7b4cc48abb4dabd46302e12fe4140f4c933b92bd3ce75b1f4ae45055312f9a6c5ddc1f8d94d4f6d11e2a13372bcd6bfd651e48997b0f767effffffff02e8030000000000001976a914dffec839eba107e556d6c4f25f90765b3d10583288acbb60da04000000001976a914bdd83cf3ab8b7a57ff9b841752c1ae764f2a02ee88ac00000000", 117 | "txid": "f9b8b0bdd0dc38b2a707faf89acf064f543c3a88d39f54fb126cbd084ffb5ed9", 118 | "hash": "f9b8b0bdd0dc38b2a707faf89acf064f543c3a88d39f54fb126cbd084ffb5ed9", 119 | "depends": [ 120 | ], 121 | "fee": 6450, 122 | "sigops": 8, 123 | "weight": 1028 124 | }, 125 | { 126 | "data": "0200000001979a795a82096fc375487778939d9193bb284c58525e5df9c3a404c81c9220ef01000000d9004730440220086f0b09ded442c84e602520f5a8b38b41a1bc860fb595bd47834c20fa8db39402200a40cb86c15198302cabfd5c620c24fa6ac9ac5d946394e37dd3f9960b65a0e701473044022049bc0be153a4535196f73455bf82667956f2089019db4eeb57cb35649d8f69b202206ddd411917cb3e54f7b9a89c0693c971a6499eb6e421dce1d5fa9358300525d301475221025ad7eedea4c87b98463b8c7316c139f94c0e75fe4c849f42dab112479e1a1bb7210257591ace4d6a9fc94b8114cffd84df9bd0349c974a792580f7f5afb74f5ba94952ae0000000002102700000000000017a914b75a640760f2caae367c0e0cd6bfb85e8d80755987e17608000000000017a914ef20c4471b54fc47c93d587a318d351e93fbc13b8700000000", 127 | "txid": "620c724890f76b802714d786d5d3fe13a89106d81e93b74c4eafd6dc04179f37", 128 | "hash": "620c724890f76b802714d786d5d3fe13a89106d81e93b74c4eafd6dc04179f37", 129 | "depends": [ 130 | ], 131 | "fee": 3493, 132 | "sigops": 8, 133 | "weight": 1328 134 | } 135 | ], 136 | "coinbaseaux": { 137 | "flags": "" 138 | }, 139 | "coinbasevalue": 2500009943, 140 | "longpollid": "b83b698bed0897ac94819041aec857d1a26a567bf7bf046d60849d5ccf24155e19620", 141 | "target": "00000000a9490000000000000000000000000000000000000000000000000000", 142 | "mintime": 1581747579, 143 | "mutable": [ 144 | "time", 145 | "transactions", 146 | "prevblock" 147 | ], 148 | "noncerange": "00000000ffffffff", 149 | "sigoplimit": 80000, 150 | "sizelimit": 4000000, 151 | "weightlimit": 4000000, 152 | "curtime": 1581749398, 153 | "bits": "1d00a949", 154 | "height": 1369986 155 | } 156 | 157 | ` 158 | var rpcData daemons.GetBlockTemplate 159 | json.Unmarshal([]byte(data), &rpcData) 160 | t.Log(hex.EncodeToString(GenerateOutputTransactions(publicKey, recipients, &rpcData))) 161 | } 162 | 163 | // 00000020fb08e0b3cb0f759671af79f108dd2dbd1a378ba27968c176c1c6d64f94741d262a7ca761bb4397d2c1a7f6cf457d680f054d43cc4f860de21b776054ab93a3cafbe34b5effff0f1e00452ef00401000000010000000000000000000000000000000000000000000000000000000000000000ffffffff1f0377ee1404fce34b5e086b3c0000000000000c2f627920436f6d6d616e642f00000000020000000000000000266a24aa21a9ed8a44e041a5a86878a1742f66fe7196400e784fee5cdc70a4becaf51c8f4a4f0266140395000000001976a91424da8749fde8fcdcde60ba1c5afea8d2bd4a4f2688ac0000000001000000000101df2565bde1779eaa6aad06a03a5262d324de29aa51735ba26d2301c0af426ee90100000000ffffffff020000000000000000136a0c0701007ac0010000c0000000530345d47106fa2f0b0000000016001407fa56d069e6174b6fa1ca3e27556be765064e150247304402203fb97652eee91717f61a9a9a66c8c233648ac3f5942aeb246c2217ffdc64b5f70220210653c0bc9c74b026e80e77a3221a6c64c9529d37a2051c53aefb19213f381d012102a56c007c837c6323332f03f2d22190f1da0aec10c2338da50ebcec85100e9a96000000000100000000010129d40378ffb37a1b2b751e4469aed63df827636e538e76550f6629dc49978f0b0100000000f0ffffff0340420f00000000001976a914ab83ab1e9284beca76ecdd1460f732acdeb5a45688ac0bd9460000000000160014756b524ee4ec544d7828cb849951b75bf46cf9d30000000000000000196a1768747470733a2f2f746c74632e6269746170732e636f6d02483045022100ab49baf3f2f0ebc910f2d7453a5a50bc10810a17d9a77ddc98638b1fc1a93929022062d798622d7f5a55655e1356b9e0e7fb0d2c40d70df0c10c5ba91311a085559c012102ab861da09e496373d8aee62107d68f8275df04dca403c182b6ab648eebb4aca50000000001000000000101fdaffc6f8c94565763bdf4c0e50c389c5eb817d2dff247a0ffc519dda211dce90100000000f0ffffff0340420f00000000001976a914ab83ab1e9284beca76ecdd1460f732acdeb5a45688ac29d5440000000000160014d3cb800cd29671af47dfd95fcb759a7e76e4b0dd0000000000000000196a1768747470733a2f2f746c74632e6269746170732e636f6d02473044022060f807e10801d10ba51870bbbaee01d80d3727730b43b9c710af80857c14e4d102201002940096d20427246a7738c358b29675108260b18cc612922b1a01b2a588ab0121031506590ee0b0a9cfa13dbc765d9ac9666e5e01d031c5bd5b5e293bcdeb2932af00000000 164 | // 00000020763600ad521ebbb8be835992a5f7e1e315d3978934ed805bfffd2e88b7d65c7c073172cf11eb40f1b663749268e2c63b1c2b1e54fb80d69467c9b5017eb9437cc0e34b5effff0f1e002aaa0e0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff1f0370ee1404c1e34b5e0840000000000000000c2f627920436f6d6d616e642f00000000020000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf900f90295000000001976a91424da8749fde8fcdcde60ba1c5afea8d2bd4a4f2688ac00000000 165 | -------------------------------------------------------------------------------- /types/block.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // Block is a basic type for db storage 4 | type Block struct { 5 | Hash string `json:"hash"` 6 | Confirmations int `json:"confirmations"` 7 | StrippedSize int `json:"strippedsize"` 8 | Size int `json:"size"` 9 | Weight int `json:"weight"` 10 | Height int `json:"height"` 11 | Version int `json:"version"` 12 | VersionHex string `json:"versionHex"` 13 | MerkleRoot string `json:"merkleroot"` 14 | Tx []string `json:"tx"` 15 | Time int `json:"time"` 16 | MedianTime int `json:"mediantime"` 17 | Nonce int `json:"nonce"` 18 | Bits string `json:"bits"` 19 | Difficulty float64 `json:"difficulty"` 20 | ChainWork string `json:"chainwork"` 21 | NTx int `json:"nTx"` 22 | PreviousBlockHash string `json:"previousblockhash"` 23 | } 24 | -------------------------------------------------------------------------------- /types/errors.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type ErrorWrap int 4 | 5 | const ( 6 | ErrJobNotFound ErrorWrap = 20 7 | ErrIncorrectExtraNonce2Size ErrorWrap = 21 8 | ErrIncorrectNTimeSize ErrorWrap = 22 9 | ErrNTimeOutOfRange ErrorWrap = 23 10 | ErrIncorrectNonceSize ErrorWrap = 24 11 | ErrDuplicateShare ErrorWrap = 25 12 | ErrLowDiffShare ErrorWrap = 26 13 | ) 14 | 15 | var codeToErrMap = map[int]string{ 16 | 10: "you are banned by pool", 17 | 20: "job not found", 18 | 21: "incorrect size of extranonce2", 19 | 22: "incorrect size of ntime", 20 | 23: "ntime out of range", 21 | 24: "incorrect size of nonce", 22 | 25: "duplicate share", 23 | 26: "low difficulty share", 24 | } 25 | 26 | func (err ErrorWrap) String() string { 27 | return codeToErrMap[int(err)] 28 | } 29 | -------------------------------------------------------------------------------- /types/share.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | type Share struct { 8 | JobId string `json:"jobId"` 9 | RemoteAddr net.Addr `json:"remoteAddr"` 10 | Miner string `json:"miner"` 11 | Rig string `json:"rig"` 12 | ErrorCode ErrorWrap `json:"errorCode"` 13 | BlockHeight int64 `json:"height"` 14 | BlockReward uint64 `json:"blockReward"` 15 | Diff float64 `json:"shareDiff"` 16 | BlockHash string `json:"blockHash"` 17 | BlockHex string `json:"blockHex"` 18 | TxHash string `json:"txHash"` 19 | } 20 | -------------------------------------------------------------------------------- /types/stats.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // Stats a cache for api, reducing the fetch times of pool status 4 | //type Stats struct { 5 | // hashrate 6 | // workerCount 7 | // blocks 8 | //} 9 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "crypto/sha256" 7 | "encoding/binary" 8 | "encoding/hex" 9 | "encoding/json" 10 | "fmt" 11 | "math/big" 12 | "os" 13 | "strconv" 14 | "strings" 15 | 16 | "github.com/c0mm4nd/go-bech32" 17 | logging "github.com/ipfs/go-log/v2" 18 | "github.com/mr-tron/base58" 19 | ) 20 | 21 | var log = logging.Logger("utils") 22 | 23 | func RandPositiveInt64() int64 { 24 | randomNumBytes := make([]byte, 8) 25 | _, err := rand.Read(randomNumBytes) 26 | if err != nil { 27 | log.Error(err) 28 | } 29 | 30 | i := int64(binary.LittleEndian.Uint64(randomNumBytes)) 31 | if i > 0 { 32 | return i 33 | } else { 34 | return -i 35 | } 36 | } 37 | 38 | func RandHexUint64() string { 39 | randomNumBytes := make([]byte, 8) 40 | _, err := rand.Read(randomNumBytes) 41 | if err != nil { 42 | log.Error(err) 43 | } 44 | 45 | return hex.EncodeToString(randomNumBytes) 46 | } 47 | 48 | func PackUint64LE(n uint64) []byte { 49 | b := make([]byte, 8) 50 | binary.LittleEndian.PutUint64(b, n) 51 | return b 52 | } 53 | 54 | func PackInt64BE(n int64) []byte { 55 | b := make([]byte, 8) 56 | binary.BigEndian.PutUint64(b, uint64(n)) 57 | return b 58 | } 59 | 60 | func PackUint64BE(n uint64) []byte { 61 | b := make([]byte, 8) 62 | binary.BigEndian.PutUint64(b, n) 63 | return b 64 | } 65 | 66 | func PackUint32LE(n uint32) []byte { 67 | b := make([]byte, 4) 68 | binary.LittleEndian.PutUint32(b, n) 69 | return b 70 | } 71 | 72 | func PackUint32BE(n uint32) []byte { 73 | b := make([]byte, 4) 74 | binary.BigEndian.PutUint32(b, n) 75 | return b 76 | } 77 | 78 | func PackInt32BE(n int32) []byte { 79 | b := make([]byte, 4) 80 | binary.BigEndian.PutUint32(b, uint32(n)) 81 | return b 82 | } 83 | 84 | func PackUint16LE(n uint16) []byte { 85 | b := make([]byte, 2) 86 | binary.LittleEndian.PutUint16(b, n) 87 | return b 88 | } 89 | 90 | func PackUint16BE(n uint16) []byte { 91 | b := make([]byte, 2) 92 | binary.BigEndian.PutUint16(b, n) 93 | return b 94 | } 95 | 96 | func VarIntBytes(n uint64) []byte { 97 | if n < 0xFD { 98 | return []byte{byte(n)} 99 | } 100 | 101 | if n <= 0xFFFF { 102 | buff := make([]byte, 3) 103 | buff[0] = 0xFD 104 | binary.LittleEndian.PutUint16(buff[1:], uint16(n)) 105 | return buff 106 | } 107 | 108 | if n <= 0xFFFFFFFF { 109 | buff := make([]byte, 5) 110 | buff[0] = 0xFE 111 | binary.LittleEndian.PutUint32(buff[1:], uint32(n)) 112 | return buff 113 | } 114 | 115 | buff := make([]byte, 9) 116 | buff[0] = 0xFF 117 | binary.LittleEndian.PutUint64(buff[1:], uint64(n)) 118 | return buff 119 | } 120 | 121 | func VarStringBytes(str string) []byte { 122 | bStr := []byte(str) 123 | return bytes.Join([][]byte{ 124 | VarIntBytes(uint64(len(bStr))), 125 | bStr, 126 | }, nil) 127 | } 128 | 129 | func SerializeString(s string) []byte { 130 | if len(s) < 253 { 131 | return bytes.Join([][]byte{ 132 | {byte(len(s))}, 133 | []byte(s), 134 | }, nil) 135 | } else if len(s) < 0x10000 { 136 | return bytes.Join([][]byte{ 137 | {253}, 138 | PackUint16LE(uint16(len(s))), 139 | []byte(s), 140 | }, nil) 141 | } else if len(s) < 0x100000000 { 142 | return bytes.Join([][]byte{ 143 | {254}, 144 | PackUint32LE(uint32(len(s))), 145 | []byte(s), 146 | }, nil) 147 | } else { 148 | return bytes.Join([][]byte{ 149 | {255}, 150 | PackUint64LE(uint64(len(s))), 151 | []byte(s), 152 | }, nil) 153 | } 154 | } 155 | 156 | func SerializeNumber(n uint64) []byte { 157 | if n >= 1 && n <= 16 { 158 | return []byte{ 159 | 0x50 + byte(n), 160 | } 161 | } 162 | 163 | l := 1 164 | buff := make([]byte, 9) 165 | for n > 0x7f { 166 | buff[l] = byte(n & 0xff) 167 | l++ 168 | n >>= 8 169 | } 170 | buff[0] = byte(l) 171 | buff[l] = byte(n) 172 | 173 | return buff[0 : l+1] 174 | } 175 | 176 | func Uint256BytesFromHash(h string) []byte { 177 | container := make([]byte, 32) 178 | fromHex, err := hex.DecodeString(h) 179 | if err != nil { 180 | log.Error(err) 181 | } 182 | 183 | copy(container, fromHex) 184 | 185 | return ReverseBytes(container) 186 | } 187 | 188 | func ReverseBytes(b []byte) []byte { 189 | _b := make([]byte, len(b)) 190 | copy(_b, b) 191 | 192 | for i, j := 0, len(_b)-1; i < j; i, j = i+1, j-1 { 193 | _b[i], _b[j] = _b[j], _b[i] 194 | } 195 | return _b 196 | } 197 | 198 | // range steps between [start, end) 199 | func Range(start, stop, step int) []int { 200 | if (step > 0 && start >= stop) || (step < 0 && start <= stop) { 201 | return []int{} 202 | } 203 | 204 | result := make([]int, 0) 205 | i := start 206 | for { 207 | if step > 0 { 208 | if i < stop { 209 | result = append(result, i) 210 | } else { 211 | break 212 | } 213 | } else { 214 | if i > stop { 215 | result = append(result, i) 216 | } else { 217 | break 218 | } 219 | } 220 | i += step 221 | } 222 | 223 | return result 224 | } 225 | 226 | func Sha256(b []byte) []byte { 227 | b32 := sha256.Sum256(b) 228 | return b32[:] 229 | } 230 | 231 | func Sha256d(b []byte) []byte { 232 | return Sha256(Sha256(b)) 233 | } 234 | 235 | func BytesIndexOf(data [][]byte, element []byte) int { 236 | for k, v := range data { 237 | if bytes.Equal(element, v) { 238 | return k 239 | } 240 | } 241 | return -1 // not found. 242 | } 243 | 244 | func StringsIndexOf(data []string, element string) int { 245 | for k, v := range data { 246 | if strings.Compare(element, v) == 0 { 247 | return k 248 | } 249 | } 250 | return -1 // not found. 251 | } 252 | 253 | func BigIntFromBitsHex(bits string) *big.Int { 254 | bBits, err := hex.DecodeString(bits) 255 | if err != nil { 256 | log.Panic(err) 257 | } 258 | return BigIntFromBitsBytes(bBits) 259 | } 260 | 261 | func BigIntFromBitsBytes(bits []byte) *big.Int { 262 | bytesNumber := bits[0] 263 | 264 | bigBits := new(big.Int).SetBytes(bits[1:]) 265 | return new(big.Int).Mul(bigBits, new(big.Int).Exp(big.NewInt(2), big.NewInt(8*int64(bytesNumber-3)), nil)) 266 | } 267 | 268 | // LE <-> BE 269 | func ReverseByteOrder(b []byte) []byte { 270 | _b := make([]byte, len(b)) 271 | copy(_b, b) 272 | 273 | for i := 0; i < 8; i++ { 274 | binary.LittleEndian.PutUint32(_b[i*4:], binary.BigEndian.Uint32(_b[i*4:])) 275 | } 276 | return ReverseBytes(_b) 277 | } 278 | 279 | // For POS coins - used to format wallet address for use in generation transaction's output 280 | func PublicKeyToScript(key string) []byte { 281 | if len(key) != 66 { 282 | log.Panic("Invalid public key: " + key) 283 | } 284 | 285 | bKey := make([]byte, 35) 286 | bKey[0] = 0x21 287 | bKey[34] = 0xAC 288 | b, _ := hex.DecodeString(key) 289 | copy(bKey[1:], b) 290 | 291 | return bKey 292 | } 293 | 294 | // For POW coins - used to format wallet address for use in generation transaction's output 295 | // Works for p2pkh only 296 | func P2PKHAddressToScript(addr string) []byte { 297 | decoded, err := base58.FastBase58Decoding(addr) 298 | if decoded == nil || err != nil { 299 | log.Fatal("base58 decode failed for " + addr) 300 | } 301 | 302 | if len(decoded) != 25 { 303 | log.Panic("invalid address length for " + addr) 304 | } 305 | 306 | publicKey := decoded[1 : len(decoded)-4] 307 | 308 | return bytes.Join([][]byte{ 309 | {0x76, 0xA9, 0x14}, 310 | publicKey, 311 | {0x88, 0xAC}, 312 | }, nil) 313 | } 314 | 315 | func P2SHAddressToScript(addr string) []byte { 316 | decoded, err := base58.FastBase58Decoding(addr) 317 | if decoded == nil || err != nil { 318 | log.Fatal("base58 decode failed for " + addr) 319 | } 320 | 321 | if len(decoded) != 25 { 322 | log.Panic("invalid address length for " + addr) 323 | } 324 | 325 | publicKey := decoded[1 : len(decoded)-4] 326 | 327 | return bytes.Join([][]byte{ 328 | {0xA9, 0x14}, 329 | publicKey, 330 | {0x87}, 331 | }, nil) 332 | } 333 | 334 | func P2WSHAddressToScript(addr string) []byte { 335 | _, decoded, err := bech32.Decode(addr) 336 | if decoded == nil || err != nil { 337 | log.Fatal("bech32 decode failed for " + addr) 338 | } 339 | witnessProgram, err := bech32.ConvertBits(decoded[1:], 5, 8, true) 340 | if err != nil { 341 | log.Panic("") 342 | } 343 | 344 | return bytes.Join([][]byte{ 345 | {0x00, 0x14}, 346 | witnessProgram, 347 | }, nil) 348 | } 349 | 350 | func ScriptPubKeyToScript(addr string) []byte { 351 | decoded, err := hex.DecodeString(addr) 352 | if decoded == nil || err != nil { 353 | log.Fatal("hex decode failed for " + addr) 354 | } 355 | return decoded 356 | } 357 | 358 | func HexDecode(b []byte) []byte { 359 | dst := make([]byte, hex.DecodedLen(len(b))) 360 | _, err := hex.Decode(dst, b) 361 | if err != nil { 362 | log.Panic("failed to decode hex: ", string(b)) 363 | } 364 | 365 | return dst 366 | } 367 | 368 | func HexEncode(b []byte) []byte { 369 | dst := make([]byte, hex.EncodedLen(len(b))) 370 | hex.Encode(dst, b) 371 | 372 | return dst 373 | } 374 | 375 | func Jsonify(i interface{}) []byte { 376 | r, err := json.Marshal(i) 377 | if err != nil { 378 | log.Error("Jsonify: ", err) 379 | return nil 380 | } 381 | 382 | return r 383 | } 384 | 385 | func JsonifyIndentString(i interface{}) string { 386 | r, err := json.MarshalIndent(i, "", " ") 387 | if err != nil { 388 | log.Error("JsonifyIndentString: ", err) 389 | return "" 390 | } 391 | return string(r) 392 | } 393 | 394 | func SatoshisToCoins(satoshis uint64, magnitude int, coinPrecision int) float64 { 395 | coins := float64(satoshis) / float64(magnitude) 396 | coins, _ = strconv.ParseFloat(fmt.Sprintf("%."+strconv.Itoa(coinPrecision)+"f", coins), 64) 397 | return coins 398 | } 399 | 400 | func CoinsToSatoshis(coins float64, magnitude int, coinPrecision int) uint64 { 401 | return uint64(coins * float64(magnitude)) 402 | } 403 | 404 | func GetReadableHashRateString(hashrate float64) string { 405 | i := 0 406 | byteUnits := []string{" H", " KH", " MH", " GH", " TH", " PH", " EH", " ZH", " YH"} 407 | for hashrate > 1000 { 408 | i++ 409 | hashrate = hashrate / 1000 410 | if i+1 == len(byteUnits) { 411 | break 412 | } 413 | } 414 | 415 | return strconv.FormatFloat(hashrate, 'f', 7, 64) + byteUnits[i] 416 | } 417 | 418 | func MiningKeyToScript(addrMiningKey string) []byte { 419 | b, err := hex.DecodeString(addrMiningKey) 420 | if err != nil { 421 | log.Fatal("Failed to decode addr mining key: ", addrMiningKey) 422 | } 423 | 424 | return bytes.Join([][]byte{ 425 | {0x76, 0xA9, 0x14}, 426 | b, 427 | {0x88, 0xAC}, 428 | }, nil) 429 | } 430 | 431 | func RawJsonToString(raw json.RawMessage) string { 432 | var str string 433 | err := json.Unmarshal(raw, &str) 434 | if err != nil { 435 | log.Error(err) 436 | } 437 | 438 | return str 439 | } 440 | 441 | func FixedLenStringBytes(s string, l int) []byte { 442 | b := make([]byte, l) 443 | copy(b, s) 444 | return b 445 | } 446 | 447 | func CommandStringBytes(s string) []byte { 448 | return FixedLenStringBytes(s, 12) 449 | } 450 | 451 | func FileExists(filename string) bool { 452 | info, err := os.Stat(filename) 453 | if os.IsNotExist(err) { 454 | return false 455 | } 456 | return !info.IsDir() 457 | } 458 | -------------------------------------------------------------------------------- /utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "fmt" 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/c0mm4nd/go-bech32" 11 | ) 12 | 13 | func TestSerializeNumber(t *testing.T) { 14 | if !bytes.Equal(SerializeNumber(100), []byte{0x01, 0x64}) { 15 | fmt.Println(SerializeNumber(100)) 16 | t.Fail() 17 | } 18 | 19 | if hex.EncodeToString(SerializeNumber(1<<31-1)) != "04ffffff7f" { 20 | t.Fail() 21 | } 22 | } 23 | 24 | func TestSerializeString(t *testing.T) { 25 | if !bytes.Equal(SerializeString("HelloWorld"), []byte{0x0a, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x57, 0x6f, 0x72, 0x6c, 0x64}) { 26 | fmt.Println(SerializeString("HelloWorld")) 27 | t.Fail() 28 | } 29 | } 30 | 31 | func TestReverseByteOrder(t *testing.T) { 32 | hash := Sha256([]byte("0000")) 33 | fmt.Println(hash) 34 | fmt.Println(ReverseByteOrder(hash)) 35 | } 36 | 37 | func TestReverseBytes(t *testing.T) { 38 | hash := Sha256([]byte("0000")) 39 | fmt.Println(hash) 40 | fmt.Println(ReverseBytes(hash)) 41 | fmt.Println(hex.EncodeToString(hash)) 42 | fmt.Println(hex.EncodeToString(ReverseBytes(hash))) 43 | } 44 | 45 | func TestUint256BytesFromHash(t *testing.T) { 46 | result, _ := hex.DecodeString("691938264876d1078051da4e30ec0643262e8b93fca661f525fe7122b38d5f18") 47 | if !bytes.Equal(Uint256BytesFromHash(hex.EncodeToString(Sha256([]byte("Hello")))), result) { 48 | t.Fail() 49 | } 50 | } 51 | 52 | func TestVarIntBytes(t *testing.T) { 53 | if hex.EncodeToString(VarIntBytes(uint64(23333))) != "fd255b" { 54 | t.Fail() 55 | } 56 | 57 | t.Log(int64(1<<31 - 1)) 58 | if hex.EncodeToString(VarIntBytes(uint64(1<<31-1))) != "feffffff7f" { 59 | t.Fail() 60 | } 61 | } 62 | 63 | func TestVarStringBytes(t *testing.T) { 64 | if hex.EncodeToString(VarStringBytes("Hello")) != "0548656c6c6f" { 65 | t.Fail() 66 | } 67 | } 68 | 69 | func TestRange(t *testing.T) { 70 | if !reflect.DeepEqual(Range(0, 8, 2), []int{0, 2, 4, 6}) { 71 | t.Fail() 72 | } 73 | 74 | if !reflect.DeepEqual(Range(2, 0, 2), []int{}) { 75 | t.Fail() 76 | } 77 | } 78 | 79 | func TestSha256d(t *testing.T) { 80 | hexStr := "01000000" + 81 | "81cd02ab7e569e8bcd9317e2fe99f2de44d49ab2b8851ba4a308000000000000" + 82 | "e320b6c2fffc8d750423db8b1eb942ae710e951ed797f7affc8892b0f1fc122b" + 83 | "c7f5d74d" + 84 | "f2b9441a" + 85 | "42a14695" 86 | raw, _ := hex.DecodeString(hexStr) 87 | if hex.EncodeToString(Sha256d(raw)) != "1dbd981fe6985776b644b173a4d0385ddc1aa2a829688d1e0000000000000000" { 88 | t.Fail() 89 | } 90 | } 91 | 92 | func TestP2PKHAddressToScript(t *testing.T) { 93 | addr := "mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn" 94 | 95 | t.Log(hex.EncodeToString(P2PKHAddressToScript(addr))) 96 | } 97 | 98 | func TestP2SHAddressToScript(t *testing.T) { 99 | addr := "QcGaxM7GsauRBS4CD2rzkE34HZci2kBeF4" 100 | 101 | t.Log(hex.EncodeToString(P2SHAddressToScript(addr))) 102 | } 103 | 104 | func TestP2WSHAddressToScript(t *testing.T) { 105 | addr := "tb1qphxtwvfxyjhq5ar2hn65eczg8u6stam2n7znx5" 106 | str, decoded, err := bech32.Decode(addr) 107 | if decoded == nil || err != nil { 108 | log.Fatal("base58 decode failed for " + addr) 109 | } 110 | if len(decoded) == 0 || len(decoded) > 65 { 111 | log.Fatal("invalid address length for " + addr) 112 | } 113 | t.Log(str) 114 | witnessProgram, _ := bech32.ConvertBits(decoded[1:], 5, 8, true) 115 | 116 | t.Log(len(decoded)) 117 | // publicKey := decoded[1:len(decoded)] 118 | 119 | t.Log(hex.EncodeToString(witnessProgram)) 120 | } 121 | 122 | func TestCommandStringBytes(t *testing.T) { 123 | if hex.EncodeToString(CommandStringBytes("version")) != "76657273696f6e0000000000" { 124 | t.Fail() 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /vardiff/ringbuffer.go: -------------------------------------------------------------------------------- 1 | package vardiff 2 | 3 | type RingBuffer struct { 4 | IsFull bool 5 | MaxSize int64 6 | Cursor int64 7 | Data []int64 8 | } 9 | 10 | func NewRingBuffer(maxSize int64) *RingBuffer { 11 | return &RingBuffer{ 12 | IsFull: false, 13 | MaxSize: maxSize, 14 | Cursor: 0, 15 | Data: make([]int64, 0), 16 | } 17 | } 18 | 19 | func (rb *RingBuffer) Append(x int64) { 20 | if rb.IsFull { 21 | rb.Data[rb.Cursor] = x 22 | rb.Cursor = (rb.Cursor + 1) % rb.MaxSize 23 | } else { 24 | rb.Data = append(rb.Data, x) 25 | rb.Cursor++ 26 | if int64(len(rb.Data)) == rb.MaxSize { 27 | rb.Cursor = 0 28 | rb.IsFull = true 29 | } 30 | } 31 | } 32 | 33 | func (rb *RingBuffer) Avg() float64 { 34 | var sum int64 35 | for i := range rb.Data { 36 | sum = sum + rb.Data[i] 37 | } 38 | 39 | return float64(sum) / float64(rb.Size()) 40 | } 41 | 42 | func (rb *RingBuffer) Size() int64 { 43 | if rb.IsFull { 44 | return rb.MaxSize 45 | } else { 46 | return rb.Cursor 47 | } 48 | } 49 | 50 | func (rb *RingBuffer) Clear() { 51 | rb.Data = make([]int64, 0) 52 | rb.Cursor = 0 53 | rb.IsFull = false 54 | } 55 | -------------------------------------------------------------------------------- /vardiff/vardiff.go: -------------------------------------------------------------------------------- 1 | package vardiff 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/mining-pool/not-only-mining-pool/config" 7 | ) 8 | 9 | type VarDiff struct { 10 | Options *config.VarDiffOptions 11 | BufferSize int64 12 | MaxTargetTime float64 13 | MinTargetTime float64 14 | 15 | TimeBuffer *RingBuffer 16 | LastRtc int64 17 | LastTimestamp int64 18 | } 19 | 20 | func NewVarDiff(options *config.VarDiffOptions) *VarDiff { 21 | timestamp := time.Now().Unix() 22 | bufferSize := options.RetargetTime / options.TargetTime * 4 23 | return &VarDiff{ 24 | Options: options, 25 | BufferSize: bufferSize, 26 | MaxTargetTime: float64(options.TargetTime) * (1 + options.VariancePercent), 27 | MinTargetTime: float64(options.TargetTime) * (1 - options.VariancePercent), 28 | TimeBuffer: NewRingBuffer(bufferSize), 29 | LastRtc: timestamp - options.RetargetTime/2, 30 | LastTimestamp: timestamp, 31 | } 32 | } 33 | 34 | //func (vd *VarDiff) ManagePort(, ) { 35 | // stratumPort := client.Socket.LocalAddr().(*net.TCPAddr).Port 36 | //} 37 | 38 | // On SubmitEvent 39 | // then client.EnqueueNextDifficulty(newDiff) 40 | func (vd *VarDiff) CalcNextDiff(currentDiff float64) (newDiff float64) { 41 | timestamp := time.Now().Unix() 42 | 43 | if vd.LastRtc == 0 { 44 | vd.LastRtc = timestamp - vd.Options.RetargetTime/2 45 | vd.LastTimestamp = timestamp 46 | return 47 | } 48 | 49 | sinceLast := timestamp - vd.LastTimestamp 50 | 51 | vd.TimeBuffer.Append(sinceLast) 52 | vd.LastTimestamp = timestamp 53 | 54 | if (timestamp-vd.LastRtc) < vd.Options.RetargetTime && vd.TimeBuffer.Size() > 0 { 55 | return 56 | } 57 | 58 | vd.LastRtc = timestamp 59 | avg := vd.TimeBuffer.Avg() 60 | ddiff := float64(time.Duration(vd.Options.TargetTime)*time.Second) / avg 61 | 62 | // currentDiff, _ := client.Difficulty.Float64() 63 | 64 | if avg > vd.MaxTargetTime && currentDiff > vd.Options.MinDiff { 65 | if vd.Options.X2Mode { 66 | ddiff = 0.5 67 | } 68 | 69 | if ddiff*currentDiff < vd.Options.MinDiff { 70 | ddiff = vd.Options.MinDiff / currentDiff 71 | } 72 | } else if avg < vd.MinTargetTime { 73 | if vd.Options.X2Mode { 74 | ddiff = 2 75 | } 76 | 77 | diffMax := vd.Options.MaxDiff 78 | 79 | if ddiff*currentDiff > diffMax { 80 | ddiff = diffMax / currentDiff 81 | } 82 | } else { 83 | return currentDiff 84 | } 85 | 86 | newDiff = currentDiff * ddiff 87 | 88 | if newDiff <= 0 { 89 | newDiff = currentDiff 90 | } 91 | 92 | vd.TimeBuffer.Clear() 93 | return 94 | } 95 | -------------------------------------------------------------------------------- /vardiff/vardiff_test.go: -------------------------------------------------------------------------------- 1 | package vardiff 2 | 3 | import "testing" 4 | 5 | func TestNewVarDiff(t *testing.T) { 6 | // vd := NewVarDiff() 7 | } 8 | --------------------------------------------------------------------------------