Skip to content

Commit

Permalink
Merge pull request #148 from ethstorage/rewards
Browse files Browse the repository at this point in the history
Estimate mining profit
  • Loading branch information
syntrust authored Dec 14, 2023
2 parents 58a9f65 + 3197e36 commit c3bcdee
Show file tree
Hide file tree
Showing 8 changed files with 230 additions and 33 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ The full list of options that you can use to configure an es-node are as follows
|`--miner.threads-per-shard`|Number of threads per shard|`runtime.NumCPU() x 2`||
|`--miner.zk-working-dir`|Path to the snarkjs folder|`build/bin`||
|`--miner.zkey`|zkey file name which should be put in the snarkjs folder|`blob_poseidon.zkey`||
|`--miner.min-profit`|Minimum profit for mining transactions|`0`||
|`--network`|Predefined L1 network selection. Available networks: devnet|||
|`--p2p.advertise.ip`|The IP address to advertise in Discv5, put into the ENR of the node. This may also be a hostname / domain name to resolve to an IP.|||
|`--p2p.advertise.tcp`|The TCP port to advertise in Discv5, put into the ENR of the node. Set to p2p.listen.tcp value if 0.|`0`||
Expand Down
30 changes: 30 additions & 0 deletions cmd/es-node/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,37 @@ func NewMinerConfig(ctx *cli.Context, client *ethclient.Client, l1Contract commo
return nil, err
}
minerConfig.DiffAdjDivisor = diffAdjDivisor
dcf, err := readBigIntFromContract(cctx, client, l1Contract, "dcfFactor")
if err != nil {
return nil, err
}
minerConfig.DcfFactor = dcf

