Skip to content
This repository has been archived by the owner on Apr 4, 2024. It is now read-only.

EIP712 support #950

Merged
merged 30 commits into from
Feb 26, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e0af05e
code migrated
hanchon Feb 10, 2022
9da0808
signed_data ported to avoid conficting dependency
hanchon Feb 10, 2022
44a15a9
correct payload
hanchon Feb 21, 2022
17987e2
eip712 working with evmos.me
hanchon Feb 21, 2022
16c011c
merge main
crypto-facs Feb 22, 2022
cec0e70
use geth TypedData types
crypto-facs Feb 22, 2022
0dc3fc5
fix linter
crypto-facs Feb 22, 2022
d9025f5
minor refactor
fedekunze Feb 23, 2022
7e6b8f0
Merge branch 'main' into guille/eip712
fedekunze Feb 24, 2022
0e8cb2d
Merge branch 'main' into guille/eip712
crypto-facs Feb 25, 2022
f8d1980
test first try
crypto-facs Feb 25, 2022
6e1a1cc
fix test
crypto-facs Feb 26, 2022
7786870
fix tests
crypto-facs Feb 26, 2022
37843ef
enforce fee delegated eip712
crypto-facs Feb 26, 2022
1991765
verify signature refactor
crypto-facs Feb 26, 2022
6a77691
SignedTypedData api refactor
crypto-facs Feb 26, 2022
c35965e
add AnteHandler test for EIP712
crypto-facs Feb 26, 2022
f73127e
remove comment
crypto-facs Feb 26, 2022
0f88c92
code clean up
hanchon Feb 26, 2022
72af14c
return more detailed error messages
hanchon Feb 26, 2022
fd8eabe
fix linter
crypto-facs Feb 26, 2022
7af8a62
remove unnecesary global vars
crypto-facs Feb 26, 2022
94d6dd4
Update app/ante/eip712.go
crypto-facs Feb 26, 2022
6160b26
fix pr comments
crypto-facs Feb 26, 2022
9d440f7
remove hardcoded value
crypto-facs Feb 26, 2022
63c22b6
add more tests
crypto-facs Feb 26, 2022
86c9a34
add changelog
crypto-facs Feb 26, 2022
34d2132
use sdk errors
crypto-facs Feb 26, 2022
019a31a
add MsgDelegate test
crypto-facs Feb 26, 2022
c949991
Merge branch 'main' into guille/eip712
fedekunze Feb 26, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/ante/ante.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ func NewAnteHandler(options HandlerOptions) sdk.AnteHandler {
case "/ethermint.evm.v1.ExtensionOptionsEthereumTx":
// handle as *evmtypes.MsgEthereumTx
anteHandler = newEthAnteHandler(options)
case "/ethermint.types.v1.ExtensionOptionsWeb3Tx":
// handle as normal Cosmos SDK tx, except signature is checked for EIP712 representation
anteHandler = newCosmosAnteHandlerEip712(options)
fedekunze marked this conversation as resolved.
Show resolved Hide resolved
default:
return ctx, sdkerrors.Wrapf(
sdkerrors.ErrUnknownExtensionOptions,
Expand Down
280 changes: 280 additions & 0 deletions app/ante/eip712.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
package ante

import (
"fmt"
"strconv"

"github.com/pkg/errors"

"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
authante "github.com/cosmos/cosmos-sdk/x/auth/ante"
"github.com/cosmos/cosmos-sdk/x/auth/legacy/legacytx"
authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing"

ethcrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/secp256k1"

"github.com/tharsis/ethermint/crypto/ethsecp256k1"
"github.com/tharsis/ethermint/ethereum/eip712"
ethermint "github.com/tharsis/ethermint/types"
evmtypes "github.com/tharsis/ethermint/x/evm/types"
)

// Verify all signatures for a tx and return an error if any are invalid. Note,
// the Eip712SigVerificationDecorator decorator will not get executed on ReCheck.
//
// CONTRACT: Pubkeys are set in context for all signers before this decorator runs
// CONTRACT: Tx must implement SigVerifiableTx interface
type Eip712SigVerificationDecorator struct {
ak evmtypes.AccountKeeper
signModeHandler authsigning.SignModeHandler
}

func NewEip712SigVerificationDecorator(ak evmtypes.AccountKeeper, signModeHandler authsigning.SignModeHandler) Eip712SigVerificationDecorator {
return Eip712SigVerificationDecorator{
ak: ak,
signModeHandler: signModeHandler,
}
}

func (svd Eip712SigVerificationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) {
crypto-facs marked this conversation as resolved.
Show resolved Hide resolved
// no need to verify signatures on recheck tx
if ctx.IsReCheckTx() {
return next(ctx, tx, simulate)
}
sigTx, ok := tx.(authsigning.SigVerifiableTx)
if !ok {
return ctx, sdkerrors.Wrap(sdkerrors.ErrTxDecode, "invalid transaction type")
}

// stdSigs contains the sequence number, account number, and signatures.
// When simulating, this would just be a 0-length slice.
sigs, err := sigTx.GetSignaturesV2()
if err != nil {
return ctx, err
}

signerAddrs := sigTx.GetSigners()

// check that signer length and signature length are the same
if len(sigs) != len(signerAddrs) {
return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnauthorized, "invalid number of signer; expected: %d, got %d", len(signerAddrs), len(sigs))
}

for i, sig := range sigs {
acc, err := authante.GetSignerAcc(ctx, svd.ak, signerAddrs[i])
if err != nil {
return ctx, err
}

// retrieve pubkey
pubKey := acc.GetPubKey()
if !simulate && pubKey == nil {
return ctx, sdkerrors.Wrap(sdkerrors.ErrInvalidPubKey, "pubkey on account is not set")
}

// Check account sequence number.
if sig.Sequence != acc.GetSequence() {
return ctx, sdkerrors.Wrapf(
sdkerrors.ErrWrongSequence,
"account sequence mismatch, expected %d, got %d", acc.GetSequence(), sig.Sequence,
)
}

// retrieve signer data
genesis := ctx.BlockHeight() == 0
chainID := ctx.ChainID()
var accNum uint64
if !genesis {
accNum = acc.GetAccountNumber()
}
signerData := authsigning.SignerData{
ChainID: chainID,
AccountNumber: accNum,
Sequence: acc.GetSequence(),
}

if !simulate {
err := VerifySignature(pubKey, signerData, sig.Data, svd.signModeHandler, tx.(authsigning.Tx))
fedekunze marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
errMsg := fmt.Sprintf("signature verification failed; please verify account number (%d) and chain-id (%s)", accNum, chainID)
return ctx, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, errMsg)

}
}
}

