From a37e7f5f3dbd6bc3a8fa174bbe7f57ee4d4b64c4 Mon Sep 17 00:00:00 2001 From: kbhat1 Date: Wed, 18 Mar 2026 14:04:34 -0400 Subject: [PATCH 1/2] Rewrite EVM field-bloat validation from scratch. The previous canonicalization approach was fundamentally broken: UnpackAny replaces all Any.Value bytes with canonical re-marshals before any validation runs, so comparing in-memory sizes always passed regardless of wire bloat. Additionally, Preprocess setting msg.Derived after UnpackAny caused a 2-byte length mismatch that rejected all normal EVM transactions at DeliverTx. This rewrite takes a correct three-layer approach: 1. Decoder level (sei-cosmos): reject tx bodies where the raw wire encoding is larger than the canonical re-marshal of the decoded struct, catching protobuf-level bloat before UnpackAny erases it. 2. Envelope level (ante): use GetProtoTx() instead of broken GetBody()/ GetAuthInfo() type assertions so Cosmos wrapper fields (memo, signatures, signer infos, fee fields) are actually enforced on wrapped EVM txs. 3. Semantic level (ethtx Validate): reject padded To addresses, leading-zero signature bytes, and oversized access-list/auth-list hex strings that survive proto round-trips but carry uncharged storage cost. Made-with: Cursor --- app/ante/evm_checktx.go | 41 +--------- giga/deps/xevm/types/ethtx/access_list_tx.go | 13 +++ giga/deps/xevm/types/ethtx/associate_tx.go | 13 ++- giga/deps/xevm/types/ethtx/blob_tx.go | 13 +++ giga/deps/xevm/types/ethtx/dynamic_fee_tx.go | 13 +++ giga/deps/xevm/types/ethtx/legacy_tx.go | 10 +++ .../xevm/types/ethtx/semantic_validation.go | 79 +++++++++++++++++++ giga/deps/xevm/types/ethtx/set_code_tx.go | 16 ++++ .../xevm/types/message_evm_transaction.go | 19 ++++- sei-cosmos/x/auth/tx/decoder.go | 20 +++++ sei-cosmos/x/auth/tx/encode_decode_test.go | 9 +-- x/evm/ante/no_cosmos_fields.go | 64 ++++++++++++--- x/evm/types/ethtx/access_list_tx.go | 13 +++ x/evm/types/ethtx/associate_tx.go | 13 ++- x/evm/types/ethtx/blob_tx.go | 13 +++ x/evm/types/ethtx/dynamic_fee_tx.go | 13 +++ x/evm/types/ethtx/legacy_tx.go | 10 +++ x/evm/types/ethtx/semantic_validation.go | 79 +++++++++++++++++++ x/evm/types/ethtx/set_code_tx.go | 16 ++++ x/evm/types/message_evm_transaction.go | 19 ++++- 20 files changed, 427 insertions(+), 59 deletions(-) create mode 100644 giga/deps/xevm/types/ethtx/semantic_validation.go create mode 100644 x/evm/types/ethtx/semantic_validation.go diff --git a/app/ante/evm_checktx.go b/app/ante/evm_checktx.go index 73878163ce..be23117907 100644 --- a/app/ante/evm_checktx.go +++ b/app/ante/evm_checktx.go @@ -75,45 +75,8 @@ func EvmCheckTxAnte( } func EvmStatelessChecks(ctx sdk.Context, tx sdk.Tx, chainID *big.Int) error { - txBody, ok := tx.(TxBody) - if ok { - body := txBody.GetBody() - if body.Memo != "" { - return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "memo must be empty for EVM txs") - } - if body.TimeoutHeight != 0 { - return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "timeout_height must be zero for EVM txs") - } - if len(body.ExtensionOptions) > 0 || len(body.NonCriticalExtensionOptions) > 0 { - return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "extension options must be empty for EVM txs") - } - } - - txAuth, ok := tx.(TxAuthInfo) - if ok { - authInfo := txAuth.GetAuthInfo() - if len(authInfo.SignerInfos) > 0 { - return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "signer_infos must be empty for EVM txs") - } - if authInfo.Fee != nil { - if len(authInfo.Fee.Amount) > 0 { - return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "fee amount must be empty for EVM txs") - } - if authInfo.Fee.Payer != "" || authInfo.Fee.Granter != "" { - return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "fee payer and granter must be empty for EVM txs") - } - } - } - - txSig, ok := tx.(TxSignaturesV2) - if ok { - sigs, err := txSig.GetSignaturesV2() - if err != nil { - return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "could not get signatures") - } - if len(sigs) > 0 { - return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "signatures must be empty for EVM txs") - } + if err := evmante.ValidateNoCosmosTxFields(tx); err != nil { + return err } if len(tx.GetMsgs()) != 1 { diff --git a/giga/deps/xevm/types/ethtx/access_list_tx.go b/giga/deps/xevm/types/ethtx/access_list_tx.go index 6acc097dce..04c8d86774 100644 --- a/giga/deps/xevm/types/ethtx/access_list_tx.go +++ b/giga/deps/xevm/types/ethtx/access_list_tx.go @@ -180,6 +180,19 @@ func (tx AccessListTx) Validate() error { } } + if err := validateAccessList(tx.Accesses); err != nil { + return err + } + if err := validateSignatureValue("v", tx.V, 32); err != nil { + return err + } + if err := validateSignatureValue("r", tx.R, 32); err != nil { + return err + } + if err := validateSignatureValue("s", tx.S, 32); err != nil { + return err + } + chainID := tx.GetChainID() if chainID == nil { diff --git a/giga/deps/xevm/types/ethtx/associate_tx.go b/giga/deps/xevm/types/ethtx/associate_tx.go index c1b2099365..d30893a319 100644 --- a/giga/deps/xevm/types/ethtx/associate_tx.go +++ b/giga/deps/xevm/types/ethtx/associate_tx.go @@ -37,7 +37,18 @@ func (tx *AssociateTx) GetRawSignatureValues() (v, r, s *big.Int) { func (tx *AssociateTx) SetSignatureValues(_, _, _, _ *big.Int) { panic("not implemented") } func (tx *AssociateTx) AsEthereumData() ethtypes.TxData { panic("not implemented") } -func (tx *AssociateTx) Validate() error { panic("not implemented") } +func (tx *AssociateTx) Validate() error { + if err := validateSignatureValue("v", tx.V, 32); err != nil { + return err + } + if err := validateSignatureValue("r", tx.R, 32); err != nil { + return err + } + if err := validateSignatureValue("s", tx.S, 32); err != nil { + return err + } + return nil +} func (tx *AssociateTx) Fee() *big.Int { panic("not implemented") } func (tx *AssociateTx) Cost() *big.Int { panic("not implemented") } diff --git a/giga/deps/xevm/types/ethtx/blob_tx.go b/giga/deps/xevm/types/ethtx/blob_tx.go index e57f82f8e8..9bc9b7330c 100644 --- a/giga/deps/xevm/types/ethtx/blob_tx.go +++ b/giga/deps/xevm/types/ethtx/blob_tx.go @@ -229,6 +229,19 @@ func (tx BlobTx) Validate() error { } } + if err := validateAccessList(tx.Accesses); err != nil { + return err + } + if err := validateSignatureValue("v", tx.V, 32); err != nil { + return err + } + if err := validateSignatureValue("r", tx.R, 32); err != nil { + return err + } + if err := validateSignatureValue("s", tx.S, 32); err != nil { + return err + } + chainID := tx.GetChainID() if chainID == nil { diff --git a/giga/deps/xevm/types/ethtx/dynamic_fee_tx.go b/giga/deps/xevm/types/ethtx/dynamic_fee_tx.go index 9d67756107..3b777d2ece 100644 --- a/giga/deps/xevm/types/ethtx/dynamic_fee_tx.go +++ b/giga/deps/xevm/types/ethtx/dynamic_fee_tx.go @@ -197,6 +197,19 @@ func (tx DynamicFeeTx) Validate() error { } } + if err := validateAccessList(tx.Accesses); err != nil { + return err + } + if err := validateSignatureValue("v", tx.V, 32); err != nil { + return err + } + if err := validateSignatureValue("r", tx.R, 32); err != nil { + return err + } + if err := validateSignatureValue("s", tx.S, 32); err != nil { + return err + } + chainID := tx.GetChainID() if chainID == nil { diff --git a/giga/deps/xevm/types/ethtx/legacy_tx.go b/giga/deps/xevm/types/ethtx/legacy_tx.go index ede8ce4e19..b9bdf4b032 100644 --- a/giga/deps/xevm/types/ethtx/legacy_tx.go +++ b/giga/deps/xevm/types/ethtx/legacy_tx.go @@ -170,6 +170,16 @@ func (tx *LegacyTx) Validate() error { } } + if err := validateSignatureValue("v", tx.V, 32); err != nil { + return err + } + if err := validateSignatureValue("r", tx.R, 32); err != nil { + return err + } + if err := validateSignatureValue("s", tx.S, 32); err != nil { + return err + } + chainID := tx.GetChainID() if chainID == nil { diff --git a/giga/deps/xevm/types/ethtx/semantic_validation.go b/giga/deps/xevm/types/ethtx/semantic_validation.go new file mode 100644 index 0000000000..4da1422952 --- /dev/null +++ b/giga/deps/xevm/types/ethtx/semantic_validation.go @@ -0,0 +1,79 @@ +package ethtx + +import ( + "encoding/hex" + "fmt" + + "github.com/ethereum/go-ethereum/common" +) + +func validateHexAddress(fieldName, value string) error { + if len(value) >= 2 && value[0] == '0' && (value[1] == 'x' || value[1] == 'X') { + value = value[2:] + } + if len(value) != common.AddressLength*2 { + return fmt.Errorf("invalid %s: wrong length", fieldName) + } + if _, err := hex.DecodeString(value); err != nil { + return fmt.Errorf("invalid %s", fieldName) + } + return nil +} + +func validateHexHash(fieldName, value string) error { + if len(value) >= 2 && value[0] == '0' && (value[1] == 'x' || value[1] == 'X') { + value = value[2:] + } + if len(value) != common.HashLength*2 { + return fmt.Errorf("invalid %s: wrong length", fieldName) + } + if _, err := hex.DecodeString(value); err != nil { + return fmt.Errorf("invalid %s", fieldName) + } + return nil +} + +func validateAccessList(accessList AccessList) error { + for _, tuple := range accessList { + if err := validateHexAddress("access list address", tuple.Address); err != nil { + return err + } + for _, storageKey := range tuple.StorageKeys { + if err := validateHexHash("access list storage key", storageKey); err != nil { + return err + } + } + } + return nil +} + +func validateAuthList(authList AuthList) error { + for _, auth := range authList { + if auth.ChainID == nil { + return fmt.Errorf("auth list chain id cannot be nil") + } + if err := validateHexAddress("auth list address", auth.Address); err != nil { + return err + } + if err := validateSignatureValue("auth list v", auth.V, 1); err != nil { + return err + } + if err := validateSignatureValue("auth list r", auth.R, 32); err != nil { + return err + } + if err := validateSignatureValue("auth list s", auth.S, 32); err != nil { + return err + } + } + return nil +} + +func validateSignatureValue(fieldName string, value []byte, maxLen int) error { + if len(value) > maxLen { + return fmt.Errorf("invalid %s: too long", fieldName) + } + if len(value) > 1 && value[0] == 0 { + return fmt.Errorf("invalid %s: leading zero", fieldName) + } + return nil +} diff --git a/giga/deps/xevm/types/ethtx/set_code_tx.go b/giga/deps/xevm/types/ethtx/set_code_tx.go index 80a579e148..3c31b61eee 100644 --- a/giga/deps/xevm/types/ethtx/set_code_tx.go +++ b/giga/deps/xevm/types/ethtx/set_code_tx.go @@ -217,6 +217,22 @@ func (tx SetCodeTx) Validate() error { } } + if err := validateAccessList(tx.Accesses); err != nil { + return err + } + if err := validateAuthList(tx.AuthList); err != nil { + return err + } + if err := validateSignatureValue("v", tx.V, 32); err != nil { + return err + } + if err := validateSignatureValue("r", tx.R, 32); err != nil { + return err + } + if err := validateSignatureValue("s", tx.S, 32); err != nil { + return err + } + chainID := tx.GetChainID() if chainID == nil { diff --git a/giga/deps/xevm/types/message_evm_transaction.go b/giga/deps/xevm/types/message_evm_transaction.go index 498478df50..fded73dbc5 100644 --- a/giga/deps/xevm/types/message_evm_transaction.go +++ b/giga/deps/xevm/types/message_evm_transaction.go @@ -42,9 +42,24 @@ func (msg *MsgEVMTransaction) GetSignBytes() []byte { } func (msg *MsgEVMTransaction) ValidateBasic() error { + if msg.Derived != nil && msg.Derived.PubKey == nil { + return sdkerrors.ErrInvalidPubKey + } + txData, err := UnpackTxData(msg.Data) + if err != nil { + return err + } + if _, ok := txData.(*ethtx.AssociateTx); !ok { + if err := txData.Validate(); err != nil { + return err + } + } amsg, isAssociate := msg.GetAssociateTx() - if isAssociate && len(amsg.CustomMessage) > MaxAssociateCustomMessageLength { - return sdkerrors.Wrapf(sdkerrors.ErrTxTooLarge, "custom message can have at most 64 characters") + if isAssociate { + if len(amsg.CustomMessage) > MaxAssociateCustomMessageLength { + return sdkerrors.Wrapf(sdkerrors.ErrTxTooLarge, "custom message can have at most 64 characters") + } + return amsg.Validate() } return nil } diff --git a/sei-cosmos/x/auth/tx/decoder.go b/sei-cosmos/x/auth/tx/decoder.go index d2dbe6068a..60283b1ebe 100644 --- a/sei-cosmos/x/auth/tx/decoder.go +++ b/sei-cosmos/x/auth/tx/decoder.go @@ -3,6 +3,7 @@ package tx import ( "fmt" + "github.com/gogo/protobuf/proto" "google.golang.org/protobuf/encoding/protowire" "github.com/sei-protocol/sei-chain/sei-cosmos/codec" @@ -47,6 +48,10 @@ func DefaultTxDecoder(cdc codec.ProtoCodecMarshaler) sdk.TxDecoder { return nil, sdkerrors.Wrap(sdkerrors.ErrTxDecode, err.Error()) } + if err := rejectBloatedBody(raw.BodyBytes, &body); err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrTxDecode, err.Error()) + } + var authInfo tx.AuthInfo // reject all unknown proto fields in AuthInfo @@ -141,6 +146,21 @@ func rejectNonADR027TxRaw(txBytes []byte) error { return nil } +// rejectBloatedBody rejects tx bodies where the raw wire encoding is larger +// than the canonical re-marshal of the decoded struct. This catches protobuf-level +// bloat (e.g. padded sdk.Int fields, oversized Any.Value) that UnpackAny would +// otherwise silently canonicalize away before validation runs. +func rejectBloatedBody(rawBodyBytes []byte, body *tx.TxBody) error { + canonicalBytes, err := proto.Marshal(body) + if err != nil { + return fmt.Errorf("failed to re-marshal tx body: %w", err) + } + if len(rawBodyBytes) != len(canonicalBytes) { + return fmt.Errorf("tx body wire size (%d) exceeds canonical size (%d)", len(rawBodyBytes), len(canonicalBytes)) + } + return nil +} + // varintMinLength returns the minimum number of bytes necessary to encode an // uint using varint encoding. func varintMinLength(n uint64) int { diff --git a/sei-cosmos/x/auth/tx/encode_decode_test.go b/sei-cosmos/x/auth/tx/encode_decode_test.go index 40f76325dc..6c975eae19 100644 --- a/sei-cosmos/x/auth/tx/encode_decode_test.go +++ b/sei-cosmos/x/auth/tx/encode_decode_test.go @@ -12,7 +12,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-cosmos/codec" codectypes "github.com/sei-protocol/sei-chain/sei-cosmos/codec/types" "github.com/sei-protocol/sei-chain/sei-cosmos/testutil/testdata" - sdkerrors "github.com/sei-protocol/sei-chain/sei-cosmos/types/errors" + "github.com/sei-protocol/sei-chain/sei-cosmos/types/tx" signingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/types/tx/signing" "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/signing" @@ -60,14 +60,13 @@ func TestUnknownFields(t *testing.T) { shouldErr: false, }, { - name: "non-critical fields in TxBody should not error on decode, but should error with amino", + name: "non-critical fields in TxBody should error on decode due to bloat rejection", body: &testdata.TestUpdatedTxBody{ Memo: "foo", SomeNewFieldNonCriticalField: "blah", }, - authInfo: &testdata.TestUpdatedAuthInfo{}, - shouldErr: false, - shouldAminoErr: fmt.Sprintf("%s: %s", aminoNonCriticalFieldsError, sdkerrors.ErrInvalidRequest.Error()), + authInfo: &testdata.TestUpdatedAuthInfo{}, + shouldErr: true, }, { name: "critical fields in TxBody should error on decode", diff --git a/x/evm/ante/no_cosmos_fields.go b/x/evm/ante/no_cosmos_fields.go index 0a156db73d..d2a7b50e57 100644 --- a/x/evm/ante/no_cosmos_fields.go +++ b/x/evm/ante/no_cosmos_fields.go @@ -15,19 +15,65 @@ func NewEVMNoCosmosFieldsDecorator() EVMNoCosmosFieldsDecorator { } func (d EVMNoCosmosFieldsDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { + if err := ValidateNoCosmosTxFields(tx); err != nil { + return ctx, err + } + return next(ctx, tx, simulate) +} + +type protoTxProvider interface { + GetProtoTx() *txtypes.Tx +} + +// ValidateNoCosmosTxFields rejects Cosmos wrapper fields that EVM txs must not use. +func ValidateNoCosmosTxFields(tx sdk.Tx) error { + if txProto, ok := tx.(protoTxProvider); ok { + pt := txProto.GetProtoTx() + if pt != nil { + if pt.Body != nil { + if pt.Body.Memo != "" { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "memo must be empty for EVM txs") + } + if pt.Body.TimeoutHeight != 0 { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "timeout_height must be zero for EVM txs") + } + if len(pt.Body.ExtensionOptions) > 0 || len(pt.Body.NonCriticalExtensionOptions) > 0 { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "extension options must be empty for EVM txs") + } + } + if pt.AuthInfo != nil { + if len(pt.AuthInfo.SignerInfos) > 0 { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "signer_infos must be empty for EVM txs") + } + if pt.AuthInfo.Fee != nil { + if len(pt.AuthInfo.Fee.Amount) > 0 { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "fee amount must be empty for EVM txs") + } + if pt.AuthInfo.Fee.Payer != "" || pt.AuthInfo.Fee.Granter != "" { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "fee payer and granter must be empty for EVM txs") + } + } + } + if len(pt.Signatures) > 0 { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "signatures must be empty for EVM txs") + } + } + return nil + } + txBody, ok := tx.(interface { GetBody() *txtypes.TxBody }) if ok { body := txBody.GetBody() if body.Memo != "" { - return ctx, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "memo must be empty for EVM txs") + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "memo must be empty for EVM txs") } if body.TimeoutHeight != 0 { - return ctx, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "timeout_height must be zero for EVM txs") + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "timeout_height must be zero for EVM txs") } if len(body.ExtensionOptions) > 0 || len(body.NonCriticalExtensionOptions) > 0 { - return ctx, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "extension options must be empty for EVM txs") + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "extension options must be empty for EVM txs") } } @@ -37,14 +83,14 @@ func (d EVMNoCosmosFieldsDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simul if ok { authInfo := txAuth.GetAuthInfo() if len(authInfo.SignerInfos) > 0 { - return ctx, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "signer_infos must be empty for EVM txs") + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "signer_infos must be empty for EVM txs") } if authInfo.Fee != nil { if len(authInfo.Fee.Amount) > 0 { - return ctx, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "fee amount must be empty for EVM txs") + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "fee amount must be empty for EVM txs") } if authInfo.Fee.Payer != "" || authInfo.Fee.Granter != "" { - return ctx, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "fee payer and granter must be empty for EVM txs") + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "fee payer and granter must be empty for EVM txs") } } } @@ -55,12 +101,12 @@ func (d EVMNoCosmosFieldsDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simul if ok { sigs, err := txSig.GetSignaturesV2() if err != nil { - return ctx, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "could not get signatures") + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "could not get signatures") } if len(sigs) > 0 { - return ctx, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "signatures must be empty for EVM txs") + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "signatures must be empty for EVM txs") } } - return next(ctx, tx, simulate) + return nil } diff --git a/x/evm/types/ethtx/access_list_tx.go b/x/evm/types/ethtx/access_list_tx.go index 6acc097dce..04c8d86774 100644 --- a/x/evm/types/ethtx/access_list_tx.go +++ b/x/evm/types/ethtx/access_list_tx.go @@ -180,6 +180,19 @@ func (tx AccessListTx) Validate() error { } } + if err := validateAccessList(tx.Accesses); err != nil { + return err + } + if err := validateSignatureValue("v", tx.V, 32); err != nil { + return err + } + if err := validateSignatureValue("r", tx.R, 32); err != nil { + return err + } + if err := validateSignatureValue("s", tx.S, 32); err != nil { + return err + } + chainID := tx.GetChainID() if chainID == nil { diff --git a/x/evm/types/ethtx/associate_tx.go b/x/evm/types/ethtx/associate_tx.go index c1b2099365..d30893a319 100644 --- a/x/evm/types/ethtx/associate_tx.go +++ b/x/evm/types/ethtx/associate_tx.go @@ -37,7 +37,18 @@ func (tx *AssociateTx) GetRawSignatureValues() (v, r, s *big.Int) { func (tx *AssociateTx) SetSignatureValues(_, _, _, _ *big.Int) { panic("not implemented") } func (tx *AssociateTx) AsEthereumData() ethtypes.TxData { panic("not implemented") } -func (tx *AssociateTx) Validate() error { panic("not implemented") } +func (tx *AssociateTx) Validate() error { + if err := validateSignatureValue("v", tx.V, 32); err != nil { + return err + } + if err := validateSignatureValue("r", tx.R, 32); err != nil { + return err + } + if err := validateSignatureValue("s", tx.S, 32); err != nil { + return err + } + return nil +} func (tx *AssociateTx) Fee() *big.Int { panic("not implemented") } func (tx *AssociateTx) Cost() *big.Int { panic("not implemented") } diff --git a/x/evm/types/ethtx/blob_tx.go b/x/evm/types/ethtx/blob_tx.go index e57f82f8e8..9bc9b7330c 100644 --- a/x/evm/types/ethtx/blob_tx.go +++ b/x/evm/types/ethtx/blob_tx.go @@ -229,6 +229,19 @@ func (tx BlobTx) Validate() error { } } + if err := validateAccessList(tx.Accesses); err != nil { + return err + } + if err := validateSignatureValue("v", tx.V, 32); err != nil { + return err + } + if err := validateSignatureValue("r", tx.R, 32); err != nil { + return err + } + if err := validateSignatureValue("s", tx.S, 32); err != nil { + return err + } + chainID := tx.GetChainID() if chainID == nil { diff --git a/x/evm/types/ethtx/dynamic_fee_tx.go b/x/evm/types/ethtx/dynamic_fee_tx.go index 9d67756107..3b777d2ece 100644 --- a/x/evm/types/ethtx/dynamic_fee_tx.go +++ b/x/evm/types/ethtx/dynamic_fee_tx.go @@ -197,6 +197,19 @@ func (tx DynamicFeeTx) Validate() error { } } + if err := validateAccessList(tx.Accesses); err != nil { + return err + } + if err := validateSignatureValue("v", tx.V, 32); err != nil { + return err + } + if err := validateSignatureValue("r", tx.R, 32); err != nil { + return err + } + if err := validateSignatureValue("s", tx.S, 32); err != nil { + return err + } + chainID := tx.GetChainID() if chainID == nil { diff --git a/x/evm/types/ethtx/legacy_tx.go b/x/evm/types/ethtx/legacy_tx.go index ede8ce4e19..b9bdf4b032 100644 --- a/x/evm/types/ethtx/legacy_tx.go +++ b/x/evm/types/ethtx/legacy_tx.go @@ -170,6 +170,16 @@ func (tx *LegacyTx) Validate() error { } } + if err := validateSignatureValue("v", tx.V, 32); err != nil { + return err + } + if err := validateSignatureValue("r", tx.R, 32); err != nil { + return err + } + if err := validateSignatureValue("s", tx.S, 32); err != nil { + return err + } + chainID := tx.GetChainID() if chainID == nil { diff --git a/x/evm/types/ethtx/semantic_validation.go b/x/evm/types/ethtx/semantic_validation.go new file mode 100644 index 0000000000..4da1422952 --- /dev/null +++ b/x/evm/types/ethtx/semantic_validation.go @@ -0,0 +1,79 @@ +package ethtx + +import ( + "encoding/hex" + "fmt" + + "github.com/ethereum/go-ethereum/common" +) + +func validateHexAddress(fieldName, value string) error { + if len(value) >= 2 && value[0] == '0' && (value[1] == 'x' || value[1] == 'X') { + value = value[2:] + } + if len(value) != common.AddressLength*2 { + return fmt.Errorf("invalid %s: wrong length", fieldName) + } + if _, err := hex.DecodeString(value); err != nil { + return fmt.Errorf("invalid %s", fieldName) + } + return nil +} + +func validateHexHash(fieldName, value string) error { + if len(value) >= 2 && value[0] == '0' && (value[1] == 'x' || value[1] == 'X') { + value = value[2:] + } + if len(value) != common.HashLength*2 { + return fmt.Errorf("invalid %s: wrong length", fieldName) + } + if _, err := hex.DecodeString(value); err != nil { + return fmt.Errorf("invalid %s", fieldName) + } + return nil +} + +func validateAccessList(accessList AccessList) error { + for _, tuple := range accessList { + if err := validateHexAddress("access list address", tuple.Address); err != nil { + return err + } + for _, storageKey := range tuple.StorageKeys { + if err := validateHexHash("access list storage key", storageKey); err != nil { + return err + } + } + } + return nil +} + +func validateAuthList(authList AuthList) error { + for _, auth := range authList { + if auth.ChainID == nil { + return fmt.Errorf("auth list chain id cannot be nil") + } + if err := validateHexAddress("auth list address", auth.Address); err != nil { + return err + } + if err := validateSignatureValue("auth list v", auth.V, 1); err != nil { + return err + } + if err := validateSignatureValue("auth list r", auth.R, 32); err != nil { + return err + } + if err := validateSignatureValue("auth list s", auth.S, 32); err != nil { + return err + } + } + return nil +} + +func validateSignatureValue(fieldName string, value []byte, maxLen int) error { + if len(value) > maxLen { + return fmt.Errorf("invalid %s: too long", fieldName) + } + if len(value) > 1 && value[0] == 0 { + return fmt.Errorf("invalid %s: leading zero", fieldName) + } + return nil +} diff --git a/x/evm/types/ethtx/set_code_tx.go b/x/evm/types/ethtx/set_code_tx.go index 80a579e148..3c31b61eee 100644 --- a/x/evm/types/ethtx/set_code_tx.go +++ b/x/evm/types/ethtx/set_code_tx.go @@ -217,6 +217,22 @@ func (tx SetCodeTx) Validate() error { } } + if err := validateAccessList(tx.Accesses); err != nil { + return err + } + if err := validateAuthList(tx.AuthList); err != nil { + return err + } + if err := validateSignatureValue("v", tx.V, 32); err != nil { + return err + } + if err := validateSignatureValue("r", tx.R, 32); err != nil { + return err + } + if err := validateSignatureValue("s", tx.S, 32); err != nil { + return err + } + chainID := tx.GetChainID() if chainID == nil { diff --git a/x/evm/types/message_evm_transaction.go b/x/evm/types/message_evm_transaction.go index 78e9595d6f..5525631be4 100644 --- a/x/evm/types/message_evm_transaction.go +++ b/x/evm/types/message_evm_transaction.go @@ -42,9 +42,24 @@ func (msg *MsgEVMTransaction) GetSignBytes() []byte { } func (msg *MsgEVMTransaction) ValidateBasic() error { + if msg.Derived != nil && msg.Derived.PubKey == nil { + return sdkerrors.ErrInvalidPubKey + } + txData, err := UnpackTxData(msg.Data) + if err != nil { + return err + } + if _, ok := txData.(*ethtx.AssociateTx); !ok { + if err := txData.Validate(); err != nil { + return err + } + } amsg, isAssociate := msg.GetAssociateTx() - if isAssociate && len(amsg.CustomMessage) > MaxAssociateCustomMessageLength { - return sdkerrors.Wrapf(sdkerrors.ErrTxTooLarge, "custom message can have at most 64 characters") + if isAssociate { + if len(amsg.CustomMessage) > MaxAssociateCustomMessageLength { + return sdkerrors.Wrapf(sdkerrors.ErrTxTooLarge, "custom message can have at most 64 characters") + } + return amsg.Validate() } return nil } From 93b784ad8ccc2085b83cb2e2d2741c8cd820b3fa Mon Sep 17 00:00:00 2001 From: kbhat1 Date: Fri, 20 Mar 2026 14:27:17 -0400 Subject: [PATCH 2/2] fix: remove Cosmos fee fields from EVM tx test helpers The EVMNoCosmosFieldsDecorator rejects EVM transactions that have non-empty Cosmos wrapper fields (fee amount, payer, granter). Test helpers were setting these fields, causing all giga and occ EVM tests to fail with "fee amount must be empty for EVM txs". Made-with: Cursor --- giga/tests/giga_test.go | 4 ---- giga/tests/harness/builder.go | 2 -- occ_tests/utils/utils.go | 39 ++++++++++++++++++++++++----------- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/giga/tests/giga_test.go b/giga/tests/giga_test.go index 619ba46476..afd0a2988f 100644 --- a/giga/tests/giga_test.go +++ b/giga/tests/giga_test.go @@ -177,7 +177,6 @@ func CreateEVMTransferTxs(t testing.TB, tCtx *GigaTestContext, transfers []EVMTr err = txBuilder.SetMsgs(msg) require.NoError(t, err) txBuilder.SetGasLimit(10000000000) - txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(10000000000)))) txBytes, err := tc.TxEncoder()(txBuilder.GetTx()) require.NoError(t, err) @@ -666,7 +665,6 @@ func CreateContractDeployTxs(t testing.TB, tCtx *GigaTestContext, deploys []EVMC err = txBuilder.SetMsgs(msg) require.NoError(t, err) txBuilder.SetGasLimit(10000000000) - txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(10000000000)))) txBytes, err := tc.TxEncoder()(txBuilder.GetTx()) require.NoError(t, err) @@ -727,7 +725,6 @@ func CreateContractCallTxs(t testing.TB, tCtx *GigaTestContext, calls []EVMContr err = txBuilder.SetMsgs(msg) require.NoError(t, err) txBuilder.SetGasLimit(10000000000) - txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(10000000000)))) txBytes, err := tc.TxEncoder()(txBuilder.GetTx()) require.NoError(t, err) @@ -1733,7 +1730,6 @@ func createCustomEVMTx( err = txBuilder.SetMsgs(msg) require.NoError(t, err) txBuilder.SetGasLimit(10000000000) - txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(10000000000)))) txBytes, err := tc.TxEncoder()(txBuilder.GetTx()) require.NoError(t, err) diff --git a/giga/tests/harness/builder.go b/giga/tests/harness/builder.go index 447534ad6d..13ae1f25f4 100644 --- a/giga/tests/harness/builder.go +++ b/giga/tests/harness/builder.go @@ -9,7 +9,6 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/sei-protocol/sei-chain/app" - sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" "github.com/sei-protocol/sei-chain/x/evm/config" "github.com/sei-protocol/sei-chain/x/evm/types" "github.com/sei-protocol/sei-chain/x/evm/types/ethtx" @@ -153,7 +152,6 @@ func EncodeTxForApp(signedTx *ethtypes.Transaction) ([]byte, error) { return nil, err } txBuilder.SetGasLimit(10000000000) - txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(10000000000)))) txBytes, err := tc.TxEncoder()(txBuilder.GetTx()) if err != nil { diff --git a/occ_tests/utils/utils.go b/occ_tests/utils/utils.go index 33e92b9770..1e87de70e3 100644 --- a/occ_tests/utils/utils.go +++ b/occ_tests/utils/utils.go @@ -14,6 +14,7 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/sei-protocol/sei-chain/sei-cosmos/baseapp" + "github.com/sei-protocol/sei-chain/sei-cosmos/client" clienttx "github.com/sei-protocol/sei-chain/sei-cosmos/client/tx" codectypes "github.com/sei-protocol/sei-chain/sei-cosmos/codec/types" cryptotypes "github.com/sei-protocol/sei-chain/sei-cosmos/crypto/types" @@ -228,19 +229,33 @@ func toTxBytes(testCtx *TestContext, msgs []*TestMessage) [][]byte { panic(err) } - tBuilder := tx.WrapTx(&txtype.Tx{ - Body: &txtype.TxBody{ - Messages: []*codectypes.Any{a}, - }, - AuthInfo: &txtype.AuthInfo{ - Fee: &txtype.Fee{ - Amount: Funds(10000000000), - GasLimit: 10000000000, - Payer: testCtx.TestAccounts[0].AccountAddress.String(), - Granter: testCtx.TestAccounts[0].AccountAddress.String(), + var tBuilder client.TxBuilder + if tm.IsEVM { + tBuilder = tx.WrapTx(&txtype.Tx{ + Body: &txtype.TxBody{ + Messages: []*codectypes.Any{a}, }, - }, - }) + AuthInfo: &txtype.AuthInfo{ + Fee: &txtype.Fee{ + GasLimit: 10000000000, + }, + }, + }) + } else { + tBuilder = tx.WrapTx(&txtype.Tx{ + Body: &txtype.TxBody{ + Messages: []*codectypes.Any{a}, + }, + AuthInfo: &txtype.AuthInfo{ + Fee: &txtype.Fee{ + Amount: Funds(10000000000), + GasLimit: 10000000000, + Payer: testCtx.TestAccounts[0].AccountAddress.String(), + Granter: testCtx.TestAccounts[0].AccountAddress.String(), + }, + }, + }) + } if tm.IsEVM { amounts := sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(1000000000000000000)), sdk.NewCoin("uusdc", sdk.NewInt(1000000000000000)))