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

Commit

Permalink
feat(eip712): Create LedgerPreprocessHook to reformat EIP-712 payloads (
Browse files Browse the repository at this point in the history
#1277)

* Create ReformatLedgerTx to reformat EIP-712 payloads

* Fix issue with public key; minor refactors

* Refactor naming; add test case

* Update preprocess_test

* Add tests and fix lint

* Refactor preprocess test

* Implement minor fix and add changelog entry

Co-authored-by: Freddy Caceres <facs95@gmail.com>
Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com>
  • Loading branch information
3 people authored Sep 13, 2022
1 parent 0fcfe44 commit 723443a
Show file tree
Hide file tree
Showing 3 changed files with 304 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
* (cli) [#1230](https://github.com/evmos/ethermint/pull/1230) Remove redundant positional height parameter from feemarket's query cli.
* (ante) [#1289](https://github.com/evmos/ethermint/pull/1289) Change the fallback tx priority mechanism to be based on gas price.
* (test) [#1311](https://github.com/evmos/ethermint/pull/1311) add integration test for the rollback cmd
* (ledger) [#1277](https://github.com/evmos/ethermint/pull/1277) Add Ledger preprocessing transaction hook for EIP-712-signed Cosmos payloads.
* (rpc) [#1296](https://github.com/evmos/ethermint/pull/1296) add backend blocks.go unit tests.

### Bug Fixes
Expand Down
83 changes: 83 additions & 0 deletions ethereum/eip712/preprocess.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package eip712

import (
"fmt"

"github.com/cosmos/cosmos-sdk/client"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
cosmoskr "github.com/cosmos/cosmos-sdk/crypto/keyring"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
authtx "github.com/cosmos/cosmos-sdk/x/auth/tx"
"github.com/evmos/ethermint/types"
)

// PreprocessLedgerTx reformats Ledger-signed Cosmos transactions to match the fork expected by Ethermint
// by including the signature in a Web3Tx extension and sending a blank signature in the body.
func PreprocessLedgerTx(chainID string, keyType cosmoskr.KeyType, txBuilder client.TxBuilder) error {
// Only process Ledger transactions
if keyType != cosmoskr.TypeLedger {
return nil
}

// Init extension builder to set Web3 extension
extensionBuilder, ok := txBuilder.(authtx.ExtensionOptionsTxBuilder)
if !ok {
return fmt.Errorf("cannot cast TxBuilder to ExtensionOptionsTxBuilder")
}

// Get signatures from TxBuilder
sigs, err := txBuilder.GetTx().GetSignaturesV2()
if err != nil {
return fmt.Errorf("could not get signatures: %w", err)
}

// Verify single-signer
if len(sigs) != 1 {
return fmt.Errorf("invalid number of signatures, expected 1 and got %v", len(sigs))
}

signature := sigs[0]
sigData, ok := signature.Data.(*signing.SingleSignatureData)
if !ok {
return fmt.Errorf("unexpected signature type, expected SingleSignatureData")
}
sigBytes := sigData.Signature

// Parse Chain ID as big.Int
chainIDInt, err := types.ParseChainID(chainID)
if err != nil {
return fmt.Errorf("could not parse chain id: %w", err)
}

// Add ExtensionOptionsWeb3Tx extension with signature
var option *codectypes.Any
option, err = codectypes.NewAnyWithValue(&types.ExtensionOptionsWeb3Tx{
FeePayer: txBuilder.GetTx().FeePayer().String(),
TypedDataChainID: chainIDInt.Uint64(),
FeePayerSig: sigBytes,
})
if err != nil {
return fmt.Errorf("could not set extension as any: %w", err)
}

extensionBuilder.SetExtensionOptions(option)

// Set blank signature with Amino Sign Type
// (Regardless of input signMode, Evmos requires Amino signature type for Ledger)
blankSig := signing.SingleSignatureData{
SignMode: signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON,
Signature: nil,
}
sig := signing.SignatureV2{
PubKey: signature.PubKey,
Data: &blankSig,
Sequence: signature.Sequence,
}

err = txBuilder.SetSignatures(sig)
if err != nil {
return fmt.Errorf("unable to set signatures on payload: %w", err)
}

return nil
}
220 changes: 220 additions & 0 deletions ethereum/eip712/preprocess_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
package eip712_test

import (
"encoding/hex"
"strings"
"testing"

"cosmossdk.io/math"
"github.com/cosmos/cosmos-sdk/client"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
"github.com/cosmos/cosmos-sdk/x/auth/ante"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/evmos/ethermint/app"
"github.com/evmos/ethermint/encoding"
"github.com/evmos/ethermint/ethereum/eip712"
"github.com/evmos/ethermint/tests"
"github.com/evmos/ethermint/types"
evmtypes "github.com/evmos/ethermint/x/evm/types"
"github.com/stretchr/testify/require"
)

// Testing Constants
var chainId = "ethermint_9000-1"
var ctx = client.Context{}.WithTxConfig(
encoding.MakeConfig(app.ModuleBasics).TxConfig,
)
var feePayerAddress = "ethm17xpfvakm2amg962yls6f84z3kell8c5lthdzgl"

type TestCaseStruct struct {
txBuilder client.TxBuilder
expectedFeePayer string
expectedGas uint64
expectedFee math.Int
expectedMemo string
expectedMsg string
expectedSignatureBytes []byte
}

func TestLedgerPreprocessing(t *testing.T) {
// Update bech32 prefix
sdk.GetConfig().SetBech32PrefixForAccount("ethm", "")

testCases := []TestCaseStruct{
createBasicTestCase(t),
createPopulatedTestCase(t),
}

for _, tc := range testCases {
// Run pre-processing
err := eip712.PreprocessLedgerTx(
chainId,
keyring.TypeLedger,
tc.txBuilder,
)

require.NoError(t, err)

// Verify Web3 extension matches expected
hasExtOptsTx, ok := tc.txBuilder.(ante.HasExtensionOptionsTx)
require.True(t, ok)
require.True(t, len(hasExtOptsTx.GetExtensionOptions()) == 1)

expectedExt := types.ExtensionOptionsWeb3Tx{
TypedDataChainID: 9000,
FeePayer: feePayerAddress,
FeePayerSig: tc.expectedSignatureBytes,
}

expectedExtAny, err := codectypes.NewAnyWithValue(&expectedExt)
require.NoError(t, err)

actualExtAny := hasExtOptsTx.GetExtensionOptions()[0]
require.Equal(t, expectedExtAny, actualExtAny)

// Verify signature type matches expected
signatures, err := tc.txBuilder.GetTx().GetSignaturesV2()
require.NoError(t, err)
require.Equal(t, len(signatures), 1)

txSig := signatures[0].Data.(*signing.SingleSignatureData)
require.Equal(t, txSig.SignMode, signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON)

// Verify signature is blank
require.Equal(t, len(txSig.Signature), 0)

// Verify tx fields are unchanged
tx := tc.txBuilder.GetTx()

require.Equal(t, tx.FeePayer().String(), tc.expectedFeePayer)
require.Equal(t, tx.GetGas(), tc.expectedGas)
require.Equal(t, tx.GetFee().AmountOf(evmtypes.DefaultParams().EvmDenom), tc.expectedFee)
require.Equal(t, tx.GetMemo(), tc.expectedMemo)

// Verify message is unchanged
if tc.expectedMsg != "" {
require.Equal(t, len(tx.GetMsgs()), 1)
require.Equal(t, tx.GetMsgs()[0].String(), tc.expectedMsg)
} else {
require.Equal(t, len(tx.GetMsgs()), 0)
}
}
}

func TestBlankTxBuilder(t *testing.T) {
txBuilder := ctx.TxConfig.NewTxBuilder()

err := eip712.PreprocessLedgerTx(
chainId,
keyring.TypeLedger,
txBuilder,
)

require.Error(t, err)
}

func TestNonLedgerTxBuilder(t *testing.T) {
txBuilder := ctx.TxConfig.NewTxBuilder()

err := eip712.PreprocessLedgerTx(
chainId,
keyring.TypeLocal,
txBuilder,
)

require.NoError(t, err)
}

func TestInvalidChainId(t *testing.T) {
txBuilder := ctx.TxConfig.NewTxBuilder()

err := eip712.PreprocessLedgerTx(
"invalid-chain-id",
keyring.TypeLedger,
txBuilder,
)

require.Error(t, err)
}

func createBasicTestCase(t *testing.T) TestCaseStruct {
t.Helper()
txBuilder := ctx.TxConfig.NewTxBuilder()

feePayer, err := sdk.AccAddressFromBech32(feePayerAddress)
require.NoError(t, err)

txBuilder.SetFeePayer(feePayer)

// Create signature unrelated to payload for testing
signatureHex := strings.Repeat("01", 65)
signatureBytes, err := hex.DecodeString(signatureHex)
require.NoError(t, err)

_, privKey := tests.NewAddrKey()
sigsV2 := signing.SignatureV2{
PubKey: privKey.PubKey(), // Use unrelated public key for testing
Data: &signing.SingleSignatureData{
SignMode: signing.SignMode_SIGN_MODE_DIRECT,
Signature: signatureBytes,
},
Sequence: 0,
}

txBuilder.SetSignatures(sigsV2)
return TestCaseStruct{
txBuilder: txBuilder,
expectedFeePayer: feePayer.String(),
expectedGas: 0,
expectedFee: math.NewInt(0),
expectedMemo: "",
expectedMsg: "",
expectedSignatureBytes: signatureBytes,
}
}

func createPopulatedTestCase(t *testing.T) TestCaseStruct {
t.Helper()
basicTestCase := createBasicTestCase(t)
txBuilder := basicTestCase.txBuilder

gasLimit := uint64(200000)
memo := ""
denom := evmtypes.DefaultParams().EvmDenom
feeAmount := math.NewInt(2000)

txBuilder.SetFeeAmount(sdk.NewCoins(
sdk.NewCoin(
denom,
feeAmount,
)))

txBuilder.SetGasLimit(gasLimit)
txBuilder.SetMemo(memo)

msgSend := banktypes.MsgSend{
FromAddress: feePayerAddress,
ToAddress: "ethm12luku6uxehhak02py4rcz65zu0swh7wjun6msa",
Amount: sdk.NewCoins(
sdk.NewCoin(
evmtypes.DefaultParams().EvmDenom,
math.NewInt(10000000),
),
),
}

txBuilder.SetMsgs(&msgSend)

return TestCaseStruct{
txBuilder: txBuilder,
expectedFeePayer: basicTestCase.expectedFeePayer,
expectedGas: gasLimit,
expectedFee: feeAmount,
expectedMemo: memo,
expectedMsg: msgSend.String(),
expectedSignatureBytes: basicTestCase.expectedSignatureBytes,
}
}

0 comments on commit 723443a

Please sign in to comment.