return next(ctx, tx, simulate)
}

var ethermintCodec codec.ProtoCodecMarshaler

func init() {
registry := codectypes.NewInterfaceRegistry()
ethermint.RegisterInterfaces(registry)
ethermintCodec = codec.NewProtoCodec(registry)
}

// VerifySignature verifies a transaction signature contained in SignatureData abstracting over different signing modes
// and single vs multi-signatures.
func VerifySignature(
pubKey cryptotypes.PubKey,
signerData authsigning.SignerData,
sigData signing.SignatureData,
_ authsigning.SignModeHandler,
tx authsigning.Tx,
) error {
switch data := sigData.(type) {
case *signing.SingleSignatureData:
if data.SignMode != signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON {
return fmt.Errorf("unexpected SignatureData %T: wrong SignMode", sigData)
crypto-facs marked this conversation as resolved.
Show resolved Hide resolved
}

// @contract: this code is reached only when Msg has Web3Tx extension (so this custom Ante handler flow),
// and the signature is SIGN_MODE_LEGACY_AMINO_JSON which is supported for EIP712 for now

msgs := tx.GetMsgs()
txBytes := legacytx.StdSignBytes(
signerData.ChainID,
signerData.AccountNumber,
signerData.Sequence,
tx.GetTimeoutHeight(),
legacytx.StdFee{
Amount: tx.GetFee(),
Gas: tx.GetGas(),
},
msgs, tx.GetMemo(),
)

var chainID uint64
var err error

var (
feePayer sdk.AccAddress
feePayerSig []byte
feeDelegated bool
)

if txWithExtensions, ok := tx.(authante.HasExtensionOptionsTx); ok {
if opts := txWithExtensions.GetExtensionOptions(); len(opts) > 0 {
var optIface ethermint.ExtensionOptionsWeb3TxI

if err := ethermintCodec.UnpackAny(opts[0], &optIface); err != nil {
return errors.Wrap(err, "failed to proto-unpack ExtensionOptionsWeb3Tx")
}

if extOpt, ok := optIface.(*ethermint.ExtensionOptionsWeb3Tx); ok {
// chainID in EIP712 typed data is allowed to not match signerData.ChainID,
// but limited to certain options: 1 (mainnet), 42 (Kovan), thus Metamask will
// be able to submit signatures without switching networks.

if extOpt.TypedDataChainID == 1 || extOpt.TypedDataChainID == 42 || extOpt.TypedDataChainID == 9000 {
chainID = extOpt.TypedDataChainID
}

if len(extOpt.FeePayer) > 0 {
feePayer, err = sdk.AccAddressFromBech32(extOpt.FeePayer)
if err != nil {
return errors.Wrap(err, "failed to parse feePayer from ExtensionOptionsWeb3Tx")
}

feePayerSig = extOpt.FeePayerSig
if len(feePayerSig) == 0 {
return errors.Wrap(err, "no feePayerSig provided in ExtensionOptionsWeb3Tx")
}

feeDelegated = true
}
}
}
}

if chainID == 0 {
chainID, err = strconv.ParseUint(signerData.ChainID, 10, 64)
if err != nil {
return errors.Wrapf(err, "failed to parse chainID: %s", signerData.ChainID)
}
}

var typedData eip712.TypedData
var sigHash []byte

if feeDelegated {
feeDelegation := &eip712.FeeDelegationOptions{
FeePayer: feePayer,
}

typedData, err = eip712.WrapTxToTypedData(ethermintCodec, chainID, msgs[0], txBytes, feeDelegation)
if err != nil {
return errors.Wrap(err, "failed to pack tx data in EIP712 object")
}

sigHash, err = eip712.ComputeTypedDataHash(typedData)
if err != nil {
return err
}

if len(feePayerSig) != 65 {
return fmt.Errorf("signature length doesn't match typical [R||S||V] signature 65 bytes")
}
if feePayerSig[64] > 4 {
// Remove the recovery offset if needed (ie. Metamask eip712 signature)
feePayerSig[64] = feePayerSig[64] - 27
}

feePayerPubkey, err := secp256k1.RecoverPubkey(sigHash, feePayerSig)
if err != nil {
return errors.Wrap(err, "failed to recover delegated fee payer from sig")
}

ecPubKey, err := ethcrypto.UnmarshalPubkey(feePayerPubkey)
if err != nil {
return errors.Wrap(err, "failed to unmarshal recovered fee payer pubkey")
}

pk := &ethsecp256k1.PubKey{
Key: ethcrypto.CompressPubkey(ecPubKey),
}

recoveredFeePayerAcc := sdk.AccAddress(pk.Address().Bytes())

if !recoveredFeePayerAcc.Equals(feePayer) {
return errors.New("failed to verify delegated fee payer sig")
}

// Overwrite the transaction signature because we are using EIP712
// TODO: should we allow only feePayerSignature and return error
// if the transaction signatures != []?
data.Signature = feePayerSig
} else {
typedData, err = eip712.WrapTxToTypedData(ethermintCodec, chainID, msgs[0], txBytes, nil)
if err != nil {
return errors.Wrap(err, "failed to pack tx data in EIP712 object")
}

sigHash, err = eip712.ComputeTypedDataHash(typedData)
if err != nil {
return err
}
}

if len(data.Signature) != 65 {
return fmt.Errorf("signature length doesn't match typical [R||S||V] signature 65 bytes")
}

// VerifySignature of ethsecp256k1 accepts 64 byte signature [R||S]
// WARNING! Under NO CIRCUMSTANCES try to use pubKey.VerifySignature there
if !secp256k1.VerifySignature(pubKey.Bytes(), sigHash, data.Signature[:len(data.Signature)-1]) {
return fmt.Errorf("unable to verify signer signature of EIP712 typed data")
}

return nil
default:
return fmt.Errorf("unexpected SignatureData %T", sigData)
}
}
23 changes: 23 additions & 0 deletions app/ante/handler_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,26 @@ func newCosmosAnteHandler(options HandlerOptions) sdk.AnteHandler {
ibcante.NewAnteDecorator(options.IBCChannelKeeper),
)
}

