Skip to content

Commit

Permalink
[action] support unprotected and EIP-2930 transaction
Browse files Browse the repository at this point in the history
  • Loading branch information
dustinxie committed Nov 8, 2023
1 parent 8e73e2b commit 81eba85
Show file tree
Hide file tree
Showing 11 changed files with 205 additions and 31 deletions.
80 changes: 72 additions & 8 deletions action/rlp_tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,18 @@ import (
"github.com/ethereum/go-ethereum/rlp"
"github.com/iotexproject/go-pkgs/crypto"
"github.com/iotexproject/go-pkgs/hash"
"github.com/iotexproject/iotex-proto/golang/iotextypes"
"github.com/pkg/errors"
"golang.org/x/crypto/sha3"
)

func rlpRawHash(rawTx *types.Transaction, chainID uint32) (hash.Hash256, error) {
h := types.NewEIP155Signer(big.NewInt(int64(chainID))).Hash(rawTx)
func rlpRawHash(rawTx *types.Transaction, signer types.Signer) (hash.Hash256, error) {
h := signer.Hash(rawTx)
return hash.BytesToHash256(h[:]), nil
}

func rlpSignedHash(tx *types.Transaction, chainID uint32, sig []byte) (hash.Hash256, error) {
signedTx, err := RawTxToSignedTx(tx, chainID, sig)
func rlpSignedHash(tx *types.Transaction, signer types.Signer, sig []byte) (hash.Hash256, error) {
signedTx, err := RawTxToSignedTx(tx, signer, sig)
if err != nil {
return hash.ZeroHash256, err
}
Expand All @@ -31,7 +32,7 @@ func rlpSignedHash(tx *types.Transaction, chainID uint32, sig []byte) (hash.Hash
}

// RawTxToSignedTx converts the raw tx to corresponding signed tx
func RawTxToSignedTx(rawTx *types.Transaction, chainID uint32, sig []byte) (*types.Transaction, error) {
func RawTxToSignedTx(rawTx *types.Transaction, signer types.Signer, sig []byte) (*types.Transaction, error) {
if len(sig) != 65 {
return nil, errors.Errorf("invalid signature length = %d, expecting 65", len(sig))
}
Expand All @@ -41,9 +42,7 @@ func RawTxToSignedTx(rawTx *types.Transaction, chainID uint32, sig []byte) (*typ
sc[64] -= 27
}

// TODO: currently all our web3 tx are EIP-155 protected tx
// in the future release, use proper signer for other supported tx types (EIP-1559, EIP-2930)
signedTx, err := rawTx.WithSignature(types.NewEIP155Signer(big.NewInt(int64(chainID))), sc)
signedTx, err := rawTx.WithSignature(signer, sc)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -83,3 +82,68 @@ func DecodeRawTx(rawData string, chainID uint32) (tx *types.Transaction, sig []b
pubkey, err = crypto.RecoverPubkey(rawHash[:], sig)
return
}

// DecodeEtherTx decodes raw data string into eth tx
func DecodeEtherTx(rawData string) (*types.Transaction, iotextypes.Encoding, []byte, crypto.PublicKey, error) {
var (
tx types.Transaction
encoding iotextypes.Encoding
sig []byte
pubkey crypto.PublicKey
)
//remove Hex prefix and decode string to byte
rawData = strings.Replace(rawData, "0x", "", -1)
rawData = strings.Replace(rawData, "0X", "", -1)
rawTxBytes, err := hex.DecodeString(rawData)
if err != nil {
return &tx, encoding, sig, pubkey, err
}

// decode raw data into rlp tx
if err = tx.UnmarshalBinary(rawTxBytes); err != nil {
return &tx, encoding, sig, pubkey, err
}

// extract correct V value
var (
signer types.Signer
V, R, S = tx.RawSignatureValues()
)
switch tx.Type() {
case types.LegacyTxType:
if tx.Protected() {
chainIdMul := tx.ChainId()

Check failure on line 115 in action/rlp_tx.go

View workflow job for this annotation

GitHub Actions / ci flow

var chainIdMul should be chainIDMul
V = new(big.Int).Sub(V, chainIdMul.Lsh(chainIdMul, 1))
V.Sub(V, big.NewInt(8))
encoding = iotextypes.Encoding_ETHEREUM_EIP155
signer = types.NewEIP155Signer(tx.ChainId())
} else {
// tx has pre-EIP155 signature
encoding = iotextypes.Encoding_ETHEREUM_UNPROTECTED
signer = types.HomesteadSigner{}
}
case types.AccessListTxType:
// AL txs are defined to use 0 and 1 as their recovery
// id, add 27 to become equivalent to unprotected Homestead signatures.
V = new(big.Int).Add(V, big.NewInt(27))
encoding = iotextypes.Encoding_ETHEREUM_ACCESSLIST
signer = types.NewEIP2930Signer(tx.ChainId())
default:
return &tx, encoding, sig, pubkey, ErrNotSupported
}

// construct signature
if V.BitLen() > 8 {
return &tx, encoding, sig, pubkey, ErrNotSupported
}
r, s := R.Bytes(), S.Bytes()
sig = make([]byte, 65)
copy(sig[32-len(r):32], r)
copy(sig[64-len(s):64], s)
sig[64] = byte(V.Uint64())

// recover public key
rawHash := signer.Hash(&tx)
pubkey, err = crypto.RecoverPubkey(rawHash[:], sig)
return &tx, encoding, sig, pubkey, err
}
32 changes: 28 additions & 4 deletions action/rlp_tx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ func TestGenerateRlp(t *testing.T) {
require.Contains(err.Error(), v.err)
continue
}
h, err := rlpSignedHash(tx, _evmNetworkID, v.sig)
signer, err := EthSigner(iotextypes.Encoding_ETHEREUM_EIP155, _evmNetworkID)
h, err := rlpSignedHash(tx, signer, v.sig)
if err != nil {
require.Contains(err.Error(), v.err)
}
Expand Down Expand Up @@ -289,14 +290,18 @@ func TestRlpDecodeVerify(t *testing.T) {

for _, v := range rlpTests {
// decode received RLP tx
tx, sig, pubkey, err := DecodeRawTx(v.raw, _evmNetworkID)
tx, encoding, sig, pubkey, err := DecodeEtherTx(v.raw)
require.NoError(err)
require.EqualValues(types.LegacyTxType, tx.Type())
require.True(tx.Protected())
require.EqualValues(1, encoding)
require.EqualValues(_evmNetworkID, tx.ChainId().Uint64())
require.Equal(v.pubkey, pubkey.HexString())
require.Equal(v.pkhash, hex.EncodeToString(pubkey.Hash()))

// convert to our Execution
pb := &iotextypes.Action{
Encoding: iotextypes.Encoding_ETHEREUM_RLP,
Encoding: iotextypes.Encoding_ETHEREUM_EIP155,
}
pb.Core = convertToNativeProto(tx, v.actType)
pb.SenderPubKey = pubkey.Bytes()
Expand Down Expand Up @@ -389,17 +394,36 @@ func TestIssue3944(t *testing.T) {
S: new(big.Int).SetBytes(sig[32:64]),
})

r.EqualValues(types.LegacyTxType, tx.Type())
r.False(tx.Protected())
v, q, s := tx.RawSignatureValues()
r.Equal(sig[:32], q.Bytes())
r.Equal(sig[32:64], s.Bytes())
r.Equal("1b", v.Text(16))
r.NotEqual(hash, tx.Hash().Hex()) // hash does not match with wrong V value in signature

tx1, err := RawTxToSignedTx(tx, 4690, sig)
signer, err := EthSigner(iotextypes.Encoding_ETHEREUM_EIP155, 4690)
r.NoError(err)
tx1, err := RawTxToSignedTx(tx, signer, sig)
r.NoError(err)
r.True(tx1.Protected())
r.EqualValues(4690, tx1.ChainId().Uint64())
v, q, s = tx1.RawSignatureValues()
r.Equal(sig[:32], q.Bytes())
r.Equal(sig[32:64], s.Bytes())
r.Equal("9415", v.String()) // this is the correct V value corresponding to chainID = 4690
r.Equal(hash, tx1.Hash().Hex())
}

func TestDetermine(t *testing.T) {
r := require.New(t)

tx, encoding, sig, pubkey, err := DecodeEtherTx("0xf8a58085174876e800830186a08080b853604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf31ba02222222222222222222222222222222222222222222222222222222222222222a02222222222222222222222222222222222222222222222222222222222222222")
r.EqualValues(types.LegacyTxType, tx.Type())
r.False(tx.Protected())
r.EqualValues(2, encoding)
r.Zero(tx.ChainId().Uint64())
r.Equal(65, len(sig))
r.Equal("3fab184622dc19b6109349b94811493bf2a45362", hex.EncodeToString(pubkey.Hash()))
r.NoError(err)
}
46 changes: 36 additions & 10 deletions action/sealedenvelope.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package action

import (
"encoding/hex"
"math/big"

"github.com/ethereum/go-ethereum/core/types"
"github.com/iotexproject/go-pkgs/crypto"
"github.com/iotexproject/go-pkgs/hash"
"github.com/iotexproject/iotex-address/address"
Expand Down Expand Up @@ -30,7 +32,7 @@ type SealedEnvelope struct {
// an all-0 return value means the transaction is invalid
func (sealed *SealedEnvelope) envelopeHash() (hash.Hash256, error) {
switch sealed.encoding {
case iotextypes.Encoding_ETHEREUM_RLP:
case iotextypes.Encoding_ETHEREUM_EIP155, iotextypes.Encoding_ETHEREUM_UNPROTECTED, iotextypes.Encoding_ETHEREUM_ACCESSLIST:
act, ok := sealed.Action().(EthCompatibleAction)
if !ok {
return hash.ZeroHash256, ErrInvalidAct
Expand All @@ -39,14 +41,31 @@ func (sealed *SealedEnvelope) envelopeHash() (hash.Hash256, error) {
if err != nil {
return hash.ZeroHash256, err
}
return rlpRawHash(tx, sealed.evmNetworkID)
signer, err := EthSigner(sealed.encoding, sealed.evmNetworkID)
if err != nil {
return hash.ZeroHash256, err
}
return rlpRawHash(tx, signer)
case iotextypes.Encoding_IOTEX_PROTOBUF:
return hash.Hash256b(byteutil.Must(proto.Marshal(sealed.Envelope.Proto()))), nil
default:
return hash.ZeroHash256, errors.Errorf("unknown encoding type %v", sealed.encoding)
}
}

func EthSigner(txType iotextypes.Encoding, chainID uint32) (types.Signer, error) {

Check failure on line 56 in action/sealedenvelope.go

View workflow job for this annotation

GitHub Actions / ci flow

exported function EthSigner should have comment or be unexported
switch txType {
case iotextypes.Encoding_ETHEREUM_EIP155:
return types.NewEIP155Signer(big.NewInt(int64(chainID))), nil
case iotextypes.Encoding_ETHEREUM_UNPROTECTED:
return types.HomesteadSigner{}, nil
case iotextypes.Encoding_ETHEREUM_ACCESSLIST:
return types.NewEIP2930Signer(big.NewInt(int64(chainID))), nil
default:
return nil, ErrInvalidAct
}
}

// Hash returns the hash value of SealedEnvelope.
// an all-0 return value means the transaction is invalid
func (sealed *SealedEnvelope) Hash() (hash.Hash256, error) {
Expand All @@ -62,7 +81,7 @@ func (sealed *SealedEnvelope) Hash() (hash.Hash256, error) {

func (sealed *SealedEnvelope) calcHash() (hash.Hash256, error) {
switch sealed.encoding {
case iotextypes.Encoding_ETHEREUM_RLP:
case iotextypes.Encoding_ETHEREUM_EIP155, iotextypes.Encoding_ETHEREUM_UNPROTECTED, iotextypes.Encoding_ETHEREUM_ACCESSLIST:
act, ok := sealed.Action().(EthCompatibleAction)
if !ok {
return hash.ZeroHash256, ErrInvalidAct
Expand All @@ -71,7 +90,11 @@ func (sealed *SealedEnvelope) calcHash() (hash.Hash256, error) {
if err != nil {
return hash.ZeroHash256, err
}
return rlpSignedHash(tx, sealed.evmNetworkID, sealed.Signature())
signer, err := EthSigner(sealed.encoding, sealed.evmNetworkID)
if err != nil {
return hash.ZeroHash256, err
}
return rlpSignedHash(tx, signer, sealed.Signature())
case iotextypes.Encoding_IOTEX_PROTOBUF:
return hash.Hash256b(byteutil.Must(proto.Marshal(sealed.Proto()))), nil
default:
Expand Down Expand Up @@ -134,9 +157,9 @@ func (sealed *SealedEnvelope) loadProto(pbAct *iotextypes.Action, evmID uint32)
if err != nil {
return err
}
encoding := pbAct.GetEncoding()
switch encoding {
case iotextypes.Encoding_ETHEREUM_RLP:
sealed.encoding = pbAct.GetEncoding()
switch sealed.encoding {
case iotextypes.Encoding_ETHEREUM_EIP155, iotextypes.Encoding_ETHEREUM_UNPROTECTED, iotextypes.Encoding_ETHEREUM_ACCESSLIST:
// verify action type can support RLP-encoding
act, ok := elp.Action().(EthCompatibleAction)
if !ok {
Expand All @@ -146,22 +169,25 @@ func (sealed *SealedEnvelope) loadProto(pbAct *iotextypes.Action, evmID uint32)
if err != nil {
return err
}
if _, err = rlpSignedHash(tx, evmID, pbAct.GetSignature()); err != nil {
signer, err := EthSigner(sealed.encoding, sealed.evmNetworkID)
if err != nil {
return err
}
if _, err = rlpSignedHash(tx, signer, pbAct.GetSignature()); err != nil {
return err
}
sealed.evmNetworkID = evmID
case iotextypes.Encoding_IOTEX_PROTOBUF:
break
default:
return errors.Errorf("unknown encoding type %v", encoding)
return errors.Errorf("unknown encoding type %v", sealed.encoding)
}

// clear 'sealed' and populate new value
sealed.Envelope = elp
sealed.srcPubkey = srcPub
sealed.signature = make([]byte, sigSize)
copy(sealed.signature, pbAct.GetSignature())
sealed.encoding = encoding
sealed.hash = hash.ZeroHash256
sealed.srcAddress = nil
return nil
Expand Down
4 changes: 2 additions & 2 deletions action/sealedenvelope_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func TestSealedEnvelope_InvalidType(t *testing.T) {
SetAction(r).
SetGasLimit(100000).Build()
selp := FakeSeal(elp, identityset.PrivateKey(27).PublicKey())
selp.encoding = iotextypes.Encoding_ETHEREUM_RLP
selp.encoding = iotextypes.Encoding_ETHEREUM_EIP155
hash1, err := selp.envelopeHash()
require.Equal(hash1, hash.ZeroHash256)
require.Contains(err.Error(), "invalid action type")
Expand Down Expand Up @@ -148,7 +148,7 @@ func TestSealedEnvelope_Proto(t *testing.T) {
err string
}{
{0, _signByte, "invalid signature length ="},
{3, _validSig, "unknown encoding type"},
{iotextypes.Encoding_ETHEREUM_ACCESSLIST + 1, _validSig, "unknown encoding type"},
} {
se.encoding = v.encoding
se.signature = v.sig
Expand Down
7 changes: 7 additions & 0 deletions api/coreservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ type (
LogsInBlockByHash(filter *logfilter.LogFilter, blockHash hash.Hash256) ([]*action.Log, error)
// LogsInRange filter logs among [start, end] blocks
LogsInRange(filter *logfilter.LogFilter, start, end, paginationSize uint64) ([]*action.Log, []hash.Hash256, error)
// Genesis returns the genesis of the chain
Genesis() genesis.Genesis
// EVMNetworkID returns the network id of evm
EVMNetworkID() uint32
// ChainID returns the chain id of evm
Expand Down Expand Up @@ -1588,6 +1590,11 @@ func (core *coreService) ActionsInActPool(actHashes []string) ([]action.SealedEn
return ret, nil
}

// Genesis returns the genesis of the chain
func (core *coreService) Genesis() genesis.Genesis {
return core.bc.Genesis()
}

// EVMNetworkID returns the network id of evm
func (core *coreService) EVMNetworkID() uint32 {
return core.bc.EvmNetworkID()
Expand Down
25 changes: 22 additions & 3 deletions api/web3server.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/eth/tracers/logger"
"github.com/iotexproject/go-pkgs/crypto"
"github.com/iotexproject/go-pkgs/hash"
"github.com/iotexproject/go-pkgs/util"
"github.com/iotexproject/iotex-address/address"
Expand Down Expand Up @@ -73,6 +74,7 @@ var (
errInvalidFormat = errors.New("invalid format of request")
errNotImplemented = errors.New("method not implemented")
errInvalidFilterID = errors.New("filter not found")
errInvalidEvmChainID = errors.New("invalid EVM chain ID")
errInvalidBlock = errors.New("invalid block")
errUnsupportedAction = errors.New("the type of action is not supported")
errMsgBatchTooLarge = errors.New("batch too large")
Expand Down Expand Up @@ -433,7 +435,24 @@ func (svr *web3Handler) sendRawTransaction(in *gjson.Result) (interface{}, error
return nil, errInvalidFormat
}
// parse raw data string from json request
tx, sig, pubkey, err := action.DecodeRawTx(dataStr.String(), svr.coreService.EVMNetworkID())
var (
tx *types.Transaction
sig []byte
pubkey crypto.PublicKey
err error
encoding iotextypes.Encoding
cs = svr.coreService
)
if g := cs.Genesis(); g.IsSumatra(cs.TipHeight()) {
tx, encoding, sig, pubkey, err = action.DecodeEtherTx(dataStr.String())
if !tx.Protected() && tx.ChainId().Uint64() != uint64(cs.EVMNetworkID()) {
return nil, errInvalidEvmChainID
}
} else {
// before Sumatra height, all tx are EIP-155 format
tx, sig, pubkey, err = action.DecodeRawTx(dataStr.String(), cs.EVMNetworkID())
encoding = iotextypes.Encoding_ETHEREUM_EIP155
}
if err != nil {
return nil, err
}
Expand All @@ -445,9 +464,9 @@ func (svr *web3Handler) sendRawTransaction(in *gjson.Result) (interface{}, error
Core: elp.Proto(),
SenderPubKey: pubkey.Bytes(),
Signature: sig,
Encoding: iotextypes.Encoding_ETHEREUM_RLP,
Encoding: encoding,
}
actionHash, err := svr.coreService.SendAction(context.Background(), req)
actionHash, err := cs.SendAction(context.Background(), req)
if err != nil {
return nil, err
}
Expand Down
Loading

0 comments on commit 81eba85

Please sign in to comment.