TX Routing Refactor (#496)

This commit is contained in:
Alexander Bezobchuk
2018-11-28 14:19:22 -08:00
committed by Jack Zampolin
parent 167d43ce38
commit a3619584f8
24 changed files with 1031 additions and 1032 deletions
+1
View File
@@ -0,0 +1 @@
package evm
+19
View File
@@ -0,0 +1,19 @@
package types
import "github.com/cosmos/cosmos-sdk/codec"
var msgCodec = codec.New()
func init() {
cdc := codec.New()
RegisterCodec(cdc)
codec.RegisterCrypto(cdc)
msgCodec = cdc.Seal()
}
// Register concrete types and interfaces on the given codec.
func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterConcrete(MsgEthereumTx{}, "ethermint/MsgEthereumTx", nil)
}
+286
View File
@@ -0,0 +1,286 @@
package types
import (
"crypto/ecdsa"
"errors"
"fmt"
"io"
"math/big"
"sync/atomic"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/ethermint/types"
ethcmn "github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/rlp"
)
var _ sdk.Msg = MsgEthereumTx{}
// message type and route constants
const (
TypeMsgEthereumTx = "ethereum_tx"
RouteMsgEthereumTx = "evm"
)
// MsgEthereumTx encapsulates an Ethereum transaction as an SDK message.
type (
MsgEthereumTx struct {
Data TxData
// caches
hash atomic.Value
size atomic.Value
from atomic.Value
}
// TxData implements the Ethereum transaction data structure. It is used
// solely as intended in Ethereum abiding by the protocol.
TxData struct {
AccountNonce uint64 `json:"nonce"`
Price *big.Int `json:"gasPrice"`
GasLimit uint64 `json:"gas"`
Recipient *ethcmn.Address `json:"to" rlp:"nil"` // nil means contract creation
Amount *big.Int `json:"value"`
Payload []byte `json:"input"`
// signature values
V *big.Int `json:"v"`
R *big.Int `json:"r"`
S *big.Int `json:"s"`
// hash is only used when marshaling to JSON
Hash *ethcmn.Hash `json:"hash" rlp:"-"`
}
// sigCache is used to cache the derived sender and contains the signer used
// to derive it.
sigCache struct {
signer ethtypes.Signer
from ethcmn.Address
}
)
// NewMsgEthereumTx returns a reference to a new Ethereum transaction message.
func NewMsgEthereumTx(
nonce uint64, to ethcmn.Address, amount *big.Int,
gasLimit uint64, gasPrice *big.Int, payload []byte,
) *MsgEthereumTx {
return newMsgEthereumTx(nonce, &to, amount, gasLimit, gasPrice, payload)
}
// NewMsgEthereumTxContract returns a reference to a new Ethereum transaction
// message designated for contract creation.
func NewMsgEthereumTxContract(
nonce uint64, amount *big.Int, gasLimit uint64, gasPrice *big.Int, payload []byte,
) *MsgEthereumTx {
return newMsgEthereumTx(nonce, nil, amount, gasLimit, gasPrice, payload)
}
func newMsgEthereumTx(
nonce uint64, to *ethcmn.Address, amount *big.Int,
gasLimit uint64, gasPrice *big.Int, payload []byte,
) *MsgEthereumTx {
if len(payload) > 0 {
payload = ethcmn.CopyBytes(payload)
}
txData := TxData{
AccountNonce: nonce,
Recipient: to,
Payload: payload,
GasLimit: gasLimit,
Amount: new(big.Int),
Price: new(big.Int),
V: new(big.Int),
R: new(big.Int),
S: new(big.Int),
}
if amount != nil {
txData.Amount.Set(amount)
}
if gasPrice != nil {
txData.Price.Set(gasPrice)
}
return &MsgEthereumTx{Data: txData}
}
// Route returns the route value of an MsgEthereumTx.
func (msg MsgEthereumTx) Route() string { return RouteMsgEthereumTx }
// Type returns the type value of an MsgEthereumTx.
func (msg MsgEthereumTx) Type() string { return TypeMsgEthereumTx }
// ValidateBasic implements the sdk.Msg interface. It performs basic validation
// checks of a Transaction. If returns an sdk.Error if validation fails.
func (msg MsgEthereumTx) ValidateBasic() sdk.Error {
if msg.Data.Price.Sign() != 1 {
return types.ErrInvalidValue("price must be positive")
}
if msg.Data.Amount.Sign() != 1 {
return types.ErrInvalidValue("amount must be positive")
}
return nil
}
// GetSigners returns the expected signers for an Ethereum transaction message.
// For such a message, there should exist only a single 'signer'.
//
// NOTE: This method cannot be used as a chain ID is needed to recover the signer
// from the signature. Use 'VerifySig' instead.
func (msg MsgEthereumTx) GetSigners() []sdk.AccAddress {
panic("must use 'VerifySig' with a chain ID to get the signer")
}
// GetSignBytes returns the Amino bytes of an Ethereum transaction message used
// for signing.
//
// NOTE: This method cannot be used as a chain ID is needed to create valid bytes
// to sign over. Use 'RLPSignBytes' instead.
func (msg MsgEthereumTx) GetSignBytes() []byte {
panic("must use 'RLPSignBytes' with a chain ID to get the valid bytes to sign")
}
// RLPSignBytes returns the RLP hash of an Ethereum transaction message with a
// given chainID used for signing.
func (msg MsgEthereumTx) RLPSignBytes(chainID *big.Int) ethcmn.Hash {
return rlpHash([]interface{}{
msg.Data.AccountNonce,
msg.Data.Price,
msg.Data.GasLimit,
msg.Data.Recipient,
msg.Data.Amount,
msg.Data.Payload,
chainID, uint(0), uint(0),
})
}
// EncodeRLP implements the rlp.Encoder interface.
func (msg *MsgEthereumTx) EncodeRLP(w io.Writer) error {
return rlp.Encode(w, &msg.Data)
}
// DecodeRLP implements the rlp.Decoder interface.
func (msg *MsgEthereumTx) DecodeRLP(s *rlp.Stream) error {
_, size, _ := s.Kind()
err := s.Decode(&msg.Data)
if err == nil {
msg.size.Store(ethcmn.StorageSize(rlp.ListSize(size)))
}
return err
}
// Hash hashes the RLP encoding of a transaction.
func (msg *MsgEthereumTx) Hash() ethcmn.Hash {
if hash := msg.hash.Load(); hash != nil {
return hash.(ethcmn.Hash)
}
v := rlpHash(msg)
msg.hash.Store(v)
return v
}
// Sign calculates a secp256k1 ECDSA signature and signs the transaction. It
// takes a private key and chainID to sign an Ethereum transaction according to
// EIP155 standard. It mutates the transaction as it populates the V, R, S
// fields of the Transaction's Signature.
func (msg *MsgEthereumTx) Sign(chainID *big.Int, priv *ecdsa.PrivateKey) {
txHash := msg.RLPSignBytes(chainID)
sig, err := ethcrypto.Sign(txHash[:], priv)
if err != nil {
panic(err)
}
if len(sig) != 65 {
panic(fmt.Sprintf("wrong size for signature: got %d, want 65", len(sig)))
}
r := new(big.Int).SetBytes(sig[:32])
s := new(big.Int).SetBytes(sig[32:64])
var v *big.Int
if chainID.Sign() == 0 {
v = new(big.Int).SetBytes([]byte{sig[64] + 27})
} else {
v = big.NewInt(int64(sig[64] + 35))
chainIDMul := new(big.Int).Mul(chainID, big.NewInt(2))
v.Add(v, chainIDMul)
}
msg.Data.V = v
msg.Data.R = r
msg.Data.S = s
}
// VerifySig attempts to verify a Transaction's signature for a given chainID.
// A derived address is returned upon success or an error if recovery fails.
func (msg MsgEthereumTx) VerifySig(chainID *big.Int) (ethcmn.Address, error) {
signer := ethtypes.NewEIP155Signer(chainID)
if sc := msg.from.Load(); sc != nil {
sigCache := sc.(sigCache)
// If the signer used to derive from in a previous call is not the same as
// used current, invalidate the cache.
if sigCache.signer.Equal(signer) {
return sigCache.from, nil
}
}
// do not allow recovery for transactions with an unprotected chainID
if chainID.Sign() == 0 {
return ethcmn.Address{}, errors.New("invalid chainID")
}
txHash := msg.RLPSignBytes(chainID)
sig := recoverEthSig(msg.Data.R, msg.Data.S, msg.Data.V, chainID)
pub, err := ethcrypto.Ecrecover(txHash[:], sig)
if err != nil {
return ethcmn.Address{}, err
}
var addr ethcmn.Address
copy(addr[:], ethcrypto.Keccak256(pub[1:])[12:])
msg.from.Store(sigCache{signer: signer, from: addr})
return addr, nil
}
// recoverEthSig recovers a signature according to the Ethereum specification.
func recoverEthSig(R, S, Vb, chainID *big.Int) []byte {
var v byte
r, s := R.Bytes(), S.Bytes()
sig := make([]byte, 65)
copy(sig[32-len(r):32], r)
copy(sig[64-len(s):64], s)
if chainID.Sign() == 0 {
v = byte(Vb.Uint64() - 27)
} else {
chainIDMul := new(big.Int).Mul(chainID, big.NewInt(2))
V := new(big.Int).Sub(Vb, chainIDMul)
v = byte(V.Uint64() - 35)
}
sig[64] = v
return sig
}
+123
View File
@@ -0,0 +1,123 @@
package types
import (
"bytes"
"fmt"
"math/big"
"testing"
ethcmn "github.com/ethereum/go-ethereum/common"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/rlp"
"github.com/stretchr/testify/require"
)
func TestMsgEthereumTx(t *testing.T) {
addr := GenerateEthAddress()
msg1 := NewMsgEthereumTx(0, addr, nil, 100000, nil, []byte("test"))
require.NotNil(t, msg1)
require.Equal(t, *msg1.Data.Recipient, addr)
msg2 := NewMsgEthereumTxContract(0, nil, 100000, nil, []byte("test"))
require.NotNil(t, msg2)
require.Nil(t, msg2.Data.Recipient)
msg3 := NewMsgEthereumTx(0, addr, nil, 100000, nil, []byte("test"))
require.Equal(t, msg3.Route(), RouteMsgEthereumTx)
require.Equal(t, msg3.Type(), TypeMsgEthereumTx)
require.Panics(t, func() { msg3.GetSigners() })
require.Panics(t, func() { msg3.GetSignBytes() })
}
func TestMsgEthereumTxValidation(t *testing.T) {
testCases := []struct {
nonce uint64
to ethcmn.Address
amount *big.Int
gasLimit uint64
gasPrice *big.Int
payload []byte
expectPass bool
}{
{amount: big.NewInt(100), gasPrice: big.NewInt(100000), expectPass: true},
{amount: big.NewInt(-1), gasPrice: big.NewInt(100000), expectPass: false},
{amount: big.NewInt(100), gasPrice: big.NewInt(-1), expectPass: false},
}
for i, tc := range testCases {
msg := NewMsgEthereumTx(tc.nonce, tc.to, tc.amount, tc.gasLimit, tc.gasPrice, tc.payload)
if tc.expectPass {
require.Nil(t, msg.ValidateBasic(), "test: %v", i)
} else {
require.NotNil(t, msg.ValidateBasic(), "test: %v", i)
}
}
}
func TestMsgEthereumTxRLPSignBytes(t *testing.T) {
addr := ethcmn.BytesToAddress([]byte("test_address"))
chainID := big.NewInt(3)
msg := NewMsgEthereumTx(0, addr, nil, 100000, nil, []byte("test"))
hash := msg.RLPSignBytes(chainID)
require.Equal(t, "5BD30E35AD27449390B14C91E6BCFDCAADF8FE44EF33680E3BC200FC0DC083C7", fmt.Sprintf("%X", hash))
}
func TestMsgEthereumTxRLPEncode(t *testing.T) {
addr := ethcmn.BytesToAddress([]byte("test_address"))
msg := NewMsgEthereumTx(0, addr, nil, 100000, nil, []byte("test"))
raw, err := rlp.EncodeToBytes(msg)
require.NoError(t, err)
require.Equal(t, ethcmn.FromHex("E48080830186A0940000000000000000746573745F61646472657373808474657374808080"), raw)
}
func TestMsgEthereumTxRLPDecode(t *testing.T) {
var msg MsgEthereumTx
raw := ethcmn.FromHex("E48080830186A0940000000000000000746573745F61646472657373808474657374808080")
addr := ethcmn.BytesToAddress([]byte("test_address"))
expectedMsg := NewMsgEthereumTx(0, addr, nil, 100000, nil, []byte("test"))
err := rlp.Decode(bytes.NewReader(raw), &msg)
require.NoError(t, err)
require.Equal(t, expectedMsg.Data, msg.Data)
}
func TestMsgEthereumTxHash(t *testing.T) {
addr := ethcmn.BytesToAddress([]byte("test_address"))
msg := NewMsgEthereumTx(0, addr, nil, 100000, nil, []byte("test"))
hash := msg.Hash()
require.Equal(t, "E2AA2E68E7586AE9700F1D3D643330866B6AC2B6CA4C804F7C85ECB11D0B0B29", fmt.Sprintf("%X", hash))
}
func TestMsgEthereumTxSig(t *testing.T) {
priv, _ := ethcrypto.GenerateKey()
addr := PrivKeyToEthAddress(priv)
msg := NewMsgEthereumTx(0, addr, nil, 100000, nil, []byte("test"))
chainID := big.NewInt(3)
msg.Sign(chainID, priv)
resultAddr, err := msg.VerifySig(chainID)
require.NoError(t, err)
require.Equal(t, addr, resultAddr)
}
func TestMsgEthereumTxAmino(t *testing.T) {
addr := GenerateEthAddress()
msg := NewMsgEthereumTx(0, addr, nil, 100000, nil, []byte("test"))
raw, err := msgCodec.MarshalBinaryBare(msg)
require.NoError(t, err)
var msg2 MsgEthereumTx
err = msgCodec.UnmarshalBinaryBare(raw, &msg2)
require.NoError(t, err)
require.Equal(t, msg.Data, msg2.Data)
}
+52
View File
@@ -0,0 +1,52 @@
package types
import (
"crypto/ecdsa"
"fmt"
ethcmn "github.com/ethereum/go-ethereum/common"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
ethsha "github.com/ethereum/go-ethereum/crypto/sha3"
"github.com/ethereum/go-ethereum/rlp"
"github.com/pkg/errors"
)
// PrivKeyToEthAddress generates an Ethereum address given an ECDSA private key.
func PrivKeyToEthAddress(p *ecdsa.PrivateKey) ethcmn.Address {
return ethcrypto.PubkeyToAddress(p.PublicKey)
}
// GenerateAddress generates an Ethereum address.
func GenerateEthAddress() ethcmn.Address {
priv, err := ethcrypto.GenerateKey()
if err != nil {
panic(err)
}
return PrivKeyToEthAddress(priv)
}
// ValidateSigner attempts to validate a signer for a given slice of bytes over
// which a signature and signer is given. An error is returned if address
// derived from the signature and bytes signed does not match the given signer.
func ValidateSigner(signBytes, sig []byte, signer ethcmn.Address) error {
pk, err := ethcrypto.SigToPub(signBytes, sig)
if err != nil {
return errors.Wrap(err, "failed to derive public key from signature")
} else if ethcrypto.PubkeyToAddress(*pk) != signer {
return fmt.Errorf("invalid signature for signer: %s", signer)
}
return nil
}
func rlpHash(x interface{}) (hash ethcmn.Hash) {
hasher := ethsha.NewKeccak256()
rlp.Encode(hasher, x)
hasher.Sum(hash[:0])
return
}