diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c34e80ddd..cfde85c776 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * (rpc) [tharsis#272](https://github.com/tharsis/ethermint/pull/272) do binary search to estimate gas accurately * (rpc) [#313](https://github.com/tharsis/ethermint/pull/313) Implement internal debug namespace (Not including logger functions nor traces). * (rpc) [#349](https://github.com/tharsis/ethermint/pull/349) Implement configurable JSON-RPC APIs to manage enabled namespaces. +* (rpc) [#377](https://github.com/tharsis/ethermint/pull/377) Implement `miner_` namespace. `miner_setEtherbase` and `miner_setGasPrice` are working as intended. All the other calls are not applicable and return `unsupported`. ### Bug Fixes diff --git a/docs/basics/json_rpc.md b/docs/basics/json_rpc.md index f3df88bdc0..8e0261bc00 100644 --- a/docs/basics/json_rpc.md +++ b/docs/basics/json_rpc.md @@ -157,12 +157,13 @@ ethermintd start --evm-rpc.api eth,txpool,personal,net,debug,web3 | `les_latestCheckpoint` | Les | | | | `les_getCheckpoint` | Les | | | | `les_getCheckpointContractAddress` | Les | | | -| `miner_getHashrate` | Miner | | | -| `miner_setExtra` | Miner | | | -| `miner_setGasPrice` | Miner | | | -| `miner_start` | Miner | | | -| `miner_stop` | Miner | | | -| `miner_setEtherbase` | Miner | | | +| `miner_getHashrate` | Miner | N/A | Not relevant to Ethermint | +| `miner_setExtra` | Miner | N/A | Not relevant to Ethermint | +| `miner_setGasPrice` | Miner | ✔ | | +| `miner_start` | Miner | N/A | Not relevant to Ethermint | +| `miner_stop` | Miner | N/A | Not relevant to Ethermint | +| `miner_setGasLimit` | Miner | N/A | Not relevant to Ethermint | +| `miner_setEtherbase` | Miner | ✔ | | | `txpool_content` | TXPool | ✔ | | | `txpool_inspect` | TXPool | ✔ | | | `txpool_status` | TXPool | ✔ | | diff --git a/ethereum/rpc/apis.go b/ethereum/rpc/apis.go index 0b93bc5edb..9c31c8917e 100644 --- a/ethereum/rpc/apis.go +++ b/ethereum/rpc/apis.go @@ -10,6 +10,7 @@ import ( "github.com/tharsis/ethermint/ethereum/rpc/namespaces/debug" "github.com/tharsis/ethermint/ethereum/rpc/namespaces/eth" "github.com/tharsis/ethermint/ethereum/rpc/namespaces/eth/filters" + "github.com/tharsis/ethermint/ethereum/rpc/namespaces/miner" "github.com/tharsis/ethermint/ethereum/rpc/namespaces/net" "github.com/tharsis/ethermint/ethereum/rpc/namespaces/personal" "github.com/tharsis/ethermint/ethereum/rpc/namespaces/txpool" @@ -27,6 +28,7 @@ const ( NetNamespace = "net" TxPoolNamespace = "txpool" DebugNamespace = "debug" + MinerNamespace = "miner" apiVersion = "1.0" ) @@ -104,6 +106,15 @@ func GetRPCAPIs(ctx *server.Context, clientCtx client.Context, tmWSClient *rpccl Public: true, }, ) + case MinerNamespace: + apis = append(apis, + rpc.API{ + Namespace: MinerNamespace, + Version: apiVersion, + Service: miner.NewMinerAPI(ctx, ethAPI, evmBackend), + Public: true, + }, + ) default: ctx.Logger.Error("invalid namespace value", "namespace", selectedAPIs[index]) } diff --git a/ethereum/rpc/backend/backend.go b/ethereum/rpc/backend/backend.go index 10909f4ff3..0213b218e5 100644 --- a/ethereum/rpc/backend/backend.go +++ b/ethereum/rpc/backend/backend.go @@ -45,6 +45,8 @@ type Backend interface { // Used by log filter GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, error) BloomStatus() (uint64, uint64) + + GetCoinbase() (sdk.AccAddress, error) } var _ Backend = (*EVMBackend)(nil) @@ -417,3 +419,28 @@ func (e *EVMBackend) GetLogsByNumber(blockNum types.BlockNumber) ([][]*ethtypes. func (e *EVMBackend) BloomStatus() (uint64, uint64) { return 4096, 0 } + +// GetCoinbase is the address that staking rewards will be send to (alias for Etherbase). +func (e *EVMBackend) GetCoinbase() (sdk.AccAddress, error) { + node, err := e.clientCtx.GetNode() + if err != nil { + return nil, err + } + + status, err := node.Status(e.ctx) + if err != nil { + return nil, err + } + + req := &evmtypes.QueryValidatorAccountRequest{ + ConsAddress: sdk.ConsAddress(status.ValidatorInfo.Address).String(), + } + + res, err := e.queryClient.ValidatorAccount(e.ctx, req) + if err != nil { + return nil, err + } + + address, _ := sdk.AccAddressFromBech32(res.AccountAddress) + return address, nil +} diff --git a/ethereum/rpc/namespaces/eth/api.go b/ethereum/rpc/namespaces/eth/api.go index 85ebce8949..aa06ff51c5 100644 --- a/ethereum/rpc/namespaces/eth/api.go +++ b/ethereum/rpc/namespaces/eth/api.go @@ -98,6 +98,14 @@ func (e *PublicAPI) ClientCtx() client.Context { return e.clientCtx } +func (e *PublicAPI) QueryClient() *rpctypes.QueryClient { + return e.queryClient +} + +func (e *PublicAPI) Ctx() context.Context { + return e.ctx +} + // ProtocolVersion returns the supported Ethereum protocol version. func (e *PublicAPI) ProtocolVersion() hexutil.Uint { e.logger.Debug("eth_protocolVersion") @@ -137,27 +145,11 @@ func (e *PublicAPI) Syncing() (interface{}, error) { func (e *PublicAPI) Coinbase() (string, error) { e.logger.Debug("eth_coinbase") - node, err := e.clientCtx.GetNode() - if err != nil { - return "", err - } - - status, err := node.Status(e.ctx) + coinbase, err := e.backend.GetCoinbase() if err != nil { return "", err } - - req := &evmtypes.QueryValidatorAccountRequest{ - ConsAddress: sdk.ConsAddress(status.ValidatorInfo.Address).String(), - } - - res, err := e.queryClient.ValidatorAccount(e.ctx, req) - if err != nil { - return "", err - } - - toAddr, _ := sdk.AccAddressFromBech32(res.AccountAddress) - ethAddr := common.BytesToAddress(toAddr.Bytes()) + ethAddr := common.BytesToAddress(coinbase.Bytes()) return ethAddr.Hex(), nil } diff --git a/ethereum/rpc/namespaces/miner/api.go b/ethereum/rpc/namespaces/miner/api.go new file mode 100644 index 0000000000..936b5d0014 --- /dev/null +++ b/ethereum/rpc/namespaces/miner/api.go @@ -0,0 +1,191 @@ +package miner + +import ( + "errors" + "math/big" + + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/cosmos/cosmos-sdk/server" + "github.com/cosmos/cosmos-sdk/server/config" + sdk "github.com/cosmos/cosmos-sdk/types" + + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + + "github.com/tendermint/tendermint/libs/log" + tmtypes "github.com/tendermint/tendermint/types" + + "github.com/tharsis/ethermint/ethereum/rpc/backend" + "github.com/tharsis/ethermint/ethereum/rpc/namespaces/eth" + rpctypes "github.com/tharsis/ethermint/ethereum/rpc/types" +) + +// API is the miner prefixed set of APIs in the Miner JSON-RPC spec. +type API struct { + ctx *server.Context + logger log.Logger + ethAPI *eth.PublicAPI + backend backend.Backend +} + +// NewMinerAPI creates an instance of the Miner API. +func NewMinerAPI( + ctx *server.Context, + ethAPI *eth.PublicAPI, + backend backend.Backend, +) *API { + return &API{ + ctx: ctx, + ethAPI: ethAPI, + logger: ctx.Logger.With("api", "miner"), + backend: backend, + } +} + +// SetEtherbase sets the etherbase of the miner +func (api *API) SetEtherbase(etherbase common.Address) bool { + api.logger.Debug("miner_setEtherbase") + + delAddr, err := api.backend.GetCoinbase() + if err != nil { + api.logger.Debug("failed to get coinbase address", "error", err.Error()) + return false + } + + withdrawAddr := sdk.AccAddress(etherbase.Bytes()) + msg := distributiontypes.NewMsgSetWithdrawAddress(delAddr, withdrawAddr) + + if err := msg.ValidateBasic(); err != nil { + api.logger.Debug("tx failed basic validation", "error", err.Error()) + return false + } + + // Assemble transaction from fields + builder, ok := api.ethAPI.ClientCtx().TxConfig.NewTxBuilder().(authtx.ExtensionOptionsTxBuilder) + if !ok { + api.logger.Debug("clientCtx.TxConfig.NewTxBuilder returns unsupported builder", "error", err.Error()) + return false + } + + err = builder.SetMsgs(msg) + if err != nil { + api.logger.Error("builder.SetMsgs failed", "error", err.Error()) + return false + } + + // Fetch minimun gas price to calculate fees using the configuration. + appConf, err := config.ParseConfig(api.ctx.Viper) + if err != nil { + api.logger.Error("failed to parse file.", "file", api.ctx.Viper.ConfigFileUsed(), "error:", err.Error()) + return false + } + + minGasPrices := appConf.GetMinGasPrices() + if len(minGasPrices) == 0 || minGasPrices.Empty() { + api.logger.Debug("the minimun fee is not set") + return false + } + minGasPriceValue := minGasPrices[0].Amount + denom := minGasPrices[0].Denom + + delCommonAddr := common.BytesToAddress(delAddr.Bytes()) + nonce, err := api.ethAPI.GetTransactionCount(delCommonAddr, rpctypes.EthPendingBlockNumber) + if err != nil { + api.logger.Debug("failed to get nonce", "error", err.Error()) + return false + } + + txFactory := tx.Factory{} + txFactory = txFactory. + WithChainID(api.ethAPI.ClientCtx().ChainID). + WithKeybase(api.ethAPI.ClientCtx().Keyring). + WithTxConfig(api.ethAPI.ClientCtx().TxConfig). + WithSequence(uint64(*nonce)). + WithGasAdjustment(1.11) + + _, gas, err := tx.CalculateGas(api.ethAPI.ClientCtx(), txFactory, msg) + if err != nil { + api.logger.Debug("failed to calculate gas", "error", err.Error()) + return false + } + + txFactory = txFactory.WithGas(gas) + + value := new(big.Int).SetUint64(gas * minGasPriceValue.Ceil().TruncateInt().Uint64()) + fees := sdk.Coins{sdk.NewCoin(denom, sdk.NewIntFromBigInt(value))} + builder.SetFeeAmount(fees) + builder.SetGasLimit(gas) + + keyInfo, err := api.ethAPI.ClientCtx().Keyring.KeyByAddress(delAddr) + if err != nil { + api.logger.Debug("failed to get the wallet address using the keyring", "error", err.Error()) + return false + } + + if err := tx.Sign(txFactory, keyInfo.GetName(), builder, false); err != nil { + api.logger.Debug("failed to sign tx", "error", err.Error()) + return false + } + + // Encode transaction by default Tx encoder + txEncoder := api.ethAPI.ClientCtx().TxConfig.TxEncoder() + txBytes, err := txEncoder(builder.GetTx()) + if err != nil { + api.logger.Debug("failed to encode eth tx using default encoder", "error", err.Error()) + return false + } + + tmHash := common.BytesToHash(tmtypes.Tx(txBytes).Hash()) + + // Broadcast transaction in sync mode (default) + // NOTE: If error is encountered on the node, the broadcast will not return an error + syncCtx := api.ethAPI.ClientCtx().WithBroadcastMode(flags.BroadcastSync) + rsp, err := syncCtx.BroadcastTx(txBytes) + if err != nil || rsp.Code != 0 { + if err == nil { + err = errors.New(rsp.RawLog) + } + api.logger.Debug("failed to broadcast tx", "error", err.Error()) + return false + } + + api.logger.Debug("broadcasted tx to set miner withdraw address (etherbase)", "hash", tmHash.String()) + return true +} + +// SetGasPrice sets the minimum accepted gas price for the miner. +// NOTE: this function accepts only integers to have the same interface than go-eth +// to use float values, the gas prices must be configured using the configuration file +func (api *API) SetGasPrice(gasPrice hexutil.Big) bool { + api.logger.Info(api.ctx.Viper.ConfigFileUsed()) + appConf, err := config.ParseConfig(api.ctx.Viper) + if err != nil { + api.logger.Debug("failed to parse config file", "file", api.ctx.Viper.ConfigFileUsed(), "error", err.Error()) + return false + } + + var unit string + minGasPrices := appConf.GetMinGasPrices() + + // fetch the base denom from the sdk Config in case it's not currently defined on the node config + if len(minGasPrices) == 0 || minGasPrices.Empty() { + unit, err = sdk.GetBaseDenom() + if err != nil { + api.logger.Debug("could not get the denom of smallest unit registered", "error", err.Error()) + return false + } + } else { + unit = minGasPrices[0].Denom + } + + c := sdk.NewDecCoin(unit, sdk.NewIntFromBigInt(gasPrice.ToInt())) + + appConf.SetMinGasPrices(sdk.DecCoins{c}) + config.WriteConfigFile(api.ctx.Viper.ConfigFileUsed(), appConf) + api.logger.Info("Your configuration file was modified. Please RESTART your node.", "gas-price", c.String()) + return true +} diff --git a/ethereum/rpc/namespaces/miner/unsupported.go b/ethereum/rpc/namespaces/miner/unsupported.go new file mode 100644 index 0000000000..67c4a81974 --- /dev/null +++ b/ethereum/rpc/namespaces/miner/unsupported.go @@ -0,0 +1,51 @@ +package miner + +import ( + "errors" + + "github.com/ethereum/go-ethereum/common/hexutil" +) + +// GetHashrate returns the current hashrate for local CPU miner and remote miner. +// Unsupported in Ethermint +func (api *API) GetHashrate() uint64 { + api.logger.Debug("miner_getHashrate") + api.logger.Debug("Unsupported rpc function: miner_getHashrate") + return 0 +} + +// SetExtra sets the extra data string that is included when this miner mines a block. +// Unsupported in Ethermint +func (api *API) SetExtra(extra string) (bool, error) { + api.logger.Debug("miner_setExtra") + api.logger.Debug("Unsupported rpc function: miner_setExtra") + return false, errors.New("unsupported rpc function: miner_setExtra") +} + +// SetGasLimit sets the gaslimit to target towards during mining. +// Unsupported in Ethermint +func (api *API) SetGasLimit(gasLimit hexutil.Uint64) bool { + api.logger.Debug("miner_setGasLimit") + api.logger.Debug("Unsupported rpc function: miner_setGasLimit") + return false +} + +// Start starts the miner with the given number of threads. If threads is nil, +// the number of workers started is equal to the number of logical CPUs that are +// usable by this process. If mining is already running, this method adjust the +// number of threads allowed to use and updates the minimum price required by the +// transaction pool. +// Unsupported in Ethermint +func (api *API) Start(threads *int) error { + api.logger.Debug("miner_start") + api.logger.Debug("Unsupported rpc function: miner_start") + return errors.New("unsupported rpc function: miner_start") +} + +// Stop terminates the miner, both at the consensus engine level as well as at +// the block creation level. +// Unsupported in Ethermint +func (api *API) Stop() { + api.logger.Debug("miner_stop") + api.logger.Debug("Unsupported rpc function: miner_stop") +} diff --git a/init.sh b/init.sh index 733d0554b6..4d8d0f1d01 100755 --- a/init.sh +++ b/init.sh @@ -86,4 +86,4 @@ if [[ $1 == "pending" ]]; then fi # Start the node (remove the --pruning=nothing flag if historical queries are not needed) -ethermintd start --pruning=nothing $TRACE --log_level $LOGLEVEL --minimum-gas-prices=0.0001aphoton --evm-rpc.api eth,txpool,personal,net,debug,web3 +ethermintd start --pruning=nothing $TRACE --log_level $LOGLEVEL --minimum-gas-prices=0.0001aphoton --evm-rpc.api eth,txpool,personal,net,debug,web3,miner