package payouts import ( "fmt" "log" "math/big" "os" "strconv" "strings" "time" "github.com/ethereum/go-ethereum/common/math" "github.com/yuriy0803/open-etc-pool-friends/rpc" "github.com/yuriy0803/open-etc-pool-friends/storage" "github.com/yuriy0803/open-etc-pool-friends/util" ) type UnlockerConfig struct { Enabled bool `json:"enabled"` PoolFee float64 `json:"poolFee"` PoolFeeAddress string `json:"poolFeeAddress"` Donate bool `json:"donate"` Depth int64 `json:"depth"` ImmatureDepth int64 `json:"immatureDepth"` KeepTxFees bool `json:"keepTxFees"` Interval string `json:"interval"` Daemon string `json:"daemon"` Timeout string `json:"timeout"` Classic bool `json:"classic"` } const minDepth = 16 const byzantiumHardForkHeight = 4370000 var homesteadReward = math.MustParseBig256("5000000000000000000") var byzantiumReward = math.MustParseBig256("3200000000000000000") var classicReward = math.MustParseBig256("3200000000000000000") // Donate 10% from pool fees to developers const donationFee = 10 const donationAccount = "0xd92fa5a9732a0aec36dc8d5a6a1305dc2d3e09e6" type BlockUnlocker struct { config *UnlockerConfig backend *storage.RedisClient rpc *rpc.RPCClient halt bool lastFail error } func NewBlockUnlocker(cfg *UnlockerConfig, backend *storage.RedisClient) *BlockUnlocker { if len(cfg.PoolFeeAddress) != 0 && !util.IsValidHexAddress(cfg.PoolFeeAddress) { log.Fatalln("Invalid poolFeeAddress", cfg.PoolFeeAddress) } if cfg.Depth < minDepth*2 { log.Fatalf("Block maturity depth can't be < %v, your depth is %v", minDepth*2, cfg.Depth) } if cfg.ImmatureDepth < minDepth { log.Fatalf("Immature depth can't be < %v, your depth is %v", minDepth, cfg.ImmatureDepth) } u := &BlockUnlocker{config: cfg, backend: backend} u.rpc = rpc.NewRPCClient("BlockUnlocker", cfg.Daemon, cfg.Timeout) return u } func (u *BlockUnlocker) Start() { log.Println("Starting block unlocker") intv := util.MustParseDuration(u.config.Interval) timer := time.NewTimer(intv) log.Printf("Set block unlock interval to %v", intv) // Immediately unlock after start u.unlockPendingBlocks() u.unlockAndCreditMiners() timer.Reset(intv) go func() { for { select { case <-timer.C: u.unlockPendingBlocks() u.unlockAndCreditMiners() timer.Reset(intv) } } }() } type UnlockResult struct { maturedBlocks []*storage.BlockData orphanedBlocks []*storage.BlockData orphans int uncles int blocks int } /* Geth does not provide consistent state when you need both new height and new job, * so in redis I am logging just what I have in a pool state on the moment when block found. * Having very likely incorrect height in database results in a weird block unlocking scheme, * when I have to check what the hell we actually found and traversing all the blocks with height-N and height+N * to make sure we will find it. We can't rely on round height here, it's just a reference point. * ISSUE: https://github.com/ethereum/go-ethereum/issues/2333 */ func (u *BlockUnlocker) unlockCandidates(candidates []*storage.BlockData) (*UnlockResult, error) { result := &UnlockResult{} // Data row is: "height:nonce:powHash:mixDigest:timestamp:diff:totalShares" for _, candidate := range candidates { orphan := true /* Search for a normal block with wrong height here by traversing 16 blocks back and forward. * Also we are searching for a block that can include this one as uncle. */ for i := int64(minDepth * -1); i < minDepth; i++ { height := candidate.Height + i if height < 0 { continue } block, err := u.rpc.GetBlockByHeight(height) if err != nil { log.Printf("Error while retrieving block %v from node: %v", height, err) return nil, err } if block == nil { return nil, fmt.Errorf("Error while retrieving block %v from node, wrong node height", height) } if matchCandidate(block, candidate) { orphan = false result.blocks++ err = u.handleBlock(block, candidate) if err != nil { u.halt = true u.lastFail = err return nil, err } result.maturedBlocks = append(result.maturedBlocks, candidate) log.Printf("Mature block %v with %v tx, hash: %v", candidate.Height, len(block.Transactions), candidate.Hash[0:10]) break } if len(block.Uncles) == 0 { continue } // Trying to find uncle in current block during our forward check for uncleIndex, uncleHash := range block.Uncles { uncle, err := u.rpc.GetUncleByBlockNumberAndIndex(height, uncleIndex) if err != nil { return nil, fmt.Errorf("Error while retrieving uncle of block %v from node: %v", uncleHash, err) } if uncle == nil { return nil, fmt.Errorf("Error while retrieving uncle of block %v from node", height) } // Found uncle if matchCandidate(uncle, candidate) { orphan = false result.uncles++ err := handleUncle(height, uncle, candidate, u.config.Classic) if err != nil { u.halt = true u.lastFail = err return nil, err } result.maturedBlocks = append(result.maturedBlocks, candidate) log.Printf("Mature uncle %v/%v of reward %v with hash: %v", candidate.Height, candidate.UncleHeight, util.FormatReward(candidate.Reward), uncle.Hash[0:10]) break } } // Found block or uncle if !orphan { break } } // Block is lost, we didn't find any valid block or uncle matching our data in a blockchain if orphan { result.orphans++ candidate.Orphan = true result.orphanedBlocks = append(result.orphanedBlocks, candidate) log.Printf("Orphaned block %v:%v", candidate.RoundHeight, candidate.Nonce) } } return result, nil } func matchCandidate(block *rpc.GetBlockReply, candidate *storage.BlockData) bool { // Just compare hash if block is unlocked as immature if len(candidate.Hash) > 0 && strings.EqualFold(candidate.Hash, block.Hash) { return true } // Geth-style candidate matching if len(block.Nonce) > 0 { return strings.EqualFold(block.Nonce, candidate.Nonce) } // Parity's EIP: https://github.com/ethereum/EIPs/issues/95 if len(block.SealFields) == 2 { return strings.EqualFold(candidate.Nonce, block.SealFields[1]) } return false } func (u *BlockUnlocker) handleBlock(block *rpc.GetBlockReply, candidate *storage.BlockData) error { correctHeight, err := strconv.ParseInt(strings.Replace(block.Number, "0x", "", -1), 16, 64) if err != nil { return err } candidate.Height = correctHeight reward := getConstReward(candidate.Height, u.config.Classic) // Add TX fees extraTxReward, err := u.getExtraRewardForTx(block) if err != nil { return fmt.Errorf("Error while fetching TX receipt: %v", err) } if u.config.KeepTxFees { candidate.ExtraReward = extraTxReward } else { reward.Add(reward, extraTxReward) } // Add reward for including uncles uncleReward := getRewardForUncle(candidate.Height, u.config.Classic) rewardForUncles := big.NewInt(0).Mul(uncleReward, big.NewInt(int64(len(block.Uncles)))) reward.Add(reward, rewardForUncles) candidate.Orphan = false candidate.Hash = block.Hash candidate.Reward = reward return nil } func handleUncle(height int64, uncle *rpc.GetBlockReply, candidate *storage.BlockData, isClassic bool) error { uncleHeight, err := strconv.ParseInt(strings.Replace(uncle.Number, "0x", "", -1), 16, 64) if err != nil { return err } reward := getUncleReward(uncleHeight, height, isClassic) candidate.Height = height candidate.UncleHeight = uncleHeight candidate.Orphan = false candidate.Hash = uncle.Hash candidate.Reward = reward return nil } func (u *BlockUnlocker) unlockPendingBlocks() { if u.halt { log.Println("Unlocking suspended due to last critical error:", u.lastFail) os.Exit(1) return } current, err := u.rpc.GetPendingBlock() if err != nil { u.halt = true u.lastFail = err log.Printf("Unable to get current blockchain height from node: %v", err) return } currentHeight, err := strconv.ParseInt(strings.Replace(current.Number, "0x", "", -1), 16, 64) if err != nil { u.halt = true u.lastFail = err log.Printf("Can't parse pending block number: %v", err) return } candidates, err := u.backend.GetCandidates(currentHeight - u.config.ImmatureDepth) if err != nil { u.halt = true u.lastFail = err log.Printf("Failed to get block candidates from backend: %v", err) return } if len(candidates) == 0 { log.Println("No block candidates to unlock") return } result, err := u.unlockCandidates(candidates) if err != nil { u.halt = true u.lastFail = err log.Printf("Failed to unlock blocks: %v", err) return } log.Printf("Immature %v blocks, %v uncles, %v orphans", result.blocks, result.uncles, result.orphans) err = u.backend.WritePendingOrphans(result.orphanedBlocks) if err != nil { u.halt = true u.lastFail = err log.Printf("Failed to insert orphaned blocks into backend: %v", err) return } else { log.Printf("Inserted %v orphaned blocks to backend", result.orphans) } totalRevenue := new(big.Rat) totalMinersProfit := new(big.Rat) totalPoolProfit := new(big.Rat) for _, block := range result.maturedBlocks { revenue, minersProfit, poolProfit, roundRewards, percents, err := u.calculateRewards(block) if err != nil { u.halt = true u.lastFail = err log.Printf("Failed to calculate rewards for round %v: %v", block.RoundKey(), err) return } err = u.backend.WriteImmatureBlock(block, roundRewards) if err != nil { u.halt = true u.lastFail = err log.Printf("Failed to credit rewards for round %v: %v", block.RoundKey(), err) return } totalRevenue.Add(totalRevenue, revenue) totalMinersProfit.Add(totalMinersProfit, minersProfit) totalPoolProfit.Add(totalPoolProfit, poolProfit) logEntry := fmt.Sprintf( "IMMATURE %v: revenue %v, miners profit %v, pool profit: %v", block.RoundKey(), util.FormatRatReward(revenue), util.FormatRatReward(minersProfit), util.FormatRatReward(poolProfit), ) entries := []string{logEntry} for login, reward := range roundRewards { entries = append(entries, fmt.Sprintf("\tREWARD %v: %v: %v Shannon", block.RoundKey(), login, reward)) per := new(big.Rat) if val, ok := percents[login]; ok { per = val } u.backend.WriteReward(login, reward, per, true, block) } log.Println(strings.Join(entries, "\n")) } log.Printf( "IMMATURE SESSION: revenue %v, miners profit %v, pool profit: %v", util.FormatRatReward(totalRevenue), util.FormatRatReward(totalMinersProfit), util.FormatRatReward(totalPoolProfit), ) } func (u *BlockUnlocker) unlockAndCreditMiners() { if u.halt { log.Println("Unlocking suspended due to last critical error:", u.lastFail) return } current, err := u.rpc.GetPendingBlock() if err != nil { u.halt = true u.lastFail = err log.Printf("Unable to get current blockchain height from node: %v", err) return } currentHeight, err := strconv.ParseInt(strings.Replace(current.Number, "0x", "", -1), 16, 64) if err != nil { u.halt = true u.lastFail = err log.Printf("Can't parse pending block number: %v", err) return } immature, err := u.backend.GetImmatureBlocks(currentHeight - u.config.Depth) if err != nil { u.halt = true u.lastFail = err log.Printf("Failed to get block candidates from backend: %v", err) return } if len(immature) == 0 { log.Println("No immature blocks to credit miners") return } result, err := u.unlockCandidates(immature) if err != nil { u.halt = true u.lastFail = err log.Printf("Failed to unlock blocks: %v", err) return } log.Printf("Unlocked %v blocks, %v uncles, %v orphans", result.blocks, result.uncles, result.orphans) for _, block := range result.orphanedBlocks { err = u.backend.WriteOrphan(block) if err != nil { u.halt = true u.lastFail = err log.Printf("Failed to insert orphaned block into backend: %v", err) return } } log.Printf("Inserted %v orphaned blocks to backend", result.orphans) totalRevenue := new(big.Rat) totalMinersProfit := new(big.Rat) totalPoolProfit := new(big.Rat) for _, block := range result.maturedBlocks { revenue, minersProfit, poolProfit, roundRewards, percents, err := u.calculateRewards(block) if err != nil { u.halt = true u.lastFail = err log.Printf("Failed to calculate rewards for round %v: %v", block.RoundKey(), err) return } err = u.backend.WriteMaturedBlock(block, roundRewards) if err != nil { u.halt = true u.lastFail = err log.Printf("Failed to credit rewards for round %v: %v", block.RoundKey(), err) return } totalRevenue.Add(totalRevenue, revenue) totalMinersProfit.Add(totalMinersProfit, minersProfit) totalPoolProfit.Add(totalPoolProfit, poolProfit) logEntry := fmt.Sprintf( "MATURED %v: revenue %v, miners profit %v, pool profit: %v", block.RoundKey(), util.FormatRatReward(revenue), util.FormatRatReward(minersProfit), util.FormatRatReward(poolProfit), ) entries := []string{logEntry} for login, reward := range roundRewards { entries = append(entries, fmt.Sprintf("\tREWARD %v: %v: %v Shannon", block.RoundKey(), login, reward)) per := new(big.Rat) if val, ok := percents[login]; ok { per = val } u.backend.WriteReward(login, reward, per, false, block) } log.Println(strings.Join(entries, "\n")) } log.Printf( "MATURE SESSION: revenue %v, miners profit %v, pool profit: %v", util.FormatRatReward(totalRevenue), util.FormatRatReward(totalMinersProfit), util.FormatRatReward(totalPoolProfit), ) } func (u *BlockUnlocker) calculateRewards(block *storage.BlockData) (*big.Rat, *big.Rat, *big.Rat, map[string]int64, map[string]*big.Rat, error) { revenue := new(big.Rat).SetInt(block.Reward) minersProfit, poolProfit := chargeFee(revenue, u.config.PoolFee) shares, err := u.backend.GetRoundShares(block.RoundHeight, block.Nonce) if err != nil { return nil, nil, nil, nil, nil, err } totalShares := int64(0) for _, val := range shares { totalShares += val } rewards, percents := calculateRewardsForShares(shares, totalShares, minersProfit) if block.ExtraReward != nil { extraReward := new(big.Rat).SetInt(block.ExtraReward) poolProfit.Add(poolProfit, extraReward) revenue.Add(revenue, extraReward) } if u.config.Donate { var donation = new(big.Rat) poolProfit, donation = chargeFee(poolProfit, donationFee) login := strings.ToLower(donationAccount) rewards[login] += weiToShannonInt64(donation) } if len(u.config.PoolFeeAddress) != 0 { address := strings.ToLower(u.config.PoolFeeAddress) rewards[address] += weiToShannonInt64(poolProfit) } return revenue, minersProfit, poolProfit, rewards, percents, nil } func calculateRewardsForShares(shares map[string]int64, total int64, reward *big.Rat) (map[string]int64, map[string]*big.Rat) { rewards := make(map[string]int64) percents := make(map[string]*big.Rat) for login, n := range shares { percents[login] = big.NewRat(n, total) workerReward := new(big.Rat).Mul(reward, percents[login]) rewards[login] += weiToShannonInt64(workerReward) } return rewards, percents } // Returns new value after fee deduction and fee value. func chargeFee(value *big.Rat, fee float64) (*big.Rat, *big.Rat) { feePercent := new(big.Rat).SetFloat64(fee / 100) feeValue := new(big.Rat).Mul(value, feePercent) return new(big.Rat).Sub(value, feeValue), feeValue } func weiToShannonInt64(wei *big.Rat) int64 { shannon := new(big.Rat).SetInt(util.Shannon) inShannon := new(big.Rat).Quo(wei, shannon) value, _ := strconv.ParseInt(inShannon.FloatString(0), 10, 64) return value } func getConstReward(height int64, isClassic bool) *big.Int { if !isClassic { if height >= byzantiumHardForkHeight { return new(big.Int).Set(byzantiumReward) } return new(big.Int).Set(homesteadReward) } else { return new(big.Int).Set(classicReward) } } func getRewardForUncle(height int64, isClassic bool) *big.Int { reward := getConstReward(height, isClassic) return new(big.Int).Div(reward, new(big.Int).SetInt64(32)) } func getUncleReward(uHeight, height int64, isClassic bool) *big.Int { if !isClassic { reward := getConstReward(height, isClassic) k := height - uHeight reward.Mul(big.NewInt(8-k), reward) reward.Div(reward, big.NewInt(8)) return reward } else { reward := getConstReward(height, isClassic) reward.Mul(reward, big.NewInt(3125)) reward.Div(reward, big.NewInt(100000)) return reward } } func (u *BlockUnlocker) getExtraRewardForTx(block *rpc.GetBlockReply) (*big.Int, error) { amount := new(big.Int) for _, tx := range block.Transactions { receipt, err := u.rpc.GetTxReceipt(tx.Hash) if err != nil { return nil, err } if receipt != nil { gasUsed := util.String2Big(receipt.GasUsed) gasPrice := util.String2Big(tx.GasPrice) fee := new(big.Int).Mul(gasUsed, gasPrice) amount.Add(amount, fee) } } return amount, nil }