forked from cerc-io/laconicd-deprecated
		
	Transaction signing and encoding (#95)
* WIP setting up evm tx command and updating emint keys output * Fix linting issue * Wip restructuring to allow for ethereum signing and encoding * WIP setting up keybase and context to use Ethermint keys * Fixed encoding and decoding of emint keys * Adds command for generating explicit ethereum tx * Fixed evm route for handling tx * Fixed tx and msg encoding which allows transactions to be sent * Added relevant documentation for changes and cleaned up code * Added documentation and indicators why code was overriden
This commit is contained in:
		
							parent
							
								
									4c29c48905
								
							
						
					
					
						commit
						72fc3ca3af
					
				| @ -231,7 +231,7 @@ func validateEthTxCheckTx( | ||||
| 	// validate sender/signature
 | ||||
| 	signer, err := ethTxMsg.VerifySig(chainID) | ||||
| 	if err != nil { | ||||
| 		return sdk.ErrUnauthorized("signature verification failed").Result() | ||||
| 		return sdk.ErrUnauthorized(fmt.Sprintf("signature verification failed: %s", err)).Result() | ||||
| 	} | ||||
| 
 | ||||
| 	// validate account (nonce and balance checks)
 | ||||
|  | ||||
| @ -13,6 +13,9 @@ import ( | ||||
| 	sdk "github.com/cosmos/cosmos-sdk/types" | ||||
| 	emintkeys "github.com/cosmos/ethermint/keys" | ||||
| 
 | ||||
| 	authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli" | ||||
| 	bankcmd "github.com/cosmos/cosmos-sdk/x/bank/client/cli" | ||||
| 
 | ||||
| 	emintapp "github.com/cosmos/ethermint/app" | ||||
| 	"github.com/spf13/cobra" | ||||
| 	"github.com/spf13/viper" | ||||
| @ -48,7 +51,7 @@ func main() { | ||||
| 		sdkrpc.StatusCommand(), | ||||
| 		client.ConfigCmd(emintapp.DefaultCLIHome), | ||||
| 		queryCmd(cdc), | ||||
| 		// TODO: Set up tx command
 | ||||
| 		txCmd(cdc), | ||||
| 		// TODO: Set up rest routes (if included, different from web3 api)
 | ||||
| 		rpc.Web3RpcCmd(cdc), | ||||
| 		client.LineBreak, | ||||
| @ -83,6 +86,28 @@ func queryCmd(cdc *amino.Codec) *cobra.Command { | ||||
| 	return queryCmd | ||||
| } | ||||
| 
 | ||||
| func txCmd(cdc *amino.Codec) *cobra.Command { | ||||
| 	txCmd := &cobra.Command{ | ||||
| 		Use:   "tx", | ||||
| 		Short: "Transactions subcommands", | ||||
| 	} | ||||
| 
 | ||||
| 	txCmd.AddCommand( | ||||
| 		bankcmd.SendTxCmd(cdc), | ||||
| 		client.LineBreak, | ||||
| 		authcmd.GetSignCommand(cdc), | ||||
| 		client.LineBreak, | ||||
| 		authcmd.GetBroadcastCommand(cdc), | ||||
| 		authcmd.GetEncodeCommand(cdc), | ||||
| 		client.LineBreak, | ||||
| 	) | ||||
| 
 | ||||
| 	// add modules' tx commands
 | ||||
| 	emintapp.ModuleBasics.AddTxCommands(txCmd, cdc) | ||||
| 
 | ||||
| 	return txCmd | ||||
| } | ||||
| 
 | ||||
| func initConfig(cmd *cobra.Command) error { | ||||
| 	home, err := cmd.PersistentFlags().GetString(cli.HomeFlag) | ||||
| 	if err != nil { | ||||
|  | ||||
| @ -6,6 +6,12 @@ import ( | ||||
| 
 | ||||
| var cryptoCodec = codec.New() | ||||
| 
 | ||||
| const ( | ||||
| 	// Amino encoding names
 | ||||
| 	PrivKeyAminoName = "crypto/PrivKeySecp256k1" | ||||
| 	PubKeyAminoName  = "crypto/PubKeySecp256k1" | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| 	RegisterCodec(cryptoCodec) | ||||
| } | ||||
| @ -13,6 +19,7 @@ func init() { | ||||
| // RegisterCodec registers all the necessary types with amino for the given
 | ||||
| // codec.
 | ||||
| func RegisterCodec(cdc *codec.Codec) { | ||||
| 	cdc.RegisterConcrete(PubKeySecp256k1{}, "crypto/PubKeySecp256k1", nil) | ||||
| 	cdc.RegisterConcrete(PrivKeySecp256k1{}, "crypto/PrivKeySecp256k1", nil) | ||||
| 	cdc.RegisterConcrete(PubKeySecp256k1{}, PubKeyAminoName, nil) | ||||
| 
 | ||||
| 	cdc.RegisterConcrete(PrivKeySecp256k1{}, PrivKeyAminoName, nil) | ||||
| } | ||||
|  | ||||
							
								
								
									
										35
									
								
								crypto/encoding/amino.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								crypto/encoding/amino.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | ||||
| package encoding | ||||
| 
 | ||||
| import ( | ||||
| 	emintcrypto "github.com/cosmos/ethermint/crypto" | ||||
| 	amino "github.com/tendermint/go-amino" | ||||
| 	tmcrypto "github.com/tendermint/tendermint/crypto" | ||||
| ) | ||||
| 
 | ||||
| var cdc = amino.NewCodec() | ||||
| 
 | ||||
| func init() { | ||||
| 	RegisterAmino(cdc) | ||||
| } | ||||
| 
 | ||||
| // RegisterAmino registers all crypto related types in the given (amino) codec.
 | ||||
| func RegisterAmino(cdc *amino.Codec) { | ||||
| 	// These are all written here instead of
 | ||||
| 	cdc.RegisterInterface((*tmcrypto.PubKey)(nil), nil) | ||||
| 	cdc.RegisterConcrete(emintcrypto.PubKeySecp256k1{}, emintcrypto.PubKeyAminoName, nil) | ||||
| 
 | ||||
| 	cdc.RegisterInterface((*tmcrypto.PrivKey)(nil), nil) | ||||
| 	cdc.RegisterConcrete(emintcrypto.PrivKeySecp256k1{}, emintcrypto.PrivKeyAminoName, nil) | ||||
| } | ||||
| 
 | ||||
| // PrivKeyFromBytes unmarshalls emint private key from encoded bytes
 | ||||
| func PrivKeyFromBytes(privKeyBytes []byte) (privKey tmcrypto.PrivKey, err error) { | ||||
| 	err = cdc.UnmarshalBinaryBare(privKeyBytes, &privKey) | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| // PubKeyFromBytes unmarshalls emint public key from encoded bytes
 | ||||
| func PubKeyFromBytes(pubKeyBytes []byte) (pubKey tmcrypto.PubKey, err error) { | ||||
| 	err = cdc.UnmarshalBinaryBare(pubKeyBytes, &pubKey) | ||||
| 	return | ||||
| } | ||||
							
								
								
									
										28
									
								
								crypto/encoding/amino_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								crypto/encoding/amino_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | ||||
| package encoding | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/require" | ||||
| 
 | ||||
| 	emintcrypto "github.com/cosmos/ethermint/crypto" | ||||
| ) | ||||
| 
 | ||||
| func TestKeyEncodingDecoding(t *testing.T) { | ||||
| 	// Priv Key encoding and decoding
 | ||||
| 	privKey, err := emintcrypto.GenerateKey() | ||||
| 	require.NoError(t, err) | ||||
| 	privBytes := privKey.Bytes() | ||||
| 
 | ||||
| 	decodedPriv, err := PrivKeyFromBytes(privBytes) | ||||
| 	require.NoError(t, err) | ||||
| 	require.Equal(t, privKey, decodedPriv) | ||||
| 
 | ||||
| 	// Pub key encoding and decoding
 | ||||
| 	pubKey := privKey.PubKey() | ||||
| 	pubBytes := pubKey.Bytes() | ||||
| 
 | ||||
| 	decodedPub, err := PubKeyFromBytes(pubBytes) | ||||
| 	require.NoError(t, err) | ||||
| 	require.Equal(t, pubKey, decodedPub) | ||||
| } | ||||
| @ -13,7 +13,7 @@ import ( | ||||
| 	cosmosKeys "github.com/cosmos/cosmos-sdk/crypto/keys" | ||||
| 	"github.com/cosmos/cosmos-sdk/crypto/keys/hd" | ||||
| 	"github.com/cosmos/cosmos-sdk/crypto/keys/keyerror" | ||||
| 	"github.com/cosmos/cosmos-sdk/crypto/keys/mintkey" | ||||
| 	"github.com/cosmos/ethermint/crypto/keys/mintkey" | ||||
| 	"github.com/cosmos/cosmos-sdk/types" | ||||
| 
 | ||||
| 	bip39 "github.com/cosmos/go-bip39" | ||||
|  | ||||
							
								
								
									
										157
									
								
								crypto/keys/mintkey/mintkey.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								crypto/keys/mintkey/mintkey.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,157 @@ | ||||
| package mintkey | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/hex" | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	"github.com/tendermint/crypto/bcrypt" | ||||
| 
 | ||||
| 	emintEncoding "github.com/cosmos/ethermint/crypto/encoding" | ||||
| 	"github.com/tendermint/tendermint/crypto" | ||||
| 	"github.com/tendermint/tendermint/crypto/armor" | ||||
| 
 | ||||
| 	"github.com/tendermint/tendermint/crypto/xsalsa20symmetric" | ||||
| 
 | ||||
| 	cmn "github.com/tendermint/tendermint/libs/common" | ||||
| 
 | ||||
| 	"github.com/cosmos/cosmos-sdk/crypto/keys/keyerror" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	blockTypePrivKey = "ETHERMINT PRIVATE KEY" | ||||
| 	blockTypeKeyInfo = "ETHERMINT KEY INFO" | ||||
| 	blockTypePubKey  = "ETHERMINT PUBLIC KEY" | ||||
| ) | ||||
| 
 | ||||
| // Make bcrypt security parameter var, so it can be changed within the lcd test
 | ||||
| // Making the bcrypt security parameter a var shouldn't be a security issue:
 | ||||
| // One can't verify an invalid key by maliciously changing the bcrypt
 | ||||
| // parameter during a runtime vulnerability. The main security
 | ||||
| // threat this then exposes would be something that changes this during
 | ||||
| // runtime before the user creates their key. This vulnerability must
 | ||||
| // succeed to update this to that same value before every subsequent call
 | ||||
| // to the keys command in future startups / or the attacker must get access
 | ||||
| // to the filesystem. However, with a similar threat model (changing
 | ||||
| // variables in runtime), one can cause the user to sign a different tx
 | ||||
| // than what they see, which is a significantly cheaper attack then breaking
 | ||||
| // a bcrypt hash. (Recall that the nonce still exists to break rainbow tables)
 | ||||
| // For further notes on security parameter choice, see README.md
 | ||||
| var BcryptSecurityParameter = 12 | ||||
| 
 | ||||
| //-----------------------------------------------------------------
 | ||||
| // add armor
 | ||||
| 
 | ||||
| // Armor the InfoBytes
 | ||||
| func ArmorInfoBytes(bz []byte) string { | ||||
| 	return armorBytes(bz, blockTypeKeyInfo) | ||||
| } | ||||
| 
 | ||||
| // Armor the PubKeyBytes
 | ||||
| func ArmorPubKeyBytes(bz []byte) string { | ||||
| 	return armorBytes(bz, blockTypePubKey) | ||||
| } | ||||
| 
 | ||||
| func armorBytes(bz []byte, blockType string) string { | ||||
| 	header := map[string]string{ | ||||
| 		"type":    "Info", | ||||
| 		"version": "0.0.0", | ||||
| 	} | ||||
| 	return armor.EncodeArmor(blockType, header, bz) | ||||
| } | ||||
| 
 | ||||
| //-----------------------------------------------------------------
 | ||||
| // remove armor
 | ||||
| 
 | ||||
| // Unarmor the InfoBytes
 | ||||
| func UnarmorInfoBytes(armorStr string) (bz []byte, err error) { | ||||
| 	return unarmorBytes(armorStr, blockTypeKeyInfo) | ||||
| } | ||||
| 
 | ||||
| // Unarmor the PubKeyBytes
 | ||||
| func UnarmorPubKeyBytes(armorStr string) (bz []byte, err error) { | ||||
| 	return unarmorBytes(armorStr, blockTypePubKey) | ||||
| } | ||||
| 
 | ||||
| func unarmorBytes(armorStr, blockType string) (bz []byte, err error) { | ||||
| 	bType, header, bz, err := armor.DecodeArmor(armorStr) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	if bType != blockType { | ||||
| 		err = fmt.Errorf("Unrecognized armor type %q, expected: %q", bType, blockType) | ||||
| 		return | ||||
| 	} | ||||
| 	if header["version"] != "0.0.0" { | ||||
| 		err = fmt.Errorf("Unrecognized version: %v", header["version"]) | ||||
| 		return | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| //-----------------------------------------------------------------
 | ||||
| // encrypt/decrypt with armor
 | ||||
| 
 | ||||
| // Encrypt and armor the private key.
 | ||||
| func EncryptArmorPrivKey(privKey crypto.PrivKey, passphrase string) string { | ||||
| 	saltBytes, encBytes := encryptPrivKey(privKey, passphrase) | ||||
| 	header := map[string]string{ | ||||
| 		"kdf":  "bcrypt", | ||||
| 		"salt": fmt.Sprintf("%X", saltBytes), | ||||
| 	} | ||||
| 	armorStr := armor.EncodeArmor(blockTypePrivKey, header, encBytes) | ||||
| 	return armorStr | ||||
| } | ||||
| 
 | ||||
| // encrypt the given privKey with the passphrase using a randomly
 | ||||
| // generated salt and the xsalsa20 cipher. returns the salt and the
 | ||||
| // encrypted priv key.
 | ||||
| func encryptPrivKey(privKey crypto.PrivKey, passphrase string) (saltBytes []byte, encBytes []byte) { | ||||
| 	saltBytes = crypto.CRandBytes(16) | ||||
| 	key, err := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), BcryptSecurityParameter) | ||||
| 	if err != nil { | ||||
| 		cmn.Exit("Error generating bcrypt key from passphrase: " + err.Error()) | ||||
| 	} | ||||
| 	key = crypto.Sha256(key) // get 32 bytes
 | ||||
| 	privKeyBytes := privKey.Bytes() | ||||
| 	return saltBytes, xsalsa20symmetric.EncryptSymmetric(privKeyBytes, key) | ||||
| } | ||||
| 
 | ||||
| // Unarmor and decrypt the private key.
 | ||||
| func UnarmorDecryptPrivKey(armorStr string, passphrase string) (crypto.PrivKey, error) { | ||||
| 	var privKey crypto.PrivKey | ||||
| 	blockType, header, encBytes, err := armor.DecodeArmor(armorStr) | ||||
| 	if err != nil { | ||||
| 		return privKey, err | ||||
| 	} | ||||
| 	if blockType != blockTypePrivKey { | ||||
| 		return privKey, fmt.Errorf("Unrecognized armor type: %v", blockType) | ||||
| 	} | ||||
| 	if header["kdf"] != "bcrypt" { | ||||
| 		return privKey, fmt.Errorf("Unrecognized KDF type: %v", header["KDF"]) | ||||
| 	} | ||||
| 	if header["salt"] == "" { | ||||
| 		return privKey, fmt.Errorf("Missing salt bytes") | ||||
| 	} | ||||
| 	saltBytes, err := hex.DecodeString(header["salt"]) | ||||
| 	if err != nil { | ||||
| 		return privKey, fmt.Errorf("Error decoding salt: %v", err.Error()) | ||||
| 	} | ||||
| 	privKey, err = decryptPrivKey(saltBytes, encBytes, passphrase) | ||||
| 	return privKey, err | ||||
| } | ||||
| 
 | ||||
| func decryptPrivKey(saltBytes []byte, encBytes []byte, passphrase string) (privKey crypto.PrivKey, err error) { | ||||
| 	key, err := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), BcryptSecurityParameter) | ||||
| 	if err != nil { | ||||
| 		cmn.Exit("Error generating bcrypt key from passphrase: " + err.Error()) | ||||
| 	} | ||||
| 	key = crypto.Sha256(key) // Get 32 bytes
 | ||||
| 	privKeyBytes, err := xsalsa20symmetric.DecryptSymmetric(encBytes, key) | ||||
| 	if err != nil && err.Error() == "Ciphertext decryption failed" { | ||||
| 		return privKey, keyerror.NewErrWrongPassword() | ||||
| 	} else if err != nil { | ||||
| 		return privKey, err | ||||
| 	} | ||||
| 	privKey, err = emintEncoding.PrivKeyFromBytes(privKeyBytes) | ||||
| 	return privKey, err | ||||
| } | ||||
| @ -3,25 +3,41 @@ package keys | ||||
| import ( | ||||
| 	"encoding/hex" | ||||
| 
 | ||||
| 	sdk "github.com/cosmos/cosmos-sdk/types" | ||||
| 
 | ||||
| 	cosmosKeys "github.com/cosmos/cosmos-sdk/crypto/keys" | ||||
| ) | ||||
| 
 | ||||
| // KeyOutput defines a structure wrapping around an Info object used for output
 | ||||
| // functionality.
 | ||||
| type KeyOutput struct { | ||||
| 	Name      string `json:"name"` | ||||
| 	Type      string `json:"type"` | ||||
| 	Address   string `json:"address"` | ||||
| 	PubKey    string `json:"pubkey"` | ||||
| 	Mnemonic  string `json:"mnemonic,omitempty"` | ||||
| 	Threshold uint   `json:"threshold,omitempty"` | ||||
| 	Name       string `json:"name"` | ||||
| 	Type       string `json:"type"` | ||||
| 	Address    string `json:"address"` | ||||
| 	ETHAddress string `json:"ethaddress"` | ||||
| 	PubKey     string `json:"pubkey"` | ||||
| 	ETHPubKey  string `json:"ethpubkey"` | ||||
| 	Mnemonic   string `json:"mnemonic,omitempty"` | ||||
| 	Threshold  uint   `json:"threshold,omitempty"` | ||||
| } | ||||
| 
 | ||||
| // NewKeyOutput creates a default KeyOutput instance without Mnemonic, Threshold and PubKeys
 | ||||
| func NewKeyOutput(name, keyType, address, ethaddress, pubkey, ethpubkey string) KeyOutput { | ||||
| 	return KeyOutput{ | ||||
| 		Name:       name, | ||||
| 		Type:       keyType, | ||||
| 		Address:    address, | ||||
| 		ETHAddress: ethaddress, | ||||
| 		PubKey:     pubkey, | ||||
| 		ETHPubKey:  ethpubkey, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Bech32KeysOutput returns a slice of KeyOutput objects, each with the "acc"
 | ||||
| // Bech32 prefixes, given a slice of Info objects. It returns an error if any
 | ||||
| // call to Bech32KeyOutput fails.
 | ||||
| func Bech32KeysOutput(infos []cosmosKeys.Info) ([]cosmosKeys.KeyOutput, error) { | ||||
| 	kos := make([]cosmosKeys.KeyOutput, len(infos)) | ||||
| func Bech32KeysOutput(infos []cosmosKeys.Info) ([]KeyOutput, error) { | ||||
| 	kos := make([]KeyOutput, len(infos)) | ||||
| 	for i, info := range infos { | ||||
| 		ko, err := Bech32KeyOutput(info) | ||||
| 		if err != nil { | ||||
| @ -34,52 +50,62 @@ func Bech32KeysOutput(infos []cosmosKeys.Info) ([]cosmosKeys.KeyOutput, error) { | ||||
| } | ||||
| 
 | ||||
| // Bech32ConsKeyOutput create a KeyOutput in with "cons" Bech32 prefixes.
 | ||||
| func Bech32ConsKeyOutput(keyInfo cosmosKeys.Info) (cosmosKeys.KeyOutput, error) { | ||||
| 	consAddr := keyInfo.GetPubKey().Address() | ||||
| 	bytes := keyInfo.GetPubKey().Bytes() | ||||
| func Bech32ConsKeyOutput(keyInfo cosmosKeys.Info) (KeyOutput, error) { | ||||
| 	address := keyInfo.GetPubKey().Address() | ||||
| 
 | ||||
| 	// bechPubKey, err := sdk.Bech32ifyConsPub(keyInfo.GetPubKey())
 | ||||
| 	// if err != nil {
 | ||||
| 	// 	return KeyOutput{}, err
 | ||||
| 	// }
 | ||||
| 	bechPubKey, err := sdk.Bech32ifyConsPub(keyInfo.GetPubKey()) | ||||
| 	if err != nil { | ||||
| 		return KeyOutput{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	return cosmosKeys.KeyOutput{ | ||||
| 		Name:    keyInfo.GetName(), | ||||
| 		Type:    keyInfo.GetType().String(), | ||||
| 		Address: consAddr.String(), | ||||
| 		PubKey:  hex.EncodeToString(bytes), | ||||
| 	}, nil | ||||
| 	return NewKeyOutput( | ||||
| 		keyInfo.GetName(), | ||||
| 		keyInfo.GetType().String(), | ||||
| 		sdk.ConsAddress(address.Bytes()).String(), | ||||
| 		getEthAddress(keyInfo), | ||||
| 		bechPubKey, | ||||
| 		hex.EncodeToString(keyInfo.GetPubKey().Bytes()), | ||||
| 	), nil | ||||
| } | ||||
| 
 | ||||
| // Bech32ValKeyOutput create a KeyOutput in with "val" Bech32 prefixes.
 | ||||
| func Bech32ValKeyOutput(keyInfo cosmosKeys.Info) (cosmosKeys.KeyOutput, error) { | ||||
| 	valAddr := keyInfo.GetPubKey().Address() | ||||
| 	bytes := keyInfo.GetPubKey().Bytes() | ||||
| func Bech32ValKeyOutput(keyInfo cosmosKeys.Info) (KeyOutput, error) { | ||||
| 	address := keyInfo.GetPubKey().Address() | ||||
| 
 | ||||
| 	// bechPubKey, err := sdk.Bech32ifyValPub(keyInfo.GetPubKey())
 | ||||
| 	// if err != nil {
 | ||||
| 	// 	return KeyOutput{}, err
 | ||||
| 	// }
 | ||||
| 	bechPubKey, err := sdk.Bech32ifyValPub(keyInfo.GetPubKey()) | ||||
| 	if err != nil { | ||||
| 		return KeyOutput{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	return cosmosKeys.KeyOutput{ | ||||
| 		Name:    keyInfo.GetName(), | ||||
| 		Type:    keyInfo.GetType().String(), | ||||
| 		Address: valAddr.String(), | ||||
| 		PubKey:  hex.EncodeToString(bytes), | ||||
| 	}, nil | ||||
| 	return NewKeyOutput( | ||||
| 		keyInfo.GetName(), | ||||
| 		keyInfo.GetType().String(), | ||||
| 		sdk.ValAddress(address.Bytes()).String(), | ||||
| 		getEthAddress(keyInfo), | ||||
| 		bechPubKey, | ||||
| 		hex.EncodeToString(keyInfo.GetPubKey().Bytes()), | ||||
| 	), nil | ||||
| } | ||||
| 
 | ||||
| // Bech32KeyOutput create a KeyOutput in with "acc" Bech32 prefixes.
 | ||||
| func Bech32KeyOutput(info cosmosKeys.Info) (cosmosKeys.KeyOutput, error) { | ||||
| 	accAddr := info.GetPubKey().Address() | ||||
| 	bytes := info.GetPubKey().Bytes() | ||||
| func Bech32KeyOutput(keyInfo cosmosKeys.Info) (KeyOutput, error) { | ||||
| 	address := keyInfo.GetPubKey().Address() | ||||
| 
 | ||||
| 	ko := cosmosKeys.KeyOutput{ | ||||
| 		Name:    info.GetName(), | ||||
| 		Type:    info.GetType().String(), | ||||
| 		Address: accAddr.String(), | ||||
| 		PubKey:  hex.EncodeToString(bytes), | ||||
| 	bechPubKey, err := sdk.Bech32ifyAccPub(keyInfo.GetPubKey()) | ||||
| 	if err != nil { | ||||
| 		return KeyOutput{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	return ko, nil | ||||
| 	return NewKeyOutput( | ||||
| 		keyInfo.GetName(), | ||||
| 		keyInfo.GetType().String(), | ||||
| 		sdk.AccAddress(address.Bytes()).String(), | ||||
| 		getEthAddress(keyInfo), | ||||
| 		bechPubKey, | ||||
| 		hex.EncodeToString(keyInfo.GetPubKey().Bytes()), | ||||
| 	), nil | ||||
| } | ||||
| 
 | ||||
| func getEthAddress(info cosmosKeys.Info) string { | ||||
| 	return info.GetPubKey().Address().String() | ||||
| } | ||||
|  | ||||
| @ -38,7 +38,7 @@ func (privkey PrivKeySecp256k1) PubKey() tmcrypto.PubKey { | ||||
| 
 | ||||
| // Bytes returns the raw ECDSA private key bytes.
 | ||||
| func (privkey PrivKeySecp256k1) Bytes() []byte { | ||||
| 	return privkey | ||||
| 	return cryptoCodec.MustMarshalBinaryBare(privkey) | ||||
| } | ||||
| 
 | ||||
| // Sign creates a recoverable ECDSA signature on the secp256k1 curve over the
 | ||||
| @ -59,7 +59,7 @@ func (privkey PrivKeySecp256k1) Equals(other tmcrypto.PrivKey) bool { | ||||
| 
 | ||||
| // ToECDSA returns the ECDSA private key as a reference to ecdsa.PrivateKey type.
 | ||||
| func (privkey PrivKeySecp256k1) ToECDSA() *ecdsa.PrivateKey { | ||||
| 	key, _ := ethcrypto.ToECDSA(privkey.Bytes()) | ||||
| 	key, _ := ethcrypto.ToECDSA(privkey) | ||||
| 	return key | ||||
| } | ||||
| 
 | ||||
| @ -80,7 +80,11 @@ func (key PubKeySecp256k1) Address() tmcrypto.Address { | ||||
| 
 | ||||
| // Bytes returns the raw bytes of the ECDSA public key.
 | ||||
| func (key PubKeySecp256k1) Bytes() []byte { | ||||
| 	return key | ||||
| 	bz, err := cryptoCodec.MarshalBinaryBare(key) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	return bz | ||||
| } | ||||
| 
 | ||||
| // VerifyBytes verifies that the ECDSA public key created a given signature over
 | ||||
| @ -93,7 +97,7 @@ func (key PubKeySecp256k1) VerifyBytes(msg []byte, sig []byte) bool { | ||||
| 	} | ||||
| 
 | ||||
| 	// the signature needs to be in [R || S] format when provided to VerifySignature
 | ||||
| 	return ethsecp256k1.VerifySignature(key.Bytes(), ethcrypto.Keccak256Hash(msg).Bytes(), sig) | ||||
| 	return ethsecp256k1.VerifySignature(key, ethcrypto.Keccak256Hash(msg).Bytes(), sig) | ||||
| } | ||||
| 
 | ||||
| // Equals returns true if two ECDSA public keys are equal and false otherwise.
 | ||||
|  | ||||
| @ -30,7 +30,7 @@ func TestPrivKeySecp256k1PrivKey(t *testing.T) { | ||||
| 	// validate we can sign some bytes
 | ||||
| 	msg := []byte("hello world") | ||||
| 	sigHash := ethcrypto.Keccak256Hash(msg) | ||||
| 	expectedSig, _ := ethsecp256k1.Sign(sigHash.Bytes(), privKey.Bytes()) | ||||
| 	expectedSig, _ := ethsecp256k1.Sign(sigHash.Bytes(), privKey) | ||||
| 
 | ||||
| 	sig, err := privKey.Sign(msg) | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							| @ -43,6 +43,7 @@ require ( | ||||
| 	github.com/steakknife/bloomfilter v0.0.0-20180922174646-6819c0d2a570 // indirect | ||||
| 	github.com/steakknife/hamming v0.0.0-20180906055917-c99c65617cd3 // indirect | ||||
| 	github.com/stretchr/testify v1.3.0 | ||||
| 	github.com/tendermint/crypto v0.0.0-20180820045704-3764759f34a5 | ||||
| 	github.com/tendermint/go-amino v0.15.0 | ||||
| 	github.com/tendermint/tendermint v0.32.2 | ||||
| 	github.com/tendermint/tm-db v0.1.1 | ||||
|  | ||||
| @ -90,7 +90,7 @@ func runAddCmd(cmd *cobra.Command, args []string) error { | ||||
| 	interactive := viper.GetBool(flagInteractive) | ||||
| 	showMnemonic := !viper.GetBool(flagNoBackup) | ||||
| 
 | ||||
| 	kb, err = clientkeys.NewKeyBaseFromHomeFlag() | ||||
| 	kb, err = NewKeyBaseFromHomeFlag() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| @ -7,7 +7,6 @@ import ( | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 
 | ||||
| 	"github.com/cosmos/cosmos-sdk/client/flags" | ||||
| 	clientkeys "github.com/cosmos/cosmos-sdk/client/keys" | ||||
| 	"github.com/cosmos/cosmos-sdk/tests" | ||||
| ) | ||||
| 
 | ||||
| @ -32,7 +31,7 @@ func TestRunShowCmd(t *testing.T) { | ||||
| 
 | ||||
| 	fakeKeyName1 := "runShowCmd_Key1" | ||||
| 	fakeKeyName2 := "runShowCmd_Key2" | ||||
| 	kb, err := clientkeys.NewKeyBaseFromHomeFlag() | ||||
| 	kb, err := NewKeyBaseFromHomeFlag() | ||||
| 	assert.NoError(t, err) | ||||
| 	_, err = kb.CreateAccount(fakeKeyName1, tests.TestMnemonic, "", "", 0, 0) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| @ -2,28 +2,33 @@ package keys | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"path/filepath" | ||||
| 
 | ||||
| 	"github.com/spf13/viper" | ||||
| 	"github.com/tendermint/tendermint/libs/cli" | ||||
| 	"gopkg.in/yaml.v2" | ||||
| 
 | ||||
| 	"github.com/cosmos/cosmos-sdk/client/flags" | ||||
| 	clientkeys "github.com/cosmos/cosmos-sdk/client/keys" | ||||
| 
 | ||||
| 	"github.com/cosmos/cosmos-sdk/client/flags" | ||||
| 	cosmosKeys "github.com/cosmos/cosmos-sdk/crypto/keys" | ||||
| 	emintKeys "github.com/cosmos/ethermint/crypto/keys" | ||||
| ) | ||||
| 
 | ||||
| // available output formats.
 | ||||
| const ( | ||||
| 	OutputFormatText = "text" | ||||
| 	OutputFormatJSON = "json" | ||||
| 
 | ||||
| 	defaultKeyDBName = "emintkeys" | ||||
| ) | ||||
| 
 | ||||
| type bechKeyOutFn func(keyInfo cosmosKeys.Info) (cosmosKeys.KeyOutput, error) | ||||
| type bechKeyOutFn func(keyInfo cosmosKeys.Info) (emintKeys.KeyOutput, error) | ||||
| 
 | ||||
| // GetKeyInfo returns key info for a given name. An error is returned if the
 | ||||
| // keybase cannot be retrieved or getting the info fails.
 | ||||
| func GetKeyInfo(name string) (cosmosKeys.Info, error) { | ||||
| 	keybase, err := clientkeys.NewKeyBaseFromHomeFlag() | ||||
| 	keybase, err := NewKeyBaseFromHomeFlag() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| @ -31,6 +36,47 @@ func GetKeyInfo(name string) (cosmosKeys.Info, error) { | ||||
| 	return keybase.Get(name) | ||||
| } | ||||
| 
 | ||||
| // NewKeyBaseFromHomeFlag initializes a Keybase based on the configuration.
 | ||||
| func NewKeyBaseFromHomeFlag() (cosmosKeys.Keybase, error) { | ||||
| 	rootDir := viper.GetString(flags.FlagHome) | ||||
| 	return NewKeyBaseFromDir(rootDir) | ||||
| } | ||||
| 
 | ||||
| // NewKeyBaseFromDir initializes a keybase at a particular dir.
 | ||||
| func NewKeyBaseFromDir(rootDir string) (cosmosKeys.Keybase, error) { | ||||
| 	return getLazyKeyBaseFromDir(rootDir) | ||||
| } | ||||
| 
 | ||||
| // NewInMemoryKeyBase returns a storage-less keybase.
 | ||||
| func NewInMemoryKeyBase() cosmosKeys.Keybase { return emintKeys.NewInMemory() } | ||||
| 
 | ||||
| func getLazyKeyBaseFromDir(rootDir string) (cosmosKeys.Keybase, error) { | ||||
| 	return emintKeys.New(defaultKeyDBName, filepath.Join(rootDir, defaultKeyDBName)), nil | ||||
| } | ||||
| 
 | ||||
| // GetPassphrase returns a passphrase for a given name. It will first retrieve
 | ||||
| // the key info for that name if the type is local, it'll fetch input from
 | ||||
| // STDIN. Otherwise, an empty passphrase is returned. An error is returned if
 | ||||
| // the key info cannot be fetched or reading from STDIN fails.
 | ||||
| func GetPassphrase(name string) (string, error) { | ||||
| 	var passphrase string | ||||
| 
 | ||||
| 	keyInfo, err := GetKeyInfo(name) | ||||
| 	if err != nil { | ||||
| 		return passphrase, err | ||||
| 	} | ||||
| 
 | ||||
| 	// we only need a passphrase for locally stored keys
 | ||||
| 	if keyInfo.GetType().String() == emintKeys.TypeLocal.String() { | ||||
| 		passphrase, err = clientkeys.ReadPassphraseFromStdin(name) | ||||
| 		if err != nil { | ||||
| 			return passphrase, err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return passphrase, nil | ||||
| } | ||||
| 
 | ||||
| func printKeyInfo(keyInfo cosmosKeys.Info, bechKeyOut bechKeyOutFn) { | ||||
| 	ko, err := bechKeyOut(keyInfo) | ||||
| 	if err != nil { | ||||
| @ -39,7 +85,7 @@ func printKeyInfo(keyInfo cosmosKeys.Info, bechKeyOut bechKeyOutFn) { | ||||
| 
 | ||||
| 	switch viper.Get(cli.OutputFlag) { | ||||
| 	case OutputFormatText: | ||||
| 		printTextInfos([]cosmosKeys.KeyOutput{ko}) | ||||
| 		printTextInfos([]emintKeys.KeyOutput{ko}) | ||||
| 
 | ||||
| 	case OutputFormatJSON: | ||||
| 		var out []byte | ||||
| @ -84,7 +130,7 @@ func printKeyInfo(keyInfo cosmosKeys.Info, bechKeyOut bechKeyOutFn) { | ||||
| // 	}
 | ||||
| // }
 | ||||
| 
 | ||||
| func printTextInfos(kos []cosmosKeys.KeyOutput) { | ||||
| func printTextInfos(kos []emintKeys.KeyOutput) { | ||||
| 	out, err := yaml.Marshal(&kos) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
|  | ||||
							
								
								
									
										135
									
								
								x/evm/client/cli/tx.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								x/evm/client/cli/tx.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,135 @@ | ||||
| package cli | ||||
| 
 | ||||
| import ( | ||||
| 	"math/big" | ||||
| 	"strconv" | ||||
| 
 | ||||
| 	"github.com/spf13/cobra" | ||||
| 
 | ||||
| 	"github.com/cosmos/cosmos-sdk/client" | ||||
| 	"github.com/cosmos/cosmos-sdk/codec" | ||||
| 	sdk "github.com/cosmos/cosmos-sdk/types" | ||||
| 	"github.com/cosmos/cosmos-sdk/x/auth" | ||||
| 	"github.com/cosmos/cosmos-sdk/x/auth/client/utils" | ||||
| 	emintkeys "github.com/cosmos/ethermint/keys" | ||||
| 	emintTypes "github.com/cosmos/ethermint/types" | ||||
| 	emintUtils "github.com/cosmos/ethermint/x/evm/client/utils" | ||||
| 	"github.com/cosmos/ethermint/x/evm/types" | ||||
| 
 | ||||
| 	ethcmn "github.com/ethereum/go-ethereum/common" | ||||
| ) | ||||
| 
 | ||||
| // GetTxCmd defines the CLI commands regarding evm module transactions
 | ||||
| func GetTxCmd(storeKey string, cdc *codec.Codec) *cobra.Command { | ||||
| 	evmTxCmd := &cobra.Command{ | ||||
| 		Use:                        types.ModuleName, | ||||
| 		Short:                      "EVM transaction subcommands", | ||||
| 		DisableFlagParsing:         true, | ||||
| 		SuggestionsMinimumDistance: 2, | ||||
| 		RunE:                       client.ValidateCmd, | ||||
| 	} | ||||
| 
 | ||||
| 	evmTxCmd.AddCommand(client.PostCommands( | ||||
| 		// TODO: Add back generating cosmos tx for Ethereum tx message
 | ||||
| 		// GetCmdGenTx(cdc),
 | ||||
| 		GetCmdGenETHTx(cdc), | ||||
| 	)...) | ||||
| 
 | ||||
| 	return evmTxCmd | ||||
| } | ||||
| 
 | ||||
| // GetCmdGenTx generates an ethereum transaction wrapped in a Cosmos standard transaction
 | ||||
| func GetCmdGenTx(cdc *codec.Codec) *cobra.Command { | ||||
| 	return &cobra.Command{ | ||||
| 		Use:   "generate-tx [ethaddress] [amount] [gaslimit] [gasprice] [payload]", | ||||
| 		Short: "generate eth tx wrapped in a Cosmos Standard tx", | ||||
| 		Args:  cobra.ExactArgs(5), | ||||
| 		RunE: func(cmd *cobra.Command, args []string) error { | ||||
| 			// TODO: remove inputs and infer based on StdTx
 | ||||
| 			cliCtx := emintUtils.NewETHCLIContext().WithCodec(cdc) | ||||
| 
 | ||||
| 			txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) | ||||
| 
 | ||||
| 			kb, err := emintkeys.NewKeyBaseFromHomeFlag() | ||||
| 			if err != nil { | ||||
| 				panic(err) | ||||
| 			} | ||||
| 
 | ||||
| 			coins, err := sdk.ParseCoins(args[1]) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			gasLimit, err := strconv.ParseUint(args[2], 0, 64) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			gasPrice, err := strconv.ParseUint(args[3], 0, 64) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			payload := args[4] | ||||
| 
 | ||||
| 			// TODO: Remove explicit photon check and check variables
 | ||||
| 			msg := types.NewEthereumTxMsg(0, ethcmn.HexToAddress(args[0]), big.NewInt(coins.AmountOf(emintTypes.DenomDefault).Int64()), gasLimit, new(big.Int).SetUint64(gasPrice), []byte(payload)) | ||||
| 			err = msg.ValidateBasic() | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			// TODO: possibly overwrite gas values in txBldr
 | ||||
| 			return emintUtils.GenerateOrBroadcastETHMsgs(cliCtx, txBldr.WithKeybase(kb), []sdk.Msg{msg}) | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // GetCmdGenTx generates an ethereum transaction
 | ||||
| func GetCmdGenETHTx(cdc *codec.Codec) *cobra.Command { | ||||
| 	return &cobra.Command{ | ||||
| 		Use:   "generate-eth-tx [nonce] [ethaddress] [amount] [gaslimit] [gasprice] [payload]", | ||||
| 		Short: "geberate and broadcast an Ethereum tx", | ||||
| 		Args:  cobra.ExactArgs(6), | ||||
| 		RunE: func(cmd *cobra.Command, args []string) error { | ||||
| 			cliCtx := emintUtils.NewETHCLIContext().WithCodec(cdc) | ||||
| 
 | ||||
| 			txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) | ||||
| 
 | ||||
| 			kb, err := emintkeys.NewKeyBaseFromHomeFlag() | ||||
| 			if err != nil { | ||||
| 				panic(err) | ||||
| 			} | ||||
| 
 | ||||
| 			nonce, err := strconv.ParseUint(args[0], 0, 64) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			coins, err := sdk.ParseCoins(args[2]) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			gasLimit, err := strconv.ParseUint(args[3], 0, 64) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			gasPrice, err := strconv.ParseUint(args[4], 0, 64) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			payload := args[5] | ||||
| 
 | ||||
| 			tx := types.NewEthereumTxMsg(nonce, ethcmn.HexToAddress(args[1]), big.NewInt(coins.AmountOf(emintTypes.DenomDefault).Int64()), gasLimit, new(big.Int).SetUint64(gasPrice), []byte(payload)) | ||||
| 			err = tx.ValidateBasic() | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			return emintUtils.BroadcastETHTx(cliCtx, txBldr.WithSequence(nonce).WithKeybase(kb), tx) | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										348
									
								
								x/evm/client/utils/tx.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										348
									
								
								x/evm/client/utils/tx.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,348 @@ | ||||
| package utils | ||||
| 
 | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"fmt" | ||||
| 	"math/big" | ||||
| 	"os" | ||||
| 
 | ||||
| 	"github.com/pkg/errors" | ||||
| 	"github.com/spf13/viper" | ||||
| 
 | ||||
| 	"github.com/cosmos/cosmos-sdk/client/context" | ||||
| 	"github.com/cosmos/cosmos-sdk/client/flags" | ||||
| 	"github.com/cosmos/cosmos-sdk/client/input" | ||||
| 	crkeys "github.com/cosmos/cosmos-sdk/crypto/keys" | ||||
| 	sdk "github.com/cosmos/cosmos-sdk/types" | ||||
| 	"github.com/cosmos/cosmos-sdk/x/auth/client/utils" | ||||
| 	authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" | ||||
| 	emintcrypto "github.com/cosmos/ethermint/crypto" | ||||
| 	emintkeys "github.com/cosmos/ethermint/keys" | ||||
| 	emint "github.com/cosmos/ethermint/types" | ||||
| 	evmtypes "github.com/cosmos/ethermint/x/evm/types" | ||||
| 
 | ||||
| 	"github.com/tendermint/tendermint/libs/cli" | ||||
| 	rpcclient "github.com/tendermint/tendermint/rpc/client" | ||||
| ) | ||||
| 
 | ||||
| // * Code from this file is a modified version of cosmos-sdk/auth/client/utils/tx.go
 | ||||
| // * to allow for using the Ethermint keybase for signing the transaction
 | ||||
| 
 | ||||
| // GenerateOrBroadcastMsgs creates a StdTx given a series of messages. If
 | ||||
| // the provided context has generate-only enabled, the tx will only be printed
 | ||||
| // to STDOUT in a fully offline manner. Otherwise, the tx will be signed and
 | ||||
| // broadcasted.
 | ||||
| func GenerateOrBroadcastETHMsgs(cliCtx context.CLIContext, txBldr authtypes.TxBuilder, msgs []sdk.Msg) error { | ||||
| 	if cliCtx.GenerateOnly { | ||||
| 		return utils.PrintUnsignedStdTx(txBldr, cliCtx, msgs) | ||||
| 	} | ||||
| 
 | ||||
| 	return completeAndBroadcastETHTxCLI(txBldr, cliCtx, msgs) | ||||
| } | ||||
| 
 | ||||
| // BroadcastETHTx Broadcasts an Ethereum Tx not wrapped in a Std Tx
 | ||||
| func BroadcastETHTx(cliCtx context.CLIContext, txBldr authtypes.TxBuilder, tx *evmtypes.EthereumTxMsg) error { | ||||
| 	txBldr, err := utils.PrepareTxBuilder(txBldr, cliCtx) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	fromName := cliCtx.GetFromName() | ||||
| 
 | ||||
| 	passphrase, err := emintkeys.GetPassphrase(fromName) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Sign V, R, S fields for tx
 | ||||
| 	ethTx, err := signEthTx(txBldr.Keybase(), fromName, passphrase, tx, txBldr.ChainID()) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Use default Tx Encoder since it will just be broadcasted to TM node at this point
 | ||||
| 	txEncoder := txBldr.TxEncoder() | ||||
| 
 | ||||
| 	txBytes, err := txEncoder(ethTx) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// broadcast to a Tendermint node
 | ||||
| 	res, err := cliCtx.BroadcastTx(txBytes) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	return cliCtx.PrintOutput(res) | ||||
| } | ||||
| 
 | ||||
| // completeAndBroadcastETHTxCLI implements a utility function that facilitates
 | ||||
| // sending a series of messages in a signed transaction given a TxBuilder and a
 | ||||
| // QueryContext. It ensures that the account exists, has a proper number and
 | ||||
| // sequence set. In addition, it builds and signs a transaction with the
 | ||||
| // supplied messages. Finally, it broadcasts the signed transaction to a node.
 | ||||
| // * Modified version from github.com/cosmos/cosmos-sdk/x/auth/client/utils/tx.go
 | ||||
| func completeAndBroadcastETHTxCLI(txBldr authtypes.TxBuilder, cliCtx context.CLIContext, msgs []sdk.Msg) error { | ||||
| 	txBldr, err := utils.PrepareTxBuilder(txBldr, cliCtx) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	fromName := cliCtx.GetFromName() | ||||
| 
 | ||||
| 	if txBldr.SimulateAndExecute() || cliCtx.Simulate { | ||||
| 		txBldr, err = utils.EnrichWithGas(txBldr, cliCtx, msgs) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		gasEst := utils.GasEstimateResponse{GasEstimate: txBldr.Gas()} | ||||
| 		_, _ = fmt.Fprintf(os.Stderr, "%s\n", gasEst.String()) | ||||
| 	} | ||||
| 
 | ||||
| 	if cliCtx.Simulate { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	if !cliCtx.SkipConfirm { | ||||
| 		stdSignMsg, err := txBldr.BuildSignMsg(msgs) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		var json []byte | ||||
| 		if viper.GetBool(flags.FlagIndentResponse) { | ||||
| 			json, err = cliCtx.Codec.MarshalJSONIndent(stdSignMsg, "", "  ") | ||||
| 			if err != nil { | ||||
| 				panic(err) | ||||
| 			} | ||||
| 		} else { | ||||
| 			json = cliCtx.Codec.MustMarshalJSON(stdSignMsg) | ||||
| 		} | ||||
| 
 | ||||
| 		_, _ = fmt.Fprintf(os.Stderr, "%s\n\n", json) | ||||
| 
 | ||||
| 		buf := bufio.NewReader(os.Stdin) | ||||
| 		ok, err := input.GetConfirmation("confirm transaction before signing and broadcasting", buf) | ||||
| 		if err != nil || !ok { | ||||
| 			_, _ = fmt.Fprintf(os.Stderr, "%s\n", "cancelled transaction") | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// * This function is overriden to change the keybase reference here
 | ||||
| 	passphrase, err := emintkeys.GetPassphrase(fromName) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// build and sign the transaction
 | ||||
| 	// * needed to be modified also to change how the data is signed
 | ||||
| 	txBytes, err := buildAndSign(txBldr, fromName, passphrase, msgs) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// broadcast to a Tendermint node
 | ||||
| 	res, err := cliCtx.BroadcastTx(txBytes) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	return cliCtx.PrintOutput(res) | ||||
| } | ||||
| 
 | ||||
| // BuildAndSign builds a single message to be signed, and signs a transaction
 | ||||
| // with the built message given a name, passphrase, and a set of messages.
 | ||||
| // * overriden from github.com/cosmos/cosmos-sdk/x/auth/types/txbuilder.go
 | ||||
| // * This is just modified to change the functionality in makeSignature, through sign
 | ||||
| func buildAndSign(bldr authtypes.TxBuilder, name, passphrase string, msgs []sdk.Msg) ([]byte, error) { | ||||
| 	msg, err := bldr.BuildSignMsg(msgs) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return sign(bldr, name, passphrase, msg) | ||||
| } | ||||
| 
 | ||||
| // Sign signs a transaction given a name, passphrase, and a single message to
 | ||||
| // signed. An error is returned if signing fails.
 | ||||
| func sign(bldr authtypes.TxBuilder, name, passphrase string, msg authtypes.StdSignMsg) ([]byte, error) { | ||||
| 	sig, err := makeSignature(bldr.Keybase(), name, passphrase, msg) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	txEncoder := bldr.TxEncoder() | ||||
| 
 | ||||
| 	return txEncoder(authtypes.NewStdTx(msg.Msgs, msg.Fee, []authtypes.StdSignature{sig}, msg.Memo)) | ||||
| } | ||||
| 
 | ||||
| // MakeSignature builds a StdSignature given keybase, key name, passphrase, and a StdSignMsg.
 | ||||
| func makeSignature(keybase crkeys.Keybase, name, passphrase string, | ||||
| 	msg authtypes.StdSignMsg) (sig authtypes.StdSignature, err error) { | ||||
| 	if keybase == nil { | ||||
| 		// * This is overriden to allow ethermint keys, but not used because keybase is set
 | ||||
| 		keybase, err = emintkeys.NewKeyBaseFromHomeFlag() | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// EthereumTxMsg always returns the data in the 0th index so it is safe to do this
 | ||||
| 	var ethTx *evmtypes.EthereumTxMsg | ||||
| 	ethTx, ok := msg.Msgs[0].(*evmtypes.EthereumTxMsg) | ||||
| 	if !ok { | ||||
| 		return sig, fmt.Errorf("Transaction message not an Ethereum Tx") | ||||
| 	} | ||||
| 
 | ||||
| 	// TODO: Move this logic to after tx is rlp decoded in keybase Sign function
 | ||||
| 	// parse the chainID from a string to a base-10 integer
 | ||||
| 	chainID, ok := new(big.Int).SetString(msg.ChainID, 10) | ||||
| 	if !ok { | ||||
| 		return sig, emint.ErrInvalidChainID(fmt.Sprintf("invalid chainID: %s", msg.ChainID)) | ||||
| 	} | ||||
| 
 | ||||
| 	privKey, err := keybase.ExportPrivateKeyObject(name, passphrase) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	emintKey, ok := privKey.(emintcrypto.PrivKeySecp256k1) | ||||
| 	if !ok { | ||||
| 		panic(fmt.Sprintf("invalid private key type: %T", privKey)) | ||||
| 	} | ||||
| 
 | ||||
| 	ethTx.Sign(chainID, emintKey.ToECDSA()) | ||||
| 
 | ||||
| 	// * This is needed to be overriden to get bytes to sign (RLPSignBytes) with the chainID
 | ||||
| 	sigBytes, pubkey, err := keybase.Sign(name, passphrase, ethTx.RLPSignBytes(chainID).Bytes()) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	return authtypes.StdSignature{ | ||||
| 		PubKey:    pubkey, | ||||
| 		Signature: sigBytes, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // signEthTx populates the V, R, and S fields of an EthereumTxMsg using an ethermint key
 | ||||
| func signEthTx(keybase crkeys.Keybase, name, passphrase string, | ||||
| 	ethTx *evmtypes.EthereumTxMsg, chainID string) (_ *evmtypes.EthereumTxMsg, err error) { | ||||
| 	if keybase == nil { | ||||
| 		keybase, err = emintkeys.NewKeyBaseFromHomeFlag() | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// parse the chainID from a string to a base-10 integer
 | ||||
| 	intChainID, ok := new(big.Int).SetString(chainID, 10) | ||||
| 	if !ok { | ||||
| 		return ethTx, emint.ErrInvalidChainID(fmt.Sprintf("invalid chainID: %s", chainID)) | ||||
| 	} | ||||
| 
 | ||||
| 	privKey, err := keybase.ExportPrivateKeyObject(name, passphrase) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// Key must be a ethermint key to be able to be converted into an ECDSA private key to sign
 | ||||
| 	emintKey, ok := privKey.(emintcrypto.PrivKeySecp256k1) | ||||
| 	if !ok { | ||||
| 		panic(fmt.Sprintf("invalid private key type: %T", privKey)) | ||||
| 	} | ||||
| 
 | ||||
| 	ethTx.Sign(intChainID, emintKey.ToECDSA()) | ||||
| 
 | ||||
| 	return ethTx, err | ||||
| } | ||||
| 
 | ||||
| // * This context is needed because the previous GetFromFields function would initialize a
 | ||||
| // * default keybase to lookup the address or name. The new one overrides the keybase with the
 | ||||
| // * ethereum compatible one
 | ||||
| 
 | ||||
| // NewCLIContextWithFrom returns a new initialized CLIContext with parameters from the
 | ||||
| // command line using Viper. It takes a key name or address and populates the FromName and
 | ||||
| // FromAddress field accordingly.
 | ||||
| func NewETHCLIContext() context.CLIContext { | ||||
| 	var nodeURI string | ||||
| 	var rpc rpcclient.Client | ||||
| 
 | ||||
| 	from := viper.GetString(flags.FlagFrom) | ||||
| 
 | ||||
| 	genOnly := viper.GetBool(flags.FlagGenerateOnly) | ||||
| 
 | ||||
| 	// * This function is needed only to override this call to access correct keybase
 | ||||
| 	fromAddress, fromName, err := getFromFields(from, genOnly) | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("failed to get from fields: %v", err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	if !genOnly { | ||||
| 		nodeURI = viper.GetString(flags.FlagNode) | ||||
| 		if nodeURI != "" { | ||||
| 			rpc = rpcclient.NewHTTP(nodeURI, "/websocket") | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return context.CLIContext{ | ||||
| 		Client:        rpc, | ||||
| 		Output:        os.Stdout, | ||||
| 		NodeURI:       nodeURI, | ||||
| 		From:          viper.GetString(flags.FlagFrom), | ||||
| 		OutputFormat:  viper.GetString(cli.OutputFlag), | ||||
| 		Height:        viper.GetInt64(flags.FlagHeight), | ||||
| 		TrustNode:     viper.GetBool(flags.FlagTrustNode), | ||||
| 		UseLedger:     viper.GetBool(flags.FlagUseLedger), | ||||
| 		BroadcastMode: viper.GetString(flags.FlagBroadcastMode), | ||||
| 		// Verifier:      verifier,
 | ||||
| 		Simulate:     viper.GetBool(flags.FlagDryRun), | ||||
| 		GenerateOnly: genOnly, | ||||
| 		FromAddress:  fromAddress, | ||||
| 		FromName:     fromName, | ||||
| 		Indent:       viper.GetBool(flags.FlagIndentResponse), | ||||
| 		SkipConfirm:  viper.GetBool(flags.FlagSkipConfirmation), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // GetFromFields returns a from account address and Keybase name given either
 | ||||
| // an address or key name. If genOnly is true, only a valid Bech32 cosmos
 | ||||
| // address is returned.
 | ||||
| func getFromFields(from string, genOnly bool) (sdk.AccAddress, string, error) { | ||||
| 	if from == "" { | ||||
| 		return nil, "", nil | ||||
| 	} | ||||
| 
 | ||||
| 	if genOnly { | ||||
| 		addr, err := sdk.AccAddressFromBech32(from) | ||||
| 		if err != nil { | ||||
| 			return nil, "", errors.Wrap(err, "must provide a valid Bech32 address for generate-only") | ||||
| 		} | ||||
| 
 | ||||
| 		return addr, "", nil | ||||
| 	} | ||||
| 
 | ||||
| 	// * This is the line that needed to be overriden, change could be to pass in optional keybase?
 | ||||
| 	keybase, err := emintkeys.NewKeyBaseFromHomeFlag() | ||||
| 	if err != nil { | ||||
| 		return nil, "", err | ||||
| 	} | ||||
| 
 | ||||
| 	var info crkeys.Info | ||||
| 	if addr, err := sdk.AccAddressFromBech32(from); err == nil { | ||||
| 		info, err = keybase.GetByAddress(addr) | ||||
| 		if err != nil { | ||||
| 			return nil, "", err | ||||
| 		} | ||||
| 	} else { | ||||
| 		info, err = keybase.Get(from) | ||||
| 		if err != nil { | ||||
| 			return nil, "", err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return info.GetAddress(), info.GetName(), nil | ||||
| } | ||||
| @ -55,7 +55,7 @@ func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command { | ||||
| 
 | ||||
| // Get the root tx command of this module
 | ||||
| func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command { | ||||
| 	return nil // cli.GetTxCmd(StoreKey, cdc)
 | ||||
| 	return cli.GetTxCmd(types.ModuleName, cdc) | ||||
| } | ||||
| 
 | ||||
| type AppModule struct { | ||||
|  | ||||
| @ -28,7 +28,7 @@ var big8 = big.NewInt(8) | ||||
| // message type and route constants
 | ||||
| const ( | ||||
| 	TypeEthereumTxMsg  = "ethereum_tx" | ||||
| 	RouteEthereumTxMsg = "evm" | ||||
| 	RouteEthereumTxMsg = RouterKey | ||||
| ) | ||||
| 
 | ||||
| // EthereumTxMsg encapsulates an Ethereum transaction as an SDK message.
 | ||||
| @ -128,11 +128,11 @@ func (msg EthereumTxMsg) Type() string { return TypeEthereumTxMsg } | ||||
| // checks of a Transaction. If returns an sdk.Error if validation fails.
 | ||||
| func (msg EthereumTxMsg) ValidateBasic() sdk.Error { | ||||
| 	if msg.Data.Price.Sign() != 1 { | ||||
| 		return types.ErrInvalidValue("price must be positive") | ||||
| 		return types.ErrInvalidValue(fmt.Sprintf("Price must be positive: %x", msg.Data.Price)) | ||||
| 	} | ||||
| 
 | ||||
| 	if msg.Data.Amount.Sign() != 1 { | ||||
| 		return types.ErrInvalidValue("amount must be positive") | ||||
| 		return types.ErrInvalidValue(fmt.Sprintf("amount must be positive: %x", msg.Data.Amount)) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
|  | ||||
							
								
								
									
										153
									
								
								x/evm/types/msg_encoding.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								x/evm/types/msg_encoding.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,153 @@ | ||||
| package types | ||||
| 
 | ||||
| import ( | ||||
| 	"math/big" | ||||
| 
 | ||||
| 	"github.com/cosmos/cosmos-sdk/codec" | ||||
| 
 | ||||
| 	ethcmn "github.com/ethereum/go-ethereum/common" | ||||
| ) | ||||
| 
 | ||||
| var cdc = codec.New() | ||||
| 
 | ||||
| func init() { | ||||
| 	RegisterAmino(cdc) | ||||
| } | ||||
| 
 | ||||
| // RegisterAmino registers all crypto related types in the given (amino) codec.
 | ||||
| func RegisterAmino(cdc *codec.Codec) { | ||||
| 	cdc.RegisterConcrete(EncodableTxData{}, "ethermint/EncodedMessage", nil) | ||||
| } | ||||
| 
 | ||||
| // TxData implements the Ethereum transaction data structure. It is used
 | ||||
| // solely as intended in Ethereum abiding by the protocol.
 | ||||
| type EncodableTxData struct { | ||||
| 	AccountNonce uint64          `json:"nonce"` | ||||
| 	Price        string          `json:"gasPrice"` | ||||
| 	GasLimit     uint64          `json:"gas"` | ||||
| 	Recipient    *ethcmn.Address `json:"to" rlp:"nil"` // nil means contract creation
 | ||||
| 	Amount       string          `json:"value"` | ||||
| 	Payload      []byte          `json:"input"` | ||||
| 
 | ||||
| 	// signature values
 | ||||
| 	V string `json:"v"` | ||||
| 	R string `json:"r"` | ||||
| 	S string `json:"s"` | ||||
| 
 | ||||
| 	// hash is only used when marshaling to JSON
 | ||||
| 	Hash *ethcmn.Hash `json:"hash" rlp:"-"` | ||||
| } | ||||
| 
 | ||||
| func marshalAmino(td EncodableTxData) (string, error) { | ||||
| 	bz, err := cdc.MarshalBinaryBare(td) | ||||
| 	return string(bz), err | ||||
| } | ||||
| 
 | ||||
| func unmarshalAmino(td *EncodableTxData, text string) (err error) { | ||||
| 	return cdc.UnmarshalBinaryBare([]byte(text), td) | ||||
| } | ||||
| 
 | ||||
| func marshalBigInt(i *big.Int) string { | ||||
| 	bz, err := i.MarshalText() | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	return string(bz) | ||||
| } | ||||
| 
 | ||||
| func unmarshalBigInt(s string) (*big.Int, error) { | ||||
| 	ret := new(big.Int) | ||||
| 	err := ret.UnmarshalText([]byte(s)) | ||||
| 	return ret, err | ||||
| } | ||||
| 
 | ||||
| // MarshalAmino defines custom encoding scheme for TxData
 | ||||
| func (td TxData) MarshalAmino() (string, error) { | ||||
| 	e := EncodableTxData{ | ||||
| 		AccountNonce: td.AccountNonce, | ||||
| 		Price:        marshalBigInt(td.Price), | ||||
| 		GasLimit:     td.GasLimit, | ||||
| 		Recipient:    td.Recipient, | ||||
| 		Amount:       marshalBigInt(td.Amount), | ||||
| 		Payload:      td.Payload, | ||||
| 
 | ||||
| 		V: marshalBigInt(td.V), | ||||
| 		R: marshalBigInt(td.R), | ||||
| 		S: marshalBigInt(td.S), | ||||
| 
 | ||||
| 		Hash: td.Hash, | ||||
| 	} | ||||
| 
 | ||||
| 	return marshalAmino(e) | ||||
| } | ||||
| 
 | ||||
| // UnmarshalAmino defines custom decoding scheme for TxData
 | ||||
| func (td *TxData) UnmarshalAmino(text string) (err error) { | ||||
| 	e := new(EncodableTxData) | ||||
| 	err = unmarshalAmino(e, text) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	td.AccountNonce = e.AccountNonce | ||||
| 	td.GasLimit = e.GasLimit | ||||
| 	td.Recipient = e.Recipient | ||||
| 	td.Payload = e.Payload | ||||
| 	td.Hash = e.Hash | ||||
| 
 | ||||
| 	price, err := unmarshalBigInt(e.Price) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	if td.Price != nil { | ||||
| 		td.Price.Set(price) | ||||
| 	} else { | ||||
| 		td.Price = price | ||||
| 	} | ||||
| 
 | ||||
| 	amt, err := unmarshalBigInt(e.Amount) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	if td.Amount != nil { | ||||
| 		td.Amount.Set(amt) | ||||
| 	} else { | ||||
| 		td.Amount = amt | ||||
| 	} | ||||
| 
 | ||||
| 	v, err := unmarshalBigInt(e.V) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	if td.V != nil { | ||||
| 		td.V.Set(v) | ||||
| 	} else { | ||||
| 		td.V = v | ||||
| 	} | ||||
| 
 | ||||
| 	r, err := unmarshalBigInt(e.R) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	if td.R != nil { | ||||
| 		td.R.Set(r) | ||||
| 	} else { | ||||
| 		td.R = r | ||||
| 	} | ||||
| 
 | ||||
| 	s, err := unmarshalBigInt(e.S) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	if td.S != nil { | ||||
| 		td.S.Set(s) | ||||
| 	} else { | ||||
| 		td.S = s | ||||
| 	} | ||||
| 
 | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| // TODO: Implement JSON marshaling/ unmarshaling for this type
 | ||||
| 
 | ||||
| // TODO: Implement YAML marshaling/ unmarshaling for this type
 | ||||
| @ -122,7 +122,11 @@ func TestMsgEthereumTxSig(t *testing.T) { | ||||
| 
 | ||||
| func TestMsgEthereumTxAmino(t *testing.T) { | ||||
| 	addr := GenerateEthAddress() | ||||
| 	msg := NewEthereumTxMsg(0, addr, nil, 100000, nil, []byte("test")) | ||||
| 	msg := NewEthereumTxMsg(5, addr, big.NewInt(1), 100000, big.NewInt(3), []byte("test")) | ||||
| 
 | ||||
| 	msg.Data.V = big.NewInt(1) | ||||
| 	msg.Data.R = big.NewInt(2) | ||||
| 	msg.Data.S = big.NewInt(3) | ||||
| 
 | ||||
| 	raw, err := ModuleCdc.MarshalBinaryBare(msg) | ||||
| 	require.NoError(t, err) | ||||
| @ -133,3 +137,39 @@ func TestMsgEthereumTxAmino(t *testing.T) { | ||||
| 	require.NoError(t, err) | ||||
| 	require.Equal(t, msg.Data, msg2.Data) | ||||
| } | ||||
| 
 | ||||
| func TestMarshalAndUnmarshalInt(t *testing.T) { | ||||
| 	i := big.NewInt(3) | ||||
| 	m := marshalBigInt(i) | ||||
| 	i2, err := unmarshalBigInt(m) | ||||
| 	require.NoError(t, err) | ||||
| 
 | ||||
| 	require.Equal(t, i, i2) | ||||
| } | ||||
| 
 | ||||
| func TestMarshalAndUnmarshalData(t *testing.T) { | ||||
| 	addr := GenerateEthAddress() | ||||
| 	hash := ethcmn.BigToHash(big.NewInt(2)) | ||||
| 	e := EncodableTxData{ | ||||
| 		AccountNonce: 2, | ||||
| 		Price:        marshalBigInt(big.NewInt(3)), | ||||
| 		GasLimit:     1, | ||||
| 		Recipient:    &addr, | ||||
| 		Amount:       marshalBigInt(big.NewInt(4)), | ||||
| 		Payload:      []byte("test"), | ||||
| 
 | ||||
| 		V: marshalBigInt(big.NewInt(5)), | ||||
| 		R: marshalBigInt(big.NewInt(6)), | ||||
| 		S: marshalBigInt(big.NewInt(7)), | ||||
| 
 | ||||
| 		Hash: &hash, | ||||
| 	} | ||||
| 	str, err := marshalAmino(e) | ||||
| 	require.NoError(t, err) | ||||
| 
 | ||||
| 	e2 := new(EncodableTxData) | ||||
| 
 | ||||
| 	err = unmarshalAmino(e2, str) | ||||
| 	require.NoError(t, err) | ||||
| 	require.Equal(t, e, *e2) | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user