Skip to content

Commit

Permalink
Add x/rollup unit tests
Browse files Browse the repository at this point in the history
Adds unit test coverage to assert behavior for the
x/rollup messages and the related helper functions
involved in the deposit and withdrawal flow.
  • Loading branch information
natebeauregard committed Aug 21, 2024
1 parent 291f66b commit 1315787
Show file tree
Hide file tree
Showing 5 changed files with 280 additions and 19 deletions.
19 changes: 11 additions & 8 deletions testutils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,11 @@ func NewLocalMemDB(t *testing.T) *localdb.DB {
// GenerateEthTxs generates an L1 attributes tx, deposit tx, and cosmos tx packed in an Ethereum transaction.
// The transactions are not meant to be executed.
func GenerateEthTxs(t *testing.T) (*gethtypes.Transaction, *gethtypes.Transaction, *gethtypes.Transaction) {
timestamp := uint64(0)
l1Block := gethtypes.NewBlock(&gethtypes.Header{
BaseFee: big.NewInt(10),
Difficulty: common.Big0,
Number: big.NewInt(0),
Time: timestamp,
}, nil, nil, nil, trie.NewStackTrie(nil))
l1Block := GenerateL1Block()
l1InfoRawTx, err := derive.L1InfoDeposit(&rollup.Config{
Genesis: rollup.Genesis{L2: eth.BlockID{Number: 0}},
L2ChainID: big.NewInt(1234),
}, eth.SystemConfig{}, 0, eth.BlockToInfo(l1Block), timestamp)
}, eth.SystemConfig{}, 0, eth.BlockToInfo(l1Block), l1Block.Time())
require.NoError(t, err)
l1InfoTx := gethtypes.NewTx(l1InfoRawTx)

Expand Down Expand Up @@ -131,3 +125,12 @@ func GenerateBlockWithParentAndTxs(t *testing.T, parent *monomer.Header, cosmosT
require.NoError(t, err)
return block
}

func GenerateL1Block() *gethtypes.Block {
return gethtypes.NewBlock(&gethtypes.Header{
BaseFee: big.NewInt(10),
Difficulty: common.Big0,
Number: big.NewInt(0),
Time: uint64(0),
}, nil, nil, nil, trie.NewStackTrie(nil))
}
19 changes: 11 additions & 8 deletions x/rollup/keeper/deposits.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,34 +24,35 @@ func (k *Keeper) setL1BlockInfo(ctx sdk.Context, info derive.L1BlockInfo) error
if err != nil {
return types.WrapError(err, "marshal L1 block info")
}
if err := k.storeService.OpenKVStore(ctx).Set([]byte(types.KeyL1BlockInfo), infoBytes); err != nil {
if err = k.storeService.OpenKVStore(ctx).Set([]byte(types.KeyL1BlockInfo), infoBytes); err != nil {
return types.WrapError(err, "set")
}
return nil
}

