Merge pull request #478 from cosmos/bez/477-re-intro-embedded-tx

R4R: Reintroduce Embedded Transactions
This commit is contained in:
Alexander Bezobchuk 2018-08-31 08:38:16 -04:00 committed by GitHub
commit ec4c4136b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 448 additions and 268 deletions

View File

@ -15,7 +15,7 @@ import (
"github.com/cosmos/ethermint/handlers" "github.com/cosmos/ethermint/handlers"
"github.com/cosmos/ethermint/types" "github.com/cosmos/ethermint/types"
ethparams "github.com/ethereum/go-ethereum/params" ethcmn "github.com/ethereum/go-ethereum/common"
abci "github.com/tendermint/tendermint/abci/types" abci "github.com/tendermint/tendermint/abci/types"
tmcmn "github.com/tendermint/tendermint/libs/common" tmcmn "github.com/tendermint/tendermint/libs/common"
@ -58,12 +58,12 @@ type (
// NewEthermintApp returns a reference to a new initialized Ethermint // NewEthermintApp returns a reference to a new initialized Ethermint
// application. // application.
func NewEthermintApp( func NewEthermintApp(
logger tmlog.Logger, db dbm.DB, ethChainCfg *ethparams.ChainConfig, baseAppOptions ...func(*bam.BaseApp), logger tmlog.Logger, db dbm.DB, sdkAddr ethcmn.Address, baseAppOpts ...func(*bam.BaseApp),
) *EthermintApp { ) *EthermintApp {
codec := CreateCodec() codec := CreateCodec()
app := &EthermintApp{ app := &EthermintApp{
BaseApp: bam.NewBaseApp(appName, logger, db, types.TxDecoder(codec), baseAppOptions...), BaseApp: bam.NewBaseApp(appName, logger, db, types.TxDecoder(codec, sdkAddr), baseAppOpts...),
codec: codec, codec: codec,
accountKey: sdk.NewKVStoreKey("acc"), accountKey: sdk.NewKVStoreKey("acc"),
mainKey: sdk.NewKVStoreKey("main"), mainKey: sdk.NewKVStoreKey("main"),
@ -177,7 +177,7 @@ func (app *EthermintApp) initChainer(ctx sdk.Context, req abci.RequestInitChain)
} }
// CreateCodec creates a new amino wire codec and registers all the necessary // CreateCodec creates a new amino wire codec and registers all the necessary
// structures and interfaces needed for the application. // concrete types and interfaces needed for the application.
func CreateCodec() *wire.Codec { func CreateCodec() *wire.Codec {
codec := wire.NewCodec() codec := wire.NewCodec()

View File

@ -6,15 +6,30 @@ and subject to change.
## Routing ## Routing
Ethermint needs to parse and handle transactions routed for both the EVM and for Ethermint needs to parse and handle transactions routed for both the EVM and for
the Cosmos hub. We attempt to achieve this by mimicking [Geth's](https://github.com/ethereum/go-ethereum) `Transaction` structure to handle the Cosmos hub. We attempt to achieve this by mimicking [Geth's](https://github.com/ethereum/go-ethereum) `Transaction` structure and utilizing
Ethereum transactions and utilizing the SDK's `auth.StdTx` for Cosmos the `Payload` as the potential encoding of a Cosmos-routed transaction. What
transactions. Both of these structures are registered with an [Amino](https://github.com/tendermint/go-amino) codec, so the `TxDecoder` that in invoked designates this encoding, and ultimately routing, is the `Recipient` address --
during the `BaseApp#runTx`, will be able to decode raw transaction bytes into the if this address matches some global unique predefined and configured address,
appropriate transaction type which will then be passed onto handlers downstream. we regard it as a transaction meant for Cosmos, otherwise, the transaction is a
pure Ethereum transaction and will be executed in the EVM.
For Cosmos routed transactions, the `Transaction.Payload` will contain an [Amino](https://github.com/tendermint/go-amino) encoded embedded transaction that must
implement the `sdk.Tx` interface. Note, the embedding (outer) `Transaction` is
still RLP encoded in order to preserve compatibility with existing tooling. In
addition, at launch, Ethermint will only support the `auth.StdTx` embedded Cosmos
transaction type.
Being that Ethermint implements the Tendermint ABCI application interface, as
transactions are consumed, they are passed through a series of handlers. Once such
handler, `runTx`, is responsible for invoking the `TxDecoder` which performs the
business logic of properly deserializing raw transaction bytes into either an
Ethereum transaction or a Cosmos transaction.
__Note__: Our goal is to utilize Geth as a library, at least as much as possible, __Note__: Our goal is to utilize Geth as a library, at least as much as possible,
so it should be expected that these types and the operations you may perform on so it should be expected that these types and the operations you may perform on
them will keep in line with Ethereum (e.g. signature algorithms and gas/fees). them will keep in line with Ethereum (e.g. signature algorithms and gas/fees).
In addition, we aim to have existing tooling and frameworks in the Ethereum
ecosystem have 100% compatibility with creating transactions in Ethermint.
## Transactions & Messages ## Transactions & Messages
@ -33,16 +48,16 @@ no distinction between transactions and messages.
Ethermint supports [EIP-155](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md) Ethermint supports [EIP-155](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md)
signatures. A `Transaction` is expected to have a single signature for Ethereum signatures. A `Transaction` is expected to have a single signature for Ethereum
routed transactions. However, just as in Cosmos, Ethermint will support multiple routed transactions. However, just as in Cosmos, Ethermint will support multiple
signers for `auth.StdTx` Cosmos routed transactions. Signatures over the signers for embedded Cosmos routed transactions. Signatures over the
`Transaction` type are identical to Ethereum. However, the `auth.StdTx` contains `Transaction` type are identical to Ethereum. However, the embedded transaction contains
a canonical signature structure that contains the signature itself and other a canonical signature structure that contains the signature itself and other
information such as an account's sequence number. This, in addition to the chainID, information such as an account's sequence number. This, in addition to the chainID,
helps prevent "replay attacks", where the same message could be executed over and helps prevent "replay attacks", where the same message could be executed over and
over again. over again.
An `auth.StdTx` list of signatures must much the unique list of addresses returned An embedded transaction's list of signatures must much the unique list of addresses
by each message's `GetSigners` call. In addition, the address of first signer of returned by each message's `GetSigners` call. In addition, the address of first
the `auth.StdTx` is responsible for paying the fees. signer of the embedded transaction is responsible for paying the fees.
## Gas & Fees ## Gas & Fees

View File

@ -9,8 +9,6 @@ import (
"github.com/cosmos/ethermint/types" "github.com/cosmos/ethermint/types"
ethcmn "github.com/ethereum/go-ethereum/common" ethcmn "github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types"
) )
const ( const (
@ -39,7 +37,7 @@ func AnteHandler(am auth.AccountMapper, _ auth.FeeCollectionKeeper) sdk.AnteHand
switch tx := tx.(type) { switch tx := tx.(type) {
case types.Transaction: case types.Transaction:
gasLimit = int64(tx.Data.GasLimit) gasLimit = int64(tx.Data().GasLimit)
handler = handleEthTx handler = handleEthTx
case auth.StdTx: case auth.StdTx:
gasLimit = tx.Fee.Gas gasLimit = tx.Fee.Gas
@ -91,16 +89,28 @@ func handleEthTx(sdkCtx sdk.Context, tx sdk.Tx, am auth.AccountMapper) (sdk.Cont
return sdkCtx, sdk.ErrInternal(fmt.Sprintf("invalid chainID: %s", sdkCtx.ChainID())).Result(), true return sdkCtx, sdk.ErrInternal(fmt.Sprintf("invalid chainID: %s", sdkCtx.ChainID())).Result(), true
} }
// validate signature sdkCtx.GasMeter().ConsumeGas(verifySigCost, "ante: verify Ethereum signature")
gethTx := ethTx.ConvertTx(chainID)
signer := ethtypes.NewEIP155Signer(chainID)
_, err := signer.Sender(&gethTx) addr, err := ethTx.VerifySig(chainID)
if err != nil { if err != nil {
return sdkCtx, sdk.ErrUnauthorized("signature verification failed").Result(), true return sdkCtx, sdk.ErrUnauthorized("signature verification failed").Result(), true
} }
return sdkCtx, sdk.Result{GasWanted: int64(ethTx.Data.GasLimit)}, false acc := am.GetAccount(sdkCtx, addr.Bytes())
// validate the account nonce (referred to as sequence in the AccountMapper)
seq := acc.GetSequence()
if ethTx.Data().AccountNonce != uint64(seq) {
return sdkCtx, sdk.ErrInvalidSequence(fmt.Sprintf("invalid account nonce; expected: %d", seq)).Result(), true
}
err = acc.SetSequence(seq + 1)
if err != nil {
return sdkCtx, sdk.ErrInternal(err.Error()).Result(), true
}
am.SetAccount(sdkCtx, acc)
return sdkCtx, sdk.Result{GasWanted: int64(ethTx.Data().GasLimit)}, false
} }
// handleEmbeddedTx implements an ante handler for an SDK transaction. It // handleEmbeddedTx implements an ante handler for an SDK transaction. It
@ -122,15 +132,15 @@ func handleEmbeddedTx(sdkCtx sdk.Context, tx sdk.Tx, am auth.AccountMapper) (sdk
for i, sig := range stdTx.Signatures { for i, sig := range stdTx.Signatures {
signer := ethcmn.BytesToAddress(signerAddrs[i].Bytes()) signer := ethcmn.BytesToAddress(signerAddrs[i].Bytes())
signerAcc, err := validateSignature(sdkCtx, stdTx, signer, sig, am) acc, err := validateSignature(sdkCtx, stdTx, signer, sig, am)
if err.Code() != sdk.CodeOK { if err.Code() != sdk.CodeOK {
return sdkCtx, err.Result(), false return sdkCtx, err.Result(), true
} }
// TODO: Fees! // TODO: Fees!
am.SetAccount(sdkCtx, signerAcc) am.SetAccount(sdkCtx, acc)
signerAccs[i] = signerAcc signerAccs[i] = acc
} }
newCtx := auth.WithSigners(sdkCtx, signerAccs) newCtx := auth.WithSigners(sdkCtx, signerAccs)

121
types/test_common.go Normal file
View File

@ -0,0 +1,121 @@
// nolint
package types
import (
"crypto/ecdsa"
"math/big"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/wire"
"github.com/cosmos/cosmos-sdk/x/auth"
ethcmn "github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
)
var (
TestSDKAddr = GenerateEthAddress()
TestChainID = big.NewInt(3)
TestPrivKey1, _ = ethcrypto.GenerateKey()
TestPrivKey2, _ = ethcrypto.GenerateKey()
TestAddr1 = PrivKeyToEthAddress(TestPrivKey1)
TestAddr2 = PrivKeyToEthAddress(TestPrivKey2)
)
func NewTestCodec() *wire.Codec {
codec := wire.NewCodec()
RegisterWire(codec)
auth.RegisterWire(codec)
wire.RegisterCrypto(codec)
codec.RegisterConcrete(&sdk.TestMsg{}, "test/TestMsg", nil)
return codec
}
func NewTestStdFee() auth.StdFee {
return auth.NewStdFee(5000, sdk.NewCoin("photon", sdk.NewInt(150)))
}
func NewTestStdTx(
chainID *big.Int, msgs []sdk.Msg, accNums []int64, seqs []int64, pKeys []*ecdsa.PrivateKey, fee auth.StdFee,
) sdk.Tx {
sigs := make([]auth.StdSignature, len(pKeys))
for i, priv := range pKeys {
signBytes := GetStdTxSignBytes(chainID.String(), accNums[i], seqs[i], NewTestStdFee(), msgs, "")
sig, err := ethcrypto.Sign(signBytes, priv)
if err != nil {
panic(err)
}
sigs[i] = auth.StdSignature{Signature: sig, AccountNumber: accNums[i], Sequence: seqs[i]}
}
return auth.NewStdTx(msgs, fee, sigs, "")
}
func NewTestGethTxs(
chainID *big.Int, seqs []int64, addrs []ethcmn.Address, pKeys []*ecdsa.PrivateKey,
) []*ethtypes.Transaction {
txs := make([]*ethtypes.Transaction, len(pKeys))
for i, privKey := range pKeys {
ethTx := ethtypes.NewTransaction(
uint64(seqs[i]), addrs[i], big.NewInt(10), 1000, big.NewInt(100), []byte{},
)
signer := ethtypes.NewEIP155Signer(chainID)
ethTx, err := ethtypes.SignTx(ethTx, signer, privKey)
if err != nil {
panic(err)
}
txs[i] = ethTx
}
return txs
}
func NewTestEthTxs(
chainID *big.Int, seqs []int64, addrs []ethcmn.Address, pKeys []*ecdsa.PrivateKey,
) []*Transaction {
txs := make([]*Transaction, len(pKeys))
for i, privKey := range pKeys {
ethTx := NewTransaction(
uint64(seqs[i]), addrs[i], big.NewInt(10), 1000, big.NewInt(100), []byte{},
)
ethTx.Sign(chainID, privKey)
txs[i] = ethTx
}
return txs
}
func NewTestSDKTxs(
codec *wire.Codec, chainID *big.Int, to ethcmn.Address, msgs []sdk.Msg,
accNums []int64, seqs []int64, pKeys []*ecdsa.PrivateKey, fee auth.StdFee,
) []*Transaction {
txs := make([]*Transaction, len(pKeys))
stdTx := NewTestStdTx(chainID, msgs, accNums, seqs, pKeys, fee)
payload := codec.MustMarshalBinary(stdTx)
for i, privKey := range pKeys {
ethTx := NewTransaction(uint64(seqs[i]), to, big.NewInt(10), 1000, big.NewInt(100), payload)
ethTx.Sign(chainID, privKey)
txs[i] = ethTx
}
return txs
}

View File

@ -1,8 +1,10 @@
package types package types
import ( import (
"bytes"
"crypto/ecdsa" "crypto/ecdsa"
"fmt" "fmt"
"io"
"math/big" "math/big"
"sync/atomic" "sync/atomic"
@ -13,17 +15,20 @@ import (
ethcrypto "github.com/ethereum/go-ethereum/crypto" ethcrypto "github.com/ethereum/go-ethereum/crypto"
ethsha "github.com/ethereum/go-ethereum/crypto/sha3" ethsha "github.com/ethereum/go-ethereum/crypto/sha3"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
var _ sdk.Tx = (*Transaction)(nil)
const ( const (
// TypeTxEthereum reflects an Ethereum Transaction type. // TypeTxEthereum reflects an Ethereum Transaction type.
TypeTxEthereum = "Ethereum" TypeTxEthereum = "Ethereum"
) )
// ----------------------------------------------------------------------------
// Ethereum transaction
// ----------------------------------------------------------------------------
var _ sdk.Tx = (*Transaction)(nil)
type ( type (
// Transaction implements the Ethereum transaction structure as an exact // Transaction implements the Ethereum transaction structure as an exact
// replica. It implements the Cosmos sdk.Tx interface. Due to the private // replica. It implements the Cosmos sdk.Tx interface. Due to the private
@ -33,7 +38,7 @@ type (
// Note: The transaction also implements the sdk.Msg interface to perform // Note: The transaction also implements the sdk.Msg interface to perform
// basic validation that is done in the BaseApp. // basic validation that is done in the BaseApp.
Transaction struct { Transaction struct {
Data TxData data TxData
// caches // caches
hash atomic.Value hash atomic.Value
@ -41,96 +46,136 @@ type (
from atomic.Value from atomic.Value
} }
// TxData defines internal Ethereum transaction information // TxData implements the Ethereum transaction data structure as an exact
// copy. It is used solely as intended in Ethereum abiding by the protocol
// except for the payload field which may embed a Cosmos SDK transaction.
TxData struct { TxData struct {
AccountNonce uint64 `json:"nonce"` AccountNonce uint64 `json:"nonce"`
Price sdk.Int `json:"gasPrice"` Price *big.Int `json:"gasPrice"`
GasLimit uint64 `json:"gas"` GasLimit uint64 `json:"gas"`
Recipient *ethcmn.Address `json:"to"` // nil means contract creation Recipient *ethcmn.Address `json:"to" rlp:"nil"` // nil means contract creation
Amount sdk.Int `json:"value"` Amount *big.Int `json:"value"`
Payload []byte `json:"input"` Payload []byte `json:"input"`
Signature *EthSignature `json:"signature"`
// 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 is only used when marshaling to JSON
Hash *ethcmn.Hash `json:"hash"` Hash *ethcmn.Hash `json:"hash" rlp:"-"`
} }
// EthSignature reflects an Ethereum signature. We wrap this in a structure // sigCache is used to cache the derived sender and contains the signer used
// to support Amino serialization of transactions. // to derive it.
EthSignature struct { sigCache struct {
v, r, s *big.Int signer ethtypes.Signer
from ethcmn.Address
} }
) )
// NewEthSignature returns a new instantiated Ethereum signature. // NewTransaction returns a reference to a new Ethereum transaction.
func NewEthSignature(v, r, s *big.Int) *EthSignature {
return &EthSignature{v, r, s}
}
func (es *EthSignature) sanitize() {
if es.v == nil {
es.v = new(big.Int)
}
if es.r == nil {
es.r = new(big.Int)
}
if es.s == nil {
es.s = new(big.Int)
}
}
// MarshalAmino defines a custom encoding scheme for a EthSignature.
func (es EthSignature) MarshalAmino() ([3]string, error) {
es.sanitize()
return ethSigMarshalAmino(es)
}
// UnmarshalAmino defines a custom decoding scheme for a EthSignature.
func (es *EthSignature) UnmarshalAmino(raw [3]string) error {
es.sanitize()
return ethSigUnmarshalAmino(es, raw)
}
// NewTransaction mimics ethereum's NewTransaction function. It returns a
// reference to a new Ethereum Transaction.
func NewTransaction( func NewTransaction(
nonce uint64, to ethcmn.Address, amount sdk.Int, nonce uint64, to ethcmn.Address, amount *big.Int, gasLimit uint64, gasPrice *big.Int, payload []byte,
gasLimit uint64, gasPrice sdk.Int, payload []byte, ) *Transaction {
) Transaction {
return newTransaction(nonce, &to, amount, gasLimit, gasPrice, payload)
}
// NewContractCreation returns a reference to a new Ethereum transaction
// designated for contract creation.
func NewContractCreation(
nonce uint64, amount *big.Int, gasLimit uint64, gasPrice *big.Int, payload []byte,
) *Transaction {
return newTransaction(nonce, nil, amount, gasLimit, gasPrice, payload)
}
func newTransaction(
nonce uint64, to *ethcmn.Address, amount *big.Int,
gasLimit uint64, gasPrice *big.Int, payload []byte,
) *Transaction {
if len(payload) > 0 { if len(payload) > 0 {
payload = ethcmn.CopyBytes(payload) payload = ethcmn.CopyBytes(payload)
} }
txData := TxData{ txData := TxData{
Recipient: &to,
AccountNonce: nonce, AccountNonce: nonce,
Recipient: to,
Payload: payload, Payload: payload,
GasLimit: gasLimit, GasLimit: gasLimit,
Amount: amount, Amount: new(big.Int),
Price: gasPrice, Price: new(big.Int),
Signature: NewEthSignature(new(big.Int), new(big.Int), new(big.Int)), V: new(big.Int),
R: new(big.Int),
S: new(big.Int),
} }
return Transaction{Data: txData} if amount != nil {
txData.Amount.Set(amount)
}
if gasPrice != nil {
txData.Price.Set(gasPrice)
}
return &Transaction{data: txData}
}
// Data returns the Transaction's data.
func (tx Transaction) Data() TxData {
return tx.data
}
// EncodeRLP implements the rlp.Encoder interface.
func (tx *Transaction) EncodeRLP(w io.Writer) error {
return rlp.Encode(w, &tx.data)
}
// DecodeRLP implements the rlp.Decoder interface.
func (tx *Transaction) DecodeRLP(s *rlp.Stream) error {
_, size, _ := s.Kind()
err := s.Decode(&tx.data)
if err == nil {
tx.size.Store(ethcmn.StorageSize(rlp.ListSize(size)))
}
return err
}
// Hash hashes the RLP encoding of a transaction.
func (tx *Transaction) Hash() ethcmn.Hash {
if hash := tx.hash.Load(); hash != nil {
return hash.(ethcmn.Hash)
}
v := rlpHash(tx)
tx.hash.Store(v)
return v
}
// SigHash returns the RLP hash of a transaction with a given chainID used for
// signing.
func (tx Transaction) SigHash(chainID *big.Int) ethcmn.Hash {
return rlpHash([]interface{}{
tx.data.AccountNonce,
tx.data.Price,
tx.data.GasLimit,
tx.data.Recipient,
tx.data.Amount,
tx.data.Payload,
chainID, uint(0), uint(0),
})
} }
// Sign calculates a secp256k1 ECDSA signature and signs the transaction. It // Sign calculates a secp256k1 ECDSA signature and signs the transaction. It
// takes a private key and chainID to sign an Ethereum transaction according to // 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 // EIP155 standard. It mutates the transaction as it populates the V, R, S
// fields of the Transaction's Signature. // fields of the Transaction's Signature.
func (tx *Transaction) Sign(chainID sdk.Int, priv *ecdsa.PrivateKey) { func (tx *Transaction) Sign(chainID *big.Int, priv *ecdsa.PrivateKey) {
h := rlpHash([]interface{}{ txHash := tx.SigHash(chainID)
tx.Data.AccountNonce,
tx.Data.Price.BigInt(),
tx.Data.GasLimit,
tx.Data.Recipient,
tx.Data.Amount.BigInt(),
tx.Data.Payload,
chainID.BigInt(), uint(0), uint(0),
})
sig, err := ethcrypto.Sign(h[:], priv) sig, err := ethcrypto.Sign(txHash[:], priv)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -147,13 +192,48 @@ func (tx *Transaction) Sign(chainID sdk.Int, priv *ecdsa.PrivateKey) {
v = new(big.Int).SetBytes([]byte{sig[64] + 27}) v = new(big.Int).SetBytes([]byte{sig[64] + 27})
} else { } else {
v = big.NewInt(int64(sig[64] + 35)) v = big.NewInt(int64(sig[64] + 35))
chainIDMul := new(big.Int).Mul(chainID.BigInt(), big.NewInt(2)) chainIDMul := new(big.Int).Mul(chainID, big.NewInt(2))
v.Add(v, chainIDMul) v.Add(v, chainIDMul)
} }
tx.Data.Signature.v = v tx.data.V = v
tx.Data.Signature.r = r tx.data.R = r
tx.Data.Signature.s = s tx.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 (tx Transaction) VerifySig(chainID *big.Int) (ethcmn.Address, error) {
signer := ethtypes.NewEIP155Signer(chainID)
if sc := tx.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 := tx.SigHash(chainID)
sig := recoverEthSig(tx.data.R, tx.data.S, tx.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:])
tx.from.Store(sigCache{signer: signer, from: addr})
return addr, nil
} }
// Type implements the sdk.Msg interface. It returns the type of the // Type implements the sdk.Msg interface. It returns the type of the
@ -165,11 +245,11 @@ func (tx Transaction) Type() string {
// ValidateBasic implements the sdk.Msg interface. It performs basic validation // ValidateBasic implements the sdk.Msg interface. It performs basic validation
// checks of a Transaction. If returns an sdk.Error if validation fails. // checks of a Transaction. If returns an sdk.Error if validation fails.
func (tx Transaction) ValidateBasic() sdk.Error { func (tx Transaction) ValidateBasic() sdk.Error {
if tx.Data.Price.Sign() != 1 { if tx.data.Price.Sign() != 1 {
return ErrInvalidValue(DefaultCodespace, "price must be positive") return ErrInvalidValue(DefaultCodespace, "price must be positive")
} }
if tx.Data.Amount.Sign() != 1 { if tx.data.Amount.Sign() != 1 {
return ErrInvalidValue(DefaultCodespace, "amount must be positive") return ErrInvalidValue(DefaultCodespace, "amount must be positive")
} }
@ -192,44 +272,57 @@ func (tx Transaction) GetMsgs() []sdk.Msg {
return []sdk.Msg{tx} return []sdk.Msg{tx}
} }
// ConvertTx attempts to converts a Transaction to a new Ethereum transaction // hasEmbeddedTx returns a boolean reflecting if the transaction contains an
// with the signature set. The signature if first recovered and then a new // SDK transaction or not based on the recipient address.
// Transaction is created with that signature. If setting the signature fails, func (tx Transaction) hasEmbeddedTx(addr ethcmn.Address) bool {
// a panic will be triggered. return bytes.Equal(tx.data.Recipient.Bytes(), addr.Bytes())
}
// GetEmbeddedTx returns the embedded SDK transaction from an Ethereum
// transaction. It returns an error if decoding the inner transaction fails.
// //
// TODO: To be removed in #470 // CONTRACT: The payload field of an Ethereum transaction must contain a valid
func (tx Transaction) ConvertTx(chainID *big.Int) ethtypes.Transaction { // encoded SDK transaction.
gethTx := ethtypes.NewTransaction( func (tx Transaction) GetEmbeddedTx(codec *wire.Codec) (sdk.Tx, sdk.Error) {
tx.Data.AccountNonce, *tx.Data.Recipient, tx.Data.Amount.BigInt(), var etx sdk.Tx
tx.Data.GasLimit, tx.Data.Price.BigInt(), tx.Data.Payload,
)
sig := recoverEthSig(tx.Data.Signature, chainID) err := codec.UnmarshalBinary(tx.data.Payload, &etx)
signer := ethtypes.NewEIP155Signer(chainID)
gethTx, err := gethTx.WithSignature(signer, sig)
if err != nil { if err != nil {
panic(errors.Wrap(err, "failed to convert transaction with a given signature")) return etx, sdk.ErrTxDecode("failed to decode embedded transaction")
} }
return *gethTx return etx, nil
} }
// TxDecoder returns an sdk.TxDecoder that given raw transaction bytes, // ----------------------------------------------------------------------------
// attempts to decode them into a valid sdk.Tx. // Utilities
func TxDecoder(codec *wire.Codec) sdk.TxDecoder { // ----------------------------------------------------------------------------
// TxDecoder returns an sdk.TxDecoder that given raw transaction bytes and an
// SDK address, attempts to decode them into a Transaction or an EmbeddedTx or
// returning an error if decoding fails.
func TxDecoder(codec *wire.Codec, sdkAddress ethcmn.Address) sdk.TxDecoder {
return func(txBytes []byte) (sdk.Tx, sdk.Error) { return func(txBytes []byte) (sdk.Tx, sdk.Error) {
var tx = Transaction{}
if len(txBytes) == 0 { if len(txBytes) == 0 {
return nil, sdk.ErrTxDecode("txBytes are empty") return nil, sdk.ErrTxDecode("transaction bytes are empty")
} }
var tx sdk.Tx err := rlp.DecodeBytes(txBytes, &tx)
// The given codec should have all the appropriate message types
// registered.
err := codec.UnmarshalBinary(txBytes, &tx)
if err != nil { if err != nil {
return nil, sdk.ErrTxDecode("failed to decode tx").TraceSDK(err.Error()) return nil, sdk.ErrTxDecode("failed to decode transaction").TraceSDK(err.Error())
}
// If the transaction is routed as an SDK transaction, decode and return
// the embedded SDK transaction.
if tx.hasEmbeddedTx(sdkAddress) {
etx, err := tx.GetEmbeddedTx(codec)
if err != nil {
return nil, err
}
return etx, nil
} }
return tx, nil return tx, nil
@ -237,20 +330,20 @@ func TxDecoder(codec *wire.Codec) sdk.TxDecoder {
} }
// recoverEthSig recovers a signature according to the Ethereum specification. // recoverEthSig recovers a signature according to the Ethereum specification.
func recoverEthSig(es *EthSignature, chainID *big.Int) []byte { func recoverEthSig(R, S, Vb, chainID *big.Int) []byte {
var v byte var v byte
r, s := es.r.Bytes(), es.s.Bytes() r, s := R.Bytes(), S.Bytes()
sig := make([]byte, 65) sig := make([]byte, 65)
copy(sig[32-len(r):32], r) copy(sig[32-len(r):32], r)
copy(sig[64-len(s):64], s) copy(sig[64-len(s):64], s)
if chainID.Sign() == 0 { if chainID.Sign() == 0 {
v = byte(es.v.Uint64() - 27) v = byte(Vb.Uint64() - 27)
} else { } else {
chainIDMul := new(big.Int).Mul(chainID, big.NewInt(2)) chainIDMul := new(big.Int).Mul(chainID, big.NewInt(2))
V := new(big.Int).Sub(es.v, chainIDMul) V := new(big.Int).Sub(Vb, chainIDMul)
v = byte(V.Uint64() - 35) v = byte(V.Uint64() - 35)
} }
@ -259,43 +352,11 @@ func recoverEthSig(es *EthSignature, chainID *big.Int) []byte {
return sig return sig
} }
func rlpHash(x interface{}) (h ethcmn.Hash) { func rlpHash(x interface{}) (hash ethcmn.Hash) {
hasher := ethsha.NewKeccak256() hasher := ethsha.NewKeccak256()
rlp.Encode(hasher, x) rlp.Encode(hasher, x)
hasher.Sum(h[:0]) hasher.Sum(hash[:0])
return h
}
func ethSigMarshalAmino(es EthSignature) (raw [3]string, err error) {
vb, err := es.v.MarshalText()
if err != nil {
return raw, err
}
rb, err := es.r.MarshalText()
if err != nil {
return raw, err
}
sb, err := es.s.MarshalText()
if err != nil {
return raw, err
}
raw[0], raw[1], raw[2] = string(vb), string(rb), string(sb)
return raw, err
}
func ethSigUnmarshalAmino(es *EthSignature, raw [3]string) (err error) {
if err = es.v.UnmarshalText([]byte(raw[0])); err != nil {
return
}
if err = es.r.UnmarshalText([]byte(raw[1])); err != nil {
return
}
if err = es.s.UnmarshalText([]byte(raw[2])); err != nil {
return
}
return return
} }

View File

@ -3,83 +3,57 @@ package types
import ( import (
"crypto/ecdsa" "crypto/ecdsa"
"fmt" "fmt"
"math/big"
"testing" "testing"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/wire"
"github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/auth"
ethcmn "github.com/ethereum/go-ethereum/common" ethcmn "github.com/ethereum/go-ethereum/common"
ethcrypto "github.com/ethereum/go-ethereum/crypto" ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rlp"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestTransactionRLPEncode(t *testing.T) {
txs := NewTestEthTxs(TestChainID, []int64{0}, []ethcmn.Address{TestAddr1}, []*ecdsa.PrivateKey{TestPrivKey1})
gtxs := NewTestGethTxs(TestChainID, []int64{0}, []ethcmn.Address{TestAddr1}, []*ecdsa.PrivateKey{TestPrivKey1})
txRLP, err := rlp.EncodeToBytes(txs[0])
require.NoError(t, err)
gtxRLP, err := rlp.EncodeToBytes(gtxs[0])
require.NoError(t, err)
require.Equal(t, gtxRLP, txRLP)
}
func TestTransactionRLPDecode(t *testing.T) {
txs := NewTestEthTxs(TestChainID, []int64{0}, []ethcmn.Address{TestAddr1}, []*ecdsa.PrivateKey{TestPrivKey1})
gtxs := NewTestGethTxs(TestChainID, []int64{0}, []ethcmn.Address{TestAddr1}, []*ecdsa.PrivateKey{TestPrivKey1})
txRLP, err := rlp.EncodeToBytes(txs[0])
require.NoError(t, err)
gtxRLP, err := rlp.EncodeToBytes(gtxs[0])
require.NoError(t, err)
var ( var (
testChainID = sdk.NewInt(3) decodedTx Transaction
decodedGtx ethtypes.Transaction
testPrivKey1, _ = ethcrypto.GenerateKey()
testPrivKey2, _ = ethcrypto.GenerateKey()
testAddr1 = PrivKeyToEthAddress(testPrivKey1)
testAddr2 = PrivKeyToEthAddress(testPrivKey2)
) )
func newTestCodec() *wire.Codec { err = rlp.DecodeBytes(txRLP, &decodedTx)
codec := wire.NewCodec() require.NoError(t, err)
RegisterWire(codec) err = rlp.DecodeBytes(gtxRLP, &decodedGtx)
codec.RegisterConcrete(auth.StdTx{}, "test/StdTx", nil) require.NoError(t, err)
codec.RegisterConcrete(&sdk.TestMsg{}, "test/TestMsg", nil)
wire.RegisterCrypto(codec)
return codec require.Equal(t, decodedGtx.Hash(), decodedTx.Hash())
}
func newStdFee() auth.StdFee {
return auth.NewStdFee(5000, sdk.NewCoin("photon", sdk.NewInt(150)))
}
func newTestStdTx(
chainID sdk.Int, msgs []sdk.Msg, pKeys []*ecdsa.PrivateKey,
accNums []int64, seqs []int64, fee auth.StdFee,
) sdk.Tx {
sigs := make([]auth.StdSignature, len(pKeys))
for i, priv := range pKeys {
signBytes := GetStdTxSignBytes(chainID.String(), accNums[i], seqs[i], newStdFee(), msgs, "")
sig, err := ethcrypto.Sign(signBytes, priv)
if err != nil {
panic(err)
}
sigs[i] = auth.StdSignature{Signature: sig, AccountNumber: accNums[i], Sequence: seqs[i]}
}
return auth.NewStdTx(msgs, fee, sigs, "")
}
func newTestEthTxs(chainID sdk.Int, pKeys []*ecdsa.PrivateKey, addrs []ethcmn.Address) []Transaction {
txs := make([]Transaction, len(pKeys))
for i, priv := range pKeys {
emintTx := NewTransaction(
uint64(i), addrs[i], sdk.NewInt(10), 100, sdk.NewInt(100), nil,
)
emintTx.Sign(chainID, priv)
txs[i] = emintTx
}
return txs
} }
func TestValidation(t *testing.T) { func TestValidation(t *testing.T) {
ethTxs := newTestEthTxs( ethTxs := NewTestEthTxs(
testChainID, TestChainID, []int64{0}, []ethcmn.Address{TestAddr1}, []*ecdsa.PrivateKey{TestPrivKey1},
[]*ecdsa.PrivateKey{testPrivKey1},
[]ethcmn.Address{testAddr1},
) )
testCases := []struct { testCases := []struct {
@ -89,13 +63,13 @@ func TestValidation(t *testing.T) {
}{ }{
{ethTxs[0], func(msg sdk.Msg) sdk.Msg { return msg }, false}, {ethTxs[0], func(msg sdk.Msg) sdk.Msg { return msg }, false},
{ethTxs[0], func(msg sdk.Msg) sdk.Msg { {ethTxs[0], func(msg sdk.Msg) sdk.Msg {
tx := msg.(Transaction) tx := msg.(*Transaction)
tx.Data.Price = sdk.NewInt(-1) tx.data.Price = big.NewInt(-1)
return tx return tx
}, true}, }, true},
{ethTxs[0], func(msg sdk.Msg) sdk.Msg { {ethTxs[0], func(msg sdk.Msg) sdk.Msg {
tx := msg.(Transaction) tx := msg.(*Transaction)
tx.Data.Amount = sdk.NewInt(-1) tx.data.Amount = big.NewInt(-1)
return tx return tx
}, true}, }, true},
} }
@ -112,59 +86,57 @@ func TestValidation(t *testing.T) {
} }
} }
func TestTransactionGetMsgs(t *testing.T) { func TestTransactionVerifySig(t *testing.T) {
ethTxs := newTestEthTxs( txs := NewTestEthTxs(
testChainID, TestChainID, []int64{0}, []ethcmn.Address{TestAddr1}, []*ecdsa.PrivateKey{TestPrivKey1},
[]*ecdsa.PrivateKey{testPrivKey1},
[]ethcmn.Address{testAddr1},
) )
msgs := ethTxs[0].GetMsgs() addr, err := txs[0].VerifySig(TestChainID)
require.Len(t, msgs, 1) require.NoError(t, err)
require.Equal(t, ethTxs[0], msgs[0]) require.Equal(t, TestAddr1, addr)
expectedMsgs := []sdk.Msg{sdk.NewTestMsg(sdk.AccAddress(testAddr1.Bytes()))} addr, err = txs[0].VerifySig(big.NewInt(100))
etx := newTestStdTx( require.Error(t, err)
testChainID, expectedMsgs, []*ecdsa.PrivateKey{testPrivKey1}, require.NotEqual(t, TestAddr1, addr)
[]int64{0}, []int64{0}, newStdFee(),
)
msgs = etx.GetMsgs()
require.Len(t, msgs, len(expectedMsgs))
require.Equal(t, expectedMsgs, msgs)
} }
func TestTxDecoder(t *testing.T) { func TestTxDecoder(t *testing.T) {
testCodec := newTestCodec() testCodec := NewTestCodec()
txDecoder := TxDecoder(testCodec) txDecoder := TxDecoder(testCodec, TestSDKAddr)
msgs := []sdk.Msg{sdk.NewTestMsg()} msgs := []sdk.Msg{sdk.NewTestMsg()}
// create a non-SDK Ethereum transaction // create a non-SDK Ethereum transaction
emintTx := NewTransaction( txs := NewTestEthTxs(
uint64(0), testAddr1, sdk.NewInt(10), 100, sdk.NewInt(100), nil, TestChainID, []int64{0}, []ethcmn.Address{TestAddr1}, []*ecdsa.PrivateKey{TestPrivKey1},
) )
emintTx.Sign(testChainID, testPrivKey1)
txBytes, err := rlp.EncodeToBytes(txs[0])
require.NoError(t, err)
// require the transaction to properly decode into a Transaction // require the transaction to properly decode into a Transaction
txBytes := testCodec.MustMarshalBinary(emintTx) decodedTx, err := txDecoder(txBytes)
tx, err := txDecoder(txBytes)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, emintTx, tx) require.IsType(t, Transaction{}, decodedTx)
require.Equal(t, txs[0].data, (decodedTx.(Transaction)).data)
// create a SDK (auth.StdTx) transaction and encode // create a SDK (auth.StdTx) transaction and encode
stdTx := newTestStdTx( txs = NewTestSDKTxs(
testChainID, msgs, []*ecdsa.PrivateKey{testPrivKey1}, testCodec, TestChainID, TestSDKAddr, msgs, []int64{0}, []int64{0},
[]int64{0}, []int64{0}, newStdFee(), []*ecdsa.PrivateKey{TestPrivKey1}, NewTestStdFee(),
) )
// require the transaction to properly decode into a Transaction txBytes, err = rlp.EncodeToBytes(txs[0])
txBytes = testCodec.MustMarshalBinary(stdTx)
tx, err = txDecoder(txBytes)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, stdTx, tx)
// require the transaction to properly decode into a Transaction
stdTx := NewTestStdTx(TestChainID, msgs, []int64{0}, []int64{0}, []*ecdsa.PrivateKey{TestPrivKey1}, NewTestStdFee())
decodedTx, err = txDecoder(txBytes)
require.NoError(t, err)
require.IsType(t, auth.StdTx{}, decodedTx)
require.Equal(t, stdTx, decodedTx)
// require the decoding to fail when no transaction bytes are given // require the decoding to fail when no transaction bytes are given
tx, err = txDecoder([]byte{}) decodedTx, err = txDecoder([]byte{})
require.Error(t, err) require.Error(t, err)
require.Nil(t, tx) require.Nil(t, decodedTx)
} }

View File

@ -35,7 +35,7 @@ func ValidateSigner(signBytes, sig []byte, signer ethcmn.Address) error {
pk, err := ethcrypto.SigToPub(signBytes, sig) pk, err := ethcrypto.SigToPub(signBytes, sig)
if err != nil { if err != nil {
return errors.Wrap(err, "signature verification failed") return errors.Wrap(err, "failed to derive public key from signature")
} else if ethcrypto.PubkeyToAddress(*pk) != signer { } else if ethcrypto.PubkeyToAddress(*pk) != signer {
return fmt.Errorf("invalid signature for signer: %s", signer) return fmt.Errorf("invalid signature for signer: %s", signer)
} }

View File

@ -10,23 +10,27 @@ import (
) )
func TestValidateSigner(t *testing.T) { func TestValidateSigner(t *testing.T) {
msgs := []sdk.Msg{sdk.NewTestMsg(sdk.AccAddress(testAddr1.Bytes()))} msgs := []sdk.Msg{sdk.NewTestMsg(sdk.AccAddress(TestAddr1.Bytes()))}
// create message signing structure and bytes // create message signing structure and bytes
signBytes := GetStdTxSignBytes(testChainID.String(), 0, 0, newStdFee(), msgs, "") signBytes := GetStdTxSignBytes(TestChainID.String(), 0, 0, NewTestStdFee(), msgs, "")
// require signing not to fail // require signing not to fail
sig, err := ethcrypto.Sign(signBytes, testPrivKey1) sig, err := ethcrypto.Sign(signBytes, TestPrivKey1)
require.NoError(t, err) require.NoError(t, err)
// require signature to be valid // require signature to be valid
err = ValidateSigner(signBytes, sig, testAddr1) err = ValidateSigner(signBytes, sig, TestAddr1)
require.NoError(t, err) require.NoError(t, err)
sig, err = ethcrypto.Sign(signBytes, testPrivKey2) sig, err = ethcrypto.Sign(signBytes, TestPrivKey2)
require.NoError(t, err) require.NoError(t, err)
// require signature to be invalid // require signature to be invalid
err = ValidateSigner(signBytes, sig, testAddr1) err = ValidateSigner(signBytes, sig, TestAddr1)
require.Error(t, err)
// require invalid signature bytes return an error
err = ValidateSigner([]byte{}, sig, TestAddr2)
require.Error(t, err) require.Error(t, err)
} }

View File

@ -16,7 +16,4 @@ func init() {
func RegisterWire(codec *wire.Codec) { func RegisterWire(codec *wire.Codec) {
sdk.RegisterWire(codec) sdk.RegisterWire(codec)
codec.RegisterConcrete(&Account{}, "types/Account", nil) codec.RegisterConcrete(&Account{}, "types/Account", nil)
codec.RegisterConcrete(&EthSignature{}, "types/EthSignature", nil)
codec.RegisterConcrete(TxData{}, "types/TxData", nil)
codec.RegisterConcrete(Transaction{}, "types/Transaction", nil)
} }