diff --git a/core/chain.go b/core/chain.go deleted file mode 100644 index 1a1f28be..00000000 --- a/core/chain.go +++ /dev/null @@ -1,166 +0,0 @@ -package core - -// TODO: This functionality and implementation may be deprecated - -import ( - "math/big" - - ethcmn "github.com/ethereum/go-ethereum/common" - ethcons "github.com/ethereum/go-ethereum/consensus" - ethstate "github.com/ethereum/go-ethereum/core/state" - ethtypes "github.com/ethereum/go-ethereum/core/types" - ethrpc "github.com/ethereum/go-ethereum/rpc" -) - -// ChainContext implements Ethereum's core.ChainContext and consensus.Engine -// interfaces. It is needed in order to apply and process Ethereum -// transactions. There should only be a single implementation in Ethermint. For -// the purposes of Ethermint, it should be support retrieving headers and -// consensus parameters from the current blockchain to be used during -// transaction processing. -// -// NOTE: Ethermint will distribute the fees out to validators, so the structure -// and functionality of this is a WIP and subject to change. -type ChainContext struct { - Coinbase ethcmn.Address - headersByNumber map[uint64]*ethtypes.Header -} - -// NewChainContext generates new ChainContext based on Ethereum's core.ChainContext and -// consensus.Engine interfaces in order to process Ethereum transactions. -func NewChainContext() *ChainContext { - return &ChainContext{ - headersByNumber: make(map[uint64]*ethtypes.Header), - } -} - -// Engine implements Ethereum's core.ChainContext interface. As a ChainContext -// implements the consensus.Engine interface, it is simply returned. -func (cc *ChainContext) Engine() ethcons.Engine { - return cc -} - -// SetHeader implements Ethereum's core.ChainContext interface. It sets the -// header for the given block number. -func (cc *ChainContext) SetHeader(number uint64, header *ethtypes.Header) { - cc.headersByNumber[number] = header -} - -// GetHeader implements Ethereum's core.ChainContext interface. -// -// TODO: The Cosmos SDK supports retreiving such information in contexts and -// multi-store, so this will be need to be integrated. -func (cc *ChainContext) GetHeader(_ ethcmn.Hash, number uint64) *ethtypes.Header { - if header, ok := cc.headersByNumber[number]; ok { - return header - } - - return nil -} - -// Author implements Ethereum's consensus.Engine interface. It is responsible -// for returned the address of the validtor to receive any fees. This function -// is only invoked if the given author in the ApplyTransaction call is nil. -// -// NOTE: Ethermint will distribute the fees out to validators, so the structure -// and functionality of this is a WIP and subject to change. -func (cc *ChainContext) Author(_ *ethtypes.Header) (ethcmn.Address, error) { - return cc.Coinbase, nil -} - -// APIs implements Ethereum's consensus.Engine interface. It currently performs -// a no-op. -// -// TODO: Do we need to support such RPC APIs? This will tie into a bigger -// discussion on if we want to support web3. -func (cc *ChainContext) APIs(_ ethcons.ChainHeaderReader) []ethrpc.API { - return nil -} - -// CalcDifficulty implements Ethereum's consensus.Engine interface. It currently -// performs a no-op. -func (cc *ChainContext) CalcDifficulty(_ ethcons.ChainHeaderReader, _ uint64, _ *ethtypes.Header) *big.Int { - return nil -} - -// Finalize implements Ethereum's consensus.Engine interface. It currently -// performs a no-op. -// -// TODO: Figure out if this needs to be hooked up to any part of the ABCI? -func (cc *ChainContext) Finalize( - _ ethcons.ChainHeaderReader, _ *ethtypes.Header, _ *ethstate.StateDB, - _ []*ethtypes.Transaction, _ []*ethtypes.Header) { -} - -// FinalizeAndAssemble runs any post-transaction state modifications (e.g. block -// rewards) and assembles the final block. -// -// Note: The block header and state database might be updated to reflect any -// consensus rules that happen at finalization (e.g. block rewards). -// TODO: Figure out if this needs to be hooked up to any part of the ABCI? -func (cc *ChainContext) FinalizeAndAssemble(_ ethcons.ChainHeaderReader, _ *ethtypes.Header, _ *ethstate.StateDB, _ []*ethtypes.Transaction, - _ []*ethtypes.Header, _ []*ethtypes.Receipt) (*ethtypes.Block, error) { - return nil, nil -} - -// Prepare implements Ethereum's consensus.Engine interface. It currently -// performs a no-op. -// -// TODO: Figure out if this needs to be hooked up to any part of the ABCI? -func (cc *ChainContext) Prepare(_ ethcons.ChainHeaderReader, _ *ethtypes.Header) error { - return nil -} - -// Seal implements Ethereum's consensus.Engine interface. It currently -// performs a no-op. -// -// TODO: Figure out if this needs to be hooked up to any part of the ABCI? -func (cc *ChainContext) Seal(_ ethcons.ChainHeaderReader, _ *ethtypes.Block, _ chan<- *ethtypes.Block, _ <-chan struct{}) error { - return nil -} - -// SealHash implements Ethereum's consensus.Engine interface. It returns the -// hash of a block prior to it being sealed. -func (cc *ChainContext) SealHash(header *ethtypes.Header) ethcmn.Hash { - return ethcmn.Hash{} -} - -// VerifyHeader implements Ethereum's consensus.Engine interface. It currently -// performs a no-op. -// -// TODO: Figure out if this needs to be hooked up to any part of the Cosmos SDK -// handlers? -func (cc *ChainContext) VerifyHeader(_ ethcons.ChainHeaderReader, _ *ethtypes.Header, _ bool) error { - return nil -} - -// VerifyHeaders implements Ethereum's consensus.Engine interface. It -// currently performs a no-op. -// -// TODO: Figure out if this needs to be hooked up to any part of the Cosmos SDK -// handlers? -func (cc *ChainContext) VerifyHeaders(_ ethcons.ChainHeaderReader, _ []*ethtypes.Header, _ []bool) (chan<- struct{}, <-chan error) { - return nil, nil -} - -// VerifySeal implements Ethereum's consensus.Engine interface. It currently -// performs a no-op. -// -// TODO: Figure out if this needs to be hooked up to any part of the Cosmos SDK -// handlers? -func (cc *ChainContext) VerifySeal(_ ethcons.ChainHeaderReader, _ *ethtypes.Header) error { - return nil -} - -// VerifyUncles implements Ethereum's consensus.Engine interface. It currently -// performs a no-op. -func (cc *ChainContext) VerifyUncles(_ ethcons.ChainReader, _ *ethtypes.Block) error { - return nil -} - -// Close implements Ethereum's consensus.Engine interface. It terminates any -// background threads maintained by the consensus engine. It currently performs -// a no-op. -func (cc *ChainContext) Close() error { - return nil -} diff --git a/core/chain_test.go b/core/chain_test.go deleted file mode 100644 index aaa81a64..00000000 --- a/core/chain_test.go +++ /dev/null @@ -1,119 +0,0 @@ -package core - -// NOTE: A bulk of these unit tests will change and evolve as the context and -// implementation of ChainConext evolves. - -import ( - "math/big" - "testing" - - "github.com/stretchr/testify/require" - - ethcmn "github.com/ethereum/go-ethereum/common" - ethcons "github.com/ethereum/go-ethereum/consensus" - ethcore "github.com/ethereum/go-ethereum/core" - ethtypes "github.com/ethereum/go-ethereum/core/types" -) - -func TestChainContextInterface(t *testing.T) { - require.Implements(t, (*ethcore.ChainContext)(nil), new(ChainContext)) - require.Implements(t, (*ethcons.Engine)(nil), new(ChainContext)) -} - -func TestNewChainContext(t *testing.T) { - cc := NewChainContext() - require.NotNil(t, cc.headersByNumber) -} - -func TestChainContextEngine(t *testing.T) { - cc := NewChainContext() - require.Equal(t, cc, cc.Engine()) -} - -func TestChainContextSetHeader(t *testing.T) { - cc := NewChainContext() - header := ðtypes.Header{ - Number: big.NewInt(64), - } - - cc.SetHeader(uint64(header.Number.Int64()), header) - require.Equal(t, header, cc.headersByNumber[uint64(header.Number.Int64())]) -} - -func TestChainContextGetHeader(t *testing.T) { - cc := NewChainContext() - header := ðtypes.Header{ - Number: big.NewInt(64), - } - - cc.SetHeader(uint64(header.Number.Int64()), header) - require.Equal(t, header, cc.GetHeader(ethcmn.Hash{}, uint64(header.Number.Int64()))) - require.Nil(t, cc.GetHeader(ethcmn.Hash{}, 0)) -} - -func TestChainContextAuthor(t *testing.T) { - cc := NewChainContext() - - cb, err := cc.Author(nil) - require.Nil(t, err) - require.Equal(t, cc.Coinbase, cb) -} - -func TestChainContextAPIs(t *testing.T) { - cc := NewChainContext() - require.Nil(t, cc.APIs(nil)) -} - -func TestChainContextCalcDifficulty(t *testing.T) { - cc := NewChainContext() - require.Nil(t, cc.CalcDifficulty(nil, 0, nil)) -} - -func TestChainContextFinalize(t *testing.T) { - cc := NewChainContext() - - cc.Finalize(nil, nil, nil, nil, nil) -} - -func TestChainContextPrepare(t *testing.T) { - cc := NewChainContext() - - err := cc.Prepare(nil, nil) - require.Nil(t, err) -} - -func TestChainContextSeal(t *testing.T) { - cc := NewChainContext() - - err := cc.Seal(nil, nil, nil, nil) - require.Nil(t, err) -} - -func TestChainContextVerifyHeader(t *testing.T) { - cc := NewChainContext() - - err := cc.VerifyHeader(nil, nil, false) - require.Nil(t, err) -} - -func TestChainContextVerifyHeaders(t *testing.T) { - cc := NewChainContext() - - ch, err := cc.VerifyHeaders(nil, nil, []bool{false}) - require.Nil(t, err) - require.Nil(t, ch) -} - -func TestChainContextVerifySeal(t *testing.T) { - cc := NewChainContext() - - err := cc.VerifySeal(nil, nil) - require.Nil(t, err) -} - -func TestChainContextVerifyUncles(t *testing.T) { - cc := NewChainContext() - - err := cc.VerifyUncles(nil, nil) - require.Nil(t, err) -} diff --git a/ethereum/rpc/apis.go b/ethereum/rpc/apis.go index bb60974a..50d174a2 100644 --- a/ethereum/rpc/apis.go +++ b/ethereum/rpc/apis.go @@ -51,5 +51,11 @@ func GetRPCAPIs(clientCtx client.Context, tmWSClient *rpcclient.WSClient) []rpc. Service: NewPublicNetAPI(clientCtx), Public: true, }, + { + Namespace: PersonalNamespace, + Version: apiVersion, + Service: NewPersonalAPI(ethAPI), + Public: true, + }, } } diff --git a/ethereum/rpc/eth_api.go b/ethereum/rpc/eth_api.go index ca218e28..7d20287f 100644 --- a/ethereum/rpc/eth_api.go +++ b/ethereum/rpc/eth_api.go @@ -9,19 +9,20 @@ import ( "strings" "sync" - "github.com/cosmos/ethermint/ethereum/rpc/types" "github.com/gogo/protobuf/jsonpb" - abci "github.com/tendermint/tendermint/abci/types" - "github.com/pkg/errors" + "github.com/spf13/viper" log "github.com/xlab/suplog" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/crypto/keyring" sdk "github.com/cosmos/cosmos-sdk/types" authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + + abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/crypto/tmhash" "github.com/ethereum/go-ethereum/accounts/keystore" @@ -30,6 +31,8 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rlp" + "github.com/cosmos/ethermint/crypto/hd" + "github.com/cosmos/ethermint/ethereum/rpc/types" rpctypes "github.com/cosmos/ethermint/ethereum/rpc/types" ethermint "github.com/cosmos/ethermint/types" evmtypes "github.com/cosmos/ethermint/x/evm/types" @@ -39,11 +42,11 @@ import ( type PublicEthAPI struct { ctx context.Context clientCtx client.Context - queryClient *types.QueryClient + queryClient *rpctypes.QueryClient chainIDEpoch *big.Int logger log.Logger backend Backend - nonceLock *types.AddrLocker + nonceLock *rpctypes.AddrLocker keyringLock sync.Mutex } @@ -58,6 +61,24 @@ func NewPublicEthAPI( panic(err) } + algos, _ := clientCtx.Keyring.SupportedAlgorithms() + + if !algos.Contains(hd.EthSecp256k1) { + kr, err := keyring.New( + sdk.KeyringServiceName(), + viper.GetString(flags.FlagKeyringBackend), + clientCtx.KeyringDir, + clientCtx.Input, + hd.EthSecp256k1Option(), + ) + + if err != nil { + panic(err) + } + + clientCtx = clientCtx.WithKeyring(kr) + } + api := &PublicEthAPI{ ctx: context.Background(), clientCtx: clientCtx, @@ -71,6 +92,11 @@ func NewPublicEthAPI( return api } +// ClientCtx returns client context +func (e *PublicEthAPI) ClientCtx() client.Context { + return e.clientCtx +} + // ProtocolVersion returns the supported Ethereum protocol version. func (e *PublicEthAPI) ProtocolVersion() hexutil.Uint { e.logger.Debugln("eth_protocolVersion") diff --git a/ethereum/rpc/personal.go b/ethereum/rpc/personal.go new file mode 100644 index 00000000..ee8a432d --- /dev/null +++ b/ethereum/rpc/personal.go @@ -0,0 +1,193 @@ +package rpc + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/cosmos/ethermint/crypto/hd" + ethermint "github.com/cosmos/ethermint/types" + + log "github.com/xlab/suplog" + + sdkcrypto "github.com/cosmos/cosmos-sdk/crypto" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/cosmos/ethermint/crypto/ethsecp256k1" + rpctypes "github.com/cosmos/ethermint/ethereum/rpc/types" +) + +// PrivateAccountAPI is the personal_ prefixed set of APIs in the Web3 JSON-RPC spec. +type PrivateAccountAPI struct { + ethAPI *PublicEthAPI + logger log.Logger +} + +// NewPersonalAPI creates an instance of the public Personal Eth API. +func NewPersonalAPI(ethAPI *PublicEthAPI) *PrivateAccountAPI { + return &PrivateAccountAPI{ + ethAPI: ethAPI, + logger: log.WithField("module", "personal"), + } +} + +// ImportRawKey armors and encrypts a given raw hex encoded ECDSA key and stores it into the key directory. +// The name of the key will have the format "personal_", where is the total number of +// keys stored on the keyring. +// +// NOTE: The key will be both armored and encrypted using the same passphrase. +func (api *PrivateAccountAPI) ImportRawKey(privkey, password string) (common.Address, error) { + api.logger.Debug("personal_importRawKey") + priv, err := crypto.HexToECDSA(privkey) + if err != nil { + return common.Address{}, err + } + + privKey := ðsecp256k1.PrivKey{Key: crypto.FromECDSA(priv)} + + addr := sdk.AccAddress(privKey.PubKey().Address().Bytes()) + ethereumAddr := common.BytesToAddress(addr) + + // return if the key has already been imported + if _, err := api.ethAPI.ClientCtx().Keyring.KeyByAddress(addr); err == nil { + return ethereumAddr, nil + } + + // ignore error as we only care about the length of the list + list, _ := api.ethAPI.ClientCtx().Keyring.List() + privKeyName := fmt.Sprintf("personal_%d", len(list)) + + armor := sdkcrypto.EncryptArmorPrivKey(privKey, password, ethsecp256k1.KeyType) + + if err := api.ethAPI.ClientCtx().Keyring.ImportPrivKey(privKeyName, armor, password); err != nil { + return common.Address{}, err + } + + api.logger.Info("key successfully imported", "name", privKeyName, "address", ethereumAddr.String()) + + return ethereumAddr, nil +} + +// ListAccounts will return a list of addresses for accounts this node manages. +func (api *PrivateAccountAPI) ListAccounts() ([]common.Address, error) { + api.logger.Debug("personal_listAccounts") + addrs := []common.Address{} + + list, err := api.ethAPI.ClientCtx().Keyring.List() + if err != nil { + return nil, err + } + + for _, info := range list { + addrs = append(addrs, common.BytesToAddress(info.GetPubKey().Address())) + } + + return addrs, nil +} + +// LockAccount will lock the account associated with the given address when it's unlocked. +// It removes the key corresponding to the given address from the API's local keys. +func (api *PrivateAccountAPI) LockAccount(address common.Address) bool { + api.logger.Debugln("personal_lockAccount", "address", address.String()) + api.logger.Info("personal_lockAccount not supported") + return false +} + +// NewAccount will create a new account and returns the address for the new account. +func (api *PrivateAccountAPI) NewAccount(password string) (common.Address, error) { + api.logger.Debug("personal_newAccount") + + name := "key_" + time.Now().UTC().Format(time.RFC3339) + + // create the mnemonic and save the account + info, _, err := api.ethAPI.ClientCtx().Keyring.NewMnemonic(name, keyring.English, ethermint.BIP44HDPath, hd.EthSecp256k1) + if err != nil { + return common.Address{}, err + } + + addr := common.BytesToAddress(info.GetPubKey().Address().Bytes()) + api.logger.Infoln("Your new key was generated", "address", addr.String()) + api.logger.Infoln("Please backup your key file!", "path", os.Getenv("HOME")+"/.ethermint/"+name) + api.logger.Infoln("Please remember your password!") + return addr, nil +} + +// UnlockAccount will unlock the account associated with the given address with +// the given password for duration seconds. If duration is nil it will use a +// default of 300 seconds. It returns an indication if the account was unlocked. +// It exports the private key corresponding to the given address from the keyring and stores it in the API's local keys. +func (api *PrivateAccountAPI) UnlockAccount(_ context.Context, addr common.Address, password string, _ *uint64) (bool, error) { // nolint: interfacer + api.logger.Debugln("personal_unlockAccount", "address", addr.String()) + api.logger.Info("personal_unlockAccount not supported") + return false, fmt.Errorf("not supported") +} + +// SendTransaction will create a transaction from the given arguments and +// tries to sign it with the key associated with args.To. If the given password isn't +// able to decrypt the key it fails. +func (api *PrivateAccountAPI) SendTransaction(_ context.Context, args rpctypes.SendTxArgs, pwrd string) (common.Hash, error) { + + return api.ethAPI.SendTransaction(args) +} + +// Sign calculates an Ethereum ECDSA signature for: +// keccak256("\x19Ethereum Signed Message:\n" + len(message) + message)) +// +// Note, the produced signature conforms to the secp256k1 curve R, S and V values, +// where the V value will be 27 or 28 for legacy reasons. +// +// The key used to calculate the signature is decrypted with the given password. +// +// https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_sign +func (api *PrivateAccountAPI) Sign(_ context.Context, data hexutil.Bytes, addr common.Address, pwrd string) (hexutil.Bytes, error) { + api.logger.Debug("personal_sign", "data", data, "address", addr.String()) + + cosmosAddr := sdk.AccAddress(addr.Bytes()) + + sig, _, err := api.ethAPI.ClientCtx().Keyring.SignByAddress(cosmosAddr, accounts.TextHash(data)) + if err != nil { + api.logger.WithError(err).Debugln("failed to sign with key", "data", data, "address", addr.String()) + return nil, err + } + + sig[crypto.RecoveryIDOffset] += 27 // transform V from 0/1 to 27/28 + return sig, nil +} + +// EcRecover returns the address for the account that was used to create the signature. +// Note, this function is compatible with eth_sign and personal_sign. As such it recovers +// the address of: +// hash = keccak256("\x19Ethereum Signed Message:\n"${message length}${message}) +// addr = ecrecover(hash, signature) +// +// Note, the signature must conform to the secp256k1 curve R, S and V values, where +// the V value must be 27 or 28 for legacy reasons. +// +// https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_ecRecove +func (api *PrivateAccountAPI) EcRecover(_ context.Context, data, sig hexutil.Bytes) (common.Address, error) { + api.logger.Debug("personal_ecRecover", "data", data, "sig", sig) + + if len(sig) != crypto.SignatureLength { + return common.Address{}, fmt.Errorf("signature must be %d bytes long", crypto.SignatureLength) + } + + if sig[crypto.RecoveryIDOffset] != 27 && sig[crypto.RecoveryIDOffset] != 28 { + return common.Address{}, fmt.Errorf("invalid Ethereum signature (V is not 27 or 28)") + } + + sig[crypto.RecoveryIDOffset] -= 27 // Transform yellow paper V from 27/28 to 0/1 + + pubkey, err := crypto.SigToPub(accounts.TextHash(data), sig) + if err != nil { + return common.Address{}, err + } + + return crypto.PubkeyToAddress(*pubkey), nil +} diff --git a/ethereum/rpc/types/utils.go b/ethereum/rpc/types/utils.go index 6b21c45b..7e047ba3 100644 --- a/ethereum/rpc/types/utils.go +++ b/ethereum/rpc/types/utils.go @@ -1,7 +1,6 @@ package types import ( - "bytes" "context" "encoding/hex" "fmt" @@ -17,7 +16,6 @@ import ( sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" - "github.com/cosmos/ethermint/crypto/ethsecp256k1" ethermint "github.com/cosmos/ethermint/types" evmtypes "github.com/cosmos/ethermint/x/evm/types" @@ -198,16 +196,6 @@ func FormatBlock( } } -// GetKeyByAddress returns the private key matching the given address. If not found it returns false. -func GetKeyByAddress(keys []ethsecp256k1.PrivKey, address common.Address) (key *ethsecp256k1.PrivKey, exist bool) { - for _, key := range keys { - if bytes.Equal(key.PubKey().Address().Bytes(), address.Bytes()) { - return &key, true - } - } - return nil, false -} - // BuildEthereumTx builds and signs a Cosmos transaction from a MsgEthereumTx and returns the tx func BuildEthereumTx(clientCtx client.Context, msg *evmtypes.MsgEthereumTx, accNumber, seq uint64, privKey cryptotypes.PrivKey) ([]byte, error) { // TODO: user defined evm coin