diff --git a/Makefile b/Makefile index 1901870..f047a3d 100644 --- a/Makefile +++ b/Makefile @@ -22,8 +22,8 @@ build_tags = netgo build_tags += $(BUILD_TAGS) build_tags := $(strip $(build_tags)) -ldflags = -X github.com/initia-labs/opinit-bots-go/version.Version=$(VERSION) \ - -X github.com/initia-labs/opinit-bots-go/version.GitCommit=$(COMMIT) +ldflags = -X github.com/initia-labs/opinit-bots/version.Version=$(VERSION) \ + -X github.com/initia-labs/opinit-bots/version.GitCommit=$(COMMIT) ldflags += $(LDFLAGS) ldflags := $(strip $(ldflags)) diff --git a/README.md b/README.md index e3dcbf8..12aa3f6 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ This repository contains the Go implementation of OPinit bots. ## Components - [Executor](./executor) +- [Challenger](./challenger) ## How to Use @@ -16,9 +17,9 @@ Before running OPinit bots, make sure you have the following prerequisites insta To ensure compatibility with the node version, check the following versions: -| L1 Node | MiniMove | MiniWasm | MiniEVM | -| ------- | -------- | -------- | ------- | -| v0.4.2 | v0.4.0 | v0.4.0 | v0.4.0 | +| L1 Node | MiniMove | MiniWasm | MiniEVM | +| ------- | -------- | -------- | ------- | +| v0.4.7 | v0.4.1 | v0.4.1 | - | ### Build and Configure @@ -37,6 +38,7 @@ Default config path is `~/.opinit/[bot-name].json` Supported bot names - `executor` +- `challenger` ### Register keys @@ -58,8 +60,12 @@ To start the bot, use the following command: opinitd start [bot-name] ``` -log level can be set by using `--log-level` flag. Default log level is `info`. - +Options +- `--log-level`: log level can be set. Default log level is `info`. +- `--polling-interval`: polling interval can be set. Default polling interval is `100ms`. +- `--config`: config file name can be set. Default config file name is `[bot-name].json`. +- `--home`: home dir can be set. Default home dir is `~/.opinit`. + ### Reset Bot DB To reset the bot database, use the following command: @@ -67,16 +73,3 @@ To reset the bot database, use the following command: ```bash opinitd reset-db [bot-name] ``` - -### Query status - -```bash -curl localhost:3000/status -``` - -### Query withdrawals - -```bash -curl localhost:3000/withdrawal/{sequence} | jq . > ./withdrawal-info.json -initiad tx ophost finalize-token-withdrawal ./withdrawal-info.json --gas= --gas-prices= --chain-id= --from= -``` diff --git a/bot/bot.go b/bot/bot.go index c160cd8..e91f963 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -6,15 +6,16 @@ import ( "fmt" "io" "os" - "path" "go.uber.org/zap" - bottypes "github.com/initia-labs/opinit-bots-go/bot/types" - "github.com/initia-labs/opinit-bots-go/db" - "github.com/initia-labs/opinit-bots-go/executor" - executortypes "github.com/initia-labs/opinit-bots-go/executor/types" - "github.com/initia-labs/opinit-bots-go/server" + bottypes "github.com/initia-labs/opinit-bots/bot/types" + "github.com/initia-labs/opinit-bots/challenger" + challengertypes "github.com/initia-labs/opinit-bots/challenger/types" + "github.com/initia-labs/opinit-bots/db" + "github.com/initia-labs/opinit-bots/executor" + executortypes "github.com/initia-labs/opinit-bots/executor/types" + "github.com/initia-labs/opinit-bots/server" ) func LoadJsonConfig(path string, config bottypes.Config) error { @@ -35,24 +36,34 @@ func LoadJsonConfig(path string, config bottypes.Config) error { return nil } -func NewBot(name bottypes.BotType, logger *zap.Logger, homePath string, configName string) (bottypes.Bot, error) { - switch name { +func NewBot(botType bottypes.BotType, logger *zap.Logger, homePath string, configPath string) (bottypes.Bot, error) { + err := botType.Validate() + if err != nil { + return nil, err + } + + db, err := db.NewDB(getDBPath(homePath, botType)) + if err != nil { + return nil, err + } + server := server.NewServer() + + switch botType { case bottypes.BotTypeExecutor: cfg := &executortypes.Config{} - - configPath := path.Join(homePath, configName) err := LoadJsonConfig(configPath, cfg) if err != nil { return nil, err } - db, err := db.NewDB(getDBPath(homePath, name)) + return executor.NewExecutor(cfg, db, server, logger.Named("executor"), homePath), nil + case bottypes.BotTypeChallenger: + cfg := &challengertypes.Config{} + err := LoadJsonConfig(configPath, cfg) if err != nil { return nil, err } - server := server.NewServer() - return executor.NewExecutor(cfg, db, server, logger.Named("executor"), homePath), nil + return challenger.NewChallenger(cfg, db, server, logger.Named("challenger"), homePath), nil } - return nil, errors.New("not providing bot name") } diff --git a/bot/types/const.go b/bot/types/const.go index c3c1319..38afc43 100644 --- a/bot/types/const.go +++ b/bot/types/const.go @@ -1,16 +1,29 @@ package types +import ( + "fmt" +) + type BotType string const ( - BotTypeExecutor BotType = "executor" + BotTypeExecutor BotType = "executor" + BotTypeChallenger BotType = "challenger" ) +func (b BotType) Validate() error { + if b != BotTypeExecutor && b != BotTypeChallenger { + return fmt.Errorf("invalid bot type: %s", b) + } + return nil +} + func BotTypeFromString(name string) BotType { switch name { case "executor": return BotTypeExecutor + case "challenger": + return BotTypeChallenger } - panic("unknown bot type") } diff --git a/challenger/README.md b/challenger/README.md new file mode 100644 index 0000000..c00c38c --- /dev/null +++ b/challenger/README.md @@ -0,0 +1,207 @@ +# Challenger + +The Challenger is responsible for +1. verifying that the `MsgInitiateTokenDeposit` event is properly relayed to `MsgFinalizeTokenDeposit`. +2. checking whether `MsgInitiateTokenDeposit` was relayed on time. +3. verifying that the `Oracle` data is properly relayed to `MsgUpdateOracle`. +4. checking whether `Oracle` was relayed on time. +5. verifying that the `OutputRoot` submitted with `MsgProposeOutput` is correct. +6. checking whether next `MsgProposeOutput` was submitted on time. + +## Config + +To configure the Executor, fill in the values in the `~/.opinit/executor.json` file. + +```json +{ + // Version is the version used to build output root. + "version": 1, + // ListenAddress is the address to listen for incoming requests. + "listen_address": "localhost:3001", + "l1_node": { + "chain_id": "testnet-l1-1", + "bech32_prefix": "init", + "rpc_address": "tcp://localhost:26657", + }, + "l2_node": { + "chain_id": "testnet-l2-1", + "bech32_prefix": "init", + "rpc_address": "tcp://localhost:27657", + }, + // L2StartHeight is the height to start the l2 node. If it is 0, it will start from the latest height. + // If the latest height stored in the db is not 0, this config is ignored. + // L2 starts from the last submitted output l2 block number + 1 before L2StartHeight. + // L1 starts from the block number of the output tx + 1 + "l2_start_height": 0, +} +``` + +### Start height config examples +If the latest height stored in the db is not 0, start height config is ignored. + +``` +Output tx 1 +- L1BlockNumber: 10 +- L2BlockNumber: 100 + +Output tx 2 +- L1BlockNumber: 20 +- L2BlockNumber: 200 + +InitializeTokenDeposit tx 1 +- Height: 5 +- L1Sequence: 1 + +InitializeTokenDeposit tx 2 +- Height: 15 +- L1Sequence: 2 + +FinalizedTokenDeposit tx 1 +- L1Sequence: 1 + +FinalizedTokenDeposit tx 2 +- L1Sequence: 2 +``` + +#### Config 1 +```json +{ + l2_start_height: 150, +} +``` +When Child's last l1 Sequence is `2`, +- L1 starts from the height 10 + 1 = 11 +- L2 starts from the height 100 + 1 = 101 + + +## Handler rules for the components of the Challenger +For registered events or tx handlers, work processed in a block is atomically saved as `pending events`. Therefore, if `pending events` with the `ChallengeEvent` interface cannot be processed due to an interrupt or error, it is guaranteed to be read from the DB and processed. When an event matching the pending event comes in and is processed, or when the block time exceeds the event's timeout, a `Challenge` is created and stored in the DB. +#### The challenger can check the generated `Challenges` and decide what action to take. + +## Deposit +When the `initiate_token_deposit` event is detected in l1, saves it as a `Deposit` challenge event and check if it is the same as the `MsgFinalizeTokenDeposit` for the same sequence. +```go +// Deposit is the challenge event for the deposit +type Deposit struct { + EventType string `json:"event_type"` + Sequence uint64 `json:"sequence"` + L1BlockHeight uint64 `json:"l1_block_height"` + From string `json:"from"` + To string `json:"to"` + L1Denom string `json:"l1_denom"` + Amount string `json:"amount"` + Time time.Time `json:"time"` + Timeout bool `json:"timeout"` +} +``` + +## Output +When the `propose_output` event is detected in l1, saves it as a `Output` challenge event, replays up to l2 block number and check if `OutputRoot` is the same as submitted. +```go +// Output is the challenge event for the output +type Output struct { + EventType string `json:"event_type"` + L2BlockNumber uint64 `json:"l2_block_number"` + OutputIndex uint64 `json:"output_index"` + OutputRoot []byte `json:"output_root"` + Time time.Time `json:"time"` + Timeout bool `json:"timeout"` +} +``` + +## Oracle +If `oracle_enable` is turned on in bridge config, saves bytes of the 0th Tx as a `Oracle` challenge event and check if it is the same data in the `MsgUpdateOracle` for the l1 height. + +## Batch +Batch data is not verified by the challenger bot. +#### TODO +* Challenger runs a L2 node it in rollup sync challenge mode in CometBFT to check whether the submitted batch is replayed properly. + +## Query + +### Status +```bash +curl localhost:3001/status +``` + +```json +{ + "bridge_id": 0, + "host": { + "node": { + "last_block_height": 0 + }, + "last_output_index": 0, + "last_output_time": "", + "num_pending_events": {} + }, + "child": { + "node": { + "last_block_height": 0 + }, + "last_updated_oracle_height": 0, + "last_finalized_deposit_l1_block_height": 0, + "last_finalized_deposit_l1_sequence": 0, + "last_withdrawal_l2_sequence": 0, + "working_tree_index": 0, + "finalizing_block_height": 0, + "last_output_submission_time": "", + "next_output_submission_time": "", + "num_pending_events": {} + }, + "latest_challenges": [] +} +``` + +### Challenges +```bash +curl localhost:3001/challenges/{page} +``` + +```json +[ + { + "event_type": "Oracle", + "id": { + "type": 2, + "id": 136 + }, + "log": "event timeout: Oracle{L1Height: 136, Data: nUYSP4e3jTUBk33jFSYuWW28U+uTO+g3IO5/iZfbDTo, Time: 2024-09-11 06:01:21.257012 +0000 UTC}", + "timestamp": "2024-09-11T06:05:21.284479Z" + }, +] +``` + +### Pending events +```bash +curl localhost:3001/pending_events/host +``` + +```json +[ + { + "event_type": "Output", + "l2_block_number": 2394, + "output_index": 7, + "output_root": "Lx+k0GuHi6jPcO5yA6dUwI/l0h4b25awy973F6CirYs=", + "time": "2024-09-11T06:28:54.186941Z", + "timeout": false + } +] +``` + +```bash +curl localhost:3001/pending_events/child +``` + +```json +[ + { + "event_type": "Oracle", + "l1_height": 1025, + "data": "49Y7iryZGsQYzpao+rxR2Vfaz6owp3s5vlPnkboXwr0=", + "time": "2024-09-11T06:31:28.657245Z", + "timeout": false + } +] +``` \ No newline at end of file diff --git a/challenger/challenger.go b/challenger/challenger.go new file mode 100644 index 0000000..ee55087 --- /dev/null +++ b/challenger/challenger.go @@ -0,0 +1,231 @@ +package challenger + +import ( + "context" + "strconv" + "sync" + + "github.com/pkg/errors" + + "github.com/gofiber/fiber/v2" + "github.com/initia-labs/opinit-bots/challenger/child" + "github.com/initia-labs/opinit-bots/challenger/host" + "github.com/initia-labs/opinit-bots/server" + + bottypes "github.com/initia-labs/opinit-bots/bot/types" + challengertypes "github.com/initia-labs/opinit-bots/challenger/types" + + "github.com/initia-labs/opinit-bots/types" + "go.uber.org/zap" +) + +var _ bottypes.Bot = &Challenger{} + +// Executor charges the execution of the bridge between the host and the child chain +// - relay l1 deposit messages to l2 +// - generate l2 output root and submit to l1 +type Challenger struct { + host *host.Host + child *child.Child + + cfg *challengertypes.Config + db types.DB + server *server.Server + logger *zap.Logger + + homePath string + + challengeCh chan challengertypes.Challenge + challengeChStopped chan struct{} + + pendingChallenges []challengertypes.Challenge + + // status info + latestChallengesMu *sync.Mutex + latestChallenges []challengertypes.Challenge +} + +func NewChallenger(cfg *challengertypes.Config, db types.DB, sv *server.Server, logger *zap.Logger, homePath string) *Challenger { + err := cfg.Validate() + if err != nil { + panic(err) + } + + challengeCh := make(chan challengertypes.Challenge) + return &Challenger{ + host: host.NewHostV1( + cfg.L1NodeConfig(homePath), + db.WithPrefix([]byte(types.HostName)), + logger.Named(types.HostName), cfg.L1Node.Bech32Prefix, + ), + child: child.NewChildV1( + cfg.L2NodeConfig(homePath), + db.WithPrefix([]byte(types.ChildName)), + logger.Named(types.ChildName), cfg.L2Node.Bech32Prefix, + ), + + cfg: cfg, + db: db, + server: sv, + logger: logger, + + homePath: homePath, + + challengeCh: challengeCh, + challengeChStopped: make(chan struct{}), + + pendingChallenges: make([]challengertypes.Challenge, 0), + + latestChallengesMu: &sync.Mutex{}, + latestChallenges: make([]challengertypes.Challenge, 0), + } +} + +func (c *Challenger) Initialize(ctx context.Context) error { + bridgeInfo, err := c.child.QueryBridgeInfo(ctx) + if err != nil { + return err + } + if bridgeInfo.BridgeId == 0 { + return errors.New("bridge info is not set") + } + + c.logger.Info( + "bridge info", + zap.Uint64("id", bridgeInfo.BridgeId), + zap.Duration("submission_interval", bridgeInfo.BridgeConfig.SubmissionInterval), + ) + + hostStartHeight, childStartHeight, startOutputIndex, err := c.getStartHeights(ctx, bridgeInfo.BridgeId) + if err != nil { + return err + } + + err = c.host.Initialize(ctx, hostStartHeight, c.child, bridgeInfo, c) + if err != nil { + return err + } + err = c.child.Initialize(ctx, childStartHeight, startOutputIndex, c.host, bridgeInfo, c) + if err != nil { + return err + } + c.RegisterQuerier() + + c.pendingChallenges, err = c.loadPendingChallenges() + if err != nil { + return err + } + + c.latestChallenges, err = c.loadChallenges() + if err != nil { + return err + } + + return nil +} + +func (c *Challenger) Start(ctx context.Context) error { + defer c.Close() + + errGrp := types.ErrGrp(ctx) + errGrp.Go(func() (err error) { + <-ctx.Done() + return c.server.Shutdown() + }) + + errGrp.Go(func() (err error) { + defer func() { + c.logger.Info("api server stopped") + }() + return c.server.Start(c.cfg.ListenAddress) + }) + + errGrp.Go(func() error { + for _, ch := range c.pendingChallenges { + c.challengeCh <- ch + } + return nil + }) + + errGrp.Go(func() (err error) { + defer func() { + c.logger.Info("challenge handler stopped") + }() + return c.challengeHandler(ctx) + }) + + c.host.Start(ctx) + c.child.Start(ctx) + return errGrp.Wait() +} + +func (c *Challenger) Close() { + c.db.Close() +} + +func (c *Challenger) RegisterQuerier() { + c.server.RegisterQuerier("/status", func(ctx *fiber.Ctx) error { + return ctx.JSON(c.GetStatus()) + }) + c.server.RegisterQuerier("/challenges/:page", func(ctx *fiber.Ctx) error { + pageStr := ctx.Params("page") + if pageStr == "" { + pageStr = "1" + } + page, err := strconv.ParseUint(pageStr, 10, 64) + if err != nil { + return err + } + res, err := c.QueryChallenges(page) + if err != nil { + return err + } + return ctx.JSON(res) + }) + + c.server.RegisterQuerier("/pending_events/host", func(ctx *fiber.Ctx) error { + return ctx.JSON(c.host.GetAllPendingEvents()) + }) + + c.server.RegisterQuerier("/pending_events/child", func(ctx *fiber.Ctx) error { + return ctx.JSON(c.child.GetAllPendingEvents()) + }) +} + +func (c *Challenger) getStartHeights(ctx context.Context, bridgeId uint64) (l1StartHeight uint64, l2StartHeight uint64, startOutputIndex uint64, err error) { + // get the bridge start height from the host + l1StartHeight, err = c.host.QueryCreateBridgeHeight(ctx, bridgeId) + if err != nil { + return 0, 0, 0, err + } + + // get the last submitted output height before the start height from the host + if c.cfg.L2StartHeight != 0 { + output, err := c.host.QueryLastFinalizedOutput(ctx, bridgeId) + if err != nil { + return 0, 0, 0, err + } else if output != nil { + l1StartHeight = output.OutputProposal.L1BlockNumber + l2StartHeight = output.OutputProposal.L2BlockNumber + startOutputIndex = output.OutputIndex + 1 + } + } + if l2StartHeight > 0 { + // get the last deposit tx height from the host + l1Sequence, err := c.child.QueryNextL1Sequence(ctx, l2StartHeight-1) + if err != nil { + return 0, 0, 0, err + } + depositTxHeight, err := c.host.QueryDepositTxHeight(ctx, bridgeId, l1Sequence-1) + if err != nil { + return 0, 0, 0, err + } + if l1StartHeight > depositTxHeight { + l1StartHeight = depositTxHeight + } + } + if l2StartHeight == 0 { + startOutputIndex = 1 + } + return l1StartHeight, l2StartHeight, startOutputIndex, err +} diff --git a/challenger/child/child.go b/challenger/child/child.go new file mode 100644 index 0000000..fd91241 --- /dev/null +++ b/challenger/child/child.go @@ -0,0 +1,89 @@ +package child + +import ( + "context" + "time" + + "go.uber.org/zap" + + opchildtypes "github.com/initia-labs/OPinit/x/opchild/types" + ophosttypes "github.com/initia-labs/OPinit/x/ophost/types" + + nodetypes "github.com/initia-labs/opinit-bots/node/types" + "github.com/initia-labs/opinit-bots/types" + + eventhandler "github.com/initia-labs/opinit-bots/challenger/eventhandler" + challengertypes "github.com/initia-labs/opinit-bots/challenger/types" + childprovider "github.com/initia-labs/opinit-bots/provider/child" +) + +type challenger interface { + PendingChallengeToRawKVs([]challengertypes.Challenge, bool) ([]types.RawKV, error) + SendPendingChallenges([]challengertypes.Challenge) +} + +type hostNode interface { + QuerySyncedOutput(context.Context, uint64, uint64) (*ophosttypes.QueryOutputProposalResponse, error) +} + +type Child struct { + *childprovider.BaseChild + + host hostNode + challenger challenger + eventHandler *eventhandler.ChallengeEventHandler + + eventQueue []challengertypes.ChallengeEvent + + finalizingBlockHeight uint64 + + // status info + lastUpdatedOracleL1Height uint64 + lastFinalizedDepositL1BlockHeight uint64 + lastFinalizedDepositL1Sequence uint64 + lastOutputTime time.Time + nextOutputTime time.Time +} + +func NewChildV1( + cfg nodetypes.NodeConfig, + db types.DB, logger *zap.Logger, bech32Prefix string, +) *Child { + return &Child{ + BaseChild: childprovider.NewBaseChildV1(cfg, db, logger, bech32Prefix), + eventHandler: eventhandler.NewChallengeEventHandler(db, logger), + eventQueue: make([]challengertypes.ChallengeEvent, 0), + } +} + +func (ch *Child) Initialize(ctx context.Context, startHeight uint64, startOutputIndex uint64, host hostNode, bridgeInfo opchildtypes.BridgeInfo, challenger challenger) error { + err := ch.BaseChild.Initialize(ctx, startHeight, startOutputIndex, bridgeInfo) + if err != nil { + return err + } + ch.host = host + ch.challenger = challenger + ch.registerHandlers() + + err = ch.eventHandler.Initialize(bridgeInfo.BridgeConfig.SubmissionInterval) + if err != nil { + return err + } + return nil +} + +func (ch *Child) registerHandlers() { + ch.Node().RegisterBeginBlockHandler(ch.beginBlockHandler) + ch.Node().RegisterTxHandler(ch.txHandler) + ch.Node().RegisterEventHandler(opchildtypes.EventTypeFinalizeTokenDeposit, ch.finalizeDepositHandler) + ch.Node().RegisterEventHandler(opchildtypes.EventTypeInitiateTokenWithdrawal, ch.initiateWithdrawalHandler) + ch.Node().RegisterEndBlockHandler(ch.endBlockHandler) +} + +func (ch *Child) PendingEventsToRawKV(events []challengertypes.ChallengeEvent, delete bool) ([]types.RawKV, error) { + return ch.eventHandler.PendingEventsToRawKV(events, delete) +} + +func (ch *Child) SetPendingEvents(events []challengertypes.ChallengeEvent) { + ch.eventHandler.SetPendingEvents(events) +} diff --git a/challenger/child/deposit.go b/challenger/child/deposit.go new file mode 100644 index 0000000..0e2e2a1 --- /dev/null +++ b/challenger/child/deposit.go @@ -0,0 +1,39 @@ +package child + +import ( + "context" + "time" + + childprovider "github.com/initia-labs/opinit-bots/provider/child" + "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" +) + +func (ch *Child) finalizeDepositHandler(_ context.Context, args nodetypes.EventHandlerArgs) error { + l1BlockHeight, l1Sequence, from, to, baseDenom, amount, err := childprovider.ParseFinalizeDeposit(args.EventAttributes) + if err != nil { + return err + } + ch.handleFinalizeDeposit(args.BlockTime, l1BlockHeight, l1Sequence, from, to, amount, baseDenom) + ch.lastFinalizedDepositL1BlockHeight = l1BlockHeight + ch.lastFinalizedDepositL1Sequence = l1Sequence + return nil +} + +func (ch *Child) handleFinalizeDeposit(l2BlockTime time.Time, l1BlockHeight uint64, l1Sequence uint64, from string, to string, amount sdk.Coin, baseDenom string) { + deposit := challengertypes.NewDeposit(l1Sequence, l1BlockHeight, from, to, baseDenom, amount.String(), l2BlockTime) + ch.eventQueue = append(ch.eventQueue, deposit) + + ch.Logger().Info("finalize token deposit", + zap.Uint64("l1_blockHeight", l1BlockHeight), + zap.Uint64("l1_sequence", l1Sequence), + zap.String("from", from), + zap.String("to", to), + zap.String("amount", amount.String()), + zap.String("base_denom", baseDenom), + ) +} diff --git a/challenger/child/handler.go b/challenger/child/handler.go new file mode 100644 index 0000000..aeb85f4 --- /dev/null +++ b/challenger/child/handler.go @@ -0,0 +1,114 @@ +package child + +import ( + "context" + + challengertypes "github.com/initia-labs/opinit-bots/challenger/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" + "github.com/initia-labs/opinit-bots/txutils" + "github.com/initia-labs/opinit-bots/types" + + opchildtypes "github.com/initia-labs/OPinit/x/opchild/types" +) + +func (ch *Child) beginBlockHandler(ctx context.Context, args nodetypes.BeginBlockArgs) (err error) { + blockHeight := uint64(args.Block.Header.Height) + ch.eventQueue = ch.eventQueue[:0] + + err = ch.prepareTree(blockHeight) + if err != nil { + return err + } + + err = ch.prepareOutput(ctx) + if err != nil { + return err + } + return nil +} + +func (ch *Child) endBlockHandler(_ context.Context, args nodetypes.EndBlockArgs) error { + blockHeight := uint64(args.Block.Header.Height) + batchKVs := make([]types.RawKV, 0) + pendingChallenges := make([]challengertypes.Challenge, 0) + + treeKVs, storageRoot, err := ch.handleTree(blockHeight, args.Block.Header) + if err != nil { + return err + } + + batchKVs = append(batchKVs, treeKVs...) + if storageRoot != nil { + err = ch.handleOutput(args.Block.Header.Time, blockHeight, ch.Version(), args.BlockID, ch.Merkle().GetWorkingTreeIndex(), storageRoot) + if err != nil { + return err + } + } + + // update the sync info + batchKVs = append(batchKVs, ch.Node().SyncInfoToRawKV(blockHeight)) + + // check value for pending events + challenges, processedEvents, err := ch.eventHandler.CheckValue(ch.eventQueue) + if err != nil { + return err + } + pendingChallenges = append(pendingChallenges, challenges...) + + // check timeout for unprocessed pending events + unprocessedEvents := ch.eventHandler.GetUnprocessedPendingEvents(processedEvents) + challenges, timeoutEvents := ch.eventHandler.CheckTimeout(args.Block.Header.Time, unprocessedEvents) + pendingChallenges = append(pendingChallenges, challenges...) + + // update timeout pending events + eventKvs, err := ch.PendingEventsToRawKV(timeoutEvents, false) + if err != nil { + return err + } + batchKVs = append(batchKVs, eventKvs...) + + // delete processed events + eventKVs, err := ch.PendingEventsToRawKV(processedEvents, true) + if err != nil { + return err + } + batchKVs = append(batchKVs, eventKVs...) + + challengesKVs, err := ch.challenger.PendingChallengeToRawKVs(pendingChallenges, false) + if err != nil { + return err + } + batchKVs = append(batchKVs, challengesKVs...) + + err = ch.DB().RawBatchSet(batchKVs...) + if err != nil { + return err + } + + ch.eventHandler.DeletePendingEvents(processedEvents) + ch.eventHandler.SetPendingEvents(timeoutEvents) + ch.challenger.SendPendingChallenges(challenges) + return nil +} + +func (ch *Child) txHandler(_ context.Context, args nodetypes.TxHandlerArgs) error { + txConfig := ch.Node().GetTxConfig() + + tx, err := txutils.DecodeTx(txConfig, args.Tx) + if err != nil { + // if tx is not oracle tx, tx parse error is expected + // ignore decoding error + return nil + } + msgs := tx.GetMsgs() + if len(msgs) > 1 { + // we only expect one message for oracle tx + return nil + } + msg, ok := msgs[0].(*opchildtypes.MsgUpdateOracle) + if !ok { + return nil + } + ch.oracleTxHandler(args.BlockTime, msg.Sender, msg.Height, msg.Data) + return nil +} diff --git a/challenger/child/oracle.go b/challenger/child/oracle.go new file mode 100644 index 0000000..d72476c --- /dev/null +++ b/challenger/child/oracle.go @@ -0,0 +1,21 @@ +package child + +import ( + "time" + + comettypes "github.com/cometbft/cometbft/types" + challengertypes "github.com/initia-labs/opinit-bots/challenger/types" + "go.uber.org/zap" +) + +func (ch *Child) oracleTxHandler(blockTime time.Time, sender string, l1BlockHeight uint64, oracleDataBytes comettypes.Tx) { + checksum := challengertypes.OracleChecksum(oracleDataBytes) + oracle := challengertypes.NewOracle(l1BlockHeight, checksum, blockTime) + ch.eventQueue = append(ch.eventQueue, oracle) + ch.lastUpdatedOracleL1Height = l1BlockHeight + + ch.Logger().Info("update oracle", + zap.Uint64("l1_blockHeight", l1BlockHeight), + zap.String("from", sender), + ) +} diff --git a/challenger/child/status.go b/challenger/child/status.go new file mode 100644 index 0000000..fe35a50 --- /dev/null +++ b/challenger/child/status.go @@ -0,0 +1,42 @@ +package child + +import ( + "time" + + challengertypes "github.com/initia-labs/opinit-bots/challenger/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" +) + +type Status struct { + Node nodetypes.Status `json:"node"` + LastUpdatedOracleL1Height uint64 `json:"last_updated_oracle_height"` + LastFinalizedDepositL1BlockHeight uint64 `json:"last_finalized_deposit_l1_block_height"` + LastFinalizedDepositL1Sequence uint64 `json:"last_finalized_deposit_l1_sequence"` + LastWithdrawalL2Sequence uint64 `json:"last_withdrawal_l2_sequence"` + WorkingTreeIndex uint64 `json:"working_tree_index"` + + FinalizingBlockHeight uint64 `json:"finalizing_block_height"` + LastOutputSubmissionTime time.Time `json:"last_output_submission_time"` + NextOutputSubmissionTime time.Time `json:"next_output_submission_time"` + + NumPendingEvents map[string]int64 `json:"num_pending_events"` +} + +func (ch Child) GetStatus() Status { + return Status{ + Node: ch.Node().GetStatus(), + LastUpdatedOracleL1Height: ch.lastUpdatedOracleL1Height, + LastFinalizedDepositL1BlockHeight: ch.lastFinalizedDepositL1BlockHeight, + LastFinalizedDepositL1Sequence: ch.lastFinalizedDepositL1Sequence, + LastWithdrawalL2Sequence: ch.Merkle().GetWorkingTreeLeafCount() + ch.Merkle().GetStartLeafIndex() - 1, + WorkingTreeIndex: ch.Merkle().GetWorkingTreeIndex(), + FinalizingBlockHeight: ch.finalizingBlockHeight, + LastOutputSubmissionTime: ch.lastOutputTime, + NextOutputSubmissionTime: ch.nextOutputTime, + NumPendingEvents: ch.eventHandler.NumPendingEvents(), + } +} + +func (ch Child) GetAllPendingEvents() []challengertypes.ChallengeEvent { + return ch.eventHandler.GetAllSortedPendingEvents() +} diff --git a/challenger/child/withdraw.go b/challenger/child/withdraw.go new file mode 100644 index 0000000..31e6421 --- /dev/null +++ b/challenger/child/withdraw.go @@ -0,0 +1,156 @@ +package child + +import ( + "context" + "encoding/base64" + "fmt" + "strconv" + "strings" + "time" + + "cosmossdk.io/math" + opchildtypes "github.com/initia-labs/OPinit/x/opchild/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/types" + "github.com/pkg/errors" + "go.uber.org/zap" + + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + dbtypes "github.com/initia-labs/opinit-bots/db/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" + childprovider "github.com/initia-labs/opinit-bots/provider/child" +) + +func (ch *Child) initiateWithdrawalHandler(_ context.Context, args nodetypes.EventHandlerArgs) error { + l2Sequence, amount, from, to, baseDenom, err := childprovider.ParseInitiateWithdrawal(args.EventAttributes) + if err != nil { + return err + } + + for _, attr := range args.EventAttributes { + switch attr.Key { + case opchildtypes.AttributeKeyL2Sequence: + l2Sequence, err = strconv.ParseUint(attr.Value, 10, 64) + if err != nil { + return err + } + case opchildtypes.AttributeKeyFrom: + from = attr.Value + case opchildtypes.AttributeKeyTo: + to = attr.Value + case opchildtypes.AttributeKeyBaseDenom: + baseDenom = attr.Value + case opchildtypes.AttributeKeyAmount: + coinAmount, ok := math.NewIntFromString(attr.Value) + if !ok { + return fmt.Errorf("invalid amount %s", attr.Value) + } + + amount = coinAmount.Uint64() + } + } + + return ch.handleInitiateWithdrawal(l2Sequence, from, to, baseDenom, amount) +} + +func (ch *Child) handleInitiateWithdrawal(l2Sequence uint64, from string, to string, baseDenom string, amount uint64) error { + withdrawalHash := ophosttypes.GenerateWithdrawalHash(ch.BridgeId(), l2Sequence, from, to, baseDenom, amount) + // generate merkle tree + err := ch.Merkle().InsertLeaf(withdrawalHash[:]) + if err != nil { + return err + } + + ch.Logger().Info("initiate token withdrawal", + zap.Uint64("l2_sequence", l2Sequence), + zap.String("from", from), + zap.String("to", to), + zap.Uint64("amount", amount), + zap.String("base_denom", baseDenom), + zap.String("withdrawal", base64.StdEncoding.EncodeToString(withdrawalHash[:])), + ) + return nil +} + +func (ch *Child) prepareTree(blockHeight uint64) error { + if ch.InitializeTree(blockHeight) { + return nil + } + + err := ch.Merkle().LoadWorkingTree(blockHeight - 1) + if err == dbtypes.ErrNotFound { + // must not happened + panic(fmt.Errorf("working tree not found at height: %d, current: %d", blockHeight-1, blockHeight)) + } else if err != nil { + return err + } + return nil +} + +func (ch *Child) prepareOutput(ctx context.Context) error { + workingOutputIndex := ch.Merkle().GetWorkingTreeIndex() + + // initialize next output time + if ch.nextOutputTime.IsZero() && workingOutputIndex > 1 { + output, err := ch.host.QuerySyncedOutput(ctx, ch.BridgeId(), workingOutputIndex-1) + if err != nil { + // TODO: maybe not return error here and roll back + return fmt.Errorf("output does not exist at index: %d", workingOutputIndex-1) + } + ch.lastOutputTime = output.OutputProposal.L1BlockTime + } + + output, err := ch.host.QuerySyncedOutput(ctx, ch.BridgeId(), ch.Merkle().GetWorkingTreeIndex()) + if err != nil { + if strings.Contains(err.Error(), "collections: not found") { + // should check the existing output. + return errors.Wrap(nodetypes.ErrIgnoreAndTryLater, fmt.Sprintf("output does not exist: %d", ch.Merkle().GetWorkingTreeIndex())) + } + return err + } else { + ch.nextOutputTime = output.OutputProposal.L1BlockTime + ch.finalizingBlockHeight = output.OutputProposal.L2BlockNumber + } + return nil +} + +func (ch *Child) handleTree(blockHeight uint64, blockHeader cmtproto.Header) (kvs []types.RawKV, storageRoot []byte, err error) { + // panic if we passed the finalizing block height + // this must not happened + if ch.finalizingBlockHeight != 0 && ch.finalizingBlockHeight < blockHeight { + panic(fmt.Errorf("INVARIANT failed; handleTree expect to finalize tree at block `%d` but we got block `%d`", blockHeight-1, blockHeight)) + } + + if ch.finalizingBlockHeight == blockHeight { + kvs, storageRoot, err = ch.Merkle().FinalizeWorkingTree(nil) + if err != nil { + return nil, nil, err + } + + ch.Logger().Info("finalize working tree", + zap.Uint64("tree_index", ch.Merkle().GetWorkingTreeIndex()), + zap.Uint64("height", blockHeight), + zap.Uint64("num_leaves", ch.Merkle().GetWorkingTreeLeafCount()), + zap.String("storage_root", base64.StdEncoding.EncodeToString(storageRoot)), + ) + + ch.finalizingBlockHeight = 0 + ch.lastOutputTime = blockHeader.Time + } + + err = ch.Merkle().SaveWorkingTree(blockHeight) + if err != nil { + return nil, nil, err + } + + return kvs, storageRoot, nil +} + +func (ch *Child) handleOutput(blockTime time.Time, blockHeight uint64, version uint8, blockId []byte, outputIndex uint64, storageRoot []byte) error { + outputRoot := ophosttypes.GenerateOutputRoot(version, storageRoot, blockId) + output := challengertypes.NewOutput(blockHeight, outputIndex, outputRoot[:], blockTime) + + ch.eventQueue = append(ch.eventQueue, output) + return nil +} diff --git a/challenger/db.go b/challenger/db.go new file mode 100644 index 0000000..d03f8c0 --- /dev/null +++ b/challenger/db.go @@ -0,0 +1,82 @@ +package challenger + +import ( + "slices" + + challengertypes "github.com/initia-labs/opinit-bots/challenger/types" + "github.com/initia-labs/opinit-bots/types" +) + +func (c *Challenger) PendingChallengeToRawKVs(challenges []challengertypes.Challenge, delete bool) ([]types.RawKV, error) { + kvs := make([]types.RawKV, 0, len(challenges)) + for _, challenge := range challenges { + var value []byte + var err error + + if !delete { + value, err = challenge.Marshal() + if err != nil { + return nil, err + } + } + kvs = append(kvs, types.RawKV{ + Key: c.db.PrefixedKey(challengertypes.PrefixedPendingChallenge(challenge.Id)), + Value: value, + }) + } + return kvs, nil +} + +func (c *Challenger) deletePendingChallenge(challenge challengertypes.Challenge) types.RawKV { + return types.RawKV{ + Key: c.db.PrefixedKey(challengertypes.PrefixedPendingChallenge(challenge.Id)), + Value: nil, + } +} + +func (c *Challenger) loadPendingChallenges() (challenges []challengertypes.Challenge, err error) { + iterErr := c.db.PrefixedIterate(challengertypes.PendingChallengeKey, func(_, value []byte) (stop bool, err error) { + challenge := challengertypes.Challenge{} + err = challenge.Unmarshal(value) + if err != nil { + return true, err + } + challenges = append(challenges, challenge) + return false, nil + }) + if iterErr != nil { + return nil, iterErr + } + return +} + +func (c *Challenger) saveChallenge(challenge challengertypes.Challenge) (types.RawKV, error) { + value, err := challenge.Marshal() + if err != nil { + return types.RawKV{}, err + } + return types.RawKV{ + Key: c.db.PrefixedKey(challengertypes.PrefixedChallenge(challenge.Time, challenge.Id)), + Value: value, + }, nil +} + +func (c *Challenger) loadChallenges() (challenges []challengertypes.Challenge, err error) { + iterErr := c.db.PrefixedReverseIterate(challengertypes.ChallengeKey, func(_, value []byte) (stop bool, err error) { + challenge := challengertypes.Challenge{} + err = challenge.Unmarshal(value) + if err != nil { + return true, err + } + challenges = append(challenges, challenge) + if len(challenges) >= 5 { + return true, nil + } + return false, nil + }) + if iterErr != nil { + return nil, iterErr + } + slices.Reverse(challenges) + return +} diff --git a/challenger/eventhandler/challenge.go b/challenger/eventhandler/challenge.go new file mode 100644 index 0000000..7ac5aaf --- /dev/null +++ b/challenger/eventhandler/challenge.go @@ -0,0 +1,73 @@ +package eventhandler + +import ( + "fmt" + "time" + + "cosmossdk.io/errors" + challengertypes "github.com/initia-labs/opinit-bots/challenger/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" + "go.uber.org/zap" +) + +func (ch *ChallengeEventHandler) CheckValue(events []challengertypes.ChallengeEvent) ([]challengertypes.Challenge, []challengertypes.ChallengeEvent, error) { + challenges := make([]challengertypes.Challenge, 0) + processedEvents := make([]challengertypes.ChallengeEvent, 0) + + for _, event := range events { + pendingEvent, ok := ch.GetPendingEvent(event.Id()) + if !ok { + // might not happened because child always syncs later than host. + return nil, nil, errors.Wrap(nodetypes.ErrIgnoreAndTryLater, fmt.Sprintf("pending event not found: %s", event.String())) + } + + ok, err := pendingEvent.Equal(event) + if err != nil { + return nil, nil, err + } else if !ok { + challenges = append(challenges, challengertypes.Challenge{ + EventType: event.Type().String(), + Id: event.Id(), + Log: fmt.Sprintf("pending event does not match; expected: %s, got: %s", pendingEvent.String(), event.String()), + Time: event.EventTime(), + }) + } else { + ch.logger.Info("pending event matched", zap.String("event", pendingEvent.String())) + } + processedEvents = append(processedEvents, pendingEvent) + + if event.Type() == challengertypes.EventTypeOracle { + oracleEvents := ch.getOraclePendingEvents(event.Id().Id) + processedEvents = append(processedEvents, oracleEvents...) + } + } + + return challenges, processedEvents, nil +} + +func (ch *ChallengeEventHandler) GetPrevPendingEvent(event challengertypes.ChallengeEvent) (challengertypes.ChallengeEvent, bool) { + prevId := event.Id() + prevId.Id-- + prevOutputEvent, ok := ch.GetPendingEvent(prevId) + return prevOutputEvent, ok +} + +func (ch *ChallengeEventHandler) CheckTimeout(blockTime time.Time, events []challengertypes.ChallengeEvent) ([]challengertypes.Challenge, []challengertypes.ChallengeEvent) { + challenges := make([]challengertypes.Challenge, 0) + processedEvents := make([]challengertypes.ChallengeEvent, 0) + + for _, pendingEvent := range events { + timeout := pendingEvent.EventTime().Add(ch.timeoutDuration) + if !pendingEvent.EventTime().IsZero() && !pendingEvent.IsTimeout() && blockTime.After(timeout) { + challenges = append(challenges, challengertypes.Challenge{ + EventType: pendingEvent.Type().String(), + Id: pendingEvent.Id(), + Log: fmt.Sprintf("event timeout: %s", pendingEvent.String()), + Time: blockTime, + }) + pendingEvent.SetTimeout() + processedEvents = append(processedEvents, pendingEvent) + } + } + return challenges, processedEvents +} diff --git a/challenger/eventhandler/db.go b/challenger/eventhandler/db.go new file mode 100644 index 0000000..d8bcf72 --- /dev/null +++ b/challenger/eventhandler/db.go @@ -0,0 +1,26 @@ +package eventhandler + +import ( + challengertypes "github.com/initia-labs/opinit-bots/challenger/types" + "github.com/initia-labs/opinit-bots/types" +) + +func (ch *ChallengeEventHandler) PendingEventsToRawKV(events []challengertypes.ChallengeEvent, delete bool) ([]types.RawKV, error) { + kvs := make([]types.RawKV, 0, len(events)) + for _, event := range events { + var data []byte + var err error + + if !delete { + data, err = event.Marshal() + if err != nil { + return nil, err + } + } + kvs = append(kvs, types.RawKV{ + Key: ch.db.PrefixedKey(challengertypes.PrefixedPendingEvent(event.Id())), + Value: data, + }) + } + return kvs, nil +} diff --git a/challenger/eventhandler/event_handler.go b/challenger/eventhandler/event_handler.go new file mode 100644 index 0000000..416461b --- /dev/null +++ b/challenger/eventhandler/event_handler.go @@ -0,0 +1,37 @@ +package eventhandler + +import ( + "sync" + "time" + + challengertypes "github.com/initia-labs/opinit-bots/challenger/types" + "github.com/initia-labs/opinit-bots/types" + "go.uber.org/zap" +) + +type ChallengeEventHandler struct { + db types.DB + logger *zap.Logger + pendingEventsMu *sync.Mutex + pendingEvents map[challengertypes.ChallengeId]challengertypes.ChallengeEvent + timeoutDuration time.Duration +} + +func NewChallengeEventHandler(db types.DB, logger *zap.Logger) *ChallengeEventHandler { + return &ChallengeEventHandler{ + db: db, + logger: logger, + pendingEventsMu: &sync.Mutex{}, + pendingEvents: make(map[challengertypes.ChallengeId]challengertypes.ChallengeEvent), + } +} + +func (ch *ChallengeEventHandler) Initialize(timeoutDuration time.Duration) error { + pendingEvents, err := ch.loadPendingEvents() + if err != nil { + return err + } + ch.SetPendingEvents(pendingEvents) + ch.timeoutDuration = timeoutDuration + return nil +} diff --git a/challenger/eventhandler/pending_events.go b/challenger/eventhandler/pending_events.go new file mode 100644 index 0000000..e912a85 --- /dev/null +++ b/challenger/eventhandler/pending_events.go @@ -0,0 +1,116 @@ +package eventhandler + +import ( + "maps" + "sort" + + challengertypes "github.com/initia-labs/opinit-bots/challenger/types" +) + +func (ch *ChallengeEventHandler) GetPendingEvent(id challengertypes.ChallengeId) (challengertypes.ChallengeEvent, bool) { + ch.pendingEventsMu.Lock() + defer ch.pendingEventsMu.Unlock() + + event, ok := ch.pendingEvents[id] + return event, ok +} + +func (ch *ChallengeEventHandler) DeletePendingEvents(events []challengertypes.ChallengeEvent) { + ch.pendingEventsMu.Lock() + defer ch.pendingEventsMu.Unlock() + + for _, event := range events { + delete(ch.pendingEvents, event.Id()) + } +} + +func (ch *ChallengeEventHandler) DeletePendingEvent(id challengertypes.ChallengeId) { + ch.pendingEventsMu.Lock() + defer ch.pendingEventsMu.Unlock() + + delete(ch.pendingEvents, id) +} + +func (ch *ChallengeEventHandler) getOraclePendingEvents(l1BlockHeight uint64) []challengertypes.ChallengeEvent { + ch.pendingEventsMu.Lock() + defer ch.pendingEventsMu.Unlock() + + events := make([]challengertypes.ChallengeEvent, 0) + for _, event := range ch.pendingEvents { + if event.Type() == challengertypes.EventTypeOracle && event.Id().Id < l1BlockHeight { + events = append(events, event) + } + } + return events +} + +func (ch *ChallengeEventHandler) NumPendingEvents() map[string]int64 { + ch.pendingEventsMu.Lock() + defer ch.pendingEventsMu.Unlock() + + numPendingEvents := make(map[string]int64) + for _, event := range ch.pendingEvents { + numPendingEvents[event.Type().String()]++ + } + return numPendingEvents +} + +func (ch *ChallengeEventHandler) GetAllPendingEvents() []challengertypes.ChallengeEvent { + return ch.GetUnprocessedPendingEvents(nil) +} + +func (ch *ChallengeEventHandler) GetAllSortedPendingEvents() []challengertypes.ChallengeEvent { + pendingEvents := ch.GetAllPendingEvents() + sort.Slice(pendingEvents, func(i, j int) bool { + if pendingEvents[i].Type() == pendingEvents[j].Type() { + return pendingEvents[i].Id().Id < pendingEvents[j].Id().Id + } + return pendingEvents[i].Type() < pendingEvents[j].Type() + }) + return pendingEvents +} + +func (ch *ChallengeEventHandler) GetUnprocessedPendingEvents(processedEvents []challengertypes.ChallengeEvent) []challengertypes.ChallengeEvent { + ch.pendingEventsMu.Lock() + defer ch.pendingEventsMu.Unlock() + + copiedPendingEvents := maps.Clone(ch.pendingEvents) + for _, event := range processedEvents { + delete(copiedPendingEvents, event.Id()) + } + + unprocessedPendingEvents := make([]challengertypes.ChallengeEvent, 0, len(copiedPendingEvents)) + for _, event := range copiedPendingEvents { + unprocessedPendingEvents = append(unprocessedPendingEvents, event) + } + return unprocessedPendingEvents +} + +func (ch *ChallengeEventHandler) SetPendingEvents(events []challengertypes.ChallengeEvent) { + ch.pendingEventsMu.Lock() + defer ch.pendingEventsMu.Unlock() + + for _, event := range events { + ch.pendingEvents[event.Id()] = event + } +} + +func (ch *ChallengeEventHandler) loadPendingEvents() (events []challengertypes.ChallengeEvent, err error) { + iterErr := ch.db.PrefixedIterate(challengertypes.PendingEventKey, func(key, value []byte) (stop bool, err error) { + id, err := challengertypes.ParsePendingEvent(key) + if err != nil { + return true, err + } + + event, err := challengertypes.UnmarshalChallengeEvent(id.Type, value) + if err != nil { + return true, err + } + events = append(events, event) + return false, nil + }) + if iterErr != nil { + return nil, iterErr + } + return +} diff --git a/challenger/handler.go b/challenger/handler.go new file mode 100644 index 0000000..65651a5 --- /dev/null +++ b/challenger/handler.go @@ -0,0 +1,86 @@ +package challenger + +import ( + "context" + "sort" + + challengertypes "github.com/initia-labs/opinit-bots/challenger/types" + "github.com/initia-labs/opinit-bots/types" + "go.uber.org/zap" +) + +func (c *Challenger) challengeHandler(ctx context.Context) error { + defer close(c.challengeChStopped) + for { + select { + case <-ctx.Done(): + return nil + case challenge := <-c.challengeCh: + kvs := make([]types.RawKV, 0) + kv := c.deletePendingChallenge(challenge) + kvs = append(kvs, kv) + kv, err := c.saveChallenge(challenge) + if err != nil { + return err + } + kvs = append(kvs, kv) + + err = c.handleChallenge(challenge) + if err != nil { + return err + } + + err = c.db.RawBatchSet(kvs...) + if err != nil { + return err + } + + c.insertLatestChallenges(challenge) + } + } +} + +func (c *Challenger) insertLatestChallenges(challenge challengertypes.Challenge) { + c.latestChallengesMu.Lock() + defer c.latestChallengesMu.Unlock() + + c.latestChallenges = append(c.latestChallenges, challenge) + sort.Slice(c.latestChallenges, func(i, j int) bool { + if c.latestChallenges[i].Time.Equal(c.latestChallenges[j].Time) { + if c.latestChallenges[i].Id.Type == c.latestChallenges[j].Id.Type { + return c.latestChallenges[i].Id.Type < c.latestChallenges[j].Id.Type + } + return c.latestChallenges[i].Id.Id < c.latestChallenges[j].Id.Id + } + return c.latestChallenges[i].Time.Before(c.latestChallenges[j].Time) + }) + if len(c.latestChallenges) > 5 { + c.latestChallenges = c.latestChallenges[1:] + } +} + +func (c *Challenger) getLatestChallenges() []challengertypes.Challenge { + c.latestChallengesMu.Lock() + defer c.latestChallengesMu.Unlock() + + res := make([]challengertypes.Challenge, len(c.latestChallenges)) + copy(res, c.latestChallenges) + return res +} + +func (c *Challenger) handleChallenge(challenge challengertypes.Challenge) error { + // TODO: warning log or send to alerting system + c.logger.Error("challenge", zap.Any("challenge", challenge)) + + return nil +} + +func (c *Challenger) SendPendingChallenges(challenges []challengertypes.Challenge) { + for _, challenge := range challenges { + select { + case <-c.challengeChStopped: + return + case c.challengeCh <- challenge: + } + } +} diff --git a/challenger/host/deposit.go b/challenger/host/deposit.go new file mode 100644 index 0000000..e84030c --- /dev/null +++ b/challenger/host/deposit.go @@ -0,0 +1,58 @@ +package host + +import ( + "context" + "errors" + "time" + + "cosmossdk.io/math" + nodetypes "github.com/initia-labs/opinit-bots/node/types" + hostprovider "github.com/initia-labs/opinit-bots/provider/host" + + sdk "github.com/cosmos/cosmos-sdk/types" + + challengertypes "github.com/initia-labs/opinit-bots/challenger/types" +) + +func (h *Host) initiateDepositHandler(_ context.Context, args nodetypes.EventHandlerArgs) error { + bridgeId, l1Sequence, from, to, l1Denom, l2Denom, amount, _, err := hostprovider.ParseMsgInitiateDeposit(args.EventAttributes) + if err != nil { + return err + } + if bridgeId != h.BridgeId() { + // pass other bridge deposit event + return nil + } + + return h.handleInitiateDeposit( + l1Sequence, + args.BlockHeight, + args.BlockTime, + from, + to, + l1Denom, + l2Denom, + amount, + ) +} + +func (h *Host) handleInitiateDeposit( + l1Sequence uint64, + blockHeight uint64, + blockTime time.Time, + from string, + to string, + l1Denom string, + l2Denom string, + amount string, +) error { + coinAmount, ok := math.NewIntFromString(amount) + if !ok { + return errors.New("invalid amount") + } + coin := sdk.NewCoin(l2Denom, coinAmount) + + deposit := challengertypes.NewDeposit(l1Sequence, blockHeight, from, to, l1Denom, coin.String(), blockTime) + h.eventQueue = append(h.eventQueue, deposit) + return nil +} diff --git a/challenger/host/handler.go b/challenger/host/handler.go new file mode 100644 index 0000000..337b7ff --- /dev/null +++ b/challenger/host/handler.go @@ -0,0 +1,83 @@ +package host + +import ( + "context" + + challengertypes "github.com/initia-labs/opinit-bots/challenger/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" + "github.com/initia-labs/opinit-bots/types" +) + +func (h *Host) beginBlockHandler(_ context.Context, args nodetypes.BeginBlockArgs) error { + h.eventQueue = h.eventQueue[:0] + h.outputPendingEventQueue = h.outputPendingEventQueue[:0] + return nil +} + +func (h *Host) endBlockHandler(_ context.Context, args nodetypes.EndBlockArgs) error { + blockHeight := uint64(args.Block.Header.Height) + batchKVs := []types.RawKV{ + h.Node().SyncInfoToRawKV(blockHeight), + } + + // save all pending events to child db + eventKVs, err := h.child.PendingEventsToRawKV(h.eventQueue, false) + if err != nil { + return err + } + batchKVs = append(batchKVs, eventKVs...) + + // save all pending events to host db + // currently, only output event is considered as pending event + if len(h.outputPendingEventQueue) > 1 || (len(h.outputPendingEventQueue) == 1 && h.outputPendingEventQueue[0].Type() != challengertypes.EventTypeOutput) { + panic("must not happen, outputPendingEventQueue should have only one output event") + } + + eventKVs, err = h.eventHandler.PendingEventsToRawKV(h.outputPendingEventQueue, false) + if err != nil { + return err + } + batchKVs = append(batchKVs, eventKVs...) + + prevEvents := make([]challengertypes.ChallengeEvent, 0) + for _, pendingEvent := range h.outputPendingEventQueue { + prevEvent, ok := h.eventHandler.GetPrevPendingEvent(pendingEvent) + if ok { + prevEvents = append(prevEvents, prevEvent) + } + } + unprocessedEvents := h.eventHandler.GetUnprocessedPendingEvents(prevEvents) + pendingChallenges, precessedEvents := h.eventHandler.CheckTimeout(args.Block.Header.Time, unprocessedEvents) + precessedEvents = append(precessedEvents, prevEvents...) + + // delete processed events + eventKVs, err = h.eventHandler.PendingEventsToRawKV(precessedEvents, true) + if err != nil { + return err + } + batchKVs = append(batchKVs, eventKVs...) + + challengesKVs, err := h.challenger.PendingChallengeToRawKVs(pendingChallenges, false) + if err != nil { + return err + } + batchKVs = append(batchKVs, challengesKVs...) + + err = h.DB().RawBatchSet(batchKVs...) + if err != nil { + return err + } + + h.child.SetPendingEvents(h.eventQueue) + h.eventHandler.DeletePendingEvents(precessedEvents) + h.eventHandler.SetPendingEvents(h.outputPendingEventQueue) + h.challenger.SendPendingChallenges(pendingChallenges) + return nil +} + +func (h *Host) txHandler(_ context.Context, args nodetypes.TxHandlerArgs) error { + if args.TxIndex == 0 { + h.oracleTxHandler(args.BlockHeight, args.BlockTime, args.Tx) + } + return nil +} diff --git a/challenger/host/host.go b/challenger/host/host.go new file mode 100644 index 0000000..fc4caf8 --- /dev/null +++ b/challenger/host/host.go @@ -0,0 +1,85 @@ +package host + +import ( + "context" + "time" + + "go.uber.org/zap" + + opchildtypes "github.com/initia-labs/OPinit/x/opchild/types" + ophosttypes "github.com/initia-labs/OPinit/x/ophost/types" + + nodetypes "github.com/initia-labs/opinit-bots/node/types" + "github.com/initia-labs/opinit-bots/types" + + hostprovider "github.com/initia-labs/opinit-bots/provider/host" + + "github.com/initia-labs/opinit-bots/challenger/eventhandler" + challengertypes "github.com/initia-labs/opinit-bots/challenger/types" +) + +type challenger interface { + PendingChallengeToRawKVs([]challengertypes.Challenge, bool) ([]types.RawKV, error) + SendPendingChallenges([]challengertypes.Challenge) +} + +type childNode interface { + PendingEventsToRawKV([]challengertypes.ChallengeEvent, bool) ([]types.RawKV, error) + SetPendingEvents([]challengertypes.ChallengeEvent) +} + +type Host struct { + *hostprovider.BaseHost + + child childNode + challenger challenger + eventHandler *eventhandler.ChallengeEventHandler + + eventQueue []challengertypes.ChallengeEvent + outputPendingEventQueue []challengertypes.ChallengeEvent + + // status info + lastOutputIndex uint64 + lastOutputTime time.Time +} + +func NewHostV1( + cfg nodetypes.NodeConfig, + db types.DB, logger *zap.Logger, bech32Prefix string, +) *Host { + return &Host{ + BaseHost: hostprovider.NewBaseHostV1(cfg, db, logger, bech32Prefix), + eventHandler: eventhandler.NewChallengeEventHandler(db, logger), + eventQueue: make([]challengertypes.ChallengeEvent, 0), + outputPendingEventQueue: make([]challengertypes.ChallengeEvent, 0), + } +} + +func (h *Host) Initialize(ctx context.Context, startHeight uint64, child childNode, bridgeInfo opchildtypes.BridgeInfo, challenger challenger) error { + err := h.BaseHost.Initialize(ctx, startHeight, bridgeInfo) + if err != nil { + return err + } + h.child = child + h.challenger = challenger + // TODO: ignore l1Sequence less than child's last l1 sequence + h.registerHandlers() + + err = h.eventHandler.Initialize(bridgeInfo.BridgeConfig.SubmissionInterval) + if err != nil { + return err + } + return nil +} + +func (h *Host) registerHandlers() { + h.Node().RegisterBeginBlockHandler(h.beginBlockHandler) + h.Node().RegisterTxHandler(h.txHandler) + h.Node().RegisterEventHandler(ophosttypes.EventTypeInitiateTokenDeposit, h.initiateDepositHandler) + h.Node().RegisterEventHandler(ophosttypes.EventTypeProposeOutput, h.proposeOutputHandler) + h.Node().RegisterEndBlockHandler(h.endBlockHandler) +} + +func (h *Host) QuerySyncedOutput(ctx context.Context, bridgeId uint64, outputIndex uint64) (*ophosttypes.QueryOutputProposalResponse, error) { + return h.BaseHost.QueryOutput(ctx, bridgeId, outputIndex, h.Height()-1) +} diff --git a/challenger/host/oracle.go b/challenger/host/oracle.go new file mode 100644 index 0000000..07cadb5 --- /dev/null +++ b/challenger/host/oracle.go @@ -0,0 +1,19 @@ +package host + +import ( + "time" + + comettypes "github.com/cometbft/cometbft/types" + + challengertypes "github.com/initia-labs/opinit-bots/challenger/types" +) + +func (h *Host) oracleTxHandler(blockHeight uint64, blockTime time.Time, oracleDataBytes comettypes.Tx) { + if !h.OracleEnabled() { + return + } + checksum := challengertypes.OracleChecksum(oracleDataBytes) + oracle := challengertypes.NewOracle(blockHeight, checksum, blockTime) + + h.eventQueue = append(h.eventQueue, oracle) +} diff --git a/challenger/host/output.go b/challenger/host/output.go new file mode 100644 index 0000000..70eefd2 --- /dev/null +++ b/challenger/host/output.go @@ -0,0 +1,43 @@ +package host + +import ( + "context" + "encoding/base64" + "time" + + nodetypes "github.com/initia-labs/opinit-bots/node/types" + hostprovider "github.com/initia-labs/opinit-bots/provider/host" + "go.uber.org/zap" + + challengertypes "github.com/initia-labs/opinit-bots/challenger/types" +) + +func (h *Host) proposeOutputHandler(_ context.Context, args nodetypes.EventHandlerArgs) error { + bridgeId, l2BlockNumber, outputIndex, proposer, outputRoot, err := hostprovider.ParseMsgProposeOutput(args.EventAttributes) + if err != nil { + return err + } + if bridgeId != h.BridgeId() { + // pass other bridge output proposal event + return nil + } + return h.handleProposeOutput(bridgeId, proposer, outputIndex, l2BlockNumber, outputRoot, args.BlockTime) +} + +func (h *Host) handleProposeOutput(bridgeId uint64, proposer string, outputIndex uint64, l2BlockNumber uint64, outputRoot []byte, blockTime time.Time) error { + output := challengertypes.NewOutput(l2BlockNumber, outputIndex, outputRoot[:], blockTime) + + h.lastOutputIndex = outputIndex + h.lastOutputTime = blockTime + h.eventQueue = append(h.eventQueue, output) + h.outputPendingEventQueue = append(h.outputPendingEventQueue, output) + + h.Logger().Info("propose output", + zap.Uint64("bridge_id", bridgeId), + zap.String("proposer", proposer), + zap.Uint64("output_index", outputIndex), + zap.Uint64("l2_block_number", l2BlockNumber), + zap.String("output_root", base64.StdEncoding.EncodeToString(outputRoot)), + ) + return nil +} diff --git a/challenger/host/status.go b/challenger/host/status.go new file mode 100644 index 0000000..5a21989 --- /dev/null +++ b/challenger/host/status.go @@ -0,0 +1,30 @@ +package host + +import ( + "time" + + challengertypes "github.com/initia-labs/opinit-bots/challenger/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" +) + +type Status struct { + Node nodetypes.Status `json:"node"` + LastOutputIndex uint64 `json:"last_output_index"` + LastOutputTime time.Time `json:"last_output_time"` + NumPendingEvents map[string]int64 `json:"num_pending_events"` +} + +func (h Host) GetStatus() Status { + return Status{ + Node: h.GetNodeStatus(), + NumPendingEvents: h.eventHandler.NumPendingEvents(), + } +} + +func (h Host) GetNodeStatus() nodetypes.Status { + return h.Node().GetStatus() +} + +func (h Host) GetAllPendingEvents() []challengertypes.ChallengeEvent { + return h.eventHandler.GetAllSortedPendingEvents() +} diff --git a/challenger/query.go b/challenger/query.go new file mode 100644 index 0000000..4ad11d0 --- /dev/null +++ b/challenger/query.go @@ -0,0 +1,27 @@ +package challenger + +import challengertypes "github.com/initia-labs/opinit-bots/challenger/types" + +func (c *Challenger) QueryChallenges(page uint64) (challenges []challengertypes.Challenge, err error) { + i := uint64(0) + iterErr := c.db.PrefixedIterate(challengertypes.ChallengeKey, func(_, value []byte) (stop bool, err error) { + i++ + if i >= (page+1)*100 { + return true, nil + } + if i < page*100 { + return false, nil + } + challenge := challengertypes.Challenge{} + err = challenge.Unmarshal(value) + if err != nil { + return true, err + } + challenges = append(challenges, challenge) + return false, nil + }) + if iterErr != nil { + return nil, iterErr + } + return +} diff --git a/challenger/status.go b/challenger/status.go new file mode 100644 index 0000000..7718f4b --- /dev/null +++ b/challenger/status.go @@ -0,0 +1,28 @@ +package challenger + +import ( + "github.com/initia-labs/opinit-bots/challenger/child" + "github.com/initia-labs/opinit-bots/challenger/host" + challengertypes "github.com/initia-labs/opinit-bots/challenger/types" +) + +type Status struct { + BridgeId uint64 `json:"bridge_id"` + Host host.Status `json:"host,omitempty"` + Child child.Status `json:"child,omitempty"` + LatestChallenges []challengertypes.Challenge `json:"latest_challenges"` +} + +func (c Challenger) GetStatus() Status { + s := Status{ + BridgeId: c.host.BridgeId(), + } + if c.host != nil { + s.Host = c.host.GetStatus() + } + if c.child != nil { + s.Child = c.child.GetStatus() + } + s.LatestChallenges = c.getLatestChallenges() + return s +} diff --git a/challenger/types/challenge.go b/challenger/types/challenge.go new file mode 100644 index 0000000..cab7b64 --- /dev/null +++ b/challenger/types/challenge.go @@ -0,0 +1,296 @@ +package types + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "time" +) + +type Challenge struct { + EventType string `json:"event_type"` + Id ChallengeId `json:"id"` + Log string `json:"log"` + Time time.Time `json:"timestamp"` +} + +func (c Challenge) Marshal() ([]byte, error) { + return json.Marshal(&c) +} + +func (c *Challenge) Unmarshal(value []byte) error { + return json.Unmarshal(value, c) +} + +type ChallengeId struct { + Type EventType `json:"type"` + Id uint64 `json:"id"` +} + +func (c ChallengeId) String() string { + return fmt.Sprintf("%s-%d", c.Type.String(), c.Id) +} + +type ChallengeEvent interface { + Equal(ChallengeEvent) (bool, error) + String() string + Marshal() ([]byte, error) + Unmarshal([]byte) error + Type() EventType + EventTime() time.Time + SetTimeout() + IsTimeout() bool + Id() ChallengeId +} + +func UnmarshalChallengeEvent(eventType EventType, data []byte) (ChallengeEvent, error) { + var event ChallengeEvent + + switch eventType { + case EventTypeDeposit: + event = &Deposit{} + case EventTypeOutput: + event = &Output{} + case EventTypeOracle: + event = &Oracle{} + default: + return nil, fmt.Errorf("invalid event type: %d", eventType) + } + if err := event.Unmarshal(data); err != nil { + return nil, err + } + return event, nil +} + +type EventType uint8 + +const ( + EventTypeDeposit EventType = iota + EventTypeOutput + EventTypeOracle +) + +func (e EventType) Validate() error { + if e != EventTypeDeposit && e != EventTypeOutput && e != EventTypeOracle { + return fmt.Errorf("invalid event type: %d", e) + } + return nil +} + +func (e EventType) String() string { + switch e { + case EventTypeDeposit: + return "Deposit" + case EventTypeOutput: + return "Output" + case EventTypeOracle: + return "Oracle" + default: + return "Unknown" + } +} + +type Deposit struct { + EventType string `json:"event_type"` + Sequence uint64 `json:"sequence"` + L1BlockHeight uint64 `json:"l1_block_height"` + From string `json:"from"` + To string `json:"to"` + L1Denom string `json:"l1_denom"` + Amount string `json:"amount"` + Time time.Time `json:"time"` + Timeout bool `json:"timeout"` +} + +var _ ChallengeEvent = &Deposit{} + +func NewDeposit(sequence, l1BlockHeight uint64, from, to, l1Denom, amount string, time time.Time) *Deposit { + d := &Deposit{ + Sequence: sequence, + L1BlockHeight: l1BlockHeight, + From: from, + To: to, + L1Denom: l1Denom, + Amount: amount, + Time: time, + } + d.EventType = d.Type().String() + return d +} + +func (d Deposit) Marshal() ([]byte, error) { + return json.Marshal(&d) +} + +func (d *Deposit) Unmarshal(data []byte) error { + return json.Unmarshal(data, d) +} + +func (d Deposit) Equal(another ChallengeEvent) (bool, error) { + anotherDeposit, ok := another.(*Deposit) + if !ok { + return false, fmt.Errorf("invalid type: %T", another) + } + return d.Sequence == anotherDeposit.Sequence && + d.L1BlockHeight == anotherDeposit.L1BlockHeight && + d.From == anotherDeposit.From && + d.To == anotherDeposit.To && + d.L1Denom == anotherDeposit.L1Denom && + d.Amount == anotherDeposit.Amount, nil +} + +func (d Deposit) String() string { + return fmt.Sprintf("Deposit{Sequence: %d, L1BlockHeight: %d, From: %s, To: %s, L1Denom: %s, Amount: %s, Time: %s}", d.Sequence, d.L1BlockHeight, d.From, d.To, d.L1Denom, d.Amount, d.Time) +} + +func (d Deposit) Type() EventType { + return EventTypeDeposit +} + +func (d Deposit) EventTime() time.Time { + return d.Time +} + +func (d Deposit) Id() ChallengeId { + return ChallengeId{ + Type: EventTypeDeposit, + Id: d.Sequence, + } +} + +func (d *Deposit) SetTimeout() { + d.Timeout = true +} + +func (d Deposit) IsTimeout() bool { + return d.Timeout +} + +type Output struct { + EventType string `json:"event_type"` + L2BlockNumber uint64 `json:"l2_block_number"` + OutputIndex uint64 `json:"output_index"` + OutputRoot []byte `json:"output_root"` + Time time.Time `json:"time"` + Timeout bool `json:"timeout"` +} + +var _ ChallengeEvent = &Output{} + +func NewOutput(l2BlockNumber, outputIndex uint64, outputRoot []byte, time time.Time) *Output { + o := &Output{ + L2BlockNumber: l2BlockNumber, + OutputIndex: outputIndex, + OutputRoot: outputRoot, + Time: time, + } + o.EventType = o.Type().String() + return o +} + +func (o Output) Marshal() ([]byte, error) { + return json.Marshal(&o) +} + +func (o *Output) Unmarshal(data []byte) error { + return json.Unmarshal(data, o) +} + +func (o Output) Equal(another ChallengeEvent) (bool, error) { + anotherOutput, ok := another.(*Output) + if !ok { + return false, fmt.Errorf("invalid type: %T", another) + } + return o.L2BlockNumber == anotherOutput.L2BlockNumber && + o.OutputIndex == anotherOutput.OutputIndex && + bytes.Equal(o.OutputRoot, anotherOutput.OutputRoot), nil +} + +func (o Output) String() string { + return fmt.Sprintf("Output{L2BlockNumber: %d, OutputIndex: %d, OutputRoot: %s, Time: %s}", o.L2BlockNumber, o.OutputIndex, base64.RawStdEncoding.EncodeToString(o.OutputRoot), o.Time) +} + +func (o Output) Type() EventType { + return EventTypeOutput +} + +func (o Output) EventTime() time.Time { + return o.Time +} + +func (o Output) Id() ChallengeId { + return ChallengeId{ + Type: EventTypeOutput, + Id: o.OutputIndex, + } +} +func (o *Output) SetTimeout() { + o.Timeout = true +} + +func (o Output) IsTimeout() bool { + return o.Timeout +} + +type Oracle struct { + EventType string `json:"event_type"` + L1Height uint64 `json:"l1_height"` + Data []byte `json:"data"` + Time time.Time `json:"time"` + Timeout bool `json:"timeout"` +} + +func NewOracle(l1Height uint64, data []byte, time time.Time) *Oracle { + o := &Oracle{ + L1Height: l1Height, + Data: data, + Time: time, + } + o.EventType = o.Type().String() + return o +} + +func (o Oracle) Marshal() ([]byte, error) { + return json.Marshal(&o) +} + +func (o *Oracle) Unmarshal(data []byte) error { + return json.Unmarshal(data, o) +} + +func (o Oracle) Equal(another ChallengeEvent) (bool, error) { + anotherOracle, ok := another.(*Oracle) + if !ok { + return false, fmt.Errorf("invalid type: %T", another) + } + return o.L1Height == anotherOracle.L1Height && + bytes.Equal(o.Data, anotherOracle.Data), nil +} + +func (o Oracle) String() string { + return fmt.Sprintf("Oracle{L1Height: %d, Data: %s, Time: %s}", o.L1Height, base64.RawStdEncoding.EncodeToString(o.Data), o.Time) +} + +func (o Oracle) Type() EventType { + return EventTypeOracle +} + +func (o Oracle) EventTime() time.Time { + return o.Time +} + +func (o Oracle) Id() ChallengeId { + return ChallengeId{ + Type: EventTypeOracle, + Id: o.L1Height, + } +} + +func (o *Oracle) SetTimeout() { + o.Timeout = true +} + +func (o Oracle) IsTimeout() bool { + return o.Timeout +} diff --git a/challenger/types/config.go b/challenger/types/config.go new file mode 100644 index 0000000..e35eb65 --- /dev/null +++ b/challenger/types/config.go @@ -0,0 +1,104 @@ +package types + +import ( + "errors" + + nodetypes "github.com/initia-labs/opinit-bots/node/types" +) + +type NodeConfig struct { + ChainID string `json:"chain_id"` + Bech32Prefix string `json:"bech32_prefix"` + RPCAddress string `json:"rpc_address"` +} + +func (nc NodeConfig) Validate() error { + if nc.ChainID == "" { + return errors.New("chain ID is required") + } + if nc.Bech32Prefix == "" { + return errors.New("bech32 prefix is required") + } + if nc.RPCAddress == "" { + return errors.New("RPC address is required") + } + return nil +} + +type Config struct { + // Version is the version used to build output root. + Version uint8 `json:"version"` + + // ListenAddress is the address to listen for incoming requests. + ListenAddress string `json:"listen_address"` + + // L1Node is the configuration for the l1 node. + L1Node NodeConfig `json:"l1_node"` + // L2Node is the configuration for the l2 node. + L2Node NodeConfig `json:"l2_node"` + + // L2StartHeight is the height to start the l2 node. If it is 0, it will start from the latest height. + // If the latest height stored in the db is not 0, this config is ignored. + // L2 starts from the last submitted output l2 block number + 1 before L2StartHeight. + // L1 starts from the block number of the output tx + 1 + L2StartHeight uint64 `json:"l2_start_height"` +} + +func DefaultConfig() *Config { + return &Config{ + Version: 1, + ListenAddress: "localhost:3001", + + L1Node: NodeConfig{ + ChainID: "testnet-l1-1", + Bech32Prefix: "init", + RPCAddress: "tcp://localhost:26657", + }, + + L2Node: NodeConfig{ + ChainID: "testnet-l2-1", + Bech32Prefix: "init", + RPCAddress: "tcp://localhost:27657", + }, + L2StartHeight: 0, + } +} + +func (cfg Config) Validate() error { + if cfg.Version == 0 { + return errors.New("version is required") + } + + if cfg.Version != 1 { + return errors.New("only version 1 is supported") + } + + if cfg.ListenAddress == "" { + return errors.New("listen address is required") + } + + if err := cfg.L1Node.Validate(); err != nil { + return err + } + + if err := cfg.L2Node.Validate(); err != nil { + return err + } + return nil +} + +func (cfg Config) L1NodeConfig(homePath string) nodetypes.NodeConfig { + nc := nodetypes.NodeConfig{ + RPC: cfg.L1Node.RPCAddress, + ProcessType: nodetypes.PROCESS_TYPE_DEFAULT, + } + return nc +} + +func (cfg Config) L2NodeConfig(homePath string) nodetypes.NodeConfig { + nc := nodetypes.NodeConfig{ + RPC: cfg.L2Node.RPCAddress, + ProcessType: nodetypes.PROCESS_TYPE_DEFAULT, + } + return nc +} diff --git a/challenger/types/keys.go b/challenger/types/keys.go new file mode 100644 index 0000000..b4e0160 --- /dev/null +++ b/challenger/types/keys.go @@ -0,0 +1,54 @@ +package types + +import ( + "time" + + dbtypes "github.com/initia-labs/opinit-bots/db/types" + "github.com/pkg/errors" +) + +var ( + // Keys + PendingEventKey = []byte("pending_event") + PendingChallengeKey = []byte("pending_challenge") + ChallengeKey = []byte("challenge") + StatusKey = []byte("status") +) + +func PrefixedEventType(eventType EventType) []byte { + return append([]byte{byte(eventType)}, dbtypes.Splitter) +} + +func PrefixedEventTypeId(eventType EventType, id uint64) []byte { + return append(PrefixedEventType(eventType), dbtypes.FromUint64Key(id)...) +} + +func PrefixedPendingEvent(id ChallengeId) []byte { + return append(append(PendingEventKey, dbtypes.Splitter), + PrefixedEventTypeId(id.Type, id.Id)...) +} + +func PrefixedPendingChallenge(id ChallengeId) []byte { + return append(append(PendingChallengeKey, dbtypes.Splitter), + PrefixedEventTypeId(id.Type, id.Id)...) +} + +func PrefixedTimeEventTypeId(eventTime time.Time, id ChallengeId) []byte { + return append(append(dbtypes.FromUint64Key(uint64(eventTime.UnixNano())), dbtypes.Splitter), + PrefixedEventTypeId(id.Type, id.Id)...) +} + +func PrefixedChallenge(eventTime time.Time, id ChallengeId) []byte { + return append(append(ChallengeKey, dbtypes.Splitter), + PrefixedTimeEventTypeId(eventTime, id)...) +} + +func ParsePendingEvent(key []byte) (ChallengeId, error) { + if len(key) < 10 { + return ChallengeId{}, errors.New("invalid key bytes") + } + + typeBz := key[len(key)-10 : len(key)-9] + idBz := key[len(key)-8:] + return ChallengeId{Type: EventType(typeBz[0]), Id: dbtypes.ToUint64Key(idBz)}, nil +} diff --git a/challenger/types/oracle.go b/challenger/types/oracle.go new file mode 100644 index 0000000..4346bb7 --- /dev/null +++ b/challenger/types/oracle.go @@ -0,0 +1,8 @@ +package types + +import "golang.org/x/crypto/sha3" + +func OracleChecksum(data []byte) []byte { + checksum := sha3.Sum256(data) + return checksum[:] +} diff --git a/cmd/opinitd/flags.go b/cmd/opinitd/flags.go index 8a7e4fb..b858c17 100644 --- a/cmd/opinitd/flags.go +++ b/cmd/opinitd/flags.go @@ -1,7 +1,10 @@ package main import ( + "errors" + "fmt" "os" + "path" "path/filepath" "github.com/spf13/cobra" @@ -9,18 +12,32 @@ import ( ) const ( - flagHome = "home" - flagConfigName = "config" - flagExecutorKeyName = "executor" + flagHome = "home" + flagConfigName = "config" ) var defaultHome = filepath.Join(os.Getenv("HOME"), ".opinit") func configFlag(v *viper.Viper, cmd *cobra.Command) *cobra.Command { - cmd.Flags().StringP(flagConfigName, "c", "executor.json", "The name of the configuration file in the home directory. Must have json extension.") + cmd.Flags().StringP(flagConfigName, "c", "", "The name of the configuration file in the home directory. Must have json extension. default: executor.json for executor, challenger.json for challenger") if err := v.BindPFlag(flagConfigName, cmd.Flags().Lookup(flagConfigName)); err != nil { panic(err) } return cmd } + +func getConfigPath(cmd *cobra.Command, homePath, botName string) (string, error) { + configName, err := cmd.Flags().GetString(flagConfigName) + if err != nil { + return "", err + } + if configName == "" { + configName = fmt.Sprintf("%s.json", botName) + } + configPath := path.Join(homePath, configName) + if path.Ext(configPath) != ".json" { + return "", errors.New("config file must be a json file") + } + return configPath, nil +} diff --git a/cmd/opinitd/init.go b/cmd/opinitd/init.go index 4bc4d79..a738129 100644 --- a/cmd/opinitd/init.go +++ b/cmd/opinitd/init.go @@ -2,14 +2,13 @@ package main import ( "encoding/json" - "errors" "os" - "path" "github.com/spf13/cobra" - bottypes "github.com/initia-labs/opinit-bots-go/bot/types" - executortypes "github.com/initia-labs/opinit-bots-go/executor/types" + bottypes "github.com/initia-labs/opinit-bots/bot/types" + challengertypes "github.com/initia-labs/opinit-bots/challenger/types" + executortypes "github.com/initia-labs/opinit-bots/executor/types" ) func initCmd(ctx *cmdContext) *cobra.Command { @@ -22,36 +21,40 @@ func initCmd(ctx *cmdContext) *cobra.Command { Currently supported bots are: executor `, RunE: func(cmd *cobra.Command, args []string) error { - configName, err := cmd.Flags().GetString(flagConfigName) + botType := bottypes.BotTypeFromString(args[0]) + if err := botType.Validate(); err != nil { + return err + } + + configPath, err := getConfigPath(cmd, ctx.homePath, args[0]) if err != nil { return err } - configPath := path.Join(ctx.homePath, configName) - if path.Ext(configPath) != ".json" { - return errors.New("config file must be a json file") + if err := os.MkdirAll(ctx.homePath, os.ModePerm); err != nil { + return err } - botType := bottypes.BotTypeFromString(args[0]) + f, err := os.Create(configPath) + if err != nil { + return err + } + + var config interface{} switch botType { case bottypes.BotTypeExecutor: - if err := os.MkdirAll(ctx.homePath, os.ModePerm); err != nil { - return err - } - - f, err := os.Create(configPath) - if err != nil { - return err - } - - bz, err := json.MarshalIndent(executortypes.DefaultConfig(), "", " ") - if err != nil { - return err - } - - if _, err := f.Write(bz); err != nil { - return err - } + config = executortypes.DefaultConfig() + case bottypes.BotTypeChallenger: + config = challengertypes.DefaultConfig() + } + + bz, err := json.MarshalIndent(config, "", " ") + if err != nil { + return err + } + + if _, err := f.Write(bz); err != nil { + return err } return nil diff --git a/cmd/opinitd/key.go b/cmd/opinitd/key.go index 456afc4..f8be9f8 100644 --- a/cmd/opinitd/key.go +++ b/cmd/opinitd/key.go @@ -30,7 +30,7 @@ import ( "github.com/cosmos/cosmos-sdk/crypto/hd" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/initia-labs/opinit-bots-go/keys" + "github.com/initia-labs/opinit-bots/keys" ) const ( diff --git a/cmd/opinitd/reset.go b/cmd/opinitd/reset.go index 20d1a35..e9e2965 100644 --- a/cmd/opinitd/reset.go +++ b/cmd/opinitd/reset.go @@ -6,7 +6,7 @@ import ( "github.com/spf13/cobra" - bottypes "github.com/initia-labs/opinit-bots-go/bot/types" + bottypes "github.com/initia-labs/opinit-bots/bot/types" ) func resetDBCmd(ctx *cmdContext) *cobra.Command { @@ -18,13 +18,17 @@ func resetDBCmd(ctx *cmdContext) *cobra.Command { `, RunE: func(cmd *cobra.Command, args []string) error { botType := bottypes.BotTypeFromString(args[0]) - switch botType { - case bottypes.BotTypeExecutor: - dbPath := path.Join(ctx.homePath, string(botType)) - err := os.RemoveAll(dbPath + ".db") - if err != nil { - return err - } + if err := botType.Validate(); err != nil { + return err + } + + dbPath := path.Join(ctx.homePath, string(botType)) + err := os.RemoveAll(dbPath + ".db") + if err != nil { + return err + } + + if botType == bottypes.BotTypeExecutor { err = os.RemoveAll(path.Join(ctx.homePath, "batch")) if err != nil { return err diff --git a/cmd/opinitd/root.go b/cmd/opinitd/root.go index f1e9565..7a9d118 100644 --- a/cmd/opinitd/root.go +++ b/cmd/opinitd/root.go @@ -7,7 +7,7 @@ import ( "go.uber.org/zap" "go.uber.org/zap/zapcore" - "github.com/initia-labs/opinit-bots-go/version" + "github.com/initia-labs/opinit-bots/version" ) func NewRootCmd() *cobra.Command { diff --git a/cmd/opinitd/start.go b/cmd/opinitd/start.go index ab3c4d6..34d33af 100644 --- a/cmd/opinitd/start.go +++ b/cmd/opinitd/start.go @@ -11,9 +11,9 @@ import ( "github.com/spf13/cobra" "golang.org/x/sync/errgroup" - "github.com/initia-labs/opinit-bots-go/bot" - bottypes "github.com/initia-labs/opinit-bots-go/bot/types" - "github.com/initia-labs/opinit-bots-go/types" + "github.com/initia-labs/opinit-bots/bot" + bottypes "github.com/initia-labs/opinit-bots/bot/types" + "github.com/initia-labs/opinit-bots/types" ) const ( @@ -31,12 +31,17 @@ Currently supported bots: - executor `, RunE: func(cmd *cobra.Command, args []string) error { - configName, err := cmd.Flags().GetString(flagConfigName) + botType := bottypes.BotTypeFromString(args[0]) + if err := botType.Validate(); err != nil { + return err + } + + configPath, err := getConfigPath(cmd, ctx.homePath, args[0]) if err != nil { return err } - botType := bottypes.BotTypeFromString(args[0]) - bot, err := bot.NewBot(botType, ctx.logger, ctx.homePath, configName) + + bot, err := bot.NewBot(botType, ctx.logger, ctx.homePath, configPath) if err != nil { return err } diff --git a/db/db.go b/db/db.go index f67cd93..4af2773 100644 --- a/db/db.go +++ b/db/db.go @@ -6,8 +6,8 @@ import ( "github.com/syndtr/goleveldb/leveldb" "github.com/syndtr/goleveldb/leveldb/util" - dbtypes "github.com/initia-labs/opinit-bots-go/db/types" - "github.com/initia-labs/opinit-bots-go/types" + dbtypes "github.com/initia-labs/opinit-bots/db/types" + "github.com/initia-labs/opinit-bots/types" ) var _ types.DB = (*LevelDB)(nil) @@ -101,6 +101,26 @@ func (db *LevelDB) PrefixedIterate(prefix []byte, cb func(key, value []byte) (st return iter.Error() } +func (db *LevelDB) PrefixedReverseIterate(prefix []byte, cb func(key, value []byte) (stop bool, err error)) error { + iter := db.db.NewIterator(util.BytesPrefix(db.PrefixedKey(prefix)), nil) + if iter.Last() { + for { + key := db.UnprefixedKey(iter.Key()) + if stop, err := cb(key, iter.Value()); err != nil { + return err + } else if stop { + break + } + + if !iter.Prev() { + break + } + } + } + iter.Release() + return iter.Error() +} + // SeekPrevInclusiveKey seeks the previous key-value pair in the database with prefixing the keys. // // @dev: `LevelDB.prefix + prefix` is used as the prefix for the iteration. diff --git a/db/types/utils_test.go b/db/types/utils_test.go index 01153e3..0fea785 100644 --- a/db/types/utils_test.go +++ b/db/types/utils_test.go @@ -4,7 +4,7 @@ import ( "bytes" "testing" - "github.com/initia-labs/opinit-bots-go/db/types" + "github.com/initia-labs/opinit-bots/db/types" "github.com/stretchr/testify/require" ) diff --git a/executor/README.md b/executor/README.md index 580f815..0b83f82 100644 --- a/executor/README.md +++ b/executor/README.md @@ -51,8 +51,6 @@ To configure the Executor, fill in the values in the `~/.opinit/executor.json` f // // If you don't want to use the bridge executor feature, you can leave it empty. "bridge_executor": "", - // RelayOracle is the flag to enable the oracle relay feature. - "relay_oracle": true, // MaxChunks is the maximum number of chunks in a batch. "max_chunks": 5000, // MaxChunkSize is the maximum size of a chunk in a batch. @@ -135,28 +133,29 @@ When Child's last l1 Sequence is `1`, ## Handler rules for the components of the Executor -For registered events or tx handlers, work processed in a block is atomically saved as ProcessedMsg. Therfore, if ProcessedMsgs or Txs cannot be processed due to an interrupt or error, it is guaranteed to be read from the DB and processed. +For registered events or tx handlers, work processed in a block is atomically saved as ProcessedMsg. Therefore, if ProcessedMsgs or Txs cannot be processed due to an interrupt or error, it is guaranteed to be read from the DB and processed. ## Deposit When the `initiate_token_deposit` event is detected in l1, `bridge_executor` submits a tx containing the `MsgFinalizeTokenDeposit` msg to l2. ## Withdrawal -`Child` always has a merkle tree. A working tree is stored for each block, and if the working tree of the previous block does not exist, a `panic` occurs. When it detects an `initiate_token_withdrawal` event, it adds it as a leaf node to the current working tree. The leaf index of the current working tree corresponding to the requested withdrawal is `l2 sequence of the withdrawal - start index of the working tree`. To add a leaf node, it calculates the leaf node with the withdrawal hash such as the [opinit spec](https://github.com/initia-labs/OPinit/blob/main/x/ophost/types/output.go#L30) and stores the withdrawal info corresponding to the l2 sequence. For rules on creating trees, refer [this](../merkle). +`Child` always has a merkle tree. A working tree is stored for each block, and if the working tree of the previous block does not exist, a `panic` occurs. When it detects an `initiate_token_withdrawal` event, it adds it as a leaf node to the current working tree. The leaf index of the current working tree corresponding to the requested withdrawal is `l2 sequence of the withdrawal - start index of the working tree`. To add a leaf node, it calculates the leaf node with the withdrawal hash such as the [opinit spec](https://github.com/initia-labs/OPinit/blob/v0.4.3/x/ophost/types/output.go#L30) and stores the withdrawal info corresponding to the l2 sequence. For rules on creating trees, refer [this](../merkle). ```go func GenerateWithdrawalHash(bridgeId uint64, l2Sequence uint64, sender string, receiver string, denom string, amount uint64) [32]byte { var withdrawalHash [32]byte seed := []byte{} seed = binary.BigEndian.AppendUint64(seed, bridgeId) seed = binary.BigEndian.AppendUint64(seed, l2Sequence) + // variable length - seed = append(seed, sender...) // put utf8 encoded address - seed = append(seed, Splitter) + senderDigest := sha3.Sum256([]byte(sender)) + seed = append(seed, senderDigest[:]...) // put utf8 encoded address // variable length - seed = append(seed, receiver...) // put utf8 encoded address - seed = append(seed, Splitter) + receiverDigest := sha3.Sum256([]byte(receiver)) + seed = append(seed, receiverDigest[:]...) // put utf8 encoded address // variable length - seed = append(seed, denom...) - seed = append(seed, Splitter) + denomDigest := sha3.Sum256([]byte(denom)) + seed = append(seed, denomDigest[:]...) seed = binary.BigEndian.AppendUint64(seed, amount) // double hash the leaf node @@ -199,26 +198,63 @@ type QueryWithdrawalResponse struct { This data contains all the data needed to finalize withdrawal. ## Oracle -Initia uses slinky to bring oracle data into the chain, which is stored in the 0th tx of each block. The bridge executor submits a `MsgUpdateOracle` containing the 0th Tx of l1 block to l2 when a block in l1 is created. Since oracle data always needs to be the latest, old oracles are discarded or ignored. To relay oracle, `relay_oracle` option in config must be set to true. -```go -{ - // RelayOracle is the flag to enable the oracle relay feature. - RelayOracle bool `json:"relay_oracle"` -} -``` +Initia uses slinky to bring oracle data into the chain, which is stored in the 0th tx of each block. The bridge executor submits a `MsgUpdateOracle` containing the 0th Tx of l1 block to l2 when a block in l1 is created. Since oracle data always needs to be the latest, old oracles are discarded or ignored. To relay oracle, `oracle_enabled` must be set to true in bridge config. ## Batch -`Batch` queries the batch info stored in the chain and submit the batch according to the account and chain ID. The user must provide the appropriate `RPC address`, `bech32-prefix` and `gas-price` via config. Also, the account in the batch info must be registered in the keyring. Each block's raw bytes is compressed with `gzip`. The collected block data is divided into max chunk size of config. When the `2/3` of the submission interval registered in the chain has passed since the previous submission time, it submits the batch header and block data to DA by adding last raw commit bytes. The batch header contains the last l2 block height and the checksums of each chunk that this data contains. +`Batch` queries the batch info stored in the chain and submit the batch according to the account and chain ID. The user must provide the appropriate `RPC address`, `bech32-prefix` and `gas-price` via config. Also, the account in the batch info must be registered in the keyring. Each block's raw bytes is compressed with `gzip`. The collected block data is divided into max chunk size of config. When the `2/3` of the submission interval registered in the chain has passed since the previous submission time, it submits the batch data header first and batch data chunks to DA by adding last raw commit bytes with headers. The batch header contains the start, end l2 block height and the checksums of each chunk that this data contains. ```go -// BatchHeader is the header of a batch -type BatchHeader struct { - // last l2 block height which is included in the batch - End uint64 `json:"end"` - // checksums of all chunks - Chunks [][]byte `json:"chunks"` +// BatchDataHeader is the header of a batch +type BatchDataHeader struct { + Start uint64 + End uint64 + Checksums [][]byte +} + +func MarshalBatchDataHeader( + start uint64, + end uint64, + checksums [][]byte, +) []byte { + data := make([]byte, 1) + data[0] = byte(BatchDataTypeHeader) + data = binary.BigEndian.AppendUint64(data, start) + data = binary.BigEndian.AppendUint64(data, end) + data = binary.BigEndian.AppendUint64(data, uint64(len(checksums))) + for _, checksum := range checksums { + data = append(data, checksum...) + } + return data +} + +// BatchDataChunk is the chunk of a batch +type BatchDataChunk struct { + Start uint64 + End uint64 + Index uint64 + Length uint64 + ChunkData []byte +} + +func MarshalBatchDataChunk( + start uint64, + end uint64, + index uint64, + length uint64, + chunkData []byte, +) []byte { + data := make([]byte, 1) + data[0] = byte(BatchDataTypeChunk) + data = binary.BigEndian.AppendUint64(data, start) + data = binary.BigEndian.AppendUint64(data, end) + data = binary.BigEndian.AppendUint64(data, index) + data = binary.BigEndian.AppendUint64(data, length) + data = append(data, chunkData...) + return data } ``` -If a l2 block contains `MsgUpdateOracle`, only the data field is submitted empty to reduce block bytes since the oracle data is already stored in l1. +### Note +* If a l2 block contains `MsgUpdateOracle`, only the data field is submitted empty to reduce block bytes since the oracle data is already stored in l1. +* Batch data is stored in a `batch` file in the home directory until it is submitted, so be careful **not to change the file.** ### Update batch info If the batch info registered in the chain is changed to change the account or DA chain for the batch, `Host` catches the `update_batch_info` event and send it to `Batch`. The batch will empty the temporal batch file and turn off the bot to resubmit from the last finalized output block number. Users must update the config file with updated information before starting the bot. @@ -233,4 +269,90 @@ If the batch info registered in the chain is changed to change the account or DA } ``` ## Sync from the beginning -If for some reason you need to re-sync from the beginning, the bot will query the outputs and deposits submitted to the chain and not resubmit them. However, the tree must always be saved, as it must provide withdrawal proofs. \ No newline at end of file +If for some reason you need to re-sync from the beginning, the bot will query the outputs and deposits submitted to the chain and not resubmit them. However, the tree must always be saved, as it must provide withdrawal proofs. + + +## Query + +### Status +```bash +curl localhost:3000/status +``` + +```json +{ + "bridge_id": 1, + "host": { + "node": { + "last_block_height": 0, + "broadcaster": { + "pending_txs": 0, + "sequence": 0 + } + }, + "last_proposed_output_index": 0, + "last_proposed_output_l2_block_number": 0 + }, + "child": { + "node": { + "last_block_height": 0, + "broadcaster": { + "pending_txs": 0, + "sequence": 0 + } + }, + "last_updated_oracle_height": 0, + "last_finalized_deposit_l1_block_height": 0, + "last_finalized_deposit_l1_sequence": 0, + "last_withdrawal_l2_sequence": 0, + "working_tree_index": 0, + "finalizing_block_height": 0, + "last_output_submission_time": "", + "next_output_submission_time": "" + }, + "batch": { + "node": { + "last_block_height": 0, + }, + "batch_info": { + "submitter": "", + "chain_type": "" + }, + "current_batch_file_size": 0, + "batch_start_block_number": 0, + "batch_end_block_number": 0, + "last_batch_submission_time": "" + }, + "da": { + "broadcaster": { + "pending_txs": 0, + "sequence": 0 + } + } +} +``` +### Withdrawals +```bash +curl localhost:3000/withdrawal/{sequence} | jq . > ./withdrawal-info.json +initiad tx ophost finalize-token-withdrawal ./withdrawal-info.json --gas= --gas-prices= --chain-id= --from= +``` + +```go +type QueryWithdrawalResponse struct { + // fields required to withdraw funds + BridgeId uint64 `json:"bridge_id"` + OutputIndex uint64 `json:"output_index"` + WithdrawalProofs [][]byte `json:"withdrawal_proofs"` + Sender string `json:"sender"` + Sequence uint64 `json:"sequence"` + Amount string `json:"amount"` + Version []byte `json:"version"` + StorageRoot []byte `json:"storage_root"` + LatestBlockHash []byte `json:"latest_block_hash"` + + // extra info + BlockNumber uint64 `json:"block_number"` + Receiver string `json:"receiver"` + WithdrawalHash []byte `json:"withdrawal_hash"` +} +``` \ No newline at end of file diff --git a/executor/batch/batch.go b/executor/batch/batch.go index d43231d..925b692 100644 --- a/executor/batch/batch.go +++ b/executor/batch/batch.go @@ -1,39 +1,29 @@ package batch import ( + "compress/gzip" "context" "errors" - "io" "os" "sync" - "time" "go.uber.org/zap" opchildtypes "github.com/initia-labs/OPinit/x/opchild/types" ophosttypes "github.com/initia-labs/OPinit/x/ophost/types" - dbtypes "github.com/initia-labs/opinit-bots-go/db/types" - "github.com/initia-labs/opinit-bots-go/executor/child" - executortypes "github.com/initia-labs/opinit-bots-go/executor/types" - "github.com/initia-labs/opinit-bots-go/node" - btypes "github.com/initia-labs/opinit-bots-go/node/broadcaster/types" - nodetypes "github.com/initia-labs/opinit-bots-go/node/types" - "github.com/initia-labs/opinit-bots-go/types" + executortypes "github.com/initia-labs/opinit-bots/executor/types" + "github.com/initia-labs/opinit-bots/node" + btypes "github.com/initia-labs/opinit-bots/node/broadcaster/types" + 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" ) type hostNode interface { QueryBatchInfos(context.Context, uint64) (*ophosttypes.QueryBatchInfosResponse, error) } -type compressionFunc interface { - Write([]byte) (int, error) - Reset(io.Writer) - Close() error -} - -var SubmissionKey = []byte("submission_time") - type BatchSubmitter struct { version uint8 @@ -50,30 +40,28 @@ type BatchSubmitter struct { opchildQueryClient opchildtypes.QueryClient - batchInfoMu *sync.Mutex - batchInfos []ophosttypes.BatchInfoWithOutput - batchWriter compressionFunc - batchFile *os.File - batchHeader *executortypes.BatchHeader + batchInfoMu *sync.Mutex + batchInfos []ophosttypes.BatchInfoWithOutput + batchWriter *gzip.Writer + batchFile *os.File + localBatchInfo *executortypes.LocalBatchInfo processedMsgs []btypes.ProcessedMsgs chainID string homePath string - lastSubmissionTime time.Time - // status info LastBatchEndBlockNumber uint64 } -func NewBatchSubmitter( - version uint8, cfg nodetypes.NodeConfig, +func NewBatchSubmitterV0( + cfg nodetypes.NodeConfig, batchCfg executortypes.BatchConfig, db types.DB, logger *zap.Logger, chainID, homePath, bech32Prefix string, ) *BatchSubmitter { - appCodec, txConfig, err := child.GetCodec(bech32Prefix) + appCodec, txConfig, err := childprovider.GetCodec(bech32Prefix) if err != nil { panic(err) } @@ -86,7 +74,7 @@ func NewBatchSubmitter( } ch := &BatchSubmitter{ - version: version, + version: 0, node: node, @@ -98,7 +86,8 @@ func NewBatchSubmitter( opchildQueryClient: opchildtypes.NewQueryClient(node.GetRPCClient()), - batchInfoMu: &sync.Mutex{}, + batchInfoMu: &sync.Mutex{}, + localBatchInfo: &executortypes.LocalBatchInfo{}, processedMsgs: make([]btypes.ProcessedMsgs, 0), homePath: homePath, @@ -108,7 +97,7 @@ func NewBatchSubmitter( } func (bs *BatchSubmitter) Initialize(ctx context.Context, startHeight uint64, host hostNode, bridgeInfo opchildtypes.BridgeInfo) error { - err := bs.node.Initialize(startHeight) + err := bs.node.Initialize(ctx, startHeight) if err != nil { return err } @@ -131,8 +120,17 @@ func (bs *BatchSubmitter) Initialize(ctx context.Context, startHeight uint64, ho } fileFlag := os.O_CREATE | os.O_RDWR - // if the node has already processed blocks, append to the file - if !bs.node.HeightInitialized() { + if bs.node.HeightInitialized() { + bs.localBatchInfo.Start = bs.node.GetHeight() + bs.localBatchInfo.End = 0 + bs.localBatchInfo.BatchFileSize = 0 + + err = bs.saveLocalBatchInfo() + if err != nil { + return err + } + } else { + // if the node has already processed blocks, append to the file fileFlag |= os.O_APPEND } @@ -140,8 +138,8 @@ func (bs *BatchSubmitter) Initialize(ctx context.Context, startHeight uint64, ho if err != nil { return err } - - err = bs.LoadSubmissionInfo() + // linux command gzip use level 6 as default + bs.batchWriter, err = gzip.NewWriterLevel(bs.batchFile, 6) if err != nil { return err } @@ -168,25 +166,6 @@ func (bs *BatchSubmitter) SetBridgeInfo(bridgeInfo opchildtypes.BridgeInfo) { bs.bridgeInfo = bridgeInfo } -func (bs *BatchSubmitter) LoadSubmissionInfo() error { - val, err := bs.db.Get(SubmissionKey) - if err != nil { - if err == dbtypes.ErrNotFound { - return nil - } - return err - } - bs.lastSubmissionTime = time.Unix(0, dbtypes.ToInt64(val)) - return nil -} - -func (bs *BatchSubmitter) SubmissionInfoToRawKV(timestamp int64) types.RawKV { - return types.RawKV{ - Key: bs.db.PrefixedKey(SubmissionKey), - Value: dbtypes.FromInt64(timestamp), - } -} - func (bs *BatchSubmitter) ChainID() string { return bs.chainID } diff --git a/executor/batch/db.go b/executor/batch/db.go new file mode 100644 index 0000000..c77b54b --- /dev/null +++ b/executor/batch/db.go @@ -0,0 +1,40 @@ +package batch + +import ( + "encoding/json" + + dbtypes "github.com/initia-labs/opinit-bots/db/types" + "github.com/initia-labs/opinit-bots/types" +) + +var LocalBatchInfoKey = []byte("local_batch_info") + +func (bs *BatchSubmitter) loadLocalBatchInfo() error { + val, err := bs.db.Get(LocalBatchInfoKey) + if err != nil { + if err == dbtypes.ErrNotFound { + return nil + } + return err + } + return json.Unmarshal(val, &bs.localBatchInfo) +} + +func (bs *BatchSubmitter) localBatchInfoToRawKV() (types.RawKV, error) { + value, err := json.Marshal(bs.localBatchInfo) + if err != nil { + return types.RawKV{}, err + } + return types.RawKV{ + Key: bs.db.PrefixedKey(LocalBatchInfoKey), + Value: value, + }, nil +} + +func (bs *BatchSubmitter) saveLocalBatchInfo() error { + value, err := json.Marshal(bs.localBatchInfo) + if err != nil { + return err + } + return bs.db.Set(LocalBatchInfoKey, value) +} diff --git a/executor/batch/handler.go b/executor/batch/handler.go index 2c044a7..8181be5 100644 --- a/executor/batch/handler.go +++ b/executor/batch/handler.go @@ -1,10 +1,8 @@ package batch import ( - "compress/gzip" + "bytes" "context" - "crypto/sha256" - "encoding/json" "fmt" "io" "time" @@ -20,16 +18,15 @@ import ( ophosttypes "github.com/initia-labs/OPinit/x/ophost/types" - executortypes "github.com/initia-labs/opinit-bots-go/executor/types" - btypes "github.com/initia-labs/opinit-bots-go/node/broadcaster/types" - nodetypes "github.com/initia-labs/opinit-bots-go/node/types" - "github.com/initia-labs/opinit-bots-go/types" + executortypes "github.com/initia-labs/opinit-bots/executor/types" + btypes "github.com/initia-labs/opinit-bots/node/broadcaster/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" + "github.com/initia-labs/opinit-bots/types" ) func (bs *BatchSubmitter) rawBlockHandler(ctx context.Context, args nodetypes.RawBlockArgs) error { - if len(bs.processedMsgs) != 0 { - panic("must not happen, msgQueue should be empty") - } + // clear processed messages + bs.processedMsgs = bs.processedMsgs[:0] pbb := new(cmtproto.Block) err := proto.Unmarshal(args.BlockBytes, pbb) @@ -37,7 +34,7 @@ func (bs *BatchSubmitter) rawBlockHandler(ctx context.Context, args nodetypes.Ra return errors.Wrap(err, "failed to unmarshal block") } - err = bs.prepareBatch(ctx, args.BlockHeight, pbb.Header.Time) + err = bs.prepareBatch(args.BlockHeight) if err != nil { return errors.Wrap(err, "failed to prepare batch") } @@ -47,12 +44,12 @@ func (bs *BatchSubmitter) rawBlockHandler(ctx context.Context, args nodetypes.Ra return err } - err = bs.handleBatch(blockBytes) + _, err = bs.handleBatch(blockBytes) if err != nil { return errors.Wrap(err, "failed to handle batch") } - err = bs.checkBatch(args.BlockHeight, pbb.Header.Time) + err = bs.checkBatch(ctx, args.BlockHeight, args.LatestHeight, pbb.Header.Time) if err != nil { return errors.Wrap(err, "failed to check batch") } @@ -65,9 +62,13 @@ func (bs *BatchSubmitter) rawBlockHandler(ctx context.Context, args nodetypes.Ra return errors.Wrap(err, "failed to convert processed messages to raw key value") } batchKVs = append(batchKVs, batchMsgKVs...) - if len(batchMsgKVs) > 0 { - batchKVs = append(batchKVs, bs.SubmissionInfoToRawKV(pbb.Header.Time.UnixNano())) + + kv, err := bs.localBatchInfoToRawKV() + if err != nil { + return err } + batchKVs = append(batchKVs, kv) + err = bs.db.RawBatchSet(batchKVs...) if err != nil { return errors.Wrap(err, "failed to set raw batch") @@ -76,13 +77,15 @@ func (bs *BatchSubmitter) rawBlockHandler(ctx context.Context, args nodetypes.Ra for _, processedMsg := range bs.processedMsgs { bs.da.BroadcastMsgs(processedMsg) } - - // clear processed messages - bs.processedMsgs = bs.processedMsgs[:0] return nil } -func (bs *BatchSubmitter) prepareBatch(ctx context.Context, blockHeight uint64, blockTime time.Time) error { +func (bs *BatchSubmitter) prepareBatch(blockHeight uint64) error { + err := bs.loadLocalBatchInfo() + if err != nil { + return err + } + // check whether the requested block height is reached to the l2 block number of the next batch info. if nextBatchInfo := bs.NextBatchInfo(); nextBatchInfo != nil && nextBatchInfo.Output.L2BlockNumber < blockHeight { // if the next batch info is reached, finalize the current batch and update the batch info. @@ -101,14 +104,18 @@ func (bs *BatchSubmitter) prepareBatch(ctx context.Context, blockHeight uint64, return errors.Wrap(err, "failed to seek batch file") } - // save sync info - // save sync info err = bs.node.SaveSyncInfo(nextBatchInfo.Output.L2BlockNumber) if err != nil { return errors.Wrap(err, "failed to save sync info") } - + bs.localBatchInfo.Start = nextBatchInfo.Output.L2BlockNumber + 1 + bs.localBatchInfo.End = 0 + bs.localBatchInfo.BatchFileSize = 0 + err = bs.saveLocalBatchInfo() + if err != nil { + return err + } // set last processed block height to l2 block number bs.node.SetSyncInfo(nextBatchInfo.Output.L2BlockNumber) bs.DequeueBatchInfo() @@ -117,47 +124,33 @@ func (bs *BatchSubmitter) prepareBatch(ctx context.Context, blockHeight uint64, panic(fmt.Errorf("batch info updated: reset from %d", nextBatchInfo.Output.L2BlockNumber)) } - if bs.batchHeader != nil { - // if the batch header end is not set, it means the batch is not finalized yet. - if bs.batchHeader.End == 0 { - return nil + if bs.localBatchInfo.End != 0 { + // reset batch file + err := bs.batchFile.Truncate(0) + if err != nil { + return err } - - err := bs.finalizeBatch(ctx, blockHeight) + _, err = bs.batchFile.Seek(0, 0) if err != nil { - return errors.Wrap(err, "failed to finalize batch") + return err } - // update last submission time - bs.lastSubmissionTime = blockTime - bs.LastBatchEndBlockNumber = blockHeight - } - - // reset batch header - var err error - bs.batchHeader = &executortypes.BatchHeader{} + bs.localBatchInfo.BatchFileSize = 0 + bs.localBatchInfo.Start = blockHeight + bs.localBatchInfo.End = 0 - // linux command gzip use level 6 as default - bs.batchWriter, err = gzip.NewWriterLevel(bs.batchFile, 6) - if err != nil { - return err + bs.batchWriter.Reset(bs.batchFile) } - return nil } // write block bytes to batch file -func (bs *BatchSubmitter) handleBatch(blockBytes []byte) error { - _, err := bs.batchWriter.Write(prependLength(blockBytes)) - if err != nil { - return err - } - return nil +func (bs *BatchSubmitter) handleBatch(blockBytes []byte) (int, error) { + return bs.batchWriter.Write(prependLength(blockBytes)) } // finalize batch and create batch messages func (bs *BatchSubmitter) finalizeBatch(ctx context.Context, blockHeight uint64) error { - // write last block's commit to batch file rawCommit, err := bs.node.GetRPCClient().QueryRawCommit(ctx, int64(blockHeight)) if err != nil { @@ -171,17 +164,18 @@ func (bs *BatchSubmitter) finalizeBatch(ctx context.Context, blockHeight uint64) if err != nil { return errors.Wrap(err, "failed to close batch writer") } + fileSize, err := bs.batchFileSize(false) + if err != nil { + return err + } + bs.localBatchInfo.BatchFileSize = fileSize batchBuffer := make([]byte, bs.batchCfg.MaxChunkSize) checksums := make([][]byte, 0) - // room for batch header - bs.processedMsgs = append(bs.processedMsgs, btypes.ProcessedMsgs{ - Timestamp: time.Now().UnixNano(), - Save: true, - }) - - for offset := int64(0); ; offset += int64(bs.batchCfg.MaxChunkSize) { + // TODO: improve this logic to avoid hold all the batch data in memory + chunks := make([][]byte, 0) + for offset := int64(0); ; { readLength, err := bs.batchFile.ReadAt(batchBuffer, offset) if err != nil && err != io.EOF { return err @@ -190,79 +184,102 @@ func (bs *BatchSubmitter) finalizeBatch(ctx context.Context, blockHeight uint64) } // trim the buffer to the actual read length - batchBuffer := batchBuffer[:readLength] - msg, err := bs.da.CreateBatchMsg(batchBuffer) - if err != nil { - return err - } - bs.processedMsgs = append(bs.processedMsgs, btypes.ProcessedMsgs{ - Msgs: []sdk.Msg{msg}, - Timestamp: time.Now().UnixNano(), - Save: true, - }) - checksum := sha256.Sum256(batchBuffer) + chunk := bytes.Clone(batchBuffer[:readLength]) + chunks = append(chunks, chunk) + + checksum := executortypes.GetChecksumFromChunk(chunk) checksums = append(checksums, checksum[:]) if uint64(readLength) < bs.batchCfg.MaxChunkSize { break } + offset += int64(readLength) } - // update batch header - bs.batchHeader.Chunks = checksums - headerBytes, err := json.Marshal(bs.batchHeader) - if err != nil { - return err - } - msg, err := bs.da.CreateBatchMsg(headerBytes) - if err != nil { - return err - } - bs.processedMsgs[0].Msgs = []sdk.Msg{msg} + headerData := executortypes.MarshalBatchDataHeader( + bs.localBatchInfo.Start, + bs.localBatchInfo.End, + checksums, + ) - // reset batch file - err = bs.batchFile.Truncate(0) + msg, err := bs.da.CreateBatchMsg(headerData) if err != nil { return err } - _, err = bs.batchFile.Seek(0, 0) - if err != nil { - return err + bs.processedMsgs = append(bs.processedMsgs, btypes.ProcessedMsgs{ + Msgs: []sdk.Msg{msg}, + Timestamp: time.Now().UnixNano(), + Save: true, + }) + + for i, chunk := range chunks { + chunkData := executortypes.MarshalBatchDataChunk( + bs.localBatchInfo.Start, + bs.localBatchInfo.End, + uint64(i), + uint64(len(checksums)), + chunk, + ) + msg, err := bs.da.CreateBatchMsg(chunkData) + if err != nil { + return err + } + bs.processedMsgs = append(bs.processedMsgs, btypes.ProcessedMsgs{ + Msgs: []sdk.Msg{msg}, + Timestamp: time.Now().UnixNano(), + Save: true, + }) } bs.logger.Info("finalize batch", zap.Uint64("height", blockHeight), - zap.Uint64("batch end", bs.batchHeader.End), + zap.Uint64("batch start", bs.localBatchInfo.Start), + zap.Uint64("batch end", bs.localBatchInfo.End), + zap.Uint64("batch file size ", uint64(bs.localBatchInfo.BatchFileSize)), zap.Int("chunks", len(checksums)), zap.Int("txs", len(bs.processedMsgs)), ) return nil } -func (bs *BatchSubmitter) checkBatch(blockHeight uint64, blockTime time.Time) error { - fileSize, err := bs.batchFileSize() +func (bs *BatchSubmitter) checkBatch(ctx context.Context, blockHeight uint64, latestHeight uint64, blockTime time.Time) error { + fileSize, err := bs.batchFileSize(true) if err != nil { return err } + bs.localBatchInfo.BatchFileSize = fileSize // if the block time is after the last submission time + submission interval * 2/3 // or the block time is after the last submission time + max submission time // or the batch file size is greater than (max chunks - 1) * max chunk size // then finalize the batch - if blockTime.After(bs.lastSubmissionTime.Add(bs.bridgeInfo.BridgeConfig.SubmissionInterval*2/3)) || - blockTime.After(bs.lastSubmissionTime.Add(time.Duration(bs.batchCfg.MaxSubmissionTime)*time.Second)) || + if (blockHeight == latestHeight && blockTime.After(bs.localBatchInfo.LastSubmissionTime.Add(bs.bridgeInfo.BridgeConfig.SubmissionInterval*2/3))) || + (blockHeight == latestHeight && blockTime.After(bs.localBatchInfo.LastSubmissionTime.Add(time.Duration(bs.batchCfg.MaxSubmissionTime)*time.Second))) || uint64(fileSize) > (bs.batchCfg.MaxChunks-1)*bs.batchCfg.MaxChunkSize { // finalize the batch - bs.batchHeader.End = blockHeight - } + bs.LastBatchEndBlockNumber = blockHeight + bs.localBatchInfo.LastSubmissionTime = blockTime + bs.localBatchInfo.End = blockHeight + err := bs.finalizeBatch(ctx, blockHeight) + if err != nil { + return errors.Wrap(err, "failed to finalize batch") + } + } return nil } -func (bs *BatchSubmitter) batchFileSize() (int64, error) { +func (bs *BatchSubmitter) batchFileSize(flush bool) (int64, error) { if bs.batchFile == nil { return 0, errors.New("batch file is not initialized") } + if flush { + err := bs.batchWriter.Flush() + if err != nil { + return 0, errors.Wrap(err, "failed to flush batch writer") + } + } + info, err := bs.batchFile.Stat() if err != nil { return 0, errors.Wrap(err, "failed to get batch file stat") diff --git a/executor/batch/status.go b/executor/batch/status.go index 8d30b09..172dfb8 100644 --- a/executor/batch/status.go +++ b/executor/batch/status.go @@ -4,25 +4,25 @@ import ( "time" ophosttypes "github.com/initia-labs/OPinit/x/ophost/types" - nodetypes "github.com/initia-labs/opinit-bots-go/node/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" ) type Status struct { Node nodetypes.Status `json:"node"` BatchInfo ophosttypes.BatchInfo `json:"batch_info"` CurrentBatchFileSize int64 `json:"current_batch_file_size"` - LastBatchEndBlockNumber uint64 `json:"last_batch_end_block_number"` + BatchStartBlockNumber uint64 `json:"batch_start_block_number"` + BatchEndBlockNumber uint64 `json:"batch_end_block_number"` LastBatchSubmissionTime time.Time `json:"last_batch_submission_time"` } func (bs BatchSubmitter) GetStatus() Status { - fileSize, _ := bs.batchFileSize() - return Status{ Node: bs.node.GetStatus(), BatchInfo: bs.BatchInfo().BatchInfo, - CurrentBatchFileSize: fileSize, - LastBatchEndBlockNumber: bs.LastBatchEndBlockNumber, - LastBatchSubmissionTime: bs.lastSubmissionTime, + CurrentBatchFileSize: bs.localBatchInfo.BatchFileSize, + BatchStartBlockNumber: bs.localBatchInfo.Start, + BatchEndBlockNumber: bs.localBatchInfo.End, + LastBatchSubmissionTime: bs.localBatchInfo.LastSubmissionTime, } } diff --git a/executor/batch/utils.go b/executor/batch/utils.go index bc90124..ab2301b 100644 --- a/executor/batch/utils.go +++ b/executor/batch/utils.go @@ -9,7 +9,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" opchildtypes "github.com/initia-labs/OPinit/x/opchild/types" - "github.com/initia-labs/opinit-bots-go/txutils" + "github.com/initia-labs/opinit-bots/txutils" ) // prependLength prepends the length of the data to the data. diff --git a/executor/celestia/celestia.go b/executor/celestia/celestia.go index 8f30fa4..7a4ab4c 100644 --- a/executor/celestia/celestia.go +++ b/executor/celestia/celestia.go @@ -16,13 +16,13 @@ import ( inclusion "github.com/celestiaorg/go-square/v2/inclusion" sh "github.com/celestiaorg/go-square/v2/share" - executortypes "github.com/initia-labs/opinit-bots-go/executor/types" - "github.com/initia-labs/opinit-bots-go/keys" - "github.com/initia-labs/opinit-bots-go/node" - btypes "github.com/initia-labs/opinit-bots-go/node/broadcaster/types" - nodetypes "github.com/initia-labs/opinit-bots-go/node/types" - "github.com/initia-labs/opinit-bots-go/types" - celestiatypes "github.com/initia-labs/opinit-bots-go/types/celestia" + executortypes "github.com/initia-labs/opinit-bots/executor/types" + "github.com/initia-labs/opinit-bots/keys" + "github.com/initia-labs/opinit-bots/node" + btypes "github.com/initia-labs/opinit-bots/node/broadcaster/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" + "github.com/initia-labs/opinit-bots/types" + celestiatypes "github.com/initia-labs/opinit-bots/types/celestia" ) type batchNode interface { @@ -38,7 +38,7 @@ type Celestia struct { node *node.Node batch batchNode - bridgeId int64 + bridgeId uint64 namespace sh.Namespace cfg nodetypes.NodeConfig @@ -92,8 +92,8 @@ func createCodec(bech32Prefix string) (codec.Codec, client.TxConfig, error) { }) } -func (c *Celestia) Initialize(batch batchNode, bridgeId int64) error { - err := c.node.Initialize(0) +func (c *Celestia) Initialize(ctx context.Context, batch batchNode, bridgeId uint64) error { + err := c.node.Initialize(ctx, 0) if err != nil { return err } @@ -128,7 +128,7 @@ func (c Celestia) ProcessedMsgsToRawKV(msgs []btypes.ProcessedMsgs, delete bool) return c.node.MustGetBroadcaster().ProcessedMsgsToRawKV(msgs, delete) } -func (c *Celestia) SetBridgeId(brigeId int64) { +func (c *Celestia) SetBridgeId(brigeId uint64) { c.bridgeId = brigeId } diff --git a/executor/celestia/handler.go b/executor/celestia/handler.go index 476eaee..53b211b 100644 --- a/executor/celestia/handler.go +++ b/executor/celestia/handler.go @@ -3,7 +3,7 @@ package celestia import ( "context" - nodetypes "github.com/initia-labs/opinit-bots-go/node/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" "go.uber.org/zap" ) diff --git a/executor/celestia/node.go b/executor/celestia/node.go index a75ca72..7491663 100644 --- a/executor/celestia/node.go +++ b/executor/celestia/node.go @@ -7,9 +7,9 @@ import ( "github.com/cosmos/cosmos-sdk/client/tx" sdk "github.com/cosmos/cosmos-sdk/types" - btypes "github.com/initia-labs/opinit-bots-go/node/broadcaster/types" - "github.com/initia-labs/opinit-bots-go/txutils" - celestiatypes "github.com/initia-labs/opinit-bots-go/types/celestia" + btypes "github.com/initia-labs/opinit-bots/node/broadcaster/types" + "github.com/initia-labs/opinit-bots/txutils" + celestiatypes "github.com/initia-labs/opinit-bots/types/celestia" ) // buildTxWithMessages creates a transaction from the given messages. diff --git a/executor/celestia/status.go b/executor/celestia/status.go index d9885de..0c59bbf 100644 --- a/executor/celestia/status.go +++ b/executor/celestia/status.go @@ -1,7 +1,7 @@ package celestia import ( - nodetypes "github.com/initia-labs/opinit-bots-go/node/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" ) func (c Celestia) GetNodeStatus() nodetypes.Status { diff --git a/executor/child/child.go b/executor/child/child.go index 2856ef8..9a32cff 100644 --- a/executor/child/child.go +++ b/executor/child/child.go @@ -2,27 +2,20 @@ package child import ( "context" - "sync" "time" "go.uber.org/zap" - "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth" - "github.com/initia-labs/OPinit/x/opchild" opchildtypes "github.com/initia-labs/OPinit/x/opchild/types" ophosttypes "github.com/initia-labs/OPinit/x/ophost/types" - executortypes "github.com/initia-labs/opinit-bots-go/executor/types" - "github.com/initia-labs/opinit-bots-go/keys" - "github.com/initia-labs/opinit-bots-go/merkle" - "github.com/initia-labs/opinit-bots-go/node" - btypes "github.com/initia-labs/opinit-bots-go/node/broadcaster/types" - nodetypes "github.com/initia-labs/opinit-bots-go/node/types" - "github.com/initia-labs/opinit-bots-go/types" + btypes "github.com/initia-labs/opinit-bots/node/broadcaster/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" + "github.com/initia-labs/opinit-bots/types" + + childprovider "github.com/initia-labs/opinit-bots/provider/child" ) type hostNode interface { @@ -31,7 +24,7 @@ type hostNode interface { BroadcastMsgs(btypes.ProcessedMsgs) ProcessedMsgsToRawKV([]btypes.ProcessedMsgs, bool) ([]types.RawKV, error) QueryLastOutput(context.Context, uint64) (*ophosttypes.QueryOutputProposalResponse, error) - QueryOutput(context.Context, uint64, uint64) (*ophosttypes.QueryOutputProposalResponse, error) + QueryOutput(context.Context, uint64, uint64, uint64) (*ophosttypes.QueryOutputProposalResponse, error) GetMsgProposeOutput( bridgeId uint64, @@ -42,29 +35,13 @@ type hostNode interface { } type Child struct { - version uint8 + *childprovider.BaseChild - node *node.Node host hostNode - mk *merkle.Merkle - - bridgeInfo opchildtypes.BridgeInfo nextOutputTime time.Time finalizingBlockHeight uint64 - initializeTreeOnce *sync.Once - initializeTreeFn func() error - - cfg nodetypes.NodeConfig - db types.DB - logger *zap.Logger - - opchildQueryClient opchildtypes.QueryClient - - processedMsgs []btypes.ProcessedMsgs - msgQueue []sdk.Msg - // status info lastUpdatedOracleL1Height uint64 lastFinalizedDepositL1BlockHeight uint64 @@ -72,118 +49,29 @@ type Child struct { lastOutputTime time.Time } -func NewChild( - version uint8, cfg nodetypes.NodeConfig, +func NewChildV1( + cfg nodetypes.NodeConfig, db types.DB, logger *zap.Logger, bech32Prefix string, ) *Child { - appCodec, txConfig, err := GetCodec(bech32Prefix) - if err != nil { - panic(err) - } - - node, err := node.NewNode(cfg, db, logger, appCodec, txConfig) - if err != nil { - panic(err) - } - - mk, err := merkle.NewMerkle(db.WithPrefix([]byte(executortypes.MerkleName)), ophosttypes.GenerateNodeHash) - if err != nil { - panic(err) + return &Child{ + BaseChild: childprovider.NewBaseChildV1(cfg, db, logger, bech32Prefix), } - - ch := &Child{ - version: version, - - node: node, - mk: mk, - - initializeTreeOnce: &sync.Once{}, - - cfg: cfg, - db: db, - logger: logger, - - opchildQueryClient: opchildtypes.NewQueryClient(node.GetRPCClient()), - - processedMsgs: make([]btypes.ProcessedMsgs, 0), - msgQueue: make([]sdk.Msg, 0), - } - return ch } -func GetCodec(bech32Prefix string) (codec.Codec, client.TxConfig, error) { - unlock := keys.SetSDKConfigContext(bech32Prefix) - defer unlock() - - return keys.CreateCodec([]keys.RegisterInterfaces{ - auth.AppModuleBasic{}.RegisterInterfaces, - opchild.AppModuleBasic{}.RegisterInterfaces, - }) -} - -func (ch *Child) Initialize(startHeight uint64, startOutputIndex uint64, host hostNode, bridgeInfo opchildtypes.BridgeInfo) error { - err := ch.node.Initialize(startHeight) +func (ch *Child) Initialize(ctx context.Context, startHeight uint64, startOutputIndex uint64, host hostNode, bridgeInfo opchildtypes.BridgeInfo) error { + err := ch.BaseChild.Initialize(ctx, startHeight, startOutputIndex, bridgeInfo) if err != nil { return err } - - if ch.node.HeightInitialized() && startOutputIndex != 0 { - ch.initializeTreeFn = func() error { - ch.logger.Info("initialize tree", zap.Uint64("index", startOutputIndex)) - err := ch.mk.InitializeWorkingTree(startOutputIndex, 1) - if err != nil { - return err - } - return nil - } - } ch.host = host - ch.bridgeInfo = bridgeInfo ch.registerHandlers() return nil } -func (ch *Child) Start(ctx context.Context) { - ch.logger.Info("child start", zap.Uint64("height", ch.node.GetHeight())) - ch.node.Start(ctx) -} - func (ch *Child) registerHandlers() { - ch.node.RegisterBeginBlockHandler(ch.beginBlockHandler) - ch.node.RegisterEventHandler(opchildtypes.EventTypeFinalizeTokenDeposit, ch.finalizeDepositHandler) - ch.node.RegisterEventHandler(opchildtypes.EventTypeUpdateOracle, ch.updateOracleHandler) - ch.node.RegisterEventHandler(opchildtypes.EventTypeInitiateTokenWithdrawal, ch.initiateWithdrawalHandler) - ch.node.RegisterEndBlockHandler(ch.endBlockHandler) -} - -func (ch Child) BroadcastMsgs(msgs btypes.ProcessedMsgs) { - if len(msgs.Msgs) == 0 { - return - } - - ch.node.MustGetBroadcaster().BroadcastMsgs(msgs) -} - -func (ch Child) ProcessedMsgsToRawKV(msgs []btypes.ProcessedMsgs, delete bool) ([]types.RawKV, error) { - if len(msgs) == 0 { - return nil, nil - } - - return ch.node.MustGetBroadcaster().ProcessedMsgsToRawKV(msgs, delete) -} - -func (ch Child) BridgeId() uint64 { - return ch.bridgeInfo.BridgeId -} - -func (ch Child) HasKey() bool { - return ch.node.HasBroadcaster() -} - -func (ch *Child) SetBridgeInfo(bridgeInfo opchildtypes.BridgeInfo) { - ch.bridgeInfo = bridgeInfo -} - -func (ch Child) GetHeight() uint64 { - return ch.node.GetHeight() + ch.Node().RegisterBeginBlockHandler(ch.beginBlockHandler) + ch.Node().RegisterEventHandler(opchildtypes.EventTypeFinalizeTokenDeposit, ch.finalizeDepositHandler) + ch.Node().RegisterEventHandler(opchildtypes.EventTypeUpdateOracle, ch.updateOracleHandler) + ch.Node().RegisterEventHandler(opchildtypes.EventTypeInitiateTokenWithdrawal, ch.initiateWithdrawalHandler) + ch.Node().RegisterEndBlockHandler(ch.endBlockHandler) } diff --git a/executor/child/deposit.go b/executor/child/deposit.go index b306093..df85226 100644 --- a/executor/child/deposit.go +++ b/executor/child/deposit.go @@ -2,52 +2,18 @@ package child import ( "context" - "fmt" - "strconv" - "cosmossdk.io/math" - opchildtypes "github.com/initia-labs/OPinit/x/opchild/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" + childprovider "github.com/initia-labs/opinit-bots/provider/child" + "go.uber.org/zap" sdk "github.com/cosmos/cosmos-sdk/types" - - nodetypes "github.com/initia-labs/opinit-bots-go/node/types" - "go.uber.org/zap" ) func (ch *Child) finalizeDepositHandler(_ context.Context, args nodetypes.EventHandlerArgs) error { - var l1BlockHeight, l1Sequence uint64 - var from, to, baseDenom string - var amount sdk.Coin - var err error - - for _, attr := range args.EventAttributes { - switch attr.Key { - case opchildtypes.AttributeKeyL1Sequence: - l1Sequence, err = strconv.ParseUint(attr.Value, 10, 64) - if err != nil { - return err - } - case opchildtypes.AttributeKeySender: - from = attr.Value - case opchildtypes.AttributeKeyRecipient: - to = attr.Value - case opchildtypes.AttributeKeyDenom: - amount.Denom = attr.Value - case opchildtypes.AttributeKeyBaseDenom: - baseDenom = attr.Value - case opchildtypes.AttributeKeyAmount: - coinAmount, ok := math.NewIntFromString(attr.Value) - if !ok { - return fmt.Errorf("invalid amount %s", attr.Value) - } - - amount.Amount = coinAmount - case opchildtypes.AttributeKeyFinalizeHeight: - l1BlockHeight, err = strconv.ParseUint(attr.Value, 10, 64) - if err != nil { - return err - } - } + l1BlockHeight, l1Sequence, from, to, baseDenom, amount, err := childprovider.ParseFinalizeDeposit(args.EventAttributes) + if err != nil { + return err } ch.handleFinalizeDeposit(l1BlockHeight, l1Sequence, from, to, amount, baseDenom) ch.lastFinalizedDepositL1BlockHeight = l1BlockHeight @@ -56,7 +22,7 @@ func (ch *Child) finalizeDepositHandler(_ context.Context, args nodetypes.EventH } func (ch *Child) handleFinalizeDeposit(l1BlockHeight uint64, l1Sequence uint64, from string, to string, amount sdk.Coin, baseDenom string) { - ch.logger.Info("finalize token deposit", + ch.Logger().Info("finalize token deposit", zap.Uint64("l1_blockHeight", l1BlockHeight), zap.Uint64("l1_sequence", l1Sequence), zap.String("from", from), diff --git a/executor/child/handler.go b/executor/child/handler.go index 11cca8b..0e8d648 100644 --- a/executor/child/handler.go +++ b/executor/child/handler.go @@ -4,17 +4,15 @@ import ( "context" "time" - btypes "github.com/initia-labs/opinit-bots-go/node/broadcaster/types" - nodetypes "github.com/initia-labs/opinit-bots-go/node/types" - "github.com/initia-labs/opinit-bots-go/types" + btypes "github.com/initia-labs/opinit-bots/node/broadcaster/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" + "github.com/initia-labs/opinit-bots/types" ) func (ch *Child) beginBlockHandler(ctx context.Context, args nodetypes.BeginBlockArgs) (err error) { blockHeight := uint64(args.Block.Header.Height) - // just to make sure that childMsgQueue is empty - if blockHeight == args.LatestHeight && len(ch.msgQueue) != 0 && len(ch.processedMsgs) != 0 { - panic("must not happen, msgQueue should be empty") - } + ch.EmptyMsgQueue() + ch.EmptyProcessedMsgs() err = ch.prepareTree(blockHeight) if err != nil { @@ -39,48 +37,38 @@ func (ch *Child) endBlockHandler(_ context.Context, args nodetypes.EndBlockArgs) batchKVs = append(batchKVs, treeKVs...) if storageRoot != nil { - err = ch.handleOutput(blockHeight, ch.version, args.BlockID, ch.mk.GetWorkingTreeIndex(), storageRoot) + err = ch.handleOutput(blockHeight, ch.Version(), args.BlockID, ch.Merkle().GetWorkingTreeIndex(), storageRoot) if err != nil { return err } } - // if we are in sync and we have a small number of messages, less than 10, - // then store the current updates in the database and process the next block. - if blockHeight < args.LatestHeight && len(ch.msgQueue) > 0 && len(ch.msgQueue) <= 10 { - return ch.db.RawBatchSet(batchKVs...) - } - // update the sync info - batchKVs = append(batchKVs, ch.node.SyncInfoToRawKV(blockHeight)) + batchKVs = append(batchKVs, ch.Node().SyncInfoToRawKV(blockHeight)) // if has key, then process the messages if ch.host.HasKey() { - if len(ch.msgQueue) != 0 { - ch.processedMsgs = append(ch.processedMsgs, btypes.ProcessedMsgs{ - Msgs: ch.msgQueue, + if len(ch.GetMsgQueue()) != 0 { + ch.AppendProcessedMsgs(btypes.ProcessedMsgs{ + Msgs: ch.GetMsgQueue(), Timestamp: time.Now().UnixNano(), Save: true, }) } - msgKVs, err := ch.host.ProcessedMsgsToRawKV(ch.processedMsgs, false) + msgKVs, err := ch.host.ProcessedMsgsToRawKV(ch.GetProcessedMsgs(), false) if err != nil { return err } batchKVs = append(batchKVs, msgKVs...) } - err = ch.db.RawBatchSet(batchKVs...) + err = ch.DB().RawBatchSet(batchKVs...) if err != nil { return err } - for _, processedMsg := range ch.processedMsgs { + for _, processedMsg := range ch.GetProcessedMsgs() { ch.host.BroadcastMsgs(processedMsg) } - - ch.msgQueue = ch.msgQueue[:0] - ch.processedMsgs = ch.processedMsgs[:0] - return nil } diff --git a/executor/child/oracle.go b/executor/child/oracle.go index 1165d86..4e34479 100644 --- a/executor/child/oracle.go +++ b/executor/child/oracle.go @@ -2,29 +2,16 @@ package child import ( "context" - "strconv" - opchildtypes "github.com/initia-labs/OPinit/x/opchild/types" - - nodetypes "github.com/initia-labs/opinit-bots-go/node/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" + childprovider "github.com/initia-labs/opinit-bots/provider/child" "go.uber.org/zap" ) func (ch *Child) updateOracleHandler(_ context.Context, args nodetypes.EventHandlerArgs) error { - var l1BlockHeight uint64 - var from string - var err error - - for _, attr := range args.EventAttributes { - switch attr.Key { - case opchildtypes.AttributeKeyHeight: - l1BlockHeight, err = strconv.ParseUint(attr.Value, 10, 64) - if err != nil { - return err - } - case opchildtypes.AttributeKeyFrom: - from = attr.Value - } + l1BlockHeight, from, err := childprovider.ParseUpdateOracle(args.EventAttributes) + if err != nil { + return err } ch.handleUpdateOracle(l1BlockHeight, from) @@ -33,7 +20,7 @@ func (ch *Child) updateOracleHandler(_ context.Context, args nodetypes.EventHand } func (ch *Child) handleUpdateOracle(l1BlockHeight uint64, from string) { - ch.logger.Info("update oracle", + ch.Logger().Info("update oracle", zap.Uint64("l1_blockHeight", l1BlockHeight), zap.String("from", from), ) diff --git a/executor/child/query.go b/executor/child/query.go index 32582ba..601c23e 100644 --- a/executor/child/query.go +++ b/executor/child/query.go @@ -1,69 +1,22 @@ package child import ( - "context" "encoding/json" "cosmossdk.io/math" - opchildtypes "github.com/initia-labs/OPinit/x/opchild/types" sdk "github.com/cosmos/cosmos-sdk/types" - executortypes "github.com/initia-labs/opinit-bots-go/executor/types" - "github.com/initia-labs/opinit-bots-go/node/rpcclient" + executortypes "github.com/initia-labs/opinit-bots/executor/types" ) -func (ch Child) GetAddress() sdk.AccAddress { - return ch.node.MustGetBroadcaster().GetAddress() -} - -func (ch Child) GetAddressStr() (string, error) { - return ch.node.MustGetBroadcaster().GetAddressString() -} - -func (ch Child) QueryBridgeInfo(ctx context.Context) (opchildtypes.BridgeInfo, error) { - req := &opchildtypes.QueryBridgeInfoRequest{} - ctx, cancel := rpcclient.GetQueryContext(ctx, 0) - defer cancel() - - res, err := ch.opchildQueryClient.BridgeInfo(ctx, req) - if err != nil { - return opchildtypes.BridgeInfo{}, err - } - return res.BridgeInfo, nil -} - -func (ch Child) QueryNextL1Sequence(ctx context.Context) (uint64, error) { - req := &opchildtypes.QueryNextL1SequenceRequest{} - ctx, cancel := rpcclient.GetQueryContext(ctx, 0) - defer cancel() - - res, err := ch.opchildQueryClient.NextL1Sequence(ctx, req) - if err != nil { - return 0, err - } - return res.NextL1Sequence, nil -} - -func (ch Child) QueryNextL2Sequence(ctx context.Context, height uint64) (uint64, error) { - req := &opchildtypes.QueryNextL2SequenceRequest{} - ctx, cancel := rpcclient.GetQueryContext(ctx, height) - defer cancel() - - res, err := ch.opchildQueryClient.NextL2Sequence(ctx, req) - if err != nil { - return 0, err - } - return res.NextL2Sequence, nil -} - func (ch Child) QueryWithdrawal(sequence uint64) (executortypes.QueryWithdrawalResponse, error) { withdrawal, err := ch.GetWithdrawal(sequence) if err != nil { return executortypes.QueryWithdrawalResponse{}, err } - proofs, outputIndex, outputRoot, extraDataBytes, err := ch.mk.GetProofs(sequence) + proofs, outputIndex, outputRoot, extraDataBytes, err := ch.Merkle().GetProofs(sequence) if err != nil { return executortypes.QueryWithdrawalResponse{}, err } @@ -83,7 +36,7 @@ func (ch Child) QueryWithdrawal(sequence uint64) (executortypes.QueryWithdrawalR Sender: withdrawal.From, Sequence: sequence, Amount: amount.String(), - Version: []byte{ch.version}, + Version: []byte{ch.Version()}, StorageRoot: outputRoot, LatestBlockHash: treeExtraData.BlockHash, BlockNumber: treeExtraData.BlockNumber, diff --git a/executor/child/status.go b/executor/child/status.go index 07a9c2c..f730bd1 100644 --- a/executor/child/status.go +++ b/executor/child/status.go @@ -3,7 +3,7 @@ package child import ( "time" - nodetypes "github.com/initia-labs/opinit-bots-go/node/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" ) type Status struct { @@ -21,12 +21,12 @@ type Status struct { func (ch Child) GetStatus() Status { return Status{ - Node: ch.node.GetStatus(), + Node: ch.Node().GetStatus(), LastUpdatedOracleL1Height: ch.lastUpdatedOracleL1Height, LastFinalizedDepositL1BlockHeight: ch.lastFinalizedDepositL1BlockHeight, LastFinalizedDepositL1Sequence: ch.lastFinalizedDepositL1Sequence, - LastWithdrawalL2Sequence: ch.mk.GetWorkingTreeLeafCount() + ch.mk.GetStartLeafIndex() - 1, - WorkingTreeIndex: ch.mk.GetWorkingTreeIndex(), + LastWithdrawalL2Sequence: ch.Merkle().GetWorkingTreeLeafCount() + ch.Merkle().GetStartLeafIndex() - 1, + WorkingTreeIndex: ch.Merkle().GetWorkingTreeIndex(), FinalizingBlockHeight: ch.finalizingBlockHeight, LastOutputSubmissionTime: ch.lastOutputTime, NextOutputSubmissionTime: ch.nextOutputTime, diff --git a/executor/child/withdraw.go b/executor/child/withdraw.go index 8470e53..8d7c2f7 100644 --- a/executor/child/withdraw.go +++ b/executor/child/withdraw.go @@ -5,49 +5,24 @@ import ( "encoding/base64" "encoding/json" "fmt" - "strconv" "strings" - "cosmossdk.io/math" - opchildtypes "github.com/initia-labs/OPinit/x/opchild/types" ophosttypes "github.com/initia-labs/OPinit/x/ophost/types" - executortypes "github.com/initia-labs/opinit-bots-go/executor/types" - "github.com/initia-labs/opinit-bots-go/types" + executortypes "github.com/initia-labs/opinit-bots/executor/types" + "github.com/initia-labs/opinit-bots/types" "go.uber.org/zap" cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" - dbtypes "github.com/initia-labs/opinit-bots-go/db/types" - nodetypes "github.com/initia-labs/opinit-bots-go/node/types" + dbtypes "github.com/initia-labs/opinit-bots/db/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" + childprovider "github.com/initia-labs/opinit-bots/provider/child" ) func (ch *Child) initiateWithdrawalHandler(_ context.Context, args nodetypes.EventHandlerArgs) error { - var l2Sequence, amount uint64 - var from, to, baseDenom string - var err error - - for _, attr := range args.EventAttributes { - switch attr.Key { - case opchildtypes.AttributeKeyL2Sequence: - l2Sequence, err = strconv.ParseUint(attr.Value, 10, 64) - if err != nil { - return err - } - case opchildtypes.AttributeKeyFrom: - from = attr.Value - case opchildtypes.AttributeKeyTo: - to = attr.Value - case opchildtypes.AttributeKeyBaseDenom: - baseDenom = attr.Value - case opchildtypes.AttributeKeyAmount: - coinAmount, ok := math.NewIntFromString(attr.Value) - if !ok { - return fmt.Errorf("invalid amount %s", attr.Value) - } - - amount = coinAmount.Uint64() - } + l2Sequence, amount, from, to, baseDenom, err := childprovider.ParseInitiateWithdrawal(args.EventAttributes) + if err != nil { + return err } - return ch.handleInitiateWithdrawal(l2Sequence, from, to, baseDenom, amount) } @@ -69,12 +44,12 @@ func (ch *Child) handleInitiateWithdrawal(l2Sequence uint64, from string, to str } // generate merkle tree - err = ch.mk.InsertLeaf(withdrawalHash[:]) + err = ch.Merkle().InsertLeaf(withdrawalHash[:]) if err != nil { return err } - ch.logger.Info("initiate token withdrawal", + ch.Logger().Info("initiate token withdrawal", zap.Uint64("l2_sequence", l2Sequence), zap.String("from", from), zap.String("to", to), @@ -86,25 +61,12 @@ func (ch *Child) handleInitiateWithdrawal(l2Sequence uint64, from string, to str return nil } -func (ch *Child) initializeTree() (executed bool) { - ch.initializeTreeOnce.Do(func() { - if ch.initializeTreeFn != nil { - executed = true - err := ch.initializeTreeFn() - if err != nil { - panic("failed to initialize working tree: " + err.Error()) - } - } - }) - return executed -} - func (ch *Child) prepareTree(blockHeight uint64) error { - if ch.initializeTree() { + if ch.InitializeTree(blockHeight) { return nil } - err := ch.mk.LoadWorkingTree(blockHeight - 1) + err := ch.Merkle().LoadWorkingTree(blockHeight - 1) if err == dbtypes.ErrNotFound { // must not happened panic(fmt.Errorf("working tree not found at height: %d, current: %d", blockHeight-1, blockHeight)) @@ -116,20 +78,20 @@ func (ch *Child) prepareTree(blockHeight uint64) error { } func (ch *Child) prepareOutput(ctx context.Context) error { - workingOutputIndex := ch.mk.GetWorkingTreeIndex() + workingOutputIndex := ch.Merkle().GetWorkingTreeIndex() // initialize next output time if ch.nextOutputTime.IsZero() && workingOutputIndex > 1 { - output, err := ch.host.QueryOutput(ctx, ch.BridgeId(), workingOutputIndex-1) + output, err := ch.host.QueryOutput(ctx, ch.BridgeId(), workingOutputIndex-1, 0) if err != nil { // TODO: maybe not return error here and roll back return fmt.Errorf("output does not exist at index: %d", workingOutputIndex-1) } ch.lastOutputTime = output.OutputProposal.L1BlockTime - ch.nextOutputTime = output.OutputProposal.L1BlockTime.Add(ch.bridgeInfo.BridgeConfig.SubmissionInterval * 2 / 3) + ch.nextOutputTime = output.OutputProposal.L1BlockTime.Add(ch.BridgeInfo().BridgeConfig.SubmissionInterval * 2 / 3) } - output, err := ch.host.QueryOutput(ctx, ch.BridgeId(), ch.mk.GetWorkingTreeIndex()) + output, err := ch.host.QueryOutput(ctx, ch.BridgeId(), ch.Merkle().GetWorkingTreeIndex(), 0) if err != nil { if strings.Contains(err.Error(), "collections: not found") { return nil @@ -163,15 +125,15 @@ func (ch *Child) handleTree(blockHeight uint64, latestHeight uint64, blockId []b return nil, nil, err } - kvs, storageRoot, err = ch.mk.FinalizeWorkingTree(data) + kvs, storageRoot, err = ch.Merkle().FinalizeWorkingTree(data) if err != nil { return nil, nil, err } - ch.logger.Info("finalize working tree", - zap.Uint64("tree_index", ch.mk.GetWorkingTreeIndex()), + ch.Logger().Info("finalize working tree", + zap.Uint64("tree_index", ch.Merkle().GetWorkingTreeIndex()), zap.Uint64("height", blockHeight), - zap.Uint64("num_leaves", ch.mk.GetWorkingTreeLeafCount()), + zap.Uint64("num_leaves", ch.Merkle().GetWorkingTreeLeafCount()), zap.String("storage_root", base64.StdEncoding.EncodeToString(storageRoot)), ) @@ -182,10 +144,10 @@ func (ch *Child) handleTree(blockHeight uint64, latestHeight uint64, blockId []b ch.finalizingBlockHeight = 0 ch.lastOutputTime = blockHeader.Time - ch.nextOutputTime = blockHeader.Time.Add(ch.bridgeInfo.BridgeConfig.SubmissionInterval * 2 / 3) + ch.nextOutputTime = blockHeader.Time.Add(ch.BridgeInfo().BridgeConfig.SubmissionInterval * 2 / 3) } - err = ch.mk.SaveWorkingTree(blockHeight) + err = ch.Merkle().SaveWorkingTree(blockHeight) if err != nil { return nil, nil, err } @@ -204,13 +166,13 @@ func (ch *Child) handleOutput(blockHeight uint64, version uint8, blockId []byte, if err != nil { return err } - ch.msgQueue = append(ch.msgQueue, msg) + ch.AppendMsgQueue(msg) return nil } // GetWithdrawal returns the withdrawal data for the given sequence from the database func (ch *Child) GetWithdrawal(sequence uint64) (executortypes.WithdrawalData, error) { - dataBytes, err := ch.db.Get(executortypes.PrefixedWithdrawalKey(sequence)) + dataBytes, err := ch.DB().Get(executortypes.PrefixedWithdrawalKey(sequence)) if err != nil { return executortypes.WithdrawalData{}, err } @@ -226,5 +188,5 @@ func (ch *Child) SetWithdrawal(sequence uint64, data executortypes.WithdrawalDat return err } - return ch.db.Set(executortypes.PrefixedWithdrawalKey(sequence), dataBytes) + return ch.DB().Set(executortypes.PrefixedWithdrawalKey(sequence), dataBytes) } diff --git a/executor/executor.go b/executor/executor.go index 26e1e68..8af5776 100644 --- a/executor/executor.go +++ b/executor/executor.go @@ -8,17 +8,18 @@ import ( "github.com/pkg/errors" "github.com/gofiber/fiber/v2" - "github.com/initia-labs/opinit-bots-go/executor/batch" - "github.com/initia-labs/opinit-bots-go/executor/celestia" - "github.com/initia-labs/opinit-bots-go/executor/child" - "github.com/initia-labs/opinit-bots-go/executor/host" - "github.com/initia-labs/opinit-bots-go/server" + "github.com/initia-labs/opinit-bots/executor/batch" + "github.com/initia-labs/opinit-bots/executor/celestia" + "github.com/initia-labs/opinit-bots/executor/child" + "github.com/initia-labs/opinit-bots/executor/host" + "github.com/initia-labs/opinit-bots/server" - bottypes "github.com/initia-labs/opinit-bots-go/bot/types" - executortypes "github.com/initia-labs/opinit-bots-go/executor/types" + bottypes "github.com/initia-labs/opinit-bots/bot/types" + executortypes "github.com/initia-labs/opinit-bots/executor/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-go/types" + "github.com/initia-labs/opinit-bots/types" "go.uber.org/zap" ) @@ -47,20 +48,20 @@ func NewExecutor(cfg *executortypes.Config, db types.DB, sv *server.Server, logg } return &Executor{ - host: host.NewHost( - cfg.Version, cfg.RelayOracle, cfg.L1NodeConfig(homePath), - db.WithPrefix([]byte(executortypes.HostNodeName)), - logger.Named(executortypes.HostNodeName), cfg.L1Node.Bech32Prefix, "", + host: host.NewHostV1( + cfg.L1NodeConfig(homePath), + db.WithPrefix([]byte(types.HostName)), + logger.Named(types.HostName), cfg.L1Node.Bech32Prefix, "", ), - child: child.NewChild( - cfg.Version, cfg.L2NodeConfig(homePath), - db.WithPrefix([]byte(executortypes.ChildNodeName)), - logger.Named(executortypes.ChildNodeName), cfg.L2Node.Bech32Prefix, + child: child.NewChildV1( + cfg.L2NodeConfig(homePath), + db.WithPrefix([]byte(types.ChildName)), + logger.Named(types.ChildName), cfg.L2Node.Bech32Prefix, ), - batch: batch.NewBatchSubmitter( - cfg.Version, cfg.L2NodeConfig(homePath), - cfg.BatchConfig(), db.WithPrefix([]byte(executortypes.BatchNodeName)), - logger.Named(executortypes.BatchNodeName), cfg.L2Node.ChainID, homePath, + batch: batch.NewBatchSubmitterV0( + cfg.L2NodeConfig(homePath), + cfg.BatchConfig(), db.WithPrefix([]byte(types.BatchName)), + logger.Named(types.BatchName), cfg.L2Node.ChainID, homePath, cfg.L2Node.Bech32Prefix, ), @@ -93,11 +94,11 @@ func (ex *Executor) Initialize(ctx context.Context) error { return err } - err = ex.host.Initialize(ctx, hostStartHeight, ex.child, ex.batch, int64(bridgeInfo.BridgeId)) + err = ex.host.Initialize(ctx, hostStartHeight, ex.child, ex.batch, bridgeInfo) if err != nil { return err } - err = ex.child.Initialize(childStartHeight, startOutputIndex, ex.host, bridgeInfo) + err = ex.child.Initialize(ctx, childStartHeight, startOutputIndex, ex.host, bridgeInfo) if err != nil { return err } @@ -106,7 +107,7 @@ func (ex *Executor) Initialize(ctx context.Context) error { return err } - da, err := ex.makeDANode(int64(bridgeInfo.BridgeId)) + da, err := ex.makeDANode(ctx, bridgeInfo) if err != nil { return err } @@ -167,29 +168,28 @@ func (ex *Executor) RegisterQuerier() { }) } -func (ex *Executor) makeDANode(bridgeId int64) (executortypes.DANode, error) { +func (ex *Executor) makeDANode(ctx context.Context, bridgeInfo opchildtypes.BridgeInfo) (executortypes.DANode, error) { batchInfo := ex.batch.BatchInfo() switch batchInfo.BatchInfo.ChainType { case ophosttypes.BatchInfo_CHAIN_TYPE_INITIA: - da := host.NewHost( - ex.cfg.Version, false, ex.cfg.DANodeConfig(ex.homePath), - ex.db.WithPrefix([]byte(executortypes.DAHostNodeName)), - ex.logger.Named(executortypes.DAHostNodeName), + da := host.NewHostV1( + ex.cfg.DANodeConfig(ex.homePath), + ex.db.WithPrefix([]byte(types.DAHostName)), + ex.logger.Named(types.DAHostName), ex.cfg.DANode.Bech32Prefix, batchInfo.BatchInfo.Submitter, ) if ex.host.GetAddress().Equals(da.GetAddress()) { return ex.host, nil } - da.SetBridgeId(bridgeId) - da.RegisterDAHandlers() - return da, nil + err := da.InitializeDA(ctx, bridgeInfo) + return da, err case ophosttypes.BatchInfo_CHAIN_TYPE_CELESTIA: da := celestia.NewDACelestia(ex.cfg.Version, ex.cfg.DANodeConfig(ex.homePath), - ex.db.WithPrefix([]byte(executortypes.DACelestiaNodeName)), - ex.logger.Named(executortypes.DACelestiaNodeName), + ex.db.WithPrefix([]byte(types.DACelestiaName)), + ex.logger.Named(types.DACelestiaName), ex.cfg.DANode.Bech32Prefix, batchInfo.BatchInfo.Submitter, ) - err := da.Initialize(ex.batch, bridgeId) + err := da.Initialize(ctx, ex.batch, bridgeInfo.BridgeId) if err != nil { return nil, err } @@ -219,17 +219,20 @@ func (ex *Executor) getStartHeights(ctx context.Context, bridgeId uint64) (l1Sta } } // get the last deposit tx height from the host - l1Sequence, err := ex.child.QueryNextL1Sequence(ctx) - if err != nil { - return 0, 0, 0, 0, err - } - depositTxHeight, err := ex.host.QueryDepositTxHeight(ctx, bridgeId, l1Sequence-1) + l1Sequence, err := ex.child.QueryNextL1Sequence(ctx, 0) if err != nil { return 0, 0, 0, 0, err } - if l1StartHeight > depositTxHeight { - l1StartHeight = depositTxHeight + if l1Sequence > 1 { + depositTxHeight, err := ex.host.QueryDepositTxHeight(ctx, bridgeId, l1Sequence-1) + if err != nil { + return 0, 0, 0, 0, err + } + if l1StartHeight > depositTxHeight { + l1StartHeight = depositTxHeight + } } + if l2StartHeight == 0 { startOutputIndex = 1 } diff --git a/executor/host/batch.go b/executor/host/batch.go index d1f5402..c45cf3b 100644 --- a/executor/host/batch.go +++ b/executor/host/batch.go @@ -2,67 +2,41 @@ package host import ( "context" - "strconv" - ophosttypes "github.com/initia-labs/OPinit/x/ophost/types" - nodetypes "github.com/initia-labs/opinit-bots-go/node/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" + hostprovider "github.com/initia-labs/opinit-bots/provider/host" "go.uber.org/zap" ) func (h *Host) recordBatchHandler(_ context.Context, args nodetypes.EventHandlerArgs) error { - var submitter string - for _, attr := range args.EventAttributes { - switch attr.Key { - case ophosttypes.AttributeKeySubmitter: - submitter = attr.Value - hostAddress, err := h.GetAddressStr() - if err != nil { - return nil - } - if submitter != hostAddress { - return nil - } - } + submitter, err := hostprovider.ParseMsgRecordBatch(args.EventAttributes) + if err != nil { + return err } - h.logger.Info("record batch", + hostAddress, err := h.GetAddressStr() + if err != nil { + return nil + } + if submitter != hostAddress { + return nil + } + h.Logger().Info("record batch", zap.String("submitter", submitter), ) return nil } func (h *Host) updateBatchInfoHandler(_ context.Context, args nodetypes.EventHandlerArgs) error { - var bridgeId uint64 - var submitter, chain string - var outputIndex, l2BlockNumber uint64 - var err error - for _, attr := range args.EventAttributes { - switch attr.Key { - case ophosttypes.AttributeKeyBridgeId: - bridgeId, err = strconv.ParseUint(attr.Value, 10, 64) - if err != nil { - return err - } - if bridgeId != uint64(h.bridgeId) { - // pass other bridge deposit event - return nil - } - case ophosttypes.AttributeKeyBatchChainType: - chain = attr.Value - case ophosttypes.AttributeKeyBatchSubmitter: - submitter = attr.Value - case ophosttypes.AttributeKeyFinalizedOutputIndex: - outputIndex, err = strconv.ParseUint(attr.Value, 10, 64) - if err != nil { - return err - } - case ophosttypes.AttributeKeyFinalizedL2BlockNumber: - l2BlockNumber, err = strconv.ParseUint(attr.Value, 10, 64) - if err != nil { - return err - } - } + bridgeId, submitter, chain, outputIndex, l2BlockNumber, err := hostprovider.ParseMsgUpdateBatchInfo(args.EventAttributes) + if err != nil { + return err } - h.logger.Info("update batch info", + if bridgeId != h.BridgeId() { + // pass other bridge deposit event + return nil + } + + h.Logger().Info("update batch info", zap.String("chain", chain), zap.String("submitter", submitter), zap.Uint64("output_index", outputIndex), diff --git a/executor/host/deposit.go b/executor/host/deposit.go index f822c42..f24868f 100644 --- a/executor/host/deposit.go +++ b/executor/host/deposit.go @@ -2,56 +2,23 @@ package host import ( "context" - "encoding/hex" "errors" - "strconv" "cosmossdk.io/math" - ophosttypes "github.com/initia-labs/OPinit/x/ophost/types" - nodetypes "github.com/initia-labs/opinit-bots-go/node/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" + hostprovider "github.com/initia-labs/opinit-bots/provider/host" sdk "github.com/cosmos/cosmos-sdk/types" ) func (h *Host) initiateDepositHandler(_ context.Context, args nodetypes.EventHandlerArgs) error { - var bridgeId uint64 - var l1Sequence uint64 - var from, to, l1Denom, l2Denom, amount string - var data []byte - var err error - - for _, attr := range args.EventAttributes { - switch attr.Key { - case ophosttypes.AttributeKeyBridgeId: - bridgeId, err = strconv.ParseUint(attr.Value, 10, 64) - if err != nil { - return err - } - if bridgeId != uint64(h.bridgeId) { - // pass other bridge deposit event - return nil - } - case ophosttypes.AttributeKeyL1Sequence: - l1Sequence, err = strconv.ParseUint(attr.Value, 10, 64) - if err != nil { - return err - } - case ophosttypes.AttributeKeyFrom: - from = attr.Value - case ophosttypes.AttributeKeyTo: - to = attr.Value - case ophosttypes.AttributeKeyL1Denom: - l1Denom = attr.Value - case ophosttypes.AttributeKeyL2Denom: - l2Denom = attr.Value - case ophosttypes.AttributeKeyAmount: - amount = attr.Value - case ophosttypes.AttributeKeyData: - data, err = hex.DecodeString(attr.Value) - if err != nil { - return err - } - } + bridgeId, l1Sequence, from, to, l1Denom, l2Denom, amount, data, err := hostprovider.ParseMsgInitiateDeposit(args.EventAttributes) + if err != nil { + return err + } + if bridgeId != h.BridgeId() { + // pass other bridge deposit event + return nil } if l1Sequence < h.initialL1Sequence { // pass old deposit event @@ -72,7 +39,7 @@ func (h *Host) initiateDepositHandler(_ context.Context, args nodetypes.EventHan return err } - h.msgQueue = append(h.msgQueue, msg) + h.AppendMsgQueue(msg) return nil } diff --git a/executor/host/handler.go b/executor/host/handler.go index 1c3e89e..d183628 100644 --- a/executor/host/handler.go +++ b/executor/host/handler.go @@ -4,61 +4,55 @@ import ( "context" "time" - "github.com/initia-labs/opinit-bots-go/types" + "github.com/initia-labs/opinit-bots/types" sdk "github.com/cosmos/cosmos-sdk/types" - btypes "github.com/initia-labs/opinit-bots-go/node/broadcaster/types" - nodetypes "github.com/initia-labs/opinit-bots-go/node/types" + btypes "github.com/initia-labs/opinit-bots/node/broadcaster/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" ) func (h *Host) beginBlockHandler(_ context.Context, args nodetypes.BeginBlockArgs) error { - blockHeight := uint64(args.Block.Header.Height) - // just to make sure that childMsgQueue is empty - if blockHeight == args.LatestHeight && len(h.msgQueue) != 0 && len(h.processedMsgs) != 0 { - panic("must not happen, msgQueue should be empty") - } + h.EmptyMsgQueue() + h.EmptyProcessedMsgs() return nil } func (h *Host) endBlockHandler(_ context.Context, args nodetypes.EndBlockArgs) error { - // temporary 50 limit for msg queue // collect more msgs if block height is not latest blockHeight := uint64(args.Block.Header.Height) - if blockHeight != args.LatestHeight && len(h.msgQueue) > 0 && len(h.msgQueue) <= 10 { + msgQueue := h.GetMsgQueue() + if blockHeight != args.LatestHeight && len(msgQueue) > 0 && len(msgQueue) <= 10 { return nil } batchKVs := []types.RawKV{ - h.node.SyncInfoToRawKV(blockHeight), + h.Node().SyncInfoToRawKV(blockHeight), } - if h.node.HasBroadcaster() { - if len(h.msgQueue) != 0 { - h.processedMsgs = append(h.processedMsgs, btypes.ProcessedMsgs{ - Msgs: h.msgQueue, + if h.Node().HasBroadcaster() { + if len(msgQueue) != 0 { + h.AppendProcessedMsgs(btypes.ProcessedMsgs{ + Msgs: msgQueue, Timestamp: time.Now().UnixNano(), Save: true, }) } - msgkvs, err := h.child.ProcessedMsgsToRawKV(h.processedMsgs, false) + msgkvs, err := h.child.ProcessedMsgsToRawKV(h.GetProcessedMsgs(), false) if err != nil { return err } batchKVs = append(batchKVs, msgkvs...) } - err := h.db.RawBatchSet(batchKVs...) + err := h.DB().RawBatchSet(batchKVs...) if err != nil { return err } - for _, processedMsg := range h.processedMsgs { + for _, processedMsg := range h.GetProcessedMsgs() { h.child.BroadcastMsgs(processedMsg) } - - h.msgQueue = h.msgQueue[:0] - h.processedMsgs = h.processedMsgs[:0] return nil } @@ -67,7 +61,7 @@ func (h *Host) txHandler(_ context.Context, args nodetypes.TxHandlerArgs) error if msg, err := h.oracleTxHandler(args.BlockHeight, args.Tx); err != nil { return err } else if msg != nil { - h.processedMsgs = append(h.processedMsgs, btypes.ProcessedMsgs{ + h.AppendProcessedMsgs(btypes.ProcessedMsgs{ Msgs: []sdk.Msg{msg}, Timestamp: time.Now().UnixNano(), Save: false, diff --git a/executor/host/host.go b/executor/host/host.go index 9809e23..4f0fe12 100644 --- a/executor/host/host.go +++ b/executor/host/host.go @@ -5,20 +5,17 @@ import ( "go.uber.org/zap" - "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth" - "github.com/initia-labs/OPinit/x/ophost" + opchildtypes "github.com/initia-labs/OPinit/x/opchild/types" ophosttypes "github.com/initia-labs/OPinit/x/ophost/types" - executortypes "github.com/initia-labs/opinit-bots-go/executor/types" - "github.com/initia-labs/opinit-bots-go/keys" - "github.com/initia-labs/opinit-bots-go/node" - btypes "github.com/initia-labs/opinit-bots-go/node/broadcaster/types" - nodetypes "github.com/initia-labs/opinit-bots-go/node/types" - "github.com/initia-labs/opinit-bots-go/types" + executortypes "github.com/initia-labs/opinit-bots/executor/types" + btypes "github.com/initia-labs/opinit-bots/node/broadcaster/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" + "github.com/initia-labs/opinit-bots/types" + + hostprovider "github.com/initia-labs/opinit-bots/provider/host" ) type childNode interface { @@ -26,7 +23,7 @@ type childNode interface { HasKey() bool BroadcastMsgs(btypes.ProcessedMsgs) ProcessedMsgsToRawKV([]btypes.ProcessedMsgs, bool) ([]types.RawKV, error) - QueryNextL1Sequence(context.Context) (uint64, error) + QueryNextL1Sequence(context.Context, uint64) (uint64, error) GetMsgFinalizeTokenDeposit(string, string, sdk.Coin, uint64, uint64, string, []byte) (sdk.Msg, error) GetMsgUpdateOracle( @@ -42,144 +39,66 @@ type batchNode interface { var _ executortypes.DANode = &Host{} type Host struct { - version uint8 - relayOracle bool + *hostprovider.BaseHost - node *node.Node child childNode batch batchNode - bridgeId int64 initialL1Sequence uint64 - cfg nodetypes.NodeConfig - db types.DB - logger *zap.Logger - - ophostQueryClient ophosttypes.QueryClient - - processedMsgs []btypes.ProcessedMsgs - msgQueue []sdk.Msg - // status info lastProposedOutputIndex uint64 lastProposedOutputL2BlockNumber uint64 } -func NewHost( - version uint8, relayOracle bool, cfg nodetypes.NodeConfig, +func NewHostV1( + cfg nodetypes.NodeConfig, db types.DB, logger *zap.Logger, bech32Prefix, batchSubmitter string, ) *Host { - appCodec, txConfig, err := GetCodec(bech32Prefix) - if err != nil { - panic(err) - } - if batchSubmitter != "" { cfg.BroadcasterConfig.Bech32Prefix = bech32Prefix cfg.BroadcasterConfig.KeyringConfig.Address = batchSubmitter } - - node, err := node.NewNode(cfg, db, logger, appCodec, txConfig) - if err != nil { - panic(err) + return &Host{ + BaseHost: hostprovider.NewBaseHostV1(cfg, db, logger, bech32Prefix), } - - h := &Host{ - version: version, - relayOracle: relayOracle, - - node: node, - - cfg: cfg, - db: db, - logger: logger, - - ophostQueryClient: ophosttypes.NewQueryClient(node.GetRPCClient()), - - processedMsgs: make([]btypes.ProcessedMsgs, 0), - msgQueue: make([]sdk.Msg, 0), - } - - return h } -func GetCodec(bech32Prefix string) (codec.Codec, client.TxConfig, error) { - unlock := keys.SetSDKConfigContext(bech32Prefix) - defer unlock() - - return keys.CreateCodec([]keys.RegisterInterfaces{ - auth.AppModuleBasic{}.RegisterInterfaces, - ophost.AppModuleBasic{}.RegisterInterfaces, - }) -} - -func (h *Host) Initialize(ctx context.Context, startHeight uint64, child childNode, batch batchNode, bridgeId int64) error { - err := h.node.Initialize(startHeight) +func (h *Host) Initialize(ctx context.Context, startHeight uint64, child childNode, batch batchNode, bridgeInfo opchildtypes.BridgeInfo) error { + err := h.BaseHost.Initialize(ctx, startHeight, bridgeInfo) if err != nil { return err } h.child = child h.batch = batch - h.bridgeId = bridgeId - - h.initialL1Sequence, err = h.child.QueryNextL1Sequence(ctx) + h.initialL1Sequence, err = h.child.QueryNextL1Sequence(ctx, 0) if err != nil { return err } - h.registerHandlers() return nil } -func (h *Host) Start(ctx context.Context) { - h.logger.Info("host start", zap.Uint64("height", h.node.GetHeight())) - h.node.Start(ctx) -} - -func (h *Host) registerHandlers() { - h.node.RegisterBeginBlockHandler(h.beginBlockHandler) - h.node.RegisterTxHandler(h.txHandler) - h.node.RegisterEventHandler(ophosttypes.EventTypeInitiateTokenDeposit, h.initiateDepositHandler) - h.node.RegisterEventHandler(ophosttypes.EventTypeProposeOutput, h.proposeOutputHandler) - h.node.RegisterEventHandler(ophosttypes.EventTypeFinalizeTokenWithdrawal, h.finalizeWithdrawalHandler) - h.node.RegisterEventHandler(ophosttypes.EventTypeRecordBatch, h.recordBatchHandler) - h.node.RegisterEventHandler(ophosttypes.EventTypeUpdateBatchInfo, h.updateBatchInfoHandler) - h.node.RegisterEndBlockHandler(h.endBlockHandler) -} - -func (h *Host) RegisterDAHandlers() { - h.node.RegisterEventHandler(ophosttypes.EventTypeRecordBatch, h.recordBatchHandler) -} - -func (h Host) BroadcastMsgs(msgs btypes.ProcessedMsgs) { - if len(msgs.Msgs) == 0 { - return - } - - h.node.MustGetBroadcaster().BroadcastMsgs(msgs) -} - -func (h Host) ProcessedMsgsToRawKV(msgs []btypes.ProcessedMsgs, delete bool) ([]types.RawKV, error) { - if len(msgs) == 0 { - return nil, nil +func (h *Host) InitializeDA(ctx context.Context, bridgeInfo opchildtypes.BridgeInfo) error { + err := h.BaseHost.Initialize(ctx, 0, bridgeInfo) + if err != nil { + return err } - - return h.node.MustGetBroadcaster().ProcessedMsgsToRawKV(msgs, delete) -} - -func (h *Host) SetBridgeId(bridgeId int64) { - h.bridgeId = bridgeId -} - -func (h Host) BridgeId() int64 { - return h.bridgeId + h.registerDAHandlers() + return nil } -func (h Host) HasKey() bool { - return h.node.HasBroadcaster() +func (h *Host) registerHandlers() { + h.Node().RegisterBeginBlockHandler(h.beginBlockHandler) + h.Node().RegisterTxHandler(h.txHandler) + h.Node().RegisterEventHandler(ophosttypes.EventTypeInitiateTokenDeposit, h.initiateDepositHandler) + h.Node().RegisterEventHandler(ophosttypes.EventTypeProposeOutput, h.proposeOutputHandler) + h.Node().RegisterEventHandler(ophosttypes.EventTypeFinalizeTokenWithdrawal, h.finalizeWithdrawalHandler) + h.Node().RegisterEventHandler(ophosttypes.EventTypeRecordBatch, h.recordBatchHandler) + h.Node().RegisterEventHandler(ophosttypes.EventTypeUpdateBatchInfo, h.updateBatchInfoHandler) + h.Node().RegisterEndBlockHandler(h.endBlockHandler) } -func (h Host) GetHeight() uint64 { - return h.node.GetHeight() +func (h *Host) registerDAHandlers() { + h.Node().RegisterEventHandler(ophosttypes.EventTypeRecordBatch, h.recordBatchHandler) } diff --git a/executor/host/oracle.go b/executor/host/oracle.go index 9455632..c94f0bb 100644 --- a/executor/host/oracle.go +++ b/executor/host/oracle.go @@ -9,7 +9,7 @@ import ( // If the relay oracle is enabled and the extended commit info contains votes, create a new MsgUpdateOracle message. // Else return nil. func (h *Host) oracleTxHandler(blockHeight uint64, extCommitBz comettypes.Tx) (sdk.Msg, error) { - if !h.relayOracle { + if !h.OracleEnabled() { return nil, nil } return h.child.GetMsgUpdateOracle( diff --git a/executor/host/status.go b/executor/host/status.go index 4e9f3e7..23ec787 100644 --- a/executor/host/status.go +++ b/executor/host/status.go @@ -1,7 +1,7 @@ package host import ( - nodetypes "github.com/initia-labs/opinit-bots-go/node/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" ) type Status struct { @@ -12,12 +12,12 @@ type Status struct { func (h Host) GetStatus() Status { return Status{ - Node: h.node.GetStatus(), + Node: h.GetNodeStatus(), LastProposedOutputIndex: h.lastProposedOutputIndex, LastProposedOutputL2BlockNumber: h.lastProposedOutputL2BlockNumber, } } func (h Host) GetNodeStatus() nodetypes.Status { - return h.node.GetStatus() + return h.Node().GetStatus() } diff --git a/executor/host/withdraw.go b/executor/host/withdraw.go index 9e1da92..91fe844 100644 --- a/executor/host/withdraw.go +++ b/executor/host/withdraw.go @@ -3,49 +3,20 @@ package host import ( "context" "encoding/base64" - "encoding/hex" - "strconv" - ophosttypes "github.com/initia-labs/OPinit/x/ophost/types" - nodetypes "github.com/initia-labs/opinit-bots-go/node/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" + hostprovider "github.com/initia-labs/opinit-bots/provider/host" "go.uber.org/zap" ) func (h *Host) proposeOutputHandler(_ context.Context, args nodetypes.EventHandlerArgs) error { - var bridgeId, l2BlockNumber, outputIndex uint64 - var proposer string - var outputRoot []byte - var err error - - for _, attr := range args.EventAttributes { - switch attr.Key { - case ophosttypes.AttributeKeyProposer: - proposer = attr.Value - case ophosttypes.AttributeKeyBridgeId: - bridgeId, err = strconv.ParseUint(attr.Value, 10, 64) - if err != nil { - return err - } - if bridgeId != uint64(h.bridgeId) { - // pass other bridge output proposal event - return nil - } - case ophosttypes.AttributeKeyOutputIndex: - outputIndex, err = strconv.ParseUint(attr.Value, 10, 64) - if err != nil { - return err - } - case ophosttypes.AttributeKeyL2BlockNumber: - l2BlockNumber, err = strconv.ParseUint(attr.Value, 10, 64) - if err != nil { - return err - } - case ophosttypes.AttributeKeyOutputRoot: - outputRoot, err = hex.DecodeString(attr.Value) - if err != nil { - return err - } - } + bridgeId, l2BlockNumber, outputIndex, proposer, outputRoot, err := hostprovider.ParseMsgProposeOutput(args.EventAttributes) + if err != nil { + return err + } + if bridgeId != h.BridgeId() { + // pass other bridge output proposal event + return nil } h.handleProposeOutput(bridgeId, proposer, outputIndex, l2BlockNumber, outputRoot) @@ -55,7 +26,7 @@ func (h *Host) proposeOutputHandler(_ context.Context, args nodetypes.EventHandl } func (h *Host) handleProposeOutput(bridgeId uint64, proposer string, outputIndex uint64, l2BlockNumber uint64, outputRoot []byte) { - h.logger.Info("propose output", + h.Logger().Info("propose output", zap.Uint64("bridge_id", bridgeId), zap.String("proposer", proposer), zap.Uint64("output_index", outputIndex), @@ -65,50 +36,21 @@ func (h *Host) handleProposeOutput(bridgeId uint64, proposer string, outputIndex } func (h *Host) finalizeWithdrawalHandler(_ context.Context, args nodetypes.EventHandlerArgs) error { - var bridgeId uint64 - var outputIndex, l2Sequence uint64 - var from, to, l1Denom, l2Denom, amount string - var err error - - for _, attr := range args.EventAttributes { - switch attr.Key { - case ophosttypes.AttributeKeyBridgeId: - bridgeId, err = strconv.ParseUint(attr.Value, 10, 64) - if err != nil { - return err - } - if bridgeId != uint64(h.bridgeId) { - // pass other bridge withdrawal event - return nil - } - case ophosttypes.AttributeKeyOutputIndex: - outputIndex, err = strconv.ParseUint(attr.Value, 10, 64) - if err != nil { - return err - } - case ophosttypes.AttributeKeyL2Sequence: - l2Sequence, err = strconv.ParseUint(attr.Value, 10, 64) - if err != nil { - return err - } - case ophosttypes.AttributeKeyFrom: - from = attr.Value - case ophosttypes.AttributeKeyTo: - to = attr.Value - case ophosttypes.AttributeKeyL1Denom: - l1Denom = attr.Value - case ophosttypes.AttributeKeyL2Denom: - l2Denom = attr.Value - case ophosttypes.AttributeKeyAmount: - amount = attr.Value - } + bridgeId, outputIndex, l2Sequence, from, to, l1Denom, l2Denom, amount, err := hostprovider.ParseMsgFinalizeWithdrawal(args.EventAttributes) + if err != nil { + return err } + if bridgeId != h.BridgeId() { + // pass other bridge withdrawal event + return nil + } + h.handleFinalizeWithdrawal(bridgeId, outputIndex, l2Sequence, from, to, l1Denom, l2Denom, amount) return nil } func (h *Host) handleFinalizeWithdrawal(bridgeId uint64, outputIndex uint64, l2Sequence uint64, from string, to string, l1Denom string, l2Denom string, amount string) { - h.logger.Info("finalize withdrawal", + h.Logger().Info("finalize withdrawal", zap.Uint64("bridge_id", bridgeId), zap.Uint64("output_index", outputIndex), zap.Uint64("l2_sequence", l2Sequence), diff --git a/executor/host/withdraw_test.go b/executor/host/withdraw_test.go index 8d72135..06cd206 100644 --- a/executor/host/withdraw_test.go +++ b/executor/host/withdraw_test.go @@ -3,8 +3,8 @@ package host // import ( // "testing" -// "github.com/initia-labs/opinit-bots-go/db" -// "github.com/initia-labs/opinit-bots-go/node/types" +// "github.com/initia-labs/opinit-bots/db" +// "github.com/initia-labs/opinit-bots/node/types" // "github.com/stretchr/testify/require" // "go.uber.org/zap" // "go.uber.org/zap/zaptest/observer" diff --git a/executor/status.go b/executor/status.go index 388d07a..ba8d9bd 100644 --- a/executor/status.go +++ b/executor/status.go @@ -1,14 +1,14 @@ package executor import ( - "github.com/initia-labs/opinit-bots-go/executor/batch" - "github.com/initia-labs/opinit-bots-go/executor/child" - "github.com/initia-labs/opinit-bots-go/executor/host" - nodetypes "github.com/initia-labs/opinit-bots-go/node/types" + "github.com/initia-labs/opinit-bots/executor/batch" + "github.com/initia-labs/opinit-bots/executor/child" + "github.com/initia-labs/opinit-bots/executor/host" + nodetypes "github.com/initia-labs/opinit-bots/node/types" ) type Status struct { - BridgeId int64 `json:"bridge_id"` + BridgeId uint64 `json:"bridge_id"` Host host.Status `json:"host,omitempty"` Child child.Status `json:"child,omitempty"` Batch batch.Status `json:"batch,omitempty"` diff --git a/executor/types/batch.go b/executor/types/batch.go index 1b4d8f6..a584463 100644 --- a/executor/types/batch.go +++ b/executor/types/batch.go @@ -2,10 +2,14 @@ package types import ( "context" + "crypto/sha256" + "encoding/binary" + "fmt" + "time" - btypes "github.com/initia-labs/opinit-bots-go/node/broadcaster/types" - nodetypes "github.com/initia-labs/opinit-bots-go/node/types" - "github.com/initia-labs/opinit-bots-go/types" + btypes "github.com/initia-labs/opinit-bots/node/broadcaster/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" + "github.com/initia-labs/opinit-bots/types" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -19,10 +23,122 @@ type DANode interface { GetNodeStatus() nodetypes.Status } -// BatchHeader is the header of a batch -type BatchHeader struct { +type LocalBatchInfo struct { + // start l2 block height which is included in the batch + Start uint64 `json:"start"` // last l2 block height which is included in the batch End uint64 `json:"end"` - // checksums of all chunks - Chunks [][]byte `json:"chunks"` + + LastSubmissionTime time.Time `json:"last_submission_time"` + BatchFileSize int64 `json:"batch_size"` +} + +type BatchDataType uint8 + +const ( + BatchDataTypeHeader BatchDataType = iota + BatchDataTypeChunk +) + +type BatchDataHeader struct { + Start uint64 + End uint64 + Checksums [][]byte +} + +type BatchDataChunk struct { + Start uint64 + End uint64 + Index uint64 + Length uint64 + ChunkData []byte +} + +func GetChecksumFromChunk(chunk []byte) [32]byte { + return sha256.Sum256(chunk) +} + +func MarshalBatchDataHeader( + start uint64, + end uint64, + checksums [][]byte, +) []byte { + data := make([]byte, 1) + data[0] = byte(BatchDataTypeHeader) + data = binary.BigEndian.AppendUint64(data, start) + data = binary.BigEndian.AppendUint64(data, end) + data = binary.BigEndian.AppendUint64(data, uint64(len(checksums))) + for _, checksum := range checksums { + data = append(data, checksum...) + } + return data +} + +func UnmarshalBatchDataHeader(data []byte) (BatchDataHeader, error) { + if len(data) < 25 { + err := fmt.Errorf("invalid data length: %d, expected > 25", len(data)) + return BatchDataHeader{}, err + } + start := binary.BigEndian.Uint64(data[1:9]) + end := binary.BigEndian.Uint64(data[9:17]) + if start > end { + return BatchDataHeader{}, fmt.Errorf("invalid start: %d, end: %d", start, end) + } + + length := binary.BigEndian.Uint64(data[17:25]) + if (len(data)-25)%32 != 0 || (uint64(len(data)-25)/32) != length { + err := fmt.Errorf("invalid checksum length: %d, data length: %d", length, len(data)-25) + return BatchDataHeader{}, err + } + + checksums := make([][]byte, 0, length) + for i := 25; i < len(data); i += 32 { + checksums = append(checksums, data[i:i+32]) + } + + return BatchDataHeader{ + Start: start, + End: end, + Checksums: checksums, + }, nil +} + +func MarshalBatchDataChunk( + start uint64, + end uint64, + index uint64, + length uint64, + chunkData []byte, +) []byte { + data := make([]byte, 1) + data[0] = byte(BatchDataTypeChunk) + data = binary.BigEndian.AppendUint64(data, start) + data = binary.BigEndian.AppendUint64(data, end) + data = binary.BigEndian.AppendUint64(data, index) + data = binary.BigEndian.AppendUint64(data, length) + data = append(data, chunkData...) + return data +} + +func UnmarshalBatchDataChunk(data []byte) (BatchDataChunk, error) { + if len(data) < 33 { + err := fmt.Errorf("invalid data length: %d, expected > 33", len(data)) + return BatchDataChunk{}, err + } + start := binary.BigEndian.Uint64(data[1:9]) + end := binary.BigEndian.Uint64(data[9:17]) + if start > end { + return BatchDataChunk{}, fmt.Errorf("invalid start: %d, end: %d", start, end) + } + index := binary.BigEndian.Uint64(data[17:25]) + length := binary.BigEndian.Uint64(data[25:33]) + chunkData := data[33:] + + return BatchDataChunk{ + Start: start, + End: end, + Index: index, + Length: length, + ChunkData: chunkData, + }, nil } diff --git a/executor/types/batch_test.go b/executor/types/batch_test.go new file mode 100644 index 0000000..72b3002 --- /dev/null +++ b/executor/types/batch_test.go @@ -0,0 +1,38 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestBatchDataHeader(t *testing.T) { + start := uint64(1) + end := uint64(100) + + chunks := [][]byte{ + []byte("chunk1"), + []byte("chunk2"), + []byte("chunk3"), + } + + checksums := make([][]byte, 0, len(chunks)) + for _, chunk := range chunks { + checksum := GetChecksumFromChunk(chunk) + checksums = append(checksums, checksum[:]) + } + + headerData := MarshalBatchDataHeader( + start, + end, + checksums) + require.Equal(t, 1+8+8+8+3*32, len(headerData)) + + header, err := UnmarshalBatchDataHeader(headerData) + require.NoError(t, err) + + require.Equal(t, start, header.Start) + require.Equal(t, end, header.End) + require.Equal(t, checksums, header.Checksums) + require.Equal(t, len(chunks), len(header.Checksums)) +} diff --git a/executor/types/config.go b/executor/types/config.go index f6254e5..b10a601 100644 --- a/executor/types/config.go +++ b/executor/types/config.go @@ -4,8 +4,8 @@ import ( "errors" "time" - btypes "github.com/initia-labs/opinit-bots-go/node/broadcaster/types" - nodetypes "github.com/initia-labs/opinit-bots-go/node/types" + btypes "github.com/initia-labs/opinit-bots/node/broadcaster/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" ) type NodeConfig struct { @@ -56,9 +56,6 @@ type Config struct { // If you don't want to use the bridge executor feature, you can leave it empty. BridgeExecutor string `json:"bridge_executor"` - // RelayOracle is the flag to enable the oracle relay feature. - RelayOracle bool `json:"relay_oracle"` - // MaxChunks is the maximum number of chunks in a batch. MaxChunks uint64 `json:"max_chunks"` // MaxChunkSize is the maximum size of a chunk in a batch. @@ -111,8 +108,6 @@ func DefaultConfig() *Config { OutputSubmitter: "", BridgeExecutor: "", - RelayOracle: true, - MaxChunks: 5000, MaxChunkSize: 300000, // 300KB MaxSubmissionTime: 60 * 60, // 1 hour @@ -127,6 +122,10 @@ func (cfg Config) Validate() error { return errors.New("version is required") } + if cfg.Version != 1 { + return errors.New("only version 1 is supported") + } + if cfg.ListenAddress == "" { return errors.New("listen address is required") } diff --git a/executor/types/const.go b/executor/types/const.go deleted file mode 100644 index 7fc4166..0000000 --- a/executor/types/const.go +++ /dev/null @@ -1,11 +0,0 @@ -package types - -const ( - HostNodeName = "host" - ChildNodeName = "child" - BatchNodeName = "batch" - MerkleName = "merkle" - - DAHostNodeName = "da_host" - DACelestiaNodeName = "da_celestia" -) diff --git a/executor/types/key.go b/executor/types/key.go index b98d969..7ffa3bd 100644 --- a/executor/types/key.go +++ b/executor/types/key.go @@ -1,7 +1,7 @@ package types import ( - dbtypes "github.com/initia-labs/opinit-bots-go/db/types" + dbtypes "github.com/initia-labs/opinit-bots/db/types" ) var ( diff --git a/go.mod b/go.mod index 351c4a0..97a834f 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,13 @@ -module github.com/initia-labs/opinit-bots-go +module github.com/initia-labs/opinit-bots go 1.22.5 require ( github.com/celestiaorg/go-square/v2 v2.0.0-rc0 github.com/cometbft/cometbft v0.38.10 - github.com/cosmos/cosmos-sdk v0.50.8 + github.com/cosmos/cosmos-sdk v0.50.9 github.com/gofiber/fiber/v2 v2.52.5 - github.com/initia-labs/OPinit v0.4.2 + github.com/initia-labs/OPinit v0.4.3 github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 @@ -25,8 +25,7 @@ require ( github.com/cosmos/ibc-go/v8 v8.4.0 // indirect github.com/cosmos/interchain-security/v5 v5.0.0 // indirect github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect - github.com/go-playground/validator/v10 v10.20.0 // indirect - github.com/goccy/go-json v0.10.2 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect github.com/gogo/googleapis v1.4.1 // indirect github.com/golang/mock v1.6.0 // indirect github.com/google/orderedcode v0.0.1 // indirect @@ -38,17 +37,16 @@ require ( github.com/hashicorp/yamux v0.1.1 // indirect github.com/huandu/skiplist v1.2.0 // indirect github.com/improbable-eng/grpc-web v0.15.0 // indirect - github.com/initia-labs/OPinit/api v0.4.1 // indirect + github.com/initia-labs/OPinit/api v0.4.3 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/minio/highwayhash v1.0.2 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/oklog/run v1.1.0 // indirect - github.com/opencontainers/runc v1.1.12 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/rs/cors v1.8.3 // indirect github.com/skip-mev/slinky v1.0.5 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect @@ -58,13 +56,13 @@ require ( require ( cosmossdk.io/api v0.7.5 // indirect cosmossdk.io/collections v0.4.0 // indirect - cosmossdk.io/core v0.11.0 + cosmossdk.io/core v0.11.1 cosmossdk.io/depinject v1.0.0 // indirect cosmossdk.io/errors v1.0.1 cosmossdk.io/log v1.3.1 // indirect cosmossdk.io/math v1.3.0 cosmossdk.io/store v1.1.0 // indirect - cosmossdk.io/x/tx v0.13.3 + cosmossdk.io/x/tx v0.13.4 filippo.io/edwards25519 v1.0.0 // indirect github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect github.com/99designs/keyring v1.2.2 // indirect diff --git a/go.sum b/go.sum index 613bbc8..46e3222 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,8 @@ cosmossdk.io/client/v2 v2.0.0-beta.1.0.20240124105859-5ad1805d0e79 h1:Hr1t0fCq1n cosmossdk.io/client/v2 v2.0.0-beta.1.0.20240124105859-5ad1805d0e79/go.mod h1:8pN6LSVReNnIxrC2QGcvuIJ/m1pJN6FNYn2kAYtYftI= cosmossdk.io/collections v0.4.0 h1:PFmwj2W8szgpD5nOd8GWH6AbYNi1f2J6akWXJ7P5t9s= cosmossdk.io/collections v0.4.0/go.mod h1:oa5lUING2dP+gdDquow+QjlF45eL1t4TJDypgGd+tv0= -cosmossdk.io/core v0.11.0 h1:vtIafqUi+1ZNAE/oxLOQQ7Oek2n4S48SWLG8h/+wdbo= -cosmossdk.io/core v0.11.0/go.mod h1:LaTtayWBSoacF5xNzoF8tmLhehqlA9z1SWiPuNC6X1w= +cosmossdk.io/core v0.11.1 h1:h9WfBey7NAiFfIcUhDVNS503I2P2HdZLebJlUIs8LPA= +cosmossdk.io/core v0.11.1/go.mod h1:OJzxcdC+RPrgGF8NJZR2uoQr56tc7gfBKhiKeDO7hH0= cosmossdk.io/depinject v1.0.0 h1:dQaTu6+O6askNXO06+jyeUAnF2/ssKwrrszP9t5q050= cosmossdk.io/depinject v1.0.0/go.mod h1:zxK/h3HgHoA/eJVtiSsoaRaRA2D5U4cJ5thIG4ssbB8= cosmossdk.io/errors v1.0.1 h1:bzu+Kcr0kS/1DuPBtUFdWjzLqyUuCiyHjyJB6srBV/0= @@ -33,8 +33,8 @@ cosmossdk.io/x/evidence v0.1.0 h1:J6OEyDl1rbykksdGynzPKG5R/zm6TacwW2fbLTW4nCk= cosmossdk.io/x/evidence v0.1.0/go.mod h1:hTaiiXsoiJ3InMz1uptgF0BnGqROllAN8mwisOMMsfw= cosmossdk.io/x/feegrant v0.1.0 h1:c7s3oAq/8/UO0EiN1H5BIjwVntujVTkYs35YPvvrdQk= cosmossdk.io/x/feegrant v0.1.0/go.mod h1:4r+FsViJRpcZif/yhTn+E0E6OFfg4n0Lx+6cCtnZElU= -cosmossdk.io/x/tx v0.13.3 h1:Ha4mNaHmxBc6RMun9aKuqul8yHiL78EKJQ8g23Zf73g= -cosmossdk.io/x/tx v0.13.3/go.mod h1:I8xaHv0rhUdIvIdptKIqzYy27+n2+zBVaxO6fscFhys= +cosmossdk.io/x/tx v0.13.4 h1:Eg0PbJgeO0gM8p5wx6xa0fKR7hIV6+8lC56UrsvSo0Y= +cosmossdk.io/x/tx v0.13.4/go.mod h1:BkFqrnGGgW50Y6cwTy+JvgAhiffbGEKW6KF9ufcDpvk= cosmossdk.io/x/upgrade v0.1.3 h1:q4XpXc6zp0dX6x74uBtfN6+J7ikaQev5Bla6Q0ADLK8= cosmossdk.io/x/upgrade v0.1.3/go.mod h1:jOdQhnaY5B8CDUoUbed23/Lre0Dk+r6BMQE40iKlVVQ= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= @@ -175,8 +175,8 @@ github.com/cosmos/cosmos-db v1.0.2 h1:hwMjozuY1OlJs/uh6vddqnk9j7VamLv+0DBlbEXbAK github.com/cosmos/cosmos-db v1.0.2/go.mod h1:Z8IXcFJ9PqKK6BIsVOB3QXtkKoqUOp1vRvPT39kOXEA= github.com/cosmos/cosmos-proto v1.0.0-beta.5 h1:eNcayDLpip+zVLRLYafhzLvQlSmyab+RC5W7ZfmxJLA= github.com/cosmos/cosmos-proto v1.0.0-beta.5/go.mod h1:hQGLpiIUloJBMdQMMWb/4wRApmI9hjHH05nefC0Ojec= -github.com/cosmos/cosmos-sdk v0.50.8 h1:2UJHssUaGHTl4/dFp8xyREKAnfiRU6VVfqtKG9n8w5g= -github.com/cosmos/cosmos-sdk v0.50.8/go.mod h1:Zb+DgHtiByNwgj71IlJBXwOq6dLhtyAq3AgqpXm/jHo= +github.com/cosmos/cosmos-sdk v0.50.9 h1:gt2usjz0H0qW6KwAxWw7ZJ3XU8uDwmhN+hYG3nTLeSg= +github.com/cosmos/cosmos-sdk v0.50.9/go.mod h1:TMH6wpoYBcg7Cp5BEg8fneLr+8XloNQkf2MRNF9V6JE= github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY= github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw= github.com/cosmos/gogogateway v1.2.0 h1:Ae/OivNhp8DqBi/sh2A8a1D0y638GpL3tkmLQAiKxTE= @@ -301,8 +301,8 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= -github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= -github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= @@ -313,8 +313,8 @@ github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA= github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= +github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -476,10 +476,10 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= -github.com/initia-labs/OPinit v0.4.2 h1:cfE6LXb3EquDK1/UTDT62o9c2HOQL0E9pWTr5BWAf8g= -github.com/initia-labs/OPinit v0.4.2/go.mod h1:bM+tav+ER4uC6U84PB5vgnRUNyjc/phNgHGYQX9ALhg= -github.com/initia-labs/OPinit/api v0.4.1 h1:Q8etW92LiwekKZxzDYVFdiHF3uOpEA4nyajy8zpcxB0= -github.com/initia-labs/OPinit/api v0.4.1/go.mod h1:Xy/Nt3ubXLQ4zKn0m7RuQOM1sj8TVdlNNyek21TGYR0= +github.com/initia-labs/OPinit v0.4.3 h1:tFpcT9qeOLS49tFdEeK9ACEibeCEYd+V4Oz69gQPvp8= +github.com/initia-labs/OPinit v0.4.3/go.mod h1:1bf2//8NDHa2geXj81wm+2tLOW7zD6PQiGR6eakim00= +github.com/initia-labs/OPinit/api v0.4.3 h1:qljFODGw3F2ClGgJD4uiw1QXb3Px9tJX3jqWolPco/Q= +github.com/initia-labs/OPinit/api v0.4.3/go.mod h1:NorLLEBESDeLPQIzTFIT2XjvD/tkS1VRE6YL59TXYT0= github.com/jhump/protoreflect v1.15.3 h1:6SFRuqU45u9hIZPJAoZ8c28T3nK64BNdp9w6jFonzls= github.com/jhump/protoreflect v1.15.3/go.mod h1:4ORHmSBmlCW8fh3xHmJMGyul1zNqZK4Elxc8qKP+p1k= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= @@ -614,8 +614,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= -github.com/opencontainers/runc v1.1.12 h1:BOIssBaW1La0/qbNZHXOOa71dZfZEQOzW7dqQf3phss= -github.com/opencontainers/runc v1.1.12/go.mod h1:S+lQwSfncpBha7XTy/5lBwWgm5+y5Ma/O44Ekby9FK8= +github.com/opencontainers/runc v1.1.3 h1:vIXrkId+0/J2Ymu2m7VjGvbSlAId9XNRPhn2p4b+d8w= +github.com/opencontainers/runc v1.1.3/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= @@ -791,11 +791,12 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= diff --git a/merkle/merkle.go b/merkle/merkle.go index 6e1c8fa..1aad2e2 100644 --- a/merkle/merkle.go +++ b/merkle/merkle.go @@ -7,8 +7,8 @@ import ( "fmt" "math/bits" - merkletypes "github.com/initia-labs/opinit-bots-go/merkle/types" - types "github.com/initia-labs/opinit-bots-go/types" + merkletypes "github.com/initia-labs/opinit-bots/merkle/types" + types "github.com/initia-labs/opinit-bots/types" ) // NodeGeneratorFn is a function type that generates parent node from two child nodes. @@ -61,10 +61,6 @@ func NewMerkle(db types.DB, nodeGeneratorFn NodeGeneratorFn) (*Merkle, error) { // InitializeWorkingTree resets the working tree with the given tree index and start leaf index. func (m *Merkle) InitializeWorkingTree(treeIndex uint64, startLeafIndex uint64) error { - if m.workingTree != nil && !m.workingTree.Done { - return fmt.Errorf("failed to initialize working tree index: %d; working tree is not finalized", treeIndex) - } - if treeIndex < 1 || startLeafIndex < 1 { return fmt.Errorf("failed to initialize working tree index: %d, leaf: %d; invalid index", treeIndex, startLeafIndex) } @@ -128,6 +124,7 @@ func (m *Merkle) LoadWorkingTree(version uint64) error { var workingTree merkletypes.TreeInfo err = json.Unmarshal(data, &workingTree) + m.workingTree = &workingTree if err != nil { return err } else if workingTree.Done { @@ -135,8 +132,6 @@ func (m *Merkle) LoadWorkingTree(version uint64) error { nextStartLeafIndex := workingTree.StartLeafIndex + workingTree.LeafCount return m.InitializeWorkingTree(nextTreeIndex, nextStartLeafIndex) } - - m.workingTree = &workingTree return nil } diff --git a/merkle/merkle_test.go b/merkle/merkle_test.go index 9d5ca7d..20462b9 100644 --- a/merkle/merkle_test.go +++ b/merkle/merkle_test.go @@ -7,8 +7,8 @@ import ( "golang.org/x/crypto/sha3" ophosttypes "github.com/initia-labs/OPinit/x/ophost/types" - "github.com/initia-labs/opinit-bots-go/db" - merkletypes "github.com/initia-labs/opinit-bots-go/merkle/types" + "github.com/initia-labs/opinit-bots/db" + merkletypes "github.com/initia-labs/opinit-bots/merkle/types" "github.com/stretchr/testify/require" ) diff --git a/merkle/types/key.go b/merkle/types/key.go index 23ca518..1865a24 100644 --- a/merkle/types/key.go +++ b/merkle/types/key.go @@ -3,7 +3,7 @@ package types import ( "encoding/binary" - dbtypes "github.com/initia-labs/opinit-bots-go/db/types" + dbtypes "github.com/initia-labs/opinit-bots/db/types" ) var ( diff --git a/node/broadcaster/account.go b/node/broadcaster/account.go index 231d9d2..c741a00 100644 --- a/node/broadcaster/account.go +++ b/node/broadcaster/account.go @@ -13,7 +13,7 @@ import ( grpctypes "github.com/cosmos/cosmos-sdk/types/grpc" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - "github.com/initia-labs/opinit-bots-go/keys" + "github.com/initia-labs/opinit-bots/keys" ) var _ client.AccountRetriever = &Broadcaster{} diff --git a/node/broadcaster/broadcaster.go b/node/broadcaster/broadcaster.go index 53a7b73..d325373 100644 --- a/node/broadcaster/broadcaster.go +++ b/node/broadcaster/broadcaster.go @@ -1,6 +1,7 @@ package broadcaster import ( + "context" "sync" "time" @@ -17,9 +18,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/tx/signing" - btypes "github.com/initia-labs/opinit-bots-go/node/broadcaster/types" - "github.com/initia-labs/opinit-bots-go/node/rpcclient" - "github.com/initia-labs/opinit-bots-go/types" + btypes "github.com/initia-labs/opinit-bots/node/broadcaster/types" + "github.com/initia-labs/opinit-bots/node/rpcclient" + "github.com/initia-labs/opinit-bots/types" ) type Broadcaster struct { @@ -56,7 +57,6 @@ func NewBroadcaster( cdc codec.Codec, txConfig client.TxConfig, rpcClient *rpcclient.RPCClient, - status *rpccoretypes.ResultStatus, ) (*Broadcaster, error) { b := &Broadcaster{ cdc: cdc, @@ -108,14 +108,12 @@ func NewBroadcaster( } b.keyAddress = addr b.keyName = keyringRecord.Name + return b, nil +} +func (b *Broadcaster) Initialize(ctx context.Context, status *rpccoretypes.ResultStatus) error { // prepare broadcaster - err = b.prepareBroadcaster(uint64(status.SyncInfo.LatestBlockHeight), status.SyncInfo.LatestBlockTime) - if err != nil { - return nil, err - } - - return b, nil + return b.prepareBroadcaster(ctx, status.SyncInfo.LatestBlockTime) } func (b Broadcaster) getClientCtx() client.Context { @@ -130,7 +128,7 @@ func (b Broadcaster) GetTxf() tx.Factory { return b.txf } -func (b *Broadcaster) prepareBroadcaster(_ /*lastBlockHeight*/ uint64, lastBlockTime time.Time) error { +func (b *Broadcaster) prepareBroadcaster(ctx context.Context, lastBlockTime time.Time) error { b.txf = tx.Factory{}. WithAccountRetriever(b). WithChainID(b.cfg.ChainID). @@ -152,7 +150,6 @@ func (b *Broadcaster) prepareBroadcaster(_ /*lastBlockHeight*/ uint64, lastBlock return err } - // TODO: handle mismatched sequence & pending txs if len(loadedPendingTxs) > 0 { pendingTxTime := time.Unix(0, loadedPendingTxs[0].Timestamp) @@ -161,7 +158,11 @@ func (b *Broadcaster) prepareBroadcaster(_ /*lastBlockHeight*/ uint64, lastBlock waitingTime := timeoutTime.Sub(lastBlockTime) timer := time.NewTimer(waitingTime) b.logger.Info("waiting for pending txs to be processed", zap.Duration("waiting_time", waitingTime)) - <-timer.C + select { + case <-ctx.Done(): + return ctx.Err() + case <-timer.C: + } } // convert pending txs to raw kv pairs for deletion diff --git a/node/broadcaster/db.go b/node/broadcaster/db.go index 7ea0d4e..9c00383 100644 --- a/node/broadcaster/db.go +++ b/node/broadcaster/db.go @@ -3,8 +3,8 @@ package broadcaster import ( "go.uber.org/zap" - btypes "github.com/initia-labs/opinit-bots-go/node/broadcaster/types" - "github.com/initia-labs/opinit-bots-go/types" + btypes "github.com/initia-labs/opinit-bots/node/broadcaster/types" + "github.com/initia-labs/opinit-bots/types" ) /////////////// diff --git a/node/broadcaster/process.go b/node/broadcaster/process.go index f7a5b55..f713855 100644 --- a/node/broadcaster/process.go +++ b/node/broadcaster/process.go @@ -11,7 +11,7 @@ import ( rpccoretypes "github.com/cometbft/cometbft/rpc/core/types" - btypes "github.com/initia-labs/opinit-bots-go/node/broadcaster/types" + btypes "github.com/initia-labs/opinit-bots/node/broadcaster/types" ) func (b Broadcaster) GetHeight() uint64 { @@ -52,29 +52,38 @@ func (b *Broadcaster) HandleNewBlock(block *rpccoretypes.ResultBlock, blockResul } // CheckPendingTx query tx info to check if pending tx is processed. -func (b *Broadcaster) CheckPendingTx(ctx context.Context) (*btypes.PendingTxInfo, *rpccoretypes.ResultTx, error) { +func (b *Broadcaster) CheckPendingTx(ctx context.Context) (*btypes.PendingTxInfo, *rpccoretypes.ResultTx, time.Time, error) { if b.LenLocalPendingTx() == 0 { - return nil, nil, nil + return nil, nil, time.Time{}, nil } pendingTx := b.peekLocalPendingTx() pendingTxTime := time.Unix(0, b.peekLocalPendingTx().Timestamp) - if time.Now().After(pendingTxTime.Add(b.cfg.TxTimeout)) { + + lastBlockResult, err := b.rpcClient.Block(ctx, nil) + if err != nil { + return nil, nil, time.Time{}, err + } + if lastBlockResult.Block.Time.After(pendingTxTime.Add(b.cfg.TxTimeout)) { // @sh-cha: should we rebroadcast pending txs? or rasing monitoring alert? panic(fmt.Errorf("something wrong, pending txs are not processed for a long time; current block time: %s, pending tx processing time: %s", time.Now().UTC().String(), pendingTxTime.UTC().String())) } txHash, err := hex.DecodeString(pendingTx.TxHash) if err != nil { - return nil, nil, err + return nil, nil, time.Time{}, err } res, err := b.rpcClient.QueryTx(ctx, txHash) if err != nil { b.logger.Debug("failed to query tx", zap.String("tx_hash", pendingTx.TxHash), zap.String("error", err.Error())) - return nil, nil, nil + return nil, nil, time.Time{}, nil } - return &pendingTx, res, nil + blockResult, err := b.rpcClient.Block(ctx, &res.Height) + if err != nil { + return nil, nil, time.Time{}, err + } + return &pendingTx, res, blockResult.Block.Time, nil } // RemovePendingTx remove pending tx from local pending txs. diff --git a/node/broadcaster/tx.go b/node/broadcaster/tx.go index 6bfa698..eb2ef15 100644 --- a/node/broadcaster/tx.go +++ b/node/broadcaster/tx.go @@ -20,8 +20,8 @@ import ( txtypes "github.com/cosmos/cosmos-sdk/types/tx" "github.com/cosmos/cosmos-sdk/types/tx/signing" - btypes "github.com/initia-labs/opinit-bots-go/node/broadcaster/types" - "github.com/initia-labs/opinit-bots-go/txutils" + btypes "github.com/initia-labs/opinit-bots/node/broadcaster/types" + "github.com/initia-labs/opinit-bots/txutils" opchildtypes "github.com/initia-labs/OPinit/x/opchild/types" ) diff --git a/node/broadcaster/types/config.go b/node/broadcaster/types/config.go index d1fbd63..c8206fd 100644 --- a/node/broadcaster/types/config.go +++ b/node/broadcaster/types/config.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - "github.com/initia-labs/opinit-bots-go/keys" + "github.com/initia-labs/opinit-bots/keys" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/crypto/keyring" diff --git a/node/broadcaster/types/keys.go b/node/broadcaster/types/keys.go index 9d8d690..c004e60 100644 --- a/node/broadcaster/types/keys.go +++ b/node/broadcaster/types/keys.go @@ -1,7 +1,7 @@ package types import ( - dbtypes "github.com/initia-labs/opinit-bots-go/db/types" + dbtypes "github.com/initia-labs/opinit-bots/db/types" ) var ( diff --git a/node/db.go b/node/db.go index 2f32a47..0e3dcb6 100644 --- a/node/db.go +++ b/node/db.go @@ -1,9 +1,9 @@ package node import ( - dbtypes "github.com/initia-labs/opinit-bots-go/db/types" - nodetypes "github.com/initia-labs/opinit-bots-go/node/types" - "github.com/initia-labs/opinit-bots-go/types" + dbtypes "github.com/initia-labs/opinit-bots/db/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" + "github.com/initia-labs/opinit-bots/types" "go.uber.org/zap" ) diff --git a/node/node.go b/node/node.go index 7726aba..b28b232 100644 --- a/node/node.go +++ b/node/node.go @@ -7,10 +7,10 @@ import ( "github.com/pkg/errors" "cosmossdk.io/core/address" - "github.com/initia-labs/opinit-bots-go/node/broadcaster" - "github.com/initia-labs/opinit-bots-go/node/rpcclient" - nodetypes "github.com/initia-labs/opinit-bots-go/node/types" - "github.com/initia-labs/opinit-bots-go/types" + "github.com/initia-labs/opinit-bots/node/broadcaster" + "github.com/initia-labs/opinit-bots/node/rpcclient" + nodetypes "github.com/initia-labs/opinit-bots/node/types" + "github.com/initia-labs/opinit-bots/types" "go.uber.org/zap" "golang.org/x/sync/errgroup" @@ -64,14 +64,6 @@ func NewNode(cfg nodetypes.NodeConfig, db types.DB, logger *zap.Logger, cdc code cdc: cdc, txConfig: txConfig, } - // check if node is catching up - status, err := n.rpcClient.Status(context.Background()) - if err != nil { - return nil, err - } - if status.SyncInfo.CatchingUp { - return nil, errors.New("node is catching up") - } // create broadcaster if n.cfg.BroadcasterConfig != nil { n.broadcaster, err = broadcaster.NewBroadcaster( @@ -81,19 +73,34 @@ func NewNode(cfg nodetypes.NodeConfig, db types.DB, logger *zap.Logger, cdc code n.cdc, n.txConfig, n.rpcClient, - status, ) if err != nil { return nil, errors.Wrap(err, "failed to create broadcaster") } } + return n, nil } // StartHeight is the height to start processing. // If it is 0, the latest height is used. // If the latest height exists in the database, this is ignored. -func (n *Node) Initialize(startHeight uint64) error { +func (n *Node) Initialize(ctx context.Context, startHeight uint64) (err error) { + // check if node is catching up + status, err := n.rpcClient.Status(ctx) + if err != nil { + return err + } + if status.SyncInfo.CatchingUp { + return errors.New("node is catching up") + } + if n.broadcaster != nil { + err = n.broadcaster.Initialize(ctx, status) + if err != nil { + return err + } + } + // load sync info return n.loadSyncInfo(startHeight) } @@ -192,11 +199,11 @@ func (n Node) MustGetBroadcaster() *broadcaster.Broadcaster { func (n Node) GetStatus() nodetypes.Status { s := nodetypes.Status{} if n.cfg.ProcessType != nodetypes.PROCESS_TYPE_ONLY_BROADCAST { - s.LastProcessedBlockHeight = n.GetHeight() + s.LastBlockHeight = n.GetHeight() } if n.broadcaster != nil { - s.Broadcaster = nodetypes.BroadcasterStatus{ + s.Broadcaster = &nodetypes.BroadcasterStatus{ PendingTxs: n.broadcaster.LenLocalPendingTx(), Sequence: n.broadcaster.GetTxf().Sequence(), } diff --git a/node/process.go b/node/process.go index 04e8f27..e2618c7 100644 --- a/node/process.go +++ b/node/process.go @@ -7,8 +7,9 @@ import ( abcitypes "github.com/cometbft/cometbft/abci/types" rpccoretypes "github.com/cometbft/cometbft/rpc/core/types" - nodetypes "github.com/initia-labs/opinit-bots-go/node/types" - "github.com/initia-labs/opinit-bots-go/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" + "github.com/initia-labs/opinit-bots/types" + "github.com/pkg/errors" "go.uber.org/zap" ) @@ -53,8 +54,15 @@ func (n *Node) blockProcessLooper(ctx context.Context, processType nodetypes.Blo err = n.handleNewBlock(ctx, block, blockResult, latestChainHeight) if err != nil { - // TODO: handle error n.logger.Error("failed to handle new block", zap.String("error", err.Error())) + if errors.Is(err, nodetypes.ErrIgnoreAndTryLater) { + sleep := time.NewTimer(time.Minute) + select { + case <-ctx.Done(): + return nil + case <-sleep.C: + } + } break } n.lastProcessedBlockHeight = queryHeight @@ -81,8 +89,9 @@ func (n *Node) blockProcessLooper(ctx context.Context, processType nodetypes.Blo default: } err := n.rawBlockHandler(ctx, nodetypes.RawBlockArgs{ - BlockHeight: i, - BlockBytes: blockBulk[i-start], + BlockHeight: i, + LatestHeight: latestChainHeight, + BlockBytes: blockBulk[i-start], }) if err != nil { n.logger.Error("failed to handle raw block", zap.String("error", err.Error())) @@ -140,6 +149,7 @@ func (n *Node) handleNewBlock(ctx context.Context, block *rpccoretypes.ResultBlo if n.txHandler != nil { err := n.txHandler(ctx, nodetypes.TxHandlerArgs{ BlockHeight: uint64(block.Block.Height), + BlockTime: block.Block.Time, LatestHeight: latestChainHeight, TxIndex: uint64(txIndex), Tx: tx, @@ -152,7 +162,7 @@ func (n *Node) handleNewBlock(ctx context.Context, block *rpccoretypes.ResultBlo if len(n.eventHandlers) != 0 { events := blockResult.TxsResults[txIndex].GetEvents() for eventIndex, event := range events { - err := n.handleEvent(ctx, uint64(block.Block.Height), latestChainHeight, event) + err := n.handleEvent(ctx, uint64(block.Block.Height), block.Block.Time, latestChainHeight, event) if err != nil { return fmt.Errorf("failed to handle event: tx_index: %d, event_index: %d; %w", txIndex, eventIndex, err) } @@ -161,7 +171,7 @@ func (n *Node) handleNewBlock(ctx context.Context, block *rpccoretypes.ResultBlo } for eventIndex, event := range blockResult.FinalizeBlockEvents { - err := n.handleEvent(ctx, uint64(block.Block.Height), latestChainHeight, event) + err := n.handleEvent(ctx, uint64(block.Block.Height), block.Block.Time, latestChainHeight, event) if err != nil { return fmt.Errorf("failed to handle event: finalize block, event_index: %d; %w", eventIndex, err) } @@ -180,7 +190,7 @@ func (n *Node) handleNewBlock(ctx context.Context, block *rpccoretypes.ResultBlo return nil } -func (n *Node) handleEvent(ctx context.Context, blockHeight uint64, latestHeight uint64, event abcitypes.Event) error { +func (n *Node) handleEvent(ctx context.Context, blockHeight uint64, blockTime time.Time, latestHeight uint64, event abcitypes.Event) error { if n.eventHandlers[event.GetType()] == nil { return nil } @@ -188,6 +198,7 @@ func (n *Node) handleEvent(ctx context.Context, blockHeight uint64, latestHeight n.logger.Debug("handle event", zap.Uint64("height", blockHeight), zap.String("type", event.GetType())) return n.eventHandlers[event.Type](ctx, nodetypes.EventHandlerArgs{ BlockHeight: blockHeight, + BlockTime: blockTime, LatestHeight: latestHeight, EventAttributes: event.GetAttributes(), }) @@ -208,7 +219,7 @@ func (n *Node) txChecker(ctx context.Context) error { case <-timer.C: } - pendingTx, res, err := n.broadcaster.CheckPendingTx(ctx) + pendingTx, res, blockTime, err := n.broadcaster.CheckPendingTx(ctx) if err != nil { return err } else if pendingTx == nil || res == nil { @@ -225,7 +236,7 @@ func (n *Node) txChecker(ctx context.Context) error { default: } - err := n.handleEvent(ctx, uint64(res.Height), 0, event) + err := n.handleEvent(ctx, uint64(res.Height), blockTime, 0, event) if err != nil { n.logger.Error("failed to handle event", zap.String("tx_hash", pendingTx.TxHash), zap.Int("event_index", eventIndex), zap.String("error", err.Error())) break diff --git a/node/rpcclient/client.go b/node/rpcclient/client.go index 755fdf7..3a588c1 100644 --- a/node/rpcclient/client.go +++ b/node/rpcclient/client.go @@ -24,7 +24,7 @@ import ( legacyerrors "github.com/cosmos/cosmos-sdk/types/errors" grpctypes "github.com/cosmos/cosmos-sdk/types/grpc" - clienthttp "github.com/initia-labs/opinit-bots-go/client" + clienthttp "github.com/initia-labs/opinit-bots/client" ) var _ gogogrpc.ClientConn = &RPCClient{} diff --git a/node/types/config.go b/node/types/config.go index dbfc689..adcd0e1 100644 --- a/node/types/config.go +++ b/node/types/config.go @@ -3,7 +3,7 @@ package types import ( "fmt" - btypes "github.com/initia-labs/opinit-bots-go/node/broadcaster/types" + btypes "github.com/initia-labs/opinit-bots/node/broadcaster/types" ) type BlockProcessType uint8 diff --git a/node/types/errors.go b/node/types/errors.go new file mode 100644 index 0000000..23c2713 --- /dev/null +++ b/node/types/errors.go @@ -0,0 +1,5 @@ +package types + +import "github.com/pkg/errors" + +var ErrIgnoreAndTryLater = errors.New("try later") diff --git a/node/types/handler.go b/node/types/handler.go index ab23f2e..184f3be 100644 --- a/node/types/handler.go +++ b/node/types/handler.go @@ -2,6 +2,7 @@ package types import ( "context" + "time" abcitypes "github.com/cometbft/cometbft/abci/types" cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" @@ -10,6 +11,7 @@ import ( type EventHandlerArgs struct { BlockHeight uint64 + BlockTime time.Time LatestHeight uint64 TxIndex uint64 EventAttributes []abcitypes.EventAttribute @@ -19,6 +21,7 @@ type EventHandlerFn func(context.Context, EventHandlerArgs) error type TxHandlerArgs struct { BlockHeight uint64 + BlockTime time.Time LatestHeight uint64 TxIndex uint64 Tx comettypes.Tx @@ -43,8 +46,9 @@ type EndBlockArgs struct { type EndBlockHandlerFn func(context.Context, EndBlockArgs) error type RawBlockArgs struct { - BlockHeight uint64 - BlockBytes []byte + BlockHeight uint64 + LatestHeight uint64 + BlockBytes []byte } type RawBlockHandlerFn func(context.Context, RawBlockArgs) error diff --git a/node/types/status.go b/node/types/status.go index 4812a41..99ed0c7 100644 --- a/node/types/status.go +++ b/node/types/status.go @@ -6,6 +6,6 @@ type BroadcasterStatus struct { } type Status struct { - LastProcessedBlockHeight uint64 `json:"last_processed_block_height,omitempty"` - Broadcaster BroadcasterStatus `json:"broadcaster,omitempty"` + LastBlockHeight uint64 `json:"last_block_height,omitempty"` + Broadcaster *BroadcasterStatus `json:"broadcaster,omitempty"` } diff --git a/proto/celestia/blob/v1/tx.proto b/proto/celestia/blob/v1/tx.proto index 064be25..914bc68 100644 --- a/proto/celestia/blob/v1/tx.proto +++ b/proto/celestia/blob/v1/tx.proto @@ -3,7 +3,7 @@ package celestia.blob.v1; import "celestia/core/v1/blob/blob.proto"; -option go_package = "github.com/initia-labs/opinit-bots-go/types/celestia"; +option go_package = "github.com/initia-labs/opinit-bots/types/celestia"; // MsgPayForBlobs pays for the inclusion of a blob in the block. message MsgPayForBlobs { diff --git a/proto/celestia/core/v1/blob/blob.proto b/proto/celestia/core/v1/blob/blob.proto index a5b7755..c6b1dd5 100644 --- a/proto/celestia/core/v1/blob/blob.proto +++ b/proto/celestia/core/v1/blob/blob.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package celestia.core.v1.blob; -option go_package = "github.com/initia-labs/opinit-bots-go/types/celestia"; +option go_package = "github.com/initia-labs/opinit-bots/types/celestia"; // Blob (named after binary large object) is a chunk of data submitted by a user // to be published to the Celestia blockchain. The data of a Blob is published diff --git a/provider/child/child.go b/provider/child/child.go new file mode 100644 index 0000000..9d56534 --- /dev/null +++ b/provider/child/child.go @@ -0,0 +1,208 @@ +package child + +import ( + "context" + + "go.uber.org/zap" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + + "github.com/initia-labs/OPinit/x/opchild" + opchildtypes "github.com/initia-labs/OPinit/x/opchild/types" + ophosttypes "github.com/initia-labs/OPinit/x/ophost/types" + + "github.com/initia-labs/opinit-bots/keys" + "github.com/initia-labs/opinit-bots/merkle" + "github.com/initia-labs/opinit-bots/node" + btypes "github.com/initia-labs/opinit-bots/node/broadcaster/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" + "github.com/initia-labs/opinit-bots/types" +) + +type BaseChild struct { + version uint8 + + node *node.Node + mk *merkle.Merkle + + bridgeInfo opchildtypes.BridgeInfo + + initializeTreeFn func(uint64) (bool, error) + + cfg nodetypes.NodeConfig + db types.DB + logger *zap.Logger + + opchildQueryClient opchildtypes.QueryClient + + processedMsgs []btypes.ProcessedMsgs + msgQueue []sdk.Msg +} + +func NewBaseChildV1( + cfg nodetypes.NodeConfig, + db types.DB, logger *zap.Logger, bech32Prefix string, +) *BaseChild { + appCodec, txConfig, err := GetCodec(bech32Prefix) + if err != nil { + panic(err) + } + + node, err := node.NewNode(cfg, db, logger, appCodec, txConfig) + if err != nil { + panic(err) + } + + mk, err := merkle.NewMerkle(db.WithPrefix([]byte(types.MerkleName)), ophosttypes.GenerateNodeHash) + if err != nil { + panic(err) + } + + ch := &BaseChild{ + version: 1, + + node: node, + mk: mk, + + cfg: cfg, + db: db, + logger: logger, + + opchildQueryClient: opchildtypes.NewQueryClient(node.GetRPCClient()), + + processedMsgs: make([]btypes.ProcessedMsgs, 0), + msgQueue: make([]sdk.Msg, 0), + } + return ch +} + +func GetCodec(bech32Prefix string) (codec.Codec, client.TxConfig, error) { + unlock := keys.SetSDKConfigContext(bech32Prefix) + defer unlock() + + return keys.CreateCodec([]keys.RegisterInterfaces{ + auth.AppModuleBasic{}.RegisterInterfaces, + opchild.AppModuleBasic{}.RegisterInterfaces, + }) +} + +func (b *BaseChild) Initialize(ctx context.Context, startHeight uint64, startOutputIndex uint64, bridgeInfo opchildtypes.BridgeInfo) error { + err := b.node.Initialize(ctx, startHeight) + if err != nil { + return err + } + + if b.node.HeightInitialized() && startOutputIndex != 0 { + b.initializeTreeFn = func(blockHeight uint64) (bool, error) { + if startHeight+1 == blockHeight { + b.logger.Info("initialize tree", zap.Uint64("index", startOutputIndex)) + err := b.mk.InitializeWorkingTree(startOutputIndex, 1) + if err != nil { + return false, err + } + return true, nil + } + return false, nil + } + } + b.SetBridgeInfo(bridgeInfo) + return nil +} + +func (b *BaseChild) Start(ctx context.Context) { + b.logger.Info("child start", zap.Uint64("height", b.Height())) + b.node.Start(ctx) +} + +func (b BaseChild) BroadcastMsgs(msgs btypes.ProcessedMsgs) { + if len(msgs.Msgs) == 0 { + return + } + + b.node.MustGetBroadcaster().BroadcastMsgs(msgs) +} + +func (b BaseChild) ProcessedMsgsToRawKV(msgs []btypes.ProcessedMsgs, delete bool) ([]types.RawKV, error) { + if len(msgs) == 0 { + return nil, nil + } + + return b.node.MustGetBroadcaster().ProcessedMsgsToRawKV(msgs, delete) +} + +func (b BaseChild) BridgeId() uint64 { + return b.bridgeInfo.BridgeId +} + +func (b BaseChild) OracleEnabled() bool { + return b.bridgeInfo.BridgeConfig.OracleEnabled +} + +func (b BaseChild) HasKey() bool { + return b.node.HasBroadcaster() +} + +func (b *BaseChild) SetBridgeInfo(bridgeInfo opchildtypes.BridgeInfo) { + b.bridgeInfo = bridgeInfo +} + +func (b BaseChild) BridgeInfo() opchildtypes.BridgeInfo { + return b.bridgeInfo +} + +func (b BaseChild) Height() uint64 { + return b.node.GetHeight() +} + +func (b BaseChild) Version() uint8 { + return b.version +} + +func (b BaseChild) Node() *node.Node { + return b.node +} + +func (b BaseChild) Logger() *zap.Logger { + return b.logger +} + +func (b BaseChild) DB() types.DB { + return b.db +} + +/// MsgQueue + +func (b BaseChild) GetMsgQueue() []sdk.Msg { + return b.msgQueue +} + +func (b *BaseChild) AppendMsgQueue(msg sdk.Msg) { + b.msgQueue = append(b.msgQueue, msg) +} + +func (b *BaseChild) EmptyMsgQueue() { + b.msgQueue = b.msgQueue[:0] +} + +/// ProcessedMsgs + +func (b BaseChild) GetProcessedMsgs() []btypes.ProcessedMsgs { + return b.processedMsgs +} + +func (b *BaseChild) AppendProcessedMsgs(msgs btypes.ProcessedMsgs) { + b.processedMsgs = append(b.processedMsgs, msgs) +} + +func (b *BaseChild) EmptyProcessedMsgs() { + b.processedMsgs = b.processedMsgs[:0] +} + +/// Merkle + +func (b BaseChild) Merkle() *merkle.Merkle { + return b.mk +} diff --git a/executor/child/msgs.go b/provider/child/msgs.go similarity index 51% rename from executor/child/msgs.go rename to provider/child/msgs.go index 82e1513..fc813ba 100644 --- a/executor/child/msgs.go +++ b/provider/child/msgs.go @@ -6,7 +6,26 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -func (ch Child) GetMsgFinalizeTokenDeposit( +func (b BaseChild) GetMsgSetBridgeInfo( + bridgeInfo opchildtypes.BridgeInfo, +) (sdk.Msg, error) { + sender, err := b.node.MustGetBroadcaster().GetAddressString() + if err != nil { + return nil, err + } + + msg := opchildtypes.NewMsgSetBridgeInfo( + sender, + bridgeInfo, + ) + err = msg.Validate(b.node.AccountCodec()) + if err != nil { + return nil, err + } + return msg, nil +} + +func (b BaseChild) GetMsgFinalizeTokenDeposit( from string, to string, coin sdk.Coin, @@ -15,7 +34,7 @@ func (ch Child) GetMsgFinalizeTokenDeposit( l1Denom string, data []byte, ) (sdk.Msg, error) { - sender, err := ch.node.MustGetBroadcaster().GetAddressString() + sender, err := b.node.MustGetBroadcaster().GetAddressString() if err != nil { return nil, err } @@ -30,18 +49,18 @@ func (ch Child) GetMsgFinalizeTokenDeposit( l1Denom, data, ) - err = msg.Validate(ch.node.AccountCodec()) + err = msg.Validate(b.node.AccountCodec()) if err != nil { return nil, err } return msg, nil } -func (ch Child) GetMsgUpdateOracle( +func (b BaseChild) GetMsgUpdateOracle( height uint64, data []byte, ) (sdk.Msg, error) { - sender, err := ch.node.MustGetBroadcaster().GetAddressString() + sender, err := b.node.MustGetBroadcaster().GetAddressString() if err != nil { return nil, err } @@ -51,7 +70,7 @@ func (ch Child) GetMsgUpdateOracle( height, data, ) - err = msg.Validate(ch.node.AccountCodec()) + err = msg.Validate(b.node.AccountCodec()) if err != nil { return nil, err } diff --git a/provider/child/parse.go b/provider/child/parse.go new file mode 100644 index 0000000..9b2c8ee --- /dev/null +++ b/provider/child/parse.go @@ -0,0 +1,96 @@ +package child + +import ( + "fmt" + "strconv" + + "cosmossdk.io/math" + abcitypes "github.com/cometbft/cometbft/abci/types" + opchildtypes "github.com/initia-labs/OPinit/x/opchild/types" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func ParseFinalizeDeposit(eventAttrs []abcitypes.EventAttribute) ( + l1BlockHeight, l1Sequence uint64, + from, to, baseDenom string, + amount sdk.Coin, + err error) { + for _, attr := range eventAttrs { + switch attr.Key { + case opchildtypes.AttributeKeyL1Sequence: + l1Sequence, err = strconv.ParseUint(attr.Value, 10, 64) + if err != nil { + return + } + case opchildtypes.AttributeKeySender: + from = attr.Value + case opchildtypes.AttributeKeyRecipient: + to = attr.Value + case opchildtypes.AttributeKeyDenom: + amount.Denom = attr.Value + case opchildtypes.AttributeKeyBaseDenom: + baseDenom = attr.Value + case opchildtypes.AttributeKeyAmount: + coinAmount, ok := math.NewIntFromString(attr.Value) + if !ok { + err = fmt.Errorf("invalid amount %s", attr.Value) + return + } + amount.Amount = coinAmount + case opchildtypes.AttributeKeyFinalizeHeight: + l1BlockHeight, err = strconv.ParseUint(attr.Value, 10, 64) + if err != nil { + return + } + } + } + return +} + +func ParseUpdateOracle(eventAttrs []abcitypes.EventAttribute) ( + l1BlockHeight uint64, + from string, + err error) { + for _, attr := range eventAttrs { + switch attr.Key { + case opchildtypes.AttributeKeyHeight: + l1BlockHeight, err = strconv.ParseUint(attr.Value, 10, 64) + if err != nil { + return + } + case opchildtypes.AttributeKeyFrom: + from = attr.Value + } + } + return +} + +func ParseInitiateWithdrawal(eventAttrs []abcitypes.EventAttribute) ( + l2Sequence, amount uint64, + from, to, baseDenom string, + err error) { + for _, attr := range eventAttrs { + switch attr.Key { + case opchildtypes.AttributeKeyL2Sequence: + l2Sequence, err = strconv.ParseUint(attr.Value, 10, 64) + if err != nil { + return + } + case opchildtypes.AttributeKeyFrom: + from = attr.Value + case opchildtypes.AttributeKeyTo: + to = attr.Value + case opchildtypes.AttributeKeyBaseDenom: + baseDenom = attr.Value + case opchildtypes.AttributeKeyAmount: + coinAmount, ok := math.NewIntFromString(attr.Value) + if !ok { + err = fmt.Errorf("invalid amount %s", attr.Value) + return + } + amount = coinAmount.Uint64() + } + } + return +} diff --git a/provider/child/query.go b/provider/child/query.go new file mode 100644 index 0000000..8aa135c --- /dev/null +++ b/provider/child/query.go @@ -0,0 +1,55 @@ +package child + +import ( + "context" + + opchildtypes "github.com/initia-labs/OPinit/x/opchild/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/initia-labs/opinit-bots/node/rpcclient" +) + +func (b BaseChild) GetAddress() sdk.AccAddress { + return b.node.MustGetBroadcaster().GetAddress() +} + +func (b BaseChild) GetAddressStr() (string, error) { + return b.node.MustGetBroadcaster().GetAddressString() +} + +func (b BaseChild) QueryBridgeInfo(ctx context.Context) (opchildtypes.BridgeInfo, error) { + req := &opchildtypes.QueryBridgeInfoRequest{} + ctx, cancel := rpcclient.GetQueryContext(ctx, 0) + defer cancel() + + res, err := b.opchildQueryClient.BridgeInfo(ctx, req) + if err != nil { + return opchildtypes.BridgeInfo{}, err + } + return res.BridgeInfo, nil +} + +func (b BaseChild) QueryNextL1Sequence(ctx context.Context, height uint64) (uint64, error) { + req := &opchildtypes.QueryNextL1SequenceRequest{} + ctx, cancel := rpcclient.GetQueryContext(ctx, height) + defer cancel() + + res, err := b.opchildQueryClient.NextL1Sequence(ctx, req) + if err != nil { + return 0, err + } + return res.NextL1Sequence, nil +} + +func (b BaseChild) QueryNextL2Sequence(ctx context.Context, height uint64) (uint64, error) { + req := &opchildtypes.QueryNextL2SequenceRequest{} + ctx, cancel := rpcclient.GetQueryContext(ctx, height) + defer cancel() + + res, err := b.opchildQueryClient.NextL2Sequence(ctx, req) + if err != nil { + return 0, err + } + return res.NextL2Sequence, nil +} diff --git a/provider/child/tree.go b/provider/child/tree.go new file mode 100644 index 0000000..8a8c73d --- /dev/null +++ b/provider/child/tree.go @@ -0,0 +1,12 @@ +package child + +func (b *BaseChild) InitializeTree(blockHeight uint64) bool { + if b.initializeTreeFn != nil { + ok, err := b.initializeTreeFn(blockHeight) + if err != nil { + panic("failed to initialize working tree: " + err.Error()) + } + return ok + } + return false +} diff --git a/provider/host/host.go b/provider/host/host.go new file mode 100644 index 0000000..31165cc --- /dev/null +++ b/provider/host/host.go @@ -0,0 +1,182 @@ +package host + +import ( + "context" + + "go.uber.org/zap" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + + opchildtypes "github.com/initia-labs/OPinit/x/opchild/types" + "github.com/initia-labs/OPinit/x/ophost" + ophosttypes "github.com/initia-labs/OPinit/x/ophost/types" + + "github.com/initia-labs/opinit-bots/keys" + "github.com/initia-labs/opinit-bots/node" + btypes "github.com/initia-labs/opinit-bots/node/broadcaster/types" + nodetypes "github.com/initia-labs/opinit-bots/node/types" + "github.com/initia-labs/opinit-bots/types" +) + +type BaseHost struct { + version uint8 + + node *node.Node + + bridgeInfo opchildtypes.BridgeInfo + + cfg nodetypes.NodeConfig + db types.DB + logger *zap.Logger + + ophostQueryClient ophosttypes.QueryClient + + processedMsgs []btypes.ProcessedMsgs + msgQueue []sdk.Msg +} + +func NewBaseHostV1(cfg nodetypes.NodeConfig, + db types.DB, logger *zap.Logger, bech32Prefix string, +) *BaseHost { + appCodec, txConfig, err := GetCodec(bech32Prefix) + if err != nil { + panic(err) + } + + node, err := node.NewNode(cfg, db, logger, appCodec, txConfig) + if err != nil { + panic(err) + } + + h := &BaseHost{ + version: 1, + + node: node, + + cfg: cfg, + db: db, + logger: logger, + + ophostQueryClient: ophosttypes.NewQueryClient(node.GetRPCClient()), + + processedMsgs: make([]btypes.ProcessedMsgs, 0), + msgQueue: make([]sdk.Msg, 0), + } + + return h +} + +func GetCodec(bech32Prefix string) (codec.Codec, client.TxConfig, error) { + unlock := keys.SetSDKConfigContext(bech32Prefix) + defer unlock() + + return keys.CreateCodec([]keys.RegisterInterfaces{ + auth.AppModuleBasic{}.RegisterInterfaces, + ophost.AppModuleBasic{}.RegisterInterfaces, + }) +} + +func (b *BaseHost) Initialize(ctx context.Context, startHeight uint64, bridgeInfo opchildtypes.BridgeInfo) error { + err := b.node.Initialize(ctx, startHeight) + if err != nil { + return err + } + b.SetBridgeInfo(bridgeInfo) + return nil +} + +func (b *BaseHost) Start(ctx context.Context) { + if b.cfg.ProcessType == nodetypes.PROCESS_TYPE_ONLY_BROADCAST { + b.logger.Info("host start") + } else { + b.logger.Info("host start", zap.Uint64("height", b.node.GetHeight())) + } + b.node.Start(ctx) +} + +func (b BaseHost) BroadcastMsgs(msgs btypes.ProcessedMsgs) { + if len(msgs.Msgs) == 0 { + return + } + + b.node.MustGetBroadcaster().BroadcastMsgs(msgs) +} + +func (b BaseHost) ProcessedMsgsToRawKV(msgs []btypes.ProcessedMsgs, delete bool) ([]types.RawKV, error) { + if len(msgs) == 0 { + return nil, nil + } + + return b.node.MustGetBroadcaster().ProcessedMsgsToRawKV(msgs, delete) +} + +func (b BaseHost) BridgeId() uint64 { + return b.bridgeInfo.BridgeId +} + +func (b BaseHost) OracleEnabled() bool { + return b.bridgeInfo.BridgeConfig.OracleEnabled +} + +func (b *BaseHost) SetBridgeInfo(bridgeInfo opchildtypes.BridgeInfo) { + b.bridgeInfo = bridgeInfo +} + +func (b BaseHost) BridgeInfo() opchildtypes.BridgeInfo { + return b.bridgeInfo +} + +func (b BaseHost) HasKey() bool { + return b.node.HasBroadcaster() +} + +func (b BaseHost) Height() uint64 { + return b.node.GetHeight() +} + +func (b BaseHost) Version() uint8 { + return b.version +} + +func (b BaseHost) Node() *node.Node { + return b.node +} + +func (b BaseHost) Logger() *zap.Logger { + return b.logger +} + +func (b BaseHost) DB() types.DB { + return b.db +} + +/// MsgQueue + +func (b BaseHost) GetMsgQueue() []sdk.Msg { + return b.msgQueue +} + +func (b *BaseHost) AppendMsgQueue(msg sdk.Msg) { + b.msgQueue = append(b.msgQueue, msg) +} + +func (b *BaseHost) EmptyMsgQueue() { + b.msgQueue = b.msgQueue[:0] +} + +/// ProcessedMsgs + +func (b BaseHost) GetProcessedMsgs() []btypes.ProcessedMsgs { + return b.processedMsgs +} + +func (b *BaseHost) AppendProcessedMsgs(msgs btypes.ProcessedMsgs) { + b.processedMsgs = append(b.processedMsgs, msgs) +} + +func (b *BaseHost) EmptyProcessedMsgs() { + b.processedMsgs = b.processedMsgs[:0] +} diff --git a/executor/host/msgs.go b/provider/host/msgs.go similarity index 64% rename from executor/host/msgs.go rename to provider/host/msgs.go index 6ecf124..c11b80d 100644 --- a/executor/host/msgs.go +++ b/provider/host/msgs.go @@ -6,13 +6,13 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -func (h Host) GetMsgProposeOutput( +func (b BaseHost) GetMsgProposeOutput( bridgeId uint64, outputIndex uint64, l2BlockNumber uint64, outputRoot []byte, ) (sdk.Msg, error) { - sender, err := h.node.MustGetBroadcaster().GetAddressString() + sender, err := b.node.MustGetBroadcaster().GetAddressString() if err != nil { return nil, err } @@ -24,25 +24,25 @@ func (h Host) GetMsgProposeOutput( l2BlockNumber, outputRoot, ) - err = msg.Validate(h.node.AccountCodec()) + err = msg.Validate(b.node.AccountCodec()) if err != nil { return nil, err } return msg, nil } -func (h Host) CreateBatchMsg(batchBytes []byte) (sdk.Msg, error) { - submitter, err := h.node.MustGetBroadcaster().GetAddressString() +func (b BaseHost) CreateBatchMsg(batchBytes []byte) (sdk.Msg, error) { + submitter, err := b.node.MustGetBroadcaster().GetAddressString() if err != nil { return nil, err } msg := ophosttypes.NewMsgRecordBatch( submitter, - uint64(h.bridgeId), + b.BridgeId(), batchBytes, ) - err = msg.Validate(h.node.AccountCodec()) + err = msg.Validate(b.node.AccountCodec()) if err != nil { return nil, err } diff --git a/provider/host/parse.go b/provider/host/parse.go new file mode 100644 index 0000000..ee3fedb --- /dev/null +++ b/provider/host/parse.go @@ -0,0 +1,158 @@ +package host + +import ( + "encoding/hex" + "strconv" + + abcitypes "github.com/cometbft/cometbft/abci/types" + ophosttypes "github.com/initia-labs/OPinit/x/ophost/types" +) + +func ParseMsgRecordBatch(eventAttrs []abcitypes.EventAttribute) ( + submitter string, err error, +) { + for _, attr := range eventAttrs { + switch attr.Key { + case ophosttypes.AttributeKeySubmitter: + submitter = attr.Value + } + } + return +} + +func ParseMsgUpdateBatchInfo(eventAttrs []abcitypes.EventAttribute) ( + bridgeId uint64, submitter, chain string, + outputIndex, l2BlockNumber uint64, + err error) { + for _, attr := range eventAttrs { + switch attr.Key { + case ophosttypes.AttributeKeyBridgeId: + bridgeId, err = strconv.ParseUint(attr.Value, 10, 64) + if err != nil { + return + } + case ophosttypes.AttributeKeyBatchChainType: + chain = attr.Value + case ophosttypes.AttributeKeyBatchSubmitter: + submitter = attr.Value + case ophosttypes.AttributeKeyFinalizedOutputIndex: + outputIndex, err = strconv.ParseUint(attr.Value, 10, 64) + if err != nil { + return + } + case ophosttypes.AttributeKeyFinalizedL2BlockNumber: + l2BlockNumber, err = strconv.ParseUint(attr.Value, 10, 64) + if err != nil { + return + } + } + } + return +} + +func ParseMsgInitiateDeposit(eventAttrs []abcitypes.EventAttribute) ( + bridgeId, l1Sequence uint64, + from, to, l1Denom, l2Denom, amount string, + data []byte, err error) { + + for _, attr := range eventAttrs { + switch attr.Key { + case ophosttypes.AttributeKeyBridgeId: + bridgeId, err = strconv.ParseUint(attr.Value, 10, 64) + if err != nil { + return + } + case ophosttypes.AttributeKeyL1Sequence: + l1Sequence, err = strconv.ParseUint(attr.Value, 10, 64) + if err != nil { + return + } + case ophosttypes.AttributeKeyFrom: + from = attr.Value + case ophosttypes.AttributeKeyTo: + to = attr.Value + case ophosttypes.AttributeKeyL1Denom: + l1Denom = attr.Value + case ophosttypes.AttributeKeyL2Denom: + l2Denom = attr.Value + case ophosttypes.AttributeKeyAmount: + amount = attr.Value + case ophosttypes.AttributeKeyData: + data, err = hex.DecodeString(attr.Value) + if err != nil { + return + } + } + } + return +} + +func ParseMsgProposeOutput(eventAttrs []abcitypes.EventAttribute) ( + bridgeId, l2BlockNumber, outputIndex uint64, + proposer string, + outputRoot []byte, + err error) { + for _, attr := range eventAttrs { + switch attr.Key { + case ophosttypes.AttributeKeyProposer: + proposer = attr.Value + case ophosttypes.AttributeKeyBridgeId: + bridgeId, err = strconv.ParseUint(attr.Value, 10, 64) + if err != nil { + return + } + case ophosttypes.AttributeKeyOutputIndex: + outputIndex, err = strconv.ParseUint(attr.Value, 10, 64) + if err != nil { + return + } + case ophosttypes.AttributeKeyL2BlockNumber: + l2BlockNumber, err = strconv.ParseUint(attr.Value, 10, 64) + if err != nil { + return + } + case ophosttypes.AttributeKeyOutputRoot: + outputRoot, err = hex.DecodeString(attr.Value) + if err != nil { + return + } + } + } + return +} + +func ParseMsgFinalizeWithdrawal(eventAttrs []abcitypes.EventAttribute) ( + bridgeId, outputIndex, l2Sequence uint64, + from, to, l1Denom, l2Denom, amount string, + err error) { + for _, attr := range eventAttrs { + switch attr.Key { + case ophosttypes.AttributeKeyBridgeId: + bridgeId, err = strconv.ParseUint(attr.Value, 10, 64) + if err != nil { + return + } + case ophosttypes.AttributeKeyOutputIndex: + outputIndex, err = strconv.ParseUint(attr.Value, 10, 64) + if err != nil { + return + } + case ophosttypes.AttributeKeyL2Sequence: + l2Sequence, err = strconv.ParseUint(attr.Value, 10, 64) + if err != nil { + return + } + case ophosttypes.AttributeKeyFrom: + from = attr.Value + case ophosttypes.AttributeKeyTo: + to = attr.Value + case ophosttypes.AttributeKeyL1Denom: + l1Denom = attr.Value + case ophosttypes.AttributeKeyL2Denom: + l2Denom = attr.Value + case ophosttypes.AttributeKeyAmount: + amount = attr.Value + } + } + return +} diff --git a/executor/host/query.go b/provider/host/query.go similarity index 57% rename from executor/host/query.go rename to provider/host/query.go index 17b0109..a215f3b 100644 --- a/executor/host/query.go +++ b/provider/host/query.go @@ -13,19 +13,46 @@ import ( ophosttypes "github.com/initia-labs/OPinit/x/ophost/types" - "github.com/initia-labs/opinit-bots-go/node/rpcclient" - "github.com/initia-labs/opinit-bots-go/types" + "github.com/initia-labs/opinit-bots/node/rpcclient" + "github.com/initia-labs/opinit-bots/types" ) -func (h Host) GetAddress() sdk.AccAddress { - return h.node.MustGetBroadcaster().GetAddress() +func (b BaseHost) GetAddress() sdk.AccAddress { + return b.node.MustGetBroadcaster().GetAddress() } -func (h Host) GetAddressStr() (string, error) { - return h.node.MustGetBroadcaster().GetAddressString() +func (b BaseHost) GetAddressStr() (string, error) { + return b.node.MustGetBroadcaster().GetAddressString() } -func (h Host) QueryLastOutput(ctx context.Context, bridgeId uint64) (*ophosttypes.QueryOutputProposalResponse, error) { +func (b BaseHost) QueryBridgeConfig(ctx context.Context, bridgeId uint64) (*ophosttypes.QueryBridgeResponse, error) { + req := &ophosttypes.QueryBridgeRequest{ + BridgeId: bridgeId, + } + ctx, cancel := rpcclient.GetQueryContext(ctx, 0) + defer cancel() + + return b.ophostQueryClient.Bridge(ctx, req) +} + +func (b BaseHost) QueryLastFinalizedOutput(ctx context.Context, bridgeId uint64) (*ophosttypes.QueryLastFinalizedOutputResponse, error) { + req := &ophosttypes.QueryLastFinalizedOutputRequest{ + BridgeId: bridgeId, + } + ctx, cancel := rpcclient.GetQueryContext(ctx, 0) + defer cancel() + + res, err := b.ophostQueryClient.LastFinalizedOutput(ctx, req) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return nil, nil + } + return nil, err + } + return res, nil +} + +func (b BaseHost) QueryLastOutput(ctx context.Context, bridgeId uint64) (*ophosttypes.QueryOutputProposalResponse, error) { req := &ophosttypes.QueryOutputProposalsRequest{ BridgeId: bridgeId, Pagination: &query.PageRequest{ @@ -36,7 +63,7 @@ func (h Host) QueryLastOutput(ctx context.Context, bridgeId uint64) (*ophosttype ctx, cancel := rpcclient.GetQueryContext(ctx, 0) defer cancel() - res, err := h.ophostQueryClient.OutputProposals(ctx, req) + res, err := b.ophostQueryClient.OutputProposals(ctx, req) if err != nil { return nil, err } @@ -46,27 +73,27 @@ func (h Host) QueryLastOutput(ctx context.Context, bridgeId uint64) (*ophosttype return &res.OutputProposals[0], nil } -func (h Host) QueryOutput(ctx context.Context, bridgeId uint64, outputIndex uint64) (*ophosttypes.QueryOutputProposalResponse, error) { +func (b BaseHost) QueryOutput(ctx context.Context, bridgeId uint64, outputIndex uint64, height uint64) (*ophosttypes.QueryOutputProposalResponse, error) { req := &ophosttypes.QueryOutputProposalRequest{ BridgeId: bridgeId, OutputIndex: outputIndex, } - ctx, cancel := rpcclient.GetQueryContext(ctx, 0) + ctx, cancel := rpcclient.GetQueryContext(ctx, height) defer cancel() - return h.ophostQueryClient.OutputProposal(ctx, req) + return b.ophostQueryClient.OutputProposal(ctx, req) } // QueryOutputByL2BlockNumber queries the last output proposal before the given L2 block number -func (h Host) QueryOutputByL2BlockNumber(ctx context.Context, bridgeId uint64, l2BlockNumber uint64) (*ophosttypes.QueryOutputProposalResponse, error) { - start, err := h.QueryOutput(ctx, bridgeId, 1) +func (b BaseHost) QueryOutputByL2BlockNumber(ctx context.Context, bridgeId uint64, l2BlockNumber uint64) (*ophosttypes.QueryOutputProposalResponse, error) { + start, err := b.QueryOutput(ctx, bridgeId, 1, 0) if err != nil { if strings.Contains(err.Error(), "not found") { return nil, nil } return nil, err } - end, err := h.QueryLastOutput(ctx, bridgeId) + end, err := b.QueryLastOutput(ctx, bridgeId) if err != nil { return nil, err } else if end == nil { @@ -76,7 +103,7 @@ func (h Host) QueryOutputByL2BlockNumber(ctx context.Context, bridgeId uint64, l for { if start.OutputProposal.L2BlockNumber >= l2BlockNumber { if start.OutputIndex != 1 { - return h.QueryOutput(ctx, bridgeId, start.OutputIndex-1) + return b.QueryOutput(ctx, bridgeId, start.OutputIndex-1, 0) } return nil, nil } else if end.OutputProposal.L2BlockNumber < l2BlockNumber { @@ -86,7 +113,7 @@ func (h Host) QueryOutputByL2BlockNumber(ctx context.Context, bridgeId uint64, l } midIndex := (start.OutputIndex + end.OutputIndex) / 2 - output, err := h.QueryOutput(ctx, bridgeId, midIndex) + output, err := b.QueryOutput(ctx, bridgeId, midIndex, 0) if err != nil { return nil, err } @@ -99,7 +126,7 @@ func (h Host) QueryOutputByL2BlockNumber(ctx context.Context, bridgeId uint64, l } } -func (h Host) QueryCreateBridgeHeight(ctx context.Context, bridgeId uint64) (uint64, error) { +func (b BaseHost) QueryCreateBridgeHeight(ctx context.Context, bridgeId uint64) (uint64, error) { ctx, cancel := rpcclient.GetQueryContext(ctx, 0) defer cancel() @@ -109,7 +136,7 @@ func (h Host) QueryCreateBridgeHeight(ctx context.Context, bridgeId uint64) (uin bridgeId, ) perPage := 1 - res, err := h.node.GetRPCClient().TxSearch(ctx, query, false, nil, &perPage, "desc") + res, err := b.node.GetRPCClient().TxSearch(ctx, query, false, nil, &perPage, "desc") if err != nil { return 0, err } @@ -120,16 +147,16 @@ func (h Host) QueryCreateBridgeHeight(ctx context.Context, bridgeId uint64) (uin return uint64(res.Txs[0].Height), nil } -func (h Host) QueryBatchInfos(ctx context.Context, bridgeId uint64) (*ophosttypes.QueryBatchInfosResponse, error) { +func (b BaseHost) QueryBatchInfos(ctx context.Context, bridgeId uint64) (*ophosttypes.QueryBatchInfosResponse, error) { req := &ophosttypes.QueryBatchInfosRequest{ BridgeId: bridgeId, } ctx, cancel := rpcclient.GetQueryContext(ctx, 0) defer cancel() - return h.ophostQueryClient.BatchInfos(ctx, req) + return b.ophostQueryClient.BatchInfos(ctx, req) } -func (h Host) QueryDepositTxHeight(ctx context.Context, bridgeId uint64, l1Sequence uint64) (uint64, error) { +func (b BaseHost) QueryDepositTxHeight(ctx context.Context, bridgeId uint64, l1Sequence uint64) (uint64, error) { if l1Sequence == 0 { return 0, nil } @@ -153,7 +180,7 @@ func (h Host) QueryDepositTxHeight(ctx context.Context, bridgeId uint64, l1Seque case <-ticker.C: } - res, err := h.node.GetRPCClient().TxSearch(ctx, query, false, &page, &per_page, "asc") + res, err := b.node.GetRPCClient().TxSearch(ctx, query, false, &page, &per_page, "asc") if err != nil { return 0, err } diff --git a/scripts/protocgen.sh b/scripts/protocgen.sh index b11f78b..abb9388 100644 --- a/scripts/protocgen.sh +++ b/scripts/protocgen.sh @@ -21,5 +21,5 @@ for dir in $proto_dirs; do done # move proto files to the right places -cp -r github.com/initia-labs/opinit-bots-go/types/* ../types/ +cp -r github.com/initia-labs/opinit-bots/types/* ../types/ rm -rf github.com diff --git a/types/const.go b/types/const.go new file mode 100644 index 0000000..beac621 --- /dev/null +++ b/types/const.go @@ -0,0 +1,11 @@ +package types + +const ( + HostName = "host" + ChildName = "child" + BatchName = "batch" + MerkleName = "merkle" + + DAHostName = "da_host" + DACelestiaName = "da_celestia" +) diff --git a/types/db.go b/types/db.go index 537a7cd..c48956d 100644 --- a/types/db.go +++ b/types/db.go @@ -20,6 +20,7 @@ type DB interface { Delete([]byte) error Close() error PrefixedIterate([]byte, func([]byte, []byte) (bool, error)) error + PrefixedReverseIterate([]byte, func([]byte, []byte) (bool, error)) error SeekPrevInclusiveKey([]byte, []byte) ([]byte, []byte, error) WithPrefix([]byte) DB PrefixedKey([]byte) []byte