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/types"
ethparams "github.com/ethereum/go-ethereum/params"
ethcmn "github.com/ethereum/go-ethereum/common"
abci "github.com/tendermint/tendermint/abci/types"
tmcmn "github.com/tendermint/tendermint/libs/common"
@ -58,12 +58,12 @@ type (
// NewEthermintApp returns a reference to a new initialized Ethermint
// application.
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 {
codec := CreateCodec()
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,
accountKey: sdk.NewKVStoreKey("acc"),
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
// structures and interfaces needed for the application.
// concrete types and interfaces needed for the application.
func CreateCodec() *wire.Codec {
codec := wire.NewCodec()

View File

@ -6,15 +6,30 @@ and subject to change.
## Routing
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
Ethereum transactions and utilizing the SDK's `auth.StdTx` for Cosmos
transactions. Both of these structures are registered with an [Amino](https://github.com/tendermint/go-amino) codec, so the `TxDecoder` that in invoked
during the `BaseApp#runTx`, will be able to decode raw transaction bytes into the
appropriate transaction type which will then be passed onto handlers downstream.
the Cosmos hub. We attempt to achieve this by mimicking [Geth's](https://github.com/ethereum/go-ethereum) `Transaction` structure and utilizing
the `Payload` as the potential encoding of a Cosmos-routed transaction. What
designates this encoding, and ultimately routing, is the `Recipient` address --
if this address matches some global unique predefined and configured address,
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,
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).
In addition, we aim to have existing tooling and frameworks in the Ethereum
ecosystem have 100% compatibility with creating transactions in Ethermint.
## 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)
signatures. A `Transaction` is expected to have a single signature for Ethereum
routed transactions. However, just as in Cosmos, Ethermint will support multiple
signers for `auth.StdTx` Cosmos routed transactions. Signatures over the
`Transaction` type are identical to Ethereum. However, the `auth.StdTx` contains
signers for embedded Cosmos routed transactions. Signatures over the
`Transaction` type are identical to Ethereum. However, the embedded transaction contains
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,
helps prevent "replay attacks", where the same message could be executed over and
over again.
An `auth.StdTx` list of signatures must much the unique list of addresses returned
by each message's `GetSigners` call. In addition, the address of first signer of
the `auth.StdTx` is responsible for paying the fees.
An embedded transaction's list of signatures must much the unique list of addresses
returned by each message's `GetSigners` call. In addition, the address of first
signer of the embedded transaction is responsible for paying the fees.
## Gas & Fees

View File

@ -9,8 +9,6 @@ import (
"github.com/cosmos/ethermint/types"
ethcmn "github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types"
)
const (
@ -39,7 +37,7 @@ func AnteHandler(am auth.AccountMapper, _ auth.FeeCollectionKeeper) sdk.AnteHand
switch tx := tx.(type) {
case types.Transaction:
gasLimit = int64(tx.Data.GasLimit)
gasLimit = int64(tx.Data().GasLimit)
handler = handleEthTx
case auth.StdTx:
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
}
// validate signature
gethTx := ethTx.ConvertTx(chainID)
signer := ethtypes.NewEIP155Signer(chainID)
sdkCtx.GasMeter().ConsumeGas(verifySigCost, "ante: verify Ethereum signature")
_, err := signer.Sender(&gethTx)
addr, err := ethTx.VerifySig(chainID)
if err != nil {
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
@ -122,15 +132,15 @@ func handleEmbeddedTx(sdkCtx sdk.Context, tx sdk.Tx, am auth.AccountMapper) (sdk
for i, sig := range stdTx.Signatures {
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 {
return sdkCtx, err.Result(), false
return sdkCtx, err.Result(), true
}
// TODO: Fees!
am.SetAccount(sdkCtx, signerAcc)
signerAccs[i] = signerAcc
am.SetAccount(sdkCtx, acc)
signerAccs[i] = acc
}
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
import (
"bytes"
"crypto/ecdsa"
"fmt"
"io"
"math/big"
"sync/atomic"
@ -13,17 +15,20 @@ import (
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"
)
var _ sdk.Tx = (*Transaction)(nil)
const (
// TypeTxEthereum reflects an Ethereum Transaction type.
TypeTxEthereum = "Ethereum"
)
// ----------------------------------------------------------------------------
// Ethereum transaction
// ----------------------------------------------------------------------------
var _ sdk.Tx = (*Transaction)(nil)
type (
// Transaction implements the Ethereum transaction structure as an exact
// 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
// basic validation that is done in the BaseApp.
Transaction struct {
Data TxData
data TxData
// caches
hash atomic.Value
@ -41,96 +46,136 @@ type (
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 {
AccountNonce uint64 `json:"nonce"`
Price sdk.Int `json:"gasPrice"`
Price *big.Int `json:"gasPrice"`
GasLimit uint64 `json:"gas"`
Recipient *ethcmn.Address `json:"to"` // nil means contract creation
Amount sdk.Int `json:"value"`
Recipient *ethcmn.Address `json:"to" rlp:"nil"` // nil means contract creation
Amount *big.Int `json:"value"`
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 *ethcmn.Hash `json:"hash"`
Hash *ethcmn.Hash `json:"hash" rlp:"-"`
}
// EthSignature reflects an Ethereum signature. We wrap this in a structure
// to support Amino serialization of transactions.
EthSignature struct {
v, r, s *big.Int
// sigCache is used to cache the derived sender and contains the signer used
// to derive it.
sigCache struct {
signer ethtypes.Signer
from ethcmn.Address
}
)
// NewEthSignature returns a new instantiated Ethereum signature.
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.
// NewTransaction returns a reference to a new Ethereum transaction.
func NewTransaction(
nonce uint64, to ethcmn.Address, amount sdk.Int,
gasLimit uint64, gasPrice sdk.Int, payload []byte,
) Transaction {
nonce uint64, to ethcmn.Address, amount *big.Int, gasLimit uint64, gasPrice *big.Int, payload []byte,
) *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 {
payload = ethcmn.CopyBytes(payload)
}
txData := TxData{
Recipient: &to,
AccountNonce: nonce,
Recipient: to,
Payload: payload,
GasLimit: gasLimit,
Amount: amount,
Price: gasPrice,
Signature: NewEthSignature(new(big.Int), new(big.Int), new(big.Int)),
Amount: new(big.Int),
Price: 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
// 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 (tx *Transaction) Sign(chainID sdk.Int, priv *ecdsa.PrivateKey) {
h := rlpHash([]interface{}{
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),
})
func (tx *Transaction) Sign(chainID *big.Int, priv *ecdsa.PrivateKey) {
txHash := tx.SigHash(chainID)
sig, err := ethcrypto.Sign(h[:], priv)
sig, err := ethcrypto.Sign(txHash[:], priv)
if err != nil {
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})
} else {
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)
}
tx.Data.Signature.v = v
tx.Data.Signature.r = r
tx.Data.Signature.s = s
tx.data.V = v
tx.data.R = r
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
@ -165,11 +245,11 @@ func (tx Transaction) Type() string {
// ValidateBasic implements the sdk.Msg interface. It performs basic validation
// checks of a Transaction. If returns an sdk.Error if validation fails.
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")
}
if tx.Data.Amount.Sign() != 1 {
if tx.data.Amount.Sign() != 1 {
return ErrInvalidValue(DefaultCodespace, "amount must be positive")
}
@ -192,44 +272,57 @@ func (tx Transaction) GetMsgs() []sdk.Msg {
return []sdk.Msg{tx}
}
// ConvertTx attempts to converts a Transaction to a new Ethereum transaction
// with the signature set. The signature if first recovered and then a new
// Transaction is created with that signature. If setting the signature fails,
// a panic will be triggered.
//
// TODO: To be removed in #470
func (tx Transaction) ConvertTx(chainID *big.Int) ethtypes.Transaction {
gethTx := ethtypes.NewTransaction(
tx.Data.AccountNonce, *tx.Data.Recipient, tx.Data.Amount.BigInt(),
tx.Data.GasLimit, tx.Data.Price.BigInt(), tx.Data.Payload,
)
sig := recoverEthSig(tx.Data.Signature, chainID)
signer := ethtypes.NewEIP155Signer(chainID)
gethTx, err := gethTx.WithSignature(signer, sig)
if err != nil {
panic(errors.Wrap(err, "failed to convert transaction with a given signature"))
}
return *gethTx
// hasEmbeddedTx returns a boolean reflecting if the transaction contains an
// SDK transaction or not based on the recipient address.
func (tx Transaction) hasEmbeddedTx(addr ethcmn.Address) bool {
return bytes.Equal(tx.data.Recipient.Bytes(), addr.Bytes())
}
// TxDecoder returns an sdk.TxDecoder that given raw transaction bytes,
// attempts to decode them into a valid sdk.Tx.
func TxDecoder(codec *wire.Codec) sdk.TxDecoder {
// GetEmbeddedTx returns the embedded SDK transaction from an Ethereum
// transaction. It returns an error if decoding the inner transaction fails.
//
// CONTRACT: The payload field of an Ethereum transaction must contain a valid
// encoded SDK transaction.
func (tx Transaction) GetEmbeddedTx(codec *wire.Codec) (sdk.Tx, sdk.Error) {
var etx sdk.Tx
err := codec.UnmarshalBinary(tx.data.Payload, &etx)
if err != nil {
return etx, sdk.ErrTxDecode("failed to decode embedded transaction")
}
return etx, nil
}
// ----------------------------------------------------------------------------
// Utilities
// ----------------------------------------------------------------------------
// 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) {
var tx = Transaction{}
if len(txBytes) == 0 {
return nil, sdk.ErrTxDecode("txBytes are empty")
return nil, sdk.ErrTxDecode("transaction bytes are empty")
}
var tx sdk.Tx
// The given codec should have all the appropriate message types
// registered.
err := codec.UnmarshalBinary(txBytes, &tx)
err := rlp.DecodeBytes(txBytes, &tx)
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
@ -237,20 +330,20 @@ func TxDecoder(codec *wire.Codec) sdk.TxDecoder {
}
// 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
r, s := es.r.Bytes(), es.s.Bytes()
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(es.v.Uint64() - 27)
v = byte(Vb.Uint64() - 27)
} else {
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)
}
@ -259,43 +352,11 @@ func recoverEthSig(es *EthSignature, chainID *big.Int) []byte {
return sig
}
func rlpHash(x interface{}) (h ethcmn.Hash) {
func rlpHash(x interface{}) (hash ethcmn.Hash) {
hasher := ethsha.NewKeccak256()
rlp.Encode(hasher, x)
hasher.Sum(h[: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
}
hasher.Sum(hash[:0])
return
}

View File

@ -3,83 +3,57 @@ package types
import (
"crypto/ecdsa"
"fmt"
"math/big"
"testing"
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"
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"
)
var (
testChainID = sdk.NewInt(3)
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})
testPrivKey1, _ = ethcrypto.GenerateKey()
testPrivKey2, _ = ethcrypto.GenerateKey()
txRLP, err := rlp.EncodeToBytes(txs[0])
require.NoError(t, err)
testAddr1 = PrivKeyToEthAddress(testPrivKey1)
testAddr2 = PrivKeyToEthAddress(testPrivKey2)
)
gtxRLP, err := rlp.EncodeToBytes(gtxs[0])
require.NoError(t, err)
func newTestCodec() *wire.Codec {
codec := wire.NewCodec()
RegisterWire(codec)
codec.RegisterConcrete(auth.StdTx{}, "test/StdTx", nil)
codec.RegisterConcrete(&sdk.TestMsg{}, "test/TestMsg", nil)
wire.RegisterCrypto(codec)
return codec
require.Equal(t, gtxRLP, txRLP)
}
func newStdFee() auth.StdFee {
return auth.NewStdFee(5000, sdk.NewCoin("photon", sdk.NewInt(150)))
}
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})
func newTestStdTx(
chainID sdk.Int, msgs []sdk.Msg, pKeys []*ecdsa.PrivateKey,
accNums []int64, seqs []int64, fee auth.StdFee,
) sdk.Tx {
txRLP, err := rlp.EncodeToBytes(txs[0])
require.NoError(t, err)
sigs := make([]auth.StdSignature, len(pKeys))
gtxRLP, err := rlp.EncodeToBytes(gtxs[0])
require.NoError(t, err)
for i, priv := range pKeys {
signBytes := GetStdTxSignBytes(chainID.String(), accNums[i], seqs[i], newStdFee(), msgs, "")
var (
decodedTx Transaction
decodedGtx ethtypes.Transaction
)
sig, err := ethcrypto.Sign(signBytes, priv)
if err != nil {
panic(err)
}
err = rlp.DecodeBytes(txRLP, &decodedTx)
require.NoError(t, err)
sigs[i] = auth.StdSignature{Signature: sig, AccountNumber: accNums[i], Sequence: seqs[i]}
}
err = rlp.DecodeBytes(gtxRLP, &decodedGtx)
require.NoError(t, err)
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
require.Equal(t, decodedGtx.Hash(), decodedTx.Hash())
}
func TestValidation(t *testing.T) {
ethTxs := newTestEthTxs(
testChainID,
[]*ecdsa.PrivateKey{testPrivKey1},
[]ethcmn.Address{testAddr1},
ethTxs := NewTestEthTxs(
TestChainID, []int64{0}, []ethcmn.Address{TestAddr1}, []*ecdsa.PrivateKey{TestPrivKey1},
)
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 {
tx := msg.(Transaction)
tx.Data.Price = sdk.NewInt(-1)
tx := msg.(*Transaction)
tx.data.Price = big.NewInt(-1)
return tx
}, true},
{ethTxs[0], func(msg sdk.Msg) sdk.Msg {
tx := msg.(Transaction)
tx.Data.Amount = sdk.NewInt(-1)
tx := msg.(*Transaction)
tx.data.Amount = big.NewInt(-1)
return tx
}, true},
}
@ -112,59 +86,57 @@ func TestValidation(t *testing.T) {
}
}
func TestTransactionGetMsgs(t *testing.T) {
ethTxs := newTestEthTxs(
testChainID,
[]*ecdsa.PrivateKey{testPrivKey1},
[]ethcmn.Address{testAddr1},
func TestTransactionVerifySig(t *testing.T) {
txs := NewTestEthTxs(
TestChainID, []int64{0}, []ethcmn.Address{TestAddr1}, []*ecdsa.PrivateKey{TestPrivKey1},
)
msgs := ethTxs[0].GetMsgs()
require.Len(t, msgs, 1)
require.Equal(t, ethTxs[0], msgs[0])
addr, err := txs[0].VerifySig(TestChainID)
require.NoError(t, err)
require.Equal(t, TestAddr1, addr)
expectedMsgs := []sdk.Msg{sdk.NewTestMsg(sdk.AccAddress(testAddr1.Bytes()))}
etx := newTestStdTx(
testChainID, expectedMsgs, []*ecdsa.PrivateKey{testPrivKey1},
[]int64{0}, []int64{0}, newStdFee(),
)
msgs = etx.GetMsgs()
require.Len(t, msgs, len(expectedMsgs))
require.Equal(t, expectedMsgs, msgs)
addr, err = txs[0].VerifySig(big.NewInt(100))
require.Error(t, err)
require.NotEqual(t, TestAddr1, addr)
}
func TestTxDecoder(t *testing.T) {
testCodec := newTestCodec()
txDecoder := TxDecoder(testCodec)
testCodec := NewTestCodec()
txDecoder := TxDecoder(testCodec, TestSDKAddr)
msgs := []sdk.Msg{sdk.NewTestMsg()}
// create a non-SDK Ethereum transaction
emintTx := NewTransaction(
uint64(0), testAddr1, sdk.NewInt(10), 100, sdk.NewInt(100), nil,
txs := NewTestEthTxs(
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
txBytes := testCodec.MustMarshalBinary(emintTx)
tx, err := txDecoder(txBytes)
decodedTx, err := txDecoder(txBytes)
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
stdTx := newTestStdTx(
testChainID, msgs, []*ecdsa.PrivateKey{testPrivKey1},
[]int64{0}, []int64{0}, newStdFee(),
txs = NewTestSDKTxs(
testCodec, TestChainID, TestSDKAddr, msgs, []int64{0}, []int64{0},
[]*ecdsa.PrivateKey{TestPrivKey1}, NewTestStdFee(),
)
// require the transaction to properly decode into a Transaction
txBytes = testCodec.MustMarshalBinary(stdTx)
tx, err = txDecoder(txBytes)
txBytes, err = rlp.EncodeToBytes(txs[0])
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
tx, err = txDecoder([]byte{})
decodedTx, err = txDecoder([]byte{})
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)
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 {
return fmt.Errorf("invalid signature for signer: %s", signer)
}

View File

@ -10,23 +10,27 @@ import (
)
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
signBytes := GetStdTxSignBytes(testChainID.String(), 0, 0, newStdFee(), msgs, "")
signBytes := GetStdTxSignBytes(TestChainID.String(), 0, 0, NewTestStdFee(), msgs, "")
// require signing not to fail
sig, err := ethcrypto.Sign(signBytes, testPrivKey1)
sig, err := ethcrypto.Sign(signBytes, TestPrivKey1)
require.NoError(t, err)
// require signature to be valid
err = ValidateSigner(signBytes, sig, testAddr1)
err = ValidateSigner(signBytes, sig, TestAddr1)
require.NoError(t, err)
sig, err = ethcrypto.Sign(signBytes, testPrivKey2)
sig, err = ethcrypto.Sign(signBytes, TestPrivKey2)
require.NoError(t, err)
// 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)
}

View File

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