From cef97e0fa74c95747033b91fd1e662dc74f73bbe Mon Sep 17 00:00:00 2001 From: Evan Gray Date: Thu, 10 Jul 2025 16:39:29 -0400 Subject: [PATCH 1/2] manual e2e test --- .github/workflows/foundry.yml | 41 +++++++ .gitmodules | 9 ++ .prettierignore | 1 + README.md | 33 ++++++ Tiltfile | 12 +- bun.lock | 23 ++++ evm/.gitignore | 14 +++ evm/README.md | 66 +++++++++++ evm/foundry.toml | 6 + evm/lib/example-messaging-executor | 1 + evm/lib/forge-std | 1 + evm/lib/wormhole-solidity-sdk | 1 + evm/src/ExecutorVAAv1Integration.sol | 73 ++++++++++++ package.json | 1 + src/api/status.ts | 3 - src/consts.ts | 18 +-- src/e2e.test.ts | 165 +++++++++++++++++++++++++++ src/mockGuardian.ts | 4 +- src/relay/platform/evm.ts | 41 ++----- src/relay/queue.ts | 1 + src/relay/relayer.ts | 32 +++++- tsconfig.json | 3 +- 22 files changed, 491 insertions(+), 58 deletions(-) create mode 100644 .github/workflows/foundry.yml create mode 100644 .gitmodules create mode 100644 .prettierignore create mode 100644 evm/.gitignore create mode 100644 evm/README.md create mode 100644 evm/foundry.toml create mode 160000 evm/lib/example-messaging-executor create mode 160000 evm/lib/forge-std create mode 160000 evm/lib/wormhole-solidity-sdk create mode 100644 evm/src/ExecutorVAAv1Integration.sol create mode 100644 src/e2e.test.ts diff --git a/.github/workflows/foundry.yml b/.github/workflows/foundry.yml new file mode 100644 index 0000000..fa0f16a --- /dev/null +++ b/.github/workflows/foundry.yml @@ -0,0 +1,41 @@ +name: foundry + +on: + pull_request: + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + name: Foundry project + runs-on: ubuntu-latest + defaults: + run: + working-directory: evm + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Show Forge version + run: | + forge --version + + - name: Run Forge fmt + run: | + forge fmt --check + id: fmt + + - name: Run Forge build + run: | + forge build --sizes + id: build + + # - name: Run Forge tests + # run: | + # forge test -vvv + # id: test diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..9ff7e9d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "evm/lib/forge-std"] + path = evm/lib/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "evm/lib/example-messaging-executor"] + path = evm/lib/example-messaging-executor + url = https://github.com/wormholelabs-xyz/example-messaging-executor +[submodule "evm/lib/wormhole-solidity-sdk"] + path = evm/lib/wormhole-solidity-sdk + url = https://github.com/wormhole-foundation/wormhole-solidity-sdk diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..b681052 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +evm diff --git a/README.md b/README.md index 7780621..8189082 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,36 @@ implied. See the License for the specific language governing permissions and lim spoken - this is a very complex piece of software which targets a bleeding-edge, experimental smart contract runtime. Mistakes happen, and no matter how hard you try and whether you pay someone to audit it, it may eat your tokens, set your printer on fire or startle your cat. Cryptocurrencies are a high-risk investment, no matter how fancy. + +## Testing Flow + +> 🚧 This is a work in progress! + +First tilt up. + +```bash +tilt up +``` + +Next, deploy integration contracts to each chain. + +```bash +forge create ExecutorVAAv1Integration -r http://localhost:8545 --broadcast --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --constructor-args 0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78 0xD0fb39f5a3361F21457653cB70F9D0C9bD86B66B 200 +forge create ExecutorVAAv1Integration -r http://localhost:8546 --broadcast --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --constructor-args 0x79A1027a6A159502049F10906D333EC57E95F083 0x51B47D493CBA7aB97e3F8F163D6Ce07592CE4482 200 +``` + +Finally, bun test + +```bash +bun test +``` + +Click the link! It should be `pending` at first and then after a couple seconds, refresh and it should be `submitted`! + +Next steps + +- Support NTT v1 +- Publish the executor docker image +- Support passing the executor chain config from env or command line so that someone using the docker image can configure the chain info +- Deploy the contracts within the e2e test and actually confirm the messages send both ways and update the contract number. +- Trigger the e2e test in CI diff --git a/Tiltfile b/Tiltfile index d908616..8aeef51 100644 --- a/Tiltfile +++ b/Tiltfile @@ -6,16 +6,12 @@ k8s_yaml("k8s/anvil-eth-sepolia.yaml") k8s_resource( "anvil-eth-sepolia", - port_forwards = [ - port_forward(8545, name = "RPC [:8545]"), - ], + port_forwards = 8545, labels = ["anvil"], ) k8s_resource( "anvil-base-sepolia", - port_forwards = [ - port_forward(8546, 8545, name = "RPC [:8546]"), - ], + port_forwards = '8546:8545', labels = ["anvil"], ) @@ -32,9 +28,7 @@ docker_build( k8s_yaml("k8s/executor.yaml") k8s_resource( "executor", - port_forwards = [ - port_forward(3000, name = "Executor [:3000]"), - ], + port_forwards = 3000, resource_deps = ["anvil-eth-sepolia", "anvil-base-sepolia"], labels = ["app"], ) diff --git a/bun.lock b/bun.lock index aa1fe9c..ebf5607 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "@types/express": "^5.0.3", "@wormhole-foundation/sdk-base": "^2.4.0", "@wormhole-foundation/sdk-definitions": "^2.4.0", + "axios": "^1.10.0", "binary-layout": "^1.3.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -73,6 +74,10 @@ "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "axios": ["axios@1.10.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw=="], + "binary-layout": ["binary-layout@1.3.0", "", {}, "sha512-jDJ6rLgjjQ9q8NP5eIumdvsegbbMsNplJ7GHMuVnMWi0Qw59o8kIOw+ew4fLAryPL3LgIp5KrjfdAMoSpmpO8w=="], "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], @@ -87,6 +92,8 @@ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], @@ -101,6 +108,8 @@ "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], @@ -115,6 +124,8 @@ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], @@ -125,6 +136,10 @@ "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], + "follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="], + + "form-data": ["form-data@4.0.3", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], @@ -139,6 +154,8 @@ "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], @@ -187,6 +204,8 @@ "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], @@ -235,6 +254,10 @@ "ws": ["ws@8.18.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ=="], + "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], } } diff --git a/evm/.gitignore b/evm/.gitignore new file mode 100644 index 0000000..85198aa --- /dev/null +++ b/evm/.gitignore @@ -0,0 +1,14 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/evm/README.md b/evm/README.md new file mode 100644 index 0000000..9265b45 --- /dev/null +++ b/evm/README.md @@ -0,0 +1,66 @@ +## Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +## Documentation + +https://book.getfoundry.sh/ + +## Usage + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/evm/foundry.toml b/evm/foundry.toml new file mode 100644 index 0000000..25b918f --- /dev/null +++ b/evm/foundry.toml @@ -0,0 +1,6 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/evm/lib/example-messaging-executor b/evm/lib/example-messaging-executor new file mode 160000 index 0000000..a875ce6 --- /dev/null +++ b/evm/lib/example-messaging-executor @@ -0,0 +1 @@ +Subproject commit a875ce6afdc624477914988928092cc3102dc7fd diff --git a/evm/lib/forge-std b/evm/lib/forge-std new file mode 160000 index 0000000..77041d2 --- /dev/null +++ b/evm/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 77041d2ce690e692d6e03cc812b57d1ddaa4d505 diff --git a/evm/lib/wormhole-solidity-sdk b/evm/lib/wormhole-solidity-sdk new file mode 160000 index 0000000..b9e129e --- /dev/null +++ b/evm/lib/wormhole-solidity-sdk @@ -0,0 +1 @@ +Subproject commit b9e129e65d34827d92fceeed8c87d3ecdfc801d0 diff --git a/evm/src/ExecutorVAAv1Integration.sol b/evm/src/ExecutorVAAv1Integration.sol new file mode 100644 index 0000000..3228255 --- /dev/null +++ b/evm/src/ExecutorVAAv1Integration.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "wormhole-solidity-sdk/interfaces/IWormhole.sol"; +import "example-messaging-executor/evm/src/interfaces/IExecutor.sol"; +import "example-messaging-executor/evm/src/interfaces/IVaaV1Receiver.sol"; +import "example-messaging-executor/evm/src/libraries/ExecutorMessages.sol"; +import {toWormholeFormat} from "wormhole-solidity-sdk/Utils.sol"; + +struct ExecutorArgs { + // The refund address used by the Executor. + address refundAddress; + // The signed quote to be passed into the Executor. + bytes signedQuote; + // The relay instructions to be passed into the Executor. + bytes instructions; +} + +/// This contract is for testing purposes only and is missing several security checks a real integration might do +/// Integrators are encouraged to follow the integration guide at +/// https://wormholelabs.notion.site/Executor-Integration-Notes-Public-1bd3029e88cb804e8281ec19e3264c3b +contract ExecutorVAAv1Integration is IVaaV1Receiver { + bytes32 public immutable emitterAddress; + uint16 public immutable ourChain; + uint8 public immutable wormholeFinality; + IWormhole public immutable wormhole; + IExecutor public immutable executor; + + uint256 public number; + + error InvalidVaa(string reason); + + constructor(address _wormhole, address _executor, uint8 _wormholeFinality) { + assert(_wormhole != address(0)); + assert(_executor != address(0)); + + wormhole = IWormhole(_wormhole); + executor = IExecutor(_executor); + + ourChain = wormhole.chainId(); + emitterAddress = toWormholeFormat(address(this)); + wormholeFinality = _wormholeFinality; + } + + function incrementAndSend(uint16 destinationChain, bytes32 destinationAddress, ExecutorArgs calldata executorArgs) + public + payable + { + number++; + uint256 wormholeFee = wormhole.messageFee(); + require(msg.value >= wormholeFee, "insufficient value"); + uint256 executionAmount = msg.value - wormholeFee; + + uint64 sequence = wormhole.publishMessage{value: wormholeFee}(0, abi.encodePacked(number), wormholeFinality); + + executor.requestExecution{value: executionAmount}( + destinationChain, + destinationAddress, // a real integration would likely send to its peer + executorArgs.refundAddress, + executorArgs.signedQuote, + ExecutorMessages.makeVAAv1Request(ourChain, emitterAddress, sequence), + executorArgs.instructions + ); + } + + function executeVAAv1(bytes memory vaa) public payable { + (IWormhole.VM memory vm, bool valid, string memory reason) = wormhole.parseAndVerifyVM(vaa); + if (!valid) { + revert InvalidVaa(reason); + } + (number) = abi.decode(vm.payload, (uint256)); + } +} diff --git a/package.json b/package.json index 58336d4..8c4b0da 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@types/express": "^5.0.3", "@wormhole-foundation/sdk-base": "^2.4.0", "@wormhole-foundation/sdk-definitions": "^2.4.0", + "axios": "^1.10.0", "binary-layout": "^1.3.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/src/api/status.ts b/src/api/status.ts index d3169ec..0da62b3 100644 --- a/src/api/status.ts +++ b/src/api/status.ts @@ -201,9 +201,6 @@ async function verifySignedQuote(signedQuote: SignedQuote): Promise { `Bad quoterAddress. Expected: ${QUOTER_PUBLIC_KEY}, Received: ${signedQuote.quote.quoterAddress}`, ); } - if (!isHex(signedQuote.signature)) { - throw new Error(`Bad signature`); - } const recoveredPublicKey = await recoverAddress({ hash: keccak256(serialize(quoteLayout, signedQuote)), signature: signedQuote.signature, diff --git a/src/consts.ts b/src/consts.ts index ed267cc..59c3644 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -1,18 +1,18 @@ import { padHex, toHex } from "viem"; import { mnemonicToAccount } from "viem/accounts"; -const account = mnemonicToAccount( - "test test test test test test test test test test test junk", - { addressIndex: 9 }, -); +export const ANVIL_MNEMONIC = + "test test test test test test test test test test test junk"; + +const account9 = mnemonicToAccount(ANVIL_MNEMONIC, { addressIndex: 9 }); -export const PAYEE_PUBLIC_KEY = account.address; +export const PAYEE_PUBLIC_KEY = "0xf7122c001b3e07d7fafd8be3670545135859954a"; -export const EVM_PUBLIC_KEY = account.address; -export const EVM_PRIVATE_KEY = toHex(account.getHdKey().privateKey || "0x"); +export const EVM_PUBLIC_KEY = account9.address; +export const EVM_PRIVATE_KEY = toHex(account9.getHdKey().privateKey || "0x"); -export const QUOTER_PUBLIC_KEY = account.address; -export const QUOTER_PRIVATE_KEY = toHex(account.getHdKey().privateKey || "0x"); +export const QUOTER_PUBLIC_KEY = account9.address; +export const QUOTER_PRIVATE_KEY = toHex(account9.getHdKey().privateKey || "0x"); export const EMPTY_ADDRESS = padHex( "0x0000000000000000000000000000000000000000000000000000000000000000", diff --git a/src/e2e.test.ts b/src/e2e.test.ts new file mode 100644 index 0000000..ddf18bd --- /dev/null +++ b/src/e2e.test.ts @@ -0,0 +1,165 @@ +import { mnemonicToAccount } from "viem/accounts"; +import { ANVIL_MNEMONIC } from "./consts"; +import { test } from "bun:test"; +import { + createPublicClient, + createWalletClient, + getContract, + http, + padHex, + toHex, +} from "viem"; +import { anvil, sepolia } from "viem/chains"; +import { serializeLayout } from "@wormhole-foundation/sdk-base"; +import { + quoteLayout, + relayInstructionsLayout, + signedQuoteLayout, +} from "@wormhole-foundation/sdk-definitions"; +import axios from "axios"; +import { deserialize } from "binary-layout"; + +const ABI = [ + { + type: "constructor", + inputs: [ + { name: "_wormhole", type: "address", internalType: "address" }, + { name: "_executor", type: "address", internalType: "address" }, + { + name: "_wormholeFinality", + type: "uint8", + internalType: "uint8", + }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "emitterAddress", + inputs: [], + outputs: [{ name: "", type: "bytes32", internalType: "bytes32" }], + stateMutability: "view", + }, + { + type: "function", + name: "executor", + inputs: [], + outputs: [ + { name: "", type: "address", internalType: "contract IExecutor" }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "incrementAndSend", + inputs: [ + { + name: "destinationChain", + type: "uint16", + internalType: "uint16", + }, + { + name: "destinationAddress", + type: "bytes32", + internalType: "bytes32", + }, + { + name: "executorArgs", + type: "tuple", + internalType: "struct ExecutorArgs", + components: [ + { + name: "refundAddress", + type: "address", + internalType: "address", + }, + { name: "signedQuote", type: "bytes", internalType: "bytes" }, + { name: "instructions", type: "bytes", internalType: "bytes" }, + ], + }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "number", + inputs: [], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "ourChain", + inputs: [], + outputs: [{ name: "", type: "uint16", internalType: "uint16" }], + stateMutability: "view", + }, + { + type: "function", + name: "wormhole", + inputs: [], + outputs: [ + { name: "", type: "address", internalType: "contract IWormhole" }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "wormholeFinality", + inputs: [], + outputs: [{ name: "", type: "uint8", internalType: "uint8" }], + stateMutability: "view", + }, +] as const; + +test("it performs a VAA v1 relay", async () => { + const relayInstructions = toHex( + serializeLayout(relayInstructionsLayout, { + requests: [ + { + request: { + type: "GasInstruction", + gasLimit: 250000n, + msgValue: 0n, + }, + }, + ], + }), + ); + const response = await axios.post("http://localhost:3000/v0/quote", { + srcChain: 10002, + dstChain: 10004, + relayInstructions, + }); + const transport = http("http://localhost:8545"); + const account = mnemonicToAccount(ANVIL_MNEMONIC, { addressIndex: 0 }); + const client = createWalletClient({ + account, + chain: sepolia, + transport, + }); + const testContract = getContract({ + address: "0x8e98Bd10a6f4c1Ee0C4b5d9F50a18D1a7E20EaF8", + abi: ABI, + client, + }); + const tx = await testContract.write.incrementAndSend( + [ + 10004, + padHex("0x7d77360666066967579a2235332d271587cd62dC", { + dir: "left", + size: 32, + }), + { + instructions: relayInstructions, + refundAddress: account.address, + signedQuote: response.data.signedQuote, + }, + ], + { value: BigInt(response.data.estimatedCost) }, + ); + console.log( + `https://wormholelabs-xyz.github.io/executor-explorer/#/chain/10002/tx/${tx}?endpoint=http%3A%2F%2Flocalhost%3A3000&env=Testnet`, + ); +}); diff --git a/src/mockGuardian.ts b/src/mockGuardian.ts index 13be5a0..abdcd94 100644 --- a/src/mockGuardian.ts +++ b/src/mockGuardian.ts @@ -87,7 +87,9 @@ export async function mockWormhole( ): Promise { const vaa = await getWormholeMessage(rpc, txHash, coreContractAddress); if (vaa) { - const guardianSet = new mocks.MockGuardians(0, [EVM_PRIVATE_KEY]); + const guardianSet = new mocks.MockGuardians(0, [ + EVM_PRIVATE_KEY.substring(2), + ]); const signedVaa = guardianSet.addSignatures(vaa); const base64 = Buffer.from(serialize(signedVaa)).toString("base64"); return base64; diff --git a/src/relay/platform/evm.ts b/src/relay/platform/evm.ts index d1ea7a9..1c90454 100644 --- a/src/relay/platform/evm.ts +++ b/src/relay/platform/evm.ts @@ -1,3 +1,8 @@ +import { + relayInstructionsLayout, + type RelayInstructions, +} from "@wormhole-foundation/sdk-definitions"; +import { deserialize } from "binary-layout"; import { createPublicClient, createWalletClient, @@ -12,36 +17,24 @@ import { toHex, trim, type Hex, - type PublicClient, - type TransactionReceipt, } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { anvil } from "viem/chains"; +import { RequestForExecutionLogABI } from "../../abis/requestForExecutionLog"; +import { vaaV1ReceiveWithGasDropAbi } from "../../abis/vaaV1ReceiveWithGasDropoffAbi"; import type { ChainConfig } from "../../chains"; +import { EVM_PRIVATE_KEY } from "../../consts"; import type { RequestId } from "../../layouts/requestId"; import type { RelayRequestData, RequestForExecution, TxInfo, } from "../../types"; -import { - anvil, - type OpStackTransactionReceipt, - type ZksyncTransactionReceipt, -} from "viem/chains"; -import { RequestForExecutionLogABI } from "../../abis/requestForExecutionLog"; import { getFirstDropOffInstruction, getTotalGasLimitAndMsgValue, getTotalMsgValueFromGasInstructions, } from "../../utils"; -import { - relayInstructionsLayout, - type RelayInstructions, -} from "@wormhole-foundation/sdk-definitions"; -import { deserialize } from "binary-layout"; -import { EVM_PRIVATE_KEY } from "../../consts"; -import { privateKeyToAccount } from "viem/accounts"; -import { vaaV1ReceiveWithGasDropAbi } from "../../abis/vaaV1ReceiveWithGasDropoffAbi"; -import { mockWormhole } from "../../mockGuardian"; const REQUEST_FOR_EXECUTION_TOPIC = toEventHash( "RequestForExecution(address,uint256,uint16,bytes32,address,bytes,bytes,bytes)", @@ -157,14 +150,8 @@ export class EvmHandler { static async relayVAAv1( chainConfig: ChainConfig, relayRequest: RelayRequestData, + base64Vaa: string, ): Promise> { - if ( - !isHex(relayRequest.txHash) || - !isHex(chainConfig.coreContractAddress) - ) { - throw new Error(`TxHash not hex!`); - } - const transport = http(chainConfig.rpc); const publicClient = createPublicClient({ chain: anvil, @@ -199,12 +186,6 @@ export class EvmHandler { transport, }); - const base64Vaa = await mockWormhole( - chainConfig.rpc, - relayRequest.txHash, - chainConfig.coreContractAddress, - ); - const payloadHex = toHex(Buffer.from(base64Vaa, "base64")); const { request } = await publicClient.simulateContract({ diff --git a/src/relay/queue.ts b/src/relay/queue.ts index 6aaf17d..5ac3494 100644 --- a/src/relay/queue.ts +++ b/src/relay/queue.ts @@ -21,6 +21,7 @@ class InMemoryRelayRequestQueue { try { txInfos = await callback(relayData); } catch (e: unknown) { + console.error(e); relayData.status = RelayStatus.Failed; if (e instanceof UnsupportedRelayRequestError) { diff --git a/src/relay/relayer.ts b/src/relay/relayer.ts index a0f3594..e33602e 100644 --- a/src/relay/relayer.ts +++ b/src/relay/relayer.ts @@ -1,4 +1,6 @@ +import { isHex } from "viem"; import { enabledChains } from "../chains"; +import { mockWormhole } from "../mockGuardian"; import { RelayAbortedError, RequestPrefix, @@ -11,15 +13,23 @@ import { EvmHandler } from "./platform/evm"; export const processRelayRequests = async ( relayRequest: RelayRequestData, ): Promise> => { - const chainConfig = enabledChains[relayRequest.chainId]; + const srcChainConfig = enabledChains[relayRequest.chainId]; + const dstChainConfig = + enabledChains[relayRequest.requestForExecution.dstChain]; console.log( `Relaying ${relayRequest.id} of type ${relayRequest.instruction?.request.prefix}`, ); - if (!chainConfig) { + if (!srcChainConfig) { throw new RelayAbortedError( - `Error in chain configuration: Chain ID ${relayRequest.chainId} not configured.`, + `Error in chain configuration: Source Chain ID ${relayRequest.chainId} not configured.`, + ); + } + + if (!dstChainConfig) { + throw new RelayAbortedError( + `Error in chain configuration: Destination Chain ID ${relayRequest.chainId} not configured.`, ); } @@ -32,7 +42,7 @@ export const processRelayRequests = async ( const { request } = relayRequest.instruction; const { prefix } = request; - if (!chainConfig.capabilities.requestPrefixes.includes(prefix)) { + if (!dstChainConfig.capabilities.requestPrefixes.includes(prefix)) { throw new UnsupportedRelayRequestError( `Request type of ${relayRequest.instruction.request.prefix} not supported for Chain ID ${relayRequest.chainId}`, ); @@ -42,9 +52,21 @@ export const processRelayRequests = async ( switch (prefix) { case RequestPrefix.ERV1: + if (!isHex(relayRequest.txHash)) { + throw new Error(`TxHash not hex!`); + } + if (!isHex(srcChainConfig.coreContractAddress)) { + throw new Error(`Core contract not hex!`); + } + const base64Vaa = await mockWormhole( + srcChainConfig.rpc, + relayRequest.txHash, + srcChainConfig.coreContractAddress, + ); relayedTransactions = await EvmHandler.relayVAAv1( - chainConfig, + dstChainConfig, relayRequest, + base64Vaa, ); break; case RequestPrefix.ERC2: diff --git a/tsconfig.json b/tsconfig.json index bfa0fea..fa048c2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,5 +25,6 @@ "noUnusedLocals": false, "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false - } + }, + "exclude": ["evm"] } From 405c4b20c417522213350d111188f21c6ac1bed7 Mon Sep 17 00:00:00 2001 From: Evan Gray Date: Fri, 11 Jul 2025 10:27:06 -0400 Subject: [PATCH 2/2] tilt e2e test --- .github/workflows/tilt.yml | 2 + Dockerfile | 2 +- Dockerfile.e2e | 9 +++ Tiltfile | 20 +++++ evm/Dockerfile | 8 ++ k8s/e2e.yaml | 15 ++++ src/e2e.test.ts | 156 ++++++++++++++++++++++++++++++------- tsconfig.json | 2 +- 8 files changed, 185 insertions(+), 29 deletions(-) create mode 100644 Dockerfile.e2e create mode 100644 evm/Dockerfile create mode 100644 k8s/e2e.yaml diff --git a/.github/workflows/tilt.yml b/.github/workflows/tilt.yml index fb382f0..babaa49 100644 --- a/.github/workflows/tilt.yml +++ b/.github/workflows/tilt.yml @@ -7,6 +7,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: recursive - name: start minikube id: minikube uses: medyagh/setup-minikube@cea33675329b799adccc9526aa5daccc26cd5052 # pinned to v0.0.19 commit diff --git a/Dockerfile b/Dockerfile index 4c62713..5fce710 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,5 +17,5 @@ USER bun EXPOSE 3000/tcp ENTRYPOINT [ "bun", "start" ] -FROM release as test +FROM release AS test diff --git a/Dockerfile.e2e b/Dockerfile.e2e new file mode 100644 index 0000000..470f3d0 --- /dev/null +++ b/Dockerfile.e2e @@ -0,0 +1,9 @@ +FROM forge + +FROM executor + +WORKDIR /usr/src/app + +COPY --from=forge /app/out evm/out + +ENTRYPOINT [ "bun", "test" ] diff --git a/Tiltfile b/Tiltfile index 8aeef51..fed2166 100644 --- a/Tiltfile +++ b/Tiltfile @@ -25,6 +25,19 @@ docker_build( ] ) +docker_build( + ref = "forge", + context = "evm", + dockerfile = "./evm/Dockerfile", + only=["foundry.toml", "lib", "src"] +) +docker_build( + ref = "e2e", + context = ".", + dockerfile = "./Dockerfile.e2e", + only=[] +) + k8s_yaml("k8s/executor.yaml") k8s_resource( "executor", @@ -32,3 +45,10 @@ k8s_resource( resource_deps = ["anvil-eth-sepolia", "anvil-base-sepolia"], labels = ["app"], ) + +k8s_yaml("k8s/e2e.yaml") +k8s_resource( + "e2e", + resource_deps=["anvil-eth-sepolia", "anvil-base-sepolia", "executor"], + labels=["tests"] +) diff --git a/evm/Dockerfile b/evm/Dockerfile new file mode 100644 index 0000000..a833bf6 --- /dev/null +++ b/evm/Dockerfile @@ -0,0 +1,8 @@ +FROM ghcr.io/foundry-rs/foundry:v1.2.3@sha256:d9133dae61c19383b72695dc7eeca29d1e7a89f1f1b5fdfd8900c660b46b4303 AS forge + +WORKDIR /app +COPY foundry.toml foundry.toml +COPY lib lib +COPY src src + +RUN forge build diff --git a/k8s/e2e.yaml b/k8s/e2e.yaml new file mode 100644 index 0000000..64132ee --- /dev/null +++ b/k8s/e2e.yaml @@ -0,0 +1,15 @@ +kind: Job +apiVersion: batch/v1 +metadata: + name: e2e +spec: + backoffLimit: 0 + template: + metadata: + labels: + app: e2e + spec: + restartPolicy: Never + containers: + - name: e2e + image: e2e diff --git a/src/e2e.test.ts b/src/e2e.test.ts index ddf18bd..11d8ec2 100644 --- a/src/e2e.test.ts +++ b/src/e2e.test.ts @@ -1,23 +1,26 @@ -import { mnemonicToAccount } from "viem/accounts"; -import { ANVIL_MNEMONIC } from "./consts"; -import { test } from "bun:test"; +import { serializeLayout } from "@wormhole-foundation/sdk-base"; +import { relayInstructionsLayout } from "@wormhole-foundation/sdk-definitions"; +import axios from "axios"; +import { sleep } from "bun"; +import { expect, test } from "bun:test"; import { createPublicClient, createWalletClient, getContract, http, + isHex, padHex, toHex, + type Account, + type Chain, + type PublicClient, + type WalletClient, } from "viem"; -import { anvil, sepolia } from "viem/chains"; -import { serializeLayout } from "@wormhole-foundation/sdk-base"; -import { - quoteLayout, - relayInstructionsLayout, - signedQuoteLayout, -} from "@wormhole-foundation/sdk-definitions"; -import axios from "axios"; -import { deserialize } from "binary-layout"; +import { mnemonicToAccount } from "viem/accounts"; +import forgeOutput from "../evm/out/ExecutorVAAv1Integration.sol/ExecutorVAAv1Integration.json"; +import { enabledChains } from "./chains"; +import { ANVIL_MNEMONIC } from "./consts"; +import { RelayStatus } from "./types"; const ABI = [ { @@ -113,7 +116,90 @@ const ABI = [ }, ] as const; +async function deployIntegrationContract( + publicClient: PublicClient, + client: WalletClient, + account: Account, + viemChain: Chain, + coreContractAddress: string, + executorAddress: string, +) { + if (!isHex(forgeOutput.bytecode.object)) { + throw new Error("invalid bytecode"); + } + if (!isHex(coreContractAddress)) { + throw new Error("invalid coreContractAddress"); + } + if (!isHex(executorAddress)) { + throw new Error("invalid executorAddress"); + } + const hash = await client.deployContract({ + account, + chain: viemChain, + abi: ABI, + bytecode: forgeOutput.bytecode.object, + args: [coreContractAddress, executorAddress, 200], + }); + const receipt = await publicClient.waitForTransactionReceipt({ + hash, + }); + if (!isHex(receipt.contractAddress)) { + throw new Error("invalid contractAddress"); + } + return receipt.contractAddress; +} + test("it performs a VAA v1 relay", async () => { + const srcChain = enabledChains[10002]!; + const dstChain = enabledChains[10004]!; + const account = mnemonicToAccount(ANVIL_MNEMONIC, { addressIndex: 0 }); + if (!srcChain.viemChain || !dstChain.viemChain) { + throw new Error("invalid viem chain"); + } + const srcTransport = http(srcChain.rpc); + const srcPublicClient = createPublicClient({ + chain: srcChain.viemChain, + transport: srcTransport, + }); + const srcClient = createWalletClient({ + account, + chain: srcChain.viemChain, + transport: srcTransport, + }); + const dstTransport = http(dstChain.rpc); + const dstPublicClient = createPublicClient({ + chain: dstChain.viemChain, + transport: dstTransport, + }); + const dstClient = createWalletClient({ + account, + chain: dstChain.viemChain, + transport: dstTransport, + }); + const srcContract = await deployIntegrationContract( + srcPublicClient, + srcClient, + account, + srcChain.viemChain, + srcChain.coreContractAddress, + srcChain.executorAddress, + ); + console.log(`Deployed source contract: ${srcContract}`); + const dstContract = await deployIntegrationContract( + dstPublicClient, + dstClient, + account, + dstChain.viemChain, + dstChain.coreContractAddress, + dstChain.executorAddress, + ); + console.log(`Deployed destination contract: ${dstContract}`); + const dstTestContract = getContract({ + address: srcContract, + abi: ABI, + client: srcClient, + }); + expect(await dstTestContract.read.number()).toBe(0n); const relayInstructions = toHex( serializeLayout(relayInstructionsLayout, { requests: [ @@ -127,27 +213,20 @@ test("it performs a VAA v1 relay", async () => { ], }), ); - const response = await axios.post("http://localhost:3000/v0/quote", { + const response = await axios.post("http://executor:3000/v0/quote", { srcChain: 10002, dstChain: 10004, relayInstructions, }); - const transport = http("http://localhost:8545"); - const account = mnemonicToAccount(ANVIL_MNEMONIC, { addressIndex: 0 }); - const client = createWalletClient({ - account, - chain: sepolia, - transport, - }); - const testContract = getContract({ - address: "0x8e98Bd10a6f4c1Ee0C4b5d9F50a18D1a7E20EaF8", + const srcTestContract = getContract({ + address: srcContract, abi: ABI, - client, + client: srcClient, }); - const tx = await testContract.write.incrementAndSend( + const hash = await srcTestContract.write.incrementAndSend( [ 10004, - padHex("0x7d77360666066967579a2235332d271587cd62dC", { + padHex(dstContract, { dir: "left", size: 32, }), @@ -160,6 +239,29 @@ test("it performs a VAA v1 relay", async () => { { value: BigInt(response.data.estimatedCost) }, ); console.log( - `https://wormholelabs-xyz.github.io/executor-explorer/#/chain/10002/tx/${tx}?endpoint=http%3A%2F%2Flocalhost%3A3000&env=Testnet`, + `Request execution: https://wormholelabs-xyz.github.io/executor-explorer/#/chain/10002/tx/${hash}?endpoint=http%3A%2F%2Flocalhost%3A3000&env=Testnet`, ); -}); + await srcPublicClient.waitForTransactionReceipt({ + hash, + }); + let statusResult; + while ( + !statusResult || + statusResult.data?.[0].status === RelayStatus.Pending + ) { + console.log(`Statusing tx: ${hash}`); + if (statusResult) { + await sleep(1000); + } + statusResult = await axios.post("http://executor:3000/v0/status/tx", { + chainId: srcChain.wormholeChainId, + txHash: hash, + }); + if (statusResult.data.length !== 1) { + throw new Error(`unexpected status result length`); + } + } + expect(statusResult.data?.[0].status).toBe(RelayStatus.Submitted); + expect(await srcTestContract.read.number()).toBe(1n); + expect(await dstTestContract.read.number()).toBe(1n); +}, 60000); diff --git a/tsconfig.json b/tsconfig.json index fa048c2..4df190b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,5 +26,5 @@ "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false }, - "exclude": ["evm"] + "exclude": ["evm", "src/e2e.test.ts"] }