diff --git a/README.md b/README.md index c9b33fb4..55cc50c7 100644 --- a/README.md +++ b/README.md @@ -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`|| diff --git a/cmd/es-node/config.go b/cmd/es-node/config.go index 8d925d13..11a6080f 100644 --- a/cmd/es-node/config.go +++ b/cmd/es-node/config.go @@ -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) diff --git a/ethstorage/eth/polling_client.go b/ethstorage/eth/polling_client.go index 10bc2eb5..08f3ed11 100644 --- a/ethstorage/eth/polling_client.go +++ b/ethstorage/eth/polling_client.go @@ -3,6 +3,7 @@ package eth import ( "context" "errors" + "fmt" "math/big" "regexp" "sync" @@ -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 +} diff --git a/ethstorage/miner/cli.go b/ethstorage/miner/cli.go index 2f7fcc70..13a335ae 100644 --- a/ethstorage/miner/cli.go +++ b/ethstorage/miner/cli.go @@ -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 { @@ -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"), @@ -69,6 +76,7 @@ type CLIConfig struct { Enabled bool GasPrice *big.Int PriorityGasPrice *big.Int + MinimumProfit *big.Int ZKeyFileName string ZKWorkingDir string ThreadsPerShard uint64 @@ -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 @@ -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 } diff --git a/ethstorage/miner/config.go b/ethstorage/miner/config.go index de137adb..97a34438 100644 --- a/ethstorage/miner/config.go +++ b/ethstorage/miner/config.go @@ -13,12 +13,20 @@ 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 @@ -26,6 +34,7 @@ type Config struct { ThreadsPerShard uint64 SignerFnFactory signer.SignerFactory SignerAddr common.Address + MinimumProfit *big.Int } var DefaultConfig = Config{ @@ -40,4 +49,5 @@ var DefaultConfig = Config{ ZKeyFileName: "blob_poseidon.zkey", ZKWorkingDir: filepath.Join("build", "bin"), ThreadsPerShard: uint64(2 * runtime.NumCPU()), + MinimumProfit: common.Big0, } diff --git a/ethstorage/miner/l1_mining_api.go b/ethstorage/miner/l1_mining_api.go index 1c38dd78..12c74f21 100644 --- a/ethstorage/miner/l1_mining_api.go +++ b/ethstorage/miner/l1_mining_api.go @@ -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} @@ -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, @@ -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, @@ -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 +} diff --git a/ethstorage/miner/worker.go b/ethstorage/miner/worker.go index 371e617c..89ceadc7 100644 --- a/ethstorage/miner/worker.go +++ b/ethstorage/miner/worker.go @@ -12,8 +12,9 @@ import ( "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" - "github.com/ethstorage/go-ethstorage/ethstorage" + "github.com/ethereum/go-ethereum/params" es "github.com/ethstorage/go-ethstorage/ethstorage" "github.com/ethstorage/go-ethstorage/ethstorage/eth" ) @@ -28,6 +29,10 @@ const ( miningTransactionTimeout = 25 // seconds ) +var ( + minedEventSig = crypto.Keccak256Hash([]byte("MinedBlock(uint256,uint256,uint256,uint256,address,uint256)")) +) + type task struct { miner common.Address shardIdx uint64 @@ -83,7 +88,7 @@ type worker struct { func newWorker( config Config, - storageMgr *ethstorage.StorageManager, + storageMgr *es.StorageManager, api L1API, chainHeadCh chan eth.L1BlockRef, prover MiningProver, @@ -344,11 +349,42 @@ func (w *worker) checkTxStatus(txHash common.Hash, miner common.Address) { log.Warn("Mining transaction not found!", "err", err, "txHash", txHash) } else if receipt.Status == 1 { log.Info("Mining transaction success! √", "miner", miner) + log.Info("Mining transaction details", "txHash", txHash, "gasUsed", receipt.GasUsed, "effectiveGasPrice", receipt.EffectiveGasPrice) + cost := new(big.Int).Mul(new(big.Int).SetUint64(receipt.GasUsed), receipt.EffectiveGasPrice) + var reward *big.Int + for _, rLog := range receipt.Logs { + if rLog.Topics[0] == minedEventSig { + // the last param of total unindexed 3 + reward = new(big.Int).SetBytes(rLog.Data[64:]) + break + } + } + if reward != nil { + log.Info("Mining transaction accounting (in ether)", + "reward", weiToEther(reward), + "cost", weiToEther(cost), + "profit", weiToEther(new(big.Int).Sub(reward, cost)), + ) + } } else if receipt.Status == 0 { log.Warn("Mining transaction failed! ×", "txHash", txHash) } } +// https://github.com/ethereum/go-ethereum/issues/21221#issuecomment-805852059 +func weiToEther(wei *big.Int) *big.Float { + f := new(big.Float) + f.SetPrec(236) // IEEE 754 octuple-precision binary floating-point format: binary256 + f.SetMode(big.ToNearestEven) + if wei == nil { + return f.SetInt64(0) + } + fWei := new(big.Float) + fWei.SetPrec(236) // IEEE 754 octuple-precision binary floating-point format: binary256 + fWei.SetMode(big.ToNearestEven) + return f.Quo(fWei.SetInt(wei), big.NewFloat(params.Ether)) +} + // mineTask acturally executes a mining task func (w *worker) mineTask(t *taskItem) (bool, error) { startTime := time.Now() diff --git a/run.sh b/run.sh index aef51565..fc8fd62a 100755 --- a/run.sh +++ b/run.sh @@ -50,8 +50,6 @@ es_node_init="init --shard_index 0" # TODO remove --miner.priority-gas-price and --miner.gas-price when gas price query is available es_node_start=" --network devnet \ --miner.enabled \ - --miner.priority-gas-price 5000000000 \ - --miner.gas-price 30000000000 \ --storage.files $storage_file_0 \ --signer.private-key $ES_NODE_SIGNER_PRIVATE_KEY \ --l1.beacon http://65.109.115.36:5052 \