startTime, err := readUintFromContract(cctx, client, l1Contract, "startTime")
if err != nil {
return nil, err
}
minerConfig.StartTime = startTime
shardEntryBits, err := readUintFromContract(cctx, client, l1Contract, "shardEntryBits")
if err != nil {
return nil, err
}
minerConfig.ShardEntry = 1 << shardEntryBits
treasuryShare, err := readUintFromContract(cctx, client, l1Contract, "treasuryShare")
if err != nil {
return nil, err
}
minerConfig.TreasuryShare = treasuryShare
storageCost, err := readBigIntFromContract(cctx, client, l1Contract, "storageCost")
if err != nil {
return nil, err
}
minerConfig.StorageCost = storageCost
prepaidAmount, err := readBigIntFromContract(cctx, client, l1Contract, "prepaidAmount")
if err != nil {
return nil, err
}
minerConfig.PrepaidAmount = prepaidAmount
signerFnFactory, signerAddr, err := NewSignerConfig(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get signer: %w", err)
Expand Down
14 changes: 14 additions & 0 deletions ethstorage/eth/polling_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package eth
import (
"context"
"errors"
"fmt"
"math/big"
"regexp"
"sync"
Expand Down Expand Up @@ -270,3 +271,16 @@ func (w *PollingClient) GetKvMetas(kvIndices []uint64, blockNumber int64) ([][32

return res[0].([][32]byte), nil
}

func (w *PollingClient) ReadContractField(fieldName string) ([]byte, error) {
h := crypto.Keccak256Hash([]byte(fieldName + "()"))
msg := ethereum.CallMsg{
To: &w.esContract,
Data: h[0:4],
}
bs, err := w.Client.CallContract(context.Background(), msg, nil)
if err != nil {
return nil, fmt.Errorf("failed to get %s from contract: %v", fieldName, err)
}
return bs, nil
}
28 changes: 19 additions & 9 deletions ethstorage/miner/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ const (
EnabledFlagName = "miner.enabled"
GasPriceFlagName = "miner.gas-price"
PriorityGasPriceFlagName = "miner.priority-gas-price"
ZKeyFileName = "miner.zkey"
ZKWorkingDir = "miner.zk-working-dir"
ThreadsPerShard = "miner.threads-per-shard"
ZKeyFileNameFlagName = "miner.zkey"
ZKWorkingDirFlagName = "miner.zk-working-dir"
ThreadsPerShardFlagName = "miner.threads-per-shard"
MinimumProfitFlagName = "miner.min-profit"
)

func CLIFlags(envPrefix string) []cli.Flag {
Expand All @@ -43,20 +44,26 @@ func CLIFlags(envPrefix string) []cli.Flag {
Value: DefaultConfig.PriorityGasPrice,
EnvVar: rollup.PrefixEnvVar(envPrefix, "PRIORITY_GAS_PRICE"),
},
&types.BigFlag{
Name: MinimumProfitFlagName,
Usage: "Minimum profit for mining transactions",
Value: DefaultConfig.MinimumProfit,
EnvVar: rollup.PrefixEnvVar(envPrefix, "MIN_PROFIT"),
},
cli.StringFlag{
Name: ZKeyFileName,
Name: ZKeyFileNameFlagName,
Usage: "zkey file name which should be put in the snarkjs folder",
Value: DefaultConfig.ZKeyFileName,
EnvVar: rollup.PrefixEnvVar(envPrefix, "ZKEY_FILE"),
},
cli.StringFlag{
Name: ZKWorkingDir,
Name: ZKWorkingDirFlagName,
Usage: "Path to the snarkjs folder",
Value: DefaultConfig.ZKWorkingDir,
EnvVar: rollup.PrefixEnvVar(envPrefix, "ZK_WORKING_DIR"),
},
cli.Uint64Flag{
Name: ThreadsPerShard,
Name: ThreadsPerShardFlagName,
Usage: "Number of threads per shard",
Value: DefaultConfig.ThreadsPerShard,
EnvVar: rollup.PrefixEnvVar(envPrefix, "THREADS_PER_SHARD"),
Expand All @@ -69,6 +76,7 @@ type CLIConfig struct {
Enabled bool
GasPrice *big.Int
PriorityGasPrice *big.Int
MinimumProfit *big.Int
ZKeyFileName string
ZKWorkingDir string
ThreadsPerShard uint64
Expand Down Expand Up @@ -97,6 +105,7 @@ func (c CLIConfig) ToMinerConfig() (Config, error) {
cfg.ZKWorkingDir = zkWorkingDir
cfg.GasPrice = c.GasPrice
cfg.PriorityGasPrice = c.PriorityGasPrice
cfg.MinimumProfit = c.MinimumProfit
cfg.ZKeyFileName = c.ZKeyFileName
cfg.ThreadsPerShard = c.ThreadsPerShard
return cfg, nil
Expand All @@ -107,9 +116,10 @@ func ReadCLIConfig(ctx *cli.Context) CLIConfig {
Enabled: ctx.GlobalBool(EnabledFlagName),
GasPrice: types.GlobalBig(ctx, GasPriceFlagName),
PriorityGasPrice: types.GlobalBig(ctx, PriorityGasPriceFlagName),
ZKeyFileName: ctx.GlobalString(ZKeyFileName),
ZKWorkingDir: ctx.GlobalString(ZKWorkingDir),
ThreadsPerShard: ctx.GlobalUint64(ThreadsPerShard),
MinimumProfit: types.GlobalBig(ctx, MinimumProfitFlagName),
ZKeyFileName: ctx.GlobalString(ZKeyFileNameFlagName),
ZKWorkingDir: ctx.GlobalString(ZKWorkingDirFlagName),
ThreadsPerShard: ctx.GlobalUint64(ThreadsPerShardFlagName),
}
return cfg
}
10 changes: 10 additions & 0 deletions ethstorage/miner/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,28 @@ import (
)

type Config struct {
// contract
RandomChecks uint64
NonceLimit uint64
StartTime uint64
ShardEntry uint64
TreasuryShare uint64
MinimumDiff *big.Int
Cutoff *big.Int
DiffAdjDivisor *big.Int
StorageCost *big.Int
PrepaidAmount *big.Int
DcfFactor *big.Int

// cli
GasPrice *big.Int
PriorityGasPrice *big.Int
ZKeyFileName string
ZKWorkingDir string
ThreadsPerShard uint64
SignerFnFactory signer.SignerFactory
SignerAddr common.Address
MinimumProfit *big.Int
}

var DefaultConfig = Config{
Expand All @@ -40,4 +49,5 @@ var DefaultConfig = Config{
ZKeyFileName: "blob_poseidon.zkey",
ZKWorkingDir: filepath.Join("build", "bin"),
ThreadsPerShard: uint64(2 * runtime.NumCPU()),
MinimumProfit: common.Big0,
}
138 changes: 118 additions & 20 deletions ethstorage/miner/l1_mining_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ import (
"github.com/ethstorage/go-ethstorage/ethstorage/eth"
)

const gasBufferRatio = 1.2
const (
gasBufferRatio = 1.2
rewardDenominator = 10000
)

func NewL1MiningAPI(l1 *eth.PollingClient, lg log.Logger) *l1MiningAPI {
return &l1MiningAPI{l1, lg}
Expand Down Expand Up @@ -99,35 +102,25 @@ func (m *l1MiningAPI) SubmitMinedResult(ctx context.Context, contract common.Add
h := crypto.Keccak256Hash([]byte(`mine(uint256,uint256,address,uint256,bytes32[],bytes[])`))
calldata := append(h[0:4], dataField...)

chainID, err := m.NetworkID(ctx)
if err != nil {
m.lg.Error("Get chainID failed", "error", err.Error())
return common.Hash{}, err
}
sign := cfg.SignerFnFactory(chainID)
nonce, err := m.NonceAt(ctx, cfg.SignerAddr, big.NewInt(rpc.LatestBlockNumber.Int64()))
if err != nil {
m.lg.Error("Query nonce failed", "error", err.Error())
return common.Hash{}, err
}
m.lg.Debug("Query nonce done", "nonce", nonce)
gasPrice := cfg.GasPrice
if gasPrice == nil {
gasPrice, err = m.SuggestGasPrice(ctx)
if gasPrice == nil || gasPrice.Cmp(common.Big0) == 0 {
suggested, err := m.SuggestGasPrice(ctx)
if err != nil {
m.lg.Error("Query gas price failed", "error", err.Error())
return common.Hash{}, err
}
m.lg.Debug("Query gas price done", "gasPrice", gasPrice)
gasPrice = suggested
m.lg.Info("Query gas price done", "gasPrice", gasPrice)
}
tip := cfg.PriorityGasPrice
if tip == nil {
tip, err = m.SuggestGasTipCap(ctx)
if tip == nil || tip.Cmp(common.Big0) == 0 {
suggested, err := m.SuggestGasTipCap(ctx)
if err != nil {
m.lg.Error("Query gas tip cap failed", "error", err.Error())
tip = common.Big0
suggested = common.Big0
}
m.lg.Debug("Query gas tip cap done", "gasTipGap", tip)
tip = suggested
m.lg.Info("Query gas tip cap done", "gasTipGap", tip)
}
estimatedGas, err := m.EstimateGas(ctx, ethereum.CallMsg{
From: cfg.SignerAddr,
Expand All @@ -142,6 +135,34 @@ func (m *l1MiningAPI) SubmitMinedResult(ctx context.Context, contract common.Add
return common.Hash{}, fmt.Errorf("failed to estimate gas: %w", err)
}
m.lg.Info("Estimated gas done", "gas", estimatedGas)
cost := new(big.Int).Mul(new(big.Int).SetUint64(estimatedGas), gasPrice)
reward, err := m.estimateReward(ctx, cfg, contract, rst.startShardId, rst.blockNumber)
if err != nil {
m.lg.Error("Calculate reward failed", "error", err.Error())
return common.Hash{}, err
}
profit := new(big.Int).Sub(reward, cost)
m.lg.Info("Estimated reward and cost (in ether)", "reward", weiToEther(reward), "cost", weiToEther(cost), "profit", weiToEther(profit))
if profit.Cmp(cfg.MinimumProfit) == -1 {
m.lg.Warn("Will drop the tx: the profit will not meet expectation",
"profitEstimated", weiToEther(profit),
"minimumProfit", weiToEther(cfg.MinimumProfit),
)
return common.Hash{}, fmt.Errorf("dropped: not enough profit")
}

chainID, err := m.NetworkID(ctx)
if err != nil {
m.lg.Error("Get chainID failed", "error", err.Error())
return common.Hash{}, err
}
sign := cfg.SignerFnFactory(chainID)
nonce, err := m.NonceAt(ctx, cfg.SignerAddr, big.NewInt(rpc.LatestBlockNumber.Int64()))
if err != nil {
m.lg.Error("Query nonce failed", "error", err.Error())
return common.Hash{}, err
}
m.lg.Debug("Query nonce done", "nonce", nonce)
gas := uint64(float64(estimatedGas) * gasBufferRatio)
rawTx := &types.DynamicFeeTx{
ChainID: chainID,
Expand All @@ -167,3 +188,80 @@ func (m *l1MiningAPI) SubmitMinedResult(ctx context.Context, contract common.Add
"nonce", rst.nonce, "txSigner", cfg.SignerAddr.Hex(), "hash", signedTx.Hash().Hex())
return signedTx.Hash(), nil
}

// TODO: implement `miningReward()` in the contract to replace this impl
func (m *l1MiningAPI) estimateReward(ctx context.Context, cfg Config, contract common.Address, shard uint64, block *big.Int) (*big.Int, error) {

lastKv, err := m.PollingClient.GetStorageLastBlobIdx(rpc.LatestBlockNumber.Int64())
if err != nil {
m.lg.Error("Failed to get lastKvIdx", "error", err)
return nil, err
}
info, err := m.GetMiningInfo(ctx, contract, shard)
if err != nil {
m.lg.Error("Failed to get es mining info", "error", err.Error())
return nil, err
}
lastMineTime := info.LastMineTime

plmt, err := m.ReadContractField("prepaidLastMineTime")
if err != nil {
m.lg.Error("Failed to read prepaidLastMineTime", "error", err.Error())
return nil, err
}
prepaidLastMineTime := new(big.Int).SetBytes(plmt).Uint64()

var lastShard uint64
if lastKv > 0 {
lastShard = (lastKv - 1) / cfg.ShardEntry
}
curBlock, err := m.HeaderByNumber(ctx, big.NewInt(rpc.LatestBlockNumber.Int64()))
if err != nil {
m.lg.Error("Failed to get latest block", "error", err.Error())
return nil, err
}

minedTs := curBlock.Time - (new(big.Int).Sub(curBlock.Number, block).Uint64())*12
var reward *big.Int
if shard < lastShard {
basePayment := new(big.Int).Mul(cfg.StorageCost, new(big.Int).SetUint64(cfg.ShardEntry))
reward = paymentIn(basePayment, cfg.DcfFactor, lastMineTime, minedTs, cfg.StartTime)
} else if shard == lastShard {
basePayment := new(big.Int).Mul(cfg.StorageCost, new(big.Int).SetUint64(lastKv%cfg.ShardEntry))
reward = paymentIn(basePayment, cfg.DcfFactor, lastMineTime, minedTs, cfg.StartTime)
// Additional prepaid for the last shard
if prepaidLastMineTime < minedTs {
additionalReward := paymentIn(cfg.PrepaidAmount, cfg.DcfFactor, prepaidLastMineTime, minedTs, cfg.StartTime)
reward = new(big.Int).Add(reward, additionalReward)
}
}
minerReward := new(big.Int).Div(
new(big.Int).Mul(new(big.Int).SetUint64((rewardDenominator-cfg.TreasuryShare)), reward),
new(big.Int).SetUint64(rewardDenominator),
)
return minerReward, nil
}

func paymentIn(x, dcfFactor *big.Int, fromTs, toTs, startTime uint64) *big.Int {
return new(big.Int).Rsh(
new(big.Int).Mul(
x,
new(big.Int).Sub(
pow(dcfFactor, fromTs-startTime),
pow(dcfFactor, toTs-startTime),
)),
128,
)
}

func pow(fp *big.Int, n uint64) *big.Int {
v := new(big.Int).Lsh(big.NewInt(1), 128)
for n != 0 {
if (n & 1) == 1 {
v = new(big.Int).Rsh(new(big.Int).Mul(v, fp), 128)
}
fp = new(big.Int).Rsh(new(big.Int).Mul(fp, fp), 128)
n = n / 2
}
return v
}
Loading

0 comments on commit c3bcdee

Please sign in to comment.