diff --git a/challenger/child/common_test.go b/challenger/child/common_test.go new file mode 100644 index 0000000..05e622a --- /dev/null +++ b/challenger/child/common_test.go @@ -0,0 +1,85 @@ +package child + +import ( + "context" + "errors" + + ophosttypes "github.com/initia-labs/OPinit/x/ophost/types" + challengertypes "github.com/initia-labs/opinit-bots/challenger/types" + "github.com/initia-labs/opinit-bots/types" +) + +type mockHost struct { + db types.DB + height int64 + pendingEvents []challengertypes.ChallengeEvent + syncedOutputs map[uint64]map[uint64]*ophosttypes.Output +} + +func NewMockHost(db types.DB, height int64) *mockHost { + return &mockHost{ + db: db, + height: height, + pendingEvents: make([]challengertypes.ChallengeEvent, 0), + syncedOutputs: make(map[uint64]map[uint64]*ophosttypes.Output), + } +} + +func (m *mockHost) DB() types.DB { + return m.db +} + +func (m *mockHost) SetPendingEvents(events []challengertypes.ChallengeEvent) { + m.pendingEvents = append(m.pendingEvents, events...) +} + +func (m *mockHost) Height() int64 { + return m.height +} + +func (m *mockHost) SetSyncedOutput(bridgeId uint64, outputIndex uint64, syncedOutput *ophosttypes.Output) { + if _, ok := m.syncedOutputs[bridgeId]; !ok { + m.syncedOutputs[bridgeId] = make(map[uint64]*ophosttypes.Output) + } + m.syncedOutputs[bridgeId][outputIndex] = syncedOutput +} + +func (m *mockHost) QuerySyncedOutput(ctx context.Context, bridgeId uint64, outputIndex uint64) (*ophosttypes.QueryOutputProposalResponse, error) { + if _, ok := m.syncedOutputs[bridgeId]; !ok { + return nil, errors.New("collections: not found") + } + + if _, ok := m.syncedOutputs[bridgeId][outputIndex]; !ok { + return nil, errors.New("collections: not found") + } + + return &ophosttypes.QueryOutputProposalResponse{ + BridgeId: bridgeId, + OutputIndex: outputIndex, + OutputProposal: *m.syncedOutputs[bridgeId][outputIndex], + }, nil +} + +var _ hostNode = (*mockHost)(nil) + +type mockChallenger struct { + db types.DB + pendingChallenges []challengertypes.Challenge +} + +func NewMockChallenger(db types.DB) *mockChallenger { + return &mockChallenger{ + db: db, + pendingChallenges: make([]challengertypes.Challenge, 0), + } +} + +func (m *mockChallenger) DB() types.DB { + return m.db +} + +func (m *mockChallenger) SendPendingChallenges(challenges []challengertypes.Challenge) { + m.pendingChallenges = append(m.pendingChallenges, challenges...) +} + +var _ challenger = (*mockChallenger)(nil) diff --git a/challenger/child/deposit.go b/challenger/child/deposit.go index 7b58b65..d44bd4e 100644 --- a/challenger/child/deposit.go +++ b/challenger/child/deposit.go @@ -1,14 +1,10 @@ package child import ( - "time" - childprovider "github.com/initia-labs/opinit-bots/provider/child" "github.com/initia-labs/opinit-bots/types" "go.uber.org/zap" - sdk "github.com/cosmos/cosmos-sdk/types" - challengertypes "github.com/initia-labs/opinit-bots/challenger/types" nodetypes "github.com/initia-labs/opinit-bots/node/types" "github.com/pkg/errors" @@ -19,14 +15,7 @@ func (ch *Child) finalizeDepositHandler(ctx types.Context, args nodetypes.EventH if err != nil { return errors.Wrap(err, "failed to parse finalize deposit event") } - ch.handleFinalizeDeposit(ctx, args.BlockTime, l1BlockHeight, l1Sequence, from, to, amount, baseDenom) - ch.lastFinalizedDepositL1BlockHeight = l1BlockHeight - ch.lastFinalizedDepositL1Sequence = l1Sequence - return nil -} - -func (ch *Child) handleFinalizeDeposit(ctx types.Context, l2BlockTime time.Time, l1BlockHeight int64, l1Sequence uint64, from string, to string, amount sdk.Coin, baseDenom string) { - deposit := challengertypes.NewDeposit(l1Sequence, l1BlockHeight, from, to, baseDenom, amount.String(), l2BlockTime) + deposit := challengertypes.NewDeposit(l1Sequence, l1BlockHeight, from, to, baseDenom, amount.String(), args.BlockTime) ch.eventQueue = append(ch.eventQueue, deposit) ctx.Logger().Info("finalize token deposit", @@ -37,4 +26,8 @@ func (ch *Child) handleFinalizeDeposit(ctx types.Context, l2BlockTime time.Time, zap.String("amount", amount.String()), zap.String("base_denom", baseDenom), ) + + ch.lastFinalizedDepositL1BlockHeight = l1BlockHeight + ch.lastFinalizedDepositL1Sequence = l1Sequence + return nil } diff --git a/challenger/child/deposit_test.go b/challenger/child/deposit_test.go new file mode 100644 index 0000000..dec7247 --- /dev/null +++ b/challenger/child/deposit_test.go @@ -0,0 +1,83 @@ +package child + +import ( + "context" + "strconv" + "testing" + "time" + + ophosttypes "github.com/initia-labs/OPinit/x/ophost/types" + challengertypes "github.com/initia-labs/opinit-bots/challenger/types" + "github.com/initia-labs/opinit-bots/db" + "github.com/initia-labs/opinit-bots/node" + nodetypes "github.com/initia-labs/opinit-bots/node/types" + childprovider "github.com/initia-labs/opinit-bots/provider/child" + "github.com/initia-labs/opinit-bots/types" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestFinalizeDepositHandler(t *testing.T) { + db, err := db.NewMemDB() + require.NoError(t, err) + + childNode := node.NewTestNode(nodetypes.NodeConfig{}, db.WithPrefix([]byte("test_child")), nil, nil, nil, nil) + bridgeInfo := ophosttypes.QueryBridgeResponse{ + BridgeId: 1, + } + + ch := Child{ + BaseChild: childprovider.NewTestBaseChild(0, childNode, nil, bridgeInfo, nil, nodetypes.NodeConfig{}), + eventQueue: make([]challengertypes.ChallengeEvent, 0), + } + + cases := []struct { + name string + eventHandlerArgs nodetypes.EventHandlerArgs + expected []challengertypes.ChallengeEvent + err bool + }{ + { + name: "empty event queue", + eventHandlerArgs: nodetypes.EventHandlerArgs{ + BlockTime: time.Unix(0, 100).UTC(), + EventAttributes: childprovider.FinalizeDepositEvents(1, "sender", "recipient", "denom", "baseDenom", sdk.NewInt64Coin("denom", 10000), 2), + }, + expected: []challengertypes.ChallengeEvent{ + &challengertypes.Deposit{ + EventType: "Deposit", + Sequence: 1, + L1BlockHeight: 2, + From: "sender", + To: "recipient", + L1Denom: "baseDenom", + Amount: "10000denom", + Time: time.Unix(0, 100).UTC(), + }, + }, + err: false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ctx := types.NewContext(context.Background(), zap.NewNop(), "") + + err := ch.finalizeDepositHandler(ctx, tc.eventHandlerArgs) + require.NoError(t, err) + expectedL1Height, err := strconv.ParseInt(tc.eventHandlerArgs.EventAttributes[6].Value, 10, 64) + require.NoError(t, err) + expectedL1Sequence, err := strconv.ParseUint(tc.eventHandlerArgs.EventAttributes[0].Value, 10, 64) + require.NoError(t, err) + + require.Equal(t, expectedL1Height, ch.lastFinalizedDepositL1BlockHeight) + require.Equal(t, expectedL1Sequence, ch.lastFinalizedDepositL1Sequence) + + require.Equal(t, tc.expected, ch.eventQueue) + ch.eventQueue = make([]challengertypes.ChallengeEvent, 0) + }) + } + require.NoError(t, err) +} diff --git a/challenger/child/handler.go b/challenger/child/handler.go index 4b71cc9..d5073ca 100644 --- a/challenger/child/handler.go +++ b/challenger/child/handler.go @@ -92,7 +92,7 @@ func (ch *Child) endBlockHandler(ctx types.Context, args nodetypes.EndBlockArgs) ch.eventHandler.DeletePendingEvents(processedEvents) ch.eventHandler.SetPendingEvents(timeoutEvents) - ch.challenger.SendPendingChallenges(challenges) + ch.challenger.SendPendingChallenges(pendingChallenges) return nil } diff --git a/challenger/child/handler_test.go b/challenger/child/handler_test.go new file mode 100644 index 0000000..fa30020 --- /dev/null +++ b/challenger/child/handler_test.go @@ -0,0 +1,424 @@ +package child + +import ( + "context" + "testing" + "time" + + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + ophosttypes "github.com/initia-labs/OPinit/x/ophost/types" + eventhandler "github.com/initia-labs/opinit-bots/challenger/eventhandler" + challengertypes "github.com/initia-labs/opinit-bots/challenger/types" + "github.com/initia-labs/opinit-bots/db" + "github.com/initia-labs/opinit-bots/merkle" + merkletypes "github.com/initia-labs/opinit-bots/merkle/types" + "github.com/initia-labs/opinit-bots/node" + nodetypes "github.com/initia-labs/opinit-bots/node/types" + childprovider "github.com/initia-labs/opinit-bots/provider/child" + "github.com/initia-labs/opinit-bots/types" + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +func TestBeginBlockHandler(t *testing.T) { + db, err := db.NewMemDB() + require.NoError(t, err) + childDB := db.WithPrefix([]byte("test_child")) + childNode := node.NewTestNode(nodetypes.NodeConfig{}, childDB, nil, nil, nil, nil) + + err = childDB.Set( + append([]byte("working_tree/"), []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09}...), + []byte(`{"version":9,"index":1,"leaf_count":2,"start_leaf_index":1,"last_siblings":{},"done":false}`), + ) + require.NoError(t, err) + + mk, err := merkle.NewMerkle(ophosttypes.GenerateNodeHash) + require.NoError(t, err) + + mockHost := NewMockHost(nil, 1) + mockHost.SetSyncedOutput(1, 1, &ophosttypes.Output{ + OutputRoot: []byte("output_root"), + L2BlockNumber: 15, + L1BlockTime: time.Unix(0, 10000).UTC(), + L1BlockNumber: 1, + }) + + ch := Child{ + BaseChild: childprovider.NewTestBaseChild(0, childNode, mk, ophosttypes.QueryBridgeResponse{ + BridgeId: 1, + }, nil, nodetypes.NodeConfig{}), + host: mockHost, + stage: db.NewStage(), + eventQueue: make([]challengertypes.ChallengeEvent, 0), + } + + require.Empty(t, ch.eventQueue) + ch.eventQueue = append(ch.eventQueue, &challengertypes.Deposit{ + EventType: "Deposit", + }) + require.Len(t, ch.eventQueue, 1) + + require.Equal(t, 0, ch.stage.Len()) + err = ch.stage.Set([]byte("key"), []byte("value")) + require.NoError(t, err) + require.Equal(t, 1, ch.stage.Len()) + + err = ch.beginBlockHandler(types.Context{}, nodetypes.BeginBlockArgs{ + Block: cmtproto.Block{ + Header: cmtproto.Header{ + Height: 10, + }, + }, + }) + require.NoError(t, err) + + require.Empty(t, ch.eventQueue) + require.Equal(t, 0, ch.stage.Len()) +} + +func TestEndBlockHandler(t *testing.T) { + mockCount := int64(0) + mockTimestampFetcher := func() int64 { + mockCount++ + return mockCount + } + types.CurrentNanoTimestamp = mockTimestampFetcher + + cases := []struct { + name string + host *mockHost + challenger *mockChallenger + pendingEvents []challengertypes.ChallengeEvent + eventQueue []challengertypes.ChallengeEvent + dbChanges []types.KV + endBlockArgs nodetypes.EndBlockArgs + expectedPendingEvents []challengertypes.ChallengeEvent + expectedChallenges []challengertypes.Challenge + expectedDB []types.KV + err bool + }{ + { + name: "no pending events", + host: NewMockHost(nil, 1), + challenger: NewMockChallenger(nil), + pendingEvents: []challengertypes.ChallengeEvent{}, + eventQueue: []challengertypes.ChallengeEvent{ + &challengertypes.Output{ + EventType: "Output", + L2BlockNumber: 15, + OutputIndex: 1, + OutputRoot: []byte(""), + Time: time.Unix(0, 10000).UTC(), + Timeout: false, + }, + &challengertypes.Deposit{ + EventType: "Deposit", + Sequence: 1, + L1BlockHeight: 5, + From: "init1hrasklz3tr6s9rls4r8fjuf0k4zuha6w9rude5", + To: "init1z3689ct7pc72yr5an97nsj89dnlefydxwdhcv0", + L1Denom: "l1Denom", + Amount: "100l2denom", + Time: time.Unix(0, 10000).UTC(), + Timeout: false, + }, + }, + dbChanges: []types.KV{ + { + Key: []byte("key1"), + Value: []byte("value1"), + }, + }, + endBlockArgs: nodetypes.EndBlockArgs{ + Block: cmtproto.Block{ + Header: cmtproto.Header{ + Height: 10, + Time: time.Unix(0, 10000).UTC(), + }, + }, + }, + expectedPendingEvents: []challengertypes.ChallengeEvent{}, + expectedChallenges: []challengertypes.Challenge{}, + expectedDB: []types.KV{}, + err: true, + }, + { + name: "existing deposit events", + host: NewMockHost(nil, 1), + challenger: NewMockChallenger(nil), + pendingEvents: []challengertypes.ChallengeEvent{ + &challengertypes.Deposit{ + EventType: "Deposit", + Sequence: 1, + L1BlockHeight: 5, + From: "init1hrasklz3tr6s9rls4r8fjuf0k4zuha6w9rude5", + To: "init1z3689ct7pc72yr5an97nsj89dnlefydxwdhcv0", + L1Denom: "l1Denom", + Amount: "100l2denom", + Time: time.Unix(0, 9000).UTC(), + Timeout: false, + }, + &challengertypes.Deposit{ + EventType: "Deposit", + Sequence: 2, + L1BlockHeight: 5, + From: "init1hrasklz3tr6s9rls4r8fjuf0k4zuha6w9rude5", + To: "init1z3689ct7pc72yr5an97nsj89dnlefydxwdhcv0", + L1Denom: "l1Denom", + Amount: "100l2denom", + Time: time.Unix(0, 9000).UTC(), + Timeout: false, + }, + &challengertypes.Deposit{ + EventType: "Deposit", + Sequence: 3, + L1BlockHeight: 5, + From: "init1hrasklz3tr6s9rls4r8fjuf0k4zuha6w9rude5", + To: "init1z3689ct7pc72yr5an97nsj89dnlefydxwdhcv0", + L1Denom: "l1Denom", + Amount: "100l2denom", + Time: time.Unix(0, 9000).UTC(), + Timeout: false, + }, + }, + eventQueue: []challengertypes.ChallengeEvent{ + &challengertypes.Deposit{ + EventType: "Deposit", + Sequence: 1, + L1BlockHeight: 5, + From: "init1hrasklz3tr6s9rls4r8fjuf0k4zuha6w9rude5", + To: "init1z3689ct7pc72yr5an97nsj89dnlefydxwdhcv0", + L1Denom: "l1Denom", + Amount: "100l2denom", + Time: time.Unix(0, 10000).UTC(), + Timeout: false, + }, + &challengertypes.Deposit{ + EventType: "Deposit", + Sequence: 2, + L1BlockHeight: 5, + From: "init1hrasklz3tr6s9rls4r8fjuf0k4zuha6w9rude5", + To: "init1z3689ct7pc72yr5an97nsj89dnlefydxwdhcv0", + L1Denom: "l1Denom", + Amount: "100l2denom", + Time: time.Unix(0, 10000).UTC(), + Timeout: false, + }, + }, + dbChanges: []types.KV{}, + endBlockArgs: nodetypes.EndBlockArgs{ + Block: cmtproto.Block{ + Header: cmtproto.Header{ + Height: 10, + Time: time.Unix(0, 10000).UTC(), + }, + }, + }, + + expectedPendingEvents: []challengertypes.ChallengeEvent{ + &challengertypes.Deposit{ + EventType: "Deposit", + Sequence: 3, + L1BlockHeight: 5, + From: "init1hrasklz3tr6s9rls4r8fjuf0k4zuha6w9rude5", + To: "init1z3689ct7pc72yr5an97nsj89dnlefydxwdhcv0", + L1Denom: "l1Denom", + Amount: "100l2denom", + Time: time.Unix(0, 9000).UTC(), + Timeout: false, + }, + }, + expectedChallenges: []challengertypes.Challenge{}, + expectedDB: []types.KV{ + { + Key: []byte("test_child/synced_height"), + Value: []byte("10"), + }, + }, + err: false, + }, + { + name: "deposit timeout", + host: NewMockHost(nil, 1), + challenger: NewMockChallenger(nil), + pendingEvents: []challengertypes.ChallengeEvent{ + &challengertypes.Deposit{ + EventType: "Deposit", + Sequence: 1, + L1BlockHeight: 5, + From: "init1hrasklz3tr6s9rls4r8fjuf0k4zuha6w9rude5", + To: "init1z3689ct7pc72yr5an97nsj89dnlefydxwdhcv0", + L1Denom: "l1Denom", + Amount: "100l2denom", + Time: time.Unix(0, 9000).UTC(), + Timeout: false, + }, + &challengertypes.Deposit{ + EventType: "Deposit", + Sequence: 2, + L1BlockHeight: 50, + From: "init1hrasklz3tr6s9rls4r8fjuf0k4zuha6w9rude5", + To: "init1z3689ct7pc72yr5an97nsj89dnlefydxwdhcv0", + L1Denom: "l1Denom", + Amount: "100l2denom", + Time: time.Unix(0, 9000).UTC(), + Timeout: false, + }, + &challengertypes.Deposit{ + EventType: "Deposit", + Sequence: 3, + L1BlockHeight: 50, + From: "init1hrasklz3tr6s9rls4r8fjuf0k4zuha6w9rude5", + To: "init1z3689ct7pc72yr5an97nsj89dnlefydxwdhcv0", + L1Denom: "l1Denom", + Amount: "100l2denom", + Time: time.Unix(0, 9000).UTC(), + Timeout: false, + }, + }, + eventQueue: []challengertypes.ChallengeEvent{ + &challengertypes.Deposit{ + EventType: "Deposit", + Sequence: 1, + L1BlockHeight: 5, + From: "init1hrasklz3tr6s9rls4r8fjuf0k4zuha6w9rude5", + To: "init1z3689ct7pc72yr5an97nsj89dnlefydxwdhcv0", + L1Denom: "l1Denom", + Amount: "100l2denom", + Time: time.Unix(0, 20000).UTC(), + Timeout: false, + }, + }, + dbChanges: []types.KV{}, + endBlockArgs: nodetypes.EndBlockArgs{ + Block: cmtproto.Block{ + Header: cmtproto.Header{ + Height: 10, + Time: time.Unix(0, 20000).UTC(), + }, + }, + }, + + expectedPendingEvents: []challengertypes.ChallengeEvent{ + &challengertypes.Deposit{ + EventType: "Deposit", + Sequence: 2, + L1BlockHeight: 50, + From: "init1hrasklz3tr6s9rls4r8fjuf0k4zuha6w9rude5", + To: "init1z3689ct7pc72yr5an97nsj89dnlefydxwdhcv0", + L1Denom: "l1Denom", + Amount: "100l2denom", + Time: time.Unix(0, 9000).UTC(), + Timeout: true, + }, + &challengertypes.Deposit{ + EventType: "Deposit", + Sequence: 3, + L1BlockHeight: 50, + From: "init1hrasklz3tr6s9rls4r8fjuf0k4zuha6w9rude5", + To: "init1z3689ct7pc72yr5an97nsj89dnlefydxwdhcv0", + L1Denom: "l1Denom", + Amount: "100l2denom", + Time: time.Unix(0, 9000).UTC(), + Timeout: true, + }, + }, + expectedChallenges: []challengertypes.Challenge{ + { + EventType: "Deposit", + Id: challengertypes.ChallengeId{ + Type: challengertypes.EventTypeDeposit, + Id: 2, + }, + Time: time.Unix(0, 20000).UTC(), + }, + { + EventType: "Deposit", + Id: challengertypes.ChallengeId{ + Type: challengertypes.EventTypeDeposit, + Id: 3, + }, + Time: time.Unix(0, 20000).UTC(), + }, + }, + expectedDB: []types.KV{ + { + Key: []byte("test_child/synced_height"), + Value: []byte("10"), + }, + }, + err: false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + db, err := db.NewMemDB() + defer func() { + require.NoError(t, db.Close()) + }() + childdb := db.WithPrefix([]byte("test_child")) + require.NoError(t, err) + tc.host.db = db.WithPrefix([]byte("test_host")) + tc.challenger.db = db + + childNode := node.NewTestNode(nodetypes.NodeConfig{}, childdb, nil, nil, nil, nil) + mk, err := merkle.NewMerkle(ophosttypes.GenerateNodeHash) + require.NoError(t, err) + err = mk.PrepareWorkingTree(merkletypes.TreeInfo{ + Version: 11, + Index: 1, + LeafCount: 1, + StartLeafIndex: 1, + LastSiblings: map[uint8][]byte{}, + Done: false, + }) + require.NoError(t, err) + ch := Child{ + BaseChild: childprovider.NewTestBaseChild(0, childNode, mk, ophosttypes.QueryBridgeResponse{}, nil, nodetypes.NodeConfig{}), + host: tc.host, + challenger: tc.challenger, + stage: childdb.NewStage(), + eventQueue: tc.eventQueue, + + eventHandler: eventhandler.NewChallengeEventHandler(childdb), + } + + tc.host.SetSyncedOutput(1, 1, &ophosttypes.Output{ + OutputRoot: []byte("output_root"), + L2BlockNumber: 15, + L1BlockTime: time.Unix(0, 10000).UTC(), + L1BlockNumber: 1, + }) + + err = ch.eventHandler.Initialize(10000) + require.NoError(t, err) + ch.eventHandler.SetPendingEvents(tc.pendingEvents) + + for _, kv := range tc.dbChanges { + err := ch.stage.Set(kv.Key, kv.Value) + require.NoError(t, err) + } + + err = ch.endBlockHandler(types.NewContext(context.Background(), zap.NewNop(), ""), tc.endBlockArgs) + if !tc.err { + require.NoError(t, err) + require.Equal(t, len(tc.expectedChallenges), len(tc.challenger.pendingChallenges)) + for i, challenge := range tc.expectedChallenges { + require.Equal(t, challenge.EventType, tc.challenger.pendingChallenges[i].EventType) + require.Equal(t, challenge.Id, tc.challenger.pendingChallenges[i].Id) + require.Equal(t, challenge.Time, tc.challenger.pendingChallenges[i].Time) + } + require.ElementsMatch(t, tc.expectedPendingEvents, ch.eventHandler.GetAllPendingEvents()) + + for _, kv := range tc.expectedDB { + value, err := db.Get(kv.Key) + require.NoError(t, err) + require.Equal(t, kv.Value, value) + } + } else { + require.Error(t, err) + } + }) + } +} diff --git a/challenger/child/oracle_test.go b/challenger/child/oracle_test.go new file mode 100644 index 0000000..e97486a --- /dev/null +++ b/challenger/child/oracle_test.go @@ -0,0 +1,70 @@ +package child + +import ( + "context" + "testing" + "time" + + ophosttypes "github.com/initia-labs/OPinit/x/ophost/types" + challengertypes "github.com/initia-labs/opinit-bots/challenger/types" + "github.com/initia-labs/opinit-bots/db" + "github.com/initia-labs/opinit-bots/node" + nodetypes "github.com/initia-labs/opinit-bots/node/types" + childprovider "github.com/initia-labs/opinit-bots/provider/child" + "github.com/initia-labs/opinit-bots/types" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "golang.org/x/crypto/sha3" +) + +func TestOracleTxHandler(t *testing.T) { + db, err := db.NewMemDB() + require.NoError(t, err) + childNode := node.NewTestNode(nodetypes.NodeConfig{}, db.WithPrefix([]byte("test_child")), nil, nil, nil, nil) + bridgeInfo := ophosttypes.QueryBridgeResponse{ + BridgeId: 1, + } + + ch := Child{ + BaseChild: childprovider.NewTestBaseChild(0, childNode, nil, bridgeInfo, nil, nodetypes.NodeConfig{}), + eventQueue: make([]challengertypes.ChallengeEvent, 0), + } + + oracleTxDataChecksum := sha3.Sum256([]byte("oracle_tx")) + + cases := []struct { + name string + oracleEnabled bool + blockHeight int64 + blockTime time.Time + extCommitBz []byte + expected []challengertypes.ChallengeEvent + }{ + { + name: "oracle enabled", + oracleEnabled: true, + blockHeight: 3, + blockTime: time.Unix(0, 100).UTC(), + extCommitBz: []byte("oracle_tx"), + expected: []challengertypes.ChallengeEvent{ + &challengertypes.Oracle{ + EventType: "Oracle", + L1Height: 3, + Data: oracleTxDataChecksum[:], + Time: time.Unix(0, 100).UTC(), + Timeout: false, + }, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ch.oracleTxHandler(types.NewContext(context.Background(), zap.NewNop(), ""), tc.blockTime, "sender", tc.blockHeight, tc.extCommitBz) + + require.Equal(t, ch.eventQueue, tc.expected) + ch.eventQueue = make([]challengertypes.ChallengeEvent, 0) + }) + } + require.NoError(t, err) +} diff --git a/challenger/child/withdraw.go b/challenger/child/withdraw.go index e1f164b..43b4bd5 100644 --- a/challenger/child/withdraw.go +++ b/challenger/child/withdraw.go @@ -21,7 +21,7 @@ import ( ) func (ch *Child) initiateWithdrawalHandler(ctx types.Context, args nodetypes.EventHandlerArgs) error { - l2Sequence, amount, from, to, baseDenom, err := childprovider.ParseInitiateWithdrawal(args.EventAttributes) + l2Sequence, amount, from, to, _, baseDenom, err := childprovider.ParseInitiateWithdrawal(args.EventAttributes) if err != nil { return errors.Wrap(err, "failed to parse initiate withdrawal event") } diff --git a/challenger/child/withdraw_test.go b/challenger/child/withdraw_test.go new file mode 100644 index 0000000..c3b9a56 --- /dev/null +++ b/challenger/child/withdraw_test.go @@ -0,0 +1,738 @@ +package child + +import ( + "context" + "testing" + "time" + + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + ophosttypes "github.com/initia-labs/OPinit/x/ophost/types" + challengertypes "github.com/initia-labs/opinit-bots/challenger/types" + "github.com/initia-labs/opinit-bots/db" + "github.com/initia-labs/opinit-bots/merkle" + merkletypes "github.com/initia-labs/opinit-bots/merkle/types" + "github.com/initia-labs/opinit-bots/node" + nodetypes "github.com/initia-labs/opinit-bots/node/types" + childprovider "github.com/initia-labs/opinit-bots/provider/child" + "github.com/initia-labs/opinit-bots/types" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestInitiateWithdrawalHandler(t *testing.T) { + bridgeInfo := ophosttypes.QueryBridgeResponse{ + BridgeId: 1, + } + + cases := []struct { + name string + lastWorkingTree merkletypes.TreeInfo + eventHandlerArgs nodetypes.EventHandlerArgs + expectedStage []types.KV + err bool + panic bool + }{ + { + name: "success", + lastWorkingTree: merkletypes.TreeInfo{ + Version: 10, + Index: 5, + LeafCount: 0, + StartLeafIndex: 1, + LastSiblings: make(map[uint8][]byte), + Done: false, + }, + eventHandlerArgs: nodetypes.EventHandlerArgs{ + BlockHeight: 11, + BlockTime: time.Unix(0, 10000).UTC(), + Tx: []byte("txbytes"), // EA58654919E6F3E08370DE723D8DA223F1DFE78DD28D0A23E6F18BFA0815BB99 + EventAttributes: childprovider.InitiateWithdrawalEvents("from", "to", "denom", "uinit", sdk.NewInt64Coin("denom", 10000), 1), + }, + expectedStage: []types.KV{ + { // local node 0 + Key: append([]byte("/test_child/node/"), []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}...), + Value: []byte{0x57, 0xee, 0xee, 0x92, 0xac, 0x2b, 0xab, 0x40, 0x5a, 0xea, 0x48, 0xfa, 0xdd, 0x31, 0x19, 0xd4, 0x2e, 0xe6, 0xe1, 0x97, 0xbb, 0xa6, 0xa1, 0x11, 0x9a, 0x27, 0x7f, 0x39, 0x0b, 0x4d, 0x9d, 0xe6}, + }, + }, + err: false, + panic: false, + }, + { + name: "second withdrawal", + lastWorkingTree: merkletypes.TreeInfo{ + Version: 10, + Index: 5, + LeafCount: 1, + StartLeafIndex: 100, + LastSiblings: map[uint8][]byte{ + 0: {0x5e, 0xc5, 0xb8, 0x13, 0x43, 0xb9, 0x76, 0xbb, 0xef, 0x23, 0xbc, 0x6e, 0x6a, 0xbe, 0x44, 0xa6, 0xa7, 0x17, 0x8c, 0x66, 0xae, 0xfd, 0x78, 0xe8, 0xd8, 0x1c, 0x73, 0x36, 0xf3, 0x32, 0xb6, 0x31}, + }, + Done: false, + }, + eventHandlerArgs: nodetypes.EventHandlerArgs{ + BlockHeight: 11, + BlockTime: time.Unix(0, 10000).UTC(), + Tx: []byte("txbytes"), + EventAttributes: childprovider.InitiateWithdrawalEvents("from", "to", "denom", "uinit", sdk.NewInt64Coin("denom", 10000), 101), + }, + expectedStage: []types.KV{ + { // local node 1 + Key: append([]byte("/test_child/node/"), []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}...), + Value: []byte{0x1f, 0x39, 0xf9, 0xf1, 0x4d, 0xb6, 0xad, 0xf5, 0xca, 0xd9, 0x56, 0x42, 0x38, 0x81, 0x73, 0x8e, 0xe7, 0x69, 0x75, 0x89, 0x30, 0xe6, 0xfd, 0x1e, 0x67, 0x64, 0x27, 0xb2, 0x92, 0x05, 0x94, 0x1b}, + }, + { // height 1, local node 0 + Key: append([]byte("/test_child/node/"), []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}...), + Value: []byte{0x06, 0x90, 0x8d, 0x0d, 0x10, 0x0f, 0x55, 0x78, 0xaa, 0x12, 0x81, 0xa1, 0x72, 0xbf, 0x46, 0x65, 0x09, 0xd3, 0xa0, 0x3c, 0xb2, 0x4c, 0xa1, 0xb4, 0x32, 0xb9, 0x11, 0x71, 0x5e, 0x10, 0xa9, 0xb6}, + }, + }, + err: false, + panic: false, + }, + { + name: "panic: working tree leaf count mismatch", + lastWorkingTree: merkletypes.TreeInfo{ + Version: 9, + Index: 5, + LeafCount: 0, + StartLeafIndex: 100, + LastSiblings: make(map[uint8][]byte), + Done: false, + }, + eventHandlerArgs: nodetypes.EventHandlerArgs{ + BlockHeight: 10, + BlockTime: time.Unix(0, 10000).UTC(), + Tx: []byte("txbytes"), + EventAttributes: childprovider.InitiateWithdrawalEvents("from", "to", "denom", "uinit", sdk.NewInt64Coin("denom", 10000), 101), + }, + expectedStage: nil, + err: false, + panic: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ctx := types.NewContext(context.Background(), zap.NewNop(), "") + + basedb, err := db.NewMemDB() + require.NoError(t, err) + + childdb := basedb.WithPrefix([]byte("test_child")) + + childNode := node.NewTestNode(nodetypes.NodeConfig{}, childdb, nil, nil, nil, nil) + + mk, err := merkle.NewMerkle(ophosttypes.GenerateNodeHash) + require.NoError(t, err) + err = mk.PrepareWorkingTree(tc.lastWorkingTree) + require.NoError(t, err) + + stage := childdb.NewStage().(db.Stage) + ch := Child{ + BaseChild: childprovider.NewTestBaseChild(0, childNode, mk, bridgeInfo, nil, nodetypes.NodeConfig{}), + stage: stage, + eventQueue: make([]challengertypes.ChallengeEvent, 0), + } + + if tc.panic { + require.Panics(t, func() { + ch.initiateWithdrawalHandler(ctx, tc.eventHandlerArgs) //nolint + }) + return + } + + err = ch.initiateWithdrawalHandler(ctx, tc.eventHandlerArgs) + if !tc.err { + require.NoError(t, err) + + if tc.expectedStage != nil { + allkvs := stage.All() + for _, kv := range tc.expectedStage { + require.Equal(t, kv.Value, allkvs[string(kv.Key)]) + } + } + } else { + require.Error(t, err) + } + }) + } +} + +func TestPrepareTree(t *testing.T) { + bridgeInfo := ophosttypes.QueryBridgeResponse{ + BridgeId: 1, + } + + cases := []struct { + name string + childDBState []types.KV + blockHeight int64 + initializeTreeFnMaker func(*merkle.Merkle) func(int64) (bool, error) + expected merkletypes.TreeInfo + err bool + panic bool + }{ + { + name: "new height 6", + childDBState: []types.KV{ + { + Key: append([]byte("working_tree/"), []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05}...), + Value: []byte(`{"version":5,"index":2,"leaf_count":0,"start_leaf_index":1,"last_siblings":{},"done":false}`), + }, + }, + blockHeight: 6, + initializeTreeFnMaker: nil, + expected: merkletypes.TreeInfo{ + Version: 6, + Index: 2, + LeafCount: 0, + StartLeafIndex: 1, + LastSiblings: make(map[uint8][]byte), + Done: false, + }, + err: false, + panic: false, + }, + { + name: "no tree height 5, new height 6, no initializeTreeFn", + childDBState: nil, + blockHeight: 6, + initializeTreeFnMaker: nil, + expected: merkletypes.TreeInfo{ + Version: 6, + Index: 2, + LeafCount: 0, + StartLeafIndex: 1, + LastSiblings: make(map[uint8][]byte), + Done: false, + }, + err: false, + panic: true, + }, + { + name: "no tree height 5, new height 6, no initializing tree", + childDBState: nil, + blockHeight: 6, + initializeTreeFnMaker: func(m *merkle.Merkle) func(i int64) (bool, error) { + return func(i int64) (bool, error) { + return false, nil + } + }, + expected: merkletypes.TreeInfo{ + Version: 6, + Index: 2, + LeafCount: 0, + StartLeafIndex: 1, + LastSiblings: make(map[uint8][]byte), + Done: false, + }, + err: false, + panic: true, + }, + { + name: "tree done at 5, new height 6", + childDBState: []types.KV{ + { + Key: append([]byte("working_tree/"), []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05}...), + Value: []byte(`{"version":5,"index":2,"leaf_count":2,"start_leaf_index":1,"last_siblings":{},"done":true}`), + }, + }, + blockHeight: 6, + initializeTreeFnMaker: nil, + expected: merkletypes.TreeInfo{ + Version: 6, + Index: 3, + LeafCount: 0, + StartLeafIndex: 3, + LastSiblings: make(map[uint8][]byte), + Done: false, + }, + err: false, + panic: false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + basedb, err := db.NewMemDB() + require.NoError(t, err) + + childdb := basedb.WithPrefix([]byte("test_child")) + + childNode := node.NewTestNode(nodetypes.NodeConfig{}, childdb, nil, nil, nil, nil) + + mk, err := merkle.NewMerkle(ophosttypes.GenerateNodeHash) + require.NoError(t, err) + + var initializeFn func(i int64) (bool, error) + if tc.initializeTreeFnMaker != nil { + initializeFn = tc.initializeTreeFnMaker(mk) + } + + ch := Child{ + BaseChild: childprovider.NewTestBaseChild(0, childNode, mk, bridgeInfo, initializeFn, nodetypes.NodeConfig{}), + eventQueue: make([]challengertypes.ChallengeEvent, 0), + } + + for _, kv := range tc.childDBState { + err = childdb.Set(kv.Key, kv.Value) + require.NoError(t, err) + } + + if tc.panic { + require.Panics(t, func() { + ch.prepareTree(tc.blockHeight) //nolint + }) + return + } + err = ch.prepareTree(tc.blockHeight) + if !tc.err { + require.NoError(t, err) + + tree, err := mk.WorkingTree() + require.NoError(t, err) + + require.Equal(t, tc.expected, tree) + } else { + require.Error(t, err) + } + }) + } +} + +func TestPrepareOutput(t *testing.T) { + cases := []struct { + name string + bridgeInfo ophosttypes.QueryBridgeResponse + hostOutputs map[uint64]ophosttypes.Output + lastWorkingTree merkletypes.TreeInfo + expected func() (lastOutputTime time.Time, nextOutputTime time.Time, finalizingBlockHeight int64) + err bool + }{ + { + name: "no output, index 1", + bridgeInfo: ophosttypes.QueryBridgeResponse{ + BridgeId: 1, + BridgeConfig: ophosttypes.BridgeConfig{ + SubmissionInterval: 100, + }, + }, + hostOutputs: map[uint64]ophosttypes.Output{}, + lastWorkingTree: merkletypes.TreeInfo{ + Version: 9, + Index: 1, + LeafCount: 2, + StartLeafIndex: 1, + LastSiblings: make(map[uint8][]byte), + Done: false, + }, + expected: func() (lastOutputTime time.Time, nextOutputTime time.Time, finalizingBlockHeight int64) { + return time.Time{}, time.Time{}, 0 + }, + err: true, + }, + { + name: "no output, index 3", // chain rolled back + bridgeInfo: ophosttypes.QueryBridgeResponse{ + BridgeId: 1, + BridgeConfig: ophosttypes.BridgeConfig{ + SubmissionInterval: 100, + }, + }, + hostOutputs: map[uint64]ophosttypes.Output{}, + lastWorkingTree: merkletypes.TreeInfo{ + Version: 9, + Index: 3, + LeafCount: 2, + StartLeafIndex: 1, + LastSiblings: make(map[uint8][]byte), + Done: false, + }, + expected: func() (lastOutputTime time.Time, nextOutputTime time.Time, finalizingBlockHeight int64) { + return time.Time{}, time.Time{}, 0 + }, + err: true, + }, + { + name: "outputs {1}, index 1", // sync + bridgeInfo: ophosttypes.QueryBridgeResponse{ + BridgeId: 1, + BridgeConfig: ophosttypes.BridgeConfig{ + SubmissionInterval: 100, + }, + }, + hostOutputs: map[uint64]ophosttypes.Output{ + 1: { + L1BlockTime: time.Time{}, + L2BlockNumber: 10, + }, + }, + lastWorkingTree: merkletypes.TreeInfo{ + Version: 9, + Index: 1, + LeafCount: 2, + StartLeafIndex: 1, + LastSiblings: make(map[uint8][]byte), + Done: false, + }, + expected: func() (lastOutputTime time.Time, nextOutputTime time.Time, finalizingBlockHeight int64) { + return time.Time{}, time.Time{}, 10 + }, + err: false, + }, + { + name: "outputs {1}, index 2", + bridgeInfo: ophosttypes.QueryBridgeResponse{ + BridgeId: 1, + BridgeConfig: ophosttypes.BridgeConfig{ + SubmissionInterval: 300, + }, + }, + hostOutputs: map[uint64]ophosttypes.Output{ + 1: { + L1BlockTime: time.Unix(0, 10000).UTC(), + L2BlockNumber: 10, + }, + }, + lastWorkingTree: merkletypes.TreeInfo{ + Version: 9, + Index: 2, + LeafCount: 2, + StartLeafIndex: 1, + LastSiblings: make(map[uint8][]byte), + Done: false, + }, + expected: func() (lastOutputTime time.Time, nextOutputTime time.Time, finalizingBlockHeight int64) { + return time.Unix(0, 10000).UTC(), time.Unix(0, 10200).UTC(), 0 + }, + err: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + basedb, err := db.NewMemDB() + require.NoError(t, err) + + childdb := basedb.WithPrefix([]byte("test_child")) + + childNode := node.NewTestNode(nodetypes.NodeConfig{}, childdb, nil, nil, nil, nil) + + mk, err := merkle.NewMerkle(ophosttypes.GenerateNodeHash) + require.NoError(t, err) + err = mk.PrepareWorkingTree(tc.lastWorkingTree) + require.NoError(t, err) + + mockHost := NewMockHost(nil, 5) + + for outputIndex, output := range tc.hostOutputs { + mockHost.SetSyncedOutput(tc.bridgeInfo.BridgeId, outputIndex, &output) + } + + ch := Child{ + BaseChild: childprovider.NewTestBaseChild(0, childNode, mk, tc.bridgeInfo, nil, nodetypes.NodeConfig{}), + host: mockHost, + eventQueue: make([]challengertypes.ChallengeEvent, 0), + } + + err = ch.prepareOutput(context.TODO()) + if !tc.err { + require.NoError(t, err) + + expectedLastOutputTime, expectedNextOutputTime, expectedFinalizingBlockHeight := tc.expected() + require.Equal(t, expectedLastOutputTime, ch.lastOutputTime) + require.Equal(t, expectedNextOutputTime, ch.nextOutputTime) + require.Equal(t, expectedFinalizingBlockHeight, ch.finalizingBlockHeight) + } else { + require.Error(t, err) + } + }) + } +} + +func TestHandleTree(t *testing.T) { + bridgeInfo := ophosttypes.QueryBridgeResponse{ + BridgeId: 1, + BridgeConfig: ophosttypes.BridgeConfig{ + SubmissionInterval: 300, + }, + } + + cases := []struct { + name string + blockHeight int64 + latestHeight int64 + blockHeader cmtproto.Header + lastWorkingTree merkletypes.TreeInfo + lastOutputTime time.Time + nextOutputTime time.Time + finalizingBlockHeight int64 + + expected func() (storageRoot []byte, lastOutputTime time.Time, nextOutputTime time.Time, finalizingBlockHeight int64) + expectedStage []types.KV + err bool + panic bool + }{ + { + name: "current height 5, latest height 5, no leaf, not saving finalized tree", // not saving finalized tree + blockHeight: 5, + latestHeight: 5, + blockHeader: cmtproto.Header{ + Time: time.Unix(0, 10100).UTC(), + }, + lastWorkingTree: merkletypes.TreeInfo{ + Version: 4, + Index: 3, + LeafCount: 0, + StartLeafIndex: 10, + LastSiblings: make(map[uint8][]byte), + Done: false, + }, + lastOutputTime: time.Time{}, + nextOutputTime: time.Unix(0, 10000).UTC(), + finalizingBlockHeight: 0, + + expected: func() (storageRoot []byte, lastOutputTime time.Time, nextOutputTime time.Time, finalizingBlockHeight int64) { + return nil, time.Time{}, time.Unix(0, 10000).UTC(), 0 + }, + expectedStage: []types.KV{ + { + Key: append([]byte("/test_child/working_tree/"), []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05}...), + Value: []byte(`{"version":5,"index":3,"leaf_count":0,"start_leaf_index":10,"last_siblings":{},"done":false}`), + }, + }, + err: false, + panic: false, + }, + { + name: "current height 5, latest height 5, 2 leaves, finalizing tree", + blockHeight: 5, + latestHeight: 5, + blockHeader: cmtproto.Header{ + Time: time.Unix(0, 10100).UTC(), + }, + lastWorkingTree: merkletypes.TreeInfo{ + Version: 4, + Index: 3, + LeafCount: 2, + StartLeafIndex: 10, + LastSiblings: map[uint8][]byte{ + 0: {0xf7, 0x58, 0xe5, 0x5d, 0xb1, 0x30, 0x74, 0x4b, 0x05, 0xad, 0x66, 0x94, 0xb2, 0x8b, 0xe4, 0xab, 0x73, 0x0d, 0xe0, 0xdc, 0x09, 0xde, 0x5c, 0x0c, 0x42, 0xab, 0x64, 0x66, 0xc8, 0x06, 0xdc, 0x10}, + 1: {0x50, 0x26, 0x55, 0x2e, 0x7b, 0x21, 0xca, 0xb5, 0x27, 0xe4, 0x16, 0x9e, 0x66, 0x46, 0x02, 0xb8, 0x5d, 0x03, 0x67, 0x0b, 0xb5, 0x57, 0xe3, 0x29, 0x18, 0xd9, 0x33, 0xe3, 0xd5, 0x92, 0x5c, 0x7e}, + }, + Done: false, + }, + lastOutputTime: time.Time{}, + nextOutputTime: time.Unix(0, 10300).UTC(), + finalizingBlockHeight: 5, + + expected: func() (storageRoot []byte, lastOutputTime time.Time, nextOutputTime time.Time, finalizingBlockHeight int64) { + return nil, time.Unix(0, 10100).UTC(), time.Unix(0, 10300).UTC(), 0 + }, + expectedStage: []types.KV{ + { + Key: append([]byte("/test_child/working_tree/"), []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05}...), + Value: []byte(`{"version":5,"index":3,"leaf_count":2,"start_leaf_index":10,"last_siblings":{"0":"91jlXbEwdEsFrWaUsovkq3MN4NwJ3lwMQqtkZsgG3BA=","1":"UCZVLnshyrUn5BaeZkYCuF0DZwu1V+MpGNkz49WSXH4="},"done":true}`), + }, + { + Key: append([]byte("/test_child/finalized_tree/"), []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a}...), + Value: []byte(`{"tree_index":3,"tree_height":1,"root":"UCZVLnshyrUn5BaeZkYCuF0DZwu1V+MpGNkz49WSXH4=","start_leaf_index":10,"leaf_count":2}`), + }, + }, + err: false, + panic: false, + }, + { + name: "current height 5, latest height 5, 3 leaves", + blockHeight: 5, + latestHeight: 5, + blockHeader: cmtproto.Header{ + Time: time.Unix(0, 10100).UTC(), + }, + lastWorkingTree: merkletypes.TreeInfo{ + Version: 4, + Index: 3, + LeafCount: 3, + StartLeafIndex: 10, + LastSiblings: map[uint8][]byte{ + 0: {0xd9, 0xf8, 0x70, 0xb0, 0x6d, 0x46, 0x43, 0xc5, 0x9f, 0xbd, 0x0a, 0x9a, 0xd1, 0xe5, 0x5c, 0x43, 0x98, 0xdd, 0xae, 0xf1, 0xca, 0xc2, 0xd7, 0xfb, 0xcf, 0xd5, 0xe0, 0x11, 0xb6, 0x83, 0xb8, 0x33}, + 1: {0x50, 0x26, 0x55, 0x2e, 0x7b, 0x21, 0xca, 0xb5, 0x27, 0xe4, 0x16, 0x9e, 0x66, 0x46, 0x02, 0xb8, 0x5d, 0x03, 0x67, 0x0b, 0xb5, 0x57, 0xe3, 0x29, 0x18, 0xd9, 0x33, 0xe3, 0xd5, 0x92, 0x5c, 0x7e}, + }, + Done: false, + }, + lastOutputTime: time.Time{}, + nextOutputTime: time.Unix(0, 10300).UTC(), + finalizingBlockHeight: 5, + + expected: func() (storageRoot []byte, lastOutputTime time.Time, nextOutputTime time.Time, finalizingBlockHeight int64) { + return nil, time.Unix(0, 10100).UTC(), time.Unix(0, 10300).UTC(), 0 + }, + expectedStage: []types.KV{ + { + Key: append([]byte("/test_child/working_tree/"), []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05}...), + Value: []byte(`{"version":5,"index":3,"leaf_count":3,"start_leaf_index":10,"last_siblings":{"0":"2fhwsG1GQ8WfvQqa0eVcQ5jdrvHKwtf7z9XgEbaDuDM=","1":"rRHIp/aKAeTbiJgLTE+o5pTqhf9HmGTslmATJK72mmc=","2":"/9R6cfY6ilAJVu80sfq71C8HyF53960hJwHgZNq99qM="},"done":true}`), + }, + { + Key: append([]byte("/test_child/finalized_tree/"), []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a}...), + Value: []byte(`{"tree_index":3,"tree_height":2,"root":"/9R6cfY6ilAJVu80sfq71C8HyF53960hJwHgZNq99qM=","start_leaf_index":10,"leaf_count":3}`), + }, + { // height 0, index 3 + Key: append([]byte("/test_child/node/"), []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3}...), + Value: []byte{0xd9, 0xf8, 0x70, 0xb0, 0x6d, 0x46, 0x43, 0xc5, 0x9f, 0xbd, 0x0a, 0x9a, 0xd1, 0xe5, 0x5c, 0x43, 0x98, 0xdd, 0xae, 0xf1, 0xca, 0xc2, 0xd7, 0xfb, 0xcf, 0xd5, 0xe0, 0x11, 0xb6, 0x83, 0xb8, 0x33}, + }, + { // height 1, index 1 + Key: append([]byte("/test_child/node/"), []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}...), + Value: []byte{0xad, 0x11, 0xc8, 0xa7, 0xf6, 0x8a, 0x01, 0xe4, 0xdb, 0x88, 0x98, 0x0b, 0x4c, 0x4f, 0xa8, 0xe6, 0x94, 0xea, 0x85, 0xff, 0x47, 0x98, 0x64, 0xec, 0x96, 0x60, 0x13, 0x24, 0xae, 0xf6, 0x9a, 0x67}, + }, + { // height 2, index 0 + Key: append([]byte("/test_child/node/"), []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}...), + Value: []byte{0xff, 0xd4, 0x7a, 0x71, 0xf6, 0x3a, 0x8a, 0x50, 0x09, 0x56, 0xef, 0x34, 0xb1, 0xfa, 0xbb, 0xd4, 0x2f, 0x07, 0xc8, 0x5e, 0x77, 0xf7, 0xad, 0x21, 0x27, 0x01, 0xe0, 0x64, 0xda, 0xbd, 0xf6, 0xa3}, + }, + }, + err: false, + panic: false, + }, + { + name: "passed finalizing block height", + blockHeight: 10, + latestHeight: 10, + blockHeader: cmtproto.Header{ + Time: time.Unix(0, 10100).UTC(), + }, + lastWorkingTree: merkletypes.TreeInfo{ + Version: 9, + Index: 3, + LeafCount: 3, + StartLeafIndex: 10, + LastSiblings: map[uint8][]byte{}, + Done: false, + }, + finalizingBlockHeight: 5, + + expected: nil, + expectedStage: nil, + err: false, + panic: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + basedb, err := db.NewMemDB() + require.NoError(t, err) + + childdb := basedb.WithPrefix([]byte("test_child")) + childNode := node.NewTestNode(nodetypes.NodeConfig{}, childdb, nil, nil, nil, nil) + + mk, err := merkle.NewMerkle(ophosttypes.GenerateNodeHash) + require.NoError(t, err) + err = mk.PrepareWorkingTree(tc.lastWorkingTree) + require.NoError(t, err) + + stage := childdb.NewStage().(db.Stage) + ch := Child{ + BaseChild: childprovider.NewTestBaseChild(0, childNode, mk, bridgeInfo, nil, nodetypes.NodeConfig{}), + stage: stage, + eventQueue: make([]challengertypes.ChallengeEvent, 0), + + finalizingBlockHeight: tc.finalizingBlockHeight, + lastOutputTime: tc.lastOutputTime, + nextOutputTime: tc.nextOutputTime, + } + + ctx := types.NewContext(context.Background(), zap.NewNop(), "") + if tc.panic { + require.Panics(t, func() { + ch.handleTree(ctx, tc.blockHeight, tc.blockHeader) //nolint + }) + return + } + + storageRoot, err := ch.handleTree(ctx, tc.blockHeight, tc.blockHeader) + if !tc.err { + require.NoError(t, err) + + if tc.expected != nil { + expectedStorageRoot, expectedLastOutputTime, expectedNextOutputTime, expectedFinalizingBlockHeight := tc.expected() + require.Equal(t, expectedStorageRoot, storageRoot) + require.Equal(t, expectedLastOutputTime, ch.lastOutputTime) + require.Equal(t, expectedNextOutputTime, ch.nextOutputTime) + require.Equal(t, expectedFinalizingBlockHeight, ch.finalizingBlockHeight) + } + + if tc.expectedStage != nil { + allkvs := stage.All() + for _, kv := range tc.expectedStage { + require.Equal(t, kv.Value, allkvs[string(kv.Key)]) + } + } + } else { + require.Error(t, err) + } + }) + } +} + +func TestHandleOutput(t *testing.T) { + cases := []struct { + name string + blockTime time.Time + blockHeight int64 + version uint8 + blockId []byte + outputIndex uint64 + storageRoot []byte + bridgeInfo ophosttypes.QueryBridgeResponse + host *mockHost + expected []challengertypes.ChallengeEvent + err bool + }{ + { + name: "success", + blockTime: time.Unix(0, 100).UTC(), + blockHeight: 10, + version: 1, + blockId: []byte("latestBlockHashlatestBlockHashla"), + outputIndex: 1, + storageRoot: []byte("storageRootstorageRootstorageRoo"), + bridgeInfo: ophosttypes.QueryBridgeResponse{BridgeId: 1}, + host: NewMockHost(nil, 5), + expected: []challengertypes.ChallengeEvent{ + &challengertypes.Output{ + EventType: "Output", + L2BlockNumber: 10, + OutputIndex: 1, + OutputRoot: []byte{0xc7, 0x4e, 0xaa, 0x00, 0xbb, 0xc8, 0x16, 0xd2, 0x94, 0x39, 0x01, 0x4c, 0xf7, 0x36, 0x3e, 0x29, 0xb1, 0x85, 0x18, 0x8c, 0xd4, 0x6a, 0x38, 0xfd, 0x64, 0x1f, 0xe5, 0x9f, 0xe4, 0x00, 0xbc, 0xf2}, + Time: time.Unix(0, 100).UTC(), + Timeout: false, + }, + }, + err: false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + basedb, err := db.NewMemDB() + require.NoError(t, err) + + childdb := basedb.WithPrefix([]byte("test_child")) + childNode := node.NewTestNode(nodetypes.NodeConfig{}, childdb, nil, nil, nil, nil) + + ch := Child{ + BaseChild: childprovider.NewTestBaseChild(0, childNode, nil, tc.bridgeInfo, nil, nodetypes.NodeConfig{}), + host: tc.host, + eventQueue: make([]challengertypes.ChallengeEvent, 0), + } + + err = ch.handleOutput(tc.blockTime, tc.blockHeight, tc.version, tc.blockId, tc.outputIndex, tc.storageRoot) + if !tc.err { + require.NoError(t, err) + require.Equal(t, ch.eventQueue, tc.expected) + } else { + require.Error(t, err) + } + }) + } +} diff --git a/challenger/host/common_test.go b/challenger/host/common_test.go index 58a1f18..e547a6a 100644 --- a/challenger/host/common_test.go +++ b/challenger/host/common_test.go @@ -64,3 +64,5 @@ func (m *mockChallenger) DB() types.DB { func (m *mockChallenger) SendPendingChallenges(challenges []challengertypes.Challenge) { m.pendingChallenges = append(m.pendingChallenges, challenges...) } + +var _ challenger = (*mockChallenger)(nil) diff --git a/executor/child/deposit_test.go b/executor/child/deposit_test.go index e804abe..e4bf4d4 100644 --- a/executor/child/deposit_test.go +++ b/executor/child/deposit_test.go @@ -5,8 +5,6 @@ import ( "strconv" "testing" - abcitypes "github.com/cometbft/cometbft/abci/types" - opchildtypes "github.com/initia-labs/OPinit/x/opchild/types" ophosttypes "github.com/initia-labs/OPinit/x/ophost/types" "github.com/initia-labs/opinit-bots/db" "github.com/initia-labs/opinit-bots/node" @@ -20,47 +18,6 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -func FinalizeDepositEvents( - l1Sequence uint64, - sender string, - recipient string, - denom string, - baseDenom string, - amount sdk.Coin, - finalizeHeight uint64, -) []abcitypes.EventAttribute { - return []abcitypes.EventAttribute{ - { - Key: opchildtypes.AttributeKeyL1Sequence, - Value: strconv.FormatUint(l1Sequence, 10), - }, - { - Key: opchildtypes.AttributeKeySender, - Value: sender, - }, - { - Key: opchildtypes.AttributeKeyRecipient, - Value: recipient, - }, - { - Key: opchildtypes.AttributeKeyDenom, - Value: denom, - }, - { - Key: opchildtypes.AttributeKeyBaseDenom, - Value: baseDenom, - }, - { - Key: opchildtypes.AttributeKeyAmount, - Value: amount.Amount.String(), - }, - { - Key: opchildtypes.AttributeKeyFinalizeHeight, - Value: strconv.FormatUint(finalizeHeight, 10), - }, - } -} - func TestFinalizeDepositHandler(t *testing.T) { db, err := db.NewMemDB() require.NoError(t, err) @@ -74,8 +31,6 @@ func TestFinalizeDepositHandler(t *testing.T) { BaseChild: childprovider.NewTestBaseChild(0, childNode, nil, bridgeInfo, nil, nodetypes.NodeConfig{}), } - fullAttributes := FinalizeDepositEvents(1, "sender", "recipient", "denom", "baseDenom", sdk.NewInt64Coin("uinit", 10000), 2) - cases := []struct { name string eventHandlerArgs nodetypes.EventHandlerArgs @@ -85,7 +40,7 @@ func TestFinalizeDepositHandler(t *testing.T) { { name: "success", eventHandlerArgs: nodetypes.EventHandlerArgs{ - EventAttributes: FinalizeDepositEvents(1, "sender", "recipient", "denom", "baseDenom", sdk.NewInt64Coin("uinit", 10000), 2), + EventAttributes: childprovider.FinalizeDepositEvents(1, "sender", "recipient", "denom", "baseDenom", sdk.NewInt64Coin("denom", 10000), 2), }, expected: func() (msg string, fields []zapcore.Field) { msg = "finalize token deposit" @@ -101,62 +56,6 @@ func TestFinalizeDepositHandler(t *testing.T) { }, err: false, }, - { - name: "missing event attribute l1 sequence", - eventHandlerArgs: nodetypes.EventHandlerArgs{ - EventAttributes: fullAttributes[1:], - }, - expected: nil, - err: true, - }, - { - name: "missing event attribute sender", - eventHandlerArgs: nodetypes.EventHandlerArgs{ - EventAttributes: append(fullAttributes[:1], fullAttributes[2:]...), - }, - expected: nil, - err: true, - }, - { - name: "missing event attribute recipient", - eventHandlerArgs: nodetypes.EventHandlerArgs{ - EventAttributes: append(fullAttributes[:2], fullAttributes[3:]...), - }, - expected: nil, - err: true, - }, - { - name: "missing event attribute l1 denom", - eventHandlerArgs: nodetypes.EventHandlerArgs{ - EventAttributes: append(fullAttributes[:3], fullAttributes[4:]...), - }, - expected: nil, - err: true, - }, - { - name: "missing event attribute l2 denom", - eventHandlerArgs: nodetypes.EventHandlerArgs{ - EventAttributes: append(fullAttributes[:4], fullAttributes[5:]...), - }, - expected: nil, - err: true, - }, - { - name: "missing event attribute amount", - eventHandlerArgs: nodetypes.EventHandlerArgs{ - EventAttributes: append(fullAttributes[:5], fullAttributes[6:]...), - }, - expected: nil, - err: true, - }, - { - name: "missing event attribute finalize height", - eventHandlerArgs: nodetypes.EventHandlerArgs{ - EventAttributes: fullAttributes[:6], - }, - expected: nil, - err: true, - }, } for _, tc := range cases { diff --git a/executor/child/oracle_test.go b/executor/child/oracle_test.go index 51b313c..0f17894 100644 --- a/executor/child/oracle_test.go +++ b/executor/child/oracle_test.go @@ -5,8 +5,6 @@ import ( "strconv" "testing" - abcitypes "github.com/cometbft/cometbft/abci/types" - opchildtypes "github.com/initia-labs/OPinit/x/opchild/types" ophosttypes "github.com/initia-labs/OPinit/x/ophost/types" "github.com/initia-labs/opinit-bots/db" "github.com/initia-labs/opinit-bots/node" @@ -18,22 +16,6 @@ import ( "go.uber.org/zap/zapcore" ) -func UpdateOracleEvents( - l1BlockHeight uint64, - from string, -) []abcitypes.EventAttribute { - return []abcitypes.EventAttribute{ - { - Key: opchildtypes.AttributeKeyHeight, - Value: strconv.FormatUint(l1BlockHeight, 10), - }, - { - Key: opchildtypes.AttributeKeyFrom, - Value: from, - }, - } -} - func TestUpdateOracleHandler(t *testing.T) { db, err := db.NewMemDB() require.NoError(t, err) @@ -47,8 +29,6 @@ func TestUpdateOracleHandler(t *testing.T) { BaseChild: childprovider.NewTestBaseChild(0, childNode, nil, bridgeInfo, nil, nodetypes.NodeConfig{}), } - fullAttributes := UpdateOracleEvents(1, "sender") - cases := []struct { name string eventHandlerArgs nodetypes.EventHandlerArgs @@ -58,7 +38,7 @@ func TestUpdateOracleHandler(t *testing.T) { { name: "success", eventHandlerArgs: nodetypes.EventHandlerArgs{ - EventAttributes: UpdateOracleEvents(1, "sender"), + EventAttributes: childprovider.UpdateOracleEvents(1, "sender"), }, expected: func() (msg string, fields []zapcore.Field) { msg = "update oracle" @@ -70,22 +50,6 @@ func TestUpdateOracleHandler(t *testing.T) { }, err: false, }, - { - name: "missing event attribute l1 block height", - eventHandlerArgs: nodetypes.EventHandlerArgs{ - EventAttributes: fullAttributes[1:], - }, - expected: nil, - err: true, - }, - { - name: "missing event attribute from", - eventHandlerArgs: nodetypes.EventHandlerArgs{ - EventAttributes: fullAttributes[:1], - }, - expected: nil, - err: true, - }, } for _, tc := range cases { diff --git a/executor/child/withdraw.go b/executor/child/withdraw.go index d750ddf..f0639c4 100644 --- a/executor/child/withdraw.go +++ b/executor/child/withdraw.go @@ -22,7 +22,7 @@ import ( ) func (ch *Child) initiateWithdrawalHandler(ctx types.Context, args nodetypes.EventHandlerArgs) error { - l2Sequence, amount, from, to, baseDenom, err := childprovider.ParseInitiateWithdrawal(args.EventAttributes) + l2Sequence, amount, from, to, _, baseDenom, err := childprovider.ParseInitiateWithdrawal(args.EventAttributes) if err != nil { return errors.Wrap(err, "failed to parse initiate withdrawal event") } diff --git a/executor/child/withdraw_test.go b/executor/child/withdraw_test.go index dffb6c8..26f3e2a 100644 --- a/executor/child/withdraw_test.go +++ b/executor/child/withdraw_test.go @@ -2,13 +2,10 @@ package child import ( "context" - "strconv" "testing" "time" - abcitypes "github.com/cometbft/cometbft/abci/types" cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" - opchildtypes "github.com/initia-labs/OPinit/x/opchild/types" ophosttypes "github.com/initia-labs/OPinit/x/ophost/types" "github.com/initia-labs/opinit-bots/db" "github.com/initia-labs/opinit-bots/merkle" @@ -24,49 +21,11 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -func InitiateWithdrawalEvents( - from string, - to string, - denom string, - baseDenom string, - amount sdk.Coin, - l2Sequence uint64, -) []abcitypes.EventAttribute { - return []abcitypes.EventAttribute{ - { - Key: opchildtypes.AttributeKeyFrom, - Value: from, - }, - { - Key: opchildtypes.AttributeKeyTo, - Value: to, - }, - { - Key: opchildtypes.AttributeKeyDenom, - Value: denom, - }, - { - Key: opchildtypes.AttributeKeyBaseDenom, - Value: baseDenom, - }, - { - Key: opchildtypes.AttributeKeyAmount, - Value: amount.Amount.String(), - }, - { - Key: opchildtypes.AttributeKeyL2Sequence, - Value: strconv.FormatUint(l2Sequence, 10), - }, - } -} - func TestInitiateWithdrawalHandler(t *testing.T) { bridgeInfo := ophosttypes.QueryBridgeResponse{ BridgeId: 1, } - fullAttributes := InitiateWithdrawalEvents("from", "to", "denom", "uinit", sdk.NewInt64Coin("uinit", 10000), 1) - cases := []struct { name string lastWorkingTree merkletypes.TreeInfo @@ -90,7 +49,7 @@ func TestInitiateWithdrawalHandler(t *testing.T) { BlockHeight: 11, BlockTime: time.Unix(0, 10000).UTC(), Tx: []byte("txbytes"), // EA58654919E6F3E08370DE723D8DA223F1DFE78DD28D0A23E6F18BFA0815BB99 - EventAttributes: InitiateWithdrawalEvents("from", "to", "denom", "uinit", sdk.NewInt64Coin("uinit", 10000), 1), + EventAttributes: childprovider.InitiateWithdrawalEvents("from", "to", "denom", "uinit", sdk.NewInt64Coin("denom", 10000), 1), }, expectedStage: []types.KV{ { @@ -139,7 +98,7 @@ func TestInitiateWithdrawalHandler(t *testing.T) { BlockHeight: 11, BlockTime: time.Unix(0, 10000).UTC(), Tx: []byte("txbytes"), - EventAttributes: InitiateWithdrawalEvents("from", "to", "denom", "uinit", sdk.NewInt64Coin("uinit", 10000), 101), + EventAttributes: childprovider.InitiateWithdrawalEvents("from", "to", "denom", "uinit", sdk.NewInt64Coin("denom", 10000), 101), }, expectedStage: []types.KV{ { @@ -190,55 +149,13 @@ func TestInitiateWithdrawalHandler(t *testing.T) { BlockHeight: 10, BlockTime: time.Unix(0, 10000).UTC(), Tx: []byte("txbytes"), - EventAttributes: InitiateWithdrawalEvents("from", "to", "denom", "uinit", sdk.NewInt64Coin("uinit", 10000), 101), + EventAttributes: childprovider.InitiateWithdrawalEvents("from", "to", "denom", "uinit", sdk.NewInt64Coin("denom", 10000), 101), }, expectedStage: nil, expectedLog: nil, err: false, panic: true, }, - { - name: "missing event attribute from", - eventHandlerArgs: nodetypes.EventHandlerArgs{ - EventAttributes: fullAttributes[1:], - }, - err: true, - }, - { - name: "missing event attribute to", - eventHandlerArgs: nodetypes.EventHandlerArgs{ - EventAttributes: append(fullAttributes[:1], fullAttributes[2:]...), - }, - err: true, - }, - { - name: "missing event attribute denom", - eventHandlerArgs: nodetypes.EventHandlerArgs{ - EventAttributes: append(fullAttributes[:2], fullAttributes[3:]...), - }, - err: true, - }, - { - name: "missing event attribute base denom", - eventHandlerArgs: nodetypes.EventHandlerArgs{ - EventAttributes: append(fullAttributes[:3], fullAttributes[4:]...), - }, - err: true, - }, - { - name: "missing event attribute amount", - eventHandlerArgs: nodetypes.EventHandlerArgs{ - EventAttributes: append(fullAttributes[:4], fullAttributes[5:]...), - }, - err: true, - }, - { - name: "missing event attribute l2 sequence", - eventHandlerArgs: nodetypes.EventHandlerArgs{ - EventAttributes: fullAttributes[:5], - }, - err: true, - }, } for _, tc := range cases { diff --git a/provider/child/parse.go b/provider/child/parse.go index f295d31..11dbdd3 100644 --- a/provider/child/parse.go +++ b/provider/child/parse.go @@ -24,7 +24,7 @@ func missingAttrsError(missingAttrs map[string]struct{}) error { } func ParseFinalizeDeposit(eventAttrs []abcitypes.EventAttribute) ( - l1BlockHeight int64, + finalizeHeight int64, l1Sequence uint64, from, to, baseDenom string, amount sdk.Coin, @@ -63,7 +63,7 @@ func ParseFinalizeDeposit(eventAttrs []abcitypes.EventAttribute) ( } amount.Amount = coinAmount case opchildtypes.AttributeKeyFinalizeHeight: - l1BlockHeight, err = strconv.ParseInt(attr.Value, 10, 64) + finalizeHeight, err = strconv.ParseInt(attr.Value, 10, 64) if err != nil { err = errors.Wrap(err, "failed to parse l1 block height") return @@ -106,12 +106,13 @@ func ParseUpdateOracle(eventAttrs []abcitypes.EventAttribute) ( func ParseInitiateWithdrawal(eventAttrs []abcitypes.EventAttribute) ( l2Sequence, amount uint64, - from, to, baseDenom string, + from, to, denom, baseDenom string, err error) { missingAttrs := map[string]struct{}{ opchildtypes.AttributeKeyL2Sequence: {}, opchildtypes.AttributeKeyFrom: {}, opchildtypes.AttributeKeyTo: {}, + opchildtypes.AttributeKeyDenom: {}, opchildtypes.AttributeKeyBaseDenom: {}, opchildtypes.AttributeKeyAmount: {}, } @@ -127,6 +128,8 @@ func ParseInitiateWithdrawal(eventAttrs []abcitypes.EventAttribute) ( from = attr.Value case opchildtypes.AttributeKeyTo: to = attr.Value + case opchildtypes.AttributeKeyDenom: + denom = attr.Value case opchildtypes.AttributeKeyBaseDenom: baseDenom = attr.Value case opchildtypes.AttributeKeyAmount: diff --git a/provider/child/parse_test.go b/provider/child/parse_test.go new file mode 100644 index 0000000..ba9d3fb --- /dev/null +++ b/provider/child/parse_test.go @@ -0,0 +1,204 @@ +package child + +import ( + "slices" + "testing" + + abcitypes "github.com/cometbft/cometbft/abci/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +func TestParseDepositEvents(t *testing.T) { + fullAttributes := FinalizeDepositEvents(1, "sender", "recipient", "uinit", "baseDenom", sdk.NewInt64Coin("uinit", 10000), 2) + cases := []struct { + name string + eventAttrs []abcitypes.EventAttribute + l1Sequence uint64 + from string + to string + baseDenom string + amount sdk.Coin + finalizeHeight int64 + err bool + }{ + { + name: "success", + eventAttrs: fullAttributes, + l1Sequence: 1, + from: "sender", + to: "recipient", + baseDenom: "baseDenom", + amount: sdk.NewInt64Coin("uinit", 10000), + finalizeHeight: 2, + err: false, + }, + { + name: "missing event attribute l1 sequence", + eventAttrs: fullAttributes[1:], + err: true, + }, + { + name: "missing event attribute sender", + eventAttrs: append(slices.Clone(fullAttributes[:1]), fullAttributes[2:]...), + err: true, + }, + { + name: "missing event attribute recipient", + eventAttrs: append(slices.Clone(fullAttributes[:2]), fullAttributes[3:]...), + err: true, + }, + { + name: "missing event attribute l1 denom", + eventAttrs: append(slices.Clone(fullAttributes[:3]), fullAttributes[4:]...), + err: true, + }, + { + name: "missing event attribute l2 denom", + eventAttrs: append(slices.Clone(fullAttributes[:4]), fullAttributes[5:]...), + err: true, + }, + { + name: "missing event attribute amount", + eventAttrs: append(slices.Clone(fullAttributes[:5]), fullAttributes[6:]...), + err: true, + }, + { + name: "missing event attribute finalize height", + eventAttrs: fullAttributes[:6], + err: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + l1BlockHeight, l1Sequence, from, to, baseDenom, amount, err := ParseFinalizeDeposit(tc.eventAttrs) + if tc.err { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tc.l1Sequence, l1Sequence) + require.Equal(t, tc.from, from) + require.Equal(t, tc.to, to) + require.Equal(t, tc.baseDenom, baseDenom) + require.Equal(t, tc.amount, amount) + require.Equal(t, tc.finalizeHeight, l1BlockHeight) + } + }) + } +} + +func TestParseUpdateOracle(t *testing.T) { + fullAttributes := UpdateOracleEvents(1, "sender") + cases := []struct { + name string + eventAttrs []abcitypes.EventAttribute + l1BlockHeight int64 + from string + err bool + }{ + { + name: "success", + eventAttrs: fullAttributes, + l1BlockHeight: 1, + from: "sender", + err: false, + }, + { + name: "missing event attribute l1 block height", + eventAttrs: fullAttributes[1:], + err: true, + }, + { + name: "missing event attribute from", + eventAttrs: fullAttributes[:1], + err: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + l1BlockHeight, from, err := ParseUpdateOracle(tc.eventAttrs) + if tc.err { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tc.l1BlockHeight, l1BlockHeight) + require.Equal(t, tc.from, from) + } + }) + } +} + +func TestParseInitiateWithdrawal(t *testing.T) { + fullAttributes := InitiateWithdrawalEvents("from", "to", "denom", "uinit", sdk.NewInt64Coin("denom", 10000), 1) + + cases := []struct { + name string + eventAttrs []abcitypes.EventAttribute + l2Sequence uint64 + amount sdk.Coin + from string + to string + baseDenom string + err bool + }{ + { + name: "success", + eventAttrs: fullAttributes, + l2Sequence: 1, + amount: sdk.NewInt64Coin("denom", 10000), + from: "from", + to: "to", + baseDenom: "uinit", + err: false, + }, + { + name: "missing event attribute from", + eventAttrs: fullAttributes[1:], + err: true, + }, + { + name: "missing event attribute to", + eventAttrs: append(slices.Clone(fullAttributes[:1]), fullAttributes[2:]...), + err: true, + }, + { + name: "missing event attribute denom", + eventAttrs: append(slices.Clone(fullAttributes[:2]), fullAttributes[3:]...), + err: true, + }, + { + name: "missing event attribute base denom", + eventAttrs: append(slices.Clone(fullAttributes[:3]), fullAttributes[4:]...), + err: true, + }, + { + name: "missing event attribute amount", + eventAttrs: append(slices.Clone(fullAttributes[:4]), fullAttributes[5:]...), + err: true, + }, + { + name: "missing event attribute l2 sequence", + eventAttrs: fullAttributes[:5], + err: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + l2Sequence, amount, from, to, denom, baseDenom, err := ParseInitiateWithdrawal(tc.eventAttrs) + if tc.err { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tc.l2Sequence, l2Sequence) + require.Equal(t, tc.amount.Amount.Uint64(), amount) + require.Equal(t, tc.from, from) + require.Equal(t, tc.to, to) + require.Equal(t, tc.amount.Denom, denom) + require.Equal(t, tc.baseDenom, baseDenom) + } + }) + } +} diff --git a/provider/child/testutil.go b/provider/child/testutil.go index c0a4382..c0e0010 100644 --- a/provider/child/testutil.go +++ b/provider/child/testutil.go @@ -1,8 +1,12 @@ package child import ( + "strconv" + + abcitypes "github.com/cometbft/cometbft/abci/types" sdk "github.com/cosmos/cosmos-sdk/types" + opchildtypes "github.com/initia-labs/OPinit/x/opchild/types" ophosttypes "github.com/initia-labs/OPinit/x/ophost/types" "github.com/initia-labs/opinit-bots/merkle" @@ -39,3 +43,96 @@ func NewTestBaseChild( msgQueue: make(map[string][]sdk.Msg), } } + +func FinalizeDepositEvents( + l1Sequence uint64, + sender string, + recipient string, + denom string, + baseDenom string, + amount sdk.Coin, + finalizeHeight uint64, +) []abcitypes.EventAttribute { + return []abcitypes.EventAttribute{ + { + Key: opchildtypes.AttributeKeyL1Sequence, + Value: strconv.FormatUint(l1Sequence, 10), + }, + { + Key: opchildtypes.AttributeKeySender, + Value: sender, + }, + { + Key: opchildtypes.AttributeKeyRecipient, + Value: recipient, + }, + { + Key: opchildtypes.AttributeKeyDenom, + Value: denom, + }, + { + Key: opchildtypes.AttributeKeyBaseDenom, + Value: baseDenom, + }, + { + Key: opchildtypes.AttributeKeyAmount, + Value: amount.Amount.String(), + }, + { + Key: opchildtypes.AttributeKeyFinalizeHeight, + Value: strconv.FormatUint(finalizeHeight, 10), + }, + } +} + +func UpdateOracleEvents( + l1BlockHeight uint64, + from string, +) []abcitypes.EventAttribute { + return []abcitypes.EventAttribute{ + { + Key: opchildtypes.AttributeKeyHeight, + Value: strconv.FormatUint(l1BlockHeight, 10), + }, + { + Key: opchildtypes.AttributeKeyFrom, + Value: from, + }, + } +} + +func InitiateWithdrawalEvents( + from string, + to string, + denom string, + baseDenom string, + amount sdk.Coin, + l2Sequence uint64, +) []abcitypes.EventAttribute { + return []abcitypes.EventAttribute{ + { + Key: opchildtypes.AttributeKeyFrom, + Value: from, + }, + { + Key: opchildtypes.AttributeKeyTo, + Value: to, + }, + { + Key: opchildtypes.AttributeKeyDenom, + Value: denom, + }, + { + Key: opchildtypes.AttributeKeyBaseDenom, + Value: baseDenom, + }, + { + Key: opchildtypes.AttributeKeyAmount, + Value: amount.Amount.String(), + }, + { + Key: opchildtypes.AttributeKeyL2Sequence, + Value: strconv.FormatUint(l2Sequence, 10), + }, + } +}