func newCosmosAnteHandlerEip712(options HandlerOptions) sdk.AnteHandler {
fedekunze marked this conversation as resolved.
Show resolved Hide resolved
return sdk.ChainAnteDecorators(
RejectMessagesDecorator{}, // reject MsgEthereumTxs
ante.NewSetUpContextDecorator(),
// NOTE: extensions option decorator removed
// ante.NewRejectExtensionOptionsDecorator(),
ante.NewMempoolFeeDecorator(),
ante.NewValidateBasicDecorator(),
ante.NewTxTimeoutHeightDecorator(),
ante.NewValidateMemoDecorator(options.AccountKeeper),
ante.NewConsumeGasForTxSizeDecorator(options.AccountKeeper),
ante.NewDeductFeeDecorator(options.AccountKeeper, options.BankKeeper, options.FeegrantKeeper),
// SetPubKeyDecorator must be called before all signature verification decorators
ante.NewSetPubKeyDecorator(options.AccountKeeper),
ante.NewValidateSigCountDecorator(options.AccountKeeper),
ante.NewSigGasConsumeDecorator(options.AccountKeeper, options.SigGasConsumer),
// Note: signature verification uses EIP instead of the cosmos signature validator
NewEip712SigVerificationDecorator(options.AccountKeeper, options.SignModeHandler),
ante.NewIncrementSequenceDecorator(options.AccountKeeper),
ibcante.NewAnteDecorator(options.IBCChannelKeeper),
)
}
Loading