From 70060802c96e84eceeb163cfba1206e92c5e86b0 Mon Sep 17 00:00:00 2001 From: JSHan94 Date: Fri, 10 Nov 2023 15:33:21 +0900 Subject: [PATCH] remove docker setup wip tested version use base64 for buffer and impelemnt challenger fix challenger handle pagination output proposal clean up update bots readme remove unnecessary file fix readme fix base port in readme fix ports in sample and config.ts update initia.js apply blockchainData fix to handle block fix sync error activate L2 Monitor fix handle event support stone-12-1 update dockerfile implement resurrector remove mnemonic --- bots/.env | 5 - bots/.envrc_sample | 33 +- bots/.gitignore | 1 + bots/.gitmodules | 3 - bots/Dockerfile | 22 - bots/README.md | 155 +++---- bots/batch.json | 11 - bots/challenger.json | 11 - bots/docker-compose-reset | 7 - bots/docker-compose.yml | 73 --- bots/dockerfile | 15 + bots/entrypoint.sh | 4 +- bots/env-gen.sh | 21 + bots/executor.json | 12 - bots/output.json | 12 - bots/package-lock.json | 137 ++---- bots/package.json | 13 +- bots/pm2.json | 34 ++ bots/src/config.ts | 130 +++--- .../src/controller/executor/CoinController.ts | 51 --- .../executor/DepositTxController.ts | 50 +++ ...ontroller.ts => WithdrawalTxController.ts} | 22 +- bots/src/controller/index.ts | 8 +- bots/src/lib/apiRequest.ts | 59 --- bots/src/lib/lcd.ts | 125 ------ bots/src/lib/monitoring.ts | 2 +- bots/src/lib/query.ts | 105 +++++ bots/src/lib/rpc.ts | 210 +++++---- bots/src/lib/slack.ts | 71 +++ bots/src/lib/storage.ts | 127 ++++-- bots/src/lib/tx.ts | 59 +-- bots/src/lib/types.ts | 48 +- bots/src/lib/util.ts | 30 +- bots/src/lib/wallet.ts | 34 +- bots/src/loader/app.ts | 2 - bots/src/orm/RecordEntity.ts | 8 +- bots/src/orm/challenger/CoinEntity.ts | 20 - .../src/orm/challenger/DeletedOutputEntity.ts | 9 +- bots/src/orm/challenger/DepositTxEntity.ts | 30 +- .../orm/challenger/FinalizeDepositTxEntity.ts | 25 ++ .../challenger/FinalizeWithdrawalTxEntity.ts | 31 ++ bots/src/orm/challenger/OutputEntity.ts | 7 +- bots/src/orm/challenger/WithdrawalTxEntity.ts | 31 +- bots/src/orm/executor/CoinEntity.ts | 20 - bots/src/orm/executor/DepositTxEntity.ts | 23 +- bots/src/orm/executor/FailedTxEntity.ts | 43 +- bots/src/orm/executor/OutputEntity.ts | 5 +- bots/src/orm/executor/WithdrawalTxEntity.ts | 26 +- bots/src/orm/index.ts | 17 +- bots/src/scripts/contract/Move.toml | 10 - bots/src/scripts/contract/sources/l2id.move | 3 - bots/src/scripts/setupL2.ts | 129 ++---- bots/src/service/batch/BatchService.ts | 4 +- bots/src/service/executor/CoinService.ts | 56 --- bots/src/service/executor/DepositTxService.ts | 33 ++ .../{TxService.ts => WithdrawalTxService.ts} | 20 +- bots/src/service/index.ts | 4 +- bots/src/test/claim.ts | 65 +++ bots/src/test/contract/Move.toml | 10 - bots/src/test/contract/sources/l2id.move | 3 - bots/src/test/fundAccounts.ts | 124 +++++ bots/src/test/integration.ts | 86 ++-- bots/src/test/storage.spec.ts | 57 +++ bots/src/test/utils/Bridge.ts | 197 +++----- bots/src/test/utils/DockerHelper.ts | 34 -- bots/src/test/utils/TxBot.ts | 153 +++---- bots/src/test/utils/helper.ts | 114 ++--- .../worker/batchSubmitter/batchSubmitter.ts | 137 +++--- bots/src/worker/batchSubmitter/db.ts | 4 +- bots/src/worker/bridgeExecutor/L1Monitor.ts | 234 ++++------ bots/src/worker/bridgeExecutor/L2Monitor.ts | 272 +++++------ bots/src/worker/bridgeExecutor/Monitor.ts | 100 ++++- .../worker/bridgeExecutor/MonitorHelper.ts | 113 +++-- bots/src/worker/bridgeExecutor/Resurrector.ts | 101 +++++ bots/src/worker/bridgeExecutor/db.ts | 2 - bots/src/worker/bridgeExecutor/index.ts | 21 +- .../src/worker/challenger/ChallegnerHelper.ts | 56 --- bots/src/worker/challenger/L1Monitor.ts | 140 +++--- bots/src/worker/challenger/L2Monitor.ts | 327 +++----------- bots/src/worker/challenger/challenger.ts | 423 ++++++++---------- bots/src/worker/challenger/db.ts | 10 +- bots/src/worker/challenger/index.ts | 26 +- bots/src/worker/outputSubmitter/db.ts | 58 +++ bots/src/worker/outputSubmitter/index.ts | 12 +- .../worker/outputSubmitter/outputSubmitter.ts | 132 +++--- bots/tsconfig.json | 2 +- bots/webpack.config.js | 60 --- package-lock.json | 6 + 88 files changed, 2482 insertions(+), 2853 deletions(-) delete mode 100644 bots/.env delete mode 100644 bots/.gitmodules delete mode 100644 bots/Dockerfile delete mode 100644 bots/batch.json delete mode 100644 bots/challenger.json delete mode 100755 bots/docker-compose-reset delete mode 100644 bots/docker-compose.yml create mode 100644 bots/dockerfile mode change 100755 => 100644 bots/entrypoint.sh create mode 100755 bots/env-gen.sh delete mode 100644 bots/executor.json delete mode 100644 bots/output.json create mode 100644 bots/pm2.json delete mode 100644 bots/src/controller/executor/CoinController.ts create mode 100644 bots/src/controller/executor/DepositTxController.ts rename bots/src/controller/executor/{TxController.ts => WithdrawalTxController.ts} (59%) delete mode 100644 bots/src/lib/apiRequest.ts delete mode 100644 bots/src/lib/lcd.ts create mode 100644 bots/src/lib/query.ts create mode 100644 bots/src/lib/slack.ts delete mode 100644 bots/src/orm/challenger/CoinEntity.ts create mode 100644 bots/src/orm/challenger/FinalizeDepositTxEntity.ts create mode 100644 bots/src/orm/challenger/FinalizeWithdrawalTxEntity.ts delete mode 100644 bots/src/orm/executor/CoinEntity.ts delete mode 100644 bots/src/scripts/contract/Move.toml delete mode 100644 bots/src/scripts/contract/sources/l2id.move delete mode 100644 bots/src/service/executor/CoinService.ts create mode 100644 bots/src/service/executor/DepositTxService.ts rename bots/src/service/executor/{TxService.ts => WithdrawalTxService.ts} (59%) create mode 100644 bots/src/test/claim.ts delete mode 100644 bots/src/test/contract/Move.toml delete mode 100644 bots/src/test/contract/sources/l2id.move create mode 100644 bots/src/test/fundAccounts.ts create mode 100644 bots/src/test/storage.spec.ts delete mode 100644 bots/src/test/utils/DockerHelper.ts create mode 100644 bots/src/worker/bridgeExecutor/Resurrector.ts delete mode 100644 bots/src/worker/challenger/ChallegnerHelper.ts create mode 100644 bots/src/worker/outputSubmitter/db.ts delete mode 100644 bots/webpack.config.js create mode 100644 package-lock.json diff --git a/bots/.env b/bots/.env deleted file mode 100644 index 7e9c9c2e..00000000 --- a/bots/.env +++ /dev/null @@ -1,5 +0,0 @@ -INITIA_VERSION=v0.1.2-beta.0 -MINITIA_VERSION=v0.1.2-beta.0 -POSTGRES_USER=user -POSTGRES_PASSWORD=password -POSTGRES_DB=rollup \ No newline at end of file diff --git a/bots/.envrc_sample b/bots/.envrc_sample index 2a6906bd..41a2a02c 100644 --- a/bots/.envrc_sample +++ b/bots/.envrc_sample @@ -1,33 +1,36 @@ export TYPEORM_CONNECTION=postgres export TYPEORM_HOST=localhost -export TYPEORM_USERNAME=jungsuhan -export TYPEORM_PASSWORD=jungsuhan -export TYPEORM_DATABASE=challenger +export TYPEORM_USERNAME=user +export TYPEORM_PASSWORD=password +export TYPEORM_DATABASE=rollup export TYPEORM_PORT=5432 export TYPEORM_SYNCHRONIZE=true export TYPEORM_LOGGING=false export TYPEORM_ENTITIES=src/orm/*Entity.ts export USE_LOG_FILE=false -export EXECUTOR_PORT=3000 -export BATCH_PORT=3001 +export EXECUTOR_PORT=5000 +export BATCH_PORT=5001 -export L1_LCD_URI=https://next-stone-rest.initia.tech -export L1_RPC_URI=https://next-stone-rpc.initia.tech -export L2_LCD_URI=http://localhost:1318 -export L2_RPC_URI=http://localhost:26658 -export L2ID=0x56ccf33c45b99546cd1da172cf6849395bbf8573::s10r1::Minitia +# l2 setup (need challenger, output, executor mnemonic) +export SUBMISSION_INTERVAL=10000 +export FINALIZED_TIME=10000 +export IBC_METADATA='channel-1' +export L1_LCD_URI=http://localhost:1317 +export L1_RPC_URI=http://localhost:26657 +export L2_LCD_URI=http://localhost:1318 +export L2_RPC_URI=http://localhost:26667 +export BRIDGE_ID=1 # executor config -export EXECUTOR_MNEMONIC='recycle sight world spoon leopard shine dizzy before public use jungle either arctic detail hawk output option august hedgehog menu keen night work become' +export EXECUTOR_MNEMONIC='' # batch submitter config -export BATCH_SUBMITTER_MNEMONIC='broken umbrella tent include pet simple amount renew insect page sound whip shock dynamic deputy left outside churn lounge raise mirror toss annual fat' +export BATCH_SUBMITTER_MNEMONIC='' # challenger config -export CHALLENGER_MNEMONIC='purity yard brush wagon note forest athlete seek offer clown surround cover present ski bargain obvious cute admit gloom text shaft super impose rubber' +export CHALLENGER_MNEMONIC='' # output submitter config -export OUTPUT_SUBMITTER_MNEMONIC='airport hidden cake dry bleak alcohol enough tower charge cash modify feature analyst suffer bus oyster initial coffee wine plug paper damp sock afraid' -export EXECUTOR_URI=http://localhost:3000 \ No newline at end of file +export OUTPUT_SUBMITTER_MNEMONIC='' \ No newline at end of file diff --git a/bots/.gitignore b/bots/.gitignore index 6c4b4922..9dd76f2e 100644 --- a/bots/.gitignore +++ b/bots/.gitignore @@ -11,6 +11,7 @@ apidoc/ static/ .envrc +.env build logs/ testing/ diff --git a/bots/.gitmodules b/bots/.gitmodules deleted file mode 100644 index 9afa3a73..00000000 --- a/bots/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "docker"] - path = local-initia - url = https://github.com/initia-labs/local-initia diff --git a/bots/Dockerfile b/bots/Dockerfile deleted file mode 100644 index b8b1a6db..00000000 --- a/bots/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM node:lts-alpine as builder - -RUN apk add --no-cache python3 make g++ libc6-compat - -WORKDIR /app - -COPY package*.json ./ -RUN npm install -RUN npm install -g ts-node typescript - -FROM node:lts-alpine - -WORKDIR /app - -COPY . . - -COPY --from=builder /app/node_modules ./node_modules/ -COPY --from=builder /usr/local/bin/ts-node /usr/local/bin/ts-node -COPY --from=builder /usr/local/bin/tsc /usr/local/bin/tsc - -ENTRYPOINT [ "./entrypoint.sh" ] -CMD [ "test:integration" ] \ No newline at end of file diff --git a/bots/README.md b/bots/README.md index cf251db6..d765891f 100644 --- a/bots/README.md +++ b/bots/README.md @@ -1,4 +1,4 @@ -# Initia Rollup JS +# OPinit Bots Initia Optimistic Rollup Bots. @@ -9,116 +9,91 @@ Initia Optimistic Rollup Bots. ## How to use -## Setup L2 +## Create Bridge -Initializes the L2 id and op-bridge/output contracts. -You should set `submissionInterval`, `finalizedTime` and `l2StartBlockHeight` before initializing. +Before running rollup bots, you should create bridge between L1 and L2. If you use `initia.js`, you can create bridge using `MsgCreateBridge` message as follows. -```bash -export SUB_INTV=10 -export FIN_TIME=10 -export L2_HEIGHT=1 -npm run l2setup -``` +```typescript +import { MsgCreateBridge, BridgeConfig, Duration } from '@initia/initia.js'; -## Bridge Executor +const bridgeConfig = new BridgeConfig( + challenger.key.accAddress, + outputSubmitter.key.accAddress, + Duration.fromString(submissionInterval.toString()), + Duration.fromString(finalizedTime.toString()), + new Date(), + this.metadata +); +const msg = new MsgCreateBridge(executor.key.accAddress, bridgeConfig); +``` -Bridge executor is a bot that monitor L1, L2 node and execute bridge transaction. It will execute following steps. +## Configuration -1. Publish L2 ID to L1 - - L2 ID should be published under executor account -2. Initialize bridge contract on L1 with L2 ID - - Execute `initialize` entry function in `bridge.move` -3. Run executor bot - - Execute L1, L2 monitor in bridge executor +| Name | Description | Default | +| ------------------------- | ------------------------------------------------------ | -------------------------------- | +| L1_LCD_URI | L1 node LCD URI | | +| L1_RPC_URI | L1 node RPC URI | | +| L2_LCD_URI | L2 node LCD URI | | +| L2_RPC_URI | L2 node RPC URI | | +| BRIDGE_ID | Bridge ID | '' | +| EXECUTOR_PORT | Executor port | 5000 | +| BATCH_PORT | Batch submitter port | 5001 | +| EXECUTOR_MNEMONIC | Mnemonic seed for executor | '' | +| BATCH_SUBMITTER_MNEMONIC | Mnemonic seed for submitter | '' | +| OUTPUT_SUBMITTER_MNEMONIC | Mnemonic seed for output submitter | '' | +| CHALLENGER_MNEMONIC | Mnemonic seed for challenger | '' | - ```bash - npm run executor - ``` +> In OPinit bots, we use [direnv](https://direnv.net) for managing environment variable for development. See [sample of .envrc](.envrc_sample). - - If you use pm2, you can run executor with following command. +## Bridge Executor - ```bash - pm2 start executor.json - ``` +Bridge executor is a bot that monitor L1, L2 node and execute bridge transaction. It will execute following steps. -4. Register coin to bridge store and prepare deposit store - - Execute `register_token` -5. Now you can deposit after registering coin is done +1. Set bridge executor mnemonic on `.envrc`. + ```bash + export EXECUTOR_MNEMONIC="..." + ``` +2. Run executor bot + ```bash + npm run executor + ``` ## Batch Submitter Batch submitter is a background process that submits transaction batches to the BatchInbox module of L1. -You can run with following command. - -```bash -npm run batch -``` - -If you use pm2, -```bash -pm2 start batch.json -``` +1. Set batch submitter mnemonic on `.envrc`. + ```bash + export BATCH_SUBMITTER_MNEMONIC="..." + ``` +2. Run batch submitter bot + ```bash + npm run batch + ``` ## Output Submitter Output submitter is the component to store the L2 output root for block finalization. -Output submitter will get the L2 output results from executor and submit it to L1 using `propose_l2_output` in `output.move`. - -```bash -npm run output -``` +Output submitter will get the L2 output results from executor and submit it to L1. -If you use pm2, - -```bash -pm2 start output.json -``` +1. Set output submitter mnemonic on `.envrc`. + ```bash + export OUTPUT_SUBMITTER_MNEMONIC="..." + ``` +2. Run output submitter bot + ```bash + npm run output + ``` ## Challenger Challenger is an entity capable of deleting invalid output proposals from the output oracle. -```bash -npm run challenger -``` - -If you use pm2, - -```bash -pm2 start challenger.json -``` - -## Configuration - -| Name | Description | Default | -| ------------------------- | ------------------------------------------------------ | -------------------------------- | -| L1_LCD_URI | L1 node LCD URI | ' | -| L1_RPC_URI | L1 node RPC URI | ' | -| L2_LCD_URI | L2 node LCD URI | | -| L2_RPC_URI | L2 node RPC URI | | -| L2ID | L2ID | '' | -| BATCH_PORT | Batch submitter port | 3000 | -| EXECUTOR_PORT | Executor port | 3001 | -| EXECUTOR_URI | Executor URI (for output submitter) | | -| EXECUTOR_MNEMONIC | Mnemonic seed for executor | '' | -| BATCH_SUBMITTER_MNEMONIC | Mnemonic seed for submitter | '' | -| OUTPUT_SUBMITTER_MNEMONIC | Mnemonic seed for output submitter | '' | -| CHALLENGER_MNEMONIC | Mnemonic seed for challenger | '' | - -> In Batch Submitter, we use [direnv](https://direnv.net) for managing environment variable for development. See [sample of .envrc](.envrc_sample) - -## Test - -Docker and docker-compose are required to run integration test. - -```bash -npm run test:integration -``` - -If you want to reset docker container, run following command. - -```bash -./docker-compose-reset -``` +1. Set challenger mnemonic on `.envrc`. + ```bash + export CHALLENGER_MNEMONIC="..." + ``` +2. Run challenger bot + ```bash + npm run challenger + ``` \ No newline at end of file diff --git a/bots/batch.json b/bots/batch.json deleted file mode 100644 index fea141b5..00000000 --- a/bots/batch.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "apps" : [ - { - "name" : "batch", - "cwd" : ".", - "script" : "npm run batch", - "watch" : true, - "autorestart" : true - } - ] -} \ No newline at end of file diff --git a/bots/challenger.json b/bots/challenger.json deleted file mode 100644 index 9cb461ad..00000000 --- a/bots/challenger.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "apps" : [ - { - "name" : "challenger", - "cwd" : ".", - "script" : "npm run challenger", - "watch" : true, - "autorestart" : true - } - ] -} \ No newline at end of file diff --git a/bots/docker-compose-reset b/bots/docker-compose-reset deleted file mode 100755 index 280c8496..00000000 --- a/bots/docker-compose-reset +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -docker-compose down --rmi all --volumes - -if [ "$(docker images -q)" ]; then - docker rmi -f $(docker images -q) -fi \ No newline at end of file diff --git a/bots/docker-compose.yml b/bots/docker-compose.yml deleted file mode 100644 index fe1643f6..00000000 --- a/bots/docker-compose.yml +++ /dev/null @@ -1,73 +0,0 @@ -version: "3.8" - -services: - initia: - build: - context: ./local-initia - dockerfile: Dockerfile.initia - args: - INITIA_VERSION: ${INITIA_VERSION} - image: ghcr.io/initia-labs/initiad:${INITIA_VERSION} - pull_policy: if_not_present - hostname: initia - platform: linux/amd64 - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:26657"] - interval: 20s - timeout: 20s - retries: 3 - volumes: - - initia:/app - networks: - - default - ports: - - "1317:1317" - - "26657:26657" - minitia: - build: - context: ./local-initia - dockerfile: Dockerfile.minitia - args: - MINITIA_VERSION: ${MINITIA_VERSION} - image: ghcr.io/initia-labs/minitiad:${MINITIA_VERSION} - pull_policy: if_not_present - hostname: minitia - platform: linux/amd64 - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:26657"] - interval: 20s - timeout: 20s - retries: 3 - volumes: - - minitia:/app - networks: - - default - ports: - - "1318:1317" - - "26658:26657" - postgres: - image: postgres:13 - pull_policy: if_not_present - environment: - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_DB: ${POSTGRES_DB} - healthcheck: - test: ["CMD-SHELL", "pg_isready -U user"] - interval: 20s - timeout: 20s - retries: 3 - volumes: - - postgres_data:/var/lib/postgresql/data - networks: - - default - ports: - - "5433:5432" -volumes: - initia: - minitia: - postgres_data: - driver: local - -networks: - default: \ No newline at end of file diff --git a/bots/dockerfile b/bots/dockerfile new file mode 100644 index 00000000..08c5489c --- /dev/null +++ b/bots/dockerfile @@ -0,0 +1,15 @@ +FROM node:16 AS builder +WORKDIR /usr/src/app + +COPY package*.json ./ + +RUN npm install + +COPY . . + +EXPOSE 5000 +EXPOSE 5001 + +RUN ["chmod", "+x", "./entrypoint.sh"] + +ENTRYPOINT [ "./entrypoint.sh" ] \ No newline at end of file diff --git a/bots/entrypoint.sh b/bots/entrypoint.sh old mode 100755 new mode 100644 index 05354eae..a6543ade --- a/bots/entrypoint.sh +++ b/bots/entrypoint.sh @@ -1,4 +1,6 @@ #!/usr/bin/env sh npm run apidoc -exec npm run "$@" \ No newline at end of file + +exec npm run $1 + diff --git a/bots/env-gen.sh b/bots/env-gen.sh new file mode 100755 index 00000000..c2a52521 --- /dev/null +++ b/bots/env-gen.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# convert envrc to env +if [ ! -f .envrc ]; then + echo ".envrc not exist." + exit 1 +fi + +cp .envrc .envrc.tmp + +# remove '' in linux +case "$(uname)" in + Linux*) sed -i 's/localhost/host.docker.internal/g' .envrc.tmp ;; + Darwin*) sed -i '' 's/localhost/host.docker.internal/g' .envrc.tmp ;; + *) echo "Unsupported OS"; exit 1 ;; +esac + +sed 's/^export //g' .envrc.tmp > .env +rm .envrc.tmp + +echo ".env generated." \ No newline at end of file diff --git a/bots/executor.json b/bots/executor.json deleted file mode 100644 index 499f2c66..00000000 --- a/bots/executor.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "apps" : [ - { - "name" : "executor", - "cwd" : ".", - "script" : "npm run executor", - "watch" : true, - "autorestart" : true, - "args": [ "--color"] - } - ] -} diff --git a/bots/output.json b/bots/output.json deleted file mode 100644 index 3c1a8e1c..00000000 --- a/bots/output.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "apps" : [ - { - "name" : "output", - "cwd" : ".", - "script" : "npm run output", - "watch" : true, - "autorestart" : true, - "restart_delay" : 10000 - } - ] -} \ No newline at end of file diff --git a/bots/package-lock.json b/bots/package-lock.json index 747e4aa8..5777d1f6 100644 --- a/bots/package-lock.json +++ b/bots/package-lock.json @@ -10,8 +10,7 @@ "license": "MIT", "dependencies": { "@initia/builder.js": "^0.0.9", - "@initia/initia.js": "^0.1.10", - "@initia/minitia.js": "^0.0.8", + "@initia/initia.js": "^0.1.19", "@koa/cors": "^4.0.0", "@sentry/node": "^7.60.0", "@types/bluebird": "^3.5.38", @@ -20,6 +19,7 @@ "@types/ws": "^8.5.5", "apidoc": "^1.1.0", "apidoc-core": "^0.15.0", + "bignumber.js": "^9.1.2", "bluebird": "^3.7.2", "chalk": "^2.4.2", "dockerode": "^3.3.5", @@ -1130,15 +1130,16 @@ } }, "node_modules/@initia/initia.js": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/@initia/initia.js/-/initia.js-0.1.10.tgz", - "integrity": "sha512-qv3DPuXUfvPvWaQp/9MNgut9JbjAsflcPIFl4gUlN7tYn9re8FlqRxBjhCuIVBwOvyExVRInnCjacW26t7la9A==", + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@initia/initia.js/-/initia.js-0.1.19.tgz", + "integrity": "sha512-OKehNvBE4hbeM4HbqOEucb4IHWSOg8S9GnvcmItHqIhqKcDiX6ZkzthqdZz2q9E1xkiUHUckEbwkcZC/gf473g==", "dependencies": { - "@initia/initia.proto": "^0.1.12", + "@initia/initia.proto": "^0.1.18", + "@initia/opinit.proto": "^0.0.1", "@ledgerhq/hw-transport": "^6.27.12", "@ledgerhq/hw-transport-webhid": "^6.27.12", "@ledgerhq/hw-transport-webusb": "^6.27.12", - "@mysten/bcs": "^0.7.0", + "@mysten/bcs": "^0.8.1", "axios": "^0.27.2", "bech32": "^2.0.0", "bignumber.js": "^9.1.0", @@ -1155,10 +1156,18 @@ "node": ">=14" } }, + "node_modules/@initia/initia.js/node_modules/@mysten/bcs": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@mysten/bcs/-/bcs-0.8.1.tgz", + "integrity": "sha512-wSEdP7QEfGQdb34g+7R0f3OdRqrv88iIABfJVDVJ6IsGLYVILreh8dZfNpZNUUyzctiyhX7zB9e/lR5qkddFPA==", + "dependencies": { + "bs58": "^5.0.0" + } + }, "node_modules/@initia/initia.proto": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/@initia/initia.proto/-/initia.proto-0.1.12.tgz", - "integrity": "sha512-dJkCgMyIonVDdWWmcAkCLx7woejp0EWbC9EXT/3xHxXw+IiZJrNBJOzY2wAjOBfOwC7n7HCJkpFtp2i2rumuTQ==", + "version": "0.1.18", + "resolved": "https://registry.npmjs.org/@initia/initia.proto/-/initia.proto-0.1.18.tgz", + "integrity": "sha512-98hSjstgjjzfasHoGIixWP0DiC1kNONyfpNCZxQ21DJAmK6wn1L/Ae51tF5N79J4UAstwTTXkmKrPDRGla9fJA==", "dependencies": { "@improbable-eng/grpc-web": "^0.15.0", "google-protobuf": "^3.21.0", @@ -1166,36 +1175,10 @@ "protobufjs": "^7.1.1" } }, - "node_modules/@initia/minitia.js": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@initia/minitia.js/-/minitia.js-0.0.8.tgz", - "integrity": "sha512-j+uv76pcXuvRf+LR4mLbR8wqRBbcxyzNycMBx6TGtZh5EDbtOzUfmg7aSJI/bOKT+cvXjFtNdKieiopMD/Pfzg==", - "dependencies": { - "@initia/minitia.proto": "^0.0.8", - "@ledgerhq/hw-transport": "^6.27.12", - "@ledgerhq/hw-transport-webhid": "^6.27.12", - "@ledgerhq/hw-transport-webusb": "^6.27.12", - "@mysten/bcs": "^0.7.0", - "axios": "^0.27.2", - "bech32": "^2.0.0", - "bignumber.js": "^9.1.0", - "bip32": "^2.0.6", - "bip39": "^3.0.4", - "jscrypto": "^1.0.3", - "long": "^5.2.0", - "ripemd160": "^2.0.2", - "secp256k1": "^4.0.3", - "tmp": "^0.2.1", - "ws": "^7.5.9" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@initia/minitia.proto": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@initia/minitia.proto/-/minitia.proto-0.0.8.tgz", - "integrity": "sha512-y5gIhiXbrrT6KRfqElwR+ZwXMEZnb9yS1bhCzoRnYmUHoujuuZdU8yzSs13wJFDxjV9EjW+lvgl+T3i46lEDFw==", + "node_modules/@initia/opinit.proto": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@initia/opinit.proto/-/opinit.proto-0.0.1.tgz", + "integrity": "sha512-tkb1y3k2hVMKqLUr1N15ZeTqgAgF/izjJ6e2rpIQxuB3mr3fqivHDFqiJ/hVe2FLSJE66VCztTQ0USG8YNr03g==", "dependencies": { "@improbable-eng/grpc-web": "^0.15.0", "google-protobuf": "^3.21.0", @@ -1975,14 +1958,6 @@ "resolved": "https://registry.npmjs.org/@ledgerhq/logs/-/logs-6.10.1.tgz", "integrity": "sha512-z+ILK8Q3y+nfUl43ctCPuR4Y2bIxk/ooCQFwZxhtci1EhAtMDzMAx2W25qx8G1PPL9UUOdnUax19+F0OjXoj4w==" }, - "node_modules/@mysten/bcs": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@mysten/bcs/-/bcs-0.7.3.tgz", - "integrity": "sha512-fbusBfsyc2MpTACi72H5edWJ670T84va+qn9jSPpb5BzZ+pzUM1Q0ApPrF5OT+mB1o5Ng+mxPQpBCZQkfiV2TA==", - "dependencies": { - "bs58": "^5.0.0" - } - }, "node_modules/@noble/hashes": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", @@ -17900,15 +17875,16 @@ } }, "@initia/initia.js": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/@initia/initia.js/-/initia.js-0.1.10.tgz", - "integrity": "sha512-qv3DPuXUfvPvWaQp/9MNgut9JbjAsflcPIFl4gUlN7tYn9re8FlqRxBjhCuIVBwOvyExVRInnCjacW26t7la9A==", + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@initia/initia.js/-/initia.js-0.1.19.tgz", + "integrity": "sha512-OKehNvBE4hbeM4HbqOEucb4IHWSOg8S9GnvcmItHqIhqKcDiX6ZkzthqdZz2q9E1xkiUHUckEbwkcZC/gf473g==", "requires": { - "@initia/initia.proto": "^0.1.12", + "@initia/initia.proto": "^0.1.18", + "@initia/opinit.proto": "^0.0.1", "@ledgerhq/hw-transport": "^6.27.12", "@ledgerhq/hw-transport-webhid": "^6.27.12", "@ledgerhq/hw-transport-webusb": "^6.27.12", - "@mysten/bcs": "^0.7.0", + "@mysten/bcs": "^0.8.1", "axios": "^0.27.2", "bech32": "^2.0.0", "bignumber.js": "^9.1.0", @@ -17920,12 +17896,22 @@ "secp256k1": "^4.0.3", "tmp": "^0.2.1", "ws": "^7.5.9" + }, + "dependencies": { + "@mysten/bcs": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@mysten/bcs/-/bcs-0.8.1.tgz", + "integrity": "sha512-wSEdP7QEfGQdb34g+7R0f3OdRqrv88iIABfJVDVJ6IsGLYVILreh8dZfNpZNUUyzctiyhX7zB9e/lR5qkddFPA==", + "requires": { + "bs58": "^5.0.0" + } + } } }, "@initia/initia.proto": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/@initia/initia.proto/-/initia.proto-0.1.12.tgz", - "integrity": "sha512-dJkCgMyIonVDdWWmcAkCLx7woejp0EWbC9EXT/3xHxXw+IiZJrNBJOzY2wAjOBfOwC7n7HCJkpFtp2i2rumuTQ==", + "version": "0.1.18", + "resolved": "https://registry.npmjs.org/@initia/initia.proto/-/initia.proto-0.1.18.tgz", + "integrity": "sha512-98hSjstgjjzfasHoGIixWP0DiC1kNONyfpNCZxQ21DJAmK6wn1L/Ae51tF5N79J4UAstwTTXkmKrPDRGla9fJA==", "requires": { "@improbable-eng/grpc-web": "^0.15.0", "google-protobuf": "^3.21.0", @@ -17933,33 +17919,10 @@ "protobufjs": "^7.1.1" } }, - "@initia/minitia.js": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@initia/minitia.js/-/minitia.js-0.0.8.tgz", - "integrity": "sha512-j+uv76pcXuvRf+LR4mLbR8wqRBbcxyzNycMBx6TGtZh5EDbtOzUfmg7aSJI/bOKT+cvXjFtNdKieiopMD/Pfzg==", - "requires": { - "@initia/minitia.proto": "^0.0.8", - "@ledgerhq/hw-transport": "^6.27.12", - "@ledgerhq/hw-transport-webhid": "^6.27.12", - "@ledgerhq/hw-transport-webusb": "^6.27.12", - "@mysten/bcs": "^0.7.0", - "axios": "^0.27.2", - "bech32": "^2.0.0", - "bignumber.js": "^9.1.0", - "bip32": "^2.0.6", - "bip39": "^3.0.4", - "jscrypto": "^1.0.3", - "long": "^5.2.0", - "ripemd160": "^2.0.2", - "secp256k1": "^4.0.3", - "tmp": "^0.2.1", - "ws": "^7.5.9" - } - }, - "@initia/minitia.proto": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@initia/minitia.proto/-/minitia.proto-0.0.8.tgz", - "integrity": "sha512-y5gIhiXbrrT6KRfqElwR+ZwXMEZnb9yS1bhCzoRnYmUHoujuuZdU8yzSs13wJFDxjV9EjW+lvgl+T3i46lEDFw==", + "@initia/opinit.proto": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@initia/opinit.proto/-/opinit.proto-0.0.1.tgz", + "integrity": "sha512-tkb1y3k2hVMKqLUr1N15ZeTqgAgF/izjJ6e2rpIQxuB3mr3fqivHDFqiJ/hVe2FLSJE66VCztTQ0USG8YNr03g==", "requires": { "@improbable-eng/grpc-web": "^0.15.0", "google-protobuf": "^3.21.0", @@ -18575,14 +18538,6 @@ "resolved": "https://registry.npmjs.org/@ledgerhq/logs/-/logs-6.10.1.tgz", "integrity": "sha512-z+ILK8Q3y+nfUl43ctCPuR4Y2bIxk/ooCQFwZxhtci1EhAtMDzMAx2W25qx8G1PPL9UUOdnUax19+F0OjXoj4w==" }, - "@mysten/bcs": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@mysten/bcs/-/bcs-0.7.3.tgz", - "integrity": "sha512-fbusBfsyc2MpTACi72H5edWJ670T84va+qn9jSPpb5BzZ+pzUM1Q0ApPrF5OT+mB1o5Ng+mxPQpBCZQkfiV2TA==", - "requires": { - "bs58": "^5.0.0" - } - }, "@noble/hashes": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", diff --git a/bots/package.json b/bots/package.json index e3357b6c..634ce1ec 100644 --- a/bots/package.json +++ b/bots/package.json @@ -7,14 +7,13 @@ "node": ">=16" }, "scripts": { - "start": "concurrently \"npm:executor\" \"npm:output\"", "executor": "ts-node -r tsconfig-paths/register ./src/worker/bridgeExecutor/index.ts", - "batch": "sleep 5 && ts-node -r tsconfig-paths/register ./src/worker/batchSubmitter/index.ts", - "challenger": "sleep 5 && ts-node -r tsconfig-paths/register ./src/worker/challenger/index.ts", - "output": "sleep 5 && ts-node -r tsconfig-paths/register ./src/worker/outputSubmitter/index.ts", + "batch": "ts-node -r tsconfig-paths/register ./src/worker/batchSubmitter/index.ts", + "challenger": "ts-node -r tsconfig-paths/register ./src/worker/challenger/index.ts", + "output": "ts-node -r tsconfig-paths/register ./src/worker/outputSubmitter/index.ts", "build": "tsc --module commonjs && webpack --mode production", "test": "jest", - "test:integration": "export DEVELOPMENT_MODE=test && ts-node -r tsconfig-paths/register ./src/test/integration.ts", + "test:integration": "ts-node -r tsconfig-paths/register ./src/test/integration.ts", "prettier": "prettier --write ./src/**/**/**/**/*.ts", "lint": "eslint src --ext .js,.jsx,.ts,.tsx", "do": "ts-node -T --files -r tsconfig-paths/register", @@ -69,8 +68,7 @@ }, "dependencies": { "@initia/builder.js": "^0.0.9", - "@initia/initia.js": "^0.1.10", - "@initia/minitia.js": "^0.0.8", + "@initia/initia.js": "^0.1.19", "@koa/cors": "^4.0.0", "@sentry/node": "^7.60.0", "@types/bluebird": "^3.5.38", @@ -79,6 +77,7 @@ "@types/ws": "^8.5.5", "apidoc": "^1.1.0", "apidoc-core": "^0.15.0", + "bignumber.js": "^9.1.2", "bluebird": "^3.7.2", "chalk": "^2.4.2", "dockerode": "^3.3.5", diff --git a/bots/pm2.json b/bots/pm2.json new file mode 100644 index 00000000..8593856e --- /dev/null +++ b/bots/pm2.json @@ -0,0 +1,34 @@ +{ + "apps" : [ + { + "name" : "executor", + "cwd" : ".", + "script" : "npm run executor", + "watch" : true, + "autorestart" : true, + "args": [ "--color"] + }, + { + "name" : "output", + "cwd" : ".", + "script" : "npm run output", + "watch" : true, + "autorestart" : true, + "restart_delay" : 10000 + }, + { + "name" : "batch", + "cwd" : ".", + "script" : "npm run batch", + "watch" : true, + "autorestart" : true + }, + { + "name" : "challenger", + "cwd" : ".", + "script" : "npm run challenger", + "watch" : true, + "autorestart" : true + } + ] +} \ No newline at end of file diff --git a/bots/src/config.ts b/bots/src/config.ts index bf39a6b0..0ed1673b 100644 --- a/bots/src/config.ts +++ b/bots/src/config.ts @@ -1,39 +1,41 @@ -import { LCDClient as MinitiaLCDClient } from '@initia/minitia.js'; -import { LCDClient as InitiaLCDClient } from '@initia/initia.js'; +import { LCDClient } from '@initia/initia.js'; interface ConfigInterface { EXECUTOR_PORT: number; BATCH_PORT: number; - L1_LCD_URI: string; - L1_RPC_URI: string; - L2_LCD_URI: string; - L2_RPC_URI: string; - EXECUTOR_URI: string; - L2ID: string; + L1_LCD_URI: string[]; + L1_RPC_URI: string[]; + L2_LCD_URI: string[]; + L2_RPC_URI: string[]; + EXECUTOR_URI: string; // only for test + BRIDGE_ID: number; OUTPUT_SUBMITTER_MNEMONIC: string; EXECUTOR_MNEMONIC: string; BATCH_SUBMITTER_MNEMONIC: string; CHALLENGER_MNEMONIC: string; USE_LOG_FILE: boolean; - l1lcd: InitiaLCDClient; - l2lcd: MinitiaLCDClient; - EXCLUDED_ROUTES: string[]; + l1lcd: LCDClient; + l2lcd: LCDClient; + L2_DENOM: string; } const defaultConfig = { - EXECUTOR_PORT: '3000', - BATCH_PORT: '3001', + EXECUTOR_PORT: '5000', + BATCH_PORT: '5001', L1_LCD_URI: 'https://stone-rest.initia.tech', L1_RPC_URI: 'https://stone-rpc.initia.tech', L2_LCD_URI: 'https://minitia-rest.initia.tech', L2_RPC_URI: 'https://minitia-rpc.initia.tech', EXECUTOR_URI: 'https://minitia-executor.initia.tech', - L2ID: '', + BRIDGE_ID: '', OUTPUT_SUBMITTER_MNEMONIC: '', EXECUTOR_MNEMONIC: '', BATCH_SUBMITTER_MNEMONIC: '', CHALLENGER_MNEMONIC: '', - USE_LOG_FILE: 'false' + USE_LOG_FILE: 'false', + L2_DENOM: 'umin', + L1_CHAIN_ID: '', + L2_CHAIN_ID: '' }; export class Config implements ConfigInterface { @@ -41,20 +43,22 @@ export class Config implements ConfigInterface { EXECUTOR_PORT: number; BATCH_PORT: number; - L1_LCD_URI: string; - L1_RPC_URI: string; - L2_LCD_URI: string; - L2_RPC_URI: string; + L1_LCD_URI: string[]; + L1_RPC_URI: string[]; + L2_LCD_URI: string[]; + L2_RPC_URI: string[]; EXECUTOR_URI: string; - L2ID: string; + BRIDGE_ID: number; OUTPUT_SUBMITTER_MNEMONIC: string; EXECUTOR_MNEMONIC: string; BATCH_SUBMITTER_MNEMONIC: string; CHALLENGER_MNEMONIC: string; USE_LOG_FILE: boolean; - l1lcd: InitiaLCDClient; - l2lcd: MinitiaLCDClient; - EXCLUDED_ROUTES: string[] = []; + l1lcd: LCDClient; + l2lcd: LCDClient; + L2_DENOM: string; + L1_CHAIN_ID: string; + L2_CHAIN_ID: string; private constructor() { const { @@ -65,34 +69,45 @@ export class Config implements ConfigInterface { L2_LCD_URI, L2_RPC_URI, EXECUTOR_URI, - L2ID, + BRIDGE_ID, OUTPUT_SUBMITTER_MNEMONIC, EXECUTOR_MNEMONIC, BATCH_SUBMITTER_MNEMONIC, CHALLENGER_MNEMONIC, - USE_LOG_FILE + USE_LOG_FILE, + L2_DENOM, + L1_CHAIN_ID, + L2_CHAIN_ID } = { ...defaultConfig, ...process.env }; this.EXECUTOR_PORT = parseInt(EXECUTOR_PORT); this.BATCH_PORT = parseInt(BATCH_PORT); - this.L1_LCD_URI = L1_LCD_URI; - this.L1_RPC_URI = L1_RPC_URI; - this.L2_LCD_URI = L2_LCD_URI; - this.L2_RPC_URI = L2_RPC_URI; + this.L1_LCD_URI = L1_LCD_URI.split(','); + this.L1_RPC_URI = L1_RPC_URI.split(','); + this.L2_LCD_URI = L2_LCD_URI.split(','); + this.L2_RPC_URI = L2_RPC_URI.split(','); this.EXECUTOR_URI = EXECUTOR_URI; - this.L2ID = L2ID; - this.OUTPUT_SUBMITTER_MNEMONIC = OUTPUT_SUBMITTER_MNEMONIC; - this.EXECUTOR_MNEMONIC = EXECUTOR_MNEMONIC; - this.BATCH_SUBMITTER_MNEMONIC = BATCH_SUBMITTER_MNEMONIC; - this.CHALLENGER_MNEMONIC = CHALLENGER_MNEMONIC; + this.BRIDGE_ID = parseInt(BRIDGE_ID); + this.OUTPUT_SUBMITTER_MNEMONIC = OUTPUT_SUBMITTER_MNEMONIC.replace( + /'/g, + '' + ); + this.EXECUTOR_MNEMONIC = EXECUTOR_MNEMONIC.replace(/'/g, ''); + this.BATCH_SUBMITTER_MNEMONIC = BATCH_SUBMITTER_MNEMONIC.replace(/'/g, ''); + this.CHALLENGER_MNEMONIC = CHALLENGER_MNEMONIC.replace(/'/g, ''); this.USE_LOG_FILE = !!JSON.parse(USE_LOG_FILE); - this.l1lcd = new InitiaLCDClient(L1_LCD_URI, { + this.l1lcd = new LCDClient(this.L1_LCD_URI[0], { gasPrices: '0.15uinit', - gasAdjustment: '10' + gasAdjustment: '2' }); - this.l2lcd = new MinitiaLCDClient(L2_LCD_URI, { - gasPrices: '0.15umin', - gasAdjustment: '10' + + this.L2_DENOM = L2_DENOM; + this.L1_CHAIN_ID = L1_CHAIN_ID; + this.L2_CHAIN_ID = L2_CHAIN_ID; + + this.l2lcd = new LCDClient(this.L2_LCD_URI[0], { + gasPrices: `0.15${this.L2_DENOM}`, + gasAdjustment: '2' }); } @@ -102,48 +117,15 @@ export class Config implements ConfigInterface { } return Config.instance; } - - public static updateConfig(newConfig: Partial) { - Config.instance = { ...Config.instance, ...newConfig }; - } } export function getConfig() { - if (process.env.DEVELOPMENT_MODE === 'test') { - process.env.TYPEORM_HOST = 'localhost'; - process.env.TYPEORM_USERNAME = 'user'; - process.env.TYPEORM_PASSWORD = 'password'; - process.env.TYPEORM_DATABASE = 'rollup'; - process.env.TYPEORM_PORT = '5433'; - - const testConfig = { - EXECUTOR_PORT: 3000, - BATCH_PORT: 3001, - L1_LCD_URI: 'http://localhost:1317', - L1_RPC_URI: 'http://localhost:26657', - L2_LCD_URI: 'http://localhost:1318', - L2_RPC_URI: 'http://localhost:26658', - EXECUTOR_URI: 'http://localhost:3000', - TYPEORM_HOST: 'http://localhost:5433' - }; - Config.updateConfig({ - ...testConfig, - l1lcd: new InitiaLCDClient(testConfig.L1_LCD_URI, { - gasAdjustment: '10' - }), - l2lcd: new MinitiaLCDClient(testConfig.L2_LCD_URI, { - gasPrices: '0.15umin', - gasAdjustment: '10' - }) - }); - } - return Config.getConfig(); } const config = Config.getConfig(); export default config; -export const INTERVAL_BATCH = 10000; +export const INTERVAL_BATCH = 10_000; export const INTERVAL_MONITOR = 100; -export const INTERVAL_OUTPUT = 10000; +export const INTERVAL_OUTPUT = 10_000; diff --git a/bots/src/controller/executor/CoinController.ts b/bots/src/controller/executor/CoinController.ts deleted file mode 100644 index aaea5a12..00000000 --- a/bots/src/controller/executor/CoinController.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { - KoaController, - Controller, - Get, - Validate, - Validator -} from 'koa-joi-controllers'; -import { success } from '../../lib/response'; -import { getCoin, getAllCoins } from '../../service/executor/CoinService'; -import { ErrorCodes } from 'lib/error'; - -const Joi = Validator.Joi; - -@Controller('') -export default class CoinController extends KoaController { - /** - * @api {get} /coin Get all coin mapping - * @apiName getAllCoins - * @apiGroup Coin - * - * @apiSuccess {Object[]} coins Coin mapping list - */ - @Get('/coin') - async getAllCoins(ctx): Promise { - success(ctx, await getAllCoins()); - } - - /** - * @api {get} /coin/:metadata Get coin mapping - * @apiName getCoin - * @apiGroup Coin - * - * @apiParam {String} l1Metadata L1 coin metadata - * - * @apiSuccess {String} l1Metadata L1 coin metadata - * @apiSuccess {String} l1Denom L1 coin denom - * @apiSuccess {String} l2Metadata L2 coin metadata - * @apiSuccess {String} l2Denom L2 coin denom - * - */ - @Get('/coin/:metadata') - @Validate({ - params: { - metadata: Joi.string().description('Coin type') - }, - failure: ErrorCodes.INVALID_REQUEST_ERROR - }) - async getCoin(ctx): Promise { - success(ctx, await getCoin(ctx.params.metadata)); - } -} diff --git a/bots/src/controller/executor/DepositTxController.ts b/bots/src/controller/executor/DepositTxController.ts new file mode 100644 index 00000000..22b7b4b9 --- /dev/null +++ b/bots/src/controller/executor/DepositTxController.ts @@ -0,0 +1,50 @@ +import { Context } from 'koa'; +import { + KoaController, + Validate, + Get, + Controller, + Validator +} from 'koa-joi-controllers'; +import { ErrorCodes } from 'lib/error'; +import { success } from 'lib/response'; +import { getDepositTx } from 'service'; + +const Joi = Validator.Joi; + +@Controller('') +export class DepositTxController extends KoaController { + /** + * + * @api {get} /tx/:bridge_id/:sequence Get tx entity + * @apiName getTx + * @apiGroup Tx + * + * @apiParam {String} bridge_id L2 bridge id + * @apiParam {Number} sequence L2 deposit tx sequence + * + * @apiSuccess {String} bridge_id L2 bridge id + * @apiSuccess {Number} sequence L2 sequence + * @apiSuccess {String} l1Denom Deposit coin L1 denom + * @apiSuccess {String} l2Denom Deposit coin L2 denom + * @apiSuccess {String} sender Deposit tx sender + * @apiSuccess {String} receiver Deposit tx receiver + * @apiSuccess {Number} amount Deposit amount + * @apiSuccess {Number} outputIndex Output index + * @apiSuccess {String} data Deposit tx data + * @apiSuccess {Number} l1Height L1 height of deposit tx + */ + @Get('/tx/deposit/:bridge_id/:sequence') + @Validate({ + params: { + bridge_id: Joi.string().description('L2 bridge id'), + sequence: Joi.number().description('Sequence') + }, + failure: ErrorCodes.INVALID_REQUEST_ERROR + }) + async getDepositTx(ctx: Context): Promise { + const bridge_id: string = ctx.params.bridge_id as string; + const sequence: number = ctx.params.sequence as number; + success(ctx, await getDepositTx(bridge_id, sequence)); + } +} diff --git a/bots/src/controller/executor/TxController.ts b/bots/src/controller/executor/WithdrawalTxController.ts similarity index 59% rename from bots/src/controller/executor/TxController.ts rename to bots/src/controller/executor/WithdrawalTxController.ts index 453aae87..713e0e73 100644 --- a/bots/src/controller/executor/TxController.ts +++ b/bots/src/controller/executor/WithdrawalTxController.ts @@ -8,23 +8,25 @@ import { } from 'koa-joi-controllers'; import { ErrorCodes } from 'lib/error'; import { success } from 'lib/response'; -import { getTx } from 'service'; +import { getWithdrawalTx } from 'service'; const Joi = Validator.Joi; @Controller('') -export class TxController extends KoaController { +export class WithdrawalTxController extends KoaController { /** * - * @api {get} /tx/:l1_metadata/:sequence Get tx entity + * @api {get} /tx/:bridge_id/:sequence Get tx entity * @apiName getTx * @apiGroup Tx * - * @apiParam {String} l1Metadata L1 coin metadata + * @apiParam {String} bridge_id L2 bridge id * @apiParam {Number} sequence L2 withdrawal tx sequence * - * @apiSuccess {String} l1Metadata L1 coin metadata + * @apiSuccess {String} bridge_id L2 bridge id * @apiSuccess {Number} sequence L2 sequence + * @apiSuccess {String} l1Denom Withdrawal coin L1 denom + * @apiSuccess {String} l2Denom Withdrawal coin L2 denom * @apiSuccess {String} sender Withdrawal tx sender * @apiSuccess {String} receiver Withdrawal tx receiver * @apiSuccess {Number} amount Withdrawal amount @@ -32,17 +34,17 @@ export class TxController extends KoaController { * @apiSuccess {String} merkleRoot Withdrawal tx merkle root * @apiSuccess {String[]} merkleProof Withdrawal txs merkle proof */ - @Get('/tx/:l1_metadata/:sequence') + @Get('/tx/withdrawal/:bridge_id/:sequence') @Validate({ params: { - l1_metadata: Joi.string().description('L1 Metadata'), + bridge_id: Joi.string().description('L2 bridge id'), sequence: Joi.number().description('Sequence') }, failure: ErrorCodes.INVALID_REQUEST_ERROR }) - async getTx(ctx: Context): Promise { - const l1_metadata: string = ctx.params.l1_metadata as string; + async getWithdrawalTx(ctx: Context): Promise { + const bridge_id: string = ctx.params.bridge_id as string; const sequence: number = ctx.params.sequence as number; - success(ctx, await getTx(l1_metadata, sequence)); + success(ctx, await getWithdrawalTx(bridge_id, sequence)); } } diff --git a/bots/src/controller/index.ts b/bots/src/controller/index.ts index 1d8720f3..b7103f09 100644 --- a/bots/src/controller/index.ts +++ b/bots/src/controller/index.ts @@ -1,13 +1,13 @@ import { KoaController } from 'koa-joi-controllers'; import BatchController from './batch/BatchController'; import { OutputController } from './executor/OutputController'; -import { TxController } from './executor/TxController'; -import CoinController from './executor/CoinController'; +import { WithdrawalTxController } from './executor/WithdrawalTxController'; +import { DepositTxController } from './executor/DepositTxController'; export const executorController = [ OutputController, - TxController, - CoinController + WithdrawalTxController, + DepositTxController ].map((prototype) => new prototype()) as KoaController[]; export const batchController = [BatchController].map( diff --git a/bots/src/lib/apiRequest.ts b/bots/src/lib/apiRequest.ts deleted file mode 100644 index bdbdf39c..00000000 --- a/bots/src/lib/apiRequest.ts +++ /dev/null @@ -1,59 +0,0 @@ -import Axios, { AxiosInstance } from 'axios'; - -export interface OutputProposedEvent { - l2_id: string; - output_root: number[]; - output_index: number; - l2_block_number: number; - l1_timestamp: number; -} - -export interface Tx { - id: string; - info: string; - height: string; - txhash: string; - data?: string; - code?: number; - codespace?: string; - raw_log?: string; - logs: { - log: string; - events: Event[]; - msg_index: number; - }[]; - events: Event[]; - gas_wanted: string; - gas_used: string; - tx: any; - timestamp: string; -} - -export interface Event { - type: string; - attributes: { - key: string; - index?: boolean; - value: string; - }[]; -} - -export class APIRequest { - private api: AxiosInstance; - - constructor(baseURL: string) { - this.api = Axios.create({ - baseURL, - timeout: 30000 - }); - } - - public async getQuery(url: string): Promise { - const response = await this.api.get(url); - return response.data; - } - - public async getBlock(blockHeight: number): Promise { - return this.getQuery(`/v1/blocks/${blockHeight}`); - } -} diff --git a/bots/src/lib/lcd.ts b/bots/src/lib/lcd.ts deleted file mode 100644 index c6041912..00000000 --- a/bots/src/lib/lcd.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { BridgeConfig } from './types'; -import { getConfig } from 'config'; -import { BCS, Wallet, MnemonicKey } from '@initia/initia.js'; -import * as crypto from 'crypto'; - -const config = getConfig(); -const bcs = BCS.getInstance(); - -const executor = new Wallet( - config.l1lcd, - new MnemonicKey({ mnemonic: config.EXECUTOR_MNEMONIC }) -); - -export interface FAMetadata { - name: string; - symbol: string; - decimals: string; - icon_url: string; - reference_url: string; -} - -export interface CoinInfo { - denom: string; - name: string; - symbol: string; - decimals: number; -} - -export async function fetchBridgeConfig(): Promise { - const cfg = await config.l1lcd.move.viewFunction( - '0x1', - 'op_output', - 'get_config_store', - [], - [ - bcs.serialize('address', executor.key.accAddress), - bcs.serialize('string', config.L2ID) - ] - ); - return cfg; -} - -const OBJECT_DERIVED_SCHEME = 0xfc; -const OBJECT_FROM_SEED_ADDRESS_SCHEME = 0xfe; -const BRIDGE_PREFIX = 0xf2; - -export function normalizeMetadata(addr: string) { - return addr.startsWith('0x') ? addr : '0x' + addr; -} - -export function computeBridgeAddress(creator:string, l2Id: string) { - const addrBytes = Buffer.from( - bcs.serialize('address', creator), - 'base64' - ).toJSON().data; - const combinedSeed = [BRIDGE_PREFIX, ...Buffer.from(l2Id)]; - const combinedBytes = [ - ...addrBytes, - ...combinedSeed, - OBJECT_FROM_SEED_ADDRESS_SCHEME - ]; - - const hash = crypto - .createHash('SHA3-256') - .update(Buffer.from(combinedBytes)) - .digest(); - return normalizeMetadata(hash.toString('hex')); -} - -export function computePrimaryMetadata(owner: string, coinMetadata: string) { - const addrBytes = Buffer.from( - bcs.serialize('address', owner), - 'base64' - ).toJSON().data; - const seed = Buffer.from(coinMetadata, 'ascii').toJSON().data; - const combinedBytes = [...addrBytes, ...seed, OBJECT_DERIVED_SCHEME]; - - const hash = crypto - .createHash('SHA3-256') - .update(Buffer.from(combinedBytes)) - .digest(); - return hash.toString('hex'); -} - -export function computeCoinMetadata(creator: string, symbol: string): string { - const addrBytes = Buffer.from( - bcs.serialize('address', creator), - 'base64' - ).toJSON().data; - const seed = Buffer.from(symbol, 'ascii').toJSON().data; - const combinedBytes = [ - ...addrBytes, - ...seed, - OBJECT_FROM_SEED_ADDRESS_SCHEME - ]; - - const hash = crypto - .createHash('SHA3-256') - .update(Buffer.from(combinedBytes)) - .digest(); - return hash.toString('hex'); -} - -export async function resolveFAMetadata( - lcd: any, - metadata: string -): Promise { - const resourceData = await lcd.move.resource( - metadata, - '0x1::fungible_asset::Metadata' - ); - const symbol = resourceData.data.symbol; - const sanitizedMetadata = metadata.startsWith('0x') - ? metadata.slice(2) - : metadata; - const isNative = sanitizedMetadata === computeCoinMetadata('0x1', symbol); - const denom = isNative ? symbol : `move/${sanitizedMetadata}`; - - return { - name: resourceData.data.name, - symbol: symbol, - denom: denom, - decimals: Number.parseInt(resourceData.data.decimals, 10) - }; -} diff --git a/bots/src/lib/monitoring.ts b/bots/src/lib/monitoring.ts index 72135bd7..a385460b 100644 --- a/bots/src/lib/monitoring.ts +++ b/bots/src/lib/monitoring.ts @@ -1,4 +1,4 @@ -import { LCDClient, TxSearchResult, TxInfo } from '@initia/minitia.js'; +import { LCDClient, TxSearchResult, TxInfo } from '@initia/initia.js'; export async function txSearch( lcd: LCDClient, diff --git a/bots/src/lib/query.ts b/bots/src/lib/query.ts new file mode 100644 index 00000000..6127358d --- /dev/null +++ b/bots/src/lib/query.ts @@ -0,0 +1,105 @@ +import { + BridgeInfo, + Coin, + LCDClient, + OutputInfo, + TokenPair +} from '@initia/initia.js'; +import { getConfig } from 'config'; +import { + DepositTxResponse, + OutputResponse, + WithdrawalTxResponse +} from './types'; +import axios from 'axios'; + +const config = getConfig(); + +/// LCD query + +// get the latest output from L1 chain +export async function getLastOutputInfo( + bridgeId: number +): Promise { + const [outputInfos, pagination] = await config.l1lcd.ophost.outputInfos( + bridgeId + ); + if (outputInfos.length === 0) return null; + return await config.l1lcd.ophost.outputInfo(bridgeId, pagination.total); +} + +// get the output by index from L1 chain +export async function getOutputInfoByIndex( + bridgeId: number, + outputIndex: number +): Promise { + return await config.l1lcd.ophost.outputInfo(bridgeId, outputIndex); +} + +export async function getBridgeInfo(bridgeId: number): Promise { + return await config.l1lcd.ophost.bridgeInfo(bridgeId); +} + +export async function getBalanceByDenom( + lcd: LCDClient, + account: string, + denom: string +): Promise { + const [coins, _pagination] = await lcd.bank.balance(account); + return coins.get(denom); +} + +export async function getTokenPairByL1Denom(denom: string): Promise { + return await config.l1lcd.ophost.tokenPairByL1Denom(config.BRIDGE_ID, denom); +} + +/// API query + +export async function getWithdrawalTxFromExecutor( + bridge_id: number, + sequence: number +): Promise { + const url = `${config.EXECUTOR_URI}/tx/withdrawal/${bridge_id}/${sequence}`; + + const res = await axios.get(url); + return res.data; +} + +export async function getDepositTxFromExecutor( + bridge_id: number, + sequence: number +): Promise { + const url = `${config.EXECUTOR_URI}/tx/deposit/${bridge_id}/${sequence}`; + const res = await axios.get(url); + return res.data; +} + +// fetching the output by index from l2 chain +export async function getOutputFromExecutor( + outputIndex: number +): Promise { + const url = `${config.EXECUTOR_URI}/output/${outputIndex}`; + const res = await axios.get(url); + return res.data; +} + +// fetching the latest output from l2 chain +export async function getLatestOutputFromExecutor(): Promise { + const url = `${config.EXECUTOR_URI}/output/latest`; + const res = await axios.get(url); + return res.data; +} + +export const checkHealth = async (url: string, timeout = 60_000) => { + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + try { + const response = await axios.get(url); + if (response.status === 200) return; + } catch { + continue; + } + await new Promise((res) => setTimeout(res, 1_000)); + } +}; diff --git a/bots/src/lib/rpc.ts b/bots/src/lib/rpc.ts index f0757b28..a3c6a627 100644 --- a/bots/src/lib/rpc.ts +++ b/bots/src/lib/rpc.ts @@ -1,9 +1,6 @@ import * as winston from 'winston'; import axios, { AxiosRequestConfig } from 'axios'; import * as Websocket from 'ws'; -import { getConfig } from 'config'; - -const config = getConfig(); export class RPCSocket { public ws: Websocket; @@ -14,9 +11,20 @@ export class RPCSocket { public updateTimer: NodeJS.Timer; public latestHeight?: number; logger: winston.Logger; - - constructor(rpcUrl: string, public interval: number, logger: winston.Logger) { - this.wsUrl = rpcUrl.replace('http', 'ws') + '/websocket'; + rpcUrl: string; + curRPCUrlIndex: number; + + constructor( + public rpcUrls: string[], + public interval: number, + logger: winston.Logger + ) { + if (this.rpcUrls.length === 0) { + throw new Error('RPC URLs list cannot be empty'); + } + this.curRPCUrlIndex = 0; + this.rpcUrl = this.rpcUrls[this.curRPCUrlIndex]; + this.wsUrl = this.rpcUrl.replace('http', 'ws') + '/websocket'; this.logger = logger; } @@ -25,6 +33,13 @@ export class RPCSocket { this.updateTimer = setTimeout(() => this.tick(), this.interval); } + public rotateRPC() { + this.curRPCUrlIndex = (this.curRPCUrlIndex + 1) % this.rpcUrls.length; + this.rpcUrl = this.rpcUrls[this.curRPCUrlIndex]; + this.wsUrl = this.rpcUrl.replace('http', 'ws') + '/websocket'; + this.logger.info(`Rotate WS RPC to ${this.rpcUrl}`); + } + public stop(): void { if (this.ws) this.ws.terminate(); } @@ -104,6 +119,7 @@ export class RPCSocket { } protected onDisconnect(code: number, reason: string): void { + this.rotateRPC(); this.logger.info( `${this.constructor.name}: websocket disconnected (${code}: ${reason})` ); @@ -141,90 +157,134 @@ export class RPCSocket { } } -async function getRequest( - rpc: string, - path: string, - params?: Record -): Promise { - const options: AxiosRequestConfig = { - headers: { - 'Content-Type': 'application/json', - 'User-Agent': 'initia-rollup' +export class RPCClient { + private curRPCUrlIndex = 0; + private rpcUrl: string; + + constructor(public rpcUrls: string[], public logger: winston.Logger) { + if (this.rpcUrls.length === 0) { + throw new Error('RPC URLs list cannot be empty'); } - }; + this.curRPCUrlIndex = 0; + this.rpcUrl = this.rpcUrls[this.curRPCUrlIndex]; + } - let url = `${rpc}${path}`; - params && - Object.keys(params).forEach( - (key) => params[key] === undefined && delete params[key] - ); - const qs = new URLSearchParams(params as any).toString(); - if (qs.length) { - url += `?${qs}`; + public rotateRPC() { + this.curRPCUrlIndex = (this.curRPCUrlIndex + 1) % this.rpcUrls.length; + this.rpcUrl = this.rpcUrls[this.curRPCUrlIndex]; + this.logger.info(`Rotate RPC to ${this.rpcUrl}`); } - try { - const response = await axios.get(url, options); + async getRequest( + path: string, + params?: Record + ): Promise { + const options: AxiosRequestConfig = { + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'initia-rollup' + } + }; + + let url = `${this.rpcUrl}${path}`; + params && + Object.keys(params).forEach( + (key) => params[key] === undefined && delete params[key] + ); + const qs = new URLSearchParams(params as any).toString(); + if (qs.length) { + url += `?${qs}`; + } + + try { + const response = await axios.get(url, options); + if (response.status !== 200) { + throw new Error(`Invalid status code: ${response.status}`); + } + + const data = response.data; + if (!data || typeof data.jsonrpc !== 'string') { + throw new Error('Failed to query RPC'); + } - if (response.status !== 200) { - throw new Error(`Invalid status code: ${response.status}`); + return data.result; + } catch (e) { + throw new Error(`RPC request to ${url} failed by ${e}`); } + } - const data = response.data; - if (!data || typeof data.jsonrpc !== 'string') { - throw new Error('Failed to query RPC'); + async getBlockchain( + min_height: number, + max_height: number + ): Promise { + const blockchainResult: Blockchain = await this.getRequest(`/blockchain`, { + minHeight: min_height.toString(), + maxHeight: max_height.toString() + }); + + if (!blockchainResult) { + this.logger.error('failed get blockchain from rpc'); + return null; } - return data.result; - } catch (e) { - throw new Error(`RPC request to ${url} failed by ${e}`); + return blockchainResult; } -} -export interface BlockBulk { - blocks: string[]; -} + async getBlockBulk(start: string, end: string): Promise { + const blockBulksResult: BlockBulk = await this.getRequest(`/block_bulk`, { + start, + end + }); + + if (!blockBulksResult) { + this.logger.error('failed get block bulks from rpc'); + return null; + } + + return blockBulksResult; + } + + async lookupInvalidBlock(): Promise { + const invalidBlockResult: InvalidBlock = await this.getRequest( + `/invalid_block` + ); + + if (invalidBlockResult.reason !== '' && invalidBlockResult.height !== '0') { + return invalidBlockResult; + } -export async function getBlockBulk( - start: string, - end: string -): Promise { - const blockBulksResult: BlockBulk = await getRequest( - config.L2_RPC_URI, - `/block_bulk`, - { start, end } - ); - - if (!blockBulksResult) { - this.logger.error('failed get block bulks from rpc'); return null; } - return blockBulksResult; -} + async getLatestBlockHeight(): Promise { + const abciInfo: ABCIInfo = await this.getRequest(`/abci_info`); -interface InvalidBlock { - reason: string; - height: string; + if (abciInfo) { + return parseInt(abciInfo.last_block_height); + } + + throw new Error(`failed to get latest block height`); + } } -/** - * Lookup invalid block on chain and return the response. - * Return null if there is no invalid block. - */ -export async function lookupInvalidBlock( - rpcUrl: string -): Promise { - const invalidBlockResult: InvalidBlock = await getRequest( - rpcUrl, - `/invalid_block` - ); +export interface Blockchain { + last_height: string; + block_metas: BlockMeta[]; +} - if (invalidBlockResult.reason !== '' && invalidBlockResult.height !== '0') { - return invalidBlockResult; - } +export interface BlockMeta { + block_id: any; + block_size: string; + header: any; + num_txs: string; +} +export interface BlockBulk { + blocks: string[]; +} - return null; +interface InvalidBlock { + reason: string; + height: string; } interface ABCIInfo { @@ -233,13 +293,3 @@ interface ABCIInfo { last_block_height: string; last_block_app_hash: string; } - -export async function getLatestBlockHeight(rpcUrl: string): Promise { - const abciInfo: ABCIInfo = await getRequest(rpcUrl, `/abci_info`); - - if (abciInfo) { - return parseInt(abciInfo.last_block_height); - } - - throw new Error(`failed to get latest block height from ${rpcUrl}`); -} diff --git a/bots/src/lib/slack.ts b/bots/src/lib/slack.ts new file mode 100644 index 00000000..2d7be80c --- /dev/null +++ b/bots/src/lib/slack.ts @@ -0,0 +1,71 @@ +import { Wallet } from '@initia/initia.js'; +import axios from 'axios'; +import BigNumber from 'bignumber.js'; +import { getConfig } from 'config'; +import * as http from 'http'; +import * as https from 'https'; +import FailedTxEntity from 'orm/executor/FailedTxEntity'; + +const config = getConfig(); + +const { SLACK_WEB_HOOK } = process.env; + +export function buildNotEnoughBalanceNotification( + wallet: Wallet, + balance: number, + denom: string +): { text: string } { + let notification = '```'; + notification += `[WARN] Enough Balance Notification\n`; + notification += `\n`; + notification += `Endpoint: ${wallet.lcd.URL}\n`; + notification += `Address : ${wallet.key.accAddress}\n`; + notification += `Balance : ${new BigNumber(balance) + .div(1e6) + .toFixed(6)} ${denom}\n`; + notification += '```'; + const text = `${notification}`; + return { + text + }; +} + +export function buildFailedTxNotification(data: FailedTxEntity): { + text: string; +} { + let notification = '```'; + notification += `[WARN] Bridge Processed Tx Notification\n`; + + notification += `[L1] ${config.L1_CHAIN_ID} => [L2] ${config.L2_CHAIN_ID}\n`; + notification += `\n`; + notification += `Bridge ID: ${data.bridgeId}\n`; + notification += `Sequence: ${data.sequence}\n`; + notification += `Sender: ${data.sender}\n`; + notification += `To: ${data.receiver}\n`; + notification += `\n`; + notification += `Amount: ${new BigNumber(data.amount) + .div(1e6) + .toFixed(6)} ${data.l1Denom}\n`; + notification += `\n`; + notification += `L1 Height: ${data.l1Height}\n`; + notification += `Error : ${data.error}\n`; + notification += '```'; + const text = `${notification}`; + + return { + text + }; +} + +const ax = axios.create({ + httpAgent: new http.Agent({ keepAlive: true }), + httpsAgent: new https.Agent({ keepAlive: true }), + timeout: 15000 +}); + +export async function notifySlack(text: { text: string }) { + if (SLACK_WEB_HOOK == undefined || SLACK_WEB_HOOK == '') return; + await ax.post(SLACK_WEB_HOOK, text).catch(() => { + console.error('Slack Notification Error'); + }); +} diff --git a/bots/src/lib/storage.ts b/bots/src/lib/storage.ts index 063ffb8d..11f81cc4 100644 --- a/bots/src/lib/storage.ts +++ b/bots/src/lib/storage.ts @@ -1,81 +1,112 @@ import { MerkleTree } from 'merkletreejs'; -import { BCS } from '@initia/minitia.js'; import { sha3_256 } from './util'; import { WithdrawalTx } from './types'; -import { computeBridgeAddress } from './lcd'; -import { WalletType, getWallet } from './wallet'; +import { AccAddress } from '@initia/initia.js'; -export class WithdrawalStorage { - public bcs = BCS.getInstance(); +function convertHexToBase64(hex: string): string { + return Buffer.from(hex, 'hex').toString('base64'); +} + +export class WithdrawStorage { private tree: MerkleTree; - constructor(txs: WithdrawalTx[]) { - const leaves = txs.map((tx) => - sha3_256( + constructor(txs: Array) { + const leaves = txs.map((tx) => { + const bridge_id_buf = Buffer.alloc(8); + bridge_id_buf.writeBigInt64BE(tx.bridge_id); + + const sequence_buf = Buffer.alloc(8); + sequence_buf.writeBigInt64BE(tx.sequence); + + const amount_buf = Buffer.alloc(8); + amount_buf.writeBigInt64BE(tx.amount); + + return sha3_256( Buffer.concat([ - Buffer.from(this.bcs.serialize(BCS.U64, tx.sequence), 'base64'), - Buffer.from(this.bcs.serialize(BCS.ADDRESS, tx.sender), 'base64'), - Buffer.from(this.bcs.serialize(BCS.ADDRESS, tx.receiver), 'base64'), - Buffer.from(this.bcs.serialize(BCS.U64, tx.amount), 'base64'), - Buffer.from( - this.bcs.serialize(BCS.OBJECT, computeBridgeAddress(getWallet(WalletType.Executor).key.accAddress, tx.l2_id)), - 'base64' - ), - Buffer.from(this.bcs.serialize(BCS.OBJECT, tx.metadata), 'base64') + bridge_id_buf, + sequence_buf, + Buffer.from(AccAddress.toHex(tx.sender).replace('0x', ''), 'hex'), + Buffer.from(AccAddress.toHex(tx.receiver).replace('0x', ''), 'hex'), + Buffer.from(tx.l1_denom, 'utf8'), + amount_buf ]) - ) - ); + ); + }); this.tree = new MerkleTree(leaves, sha3_256, { sort: true }); } public getMerkleRoot(): string { - return this.tree.getHexRoot().replace('0x', ''); + return convertHexToBase64(this.tree.getHexRoot().replace('0x', '')); } public getMerkleProof(tx: WithdrawalTx): string[] { + const bridge_id_buf = Buffer.alloc(8); + bridge_id_buf.writeBigInt64BE(tx.bridge_id); + + const sequence_buf = Buffer.alloc(8); + sequence_buf.writeBigInt64BE(tx.sequence); + + const amount_buf = Buffer.alloc(8); + amount_buf.writeBigInt64BE(tx.amount); + return this.tree .getHexProof( sha3_256( Buffer.concat([ - Buffer.from(this.bcs.serialize(BCS.U64, tx.sequence), 'base64'), - Buffer.from(this.bcs.serialize(BCS.ADDRESS, tx.sender), 'base64'), - Buffer.from(this.bcs.serialize(BCS.ADDRESS, tx.receiver), 'base64'), - Buffer.from(this.bcs.serialize(BCS.U64, tx.amount), 'base64'), - Buffer.from( - this.bcs.serialize(BCS.OBJECT, computeBridgeAddress(getWallet(WalletType.Executor).key.accAddress, tx.l2_id)), - 'base64' - ), - Buffer.from(this.bcs.serialize(BCS.OBJECT, tx.metadata), 'base64') + bridge_id_buf, + sequence_buf, + Buffer.from(AccAddress.toHex(tx.sender).replace('0x', ''), 'hex'), + Buffer.from(AccAddress.toHex(tx.receiver).replace('0x', ''), 'hex'), + Buffer.from(tx.l1_denom, 'utf8'), + amount_buf ]) ) ) - .map((v) => v.replace('0x', '')); + .map((v) => convertHexToBase64(v.replace('0x', ''))); } - public verify(proof: string[], tx: WithdrawalTx): boolean { + public verify( + proof: string[], + tx: { + bridge_id: bigint; + sequence: bigint; + sender: string; + receiver: string; + l1_denom: string; + amount: bigint; + } + ): boolean { + const bridge_id_buf = Buffer.alloc(8); + bridge_id_buf.writeBigInt64BE(tx.bridge_id); + + const sequence_buf = Buffer.alloc(8); + sequence_buf.writeBigInt64BE(tx.sequence); + + const amount_buf = Buffer.alloc(8); + amount_buf.writeBigInt64BE(tx.amount); + let hashBuf = sha3_256( Buffer.concat([ - Buffer.from(this.bcs.serialize(BCS.U64, tx.sequence), 'base64'), - Buffer.from(this.bcs.serialize(BCS.ADDRESS, tx.sender), 'base64'), - Buffer.from(this.bcs.serialize(BCS.ADDRESS, tx.receiver), 'base64'), - Buffer.from(this.bcs.serialize(BCS.U64, tx.amount), 'base64'), - Buffer.from( - this.bcs.serialize(BCS.OBJECT, computeBridgeAddress(getWallet(WalletType.Executor).key.accAddress, tx.l2_id)), - 'base64' - ), - Buffer.from(this.bcs.serialize(BCS.OBJECT, tx.metadata), 'base64') + bridge_id_buf, + sequence_buf, + Buffer.from(AccAddress.toHex(tx.sender).replace('0x', ''), 'hex'), + Buffer.from(AccAddress.toHex(tx.receiver).replace('0x', ''), 'hex'), + Buffer.from(tx.l1_denom, 'utf8'), + amount_buf ]) ); - for (const proofElem of proof) { - const proofBuf = Buffer.from(proofElem, 'hex'); - hashBuf = - Buffer.compare(hashBuf, proofBuf) === -1 - ? sha3_256(Buffer.concat([hashBuf, proofBuf])) - : sha3_256(Buffer.concat([proofBuf, hashBuf])); - } + proof.forEach((proofElem) => { + const proofBuf = Buffer.from(proofElem, 'base64'); + + if (Buffer.compare(hashBuf, proofBuf) === -1) { + hashBuf = sha3_256(Buffer.concat([hashBuf, proofBuf])); + } else { + hashBuf = sha3_256(Buffer.concat([proofBuf, hashBuf])); + } + }); - return this.getMerkleRoot() === hashBuf.toString('hex'); + return this.getMerkleRoot() === hashBuf.toString('base64'); } } diff --git a/bots/src/lib/tx.ts b/bots/src/lib/tx.ts index 37960db9..84cdfaa4 100644 --- a/bots/src/lib/tx.ts +++ b/bots/src/lib/tx.ts @@ -1,47 +1,26 @@ import { delay } from 'bluebird'; -import { LCDClient } from '@initia/minitia.js'; +import { + LCDClient, + Msg, + WaitTxBroadcastResult, + Wallet +} from '@initia/initia.js'; export async function sendTx( - wallet: any, - msgs: any[], + wallet: Wallet, + msgs: Msg[], accountNumber?: number, - sequence?: number -): Promise { - try { - const signedTx = await wallet.createAndSignTx({ - msgs, - accountNumber, - sequence - }); - const broadcastResult = await wallet.lcd.tx.broadcast(signedTx); - if (broadcastResult['code']) throw new Error(broadcastResult.raw_log); - await checkTx(wallet.lcd, broadcastResult.txhash); - - return broadcastResult; - } catch (err) { - console.log(err); - throw new Error(`Failed to execute transaction: ${err.message}`); - } -} - -export async function checkTx( - lcd: any, - txHash: string, - timeout = 60000 -): Promise { - const startedAt = Date.now(); - - while (Date.now() - startedAt < timeout) { - try { - const txInfo = await lcd.tx.txInfo(txHash); - if (txInfo) return txInfo; - await delay(1000); - } catch (err) { - throw new Error(`Failed to check transaction status: ${err.message}`); - } - } - - throw new Error('Transaction checking timed out'); + sequence?: number, + timeout = 10_000 +): Promise { + const signedTx = await wallet.createAndSignTx({ + msgs, + accountNumber, + sequence + }); + const broadcastResult = await wallet.lcd.tx.broadcast(signedTx, timeout); + if (broadcastResult['code']) throw new Error(broadcastResult.raw_log); + return broadcastResult; } // check whether batch submission interval is met diff --git a/bots/src/lib/types.ts b/bots/src/lib/types.ts index 247dfcaa..887b8872 100644 --- a/bots/src/lib/types.ts +++ b/bots/src/lib/types.ts @@ -1,44 +1,26 @@ -export interface BridgeConfig { - submission_interval: string; - challenger: string; - proposer: string; - finalization_period_seconds: string; - starting_block_number: string; -} +import DepositTxEntity from 'orm/executor/DepositTxEntity'; +import WithdrawalTxEntity from 'orm/executor/WithdrawalTxEntity'; +import { ExecutorOutputEntity } from 'orm/index'; export interface WithdrawalTx { - sequence: number; + bridge_id: bigint; + sequence: bigint; sender: string; receiver: string; - amount: number; - l2_id: string; - metadata: string; + l1_denom: string; + amount: bigint; } -export interface DepositTx { - sequence: number; - sender: string; - receiver: string; - amount: number; - l2_id: string; - l1_token: string; - l2_token: string; +/// response types + +export interface WithdrawalTxResponse { + withdrawalTx: WithdrawalTxEntity; } -export interface L1TokenBridgeInitiatedEvent { - from: string; - to: string; - l2_id: string; - l1_token: string; - l2_token: string; - amount: number; - l1_sequence: number; +export interface DepositTxResponse { + depositTx: DepositTxEntity; } -export interface L2TokenBridgeInitiatedEvent { - from: string; - to: string; - l2_token: string; - amount: number; - l2_sequence: number; +export interface OutputResponse { + output: ExecutorOutputEntity; } diff --git a/bots/src/lib/util.ts b/bots/src/lib/util.ts index ffc89924..91e14033 100644 --- a/bots/src/lib/util.ts +++ b/bots/src/lib/util.ts @@ -1,5 +1,5 @@ import { SHA3 } from 'sha3'; -import { sha256 } from '@initia/minitia.js'; +import { sha256 } from '@initia/initia.js'; export function sha3_256(value: Buffer | string | number) { value = toBuffer(value); @@ -87,31 +87,3 @@ function intToHex(i: number) { const hex = i.toString(16); return `0x${hex}`; } - -export function createOutputRoot( - version: number, - stateRoot: string, - storageRoot: string, - latestBlockHash: string -): string { - return sha3_256( - Buffer.concat([ - Buffer.from(version.toString()), - Buffer.from(stateRoot, 'hex'), - Buffer.from(storageRoot, 'hex'), - Buffer.from(latestBlockHash, 'base64') - ]) - ).toString('hex'); -} - -export function structTagToDenom(structTag: string): string { - if (structTag.startsWith('0x1::native_')) { - return structTag.split('::')[1].split('_')[1]; - } else if (structTag.startsWith('0x1::ibc_')) { - return `ibc/${structTag.split('::')[1].split('_')[1]}`; - } else { - const shaSum = sha256(Buffer.from(structTag)); - const hash = Buffer.from(shaSum).toString('hex'); - return `move/${hash}`; - } -} diff --git a/bots/src/lib/wallet.ts b/bots/src/lib/wallet.ts index 210e53eb..4589d0ec 100644 --- a/bots/src/lib/wallet.ts +++ b/bots/src/lib/wallet.ts @@ -2,12 +2,15 @@ import { Key, Wallet, Msg, - TxInfo, MnemonicKey, - LCDClient -} from '@initia/minitia.js'; + LCDClient, + WaitTxBroadcastResult, + Coins +} from '@initia/initia.js'; import { sendTx } from './tx'; import { getConfig } from 'config'; +import { getBalanceByDenom } from './query'; +import { buildNotEnoughBalanceNotification, notifySlack } from './slack'; const config = getConfig(); @@ -31,6 +34,8 @@ export const wallets: { }; export function initWallet(type: WalletType, lcd: LCDClient): void { + if (wallets[type]) return; + switch (type) { case WalletType.Challenger: wallets[type] = new TxWallet( @@ -76,7 +81,23 @@ export class TxWallet extends Wallet { super(lcd, key); } - async transaction(msgs: Msg[]): Promise { + async checkEnoughBalance() { + const gasPrices = new Coins(this.lcd.config.gasPrices); + const denom = gasPrices.denoms()[0]; + const balance = await getBalanceByDenom( + this.lcd, + this.key.accAddress, + denom + ); + + if (balance?.amount && parseInt(balance.amount) < 1_000_000_000) { + await notifySlack( + buildNotEnoughBalanceNotification(this, parseInt(balance.amount), denom) + ); + } + } + + async transaction(msgs: Msg[]): Promise { if (!this.managedAccountNumber && !this.managedSequence) { const { account_number: accountNumber, sequence } = await this.accountNumberAndSequence(); @@ -85,6 +106,7 @@ export class TxWallet extends Wallet { } try { + await this.checkEnoughBalance(); const txInfo = await sendTx( this, msgs, @@ -93,10 +115,10 @@ export class TxWallet extends Wallet { ); this.managedSequence += 1; return txInfo; - } catch (error) { + } catch (err) { delete this.managedAccountNumber; delete this.managedSequence; - throw error; + throw err; } } } diff --git a/bots/src/loader/app.ts b/bots/src/loader/app.ts index 280ce64f..fadc9ebe 100644 --- a/bots/src/loader/app.ts +++ b/bots/src/loader/app.ts @@ -9,8 +9,6 @@ import { KoaController, configureRoutes } from 'koa-joi-controllers'; import { createApiDocApp } from './apidoc'; import * as mount from 'koa-mount'; -const API_VERSION_PREFIX = '/v1'; - const notFoundMiddleware: Koa.Middleware = (ctx) => { ctx.status = 404; }; diff --git a/bots/src/orm/RecordEntity.ts b/bots/src/orm/RecordEntity.ts index 75dfbbf4..1e0f731f 100644 --- a/bots/src/orm/RecordEntity.ts +++ b/bots/src/orm/RecordEntity.ts @@ -3,11 +3,17 @@ import { Column, Entity, PrimaryColumn } from 'typeorm'; @Entity('record') export default class RecordEntity { @PrimaryColumn() - l2Id: string; + bridgeId: number; @PrimaryColumn() batchIndex: number; + @Column() + startBlockNumber: number; + + @Column() + endBlockNumber: number; + @Column({ type: 'bytea' }) diff --git a/bots/src/orm/challenger/CoinEntity.ts b/bots/src/orm/challenger/CoinEntity.ts deleted file mode 100644 index 5b49a20d..00000000 --- a/bots/src/orm/challenger/CoinEntity.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Column, Entity, Index, PrimaryColumn } from 'typeorm'; - -@Entity('challenger_coin') -export default class ChallengerCoinEntity { - @PrimaryColumn('text') - l1Metadata: string; - - @Column('text') - l1Denom: string; - - @Column('text') - l2Metadata: string; - - @Column('text') - @Index('challenger_coin_l2_denom') - l2Denom: string; - - @Column('boolean') - isChecked: boolean; -} diff --git a/bots/src/orm/challenger/DeletedOutputEntity.ts b/bots/src/orm/challenger/DeletedOutputEntity.ts index 0a4a5a5c..a62a4648 100644 --- a/bots/src/orm/challenger/DeletedOutputEntity.ts +++ b/bots/src/orm/challenger/DeletedOutputEntity.ts @@ -1,15 +1,12 @@ import { Column, Entity, Index, PrimaryColumn } from 'typeorm'; @Entity('challenger_deleted_output') -export default class DeletedOutputEntity { +export default class ChallengerDeletedOutputEntity { @PrimaryColumn('bigint') outputIndex: number; - @Column('text') - executor: string; - - @Column('text') - l2Id: string; + @Column('bigint') + bridgeId: string; @Column('text') reason: string; diff --git a/bots/src/orm/challenger/DepositTxEntity.ts b/bots/src/orm/challenger/DepositTxEntity.ts index fced971d..7e9ec82d 100644 --- a/bots/src/orm/challenger/DepositTxEntity.ts +++ b/bots/src/orm/challenger/DepositTxEntity.ts @@ -2,34 +2,26 @@ import { Column, Entity, Index, PrimaryColumn } from 'typeorm'; @Entity('challenger_deposit_tx') export default class DepositTxEntity { - @PrimaryColumn('text') - metadata: string; - - @PrimaryColumn('int') - sequence: number; + @PrimaryColumn('bigint') + sequence: string; @Column('text') - @Index('deposit_tx_sender_index') + @Index('challenger_deposit_tx_sender_index') sender: string; @Column('text') - @Index('deposit_tx_receiver_index') + @Index('challenger_deposit_tx_receiver_index') receiver: string; - @Column('int') - @Index('deposit_tx_output_index') - outputIndex: number; - @Column('bigint') - amount: number; + amount: string; - @Column('bigint') - height: number; + @Column('text') + l1Denom: string; - @Column('int', { nullable: true }) - @Index('deposit_tx_finalized_output_index') - finalizedOutputIndex: number | null; + @Column('text') + l2Denom: string; - @Column('boolean') - isChecked: boolean; + @Column('text') + data: string; } diff --git a/bots/src/orm/challenger/FinalizeDepositTxEntity.ts b/bots/src/orm/challenger/FinalizeDepositTxEntity.ts new file mode 100644 index 00000000..bb4ccb92 --- /dev/null +++ b/bots/src/orm/challenger/FinalizeDepositTxEntity.ts @@ -0,0 +1,25 @@ +import { Column, Entity, Index, PrimaryColumn } from 'typeorm'; + +@Entity('challenger_finalize_deposit_tx') +export default class FinalizeDepositTxEntity { + // l1 sequence + @PrimaryColumn('bigint') + sequence: string; + + @Column('text') + @Index('challenger_finalize_deposit_tx_sender_index') + sender: string; + + @Column('text') + @Index('challenger_finalize_deposit_tx_receiver_index') + receiver: string; + + @Column('bigint') + amount: string; + + @Column('text') + l2Denom: string; + + @Column('int') + l1Height: number; +} diff --git a/bots/src/orm/challenger/FinalizeWithdrawalTxEntity.ts b/bots/src/orm/challenger/FinalizeWithdrawalTxEntity.ts new file mode 100644 index 00000000..29a9558e --- /dev/null +++ b/bots/src/orm/challenger/FinalizeWithdrawalTxEntity.ts @@ -0,0 +1,31 @@ +import { Column, Entity, Index, PrimaryColumn } from 'typeorm'; + +@Entity('challenger_finalize_withdrawal_tx') +export default class FinalizeWithdrawalTxEntity { + @PrimaryColumn('bigint') + bridgeId: string; + + @PrimaryColumn('bigint') + sequence: string; + + @Column('text') + l1Denom: string; + + @Column('text') + l2Denom: string; + + @Column('text') + @Index('challenger_finalize_tx_sender_index') + sender: string; + + @Column('text') + @Index('challenger_finalize_tx_receiver_index') + receiver: string; + + @Column('bigint') + amount: string; + + @Column('int') + @Index('challenger_finalize_tx_output_index') + outputIndex: number; +} diff --git a/bots/src/orm/challenger/OutputEntity.ts b/bots/src/orm/challenger/OutputEntity.ts index 95d374f6..065a5f20 100644 --- a/bots/src/orm/challenger/OutputEntity.ts +++ b/bots/src/orm/challenger/OutputEntity.ts @@ -1,7 +1,7 @@ import { Column, Entity, PrimaryColumn } from 'typeorm'; @Entity('challenger_output') -export default class ChallengerOutputEntity { +export default class OutputEntity { @PrimaryColumn('int') outputIndex: number; @@ -18,5 +18,8 @@ export default class ChallengerOutputEntity { lastBlockHash: string; // last block hash of the epoch @Column('int') - checkpointBlockHeight: number; // start block height of the epoch + startBlockNumber: number; // start block height of the epoch + + @Column('int') + endBlockNumber: number; // end block height of the epoch } diff --git a/bots/src/orm/challenger/WithdrawalTxEntity.ts b/bots/src/orm/challenger/WithdrawalTxEntity.ts index fc109289..845b0da9 100644 --- a/bots/src/orm/challenger/WithdrawalTxEntity.ts +++ b/bots/src/orm/challenger/WithdrawalTxEntity.ts @@ -1,30 +1,32 @@ import { Column, Entity, Index, PrimaryColumn } from 'typeorm'; @Entity('challenger_withdrawal_tx') -export default class ChallengerWithdrawalTxEntity { - @PrimaryColumn('text') - metadata: string; +export default class WithdrawalTxEntity { + @PrimaryColumn('bigint') + bridgeId: string; - @PrimaryColumn('int') - sequence: number; + @PrimaryColumn('bigint') + sequence: string; @Column('text') - @Index('withdrawal_tx_sender_index') + l1Denom: string; + + @Column('text') + l2Denom: string; + + @Column('text') + @Index('challenger_tx_sender_index') sender: string; @Column('text') - @Index('withdrawal_tx_receiver_index') + @Index('challenger_tx_receiver_index') receiver: string; @Column('bigint') - amount: number; - - @Column('text') - @Index('withdrawal_l2id_index') - l2Id: string; + amount: string; @Column('int') - @Index('withdrawal_tx_output_index') + @Index('challenger_tx_output_index') outputIndex: number; @Column('text') @@ -32,7 +34,4 @@ export default class ChallengerWithdrawalTxEntity { @Column('text', { array: true }) merkleProof: string[]; - - @Column('boolean') - isChecked: boolean; } diff --git a/bots/src/orm/executor/CoinEntity.ts b/bots/src/orm/executor/CoinEntity.ts deleted file mode 100644 index f18739be..00000000 --- a/bots/src/orm/executor/CoinEntity.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Column, Entity, Index, PrimaryColumn } from 'typeorm'; - -@Entity('executor_coin') -export default class CoinEntity { - @PrimaryColumn('text') - l1Metadata: string; - - @Column('text') - l1Denom: string; - - @Column('text') - l2Metadata: string; - - @Column('text') - @Index('executor_coin_l2_denom') - l2Denom: string; - - @Column('boolean') - isChecked: boolean; -} diff --git a/bots/src/orm/executor/DepositTxEntity.ts b/bots/src/orm/executor/DepositTxEntity.ts index cc6ae478..56c28c44 100644 --- a/bots/src/orm/executor/DepositTxEntity.ts +++ b/bots/src/orm/executor/DepositTxEntity.ts @@ -2,11 +2,11 @@ import { Column, Entity, Index, PrimaryColumn } from 'typeorm'; @Entity('executor_deposit_tx') export default class DepositTxEntity { - @PrimaryColumn('text') - metadata: string; + @PrimaryColumn('bigint') + bridgeId: string; - @PrimaryColumn('int') - sequence: number; + @PrimaryColumn('bigint') + sequence: string; @Column('text') @Index('executor_deposit_tx_sender_index') @@ -21,8 +21,17 @@ export default class DepositTxEntity { outputIndex: number; @Column('bigint') - amount: number; + amount: string; - @Column('bigint') - height: number; + @Column('text') + l1Denom: string; + + @Column('text') + l2Denom: string; + + @Column('text') + data: string; + + @Column('int') + l1Height: number; } diff --git a/bots/src/orm/executor/FailedTxEntity.ts b/bots/src/orm/executor/FailedTxEntity.ts index 2adc2ad1..5065e24d 100644 --- a/bots/src/orm/executor/FailedTxEntity.ts +++ b/bots/src/orm/executor/FailedTxEntity.ts @@ -1,21 +1,46 @@ -import { Column, Entity, PrimaryColumn } from 'typeorm'; +import { Column, Entity, Index, PrimaryColumn } from 'typeorm'; @Entity('executor_failed_tx') export default class FailedTxEntity { - @PrimaryColumn('int') - height: number; + @PrimaryColumn('bigint') + bridgeId: string; - @PrimaryColumn('text') - monitor: string; + @PrimaryColumn('bigint') + sequence: string; - @Column({ - type: 'jsonb' - }) - messages: string[]; + @Column('text') + @Index('executor_failed_deposit_tx_sender_index') + sender: string; + + @Column('text') + @Index('executor_failed_deposit_tx_receiver_index') + receiver: string; + + @Column('int') + @Index('executor_failed_deposit_tx_output_index') + outputIndex: number; + + @Column('bigint') + amount: string; + + @Column('text') + l1Denom: string; + + @Column('text') + l2Denom: string; + + @Column('text') + data: string; + + @Column('int') + l1Height: number; @Column({ type: 'text', nullable: true }) error: string; + + @Column() + processed: boolean; } diff --git a/bots/src/orm/executor/OutputEntity.ts b/bots/src/orm/executor/OutputEntity.ts index 71a1aed6..9d843e4a 100644 --- a/bots/src/orm/executor/OutputEntity.ts +++ b/bots/src/orm/executor/OutputEntity.ts @@ -18,5 +18,8 @@ export default class OutputEntity { lastBlockHash: string; // last block hash of the epoch @Column('int') - checkpointBlockHeight: number; // start block height of the epoch + startBlockNumber: number; // start block height of the epoch + + @Column('int') + endBlockNumber: number; // end block height of the epoch } diff --git a/bots/src/orm/executor/WithdrawalTxEntity.ts b/bots/src/orm/executor/WithdrawalTxEntity.ts index dca2049f..bd644368 100644 --- a/bots/src/orm/executor/WithdrawalTxEntity.ts +++ b/bots/src/orm/executor/WithdrawalTxEntity.ts @@ -2,29 +2,31 @@ import { Column, Entity, Index, PrimaryColumn } from 'typeorm'; @Entity('executor_withdrawal_tx') export default class WithdrawalTxEntity { - @PrimaryColumn('text') - metadata: string; + @PrimaryColumn('bigint') + bridgeId: string; - @PrimaryColumn('int') - sequence: number; + @PrimaryColumn('bigint') + sequence: string; @Column('text') - @Index('executor_tx_sender_index') + l1Denom: string; + + @Column('text') + l2Denom: string; + + @Column('text') + @Index('executor_withdrawal_tx_sender_index') sender: string; @Column('text') - @Index('executor_tx_receiver_index') + @Index('executor_withdrawal_tx_receiver_index') receiver: string; @Column('bigint') - amount: number; - - @Column('text') - @Index('executor_tx_l2id_index') - l2Id: string; + amount: string; @Column('int') - @Index('executor_tx_output_index') + @Index('executor_withdrawal_tx_output_index') outputIndex: number; @Column('text') diff --git a/bots/src/orm/index.ts b/bots/src/orm/index.ts index 09ccc03b..bbccc1ce 100644 --- a/bots/src/orm/index.ts +++ b/bots/src/orm/index.ts @@ -2,27 +2,28 @@ import RecordEntity from './RecordEntity'; import StateEntity from './StateEntity'; import ExecutorWithdrawalTxEntity from './executor/WithdrawalTxEntity'; -import ExecutorCoinEntity from './executor/CoinEntity'; import ExecutorDepositTxEntity from './executor/DepositTxEntity'; import ExecutorOutputEntity from './executor/OutputEntity'; import ExecutorFailedTxEntity from './executor/FailedTxEntity'; import ChallengerDepositTxEntity from './challenger/DepositTxEntity'; import ChallengerWithdrawalTxEntity from './challenger/WithdrawalTxEntity'; +import ChallengerFinalizeDepositTxEntity from './challenger/FinalizeDepositTxEntity'; +import ChallengerFinalizeWithdrawalTxEntity from './challenger/FinalizeWithdrawalTxEntity'; + import ChallengerOutputEntity from './challenger/OutputEntity'; -import ChallengerCoinEntity from './challenger/CoinEntity'; -import DeletedOutputEntity from './challenger/DeletedOutputEntity'; +import ChallengerDeletedOutputEntity from './challenger/DeletedOutputEntity'; export * from './RecordEntity'; export * from './StateEntity'; export * from './challenger/DepositTxEntity'; export * from './challenger/WithdrawalTxEntity'; +export * from './challenger/FinalizeDepositTxEntity'; +export * from './challenger/FinalizeWithdrawalTxEntity'; export * from './challenger/OutputEntity'; -export * from './challenger/CoinEntity'; export * from './challenger/DeletedOutputEntity'; -export * from './executor/CoinEntity'; export * from './executor/OutputEntity'; export * from './executor/DepositTxEntity'; export * from './executor/WithdrawalTxEntity'; @@ -31,14 +32,14 @@ export * from './executor/FailedTxEntity'; export { RecordEntity, StateEntity, - ExecutorCoinEntity, ExecutorWithdrawalTxEntity, ExecutorDepositTxEntity, ExecutorOutputEntity, ExecutorFailedTxEntity, - ChallengerCoinEntity, ChallengerWithdrawalTxEntity, ChallengerDepositTxEntity, ChallengerOutputEntity, - DeletedOutputEntity + ChallengerFinalizeDepositTxEntity, + ChallengerFinalizeWithdrawalTxEntity, + ChallengerDeletedOutputEntity }; diff --git a/bots/src/scripts/contract/Move.toml b/bots/src/scripts/contract/Move.toml deleted file mode 100644 index d5182372..00000000 --- a/bots/src/scripts/contract/Move.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "op" -version = "0.0.0" - -[dependencies] -InitiaStdlib = { git = "https://github.com/initia-labs/initiavm.git", subdir = "precompile/modules/initia_stdlib", rev = "main" } - -[addresses] -std = "0x1" -addr = "0x56ccf33c45b99546cd1da172cf6849395bbf8573" diff --git a/bots/src/scripts/contract/sources/l2id.move b/bots/src/scripts/contract/sources/l2id.move deleted file mode 100644 index 0fa8ed22..00000000 --- a/bots/src/scripts/contract/sources/l2id.move +++ /dev/null @@ -1,3 +0,0 @@ -module addr::s12t1 { - struct Minitia {} -} \ No newline at end of file diff --git a/bots/src/scripts/setupL2.ts b/bots/src/scripts/setupL2.ts index f1d6d667..1091f667 100644 --- a/bots/src/scripts/setupL2.ts +++ b/bots/src/scripts/setupL2.ts @@ -1,119 +1,60 @@ -import { MsgExecute, MsgPublish, BCS } from '@initia/initia.js'; -import * as fs from 'fs'; -import * as path from 'path'; +import { MsgCreateBridge, BridgeConfig, Duration } from '@initia/initia.js'; import { sendTx } from 'lib/tx'; import { getConfig } from 'config'; -import { - build, - executor, - challenger, - outputSubmitter -} from 'test/utils/helper'; -import { computeCoinMetadata, normalizeMetadata } from 'lib/lcd'; - +import { executor, challenger, outputSubmitter } from 'test/utils/helper'; const config = getConfig(); -const bcs = BCS.getInstance(); -const UINIT_METADATA = normalizeMetadata(computeCoinMetadata('0x1', 'uinit')); // '0x8e4733bdabcf7d4afc3d14f0dd46c9bf52fb0fce9e4b996c939e195b8bc891d9' +const SUBMISSION_INTERVAL = parseInt(process.env.SUBMISSION_INTERVAL ?? '3600'); +const FINALIZED_TIME = parseInt(process.env.SUBMISSION_INTERVAL ?? '3600'); +const IBC_METADATA = process.env.IBC_METADATA ?? ''; // ibc channel name class L2Initializer { - l2id = config.L2ID; - moduleName = this.l2id.split('::')[1]; - contractDir = path.join(__dirname, 'contract'); + l2id = config.BRIDGE_ID; constructor( - public submissionInterval, - public finalizedTime, - public l2StartBlockHeight + public submissionInterval: number, + public finalizedTime: number, + public metadata: string ) {} - // update module name in l2id.move - updateL2ID() { - const filePath = path.join(this.contractDir, 'sources', 'l2id.move'); - const fileContent = fs.readFileSync(filePath, 'utf-8'); - const updatedContent = fileContent.replace( - /(addr::)[^\s{]+( \{)/g, - `$1${this.moduleName}$2` - ); - fs.writeFileSync(filePath, updatedContent, 'utf-8'); - } - - publishL2IDMsg(module: string) { - return new MsgPublish(executor.key.accAddress, [module], 0); - } - - bridgeInitializeMsg( - submissionInterval: number, - finalizedTime: number, - l2StartBlockHeight: number - ) { - return new MsgExecute( - executor.key.accAddress, - '0x1', - 'op_bridge', - 'initialize', - [], - [ - bcs.serialize('string', this.l2id), - bcs.serialize('u64', submissionInterval), - bcs.serialize('address', outputSubmitter.key.accAddress), - bcs.serialize('address', challenger.key.accAddress), - bcs.serialize('u64', finalizedTime), - bcs.serialize('u64', l2StartBlockHeight) - ] - ); - } - bridgeRegisterTokenMsg(metadata: string) { - return new MsgExecute( - executor.key.accAddress, - '0x1', - 'op_bridge', - 'register_token', - [], - [bcs.serialize('string', this.l2id), bcs.serialize('object', metadata)] + MsgCreateBridge(submissionInterval: number, finalizedTime: number) { + const bridgeConfig = new BridgeConfig( + challenger.key.accAddress, + outputSubmitter.key.accAddress, + Duration.fromString(submissionInterval.toString()), + Duration.fromString(finalizedTime.toString()), + new Date(), + this.metadata ); + return new MsgCreateBridge(executor.key.accAddress, bridgeConfig); } async initialize() { - this.updateL2ID(); - const module = await build(this.contractDir, this.moduleName); const msgs = [ - this.publishL2IDMsg(module), - this.bridgeInitializeMsg( - this.submissionInterval, - this.finalizedTime, - this.l2StartBlockHeight - ), - this.bridgeRegisterTokenMsg(UINIT_METADATA) + this.MsgCreateBridge(this.submissionInterval, this.finalizedTime) ]; + await sendTx(executor, msgs); } } async function main() { - if (!process.env.SUB_INTV) { - throw new Error('SUB_INTV is not set'); - } - if (!process.env.FIN_TIME) { - throw new Error('FIN_TIME is not set'); - } - if (!process.env.L2_HEIGHT) { - throw new Error('L2_HEIGHT is not set'); - } - - const initializer = new L2Initializer( - process.env.SUB_INTV, // submissionInterval - process.env.FIN_TIME, // finalizedTime - process.env.L2_HEIGHT // l2StartBlockHeight - ); - - console.log('=========Initializing L2========='); - console.log('submissionInterval: ', initializer.submissionInterval); - console.log('finalizedTime: ', initializer.finalizedTime); - console.log('l2StartBlockHeight: ', initializer.l2StartBlockHeight); + try { + const initializer = new L2Initializer( + SUBMISSION_INTERVAL, + FINALIZED_TIME, + IBC_METADATA + ); + console.log('=========Initializing L2========='); + console.log('submissionInterval: ', initializer.submissionInterval); + console.log('finalizedTime: ', initializer.finalizedTime); + console.log('metadata: ', initializer.metadata); - await initializer.initialize(); - console.log('=========L2 Initialized Done========='); + await initializer.initialize(); + console.log('=========L2 Initialized Done========='); + } catch (e) { + console.error(e); + } } if (require.main === module) { diff --git a/bots/src/service/batch/BatchService.ts b/bots/src/service/batch/BatchService.ts index bed647de..4e2b8889 100644 --- a/bots/src/service/batch/BatchService.ts +++ b/bots/src/service/batch/BatchService.ts @@ -4,7 +4,7 @@ import { getDB } from 'worker/batchSubmitter/db'; import { decompressor } from 'lib/compressor'; interface GetBatchResponse { - l2Id: string; + bridgeId: number; batchIndex: number; batch: string[]; } @@ -25,7 +25,7 @@ export async function getBatch(batchIndex: number): Promise { } return { - l2Id: batch.l2Id, + bridgeId: batch.bridgeId, batchIndex: batch.batchIndex, batch: decompressor(batch.batch) }; diff --git a/bots/src/service/executor/CoinService.ts b/bots/src/service/executor/CoinService.ts deleted file mode 100644 index 338417b6..00000000 --- a/bots/src/service/executor/CoinService.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { ExecutorCoinEntity } from 'orm'; -import { getDB } from 'worker/bridgeExecutor/db'; -import { APIError, ErrorTypes } from 'lib/error'; - -export interface GetCoinResponse { - coin: ExecutorCoinEntity; -} - -export interface GetAllCoinsResponse { - coins: ExecutorCoinEntity[]; -} - -export async function getCoin(metadata: string): Promise { - const [db] = getDB(); - const queryRunner = db.createQueryRunner('slave'); - try { - const qb = queryRunner.manager - .createQueryBuilder(ExecutorCoinEntity, 'coin') - .where('coin.l1Metadata = :metadata', { metadata }); - - const coin = await qb.getOne(); - - if (!coin) { - throw new APIError(ErrorTypes.NOT_FOUND_ERROR); - } - - return { - coin: coin - }; - } finally { - queryRunner.release(); - } -} - -export async function getAllCoins(): Promise { - const [db] = getDB(); - const queryRunner = db.createQueryRunner('slave'); - try { - const qb = queryRunner.manager.createQueryBuilder( - ExecutorCoinEntity, - 'coin' - ); - - const coins = await qb.getMany(); - - if (!coins) { - throw new APIError(ErrorTypes.NOT_FOUND_ERROR); - } - - return { - coins: coins - }; - } finally { - queryRunner.release(); - } -} diff --git a/bots/src/service/executor/DepositTxService.ts b/bots/src/service/executor/DepositTxService.ts new file mode 100644 index 00000000..05491227 --- /dev/null +++ b/bots/src/service/executor/DepositTxService.ts @@ -0,0 +1,33 @@ +import { ExecutorDepositTxEntity } from 'orm'; +import { getDB } from 'worker/bridgeExecutor/db'; +import { APIError, ErrorTypes } from 'lib/error'; + +export interface GetDepositTxResponse { + depositTx: ExecutorDepositTxEntity; +} + +export async function getDepositTx( + bridgeId: string, + sequence: number +): Promise { + const [db] = getDB(); + const queryRunner = db.createQueryRunner('slave'); + try { + const qb = queryRunner.manager + .createQueryBuilder(ExecutorDepositTxEntity, 'tx') + .where('tx.bridge_id = :bridgeId', { bridgeId }) + .andWhere('tx.sequence = :sequence', { sequence }); + + const depositTx = await qb.getOne(); + + if (!depositTx) { + throw new APIError(ErrorTypes.NOT_FOUND_ERROR); + } + + return { + depositTx + }; + } finally { + queryRunner.release(); + } +} diff --git a/bots/src/service/executor/TxService.ts b/bots/src/service/executor/WithdrawalTxService.ts similarity index 59% rename from bots/src/service/executor/TxService.ts rename to bots/src/service/executor/WithdrawalTxService.ts index 6b856680..52c72002 100644 --- a/bots/src/service/executor/TxService.ts +++ b/bots/src/service/executor/WithdrawalTxService.ts @@ -2,25 +2,31 @@ import { ExecutorWithdrawalTxEntity } from 'orm'; import { getDB } from 'worker/bridgeExecutor/db'; import { APIError, ErrorTypes } from 'lib/error'; -export async function getTx( - metadata: string, +export interface GetWithdrawalTxResponse { + withdrawalTx: ExecutorWithdrawalTxEntity; +} + +export async function getWithdrawalTx( + bridgeId: string, sequence: number -): Promise { +): Promise { const [db] = getDB(); const queryRunner = db.createQueryRunner('slave'); try { const qb = queryRunner.manager .createQueryBuilder(ExecutorWithdrawalTxEntity, 'tx') - .where('tx.metadata = :metadata', { metadata }) + .where('tx.bridge_id = :bridgeId', { bridgeId }) .andWhere('tx.sequence = :sequence', { sequence }); - const tx = await qb.getOne(); + const withdrawalTx = await qb.getOne(); - if (!tx) { + if (!withdrawalTx) { throw new APIError(ErrorTypes.NOT_FOUND_ERROR); } - return tx; + return { + withdrawalTx + }; } finally { queryRunner.release(); } diff --git a/bots/src/service/index.ts b/bots/src/service/index.ts index b595e4c0..4b1a9355 100644 --- a/bots/src/service/index.ts +++ b/bots/src/service/index.ts @@ -1,5 +1,5 @@ export * from './executor/OutputService'; -export * from './executor/TxService'; -export * from './executor/CoinService'; +export * from './executor/WithdrawalTxService'; +export * from './executor/DepositTxService'; export * from './batch/BatchService'; diff --git a/bots/src/test/claim.ts b/bots/src/test/claim.ts new file mode 100644 index 00000000..29900fa4 --- /dev/null +++ b/bots/src/test/claim.ts @@ -0,0 +1,65 @@ +import { getConfig } from 'config'; +import { TxBot } from './utils/TxBot'; +import { Coin } from '@initia/initia.js'; + +const config = getConfig(); + +const txBot = new TxBot(config.BRIDGE_ID); + +async function main() { + try { + // await withdraw() + await claim(3, 21); // set sequence, outputIndex + } catch (err) { + console.log(err); + } +} + +export async function claim(sequence: number, outputIndex: number) { + const beforeBalance = await config.l1lcd.bank.balance( + txBot.l1sender.key.accAddress + ); + + const res = await txBot.claim(txBot.l1sender, sequence, outputIndex); + + const afterBalance = await config.l1lcd.bank.balance( + txBot.l1sender.key.accAddress + ); + + console.log( + `claimed : ${afterBalance[0] + .get('uinit') + ?.sub(beforeBalance[0].get('uinit') ?? 0)} in hash ${res.txhash}` + ); +} + +export async function withdraw() { + const pair = await config.l1lcd.ophost.tokenPairByL1Denom( + config.BRIDGE_ID, + 'uinit' + ); + + const beforeBalance = await config.l2lcd.bank.balance( + txBot.l2sender.key.accAddress + ); + + const res = await txBot.withdrawal( + txBot.l2sender, + txBot.l1sender, + new Coin(pair.l2_denom, 1_000_000) + ); + + const afterBalance = await config.l2lcd.bank.balance( + txBot.l2sender.key.accAddress + ); + + console.log( + `withdraw: ${beforeBalance[0] + .get(pair.l2_denom) + ?.sub(afterBalance[0].get(pair.l2_denom) ?? 0)} in hash ${res.txhash}` + ); +} + +if (require.main === module) { + main(); +} diff --git a/bots/src/test/contract/Move.toml b/bots/src/test/contract/Move.toml deleted file mode 100644 index d5182372..00000000 --- a/bots/src/test/contract/Move.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "op" -version = "0.0.0" - -[dependencies] -InitiaStdlib = { git = "https://github.com/initia-labs/initiavm.git", subdir = "precompile/modules/initia_stdlib", rev = "main" } - -[addresses] -std = "0x1" -addr = "0x56ccf33c45b99546cd1da172cf6849395bbf8573" diff --git a/bots/src/test/contract/sources/l2id.move b/bots/src/test/contract/sources/l2id.move deleted file mode 100644 index 0fa8ed22..00000000 --- a/bots/src/test/contract/sources/l2id.move +++ /dev/null @@ -1,3 +0,0 @@ -module addr::s12t1 { - struct Minitia {} -} \ No newline at end of file diff --git a/bots/src/test/fundAccounts.ts b/bots/src/test/fundAccounts.ts new file mode 100644 index 00000000..036588da --- /dev/null +++ b/bots/src/test/fundAccounts.ts @@ -0,0 +1,124 @@ +import { Coin, MnemonicKey, MsgSend, Wallet } from '@initia/initia.js'; +import { delay } from 'bluebird'; +import { getConfig } from 'config'; +import { sendTx } from 'lib/tx'; +import { TxBot } from 'test/utils/TxBot'; + +const config = getConfig(); +const L1_FUNDER = new Wallet( + config.l1lcd, + new MnemonicKey({ + mnemonic: '' + }) +); +const L2_FUNDER = new Wallet( + config.l2lcd, + new MnemonicKey({ + mnemonic: '' + }) +); + +async function fundL1() { + const executor = new Wallet( + config.l1lcd, + new MnemonicKey({ mnemonic: config.EXECUTOR_MNEMONIC }) + ); + const output = new Wallet( + config.l1lcd, + new MnemonicKey({ mnemonic: config.OUTPUT_SUBMITTER_MNEMONIC }) + ); + const batch = new Wallet( + config.l1lcd, + new MnemonicKey({ mnemonic: config.BATCH_SUBMITTER_MNEMONIC }) + ); + const challenger = new Wallet( + config.l1lcd, + new MnemonicKey({ mnemonic: config.CHALLENGER_MNEMONIC }) + ); + const receiver = new Wallet( + config.l1lcd, + new MnemonicKey({ + mnemonic: '' + }) + ); + + const sendMsg = [ + new MsgSend( + L1_FUNDER.key.accAddress, + executor.key.accAddress, + '50000000000uinit' + ), + new MsgSend( + L1_FUNDER.key.accAddress, + output.key.accAddress, + '50000000000uinit' + ), + new MsgSend( + L1_FUNDER.key.accAddress, + batch.key.accAddress, + '50000000000uinit' + ), + new MsgSend( + L1_FUNDER.key.accAddress, + challenger.key.accAddress, + '50000000000uinit' + ), + new MsgSend( + L1_FUNDER.key.accAddress, + receiver.key.accAddress, + '100000000000uinit' + ) + ]; + await sendTx(L1_FUNDER, sendMsg); +} + +async function fundL2() { + const executor = new Wallet( + config.l2lcd, + new MnemonicKey({ mnemonic: config.EXECUTOR_MNEMONIC }) + ); + const output = new Wallet( + config.l2lcd, + new MnemonicKey({ mnemonic: config.OUTPUT_SUBMITTER_MNEMONIC }) + ); + const batch = new Wallet( + config.l2lcd, + new MnemonicKey({ mnemonic: config.BATCH_SUBMITTER_MNEMONIC }) + ); + const challenger = new Wallet( + config.l2lcd, + new MnemonicKey({ mnemonic: config.CHALLENGER_MNEMONIC }) + ); + + const sendMsg = [ + new MsgSend(L2_FUNDER.key.accAddress, executor.key.accAddress, '1umin'), + new MsgSend(L2_FUNDER.key.accAddress, output.key.accAddress, '1umin'), + new MsgSend(L2_FUNDER.key.accAddress, batch.key.accAddress, '1umin'), + new MsgSend(L2_FUNDER.key.accAddress, challenger.key.accAddress, '1umin') + ]; + await sendTx(L2_FUNDER, sendMsg); +} + +async function startDepositTxBot() { + const txBot = new TxBot(config.BRIDGE_ID); + for (;;) { + const res = await txBot.deposit( + txBot.l1sender, + txBot.l2sender, + new Coin('uinit', 1_000_000) + ); + console.log(`Deposited height ${res.height} ${res.txhash}`); + await delay(1_000); + } +} + +async function main() { + await startDepositTxBot(); + await fundL1(); + await fundL2(); + console.log('Funded accounts'); +} + +if (require.main === module) { + main(); +} diff --git a/bots/src/test/integration.ts b/bots/src/test/integration.ts index c2110ee6..1c8ab412 100644 --- a/bots/src/test/integration.ts +++ b/bots/src/test/integration.ts @@ -1,75 +1,61 @@ import Bridge from './utils/Bridge'; -import DockerHelper from './utils/DockerHelper'; -import * as path from 'path'; -import { startBatch } from 'worker/batchSubmitter'; -import { startOutput } from 'worker/outputSubmitter'; -import { startExecutor } from 'worker/bridgeExecutor'; -import { startChallenger } from 'worker/challenger'; import { Config } from 'config'; import { TxBot } from './utils/TxBot'; -import { computeCoinMetadata, normalizeMetadata } from 'lib/lcd'; -import { checkHealth } from './utils/helper'; +import { Coin } from '@initia/initia.js'; +import { startBatch } from 'worker/batchSubmitter'; +import { startExecutor } from 'worker/bridgeExecutor'; +import { startOutput } from 'worker/outputSubmitter'; +import { delay } from 'bluebird'; +import { getBalanceByDenom, getTokenPairByL1Denom } from 'lib/query'; const config = Config.getConfig(); -const docker = new DockerHelper(path.join(__dirname, '..', '..')); +const SUBMISSION_INTERVAL = 5; +const FINALIZED_TIME = 5; +const DEPOSIT_AMOUNT = 1_000_000; +const DEPOSIT_INTERVAL_MS = 100; -async function setup() { - await docker.start(); - await checkHealth(config.L1_LCD_URI, 20_000); - await checkHealth(config.L2_LCD_URI, 20_000); - await setupBridge(10, 10, 1); -} - -async function setupBridge( - submissionInterval: number, - finalizedTime: number, - l2StartBlockHeight: number -) { - const bridge = new Bridge( - submissionInterval, - finalizedTime, - l2StartBlockHeight, - config.L2ID, - path.join(__dirname, 'contract') - ); - const UINIT_METADATA = normalizeMetadata(computeCoinMetadata('0x1', 'uinit')); // '0x8e4733bdabcf7d4afc3d14f0dd46c9bf52fb0fce9e4b996c939e195b8bc891d9' - - await bridge.deployBridge(UINIT_METADATA); +async function setupBridge(submissionInterval: number, finalizedTime: number) { + const bridge = new Bridge(submissionInterval, finalizedTime); + const relayerMetadata = ''; + await bridge.clearDB(); + await bridge.tx(relayerMetadata); console.log('Bridge deployed'); } async function startBot() { try { - await Promise.all([ - startBatch(), - startExecutor(), - startChallenger(), - startOutput() - ]); + await Promise.all([startBatch(), startExecutor(), startOutput()]); } catch (err) { console.log(err); } } -async function startTxBot() { - const txBot = new TxBot(); - - try { - // TODO: Make withdraw and claim sequentially - await txBot.deposit(txBot.l1sender, txBot.l2receiver, 1_000); - // await txBot.withdrawal(txBot.l2receiver, 100); // WARN: run after deposit done - // await txBot.claim(txBot.l1receiver, 1, 19); // WARN: run after withdrawal done - console.log('tx bot done'); - } catch (err) { - console.log(err); +async function startDepositTxBot() { + const txBot = new TxBot(config.BRIDGE_ID); + const pair = await getTokenPairByL1Denom('uinit'); + for (;;) { + const balance = await getBalanceByDenom( + config.l2lcd, + txBot.l2sender.key.accAddress, + pair.l2_denom + ); + const res = await txBot.deposit( + txBot.l1sender, + txBot.l2sender, + new Coin('uinit', DEPOSIT_AMOUNT) + ); + console.log( + `[DepositBot] Deposited height ${res.height} to ${txBot.l2sender.key.accAddress} ${balance?.amount}` + ); + await delay(DEPOSIT_INTERVAL_MS); } } async function main() { try { - await setup(); + await setupBridge(SUBMISSION_INTERVAL, FINALIZED_TIME); await startBot(); - await startTxBot(); + await startDepositTxBot(); } catch (err) { console.log(err); } diff --git a/bots/src/test/storage.spec.ts b/bots/src/test/storage.spec.ts new file mode 100644 index 00000000..7b741943 --- /dev/null +++ b/bots/src/test/storage.spec.ts @@ -0,0 +1,57 @@ +import { sha3_256 } from 'lib/util'; +import { WithdrawStorage } from 'lib/storage'; + +const v1 = [ + { + bridge_id: BigInt(1), + sequence: BigInt(1), + sender: 'init1wzenw7r2t2ra39k4l9yqq95pw55ap4sm4vsa9g', + receiver: 'init174knscjg688ddtxj8smyjz073r3w5mmsp3m0m2', + l1_denom: 'uinit', + amount: BigInt(1000000) + }, + { + bridge_id: BigInt(1), + sequence: BigInt(2), + sender: 'init1wzenw7r2t2ra39k4l9yqq95pw55ap4sm4vsa9g', + receiver: 'init174knscjg688ddtxj8smyjz073r3w5mmsp3m0m2', + l1_denom: 'uinit', + amount: BigInt(1000000) + }, + { + bridge_id: BigInt(1), + sequence: BigInt(3), + sender: 'init1wzenw7r2t2ra39k4l9yqq95pw55ap4sm4vsa9g', + receiver: 'init174knscjg688ddtxj8smyjz073r3w5mmsp3m0m2', + l1_denom: 'uinit', + amount: BigInt(1000000) + } +]; + +describe('WithdrawStorage', () => { + it('verify v1', async () => { + const airdrop = new WithdrawStorage(v1); + const target = v1[0]; + + const merkleRoot = airdrop.getMerkleRoot(); + const merkleProof = airdrop.getMerkleProof(target); + const version = 2; + const stateRoot = 'C2ZdjJ7uX41NaadA/FjlMiG6btiDfYnxE2ABqJocHxI='; + const lastBlockHash = 'tgmfQJT4uipVToW631xz0RXdrfzu7n5XxGNoPpX6isI='; + const outputRoot = sha3_256( + Buffer.concat([ + sha3_256(version), + Buffer.from(stateRoot, 'base64'), // state root + Buffer.from(merkleRoot, 'base64'), + Buffer.from(lastBlockHash, 'base64') // block hash + ]) + ).toString('base64'); + expect(airdrop.verify(merkleProof, target)).toBeTruthy(); + + expect(merkleRoot).toEqual('EYgpXs1b+Z3AdGqjjtJHylrGzCjXtBKDD2UTPXelUk4='); + expect(merkleProof).toEqual([ + '5eJNy8mEqvyhysgWCqi7JQ7K602FtSpz+wDRNQitQMc=' + ]); + expect(outputRoot).toEqual('euaoJcFRXfV/6F0AiC0vYwXUY4NPHfCn9LbFMPieNsA='); + }); +}); diff --git a/bots/src/test/utils/Bridge.ts b/bots/src/test/utils/Bridge.ts index f453d570..939169f4 100644 --- a/bots/src/test/utils/Bridge.ts +++ b/bots/src/test/utils/Bridge.ts @@ -1,161 +1,104 @@ -import { MsgPublish, MsgExecute } from '@initia/initia.js'; -import * as fs from 'fs'; -import * as path from 'path'; -import { getDB, initORM } from 'worker/bridgeExecutor/db'; +import { MsgCreateBridge, BridgeConfig, Duration } from '@initia/initia.js'; +import { + getDB as getExecutorDB, + initORM as initExecutorORM +} from 'worker/bridgeExecutor/db'; +import { + getDB as getChallengerDB, + initORM as initChallengerORM +} from 'worker/challenger/db'; +import { + getDB as getBatchDB, + initORM as initBatchORM +} from 'worker/batchSubmitter/db'; import { DataSource, EntityManager } from 'typeorm'; import { - ExecutorCoinEntity, ExecutorOutputEntity, StateEntity, - ExecutorWithdrawalTxEntity + ExecutorWithdrawalTxEntity, + ExecutorDepositTxEntity, + ExecutorFailedTxEntity, + ChallengerDepositTxEntity, + ChallengerFinalizeDepositTxEntity, + ChallengerFinalizeWithdrawalTxEntity, + ChallengerOutputEntity, + ChallengerWithdrawalTxEntity, + ChallengerDeletedOutputEntity, + RecordEntity } from 'orm'; -import { getConfig } from 'config'; -import { build, executor, challenger, outputSubmitter, bcs } from './helper'; +import { executor, challenger, outputSubmitter } from './helper'; import { sendTx } from 'lib/tx'; -const config = getConfig(); - class Bridge { - db: DataSource; - submissionInterval: number; - finalizedTime: number; - l2StartBlockHeight: number; + executorDB: DataSource; + challengerDB: DataSource; + batchDB: DataSource; l1BlockHeight: number; l2BlockHeight: number; - l2id: string; - moduleName: string; - contractDir: string; constructor( - submissionInterval: number, - finalizedTime: number, - l2StartBlockHeight: number, - l2id: string, - contractDir: string - ) { - [this.db] = getDB(); - this.submissionInterval = submissionInterval; - this.finalizedTime = finalizedTime; - this.l2StartBlockHeight = l2StartBlockHeight; - this.l2id = l2id; - this.moduleName = this.l2id.split('::')[1]; - this.contractDir = contractDir; - } - - async init() { - await this.setDB(); - this.updateL2ID(); - } - - async setDB() { - const l1Monitor = `executor_l1_monitor`; - const l2Monitor = `executor_l2_monitor`; - this.l1BlockHeight = parseInt( - (await config.l1lcd.tendermint.blockInfo()).block.header.height - ); - - this.l2BlockHeight = parseInt( - (await config.l2lcd.tendermint.blockInfo()).block.header.height - ); - this.l2BlockHeight = Math.floor(this.l2BlockHeight / 100) * 100; + public submissionInterval: number, + public finalizedTime: number + ) {} + async clearDB() { // remove and initialize - await this.db.transaction( - async (transactionalEntityManager: EntityManager) => { - await transactionalEntityManager.getRepository(StateEntity).clear(); - await transactionalEntityManager - .getRepository(ExecutorWithdrawalTxEntity) - .clear(); - await transactionalEntityManager - .getRepository(ExecutorCoinEntity) - .clear(); - await transactionalEntityManager - .getRepository(ExecutorOutputEntity) - .clear(); + await initExecutorORM(); + await initChallengerORM(); + await initBatchORM(); - await transactionalEntityManager - .getRepository(StateEntity) - .save({ name: l1Monitor, height: this.l1BlockHeight - 1 }); - await transactionalEntityManager - .getRepository(StateEntity) - .save({ name: l2Monitor, height: this.l2BlockHeight - 1 }); - } - ); - } + [this.executorDB] = getExecutorDB(); + [this.challengerDB] = getChallengerDB(); + [this.batchDB] = getBatchDB(); - // update module name in l2id.move - updateL2ID() { - const filePath = path.join(this.contractDir, 'sources', 'l2id.move'); - const fileContent = fs.readFileSync(filePath, 'utf-8'); - const updatedContent = fileContent.replace( - /(addr::)[^\s{]+( \{)/g, - `$1${this.moduleName}$2` - ); - fs.writeFileSync(filePath, updatedContent, 'utf-8'); - } + await this.executorDB.transaction(async (manager: EntityManager) => { + await manager.getRepository(StateEntity).clear(); + await manager.getRepository(ExecutorWithdrawalTxEntity).clear(); + await manager.getRepository(ExecutorOutputEntity).clear(); + await manager.getRepository(ExecutorDepositTxEntity).clear(); + await manager.getRepository(ExecutorFailedTxEntity).clear(); + }); - publishL2IDMsg(module: string) { - return new MsgPublish(executor.key.accAddress, [module], 0); + await this.challengerDB.transaction(async (manager: EntityManager) => { + await manager.getRepository(ChallengerDepositTxEntity).clear(); + await manager.getRepository(ChallengerFinalizeDepositTxEntity).clear(); + await manager.getRepository(ChallengerFinalizeWithdrawalTxEntity).clear(); + await manager.getRepository(ChallengerOutputEntity).clear(); + await manager.getRepository(ChallengerWithdrawalTxEntity).clear(); + await manager.getRepository(ChallengerDeletedOutputEntity).clear(); + }); + + await this.batchDB.transaction(async (manager: EntityManager) => { + await manager.getRepository(RecordEntity).clear(); + }); } - bridgeInitializeMsg( + MsgCreateBridge( submissionInterval: number, finalizedTime: number, - l2StartBlockHeight: number + metadata: string ) { - return new MsgExecute( - executor.key.accAddress, - '0x1', - 'op_bridge', - 'initialize', - [], - [ - bcs.serialize('string', this.l2id), - bcs.serialize('u64', submissionInterval), - bcs.serialize('address', outputSubmitter.key.accAddress), - bcs.serialize('address', challenger.key.accAddress), - bcs.serialize('u64', finalizedTime), - bcs.serialize('u64', l2StartBlockHeight) - ] - ); - } - - bridgeRegisterTokenMsg(metadata: string) { - return new MsgExecute( - executor.key.accAddress, - '0x1', - 'op_bridge', - 'register_token', - [], - [bcs.serialize('string', this.l2id), bcs.serialize('object', metadata)] + const bridgeConfig = new BridgeConfig( + challenger.key.accAddress, + outputSubmitter.key.accAddress, + Duration.fromString(submissionInterval.toString()), + Duration.fromString(finalizedTime.toString()), + new Date(), + metadata ); + return new MsgCreateBridge(executor.key.accAddress, bridgeConfig); } async tx(metadata: string) { - const module = await build(this.contractDir, this.moduleName); const msgs = [ - this.publishL2IDMsg(module), - this.bridgeInitializeMsg( + this.MsgCreateBridge( this.submissionInterval, this.finalizedTime, - this.l2StartBlockHeight - ), - this.bridgeRegisterTokenMsg(metadata) + metadata + ) ]; - await sendTx(executor, msgs); - } - async deployBridge(metadata: string) { - await initORM(); - const bridge = new Bridge( - this.submissionInterval, - this.finalizedTime, - this.l2StartBlockHeight, - this.l2id, - this.contractDir - ); - await bridge.init(); - await bridge.tx(metadata); + return await sendTx(executor, msgs); } } diff --git a/bots/src/test/utils/DockerHelper.ts b/bots/src/test/utils/DockerHelper.ts deleted file mode 100644 index 583ca22b..00000000 --- a/bots/src/test/utils/DockerHelper.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { upAll } from 'docker-compose'; -import { exec as execCb } from 'child_process'; -import { promisify } from 'util'; - -export default class DockerHelper { - constructor(public path: string) {} - - async start() { - console.log('Starting docker containers...'); - const result = await upAll({ cwd: this.path, log: false }); - return result; - } - - async stopDocker(scriptPath: string): Promise { - const exec = promisify(execCb); - try { - const { stdout, stderr } = await exec( - `sh ${scriptPath}/docker-compose-reset` - ); - - if (stderr) { - console.warn(`stderr: ${stderr}`); - } - } catch (error) { - console.warn(`Error: ${error.message}`); - } - } - - async stop() { - console.log('Stopping docker containers...'); - await this.stopDocker(this.path); - console.log('Successfully stopped docker containers'); - } -} diff --git a/bots/src/test/utils/TxBot.ts b/bots/src/test/utils/TxBot.ts index 9e466aea..36a95692 100644 --- a/bots/src/test/utils/TxBot.ts +++ b/bots/src/test/utils/TxBot.ts @@ -1,116 +1,83 @@ import { - Wallet as mWallet, - MsgSend, + Wallet, + MsgInitiateTokenDeposit, Coin, - MsgExecute, - MnemonicKey as mMnemonicKey -} from '@initia/minitia.js'; - -import { - Wallet as iWallet, - MnemonicKey as iMnemonicKey + MsgInitiateTokenWithdrawal, + MnemonicKey } from '@initia/initia.js'; -import axios from 'axios'; -import { getConfig } from 'config'; -import { bcs, executor, getOutput, getTx, makeFinalizeMsg } from './helper'; +import { makeFinalizeMsg } from './helper'; import { sendTx } from 'lib/tx'; -import { computeCoinMetadata, normalizeMetadata } from 'lib/lcd'; +import { getOutputFromExecutor, getWithdrawalTxFromExecutor } from 'lib/query'; +import { getConfig } from 'config'; const config = getConfig(); export class TxBot { - l1CoinMetadata: string; - l1sender: iWallet; - l2sender: mWallet; - l1receiver: iWallet; - l2receiver: mWallet; + l1sender = new Wallet( + config.l1lcd, + new MnemonicKey({ + mnemonic: + // init1wzenw7r2t2ra39k4l9yqq95pw55ap4sm4vsa9g + '' + }) + ); - constructor() { - this.l1CoinMetadata = normalizeMetadata( - computeCoinMetadata('0x1', 'uinit') - ); - this.l1sender = this.createWallet( - config.l1lcd, - iWallet, - iMnemonicKey, - 'banner december bunker moral nasty glide slow property pen twist doctor exclude novel top material flee appear imitate cat state domain consider then age' - ); - this.l2sender = this.createWallet( - config.l2lcd, - iWallet, - iMnemonicKey, - 'banner december bunker moral nasty glide slow property pen twist doctor exclude novel top material flee appear imitate cat state domain consider then age' - ); - this.l1receiver = this.createWallet( - config.l1lcd, - mWallet, - mMnemonicKey, - 'diamond donkey opinion claw cool harbor tree elegant outer mother claw amount message result leave tank plunge harbor garment purity arrest humble figure endless' - ); - this.l2receiver = this.createWallet( - config.l2lcd, - mWallet, - mMnemonicKey, - 'diamond donkey opinion claw cool harbor tree elegant outer mother claw amount message result leave tank plunge harbor garment purity arrest humble figure endless' - ); - } + l1receiver = new Wallet( + config.l1lcd, + new MnemonicKey({ + mnemonic: + // init174knscjg688ddtxj8smyjz073r3w5mmsp3m0m2 + '' + }) + ); - createWallet(lcd, WalletClass, MnemonicKeyClass, mnemonic) { - return new WalletClass(lcd, new MnemonicKeyClass({ mnemonic })); - } + l2sender = new Wallet( + config.l2lcd, + new MnemonicKey({ + mnemonic: '' + }) + ); - async deposit(sender: iWallet, reciever: mWallet, amount: number) { - const msg = new MsgExecute( + l2receiver = new Wallet( + config.l2lcd, + new MnemonicKey({ + mnemonic: '' + }) + ); + + constructor(public bridgeId: number) {} + + async deposit(sender: Wallet, reciever: Wallet, coin: Coin) { + const msg = new MsgInitiateTokenDeposit( sender.key.accAddress, - '0x1', - 'op_bridge', - 'deposit_token', - [], - [ - bcs.serialize('address', executor.key.accAddress), - bcs.serialize('string', config.L2ID), - bcs.serialize('object', this.l1CoinMetadata), - bcs.serialize('address', reciever.key.accAddress), - bcs.serialize('u64', amount) - ] + this.bridgeId, + reciever.key.accAddress, + coin ); + return await sendTx(sender, [msg]); } - async withdrawal(wallet: mWallet, amount: number) { - const res = await axios.get( - `${config.EXECUTOR_URI}/coin/${this.l1CoinMetadata}` - ); - const l2CoinMetadata = res.data.coin.l2Metadata; - - const msg = new MsgExecute( - wallet.key.accAddress, - '0x1', - 'op_bridge', - 'withdraw_token', - [], - [ - bcs.serialize('address', wallet.key.accAddress), - bcs.serialize('object', l2CoinMetadata), - bcs.serialize('u64', amount) - ] + async withdrawal(sender: Wallet, receiver: Wallet, coin: Coin) { + const msg = new MsgInitiateTokenWithdrawal( + sender.key.accAddress, + receiver.key.accAddress, + coin ); - return await sendTx(wallet, [msg]); - } - async claim(sender: iWallet, txSequence: number, outputIndex: number) { - const txRes = await getTx(this.l1CoinMetadata, txSequence); - const outputRes: any = await getOutput(outputIndex); - const finalizeMsg = await makeFinalizeMsg(sender, txRes, outputRes.output); - return await sendTx(sender, [finalizeMsg]); + return await sendTx(sender, [msg]); } - // only for L2 account public key gen - async sendCoin(sender: any, receiver: any, amount: number, denom: string) { - const msg = new MsgSend(sender.key.accAddress, receiver.key.accAddress, [ - new Coin(denom, amount) - ]); + async claim(sender: Wallet, txSequence: number, outputIndex: number) { + const txRes = await getWithdrawalTxFromExecutor(this.bridgeId, txSequence); + const outputRes: any = await getOutputFromExecutor(outputIndex); + const finalizeMsg = await makeFinalizeMsg( + txRes.withdrawalTx, + outputRes.output + ); - return await sendTx(sender, [msg]); + const { account_number: accountNumber, sequence } = + await sender.accountNumberAndSequence(); + return await sendTx(sender, [finalizeMsg], accountNumber, sequence); } } diff --git a/bots/src/test/utils/helper.ts b/bots/src/test/utils/helper.ts index 0e785056..22d9694c 100644 --- a/bots/src/test/utils/helper.ts +++ b/bots/src/test/utils/helper.ts @@ -1,8 +1,17 @@ -import { Wallet, MnemonicKey, BCS, Msg, MsgExecute } from '@initia/initia.js'; -import axios from 'axios'; +import { + Wallet, + MnemonicKey, + BCS, + Msg, + MsgFinalizeTokenWithdrawal, + Coin +} from '@initia/initia.js'; import { MoveBuilder } from '@initia/builder.js'; import { getConfig } from 'config'; +import { sha3_256 } from 'lib/util'; +import { ExecutorOutputEntity } from 'orm/index'; +import WithdrawalTxEntity from 'orm/executor/WithdrawalTxEntity'; const config = getConfig(); export const bcs = BCS.getInstance(); @@ -29,95 +38,22 @@ export async function build( return contract.toString('base64'); } -export interface TxResponse { - metadata: string; - sequence: number; - sender: string; - receiver: string; - amount: number; - outputIndex: number; - merkleRoot: string; - merkleProof: string[]; -} - -export interface OutputResponse { - outputIndex: number; - outputRoot: string; - stateRoot: string; - storageRoot: string; - lastBlockHash: string; - checkpointBlockHeight: number; -} - export async function makeFinalizeMsg( - sender: Wallet, - txRes: TxResponse, - outputRes: OutputResponse + txRes: WithdrawalTxEntity, + outputRes: ExecutorOutputEntity ): Promise { - const msg = new MsgExecute( - sender.key.accAddress, - '0x1', - 'op_bridge', - 'finalize_token_bridge', - [], - [ - bcs.serialize('address', executor.key.accAddress), - bcs.serialize('string', config.L2ID), - bcs.serialize('object', txRes.metadata), // coin metadata - bcs.serialize('u64', outputRes.outputIndex), // output index - bcs.serialize( - 'vector>', - txRes.merkleProof.map((proof: string) => Buffer.from(proof, 'hex')) - ), // withdrawal proofs (tx table) - - // withdraw tx data (tx table) - bcs.serialize('u64', txRes.sequence), // l2_sequence (txEntity sequence) - bcs.serialize('address', txRes.sender), // sender - bcs.serialize('address', txRes.receiver), // receiver - bcs.serialize('u64', txRes.amount), // amount - - // output root proof (output table) - bcs.serialize( - 'vector', - Buffer.from(outputRes.outputIndex.toString(), 'utf8') - ), //version (==output index) - bcs.serialize('vector', Buffer.from(outputRes.stateRoot, 'base64')), // state_root - bcs.serialize('vector', Buffer.from(outputRes.storageRoot, 'hex')), // storage root - bcs.serialize( - 'vector', - Buffer.from(outputRes.lastBlockHash, 'base64') - ) // latests block hash - ] + const msg = new MsgFinalizeTokenWithdrawal( + config.BRIDGE_ID, + outputRes.outputIndex, + txRes.merkleProof, + txRes.sender, + txRes.receiver, + parseInt(txRes.sequence), + new Coin('uinit', txRes.amount), + sha3_256(outputRes.outputIndex).toString('base64'), + outputRes.stateRoot, + outputRes.storageRoot, + outputRes.lastBlockHash ); return msg; } - -export async function getTx( - coin: string, - sequence: number -): Promise { - const url = `${config.EXECUTOR_URI}/tx/${coin}/${sequence}`; - - const res = await axios.get(url); - return res.data; -} - -export async function getOutput(outputIndex: number): Promise { - const url = `${config.EXECUTOR_URI}/output/${outputIndex}`; - const res = await axios.get(url); - return res.data; -} - -export const checkHealth = async (url: string, timeout = 60_000) => { - const startTime = Date.now(); - - while (Date.now() - startTime < timeout) { - try { - const response = await axios.get(url); - if (response.status === 200) return; - } catch { - continue; - } - await new Promise((res) => setTimeout(res, 1_000)); - } -}; diff --git a/bots/src/worker/batchSubmitter/batchSubmitter.ts b/bots/src/worker/batchSubmitter/batchSubmitter.ts index 735cfbbc..e315066e 100644 --- a/bots/src/worker/batchSubmitter/batchSubmitter.ts +++ b/bots/src/worker/batchSubmitter/batchSubmitter.ts @@ -1,40 +1,36 @@ import { getDB } from './db'; -import { DataSource } from 'typeorm'; -import { batchLogger as logger } from 'lib/logger'; - -import { BlockBulk, getBlockBulk } from 'lib/rpc'; +import { DataSource, EntityManager } from 'typeorm'; +import { batchLogger, batchLogger as logger } from 'lib/logger'; +import { BlockBulk, RPCClient } from 'lib/rpc'; import { compressor } from 'lib/compressor'; -import { getLatestBlockHeight } from 'lib/tx'; -import { RecordEntity } from 'orm'; -import { Wallet, MnemonicKey, MsgExecute, BCS } from '@initia/initia.js'; -import { fetchBridgeConfig } from 'lib/lcd'; +import { ExecutorOutputEntity, RecordEntity } from 'orm'; +import { Wallet, MnemonicKey, MsgRecordBatch } from '@initia/initia.js'; import { delay } from 'bluebird'; import { INTERVAL_BATCH } from 'config'; import { getConfig } from 'config'; import { sendTx } from 'lib/tx'; +import MonitorHelper from 'worker/bridgeExecutor/MonitorHelper'; const config = getConfig(); -const bcs = BCS.getInstance(); export class BatchSubmitter { private batchIndex = 0; - private batchL2StartHeight: number; - private latestBlockHeight: number; - private dataSource: DataSource; + private db: DataSource; private submitter: Wallet; - private submissionInterval: number; + private bridgeId: number; private isRunning = false; + private rpcClient: RPCClient; + helper: MonitorHelper = new MonitorHelper(); async init() { - [this.dataSource] = getDB(); - this.latestBlockHeight = await getLatestBlockHeight(config.l2lcd); + [this.db] = getDB(); + this.rpcClient = new RPCClient(config.L2_RPC_URI, batchLogger); this.submitter = new Wallet( config.l1lcd, new MnemonicKey({ mnemonic: config.BATCH_SUBMITTER_MNEMONIC }) ); - const bridgeCfg = await fetchBridgeConfig(); - this.batchL2StartHeight = parseInt(bridgeCfg.starting_block_number); - this.submissionInterval = parseInt(bridgeCfg.submission_interval); + + this.bridgeId = config.BRIDGE_ID; this.isRunning = true; } @@ -46,39 +42,47 @@ export class BatchSubmitter { await this.init(); while (this.isRunning) { - try { - const latestBatch = await this.getStoredBatch(this.dataSource); - if (latestBatch) { - this.batchIndex = latestBatch.batchIndex + 1; - } - - // e.g [start_height + 0, start_height + 99], [start_height + 100, start_height + 199], ... - const startHeight = - this.batchL2StartHeight + this.batchIndex * this.submissionInterval; - const endHeight = - this.batchL2StartHeight + - (this.batchIndex + 1) * this.submissionInterval - - 1; - - this.latestBlockHeight = await getLatestBlockHeight(config.l2lcd); - if (endHeight > this.latestBlockHeight) { - await delay(INTERVAL_BATCH); - continue; - } - - const batch = await this.getBatch(startHeight, endHeight); + await this.processBatch(); + } + } + + async processBatch() { + try { + await this.db.transaction(async (manager: EntityManager) => { + const latestBatch = await this.getStoredBatch(manager); + this.batchIndex = latestBatch ? latestBatch.batchIndex + 1 : 1; + const output = await this.helper.getOutputByIndex( + manager, + ExecutorOutputEntity, + this.batchIndex + ); + + if (!output) return; + + const batch = await this.getBatch( + output.startBlockNumber, + output.endBlockNumber + ); await this.publishBatchToL1(batch); - await this.saveBatchToDB(this.dataSource, batch, this.batchIndex); + await this.saveBatchToDB( + manager, + batch, + this.batchIndex, + output.startBlockNumber, + output.endBlockNumber + ); logger.info(`${this.batchIndex}th batch is successfully saved`); - } catch (err) { - throw new Error(`Error in BatchSubmitter: ${err}`); - } + }); + } catch (err) { + throw new Error(`Error in BatchSubmitter: ${err}`); + } finally { + await delay(INTERVAL_BATCH); } } // Get [start, end] batch from L2 async getBatch(start: number, end: number): Promise { - const bulk: BlockBulk | null = await getBlockBulk( + const bulk: BlockBulk | null = await this.rpcClient.getBlockBulk( start.toString(), end.toString() ); @@ -89,33 +93,24 @@ export class BatchSubmitter { return compressor(bulk.blocks); } - async getStoredBatch(db: DataSource): Promise { - const storedRecord = await db - .getRepository(RecordEntity) - .find({ - order: { - batchIndex: 'DESC' - }, - take: 1 - }) - .catch((err) => { - logger.error(`Error getting stored batch: ${err}`); - return null; - }); + async getStoredBatch(manager: EntityManager): Promise { + const storedRecord = await manager.getRepository(RecordEntity).find({ + order: { + batchIndex: 'DESC' + }, + take: 1 + }); - return storedRecord ? storedRecord[0] : null; + return storedRecord[0] ?? null; } // Publish a batch to L1 async publishBatchToL1(batch: Buffer) { try { - const executeMsg = new MsgExecute( + const executeMsg = new MsgRecordBatch( this.submitter.key.accAddress, - '0x1', - 'op_batch_inbox', - 'record_batch', - [config.L2ID], - [bcs.serialize('vector', batch, this.submissionInterval * 1000)] + this.bridgeId, + batch.toString('base64') ); return await sendTx(this.submitter, [executeMsg]); @@ -126,22 +121,26 @@ export class BatchSubmitter { // Save batch record to database async saveBatchToDB( - db: DataSource, + manager: EntityManager, batch: Buffer, - batchIndex: number + batchIndex: number, + startBlockNumber: number, + endBlockNumber: number ): Promise { const record = new RecordEntity(); - record.l2Id = config.L2ID; + record.bridgeId = this.bridgeId; record.batchIndex = batchIndex; record.batch = batch; + record.startBlockNumber = startBlockNumber; + record.endBlockNumber = endBlockNumber; - await db + await manager .getRepository(RecordEntity) .save(record) .catch((error) => { throw new Error( - `Error saving record ${record.l2Id} batch ${batchIndex} to database: ${error}` + `Error saving record ${record.bridgeId} batch ${batchIndex} to database: ${error}` ); }); diff --git a/bots/src/worker/batchSubmitter/db.ts b/bots/src/worker/batchSubmitter/db.ts index 294ffa11..8bb04868 100644 --- a/bots/src/worker/batchSubmitter/db.ts +++ b/bots/src/worker/batchSubmitter/db.ts @@ -10,12 +10,12 @@ import * as CamelToSnakeNamingStrategy from 'orm/CamelToSnakeNamingStrategy'; const debug = require('debug')('orm'); -import { RecordEntity } from 'orm'; +import { RecordEntity, ExecutorOutputEntity } from 'orm'; const staticOptions = { supportBigNumbers: true, bigNumberStrings: true, - entities: [RecordEntity] + entities: [RecordEntity, ExecutorOutputEntity] }; let DB: DataSource[] = []; diff --git a/bots/src/worker/bridgeExecutor/L1Monitor.ts b/bots/src/worker/bridgeExecutor/L1Monitor.ts index ce8337f5..7e26fe99 100644 --- a/bots/src/worker/bridgeExecutor/L1Monitor.ts +++ b/bots/src/worker/bridgeExecutor/L1Monitor.ts @@ -1,188 +1,134 @@ import { Monitor } from './Monitor'; +import { Coin, Msg, MsgFinalizeTokenDeposit, Wallet } from '@initia/initia.js'; import { - CoinInfo, - computeCoinMetadata, - normalizeMetadata, - resolveFAMetadata -} from 'lib/lcd'; -import { - AccAddress, - Coin, - Msg, - MsgCreateToken, - MsgDeposit -} from '@initia/minitia.js'; - -import { - ExecutorCoinEntity, ExecutorDepositTxEntity, ExecutorFailedTxEntity, ExecutorOutputEntity } from 'orm'; -import { WalletType, getWallet, TxWallet } from 'lib/wallet'; import { EntityManager } from 'typeorm'; -import { RPCSocket } from 'lib/rpc'; +import { RPCClient, RPCSocket } from 'lib/rpc'; import { getDB } from './db'; import winston from 'winston'; import { getConfig } from 'config'; +import { TxWallet, WalletType, getWallet, initWallet } from 'lib/wallet'; const config = getConfig(); export class L1Monitor extends Monitor { - constructor(public socket: RPCSocket, logger: winston.Logger) { - super(socket, logger); + executor: TxWallet; + + constructor( + public socket: RPCSocket, + public rpcClient: RPCClient, + logger: winston.Logger + ) { + super(socket, rpcClient, logger); [this.db] = getDB(); + initWallet(WalletType.Executor, config.l2lcd); + this.executor = getWallet(WalletType.Executor); } public name(): string { return 'executor_l1_monitor'; } - public async handleTokenRegisteredEvent( - wallet: TxWallet, + public async handleInitiateTokenDeposit( + wallet: Wallet, manager: EntityManager, data: { [key: string]: string } - ): Promise { - const l1Metadata = data['l1_token']; - const l2Metadata = normalizeMetadata( - computeCoinMetadata('0x1', 'l2/' + data['l2_token']) - ); - - const l1CoinInfo: CoinInfo = await resolveFAMetadata( - config.l1lcd, - l1Metadata - ); - - const l1Denom = l1CoinInfo.denom; - const l2Denom = 'l2/' + data['l2_token']; - - const coin: ExecutorCoinEntity = { - l1Metadata: l1Metadata, - l1Denom: l1Denom, - l2Metadata: l2Metadata, - l2Denom: l2Denom, - isChecked: false - }; - - await this.helper.saveEntity(manager, ExecutorCoinEntity, coin); - - return new MsgCreateToken( - wallet.key.accAddress, - l1CoinInfo.name, - l2Denom, - l1CoinInfo.decimals - ); - } - - public async handleTokenBridgeInitiatedEvent( - wallet: TxWallet, - manager: EntityManager, - data: { [key: string]: string } - ): Promise { + ): Promise<[ExecutorDepositTxEntity, MsgFinalizeTokenDeposit]> { const lastIndex = await this.helper.getLastOutputIndex( manager, ExecutorOutputEntity ); - const l2Metadata = normalizeMetadata( - computeCoinMetadata('0x1', 'l2/' + data['l2_token']) - ); - const l2Denom = 'l2/' + data['l2_token']; - const entity: ExecutorDepositTxEntity = { - sequence: Number.parseInt(data['l1_sequence']), + sequence: data['l1_sequence'], sender: data['from'], receiver: data['to'], - amount: Number.parseInt(data['amount']), + l1Denom: data['l1_denom'], + l2Denom: data['l2_denom'], + amount: data['amount'], + data: data['data'], outputIndex: lastIndex + 1, - metadata: l2Metadata, - height: this.syncedHeight + bridgeId: this.bridgeId.toString(), + l1Height: this.currentHeight }; - this.logger.info(`Deposit tx in height ${this.syncedHeight}`); - await manager.getRepository(ExecutorDepositTxEntity).save(entity); + return [ + entity, + new MsgFinalizeTokenDeposit( + wallet.key.accAddress, + data['from'], + data['to'], + new Coin(data['l2_denom'], data['amount']), + parseInt(data['l1_sequence']), + this.currentHeight, + Buffer.from(data['data'], 'hex').toString('base64') + ) + ]; + } - return new MsgDeposit( - wallet.key.accAddress, - AccAddress.fromHex(data['from']), - AccAddress.fromHex(data['to']), - new Coin(l2Denom, data['amount']), - Number.parseInt(data['l1_sequence']), - this.syncedHeight, - Buffer.from(data['data']) + public async handleEvents(manager: EntityManager): Promise { + const [isEmpty, depositEvents] = await this.helper.fetchEvents( + config.l1lcd, + this.currentHeight, + 'initiate_token_deposit' ); - } - public async handleEvents(): Promise { - await this.db.transaction( - async (transactionalEntityManager: EntityManager) => { - const msgs: Msg[] = []; - const executor: TxWallet = getWallet(WalletType.Executor); + if (isEmpty) return false; + + const msgs: Msg[] = []; + const entities: ExecutorDepositTxEntity[] = []; - const events = await this.helper.fetchEvents( - config.l1lcd, - this.syncedHeight, - 'move' - ); + for (const evt of depositEvents) { + const attrMap = this.helper.eventsToAttrMap(evt); + if (attrMap['bridge_id'] !== this.bridgeId.toString()) continue; + const [entity, msg] = await this.handleInitiateTokenDeposit( + this.executor, + manager, + attrMap + ); - for (const evt of events) { - const attrMap = this.helper.eventsToAttrMap(evt); - const data = this.helper.parseData(attrMap); - if (data['l2_id'] !== config.L2ID) continue; + entities.push(entity); + if (msg) msgs.push(msg); + } - switch (attrMap['type_tag']) { - case '0x1::op_bridge::TokenRegisteredEvent': { - const msg: MsgCreateToken = await this.handleTokenRegisteredEvent( - executor, - transactionalEntityManager, - data - ); - msgs.push(msg); - break; - } - case '0x1::op_bridge::TokenBridgeInitiatedEvent': { - const msg: MsgDeposit = - await this.handleTokenBridgeInitiatedEvent( - executor, - transactionalEntityManager, - data - ); - msgs.push(msg); - break; - } - } - } + await this.processMsgs(manager, msgs, entities); + return true; + } - if (msgs.length > 0) { - const stringfyMsgs = msgs.map((msg) => msg.toJSON().toString()); - await executor - .transaction(msgs) - .then((info) => { - this.logger.info( - `succeed to submit tx in height: ${this.syncedHeight}\ntxhash: ${info?.txhash}\nmsgs: ${stringfyMsgs}` - ); - }) - .catch(async (err) => { - const errMsg = err.response?.data - ? JSON.stringify(err.response?.data) - : err; - this.logger.error( - `Failed to submit tx in height: ${this.syncedHeight}\nMsg: ${stringfyMsgs}\n${errMsg}` - ); - const failedTx: ExecutorFailedTxEntity = { - height: this.syncedHeight, - monitor: this.name(), - messages: stringfyMsgs, - error: errMsg - }; - await this.helper.saveEntity( - transactionalEntityManager, - ExecutorFailedTxEntity, - failedTx - ); - }); - } + async processMsgs( + manager: EntityManager, + msgs: Msg[], + entities: ExecutorDepositTxEntity[] + ): Promise { + if (msgs.length == 0) return; + const stringfyMsgs = msgs.map((msg) => msg.toJSON().toString()); + try { + for (const entity of entities) { + await this.helper.saveEntity(manager, ExecutorDepositTxEntity, entity); } - ); + await this.executor.transaction(msgs); + this.logger.info( + `Succeeded to submit tx in height: ${this.currentHeight} ${stringfyMsgs}` + ); + } catch (err) { + const errMsg = err.response?.data + ? JSON.stringify(err.response?.data) + : err.toString(); + this.logger.error( + `Failed to submit tx in height: ${this.currentHeight}\nMsg: ${stringfyMsgs}\nError: ${errMsg}` + ); + + // Save all entities in a single batch operation, if possible + for (const entity of entities) { + await this.helper.saveEntity(manager, ExecutorFailedTxEntity, { + ...entity, + error: errMsg, + processed: false + }); + } + } } } diff --git a/bots/src/worker/bridgeExecutor/L2Monitor.ts b/bots/src/worker/bridgeExecutor/L2Monitor.ts index e6171b62..c3287f5b 100644 --- a/bots/src/worker/bridgeExecutor/L2Monitor.ts +++ b/bots/src/worker/bridgeExecutor/L2Monitor.ts @@ -1,174 +1,112 @@ -import { - ExecutorCoinEntity, - ExecutorOutputEntity, - ExecutorWithdrawalTxEntity -} from 'orm'; +import { ExecutorOutputEntity, ExecutorWithdrawalTxEntity } from 'orm'; import { Monitor } from './Monitor'; -import { fetchBridgeConfig } from 'lib/lcd'; -import { WithdrawalStorage } from 'lib/storage'; -import { BridgeConfig, WithdrawalTx } from 'lib/types'; +import { WithdrawStorage } from 'lib/storage'; +import { WithdrawalTx } from 'lib/types'; import { EntityManager } from 'typeorm'; -import { BlockInfo } from '@initia/minitia.js'; +import { BlockInfo } from '@initia/initia.js'; import { getDB } from './db'; -import { RPCSocket } from 'lib/rpc'; +import { RPCClient, RPCSocket } from 'lib/rpc'; import winston from 'winston'; import { getConfig } from 'config'; +import { getBridgeInfo } from 'lib/query'; const config = getConfig(); export class L2Monitor extends Monitor { submissionInterval: number; - nextCheckpointBlockHeight: number; + nextSubmissionTimeSec: number; - constructor(public socket: RPCSocket, logger: winston.Logger) { - super(socket, logger); + constructor( + public socket: RPCSocket, + public rpcClient: RPCClient, + logger: winston.Logger + ) { + super(socket, rpcClient, logger); [this.db] = getDB(); + this.nextSubmissionTimeSec = this.getCurTimeSec(); } public name(): string { return 'executor_l2_monitor'; } - private async configureBridge( - lastCheckpointBlockHeight: number - ): Promise { - const cfg: BridgeConfig = await fetchBridgeConfig(); - this.submissionInterval = parseInt(cfg.submission_interval); - - const checkpointBlockHeight = - lastCheckpointBlockHeight === 0 - ? parseInt(cfg.starting_block_number) - : lastCheckpointBlockHeight + this.submissionInterval; - - this.nextCheckpointBlockHeight = - checkpointBlockHeight + this.submissionInterval; + dateToSeconds(date: Date): number { + return Math.floor(date.getTime() / 1000); } - public async run(): Promise { - try { - await this.db.transaction( - async (transactionalEntityManager: EntityManager) => { - const lastCheckpointBlockHeight = - await this.helper.getCheckpointBlockHeight( - transactionalEntityManager, - ExecutorOutputEntity - ); - await this.configureBridge(lastCheckpointBlockHeight); - await super.run(); - } - ); - } catch (err) { - throw new Error(err); - } + private async setNextSubmissionTimeSec(): Promise { + const bridgeInfo = await getBridgeInfo(this.bridgeId); + this.submissionInterval = + bridgeInfo.bridge_config.submission_interval.seconds.toNumber(); + this.nextSubmissionTimeSec += this.submissionInterval; } - private genTx( - data: { [key: string]: string }, - coin: ExecutorCoinEntity, - lastIndex: number - ): ExecutorWithdrawalTxEntity { - return { - sequence: Number.parseInt(data['l2_sequence']), - sender: data['from'], - receiver: data['to'], - amount: Number.parseInt(data['amount']), - l2Id: config.L2ID, - metadata: coin.l1Metadata, - outputIndex: lastIndex + 1, - merkleRoot: '', - merkleProof: [] - }; + private getCurTimeSec(): number { + return this.dateToSeconds(new Date()); } - private async handleTokenBridgeInitiatedEvent( + private async handleInitiateTokenWithdrawalEvent( manager: EntityManager, data: { [key: string]: string } - ) { - const lastIndex = await this.helper.getLastOutputIndex( + ): Promise { + const outputInfo = await this.helper.getLastOutputFromDB( manager, ExecutorOutputEntity ); - - const metadata = data['metadata']; - const coin = await this.helper.getCoin( - manager, - ExecutorCoinEntity, - metadata + if (!outputInfo) return; + const pair = await config.l1lcd.ophost.tokenPairByL2Denom( + this.bridgeId, + data['denom'] ); - if (!coin) { - this.logger.warn(`coin not found for ${metadata}`); - return; - } + const tx: ExecutorWithdrawalTxEntity = { + l1Denom: pair.l1_denom, + l2Denom: pair.l2_denom, + sequence: data['l2_sequence'], + sender: data['from'], + receiver: data['to'], + amount: data['amount'], + bridgeId: this.bridgeId.toString(), + outputIndex: outputInfo ? outputInfo.outputIndex + 1 : 1, + merkleRoot: '', + merkleProof: [] + }; - const tx: ExecutorWithdrawalTxEntity = this.genTx(data, coin, lastIndex); - this.logger.info(`withdraw tx in height ${this.syncedHeight}`); await this.helper.saveEntity(manager, ExecutorWithdrawalTxEntity, tx); } - public async handleTokenRegisteredEvent( - manager: EntityManager, - data: { [key: string]: string } - ) { - const symbol = data['symbol']; - await manager.getRepository(ExecutorCoinEntity).update( - { - l2Denom: symbol - }, - { isChecked: true } - ) - } + public async handleEvents(manager: EntityManager): Promise { + const [isEmpty, withdrawalEvents] = await this.helper.fetchEvents( + config.l2lcd, + this.currentHeight, + 'initiate_token_withdrawal' + ); + if (isEmpty) return false; + for (const evt of withdrawalEvents) { + const attrMap = this.helper.eventsToAttrMap(evt); + await this.handleInitiateTokenWithdrawalEvent(manager, attrMap); + } - public async handleEvents(): Promise { - await this.db.transaction( - async (transactionalEntityManager: EntityManager) => { - const events = await this.helper.fetchEvents( - config.l2lcd, - this.syncedHeight, - 'move' - ); - - for (const evt of events) { - const attrMap = this.helper.eventsToAttrMap(evt); - const data: { [key: string]: string } = - this.helper.parseData(attrMap); - - switch (attrMap['type_tag']) { - case '0x1::op_bridge::TokenBridgeInitiatedEvent': { - await this.handleTokenBridgeInitiatedEvent( - transactionalEntityManager, - data - ); - break; - } - case '0x1::op_bridge::TokenRegisteredEvent': { - await this.handleTokenRegisteredEvent( - transactionalEntityManager, - data - ); - break; - } - } - } - } - ); + return true; } private async saveMerkleRootAndProof( manager: EntityManager, entities: ExecutorWithdrawalTxEntity[] ): Promise { - const txs: WithdrawalTx[] = entities.map((entity) => ({ - sequence: entity.sequence, - sender: entity.sender, - receiver: entity.receiver, - amount: entity.amount, - l2_id: entity.l2Id, - metadata: entity.metadata - })); - - const storage = new WithdrawalStorage(txs); + const txs: WithdrawalTx[] = entities.map( + (entity: ExecutorWithdrawalTxEntity) => ({ + bridge_id: BigInt(entity.bridgeId), + sequence: BigInt(entity.sequence), + sender: entity.sender, + receiver: entity.receiver, + l1_denom: entity.l1Denom, + amount: BigInt(entity.amount) + }) + ); + + const storage = new WithdrawStorage(txs); const storageRoot = storage.getMerkleRoot(); for (let i = 0; i < entities.length; i++) { entities[i].merkleRoot = storageRoot; @@ -182,45 +120,45 @@ export class L2Monitor extends Monitor { return storageRoot; } - public async handleBlock(): Promise { - if (this.syncedHeight < this.nextCheckpointBlockHeight - 1) return; - - await this.db.transaction( - async (transactionalEntityManager: EntityManager) => { - const lastIndex = await this.helper.getLastOutputIndex( - transactionalEntityManager, - ExecutorOutputEntity - ); - const blockInfo: BlockInfo = await config.l2lcd.tendermint.blockInfo( - this.syncedHeight - ); - - // fetch txs and build merkle tree for withdrawal storage - const txEntities = await this.helper.getWithdrawalTxs( - transactionalEntityManager, - ExecutorWithdrawalTxEntity, - lastIndex - ); - - const storageRoot = await this.saveMerkleRootAndProof( - transactionalEntityManager, - txEntities - ); - - const outputEntity = this.helper.calculateOutputEntity( - lastIndex, - blockInfo, - storageRoot, - this.nextCheckpointBlockHeight - this.submissionInterval - ); - - await this.helper.saveEntity( - transactionalEntityManager, - ExecutorOutputEntity, - outputEntity - ); - this.nextCheckpointBlockHeight += this.submissionInterval; - } + public async handleBlock(manager: EntityManager): Promise { + if (this.getCurTimeSec() < this.nextSubmissionTimeSec) return; + const lastOutput = await this.helper.getLastOutputFromDB( + manager, + ExecutorOutputEntity + ); + + const lastOutputEndBlockNumber = lastOutput ? lastOutput.endBlockNumber : 0; + const lastOutputIndex = lastOutput ? lastOutput.outputIndex : 0; + + const startBlockNumber = lastOutputEndBlockNumber + 1; + const endBlockNumber = this.currentHeight; + const outputIndex = lastOutputIndex + 1; + + if (startBlockNumber > endBlockNumber) return; + + const blockInfo: BlockInfo = await config.l2lcd.tendermint.blockInfo( + this.currentHeight + ); + + // fetch txs and build merkle tree for withdrawal storage + const txEntities = await this.helper.getWithdrawalTxs( + manager, + ExecutorWithdrawalTxEntity, + outputIndex ); + + const storageRoot = await this.saveMerkleRootAndProof(manager, txEntities); + + const outputEntity = this.helper.calculateOutputEntity( + outputIndex, + blockInfo, + storageRoot, + startBlockNumber, + endBlockNumber + ); + + await this.helper.saveEntity(manager, ExecutorOutputEntity, outputEntity); + + await this.setNextSubmissionTimeSec(); } } diff --git a/bots/src/worker/bridgeExecutor/Monitor.ts b/bots/src/worker/bridgeExecutor/Monitor.ts index 448ad033..45a2f247 100644 --- a/bots/src/worker/bridgeExecutor/Monitor.ts +++ b/bots/src/worker/bridgeExecutor/Monitor.ts @@ -1,17 +1,31 @@ import * as Bluebird from 'bluebird'; -import { RPCSocket } from 'lib/rpc'; +import { RPCClient, RPCSocket } from 'lib/rpc'; import { StateEntity } from 'orm'; -import { DataSource } from 'typeorm'; +import { DataSource, EntityManager } from 'typeorm'; import MonitorHelper from './MonitorHelper'; import winston from 'winston'; -import { INTERVAL_MONITOR } from 'config'; +import { INTERVAL_MONITOR, getConfig } from 'config'; + +const config = getConfig(); +const MAX_BLOCKS = 20; // DO NOT CHANGE THIS, hard limit is 20 in cometbft. +const MAX_RETRY_INTERVAL = 30_000; export abstract class Monitor { public syncedHeight: number; + public currentHeight: number; protected db: DataSource; protected isRunning = false; + protected bridgeId: number; + protected retryNum = 0; helper: MonitorHelper = new MonitorHelper(); - constructor(public socket: RPCSocket, public logger: winston.Logger) {} + + constructor( + public socket: RPCSocket, + public rpcClient: RPCClient, + public logger: winston.Logger + ) { + this.bridgeId = config.BRIDGE_ID; + } public async run(): Promise { const state = await this.db.getRepository(StateEntity).findOne({ @@ -37,36 +51,84 @@ export abstract class Monitor { this.isRunning = false; } + async handleBlockWithStateUpdate(manager: EntityManager): Promise { + await this.handleBlock(manager); + if (this.syncedHeight % 10 === 0) { + this.logger.info(`${this.name()} height ${this.syncedHeight}`); + } + this.syncedHeight++; + await manager + .getRepository(StateEntity) + .update({ name: this.name() }, { height: this.syncedHeight }); + } + public async monitor(): Promise { while (this.isRunning) { try { const latestHeight = this.socket.latestHeight; - if (!latestHeight || this.syncedHeight >= latestHeight) continue; - if ((this.syncedHeight + 1) % 10 == 0 && this.syncedHeight !== 0) { - this.logger.info(`${this.name()} height ${this.syncedHeight + 1}`); - } - await this.handleEvents(); - - this.syncedHeight += 1; - await this.handleBlock(); - // update state - await this.db - .getRepository(StateEntity) - .update({ name: this.name() }, { height: this.syncedHeight }); + if (!latestHeight || !(latestHeight > this.syncedHeight)) continue; + const blockchainData = await this.rpcClient.getBlockchain( + this.syncedHeight + 1, + // cap the query to fetch 20 blocks at maximum + // DO NOT CHANGE THIS, hard limit is 20 in cometbft. + Math.min(latestHeight, this.syncedHeight + MAX_BLOCKS) + ); + if (blockchainData === null) continue; + + await this.db.transaction(async (manager: EntityManager) => { + for (const metadata of blockchainData?.block_metas.reverse()) { + this.currentHeight = this.syncedHeight + 1; + + if (this.currentHeight !== parseInt(metadata.header.height)) { + throw new Error( + `expected block meta is the height ${this.currentHeight}, but got ${metadata.header.height}` + ); + } + + // WARN: THIS SHOULD BE REMOVED AFTER MINITIA UPDATED + // every block except the first block has at least one tx (skip blockSDK). + if ( + parseInt(metadata.num_txs) === 0 || + (this.name() == 'executor_l2_monitor' && + ((this.currentHeight !== 1 && + parseInt(metadata.num_txs) === 1) || + (this.currentHeight === 1 && + parseInt(metadata.num_txs) === 0))) + ) { + await this.handleBlockWithStateUpdate(manager); + continue; + } + + // handle event always called when there is a tx in a block, + // so empty means, the tx indexing is still on going. + const ok: boolean = await this.handleEvents(manager); + if (!ok) { + this.retryNum++; + if (this.retryNum * INTERVAL_MONITOR >= MAX_RETRY_INTERVAL) { + // rotate when tx index data is not found during 30s after block stored. + this.rpcClient.rotateRPC(); + } + break; + } + this.retryNum = 0; + await this.handleBlockWithStateUpdate(manager); + } + }); } catch (err) { this.stop(); + this.logger.error(err); throw new Error(`Error in ${this.name()} ${err}`); } finally { - await Bluebird.Promise.delay(INTERVAL_MONITOR); + await Bluebird.delay(INTERVAL_MONITOR); } } } // eslint-disable-next-line - public async handleEvents(): Promise {} + public async handleEvents(manager: EntityManager): Promise {} // eslint-disable-next-line - public async handleBlock(): Promise {} + public async handleBlock(manager: EntityManager): Promise {} // eslint-disable-next-line public name(): string { diff --git a/bots/src/worker/bridgeExecutor/MonitorHelper.ts b/bots/src/worker/bridgeExecutor/MonitorHelper.ts index 95f2fccb..3a241bff 100644 --- a/bots/src/worker/bridgeExecutor/MonitorHelper.ts +++ b/bots/src/worker/bridgeExecutor/MonitorHelper.ts @@ -1,5 +1,9 @@ -import { BlockInfo } from '@initia/minitia.js'; +import { BlockInfo, LCDClient, TxInfo, TxLog } from '@initia/initia.js'; +import { getLatestOutputFromExecutor, getOutputFromExecutor } from 'lib/query'; +import { WithdrawStorage } from 'lib/storage'; +import { WithdrawalTx } from 'lib/types'; import { sha3_256 } from 'lib/util'; +import OutputEntity from 'orm/executor/OutputEntity'; import { EntityManager, EntityTarget, ObjectLiteral } from 'typeorm'; class MonitorHelper { @@ -19,10 +23,10 @@ class MonitorHelper { public async getWithdrawalTxs( manager: EntityManager, entityClass: EntityTarget, - lastIndex: number + outputIndex: number ): Promise { return await manager.getRepository(entityClass).find({ - where: { outputIndex: lastIndex + 1 } as any + where: { outputIndex } as any }); } @@ -50,11 +54,12 @@ class MonitorHelper { public async getLastOutputFromDB( manager: EntityManager, entityClass: EntityTarget - ): Promise { - return await manager.getRepository(entityClass).find({ + ): Promise { + const lastOutput = await manager.getRepository(entityClass).find({ order: { outputIndex: 'DESC' } as any, take: 1 }); + return lastOutput[0] ?? null; } public async getLastOutputIndex( @@ -62,16 +67,18 @@ class MonitorHelper { entityClass: EntityTarget ): Promise { const lastOutput = await this.getLastOutputFromDB(manager, entityClass); - const lastIndex = lastOutput.length == 0 ? -1 : lastOutput[0].outputIndex; + const lastIndex = lastOutput ? lastOutput.outputIndex : 0; return lastIndex; } - public async getCheckpointBlockHeight( + public async getOutputByIndex( manager: EntityManager, - entityClass: EntityTarget - ): Promise { - const lastOutput = await this.getLastOutputFromDB(manager, entityClass); - return lastOutput.length == 0 ? 0 : lastOutput[0].checkpointBlockHeight; + entityClass: EntityTarget, + outputIndex: number + ): Promise { + return await manager.getRepository(entityClass).findOne({ + where: { outputIndex } as any + }); } /// @@ -88,18 +95,26 @@ class MonitorHelper { /// /// UTIL /// + public async fetchEvents( - lcd: any, + lcd: LCDClient, height: number, eventType: string - ): Promise { + ): Promise<[boolean, any[]]> { const searchRes = await lcd.tx.search({ - events: [{ key: 'tx.height', value: (height + 1).toString() }] + events: [{ key: 'tx.height', value: height.toString() }] }); - return searchRes.txs - .flatMap((tx) => tx.logs ?? []) - .flatMap((log) => log.events) - .filter((evt) => evt.type === eventType); + const extractEvents = (txs) => + txs + .filter((tx: TxInfo) => tx.logs && tx.logs.length > 0) + .flatMap((tx: TxInfo) => tx.logs ?? []) + .flatMap((log: TxLog) => log.events) + .filter((evt: Event) => evt.type === eventType); + + const isEmpty = searchRes.txs.length === 0; + const events = extractEvents(searchRes.txs); + + return [isEmpty, events]; } public eventsToAttrMap(event: any): { [key: string]: string } { @@ -123,34 +138,76 @@ class MonitorHelper { /// L2 HELPER /// public calculateOutputEntity( - lastIndex: number, + outputIndex: number, blockInfo: BlockInfo, storageRoot: string, - checkpointBlockHeight: number - ) { - const version = lastIndex + 1; + startBlockNumber: number, + endBlockNumber: number + ): OutputEntity { + const version = outputIndex; const stateRoot = blockInfo.block.header.app_hash; const lastBlockHash = blockInfo.block_id.hash; - const outputRoot = sha3_256( Buffer.concat([ - Buffer.from(version.toString()), + sha3_256(version), Buffer.from(stateRoot, 'base64'), - Buffer.from(storageRoot, 'hex'), + Buffer.from(storageRoot, 'base64'), Buffer.from(lastBlockHash, 'base64') ]) - ).toString('hex'); + ).toString('base64'); const outputEntity = { - outputIndex: lastIndex + 1, + outputIndex, outputRoot, stateRoot, storageRoot, lastBlockHash, - checkpointBlockHeight + startBlockNumber, + endBlockNumber }; + return outputEntity; } + + async saveMerkleRootAndProof( + manager: EntityManager, + entityClass: EntityTarget, + entities: any[] // ChallengerWithdrawalTxEntity[] or ExecutorWithdrawalTxEntity[] + ): Promise { + const txs: WithdrawalTx[] = entities.map((entity) => ({ + bridge_id: BigInt(entity.bridgeId), + sequence: BigInt(entity.sequence), + sender: entity.sender, + receiver: entity.receiver, + l1_denom: entity.l1Denom, + amount: BigInt(entity.amount) + })); + + const storage = new WithdrawStorage(txs); + const storageRoot = storage.getMerkleRoot(); + for (let i = 0; i < entities.length; i++) { + entities[i].merkleRoot = storageRoot; + entities[i].merkleProof = storage.getMerkleProof(txs[i]); + await this.saveEntity(manager, entityClass, entities[i]); + } + return storageRoot; + } + + public async getLatestOutputFromExecutor() { + const outputRes = await getLatestOutputFromExecutor(); + if (!outputRes.output) { + throw new Error('No output from executor'); + } + return outputRes.output; + } + + public async getOutputFromExecutor(outputIndex: number) { + const outputRes = await getOutputFromExecutor(outputIndex); + if (!outputRes.output) { + throw new Error('No output from executor'); + } + return outputRes.output; + } } export default MonitorHelper; diff --git a/bots/src/worker/bridgeExecutor/Resurrector.ts b/bots/src/worker/bridgeExecutor/Resurrector.ts new file mode 100644 index 00000000..f4226769 --- /dev/null +++ b/bots/src/worker/bridgeExecutor/Resurrector.ts @@ -0,0 +1,101 @@ +import { getDB } from './db'; +import FailedTxEntity from 'orm/executor/FailedTxEntity'; +import { Coin, MsgFinalizeTokenDeposit } from '@initia/initia.js'; +import { INTERVAL_MONITOR, getConfig } from 'config'; +import { DataSource } from 'typeorm'; +import * as Bluebird from 'bluebird'; +import winston from 'winston'; +import { TxWallet, WalletType, getWallet, initWallet } from 'lib/wallet'; +import { buildFailedTxNotification, notifySlack } from 'lib/slack'; + +const config = getConfig(); + +export class Resurrector { + private db: DataSource; + isRunning = true; + executor: TxWallet; + errorCounter = 0; + + constructor(public logger: winston.Logger) { + [this.db] = getDB(); + initWallet(WalletType.Executor, config.l2lcd); + this.executor = getWallet(WalletType.Executor); + } + + async updateProcessed(failedTx: FailedTxEntity): Promise { + await this.db.getRepository(FailedTxEntity).update( + { + bridgeId: failedTx.bridgeId, + sequence: failedTx.sequence, + processed: false + }, + { processed: true } + ); + + this.logger.info( + `Resurrected failed tx: ${failedTx.bridgeId} ${failedTx.sequence}` + ); + } + + async resubmitFailedDepositTx(failedTx: FailedTxEntity): Promise { + const msg = new MsgFinalizeTokenDeposit( + this.executor.key.accAddress, + failedTx.sender, + failedTx.receiver, + new Coin(failedTx.l2Denom, failedTx.amount), + parseInt(failedTx.sequence), + failedTx.l1Height, + Buffer.from(failedTx.data, 'hex').toString('base64') + ); + + await this.executor.transaction([msg]).catch(async (_) => { + if (this.errorCounter++ < 20) { + await Bluebird.delay(5 * 1000); + return; + } + this.errorCounter = 0; + await notifySlack(buildFailedTxNotification(failedTx)); + }); + await this.updateProcessed(failedTx); + } + + async getFailedTxs(): Promise { + return await this.db.getRepository(FailedTxEntity).find({ + where: { + processed: false + } + }); + } + + public async ressurect(): Promise { + const failedTxs = await this.getFailedTxs(); + + for (const failedTx of failedTxs) { + const error = failedTx.error; + + // Check x/opchild/errors.go + if (error.includes('deposit already finalized')) { + await this.updateProcessed(failedTx); + continue; + } + await this.resubmitFailedDepositTx(failedTx); + } + } + + stop(): void { + this.isRunning = false; + } + + public async run() { + while (this.isRunning) { + try { + await this.ressurect(); + } catch (err) { + this.stop(); + throw new Error(err); + } finally { + await Bluebird.delay(INTERVAL_MONITOR); + } + } + } +} diff --git a/bots/src/worker/bridgeExecutor/db.ts b/bots/src/worker/bridgeExecutor/db.ts index 35a66586..e2169092 100644 --- a/bots/src/worker/bridgeExecutor/db.ts +++ b/bots/src/worker/bridgeExecutor/db.ts @@ -15,7 +15,6 @@ import * as CamelToSnakeNamingStrategy from 'orm/CamelToSnakeNamingStrategy'; import { ExecutorOutputEntity, ExecutorWithdrawalTxEntity, - ExecutorCoinEntity, ExecutorDepositTxEntity, ExecutorFailedTxEntity, StateEntity @@ -27,7 +26,6 @@ const staticOptions = { entities: [ ExecutorOutputEntity, ExecutorWithdrawalTxEntity, - ExecutorCoinEntity, ExecutorDepositTxEntity, ExecutorFailedTxEntity, StateEntity diff --git a/bots/src/worker/bridgeExecutor/index.ts b/bots/src/worker/bridgeExecutor/index.ts index 7848d7fd..404af75a 100644 --- a/bots/src/worker/bridgeExecutor/index.ts +++ b/bots/src/worker/bridgeExecutor/index.ts @@ -1,5 +1,4 @@ -import { RPCSocket } from 'lib/rpc'; -import { Monitor } from './Monitor'; +import { RPCClient, RPCSocket } from 'lib/rpc'; import { L1Monitor } from './L1Monitor'; import { L2Monitor } from './L2Monitor'; import { executorController } from 'controller'; @@ -8,16 +7,25 @@ import { executorLogger as logger } from 'lib/logger'; import { initORM, finalizeORM } from './db'; import { initServer, finalizeServer } from 'loader'; import { once } from 'lodash'; -import { WalletType, initWallet } from 'lib/wallet'; import { getConfig } from 'config'; +import { Resurrector } from './Resurrector'; const config = getConfig(); -let monitors: Monitor[]; +let monitors; async function runBot(): Promise { monitors = [ - new L1Monitor(new RPCSocket(config.L1_RPC_URI, 1000, logger), logger), - new L2Monitor(new RPCSocket(config.L2_RPC_URI, 1000, logger), logger) + new L1Monitor( + new RPCSocket(config.L1_RPC_URI, 10000, logger), + new RPCClient(config.L1_RPC_URI, logger), + logger + ), + new L2Monitor( + new RPCSocket(config.L2_RPC_URI, 10000, logger), + new RPCClient(config.L2_RPC_URI, logger), + logger + ), + new Resurrector(logger) ]; try { await Promise.all( @@ -52,7 +60,6 @@ export async function startExecutor(): Promise { try { await initORM(); await initServer(executorController, config.EXECUTOR_PORT); - initWallet(WalletType.Executor, config.l2lcd); await runBot(); } catch (err) { throw new Error(err); diff --git a/bots/src/worker/challenger/ChallegnerHelper.ts b/bots/src/worker/challenger/ChallegnerHelper.ts deleted file mode 100644 index 1ab210ad..00000000 --- a/bots/src/worker/challenger/ChallegnerHelper.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { EntityManager, EntityTarget, ObjectLiteral } from 'typeorm'; - -export const ENOT_EQUAL_TX = -1; - -class ChallengerHelper { - public async getUncheckedTx( - manager: EntityManager, - entityClass: EntityTarget - ): Promise { - const uncheckedWithdrawalTx = await manager - .getRepository(entityClass) - .find({ - where: { isChecked: false } as any, - order: { sequence: 'ASC' } as any, - take: 1 - }); - - if (uncheckedWithdrawalTx.length === 0) return null; - return uncheckedWithdrawalTx[0]; - } - - public async getLastOutputFromDB( - manager: EntityManager, - entityClass: EntityTarget - ): Promise { - return await manager.getRepository(entityClass).find({ - order: { outputIndex: 'DESC' } as any, - take: 1 - }); - } - - public async getLastOutputIndex( - manager: EntityManager, - entityClass: EntityTarget - ): Promise { - const lastOutput = await this.getLastOutputFromDB(manager, entityClass); - const lastIndex = lastOutput.length == 0 ? -1 : lastOutput[0].outputIndex; - return lastIndex; - } - - public async finalizeUncheckedTx( - manager: EntityManager, - entityClass: EntityTarget, - entity: T - ): Promise { - await manager.getRepository(entityClass).update( - { - metadata: entity.metadata, - sequence: entity.sequence - }, - { isChecked: true } as any - ); - } -} - -export default ChallengerHelper; diff --git a/bots/src/worker/challenger/L1Monitor.ts b/bots/src/worker/challenger/L1Monitor.ts index 93e672c7..fdc27ddb 100644 --- a/bots/src/worker/challenger/L1Monitor.ts +++ b/bots/src/worker/challenger/L1Monitor.ts @@ -1,17 +1,10 @@ import { Monitor } from 'worker/bridgeExecutor/Monitor'; import { - ChallengerCoinEntity, ChallengerDepositTxEntity, - ChallengerOutputEntity + ChallengerFinalizeWithdrawalTxEntity } from 'orm'; -import { - CoinInfo, - computeCoinMetadata, - normalizeMetadata, - resolveFAMetadata -} from 'lib/lcd'; import { EntityManager } from 'typeorm'; -import { RPCSocket } from 'lib/rpc'; +import { RPCClient, RPCSocket } from 'lib/rpc'; import { getDB } from './db'; import winston from 'winston'; import { getConfig } from 'config'; @@ -19,8 +12,12 @@ import { getConfig } from 'config'; const config = getConfig(); export class L1Monitor extends Monitor { - constructor(public socket: RPCSocket, logger: winston.Logger) { - super(socket, logger); + constructor( + public socket: RPCSocket, + public rpcClient: RPCClient, + logger: winston.Logger + ) { + super(socket, rpcClient, logger); [this.db] = getDB(); } @@ -28,94 +25,69 @@ export class L1Monitor extends Monitor { return 'challenger_l1_monitor'; } - public async handleTokenRegisteredEvent( + public async handleInitiateTokenDeposit( manager: EntityManager, data: { [key: string]: string } - ) { - const l1Metadata = data['l1_token']; - const l2Metadata = normalizeMetadata( - computeCoinMetadata('0x1', 'l2/' + data['l2_token']) - ); - - const l1CoinInfo: CoinInfo = await resolveFAMetadata( - config.l1lcd, - l1Metadata - ); - - const l1Denom = l1CoinInfo.denom; - const l2Denom = 'l2/' + data['l2_token']; - - const coin: ChallengerCoinEntity = { - l1Metadata: l1Metadata, - l1Denom: l1Denom, - l2Metadata: l2Metadata, - l2Denom: l2Denom, - isChecked: false + ): Promise { + const entity: ChallengerDepositTxEntity = { + sequence: data['l1_sequence'], + sender: data['from'], + receiver: data['to'], + l1Denom: data['l1_denom'], + l2Denom: data['l2_denom'], + amount: data['amount'], + data: data['data'] }; - - await this.helper.saveEntity(manager, ChallengerCoinEntity, coin); + await manager.getRepository(ChallengerDepositTxEntity).save(entity); } - public async handleTokenBridgeInitiatedEvent( + public async handleFinalizeTokenWithdrawalEvent( manager: EntityManager, data: { [key: string]: string } - ) { - const lastIndex = await this.helper.getLastOutputIndex( - manager, - ChallengerOutputEntity - ); - - const l2Metadata = normalizeMetadata( - computeCoinMetadata('0x1', 'l2/' + data['l2_token']) - ); - - const entity: ChallengerDepositTxEntity = { - sequence: Number.parseInt(data['l1_sequence']), + ): Promise { + const entity: ChallengerFinalizeWithdrawalTxEntity = { + bridgeId: data['bridge_id'], + outputIndex: parseInt(data['output_index']), + sequence: data['l2_sequence'], sender: data['from'], receiver: data['to'], - amount: Number.parseInt(data['amount']), - outputIndex: lastIndex + 1, - metadata: l2Metadata, - height: this.syncedHeight, - finalizedOutputIndex: null, - isChecked: false + l1Denom: data['l1_denom'], + l2Denom: data['l2_denom'], + amount: data['amount'] }; - await manager.getRepository(ChallengerDepositTxEntity).save(entity); + await manager + .getRepository(ChallengerFinalizeWithdrawalTxEntity) + .save(entity); } - public async handleEvents(): Promise { - await this.db.transaction( - async (transactionalEntityManager: EntityManager) => { - const events = await this.helper.fetchEvents( - config.l1lcd, - this.syncedHeight, - 'move' - ); + public async handleEvents(manager: EntityManager): Promise { + const [isEmpty, depositEvents] = await this.helper.fetchEvents( + config.l1lcd, + this.currentHeight, + 'initiate_token_deposit' + ); - for (const evt of events) { - const attrMap = this.helper.eventsToAttrMap(evt); - const data = this.helper.parseData(attrMap); - if (data['l2_id'] !== config.L2ID) continue; + if (isEmpty) return false; - switch (attrMap['type_tag']) { - case '0x1::op_bridge::TokenRegisteredEvent': { - await this.handleTokenRegisteredEvent( - transactionalEntityManager, - data - ); - break; - } - case '0x1::op_bridge::TokenBridgeInitiatedEvent': { - await this.handleTokenBridgeInitiatedEvent( - transactionalEntityManager, - data - ); - break; - } - } - } - } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [_, withdrawalEvents] = await this.helper.fetchEvents( + config.l1lcd, + this.currentHeight, + 'finalize_token_withdrawal' ); + + for (const evt of depositEvents) { + const attrMap = this.helper.eventsToAttrMap(evt); + if (attrMap['bridge_id'] !== this.bridgeId.toString()) continue; + await this.handleInitiateTokenDeposit(manager, attrMap); + } + + for (const evt of withdrawalEvents) { + const attrMap = this.helper.eventsToAttrMap(evt); + if (attrMap['bridge_id'] !== this.bridgeId.toString()) continue; + await this.handleFinalizeTokenWithdrawalEvent(manager, attrMap); + } + return true; } } diff --git a/bots/src/worker/challenger/L2Monitor.ts b/bots/src/worker/challenger/L2Monitor.ts index cfd5f6eb..dcdfbe89 100644 --- a/bots/src/worker/challenger/L2Monitor.ts +++ b/bots/src/worker/challenger/L2Monitor.ts @@ -1,305 +1,106 @@ import { - ChallengerCoinEntity, + ChallengerFinalizeDepositTxEntity, ChallengerOutputEntity, - ChallengerDepositTxEntity, - ChallengerWithdrawalTxEntity, - StateEntity + ChallengerWithdrawalTxEntity } from 'orm'; +import { OutputInfo } from '@initia/initia.js'; import { Monitor } from 'worker/bridgeExecutor/Monitor'; -import { fetchBridgeConfig } from 'lib/lcd'; -import { WithdrawalStorage } from 'lib/storage'; -import { BridgeConfig, WithdrawalTx } from 'lib/types'; import { EntityManager } from 'typeorm'; -import { RPCSocket } from 'lib/rpc'; +import { RPCClient, RPCSocket } from 'lib/rpc'; import winston from 'winston'; import { getDB } from './db'; import { getConfig } from 'config'; -import { delay } from 'bluebird'; -import { ENOT_EQUAL_TX } from './ChallegnerHelper'; - const config = getConfig(); export class L2Monitor extends Monitor { - submissionInterval: number; - nextCheckpointBlockHeight: number; - - constructor(public socket: RPCSocket, logger: winston.Logger) { - super(socket, logger); + outputIndex: number; + outputInfo: OutputInfo; + startBlockNumber: number; + + constructor( + public socket: RPCSocket, + public rpcClient: RPCClient, + logger: winston.Logger + ) { + super(socket, rpcClient, logger); [this.db] = getDB(); + this.outputIndex = 0; } public name(): string { return 'challenger_l2_monitor'; } - private async configureBridge( - lastCheckpointBlockHeight: number - ): Promise { - const cfg: BridgeConfig = await fetchBridgeConfig(); - this.submissionInterval = parseInt(cfg.submission_interval); - - const checkpointBlockHeight = - lastCheckpointBlockHeight === 0 - ? parseInt(cfg.starting_block_number) - : lastCheckpointBlockHeight + this.submissionInterval; - - this.nextCheckpointBlockHeight = - checkpointBlockHeight + this.submissionInterval; - } - - public async run(): Promise { - try { - await this.db.transaction( - async (transactionalEntityManager: EntityManager) => { - const lastCheckpointBlockHeight = - await this.helper.getCheckpointBlockHeight( - transactionalEntityManager, - ChallengerOutputEntity - ); - await this.configureBridge(lastCheckpointBlockHeight); - await super.run(); - } - ); - } catch (err) { - throw new Error(err); - } - } - - private genTx( - data: { [key: string]: string }, - coin: ChallengerCoinEntity, - lastIndex: number - ): ChallengerWithdrawalTxEntity { - return { - sequence: Number.parseInt(data['l2_sequence']), - sender: data['from'], - receiver: data['to'], - amount: Number.parseInt(data['amount']), - l2Id: config.L2ID, - metadata: coin.l1Metadata, - outputIndex: lastIndex + 1, - merkleRoot: '', - merkleProof: [], - isChecked: false - }; - } - - private async handleTokenBridgeInitiatedEvent( + private async handleInitiateTokenWithdrawalEvent( manager: EntityManager, data: { [key: string]: string } - ) { - const lastIndex = await this.helper.getLastOutputIndex( + ): Promise { + const outputInfo = await this.helper.getLastOutputFromDB( manager, ChallengerOutputEntity ); - - const metadata = data['metadata']; - const coin = await this.helper.getCoin( - manager, - ChallengerCoinEntity, - metadata + if (!outputInfo) return; + const pair = await config.l1lcd.ophost.tokenPairByL2Denom( + this.bridgeId, + data['denom'] ); - if (!coin) { - this.logger.warn(`coin not found for ${metadata}`); - return; - } + const tx: ChallengerWithdrawalTxEntity = { + l1Denom: pair.l1_denom, + l2Denom: pair.l2_denom, + sequence: data['l2_sequence'], + sender: data['from'], + receiver: data['to'], + amount: data['amount'], + bridgeId: this.bridgeId.toString(), + outputIndex: outputInfo ? outputInfo.outputIndex + 1 : 1, + merkleRoot: '', + merkleProof: [] + }; - const tx: ChallengerWithdrawalTxEntity = this.genTx(data, coin, lastIndex); - this.logger.info(`withdraw tx in height ${this.syncedHeight}`); await this.helper.saveEntity(manager, ChallengerWithdrawalTxEntity, tx); } - // sync deposit txs every 500ms - private async syncDepositTx() { - const depositEvents = await this.helper.fetchEvents( - config.l2lcd, - this.syncedHeight, - 'deposit' - ); - - for (const evt of depositEvents) { - const attrMap = this.helper.eventsToAttrMap(evt); - const targetHeight = parseInt(attrMap['deposit_height']); - for (;;) { - const l1State: StateEntity | null = await this.db - .getRepository(StateEntity) - .findOne({ - where: { - name: 'challenger_l1_monitor' - } - }); - if (!l1State) throw new Error('challenger l1 state not found'); - if (targetHeight < l1State.height) return; - this.logger.info( - `syncing deposit tx height ${targetHeight} in height ${this.syncedHeight}...` - ); - await delay(500); - } - } - } - - public async handleTokenRegisteredEvent( + public async handleFinalizeTokenDepositEvent( manager: EntityManager, data: { [key: string]: string } - ) { - const symbol = data['symbol']; - await manager.getRepository(ChallengerCoinEntity).update( - { - l2Denom: symbol - }, - { isChecked: true } - ) - } - - private async handleTokenBridgeFinalizedEvent( - manager: EntityManager, - data: { [key: string]: string } - ) { - await this.syncDepositTx(); - - const metadata = data['metadata']; - const depositTx = await this.helper.getDepositTx( - manager, - ChallengerDepositTxEntity, - Number.parseInt(data['l1_sequence']), - metadata - ); - if (!depositTx) throw new Error('deposit tx not found'); - - const lastIndex = await this.helper.getLastOutputIndex( - manager, - ChallengerOutputEntity - ); - - const isTxSame = (originTx: ChallengerDepositTxEntity): boolean => { - return ( - originTx.sequence === Number.parseInt(data['l1_sequence']) && - originTx.sender === data['from'] && - originTx.receiver === data['to'] && - Number(originTx.amount) === Number.parseInt(data['amount']) // originTx.amount could be string due to typeorm bigint - ); + ): Promise { + const entity: ChallengerFinalizeDepositTxEntity = { + sequence: data['l1_sequence'], + sender: data['sender'], + receiver: data['recipient'], + l2Denom: data['denom'], + amount: data['amount'], + l1Height: parseInt(data['finalize_height']) }; - - const finalizedIndex = isTxSame(depositTx) ? lastIndex + 1 : ENOT_EQUAL_TX; - - await manager.getRepository(ChallengerDepositTxEntity).update( - { - sequence: depositTx.sequence, - metadata: depositTx.metadata - }, - { finalizedOutputIndex: finalizedIndex } - ); + await manager.getRepository(ChallengerFinalizeDepositTxEntity).save(entity); } - public async handleEvents(): Promise { - await this.db.transaction( - async (transactionalEntityManager: EntityManager) => { - const events = await this.helper.fetchEvents( - config.l2lcd, - this.syncedHeight, - 'move' - ); - - for (const evt of events) { - const attrMap = this.helper.eventsToAttrMap(evt); - const data: { [key: string]: string } = - this.helper.parseData(attrMap); - - switch (attrMap['type_tag']) { - case '0x1::op_bridge::TokenBridgeInitiatedEvent': { - await this.handleTokenBridgeInitiatedEvent( - transactionalEntityManager, - data - ); - break; - } - case '0x1::op_bridge::TokenBridgeFinalizedEvent': { - await this.handleTokenBridgeFinalizedEvent( - transactionalEntityManager, - data - ); - break; - } - case '0x1::op_bridge::TokenRegisteredEvent': { - await this.handleTokenRegisteredEvent( - transactionalEntityManager, - data - ); - break; - } - } - } - } + public async handleEvents(manager: EntityManager): Promise { + const [isEmpty, withdrawalEvents] = await this.helper.fetchEvents( + config.l2lcd, + this.currentHeight, + 'initiate_token_withdrawal' ); - } + if (isEmpty) return false; - private async saveMerkleRootAndProof( - manager: EntityManager, - entities: ChallengerWithdrawalTxEntity[] - ): Promise { - const txs: WithdrawalTx[] = entities.map((entity) => ({ - sequence: entity.sequence, - sender: entity.sender, - receiver: entity.receiver, - amount: entity.amount, - l2_id: entity.l2Id, - metadata: entity.metadata - })); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [_, depositEvents] = await this.helper.fetchEvents( + config.l2lcd, + this.currentHeight, + 'finalize_token_deposit' + ); - const storage = new WithdrawalStorage(txs); - const storageRoot = storage.getMerkleRoot(); - for (let i = 0; i < entities.length; i++) { - entities[i].merkleRoot = storageRoot; - entities[i].merkleProof = storage.getMerkleProof(txs[i]); - await this.helper.saveEntity( - manager, - ChallengerWithdrawalTxEntity, - entities[i] - ); + for (const evt of withdrawalEvents) { + const attrMap = this.helper.eventsToAttrMap(evt); + await this.handleInitiateTokenWithdrawalEvent(manager, attrMap); } - return storageRoot; - } - public async handleBlock(): Promise { - if (this.syncedHeight < this.nextCheckpointBlockHeight - 1) return; - - await this.db.transaction( - async (transactionalEntityManager: EntityManager) => { - const lastIndex = await this.helper.getLastOutputIndex( - transactionalEntityManager, - ChallengerOutputEntity - ); - const blockInfo = await config.l2lcd.tendermint.blockInfo( - this.syncedHeight - ); - - // fetch txs and build merkle tree for withdrawal storage - const txEntities = await this.helper.getWithdrawalTxs( - transactionalEntityManager, - ChallengerWithdrawalTxEntity, - lastIndex - ); - - const storageRoot = await this.saveMerkleRootAndProof( - transactionalEntityManager, - txEntities - ); - - const outputEntity = this.helper.calculateOutputEntity( - lastIndex, - blockInfo, - storageRoot, - this.nextCheckpointBlockHeight - this.submissionInterval - ); + for (const evt of depositEvents) { + const attrMap = this.helper.eventsToAttrMap(evt); + await this.handleFinalizeTokenDepositEvent(manager, attrMap); + } - await this.helper.saveEntity( - transactionalEntityManager, - ChallengerOutputEntity, - outputEntity - ); - this.nextCheckpointBlockHeight += this.submissionInterval; - } - ); + return true; } } diff --git a/bots/src/worker/challenger/challenger.ts b/bots/src/worker/challenger/challenger.ts index fa89ef28..40cf6f6a 100644 --- a/bots/src/worker/challenger/challenger.ts +++ b/bots/src/worker/challenger/challenger.ts @@ -1,183 +1,208 @@ -import { Wallet, MnemonicKey, BCS, Msg, MsgExecute } from '@initia/initia.js'; -import { DataSource, ManyToMany } from 'typeorm'; +import { BridgeInfo } from '@initia/initia.js'; +import { DataSource, MoreThan } from 'typeorm'; import { getDB } from './db'; import { - ChallengerWithdrawalTxEntity, ChallengerDepositTxEntity, + ChallengerFinalizeDepositTxEntity, ChallengerOutputEntity, - StateEntity, - ChallengerCoinEntity, - DeletedOutputEntity + ChallengerWithdrawalTxEntity, + ChallengerDeletedOutputEntity } from 'orm'; import { delay } from 'bluebird'; import { challengerLogger as logger } from 'lib/logger'; -import { APIRequest } from 'lib/apiRequest'; -import { GetLatestOutputResponse } from 'service'; -import { fetchBridgeConfig } from 'lib/lcd'; -import axios from 'axios'; -import { GetAllCoinsResponse } from 'service/executor/CoinService'; -import { getConfig } from 'config'; -import { sendTx } from 'lib/tx'; -import ChallengerHelper, { ENOT_EQUAL_TX } from './ChallegnerHelper'; +import { INTERVAL_MONITOR, getConfig } from 'config'; import { EntityManager } from 'typeorm'; +import { + getLastOutputInfo, + getOutputInfoByIndex, + getBridgeInfo +} from 'lib/query'; +import MonitorHelper from 'worker/bridgeExecutor/MonitorHelper'; +import winston from 'winston'; const config = getConfig(); -const bcs = BCS.getInstance(); +const THRESHOLD_MISS_INTERVAL = 5; export class Challenger { - private challenger: Wallet; - private executor: Wallet; private isRunning = false; private db: DataSource; - private apiRequester: APIRequest; - private DEPOSIT_THRESHOLD = 10; // TODO: set threshold from contract config - private WITHDRAWAL_THRESHOLD = 10; // TODO: set threshold from contract config - helper: ChallengerHelper = new ChallengerHelper(); - - constructor(public isFetch: boolean) {} - - async init() { - // use to sync with bridge latest state - if (this.isFetch) await this.fetchBridgeState(); + bridgeId: number; + bridgeInfo: BridgeInfo; + l1LastChallengedSequence: number; + l1DepositSequenceToChallenge: number; + l2OutputIndexToChallenge: number; + submissionIntervalMs: number; + missCount: number; // count of miss interval to finalize deposit tx + threshold: number; // threshold of miss interval to finalize deposit tx + helper: MonitorHelper; + constructor(public logger: winston.Logger) { [this.db] = getDB(); - this.challenger = new Wallet( - config.l1lcd, - new MnemonicKey({ mnemonic: config.CHALLENGER_MNEMONIC }) - ); - this.executor = new Wallet( - config.l1lcd, - new MnemonicKey({ mnemonic: config.EXECUTOR_MNEMONIC }) - ); + this.bridgeId = config.BRIDGE_ID; this.isRunning = true; - } + this.l1DepositSequenceToChallenge = 1; + this.l2OutputIndexToChallenge = 1; + this.missCount = 0; - // TODO: fetch from finalized state, not latest state - public async fetchBridgeState() { - [this.db] = getDB(); - this.apiRequester = new APIRequest(config.EXECUTOR_URI); - const cfg = await fetchBridgeConfig(); - const outputRes = await this.apiRequester.getQuery( - '/output/latest' - ); - if (!outputRes) return; - const coinRes = await this.apiRequester.getQuery( - '/coin' - ); - if (!coinRes) return; - const l1Res = await axios.get( - `${config.L1_LCD_URI}/cosmos/base/tendermint/v1beta1/blocks/latest` - ); - if (!l1Res) return; + this.helper = new MonitorHelper(); + } - await this.db.transaction(async (manager: EntityManager) => { - await manager - .getRepository(ChallengerOutputEntity) - .save(outputRes.output); - await manager.getRepository(ChallengerCoinEntity).save(coinRes.coins); - await manager.getRepository(StateEntity).save([ - { - name: 'challenger_l1_monitor', - height: parseInt(l1Res.data.block.header.height) - }, - { - name: 'challenger_l2_monitor', - height: - outputRes.output.checkpointBlockHeight + - Number.parseInt(cfg.submission_interval) - - 1 - } - ]); - }); + async init(): Promise { + this.bridgeInfo = await getBridgeInfo(this.bridgeId); + this.submissionIntervalMs = + this.bridgeInfo.bridge_config.submission_interval.seconds.toNumber() * + 1000; } public async run(): Promise { await this.init(); - while (this.isRunning) { try { - await this.l1Challenge(); - await this.l2Challenge(); - } catch (e) { + await this.db.transaction(async (manager: EntityManager) => { + await this.l1Challenge(manager); + await this.l2Challenge(manager); + }); + } catch (err) { this.stop(); + console.log(err); } finally { - await delay(1_000); + await delay(INTERVAL_MONITOR); } } } - public async l1Challenge() { - await this.db.transaction(async (manager: EntityManager) => { - const unchekcedDepositTx = await this.helper.getUncheckedTx( - manager, - ChallengerDepositTxEntity - ); - if (!unchekcedDepositTx || !unchekcedDepositTx.finalizedOutputIndex) - return; + public async l1Challenge(manager: EntityManager) { + const lastOutputInfo = await getLastOutputInfo(this.bridgeId); + const depositTxFromChallenger = await manager + .getRepository(ChallengerDepositTxEntity) + .findOne({ + where: { sequence: this.l1DepositSequenceToChallenge } as any + }); - // case 1. not equal deposit tx between L1 and L2 - if (unchekcedDepositTx.finalizedOutputIndex === ENOT_EQUAL_TX) { - await this.deleteL2Outptut( - unchekcedDepositTx, - 'not same deposit tx between L1 and L2' - ); - return; - } + if (!depositTxFromChallenger) { + return; + } + this.l1DepositSequenceToChallenge = Number( + depositTxFromChallenger.sequence + ); - // case2. not finalized within threshold - if ( - unchekcedDepositTx.finalizedOutputIndex > - unchekcedDepositTx.outputIndex + this.DEPOSIT_THRESHOLD - ) { - await this.deleteL2Outptut( - unchekcedDepositTx, - 'deposit tx is not finalized for threshold submission interval' - ); - return; + // case 1. not finalized deposit tx + const depositFinalizeTxFromChallenger = await manager + .getRepository(ChallengerFinalizeDepositTxEntity) + .findOne({ + where: { sequence: this.l1DepositSequenceToChallenge } as any + }); + + if (!depositFinalizeTxFromChallenger) { + this.missCount += 1; + this.logger.warn( + `[L1 Challenger] not finalized deposit tx in sequence : ${this.l1DepositSequenceToChallenge}` + ); + if (this.missCount <= THRESHOLD_MISS_INTERVAL || !lastOutputInfo) { + return await delay(this.submissionIntervalMs); } + return await this.deleteOutputProposal( + manager, + lastOutputInfo.output_index, + `not finalized deposit tx within ${THRESHOLD_MISS_INTERVAL} submission interval ${depositFinalizeTxFromChallenger}` + ); + } + + // case 2. not equal deposit tx between L1 and L2 + const pair = await config.l1lcd.ophost.tokenPairByL1Denom( + this.bridgeId, + depositTxFromChallenger.l1Denom + ); + const isEqaul = + depositTxFromChallenger.sender === + depositFinalizeTxFromChallenger.sender && + depositTxFromChallenger.receiver === + depositFinalizeTxFromChallenger.receiver && + depositTxFromChallenger.amount === + depositFinalizeTxFromChallenger.amount && + pair.l2_denom === depositFinalizeTxFromChallenger.l2Denom; - await this.helper.finalizeUncheckedTx( + if (!isEqaul && lastOutputInfo) { + await this.deleteOutputProposal( manager, - ChallengerDepositTxEntity, - unchekcedDepositTx + lastOutputInfo.output_index, + `not equal deposit tx between L1 and L2` ); - }); + } + + if (this.l1LastChallengedSequence != this.l1DepositSequenceToChallenge) { + logger.info( + `[L1 Challenger] deposit tx matched in sequence : ${this.l1DepositSequenceToChallenge}` + ); + } + + this.missCount = 0; + this.l1LastChallengedSequence = this.l1DepositSequenceToChallenge; + // get next sequence from db with smallest sequence but bigger than last challenged sequence + const nextDepositSequenceToChallenge = await manager + .getRepository(ChallengerDepositTxEntity) + .find({ + where: { sequence: MoreThan(this.l1DepositSequenceToChallenge) } as any, + order: { sequence: 'ASC' }, + take: 1 + }); + if (nextDepositSequenceToChallenge.length === 0) return; + this.l1DepositSequenceToChallenge = Number( + nextDepositSequenceToChallenge[0].sequence + ); } public stop(): void { this.isRunning = false; + process.exit(); } - async getChallengerOutputRoot(outputIndex: number): Promise { - const challengerOutputEntity = await this.db - .getRepository(ChallengerOutputEntity) - .find({ - where: { outputIndex: outputIndex }, - take: 1 - }); + async getChallengerOutputRoot( + manager: EntityManager, + outputIndex: number + ): Promise { + const output = await getOutputInfoByIndex(this.bridgeId, outputIndex); + if (!output) return null; + const startBlockNumber = + outputIndex === 1 + ? 1 + : (await getOutputInfoByIndex(this.bridgeId, outputIndex - 1)) + .output_proposal.l2_block_number + 1; + const endBlockNumber = output.output_proposal.l2_block_number; + const blockInfo = await config.l2lcd.tendermint.blockInfo(endBlockNumber); + + const txEntities = await this.helper.getWithdrawalTxs( + manager, + ChallengerWithdrawalTxEntity, + outputIndex + ); + + const storageRoot = await this.helper.saveMerkleRootAndProof( + manager, + ChallengerWithdrawalTxEntity, + txEntities + ); + + const outputEntity = this.helper.calculateOutputEntity( + outputIndex, + blockInfo, + storageRoot, + startBlockNumber, + endBlockNumber + ); - if (challengerOutputEntity.length === 0) return null; - return challengerOutputEntity[0].outputRoot; + await this.helper.saveEntity(manager, ChallengerOutputEntity, outputEntity); + return outputEntity.outputRoot; } async getContractOutputRoot(outputIndex: number): Promise { try { - const outputRootFromContract = - await config.l1lcd.move.viewFunction( - '0x1', - 'op_output', - 'get_output_root', - [], - [ - bcs.serialize(BCS.ADDRESS, this.executor.key.accAddress), - bcs.serialize(BCS.STRING, config.L2ID), - bcs.serialize(BCS.U64, outputIndex) - ] - ); - return Array.from(outputRootFromContract) - .map((byte) => byte.toString(16)) - .join(''); - } catch (e) { + const outputInfo = await config.l1lcd.ophost.outputInfo( + this.bridgeId, + outputIndex + ); + return outputInfo.output_proposal.output_root; + } catch (err) { logger.warn( `[L2 Challenger] waiting for submitting output root in output index ${outputIndex}` ); @@ -185,116 +210,56 @@ export class Challenger { } } - public async l2Challenge() { - await this.db.transaction(async (manager: EntityManager) => { - const uncheckedWithdrawalTx = await this.helper.getUncheckedTx( - manager, - ChallengerWithdrawalTxEntity - ); - if (!uncheckedWithdrawalTx) return; + public async l2Challenge(manager: EntityManager) { + // condition 1. ouptut should be submitted + const outputInfoToChallenge = await getOutputInfoByIndex( + this.bridgeId, + this.l2OutputIndexToChallenge + ).catch(() => { + return null; + }); - // condition 1. ouptut should be submitted - const lastIndex = await this.helper.getLastOutputIndex( - manager, - ChallengerOutputEntity - ); - if ( - !uncheckedWithdrawalTx.outputIndex || - uncheckedWithdrawalTx.outputIndex > lastIndex - ) - return; + if (!outputInfoToChallenge) return; - // case 1. output root not matched - const outputRootFromContract = await this.getContractOutputRoot( - uncheckedWithdrawalTx.outputIndex - ); - const outputRootFromChallenger = await this.getChallengerOutputRoot( - uncheckedWithdrawalTx.outputIndex - ); - if (!outputRootFromContract || !outputRootFromChallenger) return; - const isOutputFinalized = await this.isFinalizedOutput( - uncheckedWithdrawalTx.outputIndex - ); - if ( - !isOutputFinalized && - outputRootFromContract !== outputRootFromChallenger - ) { - await this.deleteL2Outptut( - uncheckedWithdrawalTx, - `not equal output root from contract: ${outputRootFromContract}, from challenger: ${outputRootFromChallenger}` - ); - return; - } + // case 1. output root not matched + const outputRootFromContract = await this.getContractOutputRoot( + this.l2OutputIndexToChallenge + ); + const outputRootFromChallenger = await this.getChallengerOutputRoot( + manager, + this.l2OutputIndexToChallenge + ); + + if (!outputRootFromContract || !outputRootFromChallenger) return; - await this.helper.finalizeUncheckedTx( + if (outputRootFromContract !== outputRootFromChallenger) { + await this.deleteOutputProposal( manager, - ChallengerWithdrawalTxEntity, - uncheckedWithdrawalTx + this.l2OutputIndexToChallenge, + `not equal output root from contract: ${outputRootFromContract}, from challenger: ${outputRootFromChallenger}` ); - }); - } - - async isFinalizedOutput(outputIndex: number) { - const isFinalized: boolean = await config.l1lcd.move.viewFunction( - '0x1', - 'op_output', - 'is_finalized', - [], - [ - bcs.serialize(BCS.ADDRESS, this.executor.key.accAddress), - bcs.serialize(BCS.STRING, config.L2ID), - bcs.serialize(BCS.U64, outputIndex) - ] - ); - return isFinalized; - } + } - async isOutputSubmitted(outputIndex: number): Promise { - const nextBlockHeight = await config.l1lcd.move.viewFunction( - '0x1', - 'op_output', - 'next_block_num', - [], - [ - bcs.serialize('address', this.executor.key.accAddress), - bcs.serialize('string', config.L2ID) - ] + logger.info( + `[L2 Challenger] output root matched in output index : ${this.l2OutputIndexToChallenge}` ); - return parseInt(nextBlockHeight) > outputIndex; + this.l2OutputIndexToChallenge += 1; } - async deleteL2Outptut( - entity: ChallengerWithdrawalTxEntity | ChallengerDepositTxEntity, + async deleteOutputProposal( + manager: EntityManager, + outputIndex: number, reason?: string ) { - if (!(await this.isOutputSubmitted(entity.outputIndex))) return; - - const deletedOutput: DeletedOutputEntity = { - outputIndex: entity.outputIndex, - executor: this.executor.key.accAddress, - l2Id: config.L2ID, + const deletedOutput: ChallengerDeletedOutputEntity = { + outputIndex, + bridgeId: this.bridgeId.toString(), reason: reason ?? 'unknown' }; - await this.db.getRepository(DeletedOutputEntity).save(deletedOutput); - - const executeMsg: Msg = new MsgExecute( - this.challenger.key.accAddress, - '0x1', - 'op_output', - 'delete_l2_output', - [], - [ - bcs.serialize('address', this.executor.key.accAddress), - bcs.serialize('string', config.L2ID), - bcs.serialize('u64', entity.outputIndex) - ] - ); - - logger.info( - `output index ${entity.outputIndex} is deleted, reason: ${reason}` - ); + await manager + .getRepository(ChallengerDeletedOutputEntity) + .save(deletedOutput); - // await sendTx(this.challenger, [executeMsg]); - // process.exit(0); // exit process when output is deleted + logger.info(`output index ${outputIndex} is deleted, reason: ${reason}`); } } diff --git a/bots/src/worker/challenger/db.ts b/bots/src/worker/challenger/db.ts index f8f500f8..0b8b23d6 100644 --- a/bots/src/worker/challenger/db.ts +++ b/bots/src/worker/challenger/db.ts @@ -11,12 +11,13 @@ import * as CamelToSnakeNamingStrategy from 'orm/CamelToSnakeNamingStrategy'; const debug = require('debug')('orm'); import { - ChallengerCoinEntity, ChallengerOutputEntity, ChallengerDepositTxEntity, StateEntity, ChallengerWithdrawalTxEntity, - DeletedOutputEntity + ChallengerDeletedOutputEntity, + ChallengerFinalizeDepositTxEntity, + ChallengerFinalizeWithdrawalTxEntity } from 'orm'; const staticOptions = { @@ -24,11 +25,12 @@ const staticOptions = { bigNumberStrings: true, entities: [ StateEntity, + ChallengerFinalizeDepositTxEntity, + ChallengerFinalizeWithdrawalTxEntity, ChallengerWithdrawalTxEntity, ChallengerDepositTxEntity, ChallengerOutputEntity, - ChallengerCoinEntity, - DeletedOutputEntity + ChallengerDeletedOutputEntity ] }; diff --git a/bots/src/worker/challenger/index.ts b/bots/src/worker/challenger/index.ts index 91d41c49..1fa9694d 100644 --- a/bots/src/worker/challenger/index.ts +++ b/bots/src/worker/challenger/index.ts @@ -1,7 +1,7 @@ -import { RPCSocket } from 'lib/rpc'; +import { RPCClient, RPCSocket } from 'lib/rpc'; import { L1Monitor } from './L1Monitor'; import { Monitor } from 'worker/bridgeExecutor/Monitor'; -import { Challenger } from './Challenger'; +import { Challenger } from './challenger'; import { initORM, finalizeORM } from './db'; import { challengerLogger as logger } from 'lib/logger'; import { once } from 'lodash'; @@ -12,13 +12,19 @@ const config = getConfig(); let monitors: (Monitor | Challenger)[]; -async function runBot(isFetch?: boolean): Promise { - const challenger = new Challenger(isFetch ? true : false); - +async function runBot(): Promise { monitors = [ - new L1Monitor(new RPCSocket(config.L1_RPC_URI, 10000, logger), logger), - new L2Monitor(new RPCSocket(config.L2_RPC_URI, 10000, logger), logger), - challenger + new L1Monitor( + new RPCSocket(config.L1_RPC_URI, 10000, logger), + new RPCClient(config.L1_RPC_URI, logger), + logger + ), + new L2Monitor( + new RPCSocket(config.L2_RPC_URI, 10000, logger), + new RPCClient(config.L2_RPC_URI, logger), + logger + ), + new Challenger(logger) ]; try { await Promise.all( @@ -46,9 +52,9 @@ export async function stopChallenger(): Promise { process.exit(0); } -export async function startChallenger(isFetch = false): Promise { +export async function startChallenger(): Promise { await initORM(); - await runBot(isFetch); + await runBot(); const signals = ['SIGHUP', 'SIGINT', 'SIGTERM'] as const; signals.forEach((signal) => process.on(signal, once(stopChallenger))); diff --git a/bots/src/worker/outputSubmitter/db.ts b/bots/src/worker/outputSubmitter/db.ts new file mode 100644 index 00000000..31f1b19a --- /dev/null +++ b/bots/src/worker/outputSubmitter/db.ts @@ -0,0 +1,58 @@ +import 'reflect-metadata'; +import * as Bluebird from 'bluebird'; +import { + ConnectionOptionsReader, + DataSource, + DataSourceOptions +} from 'typeorm'; +import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions'; +import * as CamelToSnakeNamingStrategy from 'orm/CamelToSnakeNamingStrategy'; + +const debug = require('debug')('orm'); + +import { ExecutorOutputEntity } from 'orm'; + +const staticOptions = { + supportBigNumbers: true, + bigNumberStrings: true, + entities: [ExecutorOutputEntity] +}; + +let DB: DataSource[] = []; + +function initConnection(options: DataSourceOptions): Promise { + const pgOpts = options as PostgresConnectionOptions; + debug( + `creating connection default to ${pgOpts.username}@${pgOpts.host}:${ + pgOpts.port || 5432 + }` + ); + + return new DataSource({ + ...options, + ...staticOptions, + namingStrategy: new CamelToSnakeNamingStrategy() as any + }).initialize(); +} + +export async function initORM(): Promise { + const reader = new ConnectionOptionsReader(); + const options = (await reader.all()) as PostgresConnectionOptions[]; + + if (options.length && !options.filter((o) => o.name === 'default').length) { + options[0]['name' as any] = 'default'; + } + + DB = await Bluebird.map(options, (opt) => initConnection(opt)); +} + +export function getDB(): DataSource[] { + if (!DB) { + throw new Error('DB not initialized'); + } + return DB; +} + +export async function finalizeORM(): Promise { + await Promise.all(DB.map((c) => c.destroy())); +} diff --git a/bots/src/worker/outputSubmitter/index.ts b/bots/src/worker/outputSubmitter/index.ts index 167c37bc..eee69bb7 100644 --- a/bots/src/worker/outputSubmitter/index.ts +++ b/bots/src/worker/outputSubmitter/index.ts @@ -1,17 +1,12 @@ import { OutputSubmitter } from './outputSubmitter'; import { outputLogger as logger } from 'lib/logger'; import { once } from 'lodash'; -import axios from 'axios'; -import { getConfig } from 'config'; -import { checkHealth } from 'test/utils/helper'; +import { initORM } from './db'; -const config = getConfig(); let jobs: OutputSubmitter[]; async function runBot(): Promise { - const outputSubmitter = new OutputSubmitter(); - - jobs = [outputSubmitter]; + jobs = [new OutputSubmitter()]; try { await Promise.all( @@ -37,8 +32,7 @@ export async function stopOutput(): Promise { } export async function startOutput(): Promise { - await checkHealth(config.EXECUTOR_URI + '/health'); - + await initORM(); await runBot(); // attach graceful shutdown diff --git a/bots/src/worker/outputSubmitter/outputSubmitter.ts b/bots/src/worker/outputSubmitter/outputSubmitter.ts index 83cf1ddf..9c58b23c 100644 --- a/bots/src/worker/outputSubmitter/outputSubmitter.ts +++ b/bots/src/worker/outputSubmitter/outputSubmitter.ts @@ -1,96 +1,74 @@ -import { BCS, Msg, MsgExecute, Wallet, MnemonicKey } from '@initia/initia.js'; +import { Wallet, MnemonicKey, MsgProposeOutput } from '@initia/initia.js'; import { INTERVAL_OUTPUT } from 'config'; import { ExecutorOutputEntity } from 'orm'; -import { APIRequest } from 'lib/apiRequest'; import { delay } from 'bluebird'; import { outputLogger as logger } from 'lib/logger'; import { ErrorTypes } from 'lib/error'; -import { GetOutputResponse } from 'service'; import { getConfig } from 'config'; import { sendTx } from 'lib/tx'; +import { getLastOutputInfo } from 'lib/query'; +import MonitorHelper from 'worker/bridgeExecutor/MonitorHelper'; +import { DataSource, EntityManager } from 'typeorm'; +import { getDB } from './db'; const config = getConfig(); -const bcs = BCS.getInstance(); export class OutputSubmitter { + private db: DataSource; private submitter: Wallet; - private executor: Wallet; - private apiRequester: APIRequest; - private syncedHeight = 0; + private syncedOutputIndex = 1; + private processedBlockNumber = 1; private isRunning = false; + private bridgeId: number; + helper: MonitorHelper = new MonitorHelper(); async init() { + [this.db] = getDB(); this.submitter = new Wallet( config.l1lcd, new MnemonicKey({ mnemonic: config.OUTPUT_SUBMITTER_MNEMONIC }) ); - this.executor = new Wallet( - config.l1lcd, - new MnemonicKey({ mnemonic: config.EXECUTOR_MNEMONIC }) - ); - - this.apiRequester = new APIRequest(config.EXECUTOR_URI); + this.bridgeId = config.BRIDGE_ID; this.isRunning = true; } - public name(): string { - return 'output_submitter'; - } - - async getNextBlockHeight(): Promise { - const nextBlockHeight = await config.l1lcd.move.viewFunction( - '0x1', - 'op_output', - 'next_block_num', - [], - [ - bcs.serialize('address', this.executor.key.accAddress), - bcs.serialize('string', config.L2ID) - ] - ); - return parseInt(nextBlockHeight); - } - - async proposeL2Output(outputRoot: Buffer, l2BlockHeight: number) { - const executeMsg: Msg = new MsgExecute( - this.submitter.key.accAddress, - '0x1', - 'op_output', - 'propose_l2_output', - [], - [ - bcs.serialize('address', this.executor.key.accAddress), - bcs.serialize('string', config.L2ID), - bcs.serialize('vector', outputRoot, 33), // 33 is the length of output root - bcs.serialize('u64', l2BlockHeight) - ] - ); - await sendTx(this.submitter, [executeMsg]); - } - public async run() { await this.init(); while (this.isRunning) { - try { - const nextBlockHeight = await this.getNextBlockHeight(); - if (nextBlockHeight <= this.syncedHeight) continue; + await this.proccessOutput(); + } + } - const res: GetOutputResponse = - await this.apiRequester.getQuery( - `/output/height/${nextBlockHeight}` - ); - await this.processOutputEntity(res.output, nextBlockHeight); - } catch (err) { - if (err.response?.data.type === ErrorTypes.NOT_FOUND_ERROR) { - logger.warn( - `waiting for next output. not found output from executor height` - ); - await delay(INTERVAL_OUTPUT); - } else { - logger.error(err); - this.stop(); + async proccessOutput() { + try { + await this.db.transaction(async (manager: EntityManager) => { + const lastOutputInfo = await getLastOutputInfo(this.bridgeId); + if (lastOutputInfo) { + this.syncedOutputIndex = lastOutputInfo.output_index + 1; } + + const output = await this.helper.getOutputByIndex( + manager, + ExecutorOutputEntity, + this.syncedOutputIndex + ); + if (!output) return; + + await this.proposeOutput(output); + logger.info( + `successfully submitted! output index: ${this.syncedOutputIndex}, output root: ${output.outputRoot}` + ); + }); + } catch (err) { + if (err.response?.data.type === ErrorTypes.NOT_FOUND_ERROR) { + logger.warn( + `waiting for output index: ${this.syncedOutputIndex}, processed block number: ${this.processedBlockNumber}` + ); + await delay(INTERVAL_OUTPUT); + } else { + logger.error(err); + this.stop(); } } } @@ -99,17 +77,19 @@ export class OutputSubmitter { this.isRunning = false; } - private async processOutputEntity( - outputEntity: ExecutorOutputEntity, - nextBlockHeight: number - ) { - await this.proposeL2Output( - Buffer.from(outputEntity.outputRoot, 'hex'), - nextBlockHeight - ); - this.syncedHeight = nextBlockHeight; - logger.info( - `successfully submitted! height: ${nextBlockHeight}, output root: ${outputEntity.outputRoot}` + private async proposeOutput(outputEntity: ExecutorOutputEntity) { + const msg = new MsgProposeOutput( + this.submitter.key.accAddress, + this.bridgeId, + outputEntity.endBlockNumber, + outputEntity.outputRoot ); + + const { account_number, sequence } = + await this.submitter.accountNumberAndSequence(); + + await sendTx(this.submitter, [msg], account_number, sequence); + + this.processedBlockNumber = outputEntity.endBlockNumber; } } diff --git a/bots/tsconfig.json b/bots/tsconfig.json index 1bcad85b..1027115c 100644 --- a/bots/tsconfig.json +++ b/bots/tsconfig.json @@ -20,6 +20,6 @@ "*": ["*"], } }, - "include": ["src/**/*.ts", "test", "lib", "test/**/*.ts"], + "include": ["src/**/*.ts", "test", "lib", "test/**/*.ts", "src/worker/bridgeExecutor/db.ts"], "exclude": ["node_modules", "apidoc", "dist"] } diff --git a/bots/webpack.config.js b/bots/webpack.config.js deleted file mode 100644 index f0b2c84e..00000000 --- a/bots/webpack.config.js +++ /dev/null @@ -1,60 +0,0 @@ -const path = require('path'); -const webpack = require('webpack'); -const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); -const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') - .BundleAnalyzerPlugin; - -const commonConfig = { - entry: './src/index.ts', - devtool: 'source-map', - module: { - rules: [ - { - test: /\.tsx?$/, - use: 'ts-loader', - exclude: /node_modules/ - } - ] - }, - resolve: { - extensions: ['.tsx', '.ts', '.js'], - plugins: [new TsconfigPathsPlugin()] - }, - plugins: [ - new webpack.IgnorePlugin( - /wordlists\/(french|spanish|italian|korean|chinese_simplified|chinese_traditional|japanese)\.json$/ - ) - ], - node: { - net: 'empty', - tls: 'empty', - fs: 'empty' - } -}; - -const webConfig = { - ...commonConfig, - target: 'web', - output: { - filename: 'bundle.js', - libraryTarget: 'umd', - library: 'Mirror', - path: path.resolve(__dirname, 'dist') - }, - plugins: [ - ...commonConfig.plugins - // new BundleAnalyzerPlugin(), - ] -}; - -const nodeConfig = { - ...commonConfig, - target: 'node', - output: { - path: path.resolve(__dirname, 'dist'), - libraryTarget: 'commonjs', - filename: 'bundle.node.js' - } -}; - -module.exports = [webConfig, nodeConfig]; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..e68ece41 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "OPinit", + "lockfileVersion": 2, + "requires": true, + "packages": {} +}