diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..d2f752ad --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @initia-labs/core diff --git a/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..dd047dcb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE.md @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + diff --git a/.github/ISSUE_TEMPLATE/Release_notes.md b/.github/ISSUE_TEMPLATE/Release_notes.md new file mode 100644 index 00000000..c09f699b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Release_notes.md @@ -0,0 +1,50 @@ +Release Notes +================== + +Initia version 0.45.12 is now available from: + + + +This release includes new features, various bug fixes and performance +improvements, as well as updated translations. + +Please report bugs using the issue tracker at GitHub: + + + +To receive security and update notifications, please join our discord channel. + + +What's Changed +============== +### Initia +- upgrade to cosmos-sdk v0.45.12 by @zkst in #102 + +### RPC and other APIs + +- #25220 rpc: fix incorrect warning for address type p2sh-segwit in createmultisig +- #25237 rpc: Capture UniValue by ref for rpcdoccheck +- #25983 Prevent data race for pathHandlers +- #26275 Fix crash on deriveaddresses when index is 2147483647 (2^31-1) + +### Build system + +- #25201 windeploy: Renewed windows code signing certificate +- #25788 guix: patch NSIS to remove .reloc sections from installer stubs +- #25861 guix: use --build={arch}-guix-linux-gnu in cross toolchain +- #25985 Revert "build: Use Homebrew's sqlite package if it is available" + +### GUI + +- gui#631 Disallow encryption of watchonly wallets +- gui#680 Fixes MacOS 13 segfault by preventing certain notifications + +### Tests + +- #24454 tests: Fix calculation of external input weights + +### Miscellaneous + +- #26321 Adjust .tx/config for new Transifex CLI + +Full Changelog: Initia-node-v1.0.0...Initia-node-v1.0.1 [link] \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..d2c50dff --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve (use this for suspected bugs only, if not sure, open a regular issue below) +title: '' +labels: Bug +assignees: '' +--- + + + + + +**Expected behavior** + + + +**Actual behavior** + + + +**To reproduce** + + + +**System information** + + + + + + + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..2d568518 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: Feature +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** + + +**Describe the solution you'd like** + + +**Describe alternatives you've considered** + + +**Additional context** + diff --git a/.github/ISSUE_TEMPLATE/good_first_issue.md b/.github/ISSUE_TEMPLATE/good_first_issue.md new file mode 100644 index 00000000..0b925062 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/good_first_issue.md @@ -0,0 +1,18 @@ +--- +name: Good first issue +about: '(Regular devs only): Suggest a new good first issue' +title: '' +labels: '' +assignees: '' + +--- + + + + + + + +#### Useful skills: + + diff --git a/.github/ISSUE_TEMPLATE/gui_issue.md b/.github/ISSUE_TEMPLATE/gui_issue.md new file mode 100644 index 00000000..3bac53c3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/gui_issue.md @@ -0,0 +1,8 @@ +--- +name: An issue or feature request related to the GUI +about: Any report, issue or feature request related to the GUI should be reported to initia support team +title: Any report, issue or feature request related to the GUI should be reported to initia support team +labels: GUI +assignees: '' + +--- \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..ee3a2d90 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,32 @@ + + + + + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..c1ee2895 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,64 @@ +name: Test +on: + pull_request: + paths: + - "**.go" + - "**.mv" + - "**.move" + push: + branches: + - main + - "release/*" + paths: + - "**.go" + - "**.mv" + - "**.move" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test-coverage-upload: + name: Run test and upload codecov + env: + # for private repo access + GOPRIVATE: github.com/initia-labs/* + GITHUB_ACCESS_TOKEN: ${{ secrets.GH_READ_TOKEN }} + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v3 + with: + go-version: 1.19 + - name: Install openssl + run: sudo apt-get install libssl-dev + - uses: actions/checkout@v3 + - uses: technote-space/get-diff-action@v4 + with: + PATTERNS: | + **/**.go + go.mod + go.sum + # for private repo access + - run: git config --global url.https://${GITHUB_ACCESS_TOKEN}:x-oauth-basic@github.com/.insteadOf https://github.com/ + - name: test & coverage report creation + run: | + go test ./... -mod=readonly -timeout 12m -race -coverprofile=coverage.txt -covermode=atomic -tags='ledger test_ledger_mock' + if: ${{ env.GIT_DIFF != '' }} + - name: filter out DONTCOVER + run: | + excludelist="$(find ./ -type f -name '*.go' | xargs grep -l 'DONTCOVER')" + excludelist+=" $(find ./ -type f -name '*.pb.go')" + for filename in ${excludelist}; do + filename=$(echo $filename | sed 's/^./github.com\/initia-labs\/initia/g') + echo "Excluding ${filename} from coverage report..." + sed -i.bak "/$(echo $filename | sed 's/\//\\\//g')/d" coverage.txt + done + if: ${{ env.GIT_DIFF != '' }} + - uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage.txt + fail_ci_if_error: true + if: ${{ env.GIT_DIFF != '' }} diff --git a/README.md b/README.md index 5f5b5d7b..dec7a72f 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,66 @@ # OPinit CosmosSDK Modules -This repository provides CosmosSDK modules for OPinit. Any app chain can use these modules to integrate OPinit. +Initia Layer 2 solution with Optimistic Rollup. -## How to Integrate +## Optimistic Rollup Architecture -### Host(L1) Chain +![architecture](./specs/architecture.png) -### Child(L2) Chain +### L1 Components + +#### [Bridge Module](./specs/l1_bridge.md) + +The bridge module triggers a deposit event for the bridge executor, which acts as a relayer between L1 and L2. It has two interfaces: `initialize_deposit` for users and `finalize_withdrawal` for the bridge executor. Both interfaces can be executed by anyone who wants to move the tokens between L1 and L2. + +A deposit does not require any proving or confirmation period, but a withdrawal requires [withdrawal proving](./specs/withdrawal_proving.md) and a finalized output root which contains the withdrawal transaction. + +#### BatchInbox Module + +The batch inbox is the data availability (DA) layer, which can be replaced by other solutions like `Celestia`. The rollup chain can be deterministically derived using the data from the DA layer. This ability to derive the entire rollup chain based on the DA layer is what makes Minitia a rollup. + +To reduce gas costs, the batch inbox only exposes an empty function interface that receives arbitrary bytes as an argument. This trick ensures that L2 data is not a part of the state but instead resides in tx db (= calldata). + +#### [L2OutputOracle Module](./specs/l2_output_oracle.md) + +The L2 output oracle is the component to store the L2 output root for block finalization. The users who withdraw the tokens from L2 to L1 also need to use this output root to prove the withdraw transaction is in the finalized L2 output root. + +The challenger always monitor the oracle output and do challenge when the output is different from the value computed from challenger side. + +### L2 Components + +#### BridgeExecutor + +The bridge executor is the core component in minitia rollup, which is charge of following operations via [L2 Bridge Module](./specs/l2_bridge.md): + +* Finalize L1 deposit transaction to L2. +* Construct withdraw tx storage Merkle Tree. +* Compute L2 output root. +* Provide the withdrawal proofs (Merkle Proofs) to users. + +#### [Minitia](./specs/minitia.md) + +The L2 app chain implementation provides rollup-specific interfaces for a bridge executor. The minitia is a minimized version of the initia app chain, so it does not include staking-related modules such as `staking`, `distribution`, `crisis`, and `evidence`. Instead, it has a new module called `opchild`, which provides a permissioned interface for adding and removing validators, as well as executing [bridge messages](./specs/l2_bridge.md) that can be executed by the bridge executor. + +#### BatchSubmitter + +A background process that submits transaction batches to the `BatchInbox` module of L1. + +#### Challenger + +A challenger is an entity capable of deleting invalid output proposals from the output oracle. It mimics the output root generation process that a bridge executor does to check the validity of the proposed output root on the oracle module. This process confirms that the proposed output root contains a valid app hash, and all withdrawal transactions are properly relayed to L1. + +Additionally, a challenger monitors deposit transactions from L1 to L2 to ensure censorship resistance. If the transactions are not properly relayed to L2 within the timeout (L2 block numbers), the challenger deletes the output root. + +In the initia optimistic rollup spec, a challenger is supposed to run an IBC relayer between L1 and L2 to support instant bridge operation. It is the entity that can monitor an invalid state first, so it can prevent invalid IBC operation by stopping the relayer process. To accomplish this, initia is using [a new ibc middleware](https://github.com/initia-labs/initia/pull/86) on the L1 side to restrict the relayer permission to a specific address for each channel. + +### Dispute Process + +Initia's optimistic rollup uses a simplified version of the dispute mechanism with L1 governance security. This approach is very similar to Cosmos's shared security, but it does not require all validators to run a whole L2 node. Instead, the validators are only required to run an L2 node to decide the valid entity between the `proposer` and `challenger` when a dispute is opened. They do not need to run whole L2 blocks but only need to run a dispute block with the last L2 state on L1. + +The dispute process works as follows: + +1. A `challenger` deletes the invalid output root from the output oracle module. +2. Both a `challenger` and a `proposer` make a governance proposal to update invalid operator addresses: + * The `challenger` makes a governance proposal to change the `proposer` to another address if the `proposer` keeps submitting an invalid output root. + * The `proposer` makes a governance proposal to change the `challenger` to another address if the `challenger` keeps deleting a valid output root. +3. L1 validators make a validity decision by running an L2 node with L2 state and data inputs. diff --git a/bots/.dockerignore b/bots/.dockerignore new file mode 100644 index 00000000..b512c09d --- /dev/null +++ b/bots/.dockerignore @@ -0,0 +1 @@ +node_modules \ 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/README.md b/bots/README.md index 7efaae99..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 pm2.json --only executor - ``` +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 pm2.json --only batch -``` +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 pm2.json --only output -``` +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 pm2.json --only challenger -``` - -## 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/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 new file mode 100644 index 00000000..a6543ade --- /dev/null +++ b/bots/entrypoint.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh + +npm run apidoc + +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/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/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": {} +} diff --git a/specs/architecture.png b/specs/architecture.png new file mode 100644 index 00000000..951e4687 Binary files /dev/null and b/specs/architecture.png differ diff --git a/specs/l1_bridge.md b/specs/l1_bridge.md new file mode 100644 index 00000000..f504cd48 --- /dev/null +++ b/specs/l1_bridge.md @@ -0,0 +1,73 @@ +# L1 Bridge Module + +## Events + +### `TokenRegisteredEvent` + +The event is emitted when a new token support is added to bridge contract. + +* In v1 spec, the bridge executor should add a new token support manually. + +```rust +/// Emitted when deposit store is registered. +struct TokenRegisteredEvent has drop, store { + l2_id: String, + l1_token: String, + l2_token: vector, // sha3_256(type_name(`L2ID`) || type_name(`l1_token`)) +} +``` + +### `TokenBridgeInitiatedEvent` + +The event is emitted when a user executes `deposit_token` function to move a token from L1 to L2. + +* The bridge module maintain `sequence` number to give an unique identifier for each relaying operation. +* In v1, `l2_id` + `l1_sequence` is the unique identifier. + +```rust +/// Emitted when a token bridge is initiated to the l2 chain. +struct TokenBridgeInitiatedEvent has drop, store { + from: address, // l1 address + to: address, // l2 address + l2_id: String, + l1_token: String, + l2_token: vector, + amount: u64, + l1_sequence: u64, +} +``` + +### `TokenBridgeFinalizedEvent` + +The event is emitted when a withdrawal transaction is proved and finalized. + +* In v1, `sha3(bcs(l2_sequence) + bcs(from) + bcs(to) + bcs(amount) + bytes(l1_token))` is the unique identifier for each withdrawal operation. + +```rust +/// Emitted when a token bridge is finalized on l1 chain. +struct TokenBridgeFinalizedEvent has drop, store { + from: address, // l2 address + to: address, // l1 address + l2_id: String, + l1_token: String, + l2_token: vector, + amount: u64, + l2_sequence: u64, // the sequence number which is assigned from the l2 bridge +} +``` + +## Operations + +### Register Token + +This function is for the bridge executor, who controls the bridge relaying operations. In version 1, only the bridge executor can add support for a new token type. After registration, the `TokenRegisteredEvent` event is emitted to deploy a new coin type module on L2. + +The bridge executor should monitor the `TokenRegisteredEvent` and deploy a new coin type module on L2. They should also execute the L2 bridge's `/minitia.rollup.v1.MsgCreateToken` function for initialization. + +### Initiate Token Bridge + +This function enables a user to transfer their asset from L1 to L2. The deposited token will be locked in the bridge's `DepositStore` and can only be released using the `withdraw` operation in L2. When executed, this operation emits a `TokenBridgeInitiatedEvent`. A bridge executor should subscribe to this event and transfer the token to L2 by executing the `finalize_token_bridge` function in the L2 bridge. + +### Finalize Token Bridge + +This function is used to prove and finalize the withdrawal transaction from L2. The proving process is described [here](https://www.notion.so/Withdrawal-Proving-a49f7c26467044489731048f68ed584b?pvs=21). Once the proving is complete, the deposited tokens are withdrawn to the recipient address, and the `TokenBridgeFinalizedEvent` event is emitted. To prevent duplicate withdrawal attempts, the bridge uses a unique identifier calculated as `sha3(bcs(l2_sequence) + bcs(from) + bcs(to) + bcs(amount) + bytes(l1_token))`. diff --git a/specs/l2_bridge.md b/specs/l2_bridge.md new file mode 100644 index 00000000..571342f5 --- /dev/null +++ b/specs/l2_bridge.md @@ -0,0 +1,52 @@ +# L2 Bridge + +## Events + +### `TokenBridgeFinalizedEvent` + +The event is emitted when a executor finalized the token transfer from the L1 to L2. + +```rust +// Emitted when a token bridge is finalized on l2 chain. +struct TokenBridgeFinalizedEvent has drop, store { + from: address, // l1 address + to: address, // l2 address + l2_token: vector, + amount: u64, + l1_sequence: u64, // the sequence number which is assigned from the l1 bridge +} +``` + +### `TokenBridgeInitiatedEvent` + +The event is emitted when a user executes `withdraw_token` function to move token from L2 to L1. + +- The bridge module maintain `sequence` number to give unique identifier to each relay operation. +- In v1, `l2_sequence` is the unique identifier. + +```rust +// Emitted when a token bridge is initiated to the l1 chain. +struct TokenBridgeInitiatedEvent has drop, store { + from: address, // l2 address + to: address, // l1 address + l2_token: vector, + amount: u64, + l2_sequence: u64, // the operation sequence number +} +``` + +## Operations + +### Register Token + +This function allows the block executor to initialize a new token type with registration on the bridge module. The name of the newly deployed module should follow the L1 bridge contract’s event message `l2_token`, such as `01::l2_${l2_token}::Coin`. + +### Finalize Token Bridge + +This function finalizes the token transfer from L1 to L2. Only the block executor is allowed to execute this operation. + +### Initiate Token Bridge + +This function initiates the token bridge from L2 to L1. Users can execute `withdraw_token` to send tokens from L2 to L1. This operation emits the `TokenBridgeInitiatedEvent` with an `l2_sequence` number to prevent duplicate execution on L1. + +The block executor should monitor this event to build withdraw storage for withdrawal proofs. diff --git a/specs/l2_output_oracle.md b/specs/l2_output_oracle.md new file mode 100644 index 00000000..332a232f --- /dev/null +++ b/specs/l2_output_oracle.md @@ -0,0 +1,32 @@ +# L2 Output Oracle + +In version 1, output oracle maintain `proposer` and `challenger` addresses on its config store. The `proposer` can submit the `output_proposal` and the `challenger` can delete the output when the proposed output state is wrong. + +The first version of the implementation does not include a dispute system, but uses permissioned propose and challenge mechanisms. In version 2, anyone can propose the output with a certain amount of `stake`, and disputes will be resolved based on the governance of L1. + +## Operations + +### Propose L2 Output + +L2 output oracle receives `output_root` with L2 block number to check the checkpoint of L2. The checkpoints are the multiple of `submission_interval`. A proposer must submit the `output_root` at the every checkpoints. + +The followings are the components of `output_root`. + +- `version`: the version of output root +- `state_root`: l2 state root +- `storage_root`: withdrawal storage root +- `latest_block_hash`: l2 latest block hash + +To build the `output_root`, concatenate all the components in sequence and apply `sha3_256`. + +### Delete L2 Output + +A challenger can delete the output without dispute in version 1 with output index. + +### Update Proposer + +The operation is to update proposer to another address when a proposer keeps submitting a invalid output root. The operation is supposed to be executed by `0x1` via L1 governance. + +### Update Challenger + +The operation is to update challenger to another address when a challenger keeps deleting a valid output root. The operation is supposed to be executed by `0x1` via L1 governance. diff --git a/specs/minitia.md b/specs/minitia.md new file mode 100644 index 00000000..5c45358d --- /dev/null +++ b/specs/minitia.md @@ -0,0 +1,237 @@ +# Minitia + +## Messages + +There are three categories of message in `x/rollup`` module. + +* Bridge Executor messages + * [`MsgCreateToken`](#msgcreatetoken) + * [`MsgDeposit`](#msgdeposit) +* Validator messages + * [`MsgExecuteMessages`](#msgexecutemessages) + * [`MsgExecuteLegacyContents`](#msgexecutelegacycontents) +* Authority messages + * [`MsgAddValidator`](#msgaddvalidator) + * [`MsgRemoveValidator`](#msgremovevalidator) + * [`MsgUpdateParams`](#msgupdateparams) + * [`MsgWhitelist`](#msgwhitelist) + * [`MsgSpendFeePool`](#msgspendfeepool) + +### `MsgCreateToken` + +The message is for a bridge executor to publish a new coin struct tag `0x1::native_${denom}::Coin` and initialize a new coin with that struct tag. + +```proto +// MsgCreateToken is the message to create a new token from L1 +message MsgCreateToken { + option (cosmos.msg.v1.signer) = "sender"; + option (amino.name) = "rollup/MsgCreateToken"; + + // the sender address + string sender = 1 [ + (gogoproto.moretags) = "yaml:\"sender\"", + (cosmos_proto.scalar) = "cosmos.AddressString" + ]; + + // denom is the denom of the token to create. + string denom = 2; + string name = 3; + string symbol = 4; + int64 decimals = 5; +} +``` + +### `MsgDeposit` + +The message is for a bridge executor to finalize a deposit request from L1. The message handler internally executes [`finalize_token_bridge`](./l2_bridge.md#finalize-token-bridge) of `l2_bridge`. + +```proto +// MsgDeposit is the message to submit deposit funds from upper layer +message MsgDeposit { + option (cosmos.msg.v1.signer) = "sender"; + option (amino.name) = "rollup/MsgDeposit"; + + // the sender address + string sender = 1 [ + (gogoproto.moretags) = "yaml:\"sender\"", + (cosmos_proto.scalar) = "cosmos.AddressString" + ]; + + // from is l1 sender address + string from = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + + // to is l2 recipient address + string to = 3 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + + // amount is the coin amount to deposit. + cosmos.base.v1beta1.Coin amount = 4 [ + (gogoproto.moretags) = "yaml:\"amount\"", + (gogoproto.nullable) = false, + (amino.dont_omitempty) = true + ]; + + // sequence is the sequence number of l1 bridge + uint64 sequence = 5; +} +``` + +### `MsgExecuteMessages` + +The message is to execute authority messages with validator permission like `x/gov` module of cosmos-sdk. Any validator can execute the message with various authority messages. + +```proto +// MsgExecuteMessages is the message to execute the given +// authority messages with validator permission. +message MsgExecuteMessages { + option (cosmos.msg.v1.signer) = "sender"; + option (amino.name) = "rollup/MsgExecuteMessages"; + + // Sender is the that actor that signed the messages + string sender = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + + // messages are the arbitrary messages to be executed. + repeated google.protobuf.Any messages = 2; +} +``` + +### `MsgExecuteLegacyContents` + +The message is also copied from `x/gov` module of cosmos-sdk to support legacy param update of ibc modules. The execution permission is given to validators. + +```proto + +// MsgExecuteLegacyContents is the message to execute legacy +// (gov) contents with validator permission. +message MsgExecuteLegacyContents { + option (cosmos.msg.v1.signer) = "sender"; + option (amino.name) = "rollup/MsgExecuteLegacyContents"; + + // Sender is the that actor that signed the messages + string sender = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + + // contents are the arbitrary legacy (gov) contents to be executed. + repeated google.protobuf.Any contents = 2; +} +``` + +### `MsgAddValidator` + +The message is to add a new validator to the comet-bft validator set. The execution permission is given to authority, which is `rollup` module account. + +```proto +// MsgAddValidator defines a SDK message for adding a new validator. +message MsgAddValidator { + option (cosmos.msg.v1.signer) = "authority"; + option (amino.name) = "rollup/MsgAddValidator"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + // authority is the address that controls the module + // (defaults to x/rollup unless overwritten). + string authority = 1 [ + (gogoproto.moretags) = "yaml:\"authority\"", + (cosmos_proto.scalar) = "cosmos.AddressString" + ]; + + string moniker = 2; + string validator_address = 3 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + google.protobuf.Any pubkey = 4 [(cosmos_proto.accepts_interface) = "cosmos.crypto.PubKey"]; +} +``` + +### `MsgRemoveValidator` + +The message is to remove a validator from the comet-bft validator set. The execution permission is given to authority, which is `rollup` module account. + +```proto +// MsgAddValidator is the message to remove a validator from designated list +message MsgRemoveValidator { + option (cosmos.msg.v1.signer) = "authority"; + option (amino.name) = "rollup/MsgRemoveValidator"; + + // authority is the address that controls the module + // (defaults to x/rollup unless overwritten). + string authority = 1 [ + (gogoproto.moretags) = "yaml:\"authority\"", + (cosmos_proto.scalar) = "cosmos.AddressString" + ]; + + // validator is the validator to remove. + string validator_address = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; +} +``` + +### `MsgUpdateParams` + +The message is to update the rollup module params. The execution permission is given to authority, which is `rollup` module account. + +```proto +// MsgUpdateParams is the message to update legacy parameters +message MsgUpdateParams { + option (cosmos.msg.v1.signer) = "authority"; + option (amino.name) = "rollup/MsgUpdateParams"; + + // authority is the address that controls the module + // (defaults to x/rollup unless overwritten). + string authority = 1 [ + (gogoproto.moretags) = "yaml:\"authority\"", + (cosmos_proto.scalar) = "cosmos.AddressString" + ]; + + // params are the arbitrary parameters to be updated. + Params params = 2; +} +``` + +### `MsgWhitelist` + +The message is to add a coin type to whitelist for auto register. The execution permission is given to authority, which is `rollup` module account. + +```proto +// whitelist a coin type to enable auto coin module register. +message MsgWhitelist { + option (cosmos.msg.v1.signer) = "authority"; + option (amino.name) = "rollup/MsgWhitelist"; + + // authority is the address that controls the module + // (defaults to x/rollup unless overwritten). + string authority = 1 [ + (gogoproto.moretags) = "yaml:\"authority\"", + (cosmos_proto.scalar) = "cosmos.AddressString" + ]; + + // coin_type is the struct tag to whitelist. + string coin_type = 2; +} +``` + +### `MsgSpendFeePool` + +The message is to spend collected fee. The execution permission is given to authority, which is `rollup` module account. + +```proto +// MsgSpendFeePool is the message to withdraw collected fees from the module account to the recipient address. +message MsgSpendFeePool { + option (cosmos.msg.v1.signer) = "authority"; + option (amino.name) = "rollup/MsgSpendFeePool"; + + // authority is the address that controls the module + // (defaults to x/rollup unless overwritten). + string authority = 1 [ + (gogoproto.moretags) = "yaml:\"authority\"", + (cosmos_proto.scalar) = "cosmos.AddressString" + ]; + + // recipient is address to receive the coins. + string recipient = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + + // the coin amount to spend. + repeated cosmos.base.v1beta1.Coin amount = 3 [ + (gogoproto.moretags) = "yaml:\"amount\"", + (gogoproto.nullable) = false, + (amino.dont_omitempty) = true, + (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins" + ]; +} +``` diff --git a/specs/withdrawal_proving.md b/specs/withdrawal_proving.md new file mode 100644 index 00000000..ac8bad79 --- /dev/null +++ b/specs/withdrawal_proving.md @@ -0,0 +1,64 @@ +# Withdrawal Proving + +`ominitia` defines `submission_interval`, which is the L2 block number at which checkpoints must be submitted. At each `submission_interval`, the bridge executor should build the withdraw storage, which is the Merkle Tree for the withdrawal verification process on L1. + +`ominitia` uses a sorted Merkle Tree to reduce verifying cost, and each tree node is referred to as a `withdrawal_hash`. + +The following are the components of `withdrawal_hash`: + +- `sequence`: The L2 bridge sequence assigned to each withdrawal operation. +- `sender`: The address from which the withdrawal operation is made. +- `receiver`: The address to which the withdrawal operation is made. +- `amount`: The token amount of the withdrawal operation. +- `coin_type`: The L1 token struct tag. + +To build the `withdrawal_hash`, concatenate all the components and apply `sha3_256` after serializing the values with `bcs`, except for `coin_type`, because the `coin_type` is already a string that can be converted to bytes. + +```rust +fun verify( + withdrawal_proofs: vector>, + sequence: u64, + sender: address, + receiver: address, + amount: u64, + coin_type: String, +): bool { + let withdrawal_hash = { + let withdraw_tx_data = vector::empty(); + vector::append(&mut withdraw_tx_data, bcs::to_bytes(&sequence)); + vector::append(&mut withdraw_tx_data, bcs::to_bytes(&sender)); + vector::append(&mut withdraw_tx_data, bcs::to_bytes(&receiver)); + vector::append(&mut withdraw_tx_data, bcs::to_bytes(&amount)); + vector::append(&mut withdraw_tx_data, *string::bytes(&type_info::type_name())); + + sha3_256(withdraw_tx_data) + }; + + let i = 0; + let len = vector::length(&withdrawal_proofs); + let root_seed = withdrawal_hash; + while (i < len) { + let proof = vector::borrow(&withdrawal_proofs, i); + let cmp = bytes_cmp(&root_seed, proof); + root_seed = if (cmp == 2 /* less */) { + let tmp = vector::empty(); + vector::append(&mut tmp, root_seed); + vector::append(&mut tmp, *proof); + + sha3_256(tmp) + } else /* greator or equals */ { + let tmp = vector::empty(); + vector::append(&mut tmp, *proof); + vector::append(&mut tmp, root_seed); + + sha3_256(tmp) + }; + i = i + 1; + }; + + let root_hash = root_seed; + assert!(storage_root == root_hash, error::invalid_argument(EINVALID_STORAGE_ROOT_PROOFS)); +} +``` + +The example implementation of building the Merkle Tree can be found [here](https://github.com/initia-labs/op-bridge-executor). diff --git a/x/opchild/ante/fee.go b/x/opchild/ante/fee.go index ee87b57c..e3ca34b6 100644 --- a/x/opchild/ante/fee.go +++ b/x/opchild/ante/fee.go @@ -1,6 +1,7 @@ package ante import ( + "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -29,7 +30,7 @@ func NewMempoolFeeChecker( func (mfd MempoolFeeChecker) CheckTxFeeWithMinGasPrices(ctx sdk.Context, tx sdk.Tx) (sdk.Coins, int64, error) { feeTx, ok := tx.(sdk.FeeTx) if !ok { - return nil, 0, sdkerrors.Wrap(sdkerrors.ErrTxDecode, "Tx must be a FeeTx") + return nil, 0, errors.Wrap(sdkerrors.ErrTxDecode, "Tx must be a FeeTx") } feeCoins := feeTx.GetFee() @@ -46,7 +47,7 @@ func (mfd MempoolFeeChecker) CheckTxFeeWithMinGasPrices(ctx sdk.Context, tx sdk. requiredFees := computeRequiredFees(gas, minGasPrices) if !feeCoins.IsAnyGTE(requiredFees) { - return nil, 0, sdkerrors.Wrapf(sdkerrors.ErrInsufficientFee, "insufficient fees; got: %s required: %s", feeCoins, requiredFees) + return nil, 0, errors.Wrapf(sdkerrors.ErrInsufficientFee, "insufficient fees; got: %s required: %s", feeCoins, requiredFees) } } } diff --git a/x/opchild/client/cli/query.go b/x/opchild/client/cli/query.go index 0741ec38..6cd2c9f4 100644 --- a/x/opchild/client/cli/query.go +++ b/x/opchild/client/cli/query.go @@ -15,22 +15,21 @@ import ( // GetQueryCmd returns the cli query commands for this module func GetQueryCmd() *cobra.Command { - rollupQueryCmd := &cobra.Command{ + opchildQueryCmd := &cobra.Command{ Use: types.ModuleName, - Short: "Querying commands for the rollup module", + Short: "Querying commands for the opchild module", DisableFlagParsing: true, SuggestionsMinimumDistance: 2, RunE: client.ValidateCmd, } - rollupQueryCmd.AddCommand( + opchildQueryCmd.AddCommand( GetCmdQueryValidator(), GetCmdQueryValidators(), - //GetCmdQueryHistoricalInfo(), TODO: remove it if not needed GetCmdQueryParams(), ) - return rollupQueryCmd + return opchildQueryCmd } // GetCmdQueryValidator implements the validator query command. @@ -44,7 +43,7 @@ func GetCmdQueryValidator() *cobra.Command { fmt.Sprintf(`Query details about an individual validator. Example: -$ %s query rollup validator %s1gghjut3ccd8ay0zduzj64hwre2fxs9ldmqhffj +$ %s query opchild validator %s1gghjut3ccd8ay0zduzj64hwre2fxs9ldmqhffj `, version.AppName, bech32PrefixValAddr, ), @@ -87,7 +86,7 @@ func GetCmdQueryValidators() *cobra.Command { fmt.Sprintf(`Query details about all validators on a network. Example: -$ %s query rollup validators +$ %s query opchild validators `, version.AppName, ), @@ -121,61 +120,17 @@ $ %s query rollup validators return cmd } -/* TODO: remove it if not needed -// GetCmdQueryHistoricalInfo implements the historical info query command -func GetCmdQueryHistoricalInfo() *cobra.Command { - cmd := &cobra.Command{ - Use: "historical-info [height]", - Args: cobra.ExactArgs(1), - Short: "Query historical info at given height", - Long: strings.TrimSpace( - fmt.Sprintf(`Query historical info at given height. - -Example: -$ %s query rollup historical-info 5 -`, - version.AppName, - ), - ), - RunE: func(cmd *cobra.Command, args []string) error { - clientCtx, err := client.GetClientQueryContext(cmd) - if err != nil { - return err - } - queryClient := types.NewQueryClient(clientCtx) - - height, err := strconv.ParseInt(args[0], 10, 64) - if err != nil || height < 0 { - return fmt.Errorf("height argument provided must be a non-negative-integer: %v", err) - } - - params := &types.QueryHistoricalInfoRequest{Height: height} - res, err := queryClient.HistoricalInfo(cmd.Context(), params) - if err != nil { - return err - } - - return clientCtx.PrintProto(res.Hist) - }, - } - - flags.AddQueryFlagsToCmd(cmd) - - return cmd -} -*/ - // GetCmdQueryParams implements the params query command. func GetCmdQueryParams() *cobra.Command { cmd := &cobra.Command{ Use: "params", Args: cobra.NoArgs, - Short: "Query the current rollup parameters information", + Short: "Query the current opchild parameters information", Long: strings.TrimSpace( - fmt.Sprintf(`Query values set as rollup parameters. + fmt.Sprintf(`Query values set as opchild parameters. Example: -$ %s query rollup params +$ %s query opchild params `, version.AppName, ), diff --git a/x/opchild/client/cli/tx.go b/x/opchild/client/cli/tx.go index 0e916b04..6c6049b0 100644 --- a/x/opchild/client/cli/tx.go +++ b/x/opchild/client/cli/tx.go @@ -18,15 +18,15 @@ import ( // GetTxCmd returns a root CLI command handler for all x/opchild transaction commands. func GetTxCmd() *cobra.Command { - rollupTxCmd := &cobra.Command{ + opchildTxCmd := &cobra.Command{ Use: types.ModuleName, - Short: "Rollup transaction subcommands", + Short: "OPChild transaction subcommands", DisableFlagParsing: true, SuggestionsMinimumDistance: 2, RunE: client.ValidateCmd, } - rollupTxCmd.AddCommand( + opchildTxCmd.AddCommand( NewExecuteMessagesCmd(), NewDepositCmd(), NewWithdrawCmd(), @@ -35,7 +35,7 @@ func GetTxCmd() *cobra.Command { NewLegacyContentSubmitUpgradeCmd(), ) - return rollupTxCmd + return opchildTxCmd } // NewDepositCmd returns a CLI command handler for the transaction sending a deposit to an user account. diff --git a/x/opchild/keeper/genesis.go b/x/opchild/keeper/genesis.go index ec02497c..a8536d5d 100644 --- a/x/opchild/keeper/genesis.go +++ b/x/opchild/keeper/genesis.go @@ -30,7 +30,9 @@ func (k Keeper) InitGenesis(ctx sdk.Context, data *types.GenesisState) (res []ab k.SetValidator(ctx, validator) // Manually set indices for the first time - k.SetValidatorByConsAddr(ctx, validator) + if err := k.SetValidatorByConsAddr(ctx, validator); err != nil { + panic(err) + } } // don't need to run Tendermint updates if we exported diff --git a/x/opchild/keeper/genesis_test.go b/x/opchild/keeper/genesis_test.go index 0bebf601..376a279c 100644 --- a/x/opchild/keeper/genesis_test.go +++ b/x/opchild/keeper/genesis_test.go @@ -1,3 +1,29 @@ package keeper_test // TODO - implement test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_GenesisImportExport(t *testing.T) { + ctx, input := createDefaultTestInput(t) + input.OPChildKeeper.SetNextL2Sequence(ctx, 1) + + seq := input.OPChildKeeper.IncreaseNextL2Sequence(ctx) + require.Equal(t, uint64(1), seq) + seq = input.OPChildKeeper.IncreaseNextL2Sequence(ctx) + require.Equal(t, uint64(2), seq) + + input.OPChildKeeper.RecordFinalizedL1Sequence(ctx, 1) + input.OPChildKeeper.RecordFinalizedL1Sequence(ctx, 2) + + genState := input.OPChildKeeper.ExportGenesis(ctx) + input.OPChildKeeper.InitGenesis(ctx, genState) + _genState := input.OPChildKeeper.ExportGenesis(ctx) + require.Equal(t, genState, _genState) + fmt.Printf("genState: %v\n", genState) +} diff --git a/x/opchild/keeper/historical_info.go b/x/opchild/keeper/historical_info.go index f95152fd..aadffe94 100644 --- a/x/opchild/keeper/historical_info.go +++ b/x/opchild/keeper/historical_info.go @@ -1,43 +1,28 @@ package keeper import ( - "encoding/binary" - "cosmossdk.io/math" - "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" cosmostypes "github.com/cosmos/cosmos-sdk/x/staking/types" - - "github.com/initia-labs/OPinit/x/opchild/types" ) -// GetHistoricalInfo fetch height historical info that is equal or lower than the given height. +// GetHistoricalInfo gets the historical info at a given height func (k Keeper) GetHistoricalInfo(ctx sdk.Context, height int64) (cosmostypes.HistoricalInfo, bool) { store := ctx.KVStore(k.storeKey) + key := cosmostypes.GetHistoricalInfoKey(height) - // increase height by 1 because iterator is exclusive. - height += 1 - - prefixStore := prefix.NewStore(store, types.HistoricalInfoKey) - - end := make([]byte, 8) - binary.BigEndian.PutUint64(end, uint64(height)) - - iterator := prefixStore.ReverseIterator(nil, end) - defer iterator.Close() - - if !iterator.Valid() { + value := store.Get(key) + if value == nil { return cosmostypes.HistoricalInfo{}, false } - value := iterator.Value() return cosmostypes.MustUnmarshalHistoricalInfo(k.cdc, value), true } // SetHistoricalInfo sets the historical info at a given height func (k Keeper) SetHistoricalInfo(ctx sdk.Context, height int64, hi *cosmostypes.HistoricalInfo) { store := ctx.KVStore(k.storeKey) - key := types.GetHistoricalInfoKey(uint64(height)) + key := cosmostypes.GetHistoricalInfoKey(height) value := k.cdc.MustMarshal(hi) store.Set(key, value) } @@ -45,7 +30,7 @@ func (k Keeper) SetHistoricalInfo(ctx sdk.Context, height int64, hi *cosmostypes // DeleteHistoricalInfo deletes the historical info at a given height func (k Keeper) DeleteHistoricalInfo(ctx sdk.Context, height int64) { store := ctx.KVStore(k.storeKey) - key := types.GetHistoricalInfoKey(uint64(height)) + key := cosmostypes.GetHistoricalInfoKey(height) store.Delete(key) } @@ -53,7 +38,7 @@ func (k Keeper) DeleteHistoricalInfo(ctx sdk.Context, height int64) { // TrackHistoricalInfo saves the latest historical-info and deletes the oldest // heights that are below pruning height func (k Keeper) TrackHistoricalInfo(ctx sdk.Context) { - entryNum := uint64(k.HistoricalEntries(ctx)) + entryNum := k.HistoricalEntries(ctx) // Prune store to ensure we only have parameter-defined historical entries. // In most cases, this will involve removing a single historical entry. @@ -62,31 +47,12 @@ func (k Keeper) TrackHistoricalInfo(ctx sdk.Context) { // Since the entries to be deleted are always in a continuous range, we can iterate // over the historical entries starting from the most recent version to be pruned // and then return at the first empty entry. - height := uint64(ctx.BlockHeight()) - if height > entryNum { - store := ctx.KVStore(k.storeKey) - prefixStore := prefix.NewStore(store, types.HistoricalInfoKey) - - end := make([]byte, 8) - binary.BigEndian.PutUint64(end, height-entryNum) - - iterator := prefixStore.ReverseIterator(nil, end) - defer iterator.Close() - - // our historical info does not exist for every block to allow - // empty block, so it is possible when ibc request deleted block - // historical info. Then opchild module returns height historical - // historical info that is lower than the given height. - // - // Whenever we delete historical info, we have to leave first info - // for safety. - if iterator.Valid() { - iterator.Next() - } - - for ; iterator.Valid(); iterator.Next() { - key := iterator.Key() - prefixStore.Delete(key) + for i := ctx.BlockHeight() - int64(entryNum); i >= 0; i-- { + _, found := k.GetHistoricalInfo(ctx, i) + if found { + k.DeleteHistoricalInfo(ctx, i) + } else { + break } } diff --git a/x/opchild/keeper/historical_info_test.go b/x/opchild/keeper/historical_info_test.go index 585aaa31..62659b5a 100644 --- a/x/opchild/keeper/historical_info_test.go +++ b/x/opchild/keeper/historical_info_test.go @@ -3,55 +3,36 @@ package keeper_test import ( "testing" - tmtypes "github.com/cometbft/cometbft/proto/tendermint/types" - cosmostypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/stretchr/testify/require" + + cosmostypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) func Test_HistoricalInfo(t *testing.T) { ctx, input := createDefaultTestInput(t) - historicalInfo := cosmostypes.HistoricalInfo{ - Header: tmtypes.Header{ - Height: 100, - ChainID: "testnet", - }, - Valset: []cosmostypes.Validator{{ - OperatorAddress: "hihi", - }}, - } - - input.OPChildKeeper.SetHistoricalInfo(ctx, 100, &historicalInfo) + params := input.OPChildKeeper.GetParams(ctx) + params.HistoricalEntries = 2 + input.OPChildKeeper.SetParams(ctx, params) - _historicalInfo, found := input.OPChildKeeper.GetHistoricalInfo(ctx, 101) - require.True(t, found) - require.Equal(t, historicalInfo.Header.Height, _historicalInfo.Header.Height) - require.Equal(t, historicalInfo.Header.ChainID, _historicalInfo.Header.ChainID) - require.Equal(t, historicalInfo.Valset[0].OperatorAddress, _historicalInfo.Valset[0].OperatorAddress) + input.OPChildKeeper.TrackHistoricalInfo(ctx.WithBlockHeight(1)) + input.OPChildKeeper.TrackHistoricalInfo(ctx.WithBlockHeight(2)) + input.OPChildKeeper.TrackHistoricalInfo(ctx.WithBlockHeight(3)) - _, found = input.OPChildKeeper.GetHistoricalInfo(ctx, 99) + _, found := input.OPChildKeeper.GetHistoricalInfo(ctx, 1) require.False(t, found) -} -func Test_TrackHistoricalInfo(t *testing.T) { - ctx, input := createDefaultTestInput(t) - emptyHistoricalInfo := cosmostypes.HistoricalInfo{} - input.OPChildKeeper.SetHistoricalInfo(ctx, 100, &emptyHistoricalInfo) - input.OPChildKeeper.SetHistoricalInfo(ctx, 101, &emptyHistoricalInfo) - input.OPChildKeeper.SetHistoricalInfo(ctx, 102, &emptyHistoricalInfo) - input.OPChildKeeper.SetHistoricalInfo(ctx, 103, &emptyHistoricalInfo) - - ctx = ctx.WithBlockHeight(104) - params := input.OPChildKeeper.GetParams(ctx) - params.HistoricalEntries = 1 - input.OPChildKeeper.SetParams(ctx, params) - - input.OPChildKeeper.TrackHistoricalInfo(ctx) + historicalInfo, found := input.OPChildKeeper.GetHistoricalInfo(ctx, 2) + require.True(t, found) + require.Equal(t, cosmostypes.HistoricalInfo{ + Header: ctx.WithBlockHeight(2).BlockHeader(), + Valset: nil, + }, historicalInfo) - _, found := input.OPChildKeeper.GetHistoricalInfo(ctx, 102) + historicalInfo, found = input.OPChildKeeper.GetHistoricalInfo(ctx, 3) require.True(t, found) - _, found = input.OPChildKeeper.GetHistoricalInfo(ctx, 101) - require.False(t, found) - _, found = input.OPChildKeeper.GetHistoricalInfo(ctx, 100) - require.False(t, found) + require.Equal(t, cosmostypes.HistoricalInfo{ + Header: ctx.WithBlockHeight(3).BlockHeader(), + Valset: nil, + }, historicalInfo) } diff --git a/x/opchild/keeper/msg_server.go b/x/opchild/keeper/msg_server.go index 0f51b51d..b0c614fe 100644 --- a/x/opchild/keeper/msg_server.go +++ b/x/opchild/keeper/msg_server.go @@ -180,7 +180,9 @@ func (ms MsgServer) AddValidator(context context.Context, req *types.MsgAddValid } ms.SetValidator(ctx, validator) - ms.SetValidatorByConsAddr(ctx, validator) + if err = ms.SetValidatorByConsAddr(ctx, validator); err != nil { + return nil, err + } ctx.EventManager().EmitEvents(sdk.Events{ sdk.NewEvent( @@ -206,7 +208,7 @@ func (ms MsgServer) RemoveValidator(context context.Context, req *types.MsgRemov val, found := ms.Keeper.GetValidator(ctx, valAddr) if !found { - return nil, sdkerrors.Wrap(types.ErrNoValidatorFound, val.OperatorAddress) + return nil, errors.Wrap(types.ErrNoValidatorFound, val.OperatorAddress) } val.ConsPower = 0 diff --git a/x/opchild/keeper/params_test.go b/x/opchild/keeper/params_test.go new file mode 100644 index 00000000..792180a4 --- /dev/null +++ b/x/opchild/keeper/params_test.go @@ -0,0 +1,20 @@ +package keeper_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +func Test_Params(t *testing.T) { + ctx, input := createDefaultTestInput(t) + + params := input.OPChildKeeper.GetParams(ctx) + params.MinGasPrices = sdk.NewDecCoins() + + input.OPChildKeeper.SetParams(ctx, params) + + require.True(t, input.OPChildKeeper.MinGasPrices(ctx).IsZero()) + require.Equal(t, addrs[0], input.OPChildKeeper.BridgeExecutor(ctx)) +} diff --git a/x/opchild/keeper/querier_test.go b/x/opchild/keeper/querier_test.go new file mode 100644 index 00000000..86dd1807 --- /dev/null +++ b/x/opchild/keeper/querier_test.go @@ -0,0 +1,58 @@ +package keeper_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + testutilsims "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/initia-labs/OPinit/x/opchild/keeper" + "github.com/initia-labs/OPinit/x/opchild/types" +) + +func Test_QueryValidator(t *testing.T) { + ctx, input := createDefaultTestInput(t) + + valPubKeys := testutilsims.CreateTestPubKeys(1) + val, err := types.NewValidator(valAddrs[0], valPubKeys[0], "validator1") + require.NoError(t, err) + + input.OPChildKeeper.SetValidator(ctx, val) + q := keeper.NewQuerier(input.OPChildKeeper) + + res, err := q.Validator(ctx, &types.QueryValidatorRequest{ValidatorAddr: val.OperatorAddress}) + require.NoError(t, err) + require.Equal(t, types.QueryValidatorResponse{Validator: val}, *res) +} + +func Test_QueryValidators(t *testing.T) { + ctx, input := createDefaultTestInput(t) + + valPubKeys := testutilsims.CreateTestPubKeys(2) + val1, err := types.NewValidator(valAddrs[0], valPubKeys[0], "validator1") + require.NoError(t, err) + + val2, err := types.NewValidator(valAddrs[1], valPubKeys[1], "validator2") + require.NoError(t, err) + input.OPChildKeeper.SetValidator(ctx, val1) + input.OPChildKeeper.SetValidator(ctx, val2) + q := keeper.NewQuerier(input.OPChildKeeper) + + res, err := q.Validators(ctx, &types.QueryValidatorsRequest{}) + require.NoError(t, err) + require.Len(t, res.Validators, 2) +} + +func Test_QueryParams(t *testing.T) { + ctx, input := createDefaultTestInput(t) + + params := input.OPChildKeeper.GetParams(ctx) + params.MinGasPrices = sdk.NewDecCoins(sdk.NewInt64DecCoin("stake", 1)) + input.OPChildKeeper.SetParams(ctx, params) + + q := keeper.NewQuerier(input.OPChildKeeper) + res, err := q.Params(ctx, &types.QueryParamsRequest{}) + require.NoError(t, err) + require.Equal(t, params, res.Params) +} diff --git a/x/opchild/keeper/sequence_test.go b/x/opchild/keeper/sequence_test.go new file mode 100644 index 00000000..d399f4c1 --- /dev/null +++ b/x/opchild/keeper/sequence_test.go @@ -0,0 +1,55 @@ +package keeper_test + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_FinalizedL1Sequence(t *testing.T) { + ctx, input := createDefaultTestInput(t) + + res := input.OPChildKeeper.HasFinalizedL1Sequence(ctx, 1) + require.False(t, res) + + input.OPChildKeeper.RecordFinalizedL1Sequence(ctx, 1) + res = input.OPChildKeeper.HasFinalizedL1Sequence(ctx, 1) + require.True(t, res) +} + +func Test_IterateFinalizedL1Sequences(t *testing.T) { + ctx, input := createDefaultTestInput(t) + + sequences := []uint64{1, 2, 4} + for _, v := range sequences { + input.OPChildKeeper.RecordFinalizedL1Sequence(ctx, v) + } + input.OPChildKeeper.IterateFinalizedL1Sequences(ctx, func(l1Sequence uint64) bool { + require.Equal(t, sequences[0], l1Sequence) + sequences = sequences[1:] + return false + }) +} + +func Test_SetAndSetNextL2Sequence(t *testing.T) { + ctx, input := createDefaultTestInput(t) + + seq := input.OPChildKeeper.GetNextL2Sequence(ctx) + require.Equal(t, uint64(1), seq) + + input.OPChildKeeper.SetNextL2Sequence(ctx, 1204) + seq = input.OPChildKeeper.GetNextL2Sequence(ctx) + require.Equal(t, uint64(1204), seq) +} + +func Test_IncreaseNextL2Sequence(t *testing.T) { + ctx, input := createDefaultTestInput(t) + + seq := input.OPChildKeeper.GetNextL2Sequence(ctx) + require.Equal(t, uint64(1), seq) + + seq = input.OPChildKeeper.IncreaseNextL2Sequence(ctx) + require.Equal(t, uint64(1), seq) + seq = input.OPChildKeeper.IncreaseNextL2Sequence(ctx) + require.Equal(t, uint64(2), seq) +} diff --git a/x/opchild/keeper/sequences.go b/x/opchild/keeper/sequences.go index 9e69abfa..f1ff9e57 100644 --- a/x/opchild/keeper/sequences.go +++ b/x/opchild/keeper/sequences.go @@ -58,13 +58,16 @@ func (k Keeper) GetNextL2Sequence(ctx sdk.Context) uint64 { func (k Keeper) IncreaseNextL2Sequence(ctx sdk.Context) uint64 { kvStore := ctx.KVStore(k.storeKey) bz := kvStore.Get(types.NextL2SequenceKey) - if len(bz) == 0 { - bz := [8]byte{} - binary.BigEndian.PutUint64(bz[:], 2) - kvStore.Set(types.NextL2SequenceKey, bz[:]) - return 1 + nextL2Sequence := uint64(1) + if len(bz) != 0 { + nextL2Sequence = binary.BigEndian.Uint64(bz) } - return binary.BigEndian.Uint64(bz) + // increase next l2 sequence + _nextL2Sequence := [8]byte{} + binary.BigEndian.PutUint64(_nextL2Sequence[:], nextL2Sequence+1) + kvStore.Set(types.NextL2SequenceKey, _nextL2Sequence[:]) + + return nextL2Sequence } diff --git a/x/opchild/keeper/staking_test.go b/x/opchild/keeper/staking_test.go new file mode 100644 index 00000000..2d035884 --- /dev/null +++ b/x/opchild/keeper/staking_test.go @@ -0,0 +1,37 @@ +package keeper_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func Test_MaxValidators(t *testing.T) { + ctx, input := createDefaultTestInput(t) + + params := input.OPChildKeeper.GetParams(ctx) + params.MaxValidators = 10 + input.OPChildKeeper.SetParams(ctx, params) + + maxValidators := input.OPChildKeeper.MaxValidators(ctx) + require.Equal(t, uint32(10), maxValidators) +} + +func Test_HistoricalEntries(t *testing.T) { + ctx, input := createDefaultTestInput(t) + + params := input.OPChildKeeper.GetParams(ctx) + params.HistoricalEntries = 10 + input.OPChildKeeper.SetParams(ctx, params) + + entries := input.OPChildKeeper.HistoricalEntries(ctx) + require.Equal(t, uint32(10), entries) +} + +func Test_UnbondingTime(t *testing.T) { + ctx, input := createDefaultTestInput(t) + + unbondingTime := input.OPChildKeeper.UnbondingTime(ctx) + require.Equal(t, (60 * 60 * 24 * 7 * time.Second), unbondingTime) +} diff --git a/x/opchild/keeper/val_state_change.go b/x/opchild/keeper/val_state_change.go index 62900af5..1f733150 100644 --- a/x/opchild/keeper/val_state_change.go +++ b/x/opchild/keeper/val_state_change.go @@ -19,12 +19,6 @@ func (k Keeper) BlockValidatorUpdates(ctx sdk.Context) []abci.ValidatorUpdate { panic(err) } - // if there is no validator updates, - // delete tracking info to prevent empty block creation. - if len(updates) == 0 { - k.DeleteHistoricalInfo(ctx, ctx.BlockHeight()) - } - return updates } diff --git a/x/opchild/types/codec.go b/x/opchild/types/codec.go index b34e6ccc..6f2e4ade 100644 --- a/x/opchild/types/codec.go +++ b/x/opchild/types/codec.go @@ -24,7 +24,6 @@ func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { legacy.RegisterAminoMsg(cdc, &MsgFinalizeTokenDeposit{}, "opchild/MsgFinalizeTokenDeposit") legacy.RegisterAminoMsg(cdc, &MsgInitiateTokenWithdrawal{}, "opchild/MsgInitiateTokenWithdrawal") - //cdc.RegisterConcrete(&PublishAuthorization{}, "move/PublishAuthorization", nil) cdc.RegisterConcrete(Params{}, "opchild/Params", nil) } diff --git a/x/opchild/types/errors.go b/x/opchild/types/errors.go index 3135eb10..a1efba47 100644 --- a/x/opchild/types/errors.go +++ b/x/opchild/types/errors.go @@ -16,4 +16,5 @@ var ( ErrDepositAlreadyFinalized = errorsmod.Register(ModuleName, 9, "deposit already finalized") ErrInvalidAmount = errorsmod.Register(ModuleName, 10, "invalid amount") ErrInvalidSequence = errorsmod.Register(ModuleName, 11, "invalid sequence") + ErrZeroMaxValidators = errorsmod.Register(ModuleName, 12, "max validators must be non-zero") ) diff --git a/x/opchild/types/genesis.go b/x/opchild/types/genesis.go index 5024075c..46e22e9a 100644 --- a/x/opchild/types/genesis.go +++ b/x/opchild/types/genesis.go @@ -21,10 +21,12 @@ func NewGenesisState(params Params, validators []Validator) *GenesisState { // DefaultGenesisState gets the raw genesis raw message for testing func DefaultGenesisState() *GenesisState { return &GenesisState{ - Params: DefaultParams(), - LastValidatorPowers: []LastValidatorPower{}, - Validators: []Validator{}, - Exported: false, + Params: DefaultParams(), + LastValidatorPowers: []LastValidatorPower{}, + Validators: []Validator{}, + Exported: false, + NextL2Sequence: 1, + FinalizedL1Sequences: []uint64{}, } } diff --git a/x/opchild/types/keys.go b/x/opchild/types/keys.go index 6a2d3a2e..13983a38 100644 --- a/x/opchild/types/keys.go +++ b/x/opchild/types/keys.go @@ -10,7 +10,7 @@ import ( const ( // ModuleName is the name of the opchild module - // module addr: init1vl25je2ntvjy7u9dnz9qzju674vfe25tkhhp92 + // module addr: init1gz9n8jnu9fgqw7vem9ud67gqjk5q4m2w0aejne ModuleName = "opchild" // StoreKey is the string store representation diff --git a/x/opchild/types/params.go b/x/opchild/types/params.go index 1c405946..cc14edd0 100644 --- a/x/opchild/types/params.go +++ b/x/opchild/types/params.go @@ -48,5 +48,9 @@ func (p Params) Validate() error { return err } + if p.MaxValidators == 0 { + return ErrZeroMaxValidators + } + return nil } diff --git a/x/ophost/client/cli/query.go b/x/ophost/client/cli/query.go new file mode 100644 index 00000000..0a469d70 --- /dev/null +++ b/x/ophost/client/cli/query.go @@ -0,0 +1,22 @@ +package cli + +import ( + "github.com/cosmos/cosmos-sdk/client" + "github.com/initia-labs/OPinit/x/ophost/types" + "github.com/spf13/cobra" +) + +// GetQueryCmd returns the cli query commands for this module +func GetQueryCmd() *cobra.Command { + ophostQueryCmd := &cobra.Command{ + Use: types.ModuleName, + Short: "Querying commands for the ophost module", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + + ophostQueryCmd.AddCommand() + + return ophostQueryCmd +} diff --git a/x/ophost/client/cli/tx.go b/x/ophost/client/cli/tx.go new file mode 100644 index 00000000..c54c0f9d --- /dev/null +++ b/x/ophost/client/cli/tx.go @@ -0,0 +1,389 @@ +package cli + +import ( + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "strconv" + "strings" + "time" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/tx" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/version" + "github.com/initia-labs/OPinit/x/ophost/types" + "github.com/spf13/cobra" +) + +// GetTxCmd returns a root CLI command handler for all x/ophost transaction commands. +func GetTxCmd() *cobra.Command { + ophostTxCmd := &cobra.Command{ + Use: types.ModuleName, + Short: "OPHost transaction subcommands", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + + ophostTxCmd.AddCommand( + NewRecordBatchCmd(), + NewCreateBridge(), + NewProposeOutput(), + NewDeleteOutput(), + NewInitiateTokenDeposit(), + NewFinalizeTokenWithdrawal(), + ) + + return ophostTxCmd +} + +// NewRecordBatchCmd returns a CLI command handler for transaction to submitting a batch record. +func NewRecordBatchCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "record-batch [bridge_id] [base64-encoded-batch-bytes]", + Short: "send a batch-recording tx", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + bridgeId, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return err + } + + batchBytes, err := base64.StdEncoding.DecodeString(args[1]) + if err != nil { + return err + } + + msg := types.NewMsgRecordBatch(clientCtx.GetFromAddress(), bridgeId, batchBytes) + if err = msg.ValidateBasic(); err != nil { + return err + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} + +// NewCreateBridgeCmd returns a CLI command handler for transaction to creating a bridge. +func NewCreateBridge() *cobra.Command { + cmd := &cobra.Command{ + Use: "create-bridge [path/to/bridge-config.json]", + Short: "send a bridge creating tx", + Long: strings.TrimSpace( + fmt.Sprintf( + `send a tx to create a bridge with a config file as a json. + Example: + $ %s tx ophost create-bridge path/to/bridge-config.json + + Where bridge-config.json contains: + { + "challenger": "bech32-address", + "proposer": "bech32-addresss", + "submissionIntervan": "duration", + "finalizationPeriod": "duration", + "submissionStartTime" : "rfc3339-datetime", + "metadata": "channel-id" + }`, version.AppName, + ), + ), + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + configBytes, err := os.ReadFile(args[0]) + if err != nil { + return err + } + + origConfig := BridgeConfig{} + err = json.Unmarshal(configBytes, &origConfig) + if err != nil { + return err + } + + submissionInterval, err := time.ParseDuration(origConfig.SubmissionInterval) + if err != nil { + return err + } + + finalizationPeriod, err := time.ParseDuration(origConfig.FinalizationPeriod) + if err != nil { + return err + } + + submissionStartTime, err := time.Parse(time.RFC3339, origConfig.SubmissionStartTime) + if err != nil { + return err + } + + config := types.BridgeConfig{ + Challenger: origConfig.Challenger, + Proposer: origConfig.Proposer, + SubmissionInterval: submissionInterval, + FinalizationPeriod: finalizationPeriod, + SubmissionStartTime: submissionStartTime, + Metadata: []byte(origConfig.Metadata), + } + if err = config.Validate(); err != nil { + return err + } + + msg := types.NewMsgCreateBridge(clientCtx.GetFromAddress(), config) + if err = msg.ValidateBasic(); err != nil { + return err + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} + +// NewProposeOutput returns a CLI command handler for transaction to propose an output. +func NewProposeOutput() *cobra.Command { + cmd := &cobra.Command{ + Use: "propose-output [bridge-id] [l2-block-number] [output-root-hash]", + Short: "send a output-proposing tx", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + bridgeId, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return err + } + + l2BlockNumber, err := strconv.ParseUint(args[1], 10, 64) + if err != nil { + return err + } + + outputBytes, err := hex.DecodeString(args[2]) + if err != nil { + return err + } + + msg := types.NewMsgProposeOutput(clientCtx.GetFromAddress(), bridgeId, l2BlockNumber, outputBytes) + if err = msg.ValidateBasic(); err != nil { + return err + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} + +// NewDeleteOutput returns a CLI command handler for transaction to remove an output. +func NewDeleteOutput() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete-output [bridge-id] [output-index]", + Short: "send a output-proposing tx", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + bridgeId, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return err + } + + outputIndex, err := strconv.ParseUint(args[1], 10, 64) + if err != nil { + return err + } + + msg := types.NewMsgDeleteOutput(clientCtx.GetFromAddress(), bridgeId, outputIndex) + if err = msg.ValidateBasic(); err != nil { + return err + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} + +// NewInitiateTokenDeposit returns a CLI command handler for transaction to initiate token deposit. +func NewInitiateTokenDeposit() *cobra.Command { + cmd := &cobra.Command{ + Use: "initiate-token-deposit [bridge-id] [to] [amount] [data]", + Short: "send a token deposit initiating tx", + Args: cobra.ExactArgs(4), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + bridgeId, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return err + } + + to, err := sdk.AccAddressFromBech32(args[1]) + if err != nil { + return err + } + + amount, err := sdk.ParseCoinNormalized(args[2]) + if err != nil { + return err + } + + data, err := hex.DecodeString(args[3]) + if err != nil { + return err + } + + msg := types.NewMsgInitiateTokenDeposit(clientCtx.GetFromAddress(), bridgeId, to, amount, data) + if err = msg.ValidateBasic(); err != nil { + return err + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} + +// NewFinalizeTokenWithdrawal returns a CLI command handler for transaction to finalize token withdrawal. +func NewFinalizeTokenWithdrawal() *cobra.Command { + cmd := &cobra.Command{ + Use: "finalize-token-withdrawal [path/to/withdrawal-info.json]", + Short: "send a token deposit initiating tx", + Long: strings.TrimSpace( + fmt.Sprintf( + `send a tx to finalize tokwn withdrawal with withdrawal info json. + Example: + $ %s tx ophost finalize-token-withdrawal path/to/withdrawal-info.json + + Where withrawal-info.json contains: + { + "bridge_id": 1, + "output_index": 0, + "withdrawal_proofs": [ "proof1", "proof2", ... ], + "receiver": "bech32-address", + "sequence": 0, + "amount": "10000000uatom", + "version": "version hex", + "state_root": "state-root hex", + "storage_root": "storage-root hex", + "latest_block_hash": "latest-block-hash" + }`, version.AppName, + ), + ), + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + withdrawalBytes, err := os.ReadFile(args[0]) + if err != nil { + return err + } + withdrawalInfo := MsgFinalizeTokenWithdrawal{} + err = json.Unmarshal(withdrawalBytes, &withdrawalInfo) + if err != nil { + return err + } + + withdrawalProofs := make([][]byte, len(withdrawalInfo.WithdrawalProofs)) + for i, wp := range withdrawalInfo.WithdrawalProofs { + withdrawalProofs[i], err = hex.DecodeString(wp) + if err != nil { + return err + } + } + + receiver, err := sdk.AccAddressFromBech32(withdrawalInfo.Receiver) + if err != nil { + return err + } + + amount, err := sdk.ParseCoinNormalized(withdrawalInfo.Amount) + if err != nil { + return err + } + + version, err := hex.DecodeString(withdrawalInfo.Version) + if err != nil { + return err + } + + stateRoot, err := hex.DecodeString(withdrawalInfo.StateRoot) + if err != nil { + return err + } + + storageRoot, err := hex.DecodeString(withdrawalInfo.StorageRoot) + if err != nil { + return err + } + + latestBlockHash, err := hex.DecodeString(withdrawalInfo.LatestBlockHash) + if err != nil { + return err + } + + msg := types.NewMsgFinalizeTokenWithdrawal( + withdrawalInfo.BridgeId, + withdrawalInfo.OutputIndex, + withdrawalInfo.Sequence, + withdrawalProofs, + clientCtx.GetFromAddress(), + receiver, + amount, + version, + stateRoot, + storageRoot, + latestBlockHash, + ) + if err = msg.ValidateBasic(); err != nil { + return err + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} diff --git a/x/ophost/client/cli/tx_test.go b/x/ophost/client/cli/tx_test.go new file mode 100644 index 00000000..b348d84f --- /dev/null +++ b/x/ophost/client/cli/tx_test.go @@ -0,0 +1,547 @@ +package cli_test + +import ( + "bytes" + "fmt" + "io" + "os" + "testing" + + abci "github.com/cometbft/cometbft/abci/types" + rpcclientmock "github.com/cometbft/cometbft/rpc/client/mock" + "github.com/cosmos/gogoproto/proto" + "github.com/stretchr/testify/suite" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/crypto/hd" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + "github.com/cosmos/cosmos-sdk/x/bank" + + sdk "github.com/cosmos/cosmos-sdk/types" + testutilmod "github.com/cosmos/cosmos-sdk/types/module/testutil" + + "github.com/initia-labs/OPinit/x/ophost" + "github.com/initia-labs/OPinit/x/ophost/client/cli" +) + +var PKs = simtestutil.CreateTestPubKeys(500) + +type CLITestSuite struct { + suite.Suite + + kr keyring.Keyring + encCfg testutilmod.TestEncodingConfig + baseCtx client.Context + clientCtx client.Context + addrs []sdk.AccAddress +} + +func (s *CLITestSuite) SetupSuite() { + s.encCfg = testutilmod.MakeTestEncodingConfig(ophost.AppModuleBasic{}, bank.AppModuleBasic{}) + s.kr = keyring.NewInMemory(s.encCfg.Codec) + s.baseCtx = client.Context{}. + WithKeyring(s.kr). + WithTxConfig(s.encCfg.TxConfig). + WithCodec(s.encCfg.Codec). + WithClient(clitestutil.MockTendermintRPC{Client: rpcclientmock.Client{}}). + WithAccountRetriever(client.MockAccountRetriever{}). + WithOutput(io.Discard). + WithChainID("test-chain") + + var outBuf bytes.Buffer + ctxGen := func() client.Context { + bz, _ := s.encCfg.Codec.Marshal(&sdk.TxResponse{}) + c := clitestutil.NewMockTendermintRPC(abci.ResponseQuery{ + Value: bz, + }) + return s.baseCtx.WithClient(c) + } + s.clientCtx = ctxGen().WithOutput(&outBuf) + + s.addrs = make([]sdk.AccAddress, 0) + for i := 0; i < 3; i++ { + k, _, err := s.clientCtx.Keyring.NewMnemonic("NewValidator", keyring.English, sdk.FullFundraiserPath, keyring.DefaultBIP39Passphrase, hd.Secp256k1) + s.Require().NoError(err) + + pub, err := k.GetPubKey() + s.Require().NoError(err) + + newAddr := sdk.AccAddress(pub.Address()) + s.addrs = append(s.addrs, newAddr) + } +} + +func (s *CLITestSuite) TestNewRecordBatchCmd() { + require := s.Require() + cmd := cli.NewRecordBatchCmd() + + testCases := []struct { + name string + args []string + expectErr bool + expectedCode uint32 + respType proto.Message + }{ + { + "invalid transaction (invalid bridge_id)", + []string{ + "0", + "Ynl0ZXM=", + fmt.Sprintf("--%s=%s", flags.FlagFrom, s.addrs[0]), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()), + }, + true, 0, &sdk.TxResponse{}, + }, + { + "invalid transaction (invalid batch_bytes)", + []string{ + "1", + "batch_bytes_should_be_base64_encoded", + fmt.Sprintf("--%s=%s", flags.FlagFrom, s.addrs[0]), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()), + }, + true, 0, &sdk.TxResponse{}, + }, + { + "valid transaction", + []string{ + "1", + "Ynl0ZXM=", + fmt.Sprintf("--%s=%s", flags.FlagFrom, s.addrs[0]), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()), + }, + false, 0, &sdk.TxResponse{}, + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + out, err := clitestutil.ExecTestCLICmd(s.clientCtx, cmd, tc.args) + if tc.expectErr { + require.Error(err) + } else { + require.NoError(err, "test: %s\noutput: %s", tc.name, out.String()) + err = s.clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType) + require.NoError(err, out.String(), "test: %s, output\n:", tc.name, out.String()) + + txResp := tc.respType.(*sdk.TxResponse) + require.Equal(tc.expectedCode, txResp.Code, + "test: %s, output\n:", tc.name, out.String()) + } + }) + } +} + +func (s *CLITestSuite) TestNewCreateBridge() { + require := s.Require() + cmd := cli.NewCreateBridge() + + invalidConfig, err := os.CreateTemp("/tmp", "bridge_config") + require.NoError(err) + defer os.Remove(invalidConfig.Name()) + validConfig, err := os.CreateTemp("/tmp", "bridge_config") + require.NoError(err) + defer os.Remove(validConfig.Name()) + + invalidConfig.WriteString(`{}`) + validConfig.WriteString(`{ + "challenger": "cosmos1q6jhwnarkw2j5qqgx3qlu20k8nrdglft6qssy2", + "proposer": "cosmos1k2svyvm60r8rhnzr9vemk5f6fksvm6tyh2jj66", + "submission_interval": "100s", + "finalization_period": "1000s", + "submission_start_time" : "2023-12-01T00:00:00Z", + "metadata": "channel-0" + }`) + + testCases := []struct { + name string + args []string + expectErr bool + expectedCode uint32 + respType proto.Message + }{ + { + "invalid transaction (invalid bridge config)", + []string{ + invalidConfig.Name(), + fmt.Sprintf("--%s=%s", flags.FlagFrom, s.addrs[0]), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()), + }, + true, 0, &sdk.TxResponse{}, + }, + { + "valid transaction", + []string{ + validConfig.Name(), + fmt.Sprintf("--%s=%s", flags.FlagFrom, s.addrs[0]), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()), + }, + false, 0, &sdk.TxResponse{}, + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + out, err := clitestutil.ExecTestCLICmd(s.clientCtx, cmd, tc.args) + if tc.expectErr { + require.Error(err) + } else { + require.NoError(err, "test: %s\noutput: %s", tc.name, out.String()) + err = s.clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType) + require.NoError(err, out.String(), "test: %s, output\n:", tc.name, out.String()) + + txResp := tc.respType.(*sdk.TxResponse) + require.Equal(tc.expectedCode, txResp.Code, + "test: %s, output\n:", tc.name, out.String()) + } + }) + } +} + +func (s *CLITestSuite) TestNewProposeOutput() { + require := s.Require() + cmd := cli.NewProposeOutput() + + testCases := []struct { + name string + args []string + expectErr bool + expectedCode uint32 + respType proto.Message + }{ + { + "invalid transaction (invalid bridge-id)", + []string{ + "0", + "1234", + "12e297e695e451144fc44db083d6b3d56f0a5f920721e3efc90ec7662c7775d1", + fmt.Sprintf("--%s=%s", flags.FlagFrom, s.addrs[0]), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()), + }, + true, 0, &sdk.TxResponse{}, + }, + { + "invalid transaction (invalid l2-block-nmber)", + []string{ + "1", + "-1", + "12e297e695e451144fc44db083d6b3d56f0a5f920721e3efc90ec7662c7775d1", + fmt.Sprintf("--%s=%s", flags.FlagFrom, s.addrs[0]), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()), + }, + true, 0, &sdk.TxResponse{}, + }, + { + "invalid transaction (invalid output-root-hash)", + []string{ + "1", + "1234", + "2e297e695e451144fc44db083d6b3d56f0a5f920721e3efc90ec7662c7775d1", + fmt.Sprintf("--%s=%s", flags.FlagFrom, s.addrs[0]), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()), + }, + true, 0, &sdk.TxResponse{}, + }, + { + "valid transaction", + []string{ + "1", + "1234", + "12e297e695e451144fc44db083d6b3d56f0a5f920721e3efc90ec7662c7775d1", + fmt.Sprintf("--%s=%s", flags.FlagFrom, s.addrs[0]), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()), + }, + false, 0, &sdk.TxResponse{}, + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + out, err := clitestutil.ExecTestCLICmd(s.clientCtx, cmd, tc.args) + if tc.expectErr { + require.Error(err) + } else { + require.NoError(err, "test: %s\noutput: %s", tc.name, out.String()) + err = s.clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType) + require.NoError(err, out.String(), "test: %s, output\n:", tc.name, out.String()) + + txResp := tc.respType.(*sdk.TxResponse) + require.Equal(tc.expectedCode, txResp.Code, + "test: %s, output\n:", tc.name, out.String()) + } + }) + } +} + +func (s *CLITestSuite) TestNewDeleteOutput() { + require := s.Require() + cmd := cli.NewDeleteOutput() + + testCases := []struct { + name string + args []string + expectErr bool + expectedCode uint32 + respType proto.Message + }{ + { + "invalid transaction (invalid bridge-id)", + []string{ + "0", + "1000", + fmt.Sprintf("--%s=%s", flags.FlagFrom, s.addrs[0]), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()), + }, + true, 0, &sdk.TxResponse{}, + }, + { + "invalid transaction (invalid output-index)", + []string{ + "1", + "-1", + "2e297e695e451144fc44db083d6b3d56f0a5f920721e3efc90ec7662c7775d1", + fmt.Sprintf("--%s=%s", flags.FlagFrom, s.addrs[0]), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()), + }, + true, 0, &sdk.TxResponse{}, + }, + { + "valid transaction", + []string{ + "1", + "2", + fmt.Sprintf("--%s=%s", flags.FlagFrom, s.addrs[0]), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()), + }, + false, 0, &sdk.TxResponse{}, + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + out, err := clitestutil.ExecTestCLICmd(s.clientCtx, cmd, tc.args) + if tc.expectErr { + require.Error(err) + } else { + require.NoError(err, "test: %s\noutput: %s", tc.name, out.String()) + err = s.clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType) + require.NoError(err, out.String(), "test: %s, output\n:", tc.name, out.String()) + + txResp := tc.respType.(*sdk.TxResponse) + require.Equal(tc.expectedCode, txResp.Code, + "test: %s, output\n:", tc.name, out.String()) + } + }) + } +} + +func (s *CLITestSuite) TestNewInitiateTokenDeposit() { + require := s.Require() + cmd := cli.NewInitiateTokenDeposit() + + testCases := []struct { + name string + args []string + expectErr bool + expectedCode uint32 + respType proto.Message + }{ + { + "invalid transaction (invalid bridge-id)", + []string{ + "0", + "cosmos1q6jhwnarkw2j5qqgx3qlu20k8nrdglft6qssy2", + "10000uatom", + "", + fmt.Sprintf("--%s=%s", flags.FlagFrom, s.addrs[0]), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()), + }, + true, 0, &sdk.TxResponse{}, + }, + { + "invalid transaction (invalid to)", + []string{ + "0", + "cosmos1q6jhwnarkw2j5qqgx3qlu20k8nrdglft6qssy3", + "10000uatom", + "", + fmt.Sprintf("--%s=%s", flags.FlagFrom, s.addrs[0]), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()), + }, + true, 0, &sdk.TxResponse{}, + }, + { + "invalid transaction (invalid amount)", + []string{ + "0", + "cosmos1q6jhwnarkw2j5qqgx3qlu20k8nrdglft6qssy2", + "invalid_amount", + "", + fmt.Sprintf("--%s=%s", flags.FlagFrom, s.addrs[0]), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()), + }, + true, 0, &sdk.TxResponse{}, + }, + { + "invalid transaction (invalid data)", + []string{ + "0", + "cosmos1q6jhwnarkw2j5qqgx3qlu20k8nrdglft6qssy2", + "10000uatom", + fmt.Sprintf("--%s=%s", flags.FlagFrom, s.addrs[0]), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()), + }, + true, 0, &sdk.TxResponse{}, + }, + { + "valid transaction", + []string{ + "1", + "cosmos1q6jhwnarkw2j5qqgx3qlu20k8nrdglft6qssy2", + "10000uatom", + "", + fmt.Sprintf("--%s=%s", flags.FlagFrom, s.addrs[0]), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()), + }, + false, 0, &sdk.TxResponse{}, + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + out, err := clitestutil.ExecTestCLICmd(s.clientCtx, cmd, tc.args) + if tc.expectErr { + require.Error(err) + } else { + require.NoError(err, "test: %s\noutput: %s", tc.name, out.String()) + err = s.clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType) + require.NoError(err, out.String(), "test: %s, output\n:", tc.name, out.String()) + + txResp := tc.respType.(*sdk.TxResponse) + require.Equal(tc.expectedCode, txResp.Code, + "test: %s, output\n:", tc.name, out.String()) + } + }) + } +} + +func (s *CLITestSuite) TestNewFinalizeTokenWithdrawal() { + require := s.Require() + cmd := cli.NewFinalizeTokenWithdrawal() + + invalidConfig, err := os.CreateTemp("/tmp", "withdrawal_info") + require.NoError(err) + defer os.Remove(invalidConfig.Name()) + validConfig, err := os.CreateTemp("/tmp", "withdrawal_info") + require.NoError(err) + defer os.Remove(validConfig.Name()) + + invalidConfig.WriteString(`{}`) + validConfig.WriteString(`{ + "bridge_id": 1, + "output_index": 2, + "withdrawal_proofs": ["8e1fa5cd035b30e5d5818934dbc7491fe44f4ab15d30b3abcbc01d44edf25f18", "80d66720e75121fedc738e9847048466ac8d05626406fe3b438b1699dcbfa37e"], + "receiver": "cosmos1k2svyvm60r8rhnzr9vemk5f6fksvm6tyh2jj66", + "sequence": 3, + "amount": "10000000uatom", + "version": "5ca4f3850ccc331aaf8a257d6086e526a3b42a63e18cb11d020847985b31d188", + "state_root": "1d844ab7b05fad0adab1efa288baeb640ceddc1931dccdd89b521379329bc55d", + "storage_root": "45cba73df03a0d62aa297ea7e949bb0e608b01290205dde56a8fdc8f96239f3b", + "latest_block_hash": "32935c42573839f5ff3065941d98e378e3e73227bf29e349de4aa7af0ca8addd" + }`) + + testCases := []struct { + name string + args []string + expectErr bool + expectedCode uint32 + respType proto.Message + }{ + { + "invalid transaction (invalid withdrawal info)", + []string{ + invalidConfig.Name(), + fmt.Sprintf("--%s=%s", flags.FlagFrom, s.addrs[0]), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()), + }, + true, 0, &sdk.TxResponse{}, + }, + { + "valid transaction", + []string{ + validConfig.Name(), + fmt.Sprintf("--%s=%s", flags.FlagFrom, s.addrs[0]), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10))).String()), + }, + false, 0, &sdk.TxResponse{}, + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + out, err := clitestutil.ExecTestCLICmd(s.clientCtx, cmd, tc.args) + if tc.expectErr { + require.Error(err) + } else { + require.NoError(err, "test: %s\noutput: %s", tc.name, out.String()) + err = s.clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType) + require.NoError(err, out.String(), "test: %s, output\n:", tc.name, out.String()) + + txResp := tc.respType.(*sdk.TxResponse) + require.Equal(tc.expectedCode, txResp.Code, + "test: %s, output\n:", tc.name, out.String()) + } + }) + } +} + +func TestCLITestSuite(t *testing.T) { + suite.Run(t, new(CLITestSuite)) +} diff --git a/x/ophost/client/cli/types.go b/x/ophost/client/cli/types.go new file mode 100644 index 00000000..226a2523 --- /dev/null +++ b/x/ophost/client/cli/types.go @@ -0,0 +1,40 @@ +package cli + +// BridgeConfig defines the set of bridge config. +// +// NOTE: it is a modified BridgeConfig from x/ophost/types/types.pb.go to make unmarshal easier +type BridgeConfig struct { + // The address of the challenger. + Challenger string `protobuf:"bytes,1,opt,name=challenger,proto3" json:"challenger,omitempty"` + // The address of the proposer. + Proposer string `protobuf:"bytes,2,opt,name=proposer,proto3" json:"proposer,omitempty"` + // The time interval at which checkpoints must be submitted. + // NOTE: this param is currently not used, but will be used for challenge in future. + SubmissionInterval string `protobuf:"bytes,3,opt,name=submission_interval,json=submissionInterval,proto3,stdduration" json:"submission_interval,omitempty"` + // The minium time duration that must elapse before a withdrawal can be finalized. + FinalizationPeriod string `protobuf:"bytes,4,opt,name=finalization_period,json=finalizationPeriod,proto3,stdduration" json:"finalization_period,omitempty"` + // The time of the first l2 block recorded. + // NOTE: this param is currently not used, but will be used for challenge in future. + SubmissionStartTime string `protobuf:"bytes,5,opt,name=submission_start_time,json=submissionStartTime,proto3,stdtime" json:"submission_start_time"` + // Normally it is IBC channelID for permissioned IBC relayer. + Metadata string `protobuf:"bytes,6,opt,name=metadata,proto3" json:"metadata,omitempty"` +} + +// MsgFinalizeTokenWithdrawal is a message to remove a validator from designated list +// +// NOTE: it is a modified MsgFinalizeTokenWithdrawal from x/ophost/types/txpb.go to make unmarshal easier +type MsgFinalizeTokenWithdrawal struct { + BridgeId uint64 `protobuf:"varint,2,opt,name=bridge_id,json=bridgeId,proto3" json:"bridge_id,omitempty" yaml:"bridge_id"` + OutputIndex uint64 `protobuf:"varint,3,opt,name=output_index,json=outputIndex,proto3" json:"output_index,omitempty" yaml:"output_index"` + WithdrawalProofs []string `protobuf:"bytes,4,rep,name=withdrawal_proofs,json=withdrawalProofs,proto3" json:"withdrawal_proofs,omitempty"` + // no sender here + //Sender string `protobuf:"bytes,1,opt,name=sender,proto3" json:"sender,omitempty" yaml:"sender"` + Receiver string `protobuf:"bytes,5,opt,name=receiver,proto3" json:"receiver,omitempty" yaml:"receiver"` + Sequence uint64 `protobuf:"varint,6,opt,name=sequence,proto3" json:"sequence,omitempty" yaml:"sequence"` + Amount string `protobuf:"bytes,7,opt,name=amount,proto3" json:"amount" yaml:"amount"` + // version of the output root + Version string `protobuf:"bytes,8,opt,name=version,proto3" json:"version,omitempty" yaml:"version"` + StateRoot string `protobuf:"bytes,9,opt,name=state_root,json=stateRoot,proto3" json:"state_root,omitempty" yaml:"state_root"` + StorageRoot string `protobuf:"bytes,10,opt,name=storage_root,json=storageRoot,proto3" json:"storage_root,omitempty" yaml:"storage_root"` + LatestBlockHash string `protobuf:"bytes,11,opt,name=latest_block_hash,json=latestBlockHash,proto3" json:"latest_block_hash,omitempty" yaml:"latest_block_hash"` +} diff --git a/x/ophost/keeper/bridge_test.go b/x/ophost/keeper/bridge_test.go new file mode 100644 index 00000000..4ce2a5ff --- /dev/null +++ b/x/ophost/keeper/bridge_test.go @@ -0,0 +1,57 @@ +package keeper_test + +import ( + "testing" + "time" + + "github.com/initia-labs/OPinit/x/ophost/types" + "github.com/stretchr/testify/require" +) + +func Test_BridgeConfig(t *testing.T) { + ctx, input := createDefaultTestInput(t) + config := types.BridgeConfig{ + Challenger: addrs[0].String(), + Proposer: addrs[1].String(), + SubmissionInterval: time.Second * 100, + FinalizationPeriod: time.Second * 10, + SubmissionStartTime: time.Now().UTC(), + Metadata: []byte{1, 2, 3}, + } + require.NoError(t, input.OPHostKeeper.SetBridgeConfig(ctx, 1, config)) + _config, err := input.OPHostKeeper.GetBridgeConfig(ctx, 1) + require.NoError(t, err) + require.Equal(t, config, _config) +} + +func Test_IterateBridgeConfig(t *testing.T) { + ctx, input := createDefaultTestInput(t) + config1 := types.BridgeConfig{ + Challenger: addrs[0].String(), + Proposer: addrs[1].String(), + SubmissionInterval: time.Second * 100, + FinalizationPeriod: time.Second * 10, + SubmissionStartTime: time.Now().UTC(), + Metadata: []byte{1, 2, 3}, + } + config2 := types.BridgeConfig{ + Challenger: addrs[2].String(), + Proposer: addrs[3].String(), + SubmissionInterval: time.Second * 100, + FinalizationPeriod: time.Second * 10, + SubmissionStartTime: time.Now().UTC(), + Metadata: []byte{3, 4, 5}, + } + require.NoError(t, input.OPHostKeeper.SetBridgeConfig(ctx, 1, config1)) + require.NoError(t, input.OPHostKeeper.SetBridgeConfig(ctx, 2, config2)) + + input.OPHostKeeper.IterateBridgeConfig(ctx, func(bridgeId uint64, bridgeConfig types.BridgeConfig) bool { + if bridgeId == 1 { + require.Equal(t, config1, bridgeConfig) + } else { + require.Equal(t, config2, bridgeConfig) + } + + return false + }) +} diff --git a/x/ophost/keeper/common_test.go b/x/ophost/keeper/common_test.go index b7141f76..5bda556d 100644 --- a/x/ophost/keeper/common_test.go +++ b/x/ophost/keeper/common_test.go @@ -33,6 +33,7 @@ import ( banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" ophost "github.com/initia-labs/OPinit/x/ophost" @@ -252,7 +253,7 @@ func _createTestInput( authtypes.ProtoBaseAccount, // prototype maccPerms, sdk.GetConfig().GetBech32AccountAddrPrefix(), - authtypes.NewModuleAddress(ophosttypes.ModuleName).String(), + authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) blockedAddrs := make(map[string]bool) for acc := range maccPerms { @@ -264,7 +265,7 @@ func _createTestInput( keys[banktypes.StoreKey], accountKeeper, blockedAddrs, - authtypes.NewModuleAddress(ophosttypes.ModuleName).String(), + authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) bankKeeper.SetParams(ctx, banktypes.DefaultParams()) @@ -278,7 +279,7 @@ func _createTestInput( accountKeeper, bankKeeper, bridgeHook, - authtypes.NewModuleAddress(ophosttypes.ModuleName).String(), + authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) ophostParams := ophosttypes.DefaultParams() diff --git a/x/ophost/keeper/genesis_test.go b/x/ophost/keeper/genesis_test.go index 0bebf601..6d92c8b3 100644 --- a/x/ophost/keeper/genesis_test.go +++ b/x/ophost/keeper/genesis_test.go @@ -1,3 +1,99 @@ package keeper_test -// TODO - implement test +import ( + "testing" + "time" + + "github.com/initia-labs/OPinit/x/ophost/types" + "github.com/stretchr/testify/require" +) + +func Test_GenesisImportExport(t *testing.T) { + ctx, input := createDefaultTestInput(t) + + params := input.OPHostKeeper.GetParams(ctx) + config1 := types.BridgeConfig{ + Challenger: "challenger", + Proposer: "proposer", + SubmissionInterval: 100, + FinalizationPeriod: 100, + SubmissionStartTime: time.Now().UTC(), + Metadata: []byte{1, 2, 3}, + } + config2 := types.BridgeConfig{ + Challenger: "challenger2", + Proposer: "proposer2", + SubmissionInterval: 200, + FinalizationPeriod: 200, + SubmissionStartTime: time.Now().UTC(), + Metadata: []byte{3, 4, 5}, + } + require.NoError(t, input.OPHostKeeper.SetBridgeConfig(ctx, 1, config1)) + require.NoError(t, input.OPHostKeeper.SetBridgeConfig(ctx, 2, config2)) + + input.OPHostKeeper.SetNextBridgeId(ctx, 3) + input.OPHostKeeper.SetNextL1Sequence(ctx, 1, 100) + input.OPHostKeeper.SetNextL1Sequence(ctx, 2, 200) + input.OPHostKeeper.SetNextOutputIndex(ctx, 1, 10) + input.OPHostKeeper.SetNextOutputIndex(ctx, 2, 20) + + output1 := types.Output{ + OutputRoot: []byte{1, 2, 3}, + L1BlockTime: time.Now().UTC(), + L2BlockNumber: 100, + } + output2 := types.Output{ + OutputRoot: []byte{1, 2, 3}, + L1BlockTime: time.Now().UTC(), + L2BlockNumber: 200, + } + output3 := types.Output{ + OutputRoot: []byte{1, 2, 3}, + L1BlockTime: time.Now().UTC(), + L2BlockNumber: 100, + } + require.NoError(t, input.OPHostKeeper.SetOutputProposal(ctx, 1, 1, output1)) + require.NoError(t, input.OPHostKeeper.SetOutputProposal(ctx, 1, 2, output2)) + require.NoError(t, input.OPHostKeeper.SetOutputProposal(ctx, 2, 1, output3)) + + input.OPHostKeeper.SetTokenPair(ctx, 1, "l2denom", "l1denom") + input.OPHostKeeper.SetTokenPair(ctx, 2, "l12denom", "l11denom") + + input.OPHostKeeper.RecordProvenWithdrawal(ctx, 1, [32]byte{1, 2, 3}) + input.OPHostKeeper.RecordProvenWithdrawal(ctx, 1, [32]byte{3, 4, 5}) + + genState := input.OPHostKeeper.ExportGenesis(ctx) + input.OPHostKeeper.InitGenesis(ctx, genState) + + _genState := input.OPHostKeeper.ExportGenesis(ctx) + require.Equal(t, genState, _genState) + + require.Equal(t, uint64(3), genState.NextBridgeId) + require.Equal(t, params, genState.Params) + require.Equal(t, types.Bridge{ + BridgeId: 1, + NextL1Sequence: 100, + NextOutputIndex: 10, + BridgeConfig: config1, + TokenPairs: []types.TokenPair{ + { + L1Denom: "l1denom", + L2Denom: "l2denom", + }, + }, + Proposals: []types.WrappedOutput{ + { + OutputIndex: 1, + OutputProposal: output1, + }, + { + OutputIndex: 2, + OutputProposal: output2, + }, + }, + ProvenWithdrawals: [][]byte{ + {1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + {3, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + }, + }, genState.Bridges[0]) +} diff --git a/x/ophost/keeper/msg_server.go b/x/ophost/keeper/msg_server.go index 830ee8ff..01955388 100644 --- a/x/ophost/keeper/msg_server.go +++ b/x/ophost/keeper/msg_server.go @@ -98,6 +98,18 @@ func (ms MsgServer) ProposeOutput(context context.Context, req *types.MsgPropose // fetch next output index outputIndex := ms.IncreaseNextOutputIndex(ctx, bridgeId) + // check this is first submission or not + if outputIndex != 1 { + lastOutputProposal, err := ms.GetOutputProposal(ctx, bridgeId, outputIndex-1) + if err != nil { + return nil, err + } + + if l2BlockNumber <= lastOutputProposal.L2BlockNumber { + return nil, types.ErrInvalidL2BlockNumber + } + } + // store output proposal if err := ms.SetOutputProposal(ctx, bridgeId, outputIndex, types.Output{ OutputRoot: outputRoot, @@ -207,7 +219,7 @@ func (ms MsgServer) FinalizeTokenWithdrawal(context context.Context, req *types. outputIndex := req.OutputIndex l2Sequence := req.Sequence amount := req.Amount.Amount - l2Denom := req.Amount.Denom + denom := req.Amount.Denom if ok, err := ms.IsFinalized(ctx, bridgeId, outputIndex); err != nil { return nil, err @@ -240,12 +252,12 @@ func (ms MsgServer) FinalizeTokenWithdrawal(context context.Context, req *types. var withdrawalHash [32]byte { seed := []byte{} - binary.BigEndian.AppendUint64(seed, bridgeId) - binary.BigEndian.AppendUint64(seed, req.Sequence) + seed = binary.BigEndian.AppendUint64(seed, bridgeId) + seed = binary.BigEndian.AppendUint64(seed, req.Sequence) seed = append(seed, sender[:]...) seed = append(seed, receiver[:]...) - seed = append(seed, []byte(l2Denom)...) - binary.BigEndian.AppendUint64(seed, amount.Uint64()) + seed = append(seed, []byte(denom)...) + seed = binary.BigEndian.AppendUint64(seed, amount.Uint64()) withdrawalHash = sha3.Sum256(seed) } @@ -274,15 +286,9 @@ func (ms MsgServer) FinalizeTokenWithdrawal(context context.Context, req *types. ms.RecordProvenWithdrawal(ctx, bridgeId, withdrawalHash) } - // load l1denom from the token pair store - l1Denom, err := ms.GetTokenPair(ctx, bridgeId, l2Denom) - if err != nil { - return nil, err - } - // transfer asset to a user from the bridge account bridgeAddr := types.BridgeAddress(bridgeId) - if err := ms.bankKeeper.SendCoins(ctx, bridgeAddr, receiver, sdk.NewCoins(sdk.NewCoin(l1Denom, amount))); err != nil { + if err := ms.bankKeeper.SendCoins(ctx, bridgeAddr, receiver, sdk.NewCoins(sdk.NewCoin(denom, amount))); err != nil { return nil, err } @@ -293,8 +299,8 @@ func (ms MsgServer) FinalizeTokenWithdrawal(context context.Context, req *types. sdk.NewAttribute(types.AttributeKeyL2Sequence, strconv.FormatUint(l2Sequence, 10)), sdk.NewAttribute(types.AttributeKeyFrom, sender.String()), sdk.NewAttribute(types.AttributeKeyTo, receiver.String()), - sdk.NewAttribute(types.AttributeKeyL1Denom, l1Denom), - sdk.NewAttribute(types.AttributeKeyL2Denom, l2Denom), + sdk.NewAttribute(types.AttributeKeyL1Denom, denom), + sdk.NewAttribute(types.AttributeKeyL2Denom, types.L2Denom(bridgeId, denom)), sdk.NewAttribute(types.AttributeKeyAmount, amount.String()), )) @@ -314,7 +320,10 @@ func (ms MsgServer) UpdateProposer(context context.Context, req *types.MsgUpdate } config.Proposer = req.NewProposer - ms.Keeper.bridgeHook.BridgeProposerUpdated(ctx, bridgeId, config) + if err := ms.Keeper.bridgeHook.BridgeProposerUpdated(ctx, bridgeId, config); err != nil { + return nil, err + } + if err := ms.SetBridgeConfig(ctx, bridgeId, config); err != nil { return nil, err } @@ -335,7 +344,10 @@ func (ms MsgServer) UpdateChallenger(context context.Context, req *types.MsgUpda } config.Challenger = req.NewChallenger - ms.Keeper.bridgeHook.BridgeChallengerUpdated(ctx, bridgeId, config) + if err := ms.Keeper.bridgeHook.BridgeChallengerUpdated(ctx, bridgeId, config); err != nil { + return nil, err + } + if err := ms.SetBridgeConfig(ctx, bridgeId, config); err != nil { return nil, err } diff --git a/x/ophost/keeper/msg_server_test.go b/x/ophost/keeper/msg_server_test.go index 94292649..01383986 100644 --- a/x/ophost/keeper/msg_server_test.go +++ b/x/ophost/keeper/msg_server_test.go @@ -1 +1,291 @@ package keeper_test + +import ( + "encoding/hex" + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/stretchr/testify/require" + + "github.com/initia-labs/OPinit/x/ophost/keeper" + "github.com/initia-labs/OPinit/x/ophost/types" +) + +func Test_RecordBatch(t *testing.T) { + ctx, input := createDefaultTestInput(t) + + ms := keeper.NewMsgServerImpl(input.OPHostKeeper) + _, err := ms.RecordBatch(sdk.WrapSDKContext(ctx), types.NewMsgRecordBatch(addrs[0], 1, []byte{1, 2, 3})) + require.NoError(t, err) +} + +func Test_CreateBridge(t *testing.T) { + ctx, input := createDefaultTestInput(t) + + ms := keeper.NewMsgServerImpl(input.OPHostKeeper) + config := types.BridgeConfig{ + Challenger: addrs[0].String(), + Proposer: addrs[0].String(), + SubmissionInterval: time.Second * 10, + FinalizationPeriod: time.Second * 60, + SubmissionStartTime: time.Now().UTC(), + Metadata: []byte{1, 2, 3}, + } + res, err := ms.CreateBridge(sdk.WrapSDKContext(ctx), types.NewMsgCreateBridge(addrs[0], config)) + require.NoError(t, err) + require.Equal(t, uint64(1), res.BridgeId) + + _config, err := input.OPHostKeeper.GetBridgeConfig(ctx, res.BridgeId) + require.NoError(t, err) + require.Equal(t, config, _config) +} + +func Test_ProposeOutput(t *testing.T) { + ctx, input := createDefaultTestInput(t) + + ms := keeper.NewMsgServerImpl(input.OPHostKeeper) + config := types.BridgeConfig{ + Challenger: addrs[0].String(), + Proposer: addrs[0].String(), + SubmissionInterval: time.Second * 10, + FinalizationPeriod: time.Second * 60, + SubmissionStartTime: time.Now().UTC(), + Metadata: []byte{1, 2, 3}, + } + createRes, err := ms.CreateBridge(sdk.WrapSDKContext(ctx), types.NewMsgCreateBridge(addrs[0], config)) + require.NoError(t, err) + require.Equal(t, uint64(1), createRes.BridgeId) + + blockTime := time.Now().UTC() + ctx = ctx.WithBlockTime(blockTime) + + // unauthorized + _, err = ms.ProposeOutput(sdk.WrapSDKContext(ctx), types.NewMsgProposeOutput(addrs[1], 1, 100, []byte{1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})) + require.Error(t, err) + + // valid + proposeRes, err := ms.ProposeOutput(sdk.WrapSDKContext(ctx), types.NewMsgProposeOutput(addrs[0], 1, 100, []byte{1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})) + require.NoError(t, err) + require.Equal(t, uint64(1), proposeRes.OutputIndex) + + output, err := input.OPHostKeeper.GetOutputProposal(ctx, 1, 1) + require.NoError(t, err) + require.Equal(t, types.Output{ + OutputRoot: []byte{1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + L1BlockTime: blockTime, + L2BlockNumber: 100, + }, output) +} + +func Test_DeleteOutput(t *testing.T) { + ctx, input := createDefaultTestInput(t) + + ms := keeper.NewMsgServerImpl(input.OPHostKeeper) + config := types.BridgeConfig{ + Proposer: addrs[0].String(), + Challenger: addrs[1].String(), + SubmissionInterval: time.Second * 10, + FinalizationPeriod: time.Second * 60, + SubmissionStartTime: time.Now().UTC(), + Metadata: []byte{1, 2, 3}, + } + createReq := types.NewMsgCreateBridge(addrs[0], config) + createRes, err := ms.CreateBridge(sdk.WrapSDKContext(ctx), createReq) + require.NoError(t, err) + require.Equal(t, uint64(1), createRes.BridgeId) + + blockTime := time.Now().UTC() + ctx = ctx.WithBlockTime(blockTime) + + proposeRes, err := ms.ProposeOutput(sdk.WrapSDKContext(ctx), types.NewMsgProposeOutput(addrs[0], 1, 100, []byte{1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})) + require.NoError(t, err) + require.Equal(t, uint64(1), proposeRes.OutputIndex) + + // unauthorized + _, err = ms.DeleteOutput(sdk.WrapSDKContext(ctx), types.NewMsgDeleteOutput(addrs[0], 1, 1)) + require.Error(t, err) + + // valid + _, err = ms.DeleteOutput(sdk.WrapSDKContext(ctx), types.NewMsgDeleteOutput(addrs[1], 1, 1)) + require.NoError(t, err) + + // should return error; deleted + _, err = input.OPHostKeeper.GetOutputProposal(ctx, 1, 1) + require.Error(t, err) +} + +func Test_InitiateTokenDeposit(t *testing.T) { + ctx, input := createDefaultTestInput(t) + + ms := keeper.NewMsgServerImpl(input.OPHostKeeper) + config := types.BridgeConfig{ + Proposer: addrs[0].String(), + Challenger: addrs[1].String(), + SubmissionInterval: time.Second * 10, + FinalizationPeriod: time.Second * 60, + SubmissionStartTime: time.Now().UTC(), + Metadata: []byte{1, 2, 3}, + } + createRes, err := ms.CreateBridge(sdk.WrapSDKContext(ctx), types.NewMsgCreateBridge(addrs[0], config)) + require.NoError(t, err) + require.Equal(t, uint64(1), createRes.BridgeId) + + amount := sdk.NewCoin(baseDenom, sdk.NewInt(100)) + input.Faucet.Fund(ctx, addrs[1], amount) + _, err = ms.InitiateTokenDeposit( + sdk.WrapSDKContext(ctx), + types.NewMsgInitiateTokenDeposit(addrs[1], 1, addrs[2], amount, []byte("messages")), + ) + require.NoError(t, err) + require.True(t, input.BankKeeper.GetBalance(ctx, addrs[1], baseDenom).IsZero()) + require.Equal(t, amount, input.BankKeeper.GetBalance(ctx, types.BridgeAddress(1), baseDenom)) +} + +func Test_FinalizeTokenWithdrawal(t *testing.T) { + ctx, input := createDefaultTestInput(t) + + ms := keeper.NewMsgServerImpl(input.OPHostKeeper) + config := types.BridgeConfig{ + Proposer: addrs[0].String(), + Challenger: addrs[1].String(), + SubmissionInterval: time.Second * 10, + FinalizationPeriod: time.Second * 60, + SubmissionStartTime: time.Now().UTC(), + Metadata: []byte{1, 2, 3}, + } + _, err := ms.CreateBridge(sdk.WrapSDKContext(ctx), types.NewMsgCreateBridge(addrs[0], config)) + require.NoError(t, err) + + // fund amount + amount := sdk.NewCoin("l1denom", sdk.NewInt(3_000_000)) + input.Faucet.Fund(ctx, types.BridgeAddress(1), amount) + + outputRoot := decodeHex(t, "d87b15f515e52e234f5ddca84627128ad842fa6c741d6b85d589a13bbdad3a89") + version := decodeHex(t, "0000000000000000000000000000000000000000000000000000000000000001") + stateRoot := decodeHex(t, "0000000000000000000000000000000000000000000000000000000000000002") + storageRoot := decodeHex(t, "326ca35f4738f837ad9f335349fc71bdecf4c4ed3485fff1763d3bab55efc88a") + blockHash := decodeHex(t, "0000000000000000000000000000000000000000000000000000000000000003") + proofs := [][]byte{ + decodeHex(t, "32e1a72a7c215563f9426bfe267b6fa22ba49b1fba7162d80094dc2f2b6c5a3a"), + decodeHex(t, "627dc2af9ee001b0e119100599dc3923ccdff2c53f06d89f40400edb1e7907e1"), + decodeHex(t, "bafac86e9ebc05a07701c151846c6de7bca68cd315f7a82fffe05fc4301ac47e"), + } + + now := time.Now().UTC() + ctx = ctx.WithBlockTime(now) + _, err = ms.ProposeOutput(sdk.WrapSDKContext(ctx), types.NewMsgProposeOutput(addrs[0], 1, 100, outputRoot)) + require.NoError(t, err) + + ctx = ctx.WithBlockTime(now.Add(time.Second * 60)) + _, err = ms.FinalizeTokenWithdrawal(sdk.WrapSDKContext(ctx), types.NewMsgFinalizeTokenWithdrawal( + 1, 1, 4, proofs, + decodeHex(t, "0000000000000000000000000000000000000004"), + decodeHex(t, "0000000000000000000000000000000000000001"), + amount, + version, stateRoot, storageRoot, blockHash, + )) + require.NoError(t, err) + require.Equal(t, amount, input.BankKeeper.GetBalance(ctx, decodeHex(t, "0000000000000000000000000000000000000001"), amount.Denom)) +} + +func decodeHex(t *testing.T, str string) []byte { + bz, err := hex.DecodeString(str) + require.NoError(t, err) + + return bz +} + +func Test_UpdateProposal(t *testing.T) { + ctx, input := createDefaultTestInput(t) + ms := keeper.NewMsgServerImpl(input.OPHostKeeper) + + config := types.BridgeConfig{ + Proposer: addrs[0].String(), + Challenger: addrs[1].String(), + SubmissionInterval: time.Second * 10, + FinalizationPeriod: time.Second * 60, + SubmissionStartTime: time.Now().UTC(), + Metadata: []byte{1, 2, 3}, + } + + _, err := ms.CreateBridge(sdk.WrapSDKContext(ctx), types.NewMsgCreateBridge(addrs[0], config)) + require.NoError(t, err) + + msg := types.NewMsgUpdateProposer(authtypes.NewModuleAddress("gov"), 1, addrs[1]) + _, err = ms.UpdateProposer(sdk.WrapSDKContext(ctx), msg) + require.NoError(t, err) + _config, err := ms.GetBridgeConfig(ctx, 1) + require.NoError(t, err) + require.Equal(t, addrs[1].String(), _config.Proposer) + require.Equal(t, addrs[1].String(), input.BridgeHook.proposer) + + // invalid signer + msg = types.NewMsgUpdateProposer(authtypes.NewModuleAddress(types.ModuleName), 1, addrs[1]) + require.NoError(t, err) + + _, err = ms.UpdateProposer( + sdk.WrapSDKContext(ctx), + msg, + ) + require.Error(t, err) +} + +func Test_UpdateChallenger(t *testing.T) { + ctx, input := createDefaultTestInput(t) + ms := keeper.NewMsgServerImpl(input.OPHostKeeper) + + config := types.BridgeConfig{ + Proposer: addrs[0].String(), + Challenger: addrs[1].String(), + SubmissionInterval: time.Second * 10, + FinalizationPeriod: time.Second * 60, + SubmissionStartTime: time.Now().UTC(), + Metadata: []byte{1, 2, 3}, + } + + _, err := ms.CreateBridge(sdk.WrapSDKContext(ctx), types.NewMsgCreateBridge(addrs[0], config)) + require.NoError(t, err) + + msg := types.NewMsgUpdateChallenger(authtypes.NewModuleAddress("gov"), 1, addrs[2]) + _, err = ms.UpdateChallenger(sdk.WrapSDKContext(ctx), msg) + require.NoError(t, err) + _config, err := ms.GetBridgeConfig(ctx, 1) + require.NoError(t, err) + require.Equal(t, addrs[2].String(), _config.Challenger) + require.Equal(t, addrs[2].String(), input.BridgeHook.challenger) + + // invalid signer + msg = types.NewMsgUpdateChallenger(authtypes.NewModuleAddress(types.ModuleName), 1, addrs[1]) + require.NoError(t, err) + + _, err = ms.UpdateChallenger( + sdk.WrapSDKContext(ctx), + msg, + ) + require.Error(t, err) +} + +func Test_MsgServer_UpdateParams(t *testing.T) { + ctx, input := createDefaultTestInput(t) + ms := keeper.NewMsgServerImpl(input.OPHostKeeper) + + params := ms.GetParams(ctx) + params.RegistrationFee = sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(100))) + + msg := types.NewMsgUpdateParams(authtypes.NewModuleAddress("gov"), ¶ms) + _, err := ms.UpdateParams(sdk.WrapSDKContext(ctx), msg) + require.NoError(t, err) + require.Equal(t, params, ms.GetParams(ctx)) + + // invalid signer + msg = types.NewMsgUpdateParams(authtypes.NewModuleAddress(types.ModuleName), ¶ms) + require.NoError(t, err) + + _, err = ms.UpdateParams( + sdk.WrapSDKContext(ctx), + msg, + ) + require.Error(t, err) +} diff --git a/x/ophost/keeper/params_test.go b/x/ophost/keeper/params_test.go new file mode 100644 index 00000000..78993ff6 --- /dev/null +++ b/x/ophost/keeper/params_test.go @@ -0,0 +1,19 @@ +package keeper_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +func Test_Params(t *testing.T) { + ctx, input := createDefaultTestInput(t) + + params := input.OPHostKeeper.GetParams(ctx) + params.RegistrationFee = sdk.NewCoins() + + input.OPHostKeeper.SetParams(ctx, params) + + require.True(t, input.OPHostKeeper.RegistrationFee(ctx).IsZero()) +} diff --git a/x/ophost/keeper/querier_test.go b/x/ophost/keeper/querier_test.go new file mode 100644 index 00000000..4b6ca35b --- /dev/null +++ b/x/ophost/keeper/querier_test.go @@ -0,0 +1,177 @@ +package keeper_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/initia-labs/OPinit/x/ophost/keeper" + "github.com/initia-labs/OPinit/x/ophost/types" +) + +func Test_QueryBridge(t *testing.T) { + ctx, input := createDefaultTestInput(t) + config := types.BridgeConfig{ + Challenger: addrs[0].String(), + Proposer: addrs[0].String(), + SubmissionInterval: time.Second * 10, + FinalizationPeriod: time.Second * 60, + SubmissionStartTime: time.Now().UTC(), + Metadata: []byte{1, 2, 3}, + } + err := input.OPHostKeeper.SetBridgeConfig(ctx, 1, config) + require.NoError(t, err) + + q := keeper.NewQuerier(input.OPHostKeeper) + res, err := q.Bridge(sdk.WrapSDKContext(ctx), &types.QueryBridgeRequest{ + BridgeId: 1, + }) + + require.NoError(t, err) + require.Equal(t, types.QueryBridgeResponse{ + BridgeId: 1, + BridgeAddr: types.BridgeAddress(1).String(), + BridgeConfig: config, + }, *res) +} + +func Test_QueryBridges(t *testing.T) { + ctx, input := createDefaultTestInput(t) + config1 := types.BridgeConfig{ + Challenger: addrs[0].String(), + Proposer: addrs[0].String(), + SubmissionInterval: time.Second * 10, + FinalizationPeriod: time.Second * 60, + SubmissionStartTime: time.Now().UTC(), + Metadata: []byte{1, 2, 3}, + } + config2 := types.BridgeConfig{ + Challenger: addrs[1].String(), + Proposer: addrs[0].String(), + SubmissionInterval: time.Second * 10, + FinalizationPeriod: time.Second * 60, + SubmissionStartTime: time.Now().UTC(), + Metadata: []byte{3, 4, 5}, + } + require.NoError(t, input.OPHostKeeper.SetBridgeConfig(ctx, 1, config1)) + require.NoError(t, input.OPHostKeeper.SetBridgeConfig(ctx, 2, config2)) + + q := keeper.NewQuerier(input.OPHostKeeper) + res, err := q.Bridges(sdk.WrapSDKContext(ctx), &types.QueryBridgesRequest{}) + + require.NoError(t, err) + require.Equal(t, []types.QueryBridgeResponse{ + { + BridgeId: 1, + BridgeAddr: types.BridgeAddress(1).String(), + BridgeConfig: config1, + }, { + BridgeId: 2, + BridgeAddr: types.BridgeAddress(2).String(), + BridgeConfig: config2, + }, + }, res.Bridges) +} + +func Test_QueryTokenPair(t *testing.T) { + ctx, input := createDefaultTestInput(t) + pair := types.TokenPair{ + L1Denom: "l1denom", + L2Denom: types.L2Denom(1, "l1denom"), + } + input.OPHostKeeper.SetTokenPair(ctx, 1, pair.L2Denom, pair.L1Denom) + + q := keeper.NewQuerier(input.OPHostKeeper) + res, err := q.TokenPairByL1Denom(sdk.WrapSDKContext(ctx), &types.QueryTokenPairByL1DenomRequest{ + BridgeId: 1, + L1Denom: pair.L1Denom, + }) + require.NoError(t, err) + require.Equal(t, pair, res.TokenPair) + + res2, err := q.TokenPairByL2Denom(sdk.WrapSDKContext(ctx), &types.QueryTokenPairByL2DenomRequest{ + BridgeId: 1, + L2Denom: pair.L2Denom, + }) + require.NoError(t, err) + require.Equal(t, pair, res2.TokenPair) +} + +func Test_QueryTokenPairs(t *testing.T) { + ctx, input := createDefaultTestInput(t) + pair1 := types.TokenPair{ + L1Denom: "l1denom1", + L2Denom: types.L2Denom(1, "l1denom1"), + } + pair2 := types.TokenPair{ + L1Denom: "l1denom2", + L2Denom: types.L2Denom(1, "l1denom2"), + } + input.OPHostKeeper.SetTokenPair(ctx, 1, pair1.L2Denom, pair1.L1Denom) + input.OPHostKeeper.SetTokenPair(ctx, 1, pair2.L2Denom, pair2.L1Denom) + + q := keeper.NewQuerier(input.OPHostKeeper) + res, err := q.TokenPairs(sdk.WrapSDKContext(ctx), &types.QueryTokenPairsRequest{ + BridgeId: 1, + }) + + require.NoError(t, err) + require.Equal(t, []types.TokenPair{ + pair1, pair2, + }, res.TokenPairs) +} + +func Test_QueryOutputProposal(t *testing.T) { + ctx, input := createDefaultTestInput(t) + output := types.Output{ + OutputRoot: []byte{1, 2, 3}, + L1BlockTime: time.Now().UTC(), + L2BlockNumber: 100, + } + require.NoError(t, input.OPHostKeeper.SetOutputProposal(ctx, 1, 1, output)) + + q := keeper.NewQuerier(input.OPHostKeeper) + res, err := q.OutputProposal(sdk.WrapSDKContext(ctx), &types.QueryOutputProposalRequest{ + BridgeId: 1, + OutputIndex: 1, + }) + require.NoError(t, err) + require.Equal(t, output, res.OutputProposal) +} + +func Test_QueryOutputProposals(t *testing.T) { + ctx, input := createDefaultTestInput(t) + output1 := types.Output{ + OutputRoot: []byte{1, 2, 3}, + L1BlockTime: time.Now().UTC(), + L2BlockNumber: 100, + } + output2 := types.Output{ + OutputRoot: []byte{3, 4, 5}, + L1BlockTime: time.Now().UTC(), + L2BlockNumber: 100, + } + require.NoError(t, input.OPHostKeeper.SetOutputProposal(ctx, 1, 1, output1)) + require.NoError(t, input.OPHostKeeper.SetOutputProposal(ctx, 1, 2, output2)) + + q := keeper.NewQuerier(input.OPHostKeeper) + res, err := q.OutputProposals(sdk.WrapSDKContext(ctx), &types.QueryOutputProposalsRequest{ + BridgeId: 1, + }) + + require.NoError(t, err) + require.Equal(t, []types.QueryOutputProposalResponse{ + { + BridgeId: 1, + OutputIndex: 1, + OutputProposal: output1, + }, { + BridgeId: 1, + OutputIndex: 2, + OutputProposal: output2, + }, + }, res.OutputProposals) +} diff --git a/x/ophost/keeper/token_pair_test.go b/x/ophost/keeper/token_pair_test.go index 5c38223a..5d0ffeb2 100644 --- a/x/ophost/keeper/token_pair_test.go +++ b/x/ophost/keeper/token_pair_test.go @@ -19,3 +19,33 @@ func Test_TokenPair(t *testing.T) { require.Equal(t, tokenPair.L1Denom, l1Denom) require.NoError(t, err) } + +func Test_IterateTokenPair(t *testing.T) { + ctx, input := createDefaultTestInput(t) + + tokenPair1 := types.TokenPair{ + L1Denom: "l11_denom", + L2Denom: "l12_denom", + } + tokenPair2 := types.TokenPair{ + L1Denom: "l21_denom", + L2Denom: "l22_denom", + } + tokenPair3 := types.TokenPair{ + L1Denom: "l31_denom", + L2Denom: "l32_denom", + } + input.OPHostKeeper.SetTokenPair(ctx, 1, tokenPair1.L2Denom, tokenPair1.L1Denom) + input.OPHostKeeper.SetTokenPair(ctx, 1, tokenPair2.L2Denom, tokenPair2.L1Denom) + input.OPHostKeeper.SetTokenPair(ctx, 2, tokenPair3.L2Denom, tokenPair3.L1Denom) + + input.OPHostKeeper.IterateTokenPair(ctx, 1, func(bridgeId uint64, tokenPair types.TokenPair) bool { + require.Equal(t, bridgeId, uint64(1)) + if tokenPair.L1Denom == tokenPair1.L1Denom { + require.Equal(t, tokenPair1, tokenPair) + } else { + require.Equal(t, tokenPair2, tokenPair) + } + return false + }) +} diff --git a/x/ophost/module.go b/x/ophost/module.go index c812b5cf..87dc03d1 100644 --- a/x/ophost/module.go +++ b/x/ophost/module.go @@ -15,6 +15,7 @@ import ( "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/spf13/cobra" + "github.com/initia-labs/OPinit/x/ophost/client/cli" "github.com/initia-labs/OPinit/x/ophost/keeper" "github.com/initia-labs/OPinit/x/ophost/types" ) @@ -64,12 +65,12 @@ func (b AppModuleBasic) ValidateGenesis(marshaler codec.JSONCodec, config client // GetTxCmd returns the root tx command for the move module. func (b AppModuleBasic) GetTxCmd() *cobra.Command { - return nil + return cli.GetTxCmd() } // GetQueryCmd returns no root query command for the move module. func (b AppModuleBasic) GetQueryCmd() *cobra.Command { - return nil + return cli.GetQueryCmd() } // RegisterInterfaces implements InterfaceModule diff --git a/x/ophost/types/codec.go b/x/ophost/types/codec.go index 74af11b9..0c7f26eb 100644 --- a/x/ophost/types/codec.go +++ b/x/ophost/types/codec.go @@ -8,6 +8,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/msgservice" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + authzcodec "github.com/cosmos/cosmos-sdk/x/authz/codec" + govcodec "github.com/cosmos/cosmos-sdk/x/gov/codec" + groupcodec "github.com/cosmos/cosmos-sdk/x/group/codec" ) // RegisterLegacyAminoCodec registers the move types and interface @@ -23,7 +26,7 @@ func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { legacy.RegisterAminoMsg(cdc, &MsgUpdateParams{}, "ophost/MsgUpdateParams") cdc.RegisterConcrete(Params{}, "ophost/Params", nil) - cdc.RegisterConcrete(&BridgeAccount{}, "move/BridgeAccount", nil) + cdc.RegisterConcrete(&BridgeAccount{}, "ophost/BridgeAccount", nil) } // RegisterInterfaces registers the x/market interfaces types with the interface registry @@ -62,4 +65,10 @@ func init() { RegisterLegacyAminoCodec(amino) cryptocodec.RegisterCrypto(amino) sdk.RegisterLegacyAminoCodec(amino) + + // Register all Amino interfaces and concrete types on the authz and gov Amino codec so that this can later be + // used to properly serialize MsgGrant, MsgExec and MsgSubmitProposal instances + RegisterLegacyAminoCodec(authzcodec.Amino) + RegisterLegacyAminoCodec(govcodec.Amino) + RegisterLegacyAminoCodec(groupcodec.Amino) } diff --git a/x/ophost/types/denom.go b/x/ophost/types/denom.go index fdb96c2b..20d4a889 100644 --- a/x/ophost/types/denom.go +++ b/x/ophost/types/denom.go @@ -11,7 +11,7 @@ const L2_DENOM_PREFIX = "l2/" func L2Denom(bridgeId uint64, l1Denom string) string { var bz []byte - binary.BigEndian.AppendUint64(bz, bridgeId) + bz = binary.BigEndian.AppendUint64(bz, bridgeId) bz = append(bz, []byte(l1Denom)...) hash := sha3.Sum256(bz) diff --git a/x/ophost/types/error.go b/x/ophost/types/error.go index 4a84d6d9..fcc93b3f 100644 --- a/x/ophost/types/error.go +++ b/x/ophost/types/error.go @@ -11,7 +11,7 @@ var ( ErrInvalidOutputIndex = errorsmod.Register(ModuleName, 4, "invalid output index") ErrInvalidAmount = errorsmod.Register(ModuleName, 5, "invalid bridge amount") ErrInvalidSequence = errorsmod.Register(ModuleName, 6, "invalid sequence") - ErrSubmissionInterval = errorsmod.Register(ModuleName, 7, "submission interval has not passed") + ErrInvalidL2BlockNumber = errorsmod.Register(ModuleName, 7, "invalid l2 block number") ErrNotFinalized = errorsmod.Register(ModuleName, 8, "output has not finalized") ErrFailedToVerifyWithdrawal = errorsmod.Register(ModuleName, 9, "failed to verify withdrawal tx") ErrWithdrawalAlreadyFinalized = errorsmod.Register(ModuleName, 10, "withdrawal already finalized") diff --git a/x/ophost/types/tx.go b/x/ophost/types/tx.go index 0d79932e..235defac 100644 --- a/x/ophost/types/tx.go +++ b/x/ophost/types/tx.go @@ -47,8 +47,8 @@ func NewMsgRecordBatch( submitter sdk.AccAddress, bridgeId uint64, batchBytes []byte, -) MsgRecordBatch { - return MsgRecordBatch{ +) *MsgRecordBatch { + return &MsgRecordBatch{ Submitter: submitter.String(), BridgeId: bridgeId, BatchBytes: batchBytes, @@ -71,6 +71,10 @@ func (msg MsgRecordBatch) ValidateBasic() error { return err } + if msg.BridgeId == 0 { + return ErrInvalidBridgeId + } + return nil } @@ -95,8 +99,8 @@ func (msg MsgRecordBatch) GetSigners() []sdk.AccAddress { func NewMsgCreateBridge( creator sdk.AccAddress, config BridgeConfig, -) MsgCreateBridge { - return MsgCreateBridge{ +) *MsgCreateBridge { + return &MsgCreateBridge{ Creator: creator.String(), Config: config, } @@ -149,8 +153,8 @@ func NewMsgProposeOutput( bridgeId uint64, l2BlockNumber uint64, outputRoot []byte, -) MsgProposeOutput { - return MsgProposeOutput{ +) *MsgProposeOutput { + return &MsgProposeOutput{ Proposer: proposer.String(), BridgeId: bridgeId, L2BlockNumber: l2BlockNumber, @@ -206,8 +210,8 @@ func NewMsgDeleteOutput( challenger sdk.AccAddress, bridgeId uint64, outputIndex uint64, -) MsgDeleteOutput { - return MsgDeleteOutput{ +) *MsgDeleteOutput { + return &MsgDeleteOutput{ Challenger: challenger.String(), BridgeId: bridgeId, OutputIndex: outputIndex, @@ -327,17 +331,17 @@ func (msg MsgInitiateTokenDeposit) GetSigners() []sdk.AccAddress { func NewMsgFinalizeTokenWithdrawal( bridgeId uint64, outputIndex uint64, + sequence uint64, withdrawalProofs [][]byte, sender sdk.AccAddress, receiver sdk.AccAddress, - sequence uint64, amount sdk.Coin, version []byte, stateRoot []byte, storageRoot []byte, latestBlockHash []byte, -) MsgFinalizeTokenWithdrawal { - return MsgFinalizeTokenWithdrawal{ +) *MsgFinalizeTokenWithdrawal { + return &MsgFinalizeTokenWithdrawal{ BridgeId: bridgeId, OutputIndex: outputIndex, WithdrawalProofs: withdrawalProofs, @@ -435,8 +439,8 @@ func NewMsgUpdateProposer( authority sdk.AccAddress, bridgeId uint64, newProposer sdk.AccAddress, -) MsgUpdateProposer { - return MsgUpdateProposer{ +) *MsgUpdateProposer { + return &MsgUpdateProposer{ Authority: authority.String(), BridgeId: bridgeId, NewProposer: newProposer.String(), @@ -492,8 +496,8 @@ func NewMsgUpdateChallenger( authority sdk.AccAddress, bridgeId uint64, newChallenger sdk.AccAddress, -) MsgUpdateChallenger { - return MsgUpdateChallenger{ +) *MsgUpdateChallenger { + return &MsgUpdateChallenger{ Authority: authority.String(), BridgeId: bridgeId, NewChallenger: newChallenger.String(),