diff --git a/README.md b/README.md index 133cef4cd..a0098e194 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ * Modern beautiful Ember.js frontend * Separate stats for workers: can highlight timed-out workers so miners can perform maintenance of rigs * JSON-API for stats +* PPLNS block reward #### Proxies diff --git a/config.example.json b/config.example.json index 1a264b8cc..f4a0687e1 100644 --- a/config.example.json +++ b/config.example.json @@ -2,6 +2,7 @@ "threads": 2, "coin": "eth", "name": "main", + "pplns": 9000, "proxy": { "enabled": true, diff --git a/main.go b/main.go index faff57e60..871d8a0e3 100644 --- a/main.go +++ b/main.go @@ -33,6 +33,7 @@ func startApi() { } func startBlockUnlocker() { + cfg.BlockUnlocker.Pplns = cfg.Pplns u := payouts.NewBlockUnlocker(&cfg.BlockUnlocker, backend) u.Start() } @@ -82,7 +83,7 @@ func main() { startNewrelic() - backend = storage.NewRedisClient(&cfg.Redis, cfg.Coin) + backend = storage.NewRedisClient(&cfg.Redis, cfg.Coin, cfg.Pplns) pong, err := backend.Check() if err != nil { log.Printf("Can't establish connection to backend: %v", err) diff --git a/payouts/unlocker.go b/payouts/unlocker.go index c073ef0b3..e2a7f5f84 100644 --- a/payouts/unlocker.go +++ b/payouts/unlocker.go @@ -26,6 +26,7 @@ type UnlockerConfig struct { Interval string `json:"interval"` Daemon string `json:"daemon"` Timeout string `json:"timeout"` + Pplns int64 } const minDepth = 16 @@ -448,12 +449,28 @@ func (u *BlockUnlocker) calculateRewards(block *storage.BlockData) (*big.Rat, *b revenue := new(big.Rat).SetInt(block.Reward) minersProfit, poolProfit := chargeFee(revenue, u.config.PoolFee) - shares, err := u.backend.GetRoundShares(block.RoundHeight, block.Nonce) + var shares map[string]int64 + var err error + if u.config.Pplns > 0 { + shares, err = u.backend.GetNShares(block.RoundHeight, block.Nonce) + } else { + shares, err = u.backend.GetRoundShares(block.RoundHeight, block.Nonce) + } if err != nil { return nil, nil, nil, nil, err } - rewards := calculateRewardsForShares(shares, block.TotalShares, minersProfit) + var rewards map[string]int64 + if u.config.Pplns > 0 { + totalShares := int64(0) + for _, val := range shares { + totalShares += val + } + + rewards = calculateRewardsForShares(shares, totalShares, minersProfit) + } else { + rewards = calculateRewardsForShares(shares, block.TotalShares, minersProfit) + } if block.ExtraReward != nil { extraReward := new(big.Rat).SetInt(block.ExtraReward) diff --git a/proxy/config.go b/proxy/config.go index 6248c538c..457cdd412 100644 --- a/proxy/config.go +++ b/proxy/config.go @@ -17,6 +17,7 @@ type Config struct { Threads int `json:"threads"` Coin string `json:"coin"` + Pplns int64 `json:"pplns"` Redis storage.Config `json:"redis"` BlockUnlocker payouts.UnlockerConfig `json:"unlocker"` diff --git a/storage/redis.go b/storage/redis.go index 449b58fcc..61d0d5252 100644 --- a/storage/redis.go +++ b/storage/redis.go @@ -22,6 +22,7 @@ type Config struct { type RedisClient struct { client *redis.Client prefix string + pplns int64 } type BlockData struct { @@ -78,14 +79,14 @@ type Worker struct { TotalHR int64 `json:"hr2"` } -func NewRedisClient(cfg *Config, prefix string) *RedisClient { +func NewRedisClient(cfg *Config, prefix string, pplns int64) *RedisClient { client := redis.NewClient(&redis.Options{ Addr: cfg.Endpoint, Password: cfg.Password, DB: cfg.Database, PoolSize: cfg.PoolSize, }) - return &RedisClient{client: client, prefix: prefix} + return &RedisClient{client: client, prefix: prefix, pplns: pplns} } func (r *RedisClient) Client() *redis.Client { @@ -212,12 +213,33 @@ func (r *RedisClient) WriteBlock(login, id string, params []string, diff, roundD tx.HIncrBy(r.formatKey("miners", login), "blocksFound", 1) tx.Rename(r.formatKey("shares", "roundCurrent"), r.formatRound(int64(height), params[0])) tx.HGetAllMap(r.formatRound(int64(height), params[0])) + tx.LRange(r.formatKey("lastshares"), 0, r.pplns) return nil }) if err != nil { return false, err } else { - sharesMap, _ := cmds[10].(*redis.StringStringMapCmd).Result() + lastshares := cmds[len(cmds)-1].(*redis.StringSliceCmd).Val() + + totalnshares := make(map[string]int64) + for _, val := range lastshares { + totalnshares[val] += 1 + } + + ntx := r.client.Multi() + defer ntx.Close() + + _, err := ntx.Exec(func() error { + for k, v := range totalnshares { + ntx.HIncrBy(r.formatNShare(int64(height), params[0]), k, v) + } + return nil + }) + if err != nil { + return false, err + } + + sharesMap, _ := cmds[len(cmds)-2].(*redis.StringStringMapCmd).Result() totalShares := int64(0) for _, v := range sharesMap { n, _ := strconv.ParseInt(v, 10, 64) @@ -231,6 +253,9 @@ func (r *RedisClient) WriteBlock(login, id string, params []string, diff, roundD } func (r *RedisClient) writeShare(tx *redis.Multi, ms, ts int64, login, id string, diff int64, expire time.Duration) { + tx.LPush(r.formatKey("lastshares"), login) + tx.LTrim(r.formatKey("lastshares"), 0, r.pplns) + tx.HIncrBy(r.formatKey("shares", "roundCurrent"), login, diff) tx.ZAdd(r.formatKey("hashrate"), redis.Z{Score: float64(ts), Member: join(diff, login, id, ms)}) tx.ZAdd(r.formatKey("hashrate", login), redis.Z{Score: float64(ts), Member: join(diff, id, ms)}) @@ -246,6 +271,10 @@ func (r *RedisClient) formatRound(height int64, nonce string) string { return r.formatKey("shares", "round"+strconv.FormatInt(height, 10), nonce) } +func (r *RedisClient) formatNShare(height int64, nonce string) string { + return r.formatKey("nshares", "round"+strconv.FormatInt(height, 10), nonce) +} + func join(args ...interface{}) string { s := make([]string, len(args)) for i, v := range args { @@ -271,6 +300,13 @@ func join(args ...interface{}) string { } else { s[i] = "0" } + case *big.Rat: + x := v.(*big.Rat) + if x != nil { + s[i] = x.FloatString(9) + } else { + s[i] = "0" + } default: panic("Invalid type specified for conversion") } @@ -310,6 +346,20 @@ func (r *RedisClient) GetRoundShares(height int64, nonce string) (map[string]int return result, nil } +func (r *RedisClient) GetNShares(height int64, nonce string) (map[string]int64, error) { + result := make(map[string]int64) + cmd := r.client.HGetAllMap(r.formatNShare(height, nonce)) + if cmd.Err() != nil { + return nil, cmd.Err() + } + sharesMap, _ := cmd.Result() + for login, v := range sharesMap { + n, _ := strconv.ParseInt(v, 10, 64) + result[login] = n + } + return result, nil +} + func (r *RedisClient) GetPayees() ([]string, error) { payees := make(map[string]struct{}) var result []string @@ -552,6 +602,7 @@ func (r *RedisClient) writeImmatureBlock(tx *redis.Multi, block *BlockData) { // Redis 2.8.x returns "ERR source and destination objects are the same" if block.Height != block.RoundHeight { tx.Rename(r.formatRound(block.RoundHeight, block.Nonce), r.formatRound(block.Height, block.Nonce)) + tx.Rename(r.formatNShare(block.RoundHeight, block.Nonce), r.formatNShare(block.Height, block.Nonce)) } tx.ZRem(r.formatKey("blocks", "candidates"), block.candidateKey) tx.ZAdd(r.formatKey("blocks", "immature"), redis.Z{Score: float64(block.Height), Member: block.key()}) @@ -559,6 +610,7 @@ func (r *RedisClient) writeImmatureBlock(tx *redis.Multi, block *BlockData) { func (r *RedisClient) writeMaturedBlock(tx *redis.Multi, block *BlockData) { tx.Del(r.formatRound(block.RoundHeight, block.Nonce)) + tx.Del(r.formatNShare(block.RoundHeight, block.Nonce)) tx.ZRem(r.formatKey("blocks", "immature"), block.immatureKey) tx.ZAdd(r.formatKey("blocks", "matured"), redis.Z{Score: float64(block.Height), Member: block.key()}) } @@ -578,6 +630,7 @@ func (r *RedisClient) GetMinerStats(login string, maxPayments int64) (map[string tx.ZRevRangeWithScores(r.formatKey("payments", login), 0, maxPayments-1) tx.ZCard(r.formatKey("payments", login)) tx.HGet(r.formatKey("shares", "roundCurrent"), login) + tx.LRange(r.formatKey("lastshares"), 0, r.pplns) return nil }) @@ -591,6 +644,15 @@ func (r *RedisClient) GetMinerStats(login string, maxPayments int64) (map[string stats["paymentsTotal"] = cmds[2].(*redis.IntCmd).Val() roundShares, _ := cmds[3].(*redis.StringCmd).Int64() stats["roundShares"] = roundShares + + lastnshares := cmds[4].(*redis.StringSliceCmd).Val() + nsh := 0 + for _, val := range lastnshares { + if val == login { + nsh++ + } + } + stats["lastNShares"] = nsh } return stats, nil @@ -668,6 +730,7 @@ func (r *RedisClient) CollectStats(smallWindow time.Duration, maxBlocks, maxPaym tx.ZCard(r.formatKey("blocks", "matured")) tx.ZCard(r.formatKey("payments", "all")) tx.ZRevRangeWithScores(r.formatKey("payments", "all"), 0, maxPayments-1) + tx.LLen(r.formatKey("lastshares")) return nil }) @@ -676,6 +739,7 @@ func (r *RedisClient) CollectStats(smallWindow time.Duration, maxBlocks, maxPaym } result, _ := cmds[2].(*redis.StringStringMapCmd).Result() + result["lastNShares"] = strconv.FormatInt(cmds[11].(*redis.IntCmd).Val(), 10) stats["stats"] = convertStringMap(result) candidates := convertCandidateResults(cmds[3].(*redis.ZSliceCmd)) stats["candidates"] = candidates diff --git a/storage/redis_test.go b/storage/redis_test.go index 29c60d9f7..666722e84 100644 --- a/storage/redis_test.go +++ b/storage/redis_test.go @@ -14,7 +14,7 @@ var r *RedisClient const prefix = "test" func TestMain(m *testing.M) { - r = NewRedisClient(&Config{Endpoint: "127.0.0.1:6379"}, prefix) + r = NewRedisClient(&Config{Endpoint: "127.0.0.1:6379"}, prefix, 3000) reset() c := m.Run() reset() diff --git a/www/app/controllers/account.js b/www/app/controllers/account.js index 79782f798..fa822524c 100644 --- a/www/app/controllers/account.js +++ b/www/app/controllers/account.js @@ -6,7 +6,7 @@ export default Ember.Controller.extend({ roundPercent: Ember.computed('stats', 'model', { get() { - var percent = this.get('model.roundShares') / this.get('stats.roundShares'); + var percent = this.get('model.lastNShares') / this.get('stats.lastNShares'); if (!percent) { return 0; }