442 lines
12 KiB
Go
442 lines
12 KiB
Go
package types
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/ecdsa"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/big"
|
|
"sync/atomic"
|
|
|
|
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"
|
|
ethsha "github.com/ethereum/go-ethereum/crypto/sha3"
|
|
"github.com/ethereum/go-ethereum/rlp"
|
|
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
const (
|
|
// TypeTxEthereum reflects an Ethereum Transaction type.
|
|
TypeTxEthereum = "Ethereum"
|
|
)
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Ethereum transaction
|
|
// ----------------------------------------------------------------------------
|
|
|
|
type (
|
|
// Transaction implements the Ethereum transaction structure as an exact
|
|
// copy. It implements the Cosmos sdk.Tx interface. Due to the private
|
|
// fields, it must be replicated here and cannot be embedded or used
|
|
// directly.
|
|
//
|
|
// Note: The transaction also implements the sdk.Msg interface to perform
|
|
// basic validation that is done in the BaseApp.
|
|
Transaction struct {
|
|
Data TxData
|
|
|
|
// caches
|
|
hash atomic.Value
|
|
size atomic.Value
|
|
from atomic.Value
|
|
}
|
|
|
|
// 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"`
|
|
GasLimit uint64 `json:"gas"`
|
|
Recipient *ethcmn.Address `json:"to"` // nil means contract creation
|
|
Amount sdk.Int `json:"value"`
|
|
Payload []byte `json:"input"`
|
|
Signature *EthSignature `json:"signature"`
|
|
|
|
// hash is only used when marshaling to JSON
|
|
Hash *ethcmn.Hash `json:"hash"`
|
|
}
|
|
|
|
// EthSignature reflects an Ethereum signature. We wrap this in a structure
|
|
// to support Amino serialization of transactions.
|
|
EthSignature struct {
|
|
v, r, s *big.Int
|
|
}
|
|
)
|
|
|
|
// 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.
|
|
func NewTransaction(
|
|
nonce uint64, to ethcmn.Address, amount sdk.Int,
|
|
gasLimit uint64, gasPrice sdk.Int, payload []byte,
|
|
) Transaction {
|
|
|
|
if len(payload) > 0 {
|
|
payload = ethcmn.CopyBytes(payload)
|
|
}
|
|
|
|
txData := TxData{
|
|
Recipient: &to,
|
|
AccountNonce: nonce,
|
|
Payload: payload,
|
|
GasLimit: gasLimit,
|
|
Amount: amount,
|
|
Price: gasPrice,
|
|
Signature: NewEthSignature(new(big.Int), new(big.Int), new(big.Int)),
|
|
}
|
|
|
|
return Transaction{Data: txData}
|
|
}
|
|
|
|
// 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),
|
|
})
|
|
|
|
sig, err := ethcrypto.Sign(h[:], 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.BigInt(), big.NewInt(2))
|
|
v.Add(v, chainIDMul)
|
|
}
|
|
|
|
tx.Data.Signature.v = v
|
|
tx.Data.Signature.r = r
|
|
tx.Data.Signature.s = s
|
|
}
|
|
|
|
// Type implements the sdk.Msg interface. It returns the type of the
|
|
// Transaction.
|
|
func (tx Transaction) Type() string {
|
|
return TypeTxEthereum
|
|
}
|
|
|
|
// 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 {
|
|
return ErrInvalidValue(DefaultCodespace, "price must be positive")
|
|
}
|
|
|
|
if tx.Data.Amount.Sign() != 1 {
|
|
return ErrInvalidValue(DefaultCodespace, "amount must be positive")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetSignBytes performs a no-op and should not be used. It implements the
|
|
// sdk.Msg Interface
|
|
func (tx Transaction) GetSignBytes() (sigBytes []byte) { return }
|
|
|
|
// GetSigners performs a no-op and should not be used. It implements the
|
|
// sdk.Msg Interface
|
|
//
|
|
// CONTRACT: The transaction must already be signed.
|
|
func (tx Transaction) GetSigners() (signers []sdk.AccAddress) { return }
|
|
|
|
// GetMsgs returns a single message containing the Transaction itself. It
|
|
// implements the Cosmos sdk.Tx interface.
|
|
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.
|
|
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())
|
|
}
|
|
|
|
// 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) (EmbeddedTx, sdk.Error) {
|
|
etx := EmbeddedTx{}
|
|
|
|
err := codec.UnmarshalBinary(tx.Data.Payload, &etx)
|
|
if err != nil {
|
|
return EmbeddedTx{}, sdk.ErrTxDecode("failed to encode embedded tx")
|
|
}
|
|
|
|
return etx, nil
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// embedded SDK transaction
|
|
// ----------------------------------------------------------------------------
|
|
|
|
type (
|
|
// EmbeddedTx implements an SDK transaction. It is to be encoded into the
|
|
// payload field of an Ethereum transaction in order to route and handle SDK
|
|
// transactions.
|
|
EmbeddedTx struct {
|
|
Messages []sdk.Msg `json:"messages"`
|
|
Fee auth.StdFee `json:"fee"`
|
|
Signatures [][]byte `json:"signatures"`
|
|
}
|
|
|
|
// embeddedSignDoc implements a simple SignDoc for a EmbeddedTx signer to
|
|
// sign over.
|
|
embeddedSignDoc struct {
|
|
ChainID string `json:"chainID"`
|
|
AccountNumber int64 `json:"accountNumber"`
|
|
Sequence int64 `json:"sequence"`
|
|
Messages []json.RawMessage `json:"messages"`
|
|
Fee json.RawMessage `json:"fee"`
|
|
}
|
|
|
|
// EmbeddedTxSign implements a structure for containing the information
|
|
// necessary for building and signing an EmbeddedTx.
|
|
EmbeddedTxSign struct {
|
|
ChainID string
|
|
AccountNumber int64
|
|
Sequence int64
|
|
Messages []sdk.Msg
|
|
Fee auth.StdFee
|
|
}
|
|
)
|
|
|
|
// GetMsgs implements the sdk.Tx interface. It returns all the SDK transaction
|
|
// messages.
|
|
func (etx EmbeddedTx) GetMsgs() []sdk.Msg {
|
|
return etx.Messages
|
|
}
|
|
|
|
// GetRequiredSigners returns all the required signers of an SDK transaction
|
|
// accumulated from messages. It returns them in a deterministic fashion given
|
|
// a list of messages.
|
|
func (etx EmbeddedTx) GetRequiredSigners() []sdk.AccAddress {
|
|
seen := map[string]bool{}
|
|
|
|
var signers []sdk.AccAddress
|
|
for _, msg := range etx.GetMsgs() {
|
|
for _, addr := range msg.GetSigners() {
|
|
if !seen[addr.String()] {
|
|
signers = append(signers, sdk.AccAddress(addr))
|
|
seen[addr.String()] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
return signers
|
|
}
|
|
|
|
// Bytes returns the EmbeddedTxSign signature bytes for a signer to sign over.
|
|
func (ets EmbeddedTxSign) Bytes() ([]byte, error) {
|
|
sigBytes, err := EmbeddedSignBytes(ets.ChainID, ets.AccountNumber, ets.Sequence, ets.Messages, ets.Fee)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hash := sha256.Sum256(sigBytes)
|
|
return hash[:], nil
|
|
}
|
|
|
|
// EmbeddedSignBytes creates signature bytes for a signer to sign an embedded
|
|
// transaction. The signature bytes require a chainID and an account number.
|
|
// The signature bytes are JSON encoded.
|
|
func EmbeddedSignBytes(chainID string, accnum, sequence int64, msgs []sdk.Msg, fee auth.StdFee) ([]byte, error) {
|
|
var msgsBytes []json.RawMessage
|
|
for _, msg := range msgs {
|
|
msgsBytes = append(msgsBytes, json.RawMessage(msg.GetSignBytes()))
|
|
}
|
|
|
|
signDoc := embeddedSignDoc{
|
|
ChainID: chainID,
|
|
AccountNumber: accnum,
|
|
Sequence: sequence,
|
|
Messages: msgsBytes,
|
|
Fee: json.RawMessage(fee.Bytes()),
|
|
}
|
|
|
|
bz, err := typesCodec.MarshalJSON(signDoc)
|
|
if err != nil {
|
|
errors.Wrap(err, "failed to JSON encode EmbeddedSignDoc")
|
|
}
|
|
|
|
return bz, nil
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Utilities
|
|
// ----------------------------------------------------------------------------
|
|
|
|
// TxDecoder returns an sdk.TxDecoder that given raw transaction bytes,
|
|
// 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")
|
|
}
|
|
|
|
// The given codec should have all the appropriate message types
|
|
// registered.
|
|
err := codec.UnmarshalBinary(txBytes, &tx)
|
|
if err != nil {
|
|
return nil, sdk.ErrTxDecode("failed to decode tx").TraceSDK(err.Error())
|
|
}
|
|
|
|
// If the transaction is routed as an SDK transaction, decode and
|
|
// return the embedded transaction.
|
|
if tx.HasEmbeddedTx(sdkAddress) {
|
|
etx, err := tx.GetEmbeddedTx(codec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return etx, nil
|
|
}
|
|
|
|
return tx, nil
|
|
}
|
|
}
|
|
|
|
// recoverEthSig recovers a signature according to the Ethereum specification.
|
|
func recoverEthSig(es *EthSignature, chainID *big.Int) []byte {
|
|
var v byte
|
|
|
|
r, s := es.r.Bytes(), es.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)
|
|
} else {
|
|
chainIDMul := new(big.Int).Mul(chainID, big.NewInt(2))
|
|
V := new(big.Int).Sub(es.v, chainIDMul)
|
|
|
|
v = byte(V.Uint64() - 35)
|
|
}
|
|
|
|
sig[64] = v
|
|
return sig
|
|
}
|
|
|
|
func rlpHash(x interface{}) (h 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
|
|
}
|
|
|
|
return
|
|
}
|