// TODO: include the logic to also store the L1 block info by blockhash in setL1BlockInfo and remove setL1BlockHistory
// setL1BlockHistory sets the L1 block info to the app state, with the key being the blockhash, so we can look it up easily later.
func (k *Keeper) setL1BlockHistory(ctx context.Context, info *derive.L1BlockInfo) error {
infoBytes, err := json.Marshal(info)
if err != nil {
return types.WrapError(err, "marshal L1 block info")
}
if err := k.storeService.OpenKVStore(ctx).Set(info.BlockHash.Bytes(), infoBytes); err != nil {
if err = k.storeService.OpenKVStore(ctx).Set(info.BlockHash.Bytes(), infoBytes); err != nil {
return types.WrapError(err, "set")
}
return nil
}

// processL1SystemDepositTx processes the L1 Attributes deposit tx and returns the L1 block info.
func (k *Keeper) processL1SystemDepositTx(ctx sdk.Context, txBytes []byte) (*derive.L1BlockInfo, error) { //nolint:gocritic // hugeParam
// processL1AttributesTx processes the L1 Attributes tx and returns the L1 block info.
func (k *Keeper) processL1AttributesTx(ctx sdk.Context, txBytes []byte) (*derive.L1BlockInfo, error) { //nolint:gocritic // hugeParam
var tx ethtypes.Transaction
if err := tx.UnmarshalBinary(txBytes); err != nil {
ctx.Logger().Error("Failed to unmarshal system deposit transaction", "index", 0, "err", err, "txBytes", txBytes)
return nil, types.WrapError(types.ErrInvalidL1Txs, "failed to unmarshal system deposit transaction: %v", err)
ctx.Logger().Error("Failed to unmarshal L1 attributes transaction", "index", 0, "err", err, "txBytes", txBytes)
return nil, types.WrapError(types.ErrInvalidL1Txs, "failed to unmarshal L1 attributes transaction: %v", err)
}
if !tx.IsDepositTx() {
ctx.Logger().Error("First L1 tx must be a system deposit tx", "type", tx.Type())
return nil, types.WrapError(types.ErrInvalidL1Txs, "first L1 tx must be a system deposit tx, but got type %d", tx.Type())
ctx.Logger().Error("First L1 tx must be a L1 attributes tx", "type", tx.Type())
return nil, types.WrapError(types.ErrInvalidL1Txs, "first L1 tx must be a L1 attributes tx, but got type %d", tx.Type())
}
l1blockInfo, err := derive.L1BlockInfoFromBytes(k.rollupCfg, 0, tx.Data())
if err != nil {
Expand Down Expand Up @@ -106,6 +107,8 @@ func (k *Keeper) mintETH(ctx sdk.Context, addr sdk.AccAddress, amount sdkmath.In
if err := k.bankkeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, addr, sdk.NewCoins(coin)); err != nil {
return fmt.Errorf("failed to send deposit coins from rollup module to user account %v: %v", addr, err)
}

// TODO: only emit sdk.EventTypeMessage once per message
ctx.EventManager().EmitEvents(sdk.Events{
sdk.NewEvent(
sdk.EventTypeMessage,
Expand Down
54 changes: 54 additions & 0 deletions x/rollup/keeper/keeper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package keeper_test

import (
"context"
"testing"

storetypes "cosmossdk.io/store/types"
"github.com/cosmos/cosmos-sdk/runtime"
"github.com/cosmos/cosmos-sdk/testutil"
sdk "github.com/cosmos/cosmos-sdk/types"
moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil"
"github.com/golang/mock/gomock"
"github.com/polymerdao/monomer/x/rollup/keeper"
rolluptestutil "github.com/polymerdao/monomer/x/rollup/testutil"
"github.com/polymerdao/monomer/x/rollup/types"
"github.com/stretchr/testify/suite"
)

type KeeperTestSuite struct {
suite.Suite
ctx context.Context
rollupKeeper *keeper.Keeper
bankKeeper *rolluptestutil.MockBankKeeper
rollupStore storetypes.KVStore
}

func TestKeeperTestSuite(t *testing.T) {
suite.Run(t, new(KeeperTestSuite))
}

func (s *KeeperTestSuite) SetupSubTest() {
storeKey := storetypes.NewKVStoreKey(types.StoreKey)
s.ctx = testutil.DefaultContextWithDB(
s.T(),
storeKey,
storetypes.NewTransientStoreKey("transient_test")).Ctx
s.bankKeeper = rolluptestutil.NewMockBankKeeper(gomock.NewController(s.T()))
s.rollupKeeper = keeper.NewKeeper(
moduletestutil.MakeTestEncodingConfig().Codec,
runtime.NewKVStoreService(storeKey),
s.bankKeeper,
)
s.rollupStore = sdk.UnwrapSDKContext(s.ctx).KVStore(storeKey)
}

func (s *KeeperTestSuite) mockBurnETH() {
s.bankKeeper.EXPECT().SendCoinsFromAccountToModule(gomock.Any(), gomock.Any(), types.ModuleName, gomock.Any()).Return(nil).AnyTimes()
s.bankKeeper.EXPECT().BurnCoins(gomock.Any(), types.ModuleName, gomock.Any()).Return(nil).AnyTimes()
}

func (s *KeeperTestSuite) mockMintETH() {
s.bankKeeper.EXPECT().MintCoins(gomock.Any(), types.ModuleName, gomock.Any()).Return(nil).AnyTimes()
s.bankKeeper.EXPECT().SendCoinsFromModuleToAccount(gomock.Any(), types.ModuleName, gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
}
6 changes: 3 additions & 3 deletions x/rollup/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ func (k *Keeper) ApplyL1Txs(goCtx context.Context, msg *types.MsgApplyL1Txs) (*t

ctx.Logger().Debug("Processing L1 txs", "txCount", len(msg.TxBytes))

// process L1 system deposit tx and get L1 block info
l1blockInfo, err := k.processL1SystemDepositTx(ctx, msg.TxBytes[0])
// process L1 attributes tx and get L1 block info
l1blockInfo, err := k.processL1AttributesTx(ctx, msg.TxBytes[0])
if err != nil {
ctx.Logger().Error("Failed to process L1 system deposit tx", "err", err)
return nil, types.WrapError(types.ErrProcessL1SystemDepositTx, "err: %v", err)
Expand Down Expand Up @@ -73,7 +73,7 @@ func (k *Keeper) InitiateWithdrawal(
return nil, types.WrapError(types.ErrInvalidSender, "failed to create cosmos address for sender: %v; error: %v", msg.Sender, err)
}

if err := k.burnETH(ctx, cosmAddr, msg.Value); err != nil {
if err = k.burnETH(ctx, cosmAddr, msg.Value); err != nil {
ctx.Logger().Error("Failed to burn ETH", "cosmosAddress", cosmAddr, "evmAddress", msg.Target, "err", err)
return nil, types.WrapError(types.ErrBurnETH, "failed to burn ETH for cosmosAddress: %v; err: %v", cosmAddr, err)
}
Expand Down
201 changes: 201 additions & 0 deletions x/rollup/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package keeper_test

import (
"encoding/json"

"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-service/eth"
gethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/golang/mock/gomock"
"github.com/polymerdao/monomer/testutils"
"github.com/polymerdao/monomer/x/rollup/keeper"
"github.com/polymerdao/monomer/x/rollup/types"
)

func (s *KeeperTestSuite) TestApplyL1Txs() {
l1AttributesTx, depositTx, cosmosEthTx := testutils.GenerateEthTxs(s.T())
// The only constraint for a contract creation tx is that it must be a non-system DepositTx with no To field
contractCreationTx := gethtypes.NewTx(&gethtypes.DepositTx{})

l1AttributesTxBz, err := l1AttributesTx.MarshalBinary()
s.Require().NoError(err)
depositTxBz, err := depositTx.MarshalBinary()
s.Require().NoError(err)
cosmosEthTxBz, err := cosmosEthTx.MarshalBinary()
s.Require().NoError(err)
contractCreationTxBz, err := contractCreationTx.MarshalBinary()
s.Require().NoError(err)
invalidTxBz := []byte("invalid tx bytes")

tests := map[string]struct {
txBytes [][]byte
setupMocks func()
shouldError bool
}{
"successful message with single user deposit tx": {
txBytes: [][]byte{l1AttributesTxBz, depositTxBz},
shouldError: false,
},
"successful message with multiple user deposit txs": {
txBytes: [][]byte{l1AttributesTxBz, depositTxBz, depositTxBz},
shouldError: false,
},
"invalid l1 attributes tx bytes": {
txBytes: [][]byte{invalidTxBz, depositTxBz},
shouldError: true,
},
"non-deposit tx passed in as l1 attributes tx": {
txBytes: [][]byte{cosmosEthTxBz, depositTxBz},
shouldError: true,
},
"user deposit tx passed in as l1 attributes tx": {
txBytes: [][]byte{depositTxBz, depositTxBz},
shouldError: true,
},
"invalid user deposit tx bytes": {
txBytes: [][]byte{l1AttributesTxBz, invalidTxBz},
shouldError: true,
},
"non-deposit tx passed in as user deposit tx": {
txBytes: [][]byte{l1AttributesTxBz, cosmosEthTxBz},
shouldError: true,
},
"l1 attributes tx passed in as user deposit tx": {
txBytes: [][]byte{l1AttributesTxBz, l1AttributesTxBz},
shouldError: true,
},
"contract creation tx passed in as user deposit tx": {
txBytes: [][]byte{l1AttributesTxBz, contractCreationTxBz},
shouldError: true,
},
"one valid l1 user deposit tx and an invalid tx passed in as user deposit txs": {
txBytes: [][]byte{l1AttributesTxBz, depositTxBz, invalidTxBz},
shouldError: true,
},
"bank keeper mint coins failure": {
txBytes: [][]byte{l1AttributesTxBz, depositTxBz},
setupMocks: func() {
s.bankKeeper.EXPECT().MintCoins(gomock.Any(), types.ModuleName, gomock.Any()).Return(sdkerrors.ErrUnauthorized)
},
shouldError: true,
},
"bank keeper send coins failure": {
txBytes: [][]byte{l1AttributesTxBz, depositTxBz},
setupMocks: func() {
s.bankKeeper.EXPECT().SendCoinsFromModuleToAccount(gomock.Any(), types.ModuleName, gomock.Any(), gomock.Any()).Return(sdkerrors.ErrUnknownRequest)
},
shouldError: true,
},
}

for name, test := range tests {
s.Run(name, func() {
if test.setupMocks != nil {
test.setupMocks()
}
s.mockMintETH()

var resp *types.MsgApplyL1TxsResponse
resp, err = keeper.NewMsgServerImpl(s.rollupKeeper).ApplyL1Txs(s.ctx, &types.MsgApplyL1Txs{
TxBytes: test.txBytes,
})

if test.shouldError {
s.Require().Error(err)
s.Require().Nil(resp)
} else {
s.Require().NoError(err)
s.Require().NotNil(resp)

// TODO: Verify that the expected event types are emitted

// Verify that the l1 block info and l1 block history are saved to the store
expectedBlockInfo := eth.BlockToInfo(testutils.GenerateL1Block())
l1BlockInfoBz := s.rollupStore.Get([]byte(types.KeyL1BlockInfo))
historicalL1BlockInfoBz := s.rollupStore.Get(expectedBlockInfo.Hash().Bytes())
s.Require().NotNil(l1BlockInfoBz)
s.Require().NotNil(historicalL1BlockInfoBz)
s.Require().Equal(l1BlockInfoBz, historicalL1BlockInfoBz)

var l1BlockInfo *derive.L1BlockInfo
err = json.Unmarshal(l1BlockInfoBz, &l1BlockInfo)
s.Require().NoError(err)
s.Require().Equal(expectedBlockInfo.NumberU64(), l1BlockInfo.Number)
s.Require().Equal(expectedBlockInfo.BaseFee(), l1BlockInfo.BaseFee)
s.Require().Equal(expectedBlockInfo.Time(), l1BlockInfo.Time)
s.Require().Equal(expectedBlockInfo.Hash(), l1BlockInfo.BlockHash)
}
})
}
}

func (s *KeeperTestSuite) TestInitiateWithdrawal() {
sender := sdk.AccAddress("addr").String()
l1Target := "0x12345abcde"
withdrawalAmount := math.NewInt(1000000)

tests := map[string]struct {
sender string
setupMocks func()
shouldError bool
}{
"successful message": {
sender: sender,
shouldError: false,
},
"invalid sender addr": {
sender: "invalid",
shouldError: true,
},
"bank keeper insufficient funds failure": {
setupMocks: func() {
s.bankKeeper.EXPECT().SendCoinsFromAccountToModule(gomock.Any(), gomock.Any(), types.ModuleName, gomock.Any()).Return(types.ErrBurnETH)
},
sender: sender,
shouldError: true,
},
"bank keeper burn coins failure": {
setupMocks: func() {
s.bankKeeper.EXPECT().BurnCoins(gomock.Any(), types.ModuleName, gomock.Any()).Return(sdkerrors.ErrUnknownRequest)
},
sender: sender,
shouldError: true,
},
}

for name, test := range tests {
s.Run(name, func() {
if test.setupMocks != nil {
test.setupMocks()
}
s.mockBurnETH()

resp, err := keeper.NewMsgServerImpl(s.rollupKeeper).InitiateWithdrawal(s.ctx, &types.MsgInitiateWithdrawal{
Sender: test.sender,
Target: l1Target,
Value: withdrawalAmount,
})

if test.shouldError {
s.Require().Error(err)
s.Require().Nil(resp)
} else {
s.Require().NoError(err)
s.Require().NotNil(resp)

// Verify that the expected event types are emitted
expectedEventTypes := []string{
sdk.EventTypeMessage,
types.EventTypeWithdrawalInitiated,
types.EventTypeBurnETH,
}
for i, event := range sdk.UnwrapSDKContext(s.ctx).EventManager().Events() {
s.Require().Equal(expectedEventTypes[i], event.Type)
}
}
})
}
}

0 comments on commit 1315787

Please sign in to comment.